def test_check_for_existing_entitlement_and_enroll(self, mock_get_course_uuid): course = CourseFactory() CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1)) entitlement = CourseEntitlementFactory.create( mode=CourseMode.VERIFIED, user=self.user, ) mock_get_course_uuid.return_value = entitlement.course_uuid assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=course.id, ) assert CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) entitlement.refresh_from_db() assert entitlement.enrollment_course_run
def unenroll_entitlement(sender, course_enrollment=None, skip_refund=False, **kwargs): # pylint: disable=unused-argument """ Un-enroll user from entitlement upon course run un-enrollment if exist. """ CourseEntitlement.unenroll_entitlement(course_enrollment, skip_refund)
def __init__(self, site, user, enrollments=None, uuid=None, mobile_only=False): self.site = site self.user = user self.mobile_only = mobile_only self.enrollments = enrollments or list(CourseEnrollment.enrollments_for_user(self.user)) self.enrollments.sort(key=lambda e: e.created, reverse=True) self.enrolled_run_modes = {} self.course_run_ids = [] for enrollment in self.enrollments: # enrollment.course_id is really a CourseKey (╯ಠ_ಠ)╯︵ ┻━┻ enrollment_id = str(enrollment.course_id) mode = enrollment.mode if mode == CourseMode.NO_ID_PROFESSIONAL_MODE: mode = CourseMode.PROFESSIONAL self.enrolled_run_modes[enrollment_id] = mode # We can't use dict.keys() for this because the course run ids need to be ordered self.course_run_ids.append(enrollment_id) self.entitlements = list(CourseEntitlement.unexpired_entitlements_for_user(self.user)) self.course_uuids = [str(entitlement.course_uuid) for entitlement in self.entitlements] if uuid: self.programs = [get_programs(uuid=uuid)] else: self.programs = attach_program_detail_url(get_programs(self.site), self.mobile_only)
def post(self, request, username_or_email): """Allows support staff to alter a user's enrollment.""" try: user = User.objects.get( Q(username=username_or_email) | Q(email=username_or_email)) course_id = request.data['course_id'] course_key = CourseKey.from_string(course_id) old_mode = request.data['old_mode'] new_mode = request.data['new_mode'] reason = request.data['reason'] enrollment = CourseEnrollment.objects.get(user=user, course_id=course_key) if enrollment.mode != old_mode: return HttpResponseBadRequest( 'User {username} is not enrolled with mode {old_mode}.'. format(username=user.username, old_mode=old_mode)) except KeyError as err: return HttpResponseBadRequest('The field {} is required.'.format( str(err))) except InvalidKeyError: return HttpResponseBadRequest('Could not parse course key.') except (CourseEnrollment.DoesNotExist, User.DoesNotExist): return HttpResponseBadRequest( 'Could not find enrollment for user {username} in course {course}.' .format(username=username_or_email, course=str(course_key))) try: # Wrapped in a transaction so that we can be sure the # ManualEnrollmentAudit record is always created correctly. with transaction.atomic(): update_enrollment(user.username, course_id, mode=new_mode, include_expired=True) manual_enrollment = ManualEnrollmentAudit.create_manual_enrollment_audit( request.user, enrollment.user.email, ENROLLED_TO_ENROLLED, reason=reason, enrollment=enrollment) if new_mode == CourseMode.CREDIT_MODE: provider_ids = get_credit_provider_attribute_values( course_key, 'id') credit_provider_attr = { 'namespace': 'credit', 'name': 'provider_id', 'value': provider_ids[0], } CourseEnrollmentAttribute.add_enrollment_attr( enrollment=enrollment, data_list=[credit_provider_attr]) entitlement = CourseEntitlement.get_fulfillable_entitlement_for_user_course_run( user=user, course_run_key=course_id) if entitlement is not None and entitlement.mode == new_mode: entitlement.set_enrollment( CourseEnrollment.get_enrollment(user, course_id)) return JsonResponse( ManualEnrollmentSerializer( instance=manual_enrollment).data) except CourseModeNotFoundError as err: return HttpResponseBadRequest(str(err))
def test_check_for_no_entitlement_and_do_not_enroll( self, mock_get_course_uuid): course = CourseFactory() CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1)) entitlement = CourseEntitlementFactory.create( mode=CourseMode.VERIFIED, user=self.user, ) mock_get_course_uuid.return_value = None assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=course.id, ) assert not CourseEnrollment.is_enrolled(user=self.user, course_key=course.id) entitlement.refresh_from_db() assert entitlement.enrollment_course_run is None new_course = CourseFactory() CourseModeFactory( course_id=new_course.id, # lint-amnesty, pylint: disable=no-member mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1)) # Return invalid uuid so that no entitlement returned for this new course mock_get_course_uuid.return_value = uuid4().hex try: CourseEntitlement.check_for_existing_entitlement_and_enroll( user=self.user, course_run_key=new_course.id, ) assert not CourseEnrollment.is_enrolled(user=self.user, course_key=new_course.id) except AttributeError as error: self.fail(error.message) # lint-amnesty, pylint: disable=no-member
def get_filtered_course_entitlements(user, org_whitelist, org_blacklist): """ Given a user, return a filtered set of their course entitlements. Arguments: user (User): the user in question. org_whitelist (list[str]): If not None, ONLY entitlements of these orgs will be returned. org_blacklist (list[str]): CourseEntitlements of these orgs will be excluded. Returns: generator[CourseEntitlement]: a sequence of entitlements to be displayed on the user's dashboard. """ course_entitlement_available_sessions = {} unfulfilled_entitlement_pseudo_sessions = {} course_entitlements = list( CourseEntitlement.get_active_entitlements_for_user(user)) filtered_entitlements = [] pseudo_session = None course_run_key = None for course_entitlement in course_entitlements: course_entitlement.update_expired_at() available_runs = get_visible_sessions_for_entitlement( course_entitlement) if not course_entitlement.enrollment_course_run: # Unfulfilled entitlements need a mock session for metadata pseudo_session = get_pseudo_session_for_entitlement( course_entitlement) unfulfilled_entitlement_pseudo_sessions[str( course_entitlement.uuid)] = pseudo_session # Check the org of the Course and filter out entitlements that are not available. if course_entitlement.enrollment_course_run: course_run_key = course_entitlement.enrollment_course_run.course_id elif available_runs: course_run_key = CourseKey.from_string(available_runs[0]['key']) elif pseudo_session: course_run_key = CourseKey.from_string(pseudo_session['key']) if course_run_key: # If there is no course_run_key at this point we will be unable to determine if it should be shown. # Therefore it should be excluded by default. if org_whitelist and course_run_key.org not in org_whitelist: continue elif org_blacklist and course_run_key.org in org_blacklist: continue course_entitlement_available_sessions[str( course_entitlement.uuid)] = available_runs filtered_entitlements.append(course_entitlement) return filtered_entitlements, course_entitlement_available_sessions, unfulfilled_entitlement_pseudo_sessions
def change_enrollment(request, check_access=True): """ Modify the enrollment status for the logged-in user. TODO: This is lms specific and does not belong in common code. The request parameter must be a POST request (other methods return 405) that specifies course_id and enrollment_action parameters. If course_id or enrollment_action is not specified, if course_id is not valid, if enrollment_action is something other than "enroll" or "unenroll", if enrollment_action is "enroll" and enrollment is closed for the course, or if enrollment_action is "unenroll" and the user is not enrolled in the course, a 400 error will be returned. If the user is not logged in, 403 will be returned; it is important that only this case return 403 so the front end can redirect the user to a registration or login page when this happens. This function should only be called from an AJAX request, so the error messages in the responses should never actually be user-visible. Args: request (`Request`): The Django request object Keyword Args: check_access (boolean): If True, we check that an accessible course actually exists for the given course_key before we enroll the student. The default is set to False to avoid breaking legacy code or code with non-standard flows (ex. beta tester invitations), but for any standard enrollment flow you probably want this to be True. Returns: Response """ # Get the user user = request.user # Ensure the user is authenticated if not user.is_authenticated: return HttpResponseForbidden() # Ensure we received a course_id action = request.POST.get("enrollment_action") if 'course_id' not in request.POST: return HttpResponseBadRequest(_("Course id not specified")) try: course_id = CourseKey.from_string(request.POST.get("course_id")) except InvalidKeyError: log.warning( "User %s tried to %s with invalid course id: %s", user.username, action, request.POST.get("course_id"), ) return HttpResponseBadRequest(_("Invalid course id")) # Allow us to monitor performance of this transaction on a per-course basis since we often roll-out features # on a per-course basis. monitoring_utils.set_custom_attribute('course_id', str(course_id)) if action == "enroll": # Make sure the course exists # We don't do this check on unenroll, or a bad course id can't be unenrolled from if not modulestore().has_course(course_id): log.warning( "User %s tried to enroll in non-existent course %s", user.username, course_id ) return HttpResponseBadRequest(_("Course id is invalid")) # Record the user's email opt-in preference if settings.FEATURES.get('ENABLE_MKTG_EMAIL_OPT_IN'): _update_email_opt_in(request, course_id.org) available_modes = CourseMode.modes_for_course_dict(course_id) # Check whether the user is blocked from enrolling in this course # This can occur if the user's IP is on a global blacklist # or if the user is enrolling in a country in which the course # is not available. redirect_url = embargo_api.redirect_if_blocked( course_id, user=user, ip_address=get_client_ip(request)[0], url=request.path ) if redirect_url: return HttpResponse(redirect_url) if CourseEntitlement.check_for_existing_entitlement_and_enroll(user=user, course_run_key=course_id): return HttpResponse(reverse('courseware', args=[str(course_id)])) # Check that auto enrollment is allowed for this course # (= the course is NOT behind a paywall) if CourseMode.can_auto_enroll(course_id): # Enroll the user using the default mode (audit) # We're assuming that users of the course enrollment table # will NOT try to look up the course enrollment model # by its slug. If they do, it's possible (based on the state of the database) # for no such model to exist, even though we've set the enrollment type # to "audit". try: enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes) if enroll_mode: CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) except Exception: # pylint: disable=broad-except return HttpResponseBadRequest(_("Could not enroll")) # If we have more than one course mode or professional ed is enabled, # then send the user to the choose your track page. # (In the case of no-id-professional/professional ed, this will redirect to a page that # funnels users directly into the verification / payment flow) if CourseMode.has_verified_mode(available_modes) or CourseMode.has_professional_mode(available_modes): return HttpResponse( reverse("course_modes_choose", kwargs={'course_id': str(course_id)}) ) # Otherwise, there is only one mode available (the default) return HttpResponse() elif action == "unenroll": enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: return HttpResponseBadRequest(_("You are not enrolled in this course")) certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: return HttpResponseBadRequest(_("Your certificate prevents you from unenrolling from this course")) CourseEnrollment.unenroll(user, course_id) REFUND_ORDER.send(sender=None, course_enrollment=enrollment) return HttpResponse() else: return HttpResponseBadRequest(_("Enrollment action is invalid"))
def progress(self, programs=None, count_only=True): """Gauge a user's progress towards program completion. Keyword Arguments: programs (list): Specific list of programs to check the user's progress against. If left unspecified, self.engaged_programs will be used. count_only (bool): Whether or not to return counts of completed, in progress, and unstarted courses instead of serialized representations of the courses. Returns: list of dict, each containing information about a user's progress towards completing a program. """ now = datetime.datetime.now(utc) progress = [] programs = programs or self.engaged_programs for program in programs: program_copy = deepcopy(program) completed, in_progress, not_started = [], [], [] for course in program_copy['courses']: active_entitlement = CourseEntitlement.get_entitlement_if_active( user=self.user, course_uuid=course['uuid']) if self._is_course_complete(course): completed.append(course) elif self._is_course_enrolled(course) or active_entitlement: # Show all currently enrolled courses and active entitlements as in progress if active_entitlement: course[ 'course_runs'] = get_fulfillable_course_runs_for_entitlement( active_entitlement, course['course_runs']) course[ 'user_entitlement'] = active_entitlement.to_dict() course['enroll_url'] = reverse( 'entitlements_api:v1:enrollments', args=[str(active_entitlement.uuid)]) in_progress.append(course) else: course_in_progress = self._is_course_in_progress( now, course) if course_in_progress: in_progress.append(course) else: course['expired'] = not course_in_progress not_started.append(course) else: not_started.append(course) progress.append({ 'uuid': program_copy['uuid'], 'completed': len(completed) if count_only else completed, 'in_progress': len(in_progress) if count_only else in_progress, 'not_started': len(not_started) if count_only else not_started, }) return progress
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)