def test_eligible_for_cert(self, disable_honor_cert, mode_slug, expected_eligibility): """Verify that non-audit modes are eligible for a cert.""" with override_settings( FEATURES={'DISABLE_HONOR_CERTIFICATES': disable_honor_cert}): assert CourseMode.is_eligible_for_certificate( mode_slug) == expected_eligibility
def description(self): """ Returns a description for what experience changes a learner encounters when the course end date passes. Note that this currently contains 4 scenarios: 1. End date is in the future and learner is enrolled in a certificate earning mode 2. End date is in the future and learner is not enrolled at all or not enrolled in a certificate earning mode 3. End date is in the past 4. End date does not exist (and now neither does the description) """ if self.date and self.current_time <= self.date: mode, is_active = CourseEnrollment.enrollment_mode_for_user( self.user, self.course_id) if is_active and CourseMode.is_eligible_for_certificate(mode): return _( 'After this date, the course will be archived, which means you can review the ' 'course content but can no longer participate in graded assignments or work towards earning ' 'a certificate.') else: return _( 'After the course ends, the course content will be archived and no longer active.' ) elif self.date: return _( 'This course is archived, which means you can review course content but it is no longer active.' ) else: return ''
def description(self): if self.current_time <= self.date: mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id) if is_active and CourseMode.is_eligible_for_certificate(mode): return _('To earn a certificate, you must complete all requirements before this date.') else: return _('After this date, course content will be archived.') return _('This course is archived, which means you can review course content but it is no longer active.')
def test_add_cert_with_honor_certificates(self, mode): """Test certificates generations for honor and audit modes.""" template_name = 'certificate-template-{id.org}-{id.course}.pdf'.format( id=self.course.id) mock_send = self.add_cert_to_queue(mode) if CourseMode.is_eligible_for_certificate(mode): self.assert_certificate_generated(mock_send, mode, template_name) else: self.assert_ineligible_certificate_generated(mock_send, mode)
def is_eligible_for_certificate(mode_slug, status=None): """ Returns whether or not the given mode_slug is eligible for a certificate. Currently all modes other than 'audit' grant a certificate. Note that audit enrollments which existed prior to December 2015 *were* given certificates, so there will be GeneratedCertificate records with mode='audit' which are eligible. """ return _CourseMode.is_eligible_for_certificate(mode_slug, status)
def can_access_proctored_exams(self): """Returns if the user is eligible to access proctored exams""" if is_masquerading_as_non_audit_enrollment(self.effective_user, self.course_key, self.course_masquerade): # Masquerading should mimic the correct enrollment track behavior. return True else: enrollment_mode = self.enrollment['mode'] enrollment_active = self.enrollment['is_active'] return enrollment_active and CourseMode.is_eligible_for_certificate( enrollment_mode)
def _cert_info(user, enrollment, cert_status): """ Implements the logic for cert_info -- split out for testing. TODO: replace with a method that lives in the certificates app and combines this logic with lms.djangoapps.certificates.api.can_show_certificate_message and lms.djangoapps.courseware.views.get_cert_data Arguments: user (User): A user. enrollment (CourseEnrollment): A course enrollment. cert_status (dict): dictionary containing information about certificate status for the user Returns: dictionary containing: 'status': one of 'generating', 'downloadable', 'notpassing', 'restricted', 'auditing', 'processing', 'unverified', 'unavailable', or 'certificate_earned_but_not_available' 'show_survey_button': bool 'can_unenroll': if status allows for unenrollment The dictionary may also contain: 'linked_in_url': url to add cert to LinkedIn profile 'survey_url': url, only if course_overview.end_of_course_survey_url is not None 'show_cert_web_view': bool if html web certs are enabled and there is an active web cert 'cert_web_view_url': url if html web certs are enabled and there is an active web cert 'download_url': url to download a cert 'grade': if status is in 'generating', 'downloadable', 'notpassing', 'restricted', 'auditing', or 'unverified' """ # simplify the status for the template using this lookup table template_state = { CertificateStatuses.generating: 'generating', CertificateStatuses.downloadable: 'downloadable', CertificateStatuses.notpassing: 'notpassing', CertificateStatuses.restricted: 'restricted', CertificateStatuses.auditing: 'auditing', CertificateStatuses.audit_passing: 'auditing', CertificateStatuses.audit_notpassing: 'auditing', CertificateStatuses.unverified: 'unverified', } certificate_earned_but_not_available_status = 'certificate_earned_but_not_available' default_status = 'processing' default_info = { 'status': default_status, 'show_survey_button': False, 'can_unenroll': True, } if cert_status is None or enrollment is None: return default_info course_overview = enrollment.course_overview if enrollment else None status = template_state.get(cert_status['status'], default_status) is_hidden_status = status in ('processing', 'generating', 'notpassing', 'auditing') if _is_certificate_earned_but_not_available(course_overview, status): status = certificate_earned_but_not_available_status if (course_overview.certificates_display_behavior == CertificatesDisplayBehaviors.EARLY_NO_INFO and is_hidden_status): return default_info if not CourseMode.is_eligible_for_certificate(enrollment.mode, status=status): return default_info if course_overview and access.is_beta_tester(user, course_overview.id): # Beta testers are not eligible for a course certificate return default_info status_dict = { 'status': status, 'mode': cert_status.get('mode', None), 'linked_in_url': None, 'can_unenroll': status not in DISABLE_UNENROLL_CERT_STATES, } if status != default_status and course_overview.end_of_course_survey_url is not None: status_dict.update({ 'show_survey_button': True, 'survey_url': process_survey_link(course_overview.end_of_course_survey_url, user) }) else: status_dict['show_survey_button'] = False if status == 'downloadable': # showing the certificate web view button if certificate is downloadable state and feature flags are enabled. if has_html_certificates_enabled(course_overview): if course_overview.has_any_active_web_certificate: status_dict.update({ 'show_cert_web_view': True, 'cert_web_view_url': get_certificate_url(course_id=course_overview.id, uuid=cert_status['uuid']) }) elif cert_status['download_url']: status_dict['download_url'] = cert_status['download_url'] else: # don't show download certificate button if we don't have an active certificate for course status_dict['status'] = 'unavailable' elif 'download_url' not in cert_status: log.warning( "User %s has a downloadable cert for %s, but no download url", user.username, course_overview.id) return default_info else: status_dict['download_url'] = cert_status['download_url'] # If enabled, show the LinkedIn "add to profile" button # Clicking this button sends the user to LinkedIn where they # can add the certificate information to their profile. linkedin_config = LinkedInAddToProfileConfiguration.current() if linkedin_config.is_enabled(): status_dict[ 'linked_in_url'] = linkedin_config.add_to_profile_url( course_overview.display_name, cert_status.get('mode'), cert_status['download_url'], ) if status in { 'generating', 'downloadable', 'notpassing', 'restricted', 'auditing', 'unverified' }: cert_grade_percent = -1 persisted_grade_percent = -1 persisted_grade = CourseGradeFactory().read(user, course=course_overview, create_if_needed=False) if persisted_grade is not None: persisted_grade_percent = persisted_grade.percent if 'grade' in cert_status: cert_grade_percent = float(cert_status['grade']) if cert_grade_percent == -1 and persisted_grade_percent == -1: # Note: as of 11/20/2012, we know there are students in this state-- cs169.1x, # who need to be regraded (we weren't tracking 'notpassing' at first). # We can add a log.warning here once we think it shouldn't happen. return default_info grades_input = [cert_grade_percent, persisted_grade_percent] max_grade = (None if all(grade is None for grade in grades_input) else max(filter(lambda x: x is not None, grades_input))) status_dict['grade'] = str(max_grade) # If the grade is passing, the status is one of these statuses, and request certificate # is enabled for a course then we need to provide the option to the learner cert_gen_enabled = (has_self_generated_certificates_enabled( course_overview.id) or auto_certificate_generation_enabled()) passing_grade = persisted_grade and persisted_grade.passed if (status_dict['status'] != CertificateStatuses.downloadable and cert_gen_enabled and passing_grade and course_overview.has_any_active_web_certificate): status_dict['status'] = CertificateStatuses.requesting return status_dict
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. generate_pdf - Boolean should a message be sent in queue to generate certificate PDF Will change the certificate status to 'generating' or `downloadable` in case of web view certificates. The course must not be a CCX. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the newly created certificate instance """ if hasattr(course_id, 'ccx'): LOGGER.warning( ("Cannot create certificate generation task for user %s " "in the course '%s'; " "certificates are not allowed for CCX courses."), student.id, str(course_id)) return None valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing, status.downloadable, status.auditing, status.audit_passing, status.audit_notpassing, status.unverified, ] cert_status_dict = certificate_status_for_student(student, course_id) cert_status = cert_status_dict.get('status') download_url = cert_status_dict.get('download_url') cert = None if download_url: self._log_pdf_cert_generation_discontinued_warning( student.id, course_id, cert_status, download_url) return None if cert_status not in valid_statuses: LOGGER.warning( ("Cannot create certificate generation task for user %s " "in the course '%s'; " "the certificate status '%s' is not one of %s."), student.id, str(course_id), cert_status, str(valid_statuses)) return None # The caller can optionally pass a course in to avoid # re-fetching it from Mongo. If they have not provided one, # get it from the modulestore. if course is None: course = modulestore().get_course(course_id, depth=0) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed for access control in grading. self.request.user = student self.request.session = {} is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() course_grade = CourseGradeFactory().read(student, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( student, course_id) mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES user_is_verified = IDVerificationService.user_is_verified(student) cert_mode = enrollment_mode is_eligible_for_certificate = CourseMode.is_eligible_for_certificate( enrollment_mode, cert_status) if is_whitelisted and not is_eligible_for_certificate: # check if audit certificates are enabled for audit mode is_eligible_for_certificate = enrollment_mode != CourseMode.AUDIT or \ not settings.FEATURES['DISABLE_AUDIT_CERTIFICATES'] unverified = False # For credit mode generate verified certificate if cert_mode in (CourseMode.CREDIT_MODE, CourseMode.MASTERS): cert_mode = CourseMode.VERIFIED if template_file is not None: template_pdf = template_file elif mode_is_verified and user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format( id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) if CourseMode.mode_for_course(course_id, CourseMode.HONOR): cert_mode = GeneratedCertificate.MODES.honor else: unverified = True else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) LOGGER.info(( "Certificate generated for student %s in the course: %s with template: %s. " "given template: %s, " "user is verified: %s, " "mode is verified: %s," "generate_pdf is: %s"), student.username, str(course_id), template_pdf, template_file, user_is_verified, mode_is_verified, generate_pdf) cert, __ = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = course_grade.percent cert.course_id = course_id cert.name = profile_name cert.download_url = '' # Strip HTML from grade range label grade_contents = forced_grade or course_grade.letter_grade passing = False try: grade_contents = lxml.html.fromstring( grade_contents).text_content() passing = True except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info(("Could not retrieve grade for student %s " "in the course '%s' " "because an exception occurred while parsing the " "grade contents '%s' as HTML. " "The exception was: '%s'"), student.id, str(course_id), grade_contents, str(exc)) # Check if the student is whitelisted if is_whitelisted: LOGGER.info("Student %s is whitelisted in '%s'", student.id, str(course_id)) passing = True # If this user's enrollment is not eligible to receive a # certificate, mark it as such for reporting and # analytics. Only do this if the certificate is new, or # already marked as ineligible -- we don't want to mark # existing audit certs as ineligible. cutoff = settings.AUDIT_CERT_CUTOFF_DATE if (cutoff and cert.created_date >= cutoff ) and not is_eligible_for_certificate: cert.status = status.audit_passing if passing else status.audit_notpassing cert.save() LOGGER.info( "Student %s with enrollment mode %s is not eligible for a certificate.", student.id, enrollment_mode) return cert # If they are not passing, short-circuit and don't generate cert elif not passing: cert.status = status.notpassing cert.save() LOGGER.info( ("Student %s does not have a grade for '%s', " "so their certificate status has been set to '%s'. " "No certificate generation task was sent to the XQueue."), student.id, str(course_id), cert.status) return cert # Check to see whether the student is on the the embargoed # country restricted list. If so, they should not receive a # certificate -- set their status to restricted and log it. if self.restricted.filter(user=student).exists(): cert.status = status.restricted cert.save() LOGGER.info( ("Student %s is in the embargoed country restricted " "list, so their certificate status has been set to '%s' " "for the course '%s'. " "No certificate generation task was sent to the XQueue."), student.id, cert.status, str(course_id)) return cert if unverified: cert.status = status.unverified cert.save() LOGGER.info( ("User %s has a verified enrollment in course %s " "but is missing ID verification. " "Certificate status has been set to unverified"), student.id, str(course_id), ) return cert # Finally, generate the certificate and send it off. return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf)