def test_modes_for_course_multiple(self): """ Finding the modes when there's multiple modes """ mode1 = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None) mode2 = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None) set_modes = [mode1, mode2] for mode in set_modes: self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices) modes = CourseMode.modes_for_course(self.course_key) assert modes == set_modes assert mode1 == CourseMode.mode_for_course(self.course_key, u'honor') assert mode2 == CourseMode.mode_for_course(self.course_key, u'verified') assert CourseMode.mode_for_course(self.course_key, 'DNE') is None
def move_users_for_course(self, course_key, from_mode, to_mode, commit): """ Change the enrollment mode of users for a course. Arguments: course_key (CourseKey): to lookup the course. from_mode (str): the enrollment mode to change. to_mode (str): the enrollment mode to change to. commit (bool): required to make the change to the database. Otherwise just a count will be displayed. """ unicode_course_key = str(course_key) if CourseMode.mode_for_course(course_key, to_mode) is None: logger.info( f'Mode ({to_mode}) does not exist for course ({unicode_course_key}).' ) return course_enrollments = CourseEnrollment.objects.filter( course_id=course_key, mode=from_mode) logger.info('Moving %d users from %s to %s in course %s.', course_enrollments.count(), from_mode, to_mode, unicode_course_key) if commit: # call `change_mode` which will change the mode and also emit tracking event for enrollment in course_enrollments: with transaction.atomic(): enrollment.change_mode(mode=to_mode) logger.info('Finished moving users from %s to %s in course %s.', from_mode, to_mode, unicode_course_key)
def test_nodes_for_course_single(self): """ Find the modes for a course with only one mode """ self.create_mode('verified', 'Verified Certificate', 10) modes = CourseMode.modes_for_course(self.course_key) mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None) assert [mode] == modes modes_dict = CourseMode.modes_for_course_dict(self.course_key) assert modes_dict['verified'] == mode assert CourseMode.mode_for_course(self.course_key, 'verified') == mode
def correct_modes_for_fbe(course_key=None, enrollment=None, user=None, course=None): """ If CONTENT_TYPE_GATING is enabled use the following logic to determine whether enabled_for_enrollment should be false """ if course_key is None and course is None: return True # Separate these two calls to help with cache hits (most modes_for_course callers pass in a positional course key) if course: modes = CourseMode.modes_for_course(course=course, include_expired=True, only_selectable=False) else: modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False) modes_dict = {mode.slug: mode for mode in modes} course_key = course_key or course.id # If there is no audit mode or no verified mode, FBE will not be enabled if (CourseMode.AUDIT not in modes_dict) or (CourseMode.VERIFIED not in modes_dict): return False if enrollment and user: mode_slug = enrollment.mode if enrollment.is_active: course_mode = CourseMode.mode_for_course( course_key, mode_slug, modes=modes, ) if course_mode is None: LOG.error( "User %s is in an unknown CourseMode '%s'" " for course %s. Granting full access to content for this user", user.username, mode_slug, course_key, ) return False if mode_slug != CourseMode.AUDIT: return False return True
def test_nodes_for_course_single(self): """ Find the modes for a course with only one mode """ self.create_mode('verified', 'Verified Certificate', 10) modes = CourseMode.modes_for_course(self.course_key) mode = Mode(u'verified', u'Verified Certificate', 10, '', 'usd', None, None, None, None) self.assertEqual([mode], modes) modes_dict = CourseMode.modes_for_course_dict(self.course_key) self.assertEqual(modes_dict['verified'], mode) self.assertEqual( CourseMode.mode_for_course(self.course_key, 'verified'), mode)
def _attach_course_run_upgrade_url(self, run_mode): required_mode_slug = run_mode['type'] enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_run_key) is_mode_mismatch = required_mode_slug != enrolled_mode_slug is_upgrade_required = is_mode_mismatch and CourseEnrollment.is_enrolled(self.user, self.course_run_key) if is_upgrade_required: # Requires that the ecommerce service be in use. required_mode = CourseMode.mode_for_course(self.course_run_key, required_mode_slug) ecommerce = EcommerceService() sku = getattr(required_mode, 'sku', None) if ecommerce.is_enabled(self.user) and sku: run_mode['upgrade_url'] = ecommerce.get_checkout_page_url(required_mode.sku) else: run_mode['upgrade_url'] = None else: run_mode['upgrade_url'] = None
def get_group_for_user(cls, course_key, user, user_partition, **kwargs): # pylint: disable=unused-argument """ Returns the Group from the specified user partition to which the user is assigned, via enrollment mode. If a user is in a Credit mode, the Verified or Professional mode for the course is returned instead. If a course is using the Verified Track Cohorting pilot feature, this method returns None regardless of the user's enrollment mode. """ if is_course_using_cohort_instead(course_key): return None # First, check if we have to deal with masquerading. # If the current user is masquerading as a specific student, use the # same logic as normal to return that student's group. If the current # user is masquerading as a generic student in a specific group, then # return that group. if get_course_masquerade( user, course_key) and not is_masquerading_as_specific_student( user, course_key): return get_masquerading_user_group(course_key, user, user_partition) mode_slug, is_active = CourseEnrollment.enrollment_mode_for_user( user, course_key) if mode_slug and is_active: course_mode = CourseMode.mode_for_course( course_key, mode_slug, modes=CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False), ) if course_mode and CourseMode.is_credit_mode(course_mode): # We want the verified track even if the upgrade deadline has passed, since we # are determining what content to show the user, not whether the user can enroll # in the verified track. course_mode = CourseMode.verified_mode_for_course( course_key, include_expired=True) if not course_mode: course_mode = CourseMode.DEFAULT_MODE return Group(ENROLLMENT_GROUP_IDS[course_mode.slug]["id"], str(course_mode.name)) else: return None
def add_cert(self, student, course_id, 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 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 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_key=course_id) 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 = modes_api.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 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, student, grade_contents, template_pdf, generate_pdf)
def post(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument """ Attempt to enroll the user. """ user = request.user valid, course_key, error = self._is_data_valid(request) if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) embargo_response = embargo_api.get_embargo_response( request, course_key, user) if embargo_response: return embargo_response # Don't do anything if an enrollment already exists course_id = str(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) if enrollment and enrollment.is_active: msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) return DetailResponse(msg, status=HTTP_409_CONFLICT) # Check to see if enrollment for this course is closed. course = courses.get_course(course_key) if CourseEnrollment.is_enrollment_closed(user, course): msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id) log.info('Unable to enroll user %s in closed course %s.', user.id, course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) # If there is no audit or honor course mode, this most likely # a Prof-Ed course. Return an error so that the JS redirects # to track selection. honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT) # Check to see if the User has an entitlement and enroll them if they have one for this course if CourseEntitlement.check_for_existing_entitlement_and_enroll( user=user, course_run_key=course_key): return JsonResponse( { 'redirect_destination': reverse('courseware', args=[str(course_id)]), }, ) # Accept either honor or audit as an enrollment mode to # maintain backwards compatibility with existing courses default_enrollment_mode = audit_mode or honor_mode course_name = None course_announcement = None if course is not None: course_name = course.display_name course_announcement = course.announcement if default_enrollment_mode: msg = Messages.ENROLL_DIRECTLY.format(username=user.username, course_id=course_id) if not default_enrollment_mode.sku: # If there are no course modes with SKUs, return a different message. msg = Messages.NO_SKU_ENROLLED.format( enrollment_mode=default_enrollment_mode.slug, course_id=course_id, course_name=course_name, username=user.username, announcement=course_announcement) log.info(msg) self._enroll(course_key, user, default_enrollment_mode.slug) mode = CourseMode.AUDIT if audit_mode else CourseMode.HONOR # lint-amnesty, pylint: disable=unused-variable self._handle_marketing_opt_in(request, course_key, user) return DetailResponse(msg) else: msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format( course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
def _terminate_enrollment(self, enrollment_api_client, enterprise_enrollment, course_overview): """ Helper method that switches the given enrollment to audit track, or, if no audit track exists for the given course, deletes the enrollment. Will do nothing if the user has already "completed" the course run. Args: enrollment_api_client (EnrollmentApiClient): The client with which we make requests to modify enrollments. enterprise_enrollment (EnterpriseCourseEnrollment): The enterprise enrollment which we attempt to revoke. course_overview (CourseOverview): The course overview object associated with the enrollment. Used to check for course completion. """ course_run_id = course_overview.get('id') enterprise_customer_user = enterprise_enrollment.enterprise_customer_user audit_mode = CourseMode.AUDIT enterprise_id = enterprise_customer_user.enterprise_customer.uuid log_message_kwargs = { 'user': enterprise_customer_user.username, 'enterprise': enterprise_id, 'course_id': course_run_id, 'mode': audit_mode, } if self._has_user_completed_course_run(enterprise_enrollment, course_overview): LOGGER.info( 'enrollment termination: not updating enrollment in {course_id} for User {user} ' 'in Enterprise {enterprise}, course is already complete.'. format(**log_message_kwargs)) return self.EnrollmentTerminationStatus.COURSE_COMPLETED if CourseMode.mode_for_course(course_run_id, audit_mode): try: enrollment_api_client.update_course_enrollment_mode_for_user( username=enterprise_customer_user.username, course_id=course_run_id, mode=audit_mode, ) LOGGER.info( 'Enrollment termination: updated LMS enrollment for User {user} and Enterprise {enterprise} ' 'in Course {course_id} to Course Mode {mode}.'.format( **log_message_kwargs)) return self.EnrollmentTerminationStatus.MOVED_TO_AUDIT except Exception as exc: # pylint: disable=broad-except msg = ( 'Enrollment termination: unable to update LMS enrollment for User {user} and ' 'Enterprise {enterprise} in Course {course_id} to Course Mode {mode} because: {reason}' .format(reason=str(exc), **log_message_kwargs)) LOGGER.error('{msg}: {exc}'.format(msg=msg, exc=exc)) raise EnrollmentModificationException(msg) from exc else: try: successfully_unenrolled = enrollment_api_client.unenroll_user_from_course( username=enterprise_customer_user.username, course_id=course_run_id, ) if not successfully_unenrolled: raise Exception( self.EnrollmentTerminationStatus.UNENROLL_FAILED) LOGGER.info( 'Enrollment termination: successfully unenrolled User {user}, in Enterprise {enterprise} ' 'from Course {course_id} that contains no audit mode.'. format(**log_message_kwargs)) return self.EnrollmentTerminationStatus.UNENROLLED except Exception as exc: # pylint: disable=broad-except msg = ( 'Enrollment termination: unable to unenroll User {user} in Enterprise {enterprise} ' 'from Course {course_id} because: {reason}'.format( reason=str(exc), **log_message_kwargs)) LOGGER.error('{msg}: {exc}'.format(msg=msg, exc=exc)) raise EnrollmentModificationException(msg) from exc