def _unenroll_entitlement(course_entitlement, course_run_key): """ Internal method to handle the details of Unenrolling a User in a Course Run. """ CourseEnrollment.unenroll(course_entitlement.user, course_run_key, skip_refund=True)
def test_not_enrolled_public_course(self): """ Verify behaviour when accessing course blocks for a public course as a user not enrolled in course. """ self.query_params['username'] = '' CourseEnrollment.unenroll(self.user, self.course_key) self.verify_response(cacheable=True)
def test_unenroll_entitlement_with_audit_course_enrollment( self, mock_refund, mock_get_course_uuid): """ Test that entitlement is not refunded if un-enroll is called on audit course un-enroll. """ self.enrollment.mode = CourseMode.AUDIT self.enrollment.user = self.user self.enrollment.save() entitlement = CourseEntitlementFactory.create(user=self.user) mock_get_course_uuid.return_value = entitlement.course_uuid CourseEnrollment.unenroll(self.user, self.course.id) assert not mock_refund.called entitlement.refresh_from_db() assert entitlement.expired_at is None self.enrollment.mode = CourseMode.VERIFIED self.enrollment.is_active = True self.enrollment.save() entitlement.enrollment_course_run = self.enrollment entitlement.save() CourseEnrollment.unenroll(self.user, self.course.id) assert mock_refund.called entitlement.refresh_from_db() assert entitlement.expired_at < now()
def test_choose_mode_audit_enroll_on_post(self): audit_mode = 'audit' # Create the course modes for mode in (audit_mode, 'verified'): min_price = 0 if mode in [audit_mode] else 1 CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=min_price) # Assert learner is not enrolled in Audit track pre-POST mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) assert mode is None assert is_active is None # Choose the audit mode (POST request) choose_track_url = reverse('course_modes_choose', args=[str(self.course.id)]) self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[audit_mode]) # Assert learner is enrolled in Audit track post-POST mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) assert mode == audit_mode assert is_active # Unenroll learner from Audit track and confirm the enrollment record is now 'inactive' CourseEnrollment.unenroll(self.user, self.course.id) mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) assert mode == audit_mode assert not is_active # Choose the audit mode again self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[audit_mode]) # Assert learner is again enrolled in Audit track post-POST-POST mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) assert mode == audit_mode assert is_active
def test_get_authenticated_user_not_enrolled(self, has_previously_enrolled): if has_previously_enrolled: # Create an enrollment, then unenroll to set is_active to False CourseEnrollment.enroll(self.user, self.course.id) CourseEnrollment.unenroll(self.user, self.course.id) response = self.client.get(self.url) assert response.status_code == 401
def delete(self, request, course_key_string): course_key = CourseKey.from_string(course_key_string) email = request.data.get("email", None) validate_param_exist(email, "email") try: user = User.objects.get(email=email) except Exception: # pylint: disable=broad-except msg = { "error": "Could not find user by email address '{email}'".format( email=email) } return Response(msg, 404) auth.get_user_permissions(request.user, course_key) auth.remove_users(request.user, CourseStaffRole(course_key), user) auth.remove_users(request.user, CourseInstructorRole(course_key), user) CourseEnrollment.unenroll(user, course_key) msg = "'{email}''s permissions are revoked from '{course_key}'".format( email=email, course_key=course_key) log.info(msg) return Response( {'message': "User is removed from {}.".format(course_key)})
def test_unenrolled_from_some_courses(self): # Enroll in several courses in the org self._create_courses_and_enrollments( (self.TEST_ORG, True), (self.TEST_ORG, True), (self.TEST_ORG, True), ("org_alias", True) ) # Set a preference for the aliased course self._set_opt_in_pref(self.user, "org_alias", False) # Unenroll from the aliased course CourseEnrollment.unenroll(self.user, self.courses[3].id, skip_refund=True) # Expect that the preference still applies, # and all the enrollments should appear in the list output = self._run_command(self.TEST_ORG, other_names=["org_alias"]) self._assert_output( output, (self.user, self.courses[0].id, False), (self.user, self.courses[1].id, False), (self.user, self.courses[2].id, False), (self.user, self.courses[3].id, False) )
def test_generate_user_is_not_enrolled(self): # Unenroll the user CourseEnrollment.unenroll(self.student, self.EXISTED_COURSE_KEY_2) # Can no longer regenerate certificates for the user response = self._generate(course_key=self.EXISTED_COURSE_KEY_2, username=self.STUDENT_USERNAME) assert response.status_code == 400
def test_regenerate_user_is_not_enrolled(self): # Unenroll the user CourseEnrollment.unenroll(self.student, self.CERT_COURSE_KEY) # Can no longer regenerate certificates for the user response = self._regenerate(course_key=self.CERT_COURSE_KEY, username=self.STUDENT_USERNAME) self.assertEqual(response.status_code, 400)
def setUp(self): super(PermissionTests, self).setUp() self.user = UserFactory() self.course_id = CourseLocator('MITx', '000', 'Perm_course') CourseModeFactory(mode_slug='verified', course_id=self.course_id) CourseModeFactory(mode_slug='masters', course_id=self.course_id) CourseModeFactory(mode_slug='professional', course_id=self.course_id) CourseEnrollment.unenroll(self.user, self.course_id)
def setUp(self): super().setUp() self.user = UserFactory() self.course = CourseFactory(enable_proctored_exams=True) self.course_id = self.course.id # pylint: disable=no-member CourseModeFactory(mode_slug='verified', course_id=self.course_id) CourseModeFactory(mode_slug='masters', course_id=self.course_id) CourseModeFactory(mode_slug='professional', course_id=self.course_id) CourseEnrollment.unenroll(self.user, self.course_id)
def test_existing_inactive_enrollment(self): """ If the user has an inactive enrollment for the course, the view should behave as if the user has no enrollment. """ # Create an inactive enrollment CourseEnrollment.enroll(self.user, self.course.id) CourseEnrollment.unenroll(self.user, self.course.id, True) assert not CourseEnrollment.is_enrolled(self.user, self.course.id) assert get_enrollment(self.user.username, str(self.course.id)) is not None
def test_existing_inactive_enrollment(self): """ If the user has an inactive enrollment for the course, the view should behave as if the user has no enrollment. """ # Create an inactive enrollment CourseEnrollment.enroll(self.user, self.course.id) CourseEnrollment.unenroll(self.user, self.course.id, True) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) self.assertIsNotNone(get_enrollment(self.user.username, six.text_type(self.course.id)))
def test_cea_enrolls_only_one_user(self): """ Tests that a CourseEnrollmentAllowed can be used by just one user. If the user changes e-mail and then a second user tries to enroll with the same accepted e-mail, the second enrollment should fail. However, the original user can reuse the CEA many times. """ cea = CourseEnrollmentAllowedFactory( email='*****@*****.**', course_id=self.course.id, auto_enroll=False, ) # Still unlinked assert cea.user is None user1 = UserFactory.create(username="******", email="*****@*****.**", password="******") user2 = UserFactory.create(username="******", email="*****@*****.**", password="******") assert not CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists() user1.email = '*****@*****.**' user1.save() CourseEnrollment.enroll(user1, self.course.id, check_access=True) assert CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists() # The CEA is now linked cea.refresh_from_db() assert cea.user == user1 # user2 wants to enroll too, (ab)using the same allowed e-mail, but cannot user1.email = '*****@*****.**' user1.save() user2.email = '*****@*****.**' user2.save() with pytest.raises(EnrollmentClosedError): CourseEnrollment.enroll(user2, self.course.id, check_access=True) # CEA still linked to user1. Also after unenrolling cea.refresh_from_db() assert cea.user == user1 CourseEnrollment.unenroll(user1, self.course.id) cea.refresh_from_db() assert cea.user == user1 # Enroll user1 again. Because it's the original owner of the CEA, the enrollment is allowed CourseEnrollment.enroll(user1, self.course.id, check_access=True) # Still same cea.refresh_from_db() assert cea.user == user1
def test_unenrolled_from_all_courses(self, opt_in_pref): # Enroll in the course and set a preference self._create_courses_and_enrollments((self.TEST_ORG, True)) self._set_opt_in_pref(self.user, self.TEST_ORG, opt_in_pref) # Unenroll from the course CourseEnrollment.unenroll(self.user, self.courses[0].id, skip_refund=True) # Enrollments should still appear in the outpu output = self._run_command(self.TEST_ORG) self._assert_output(output, (self.user, self.courses[0].id, opt_in_pref))
def test_unenrollment_filter_prevent_unenroll(self): """ Test prevent the user's unenrollment through a pipeline step. Expected result: - CourseUnenrollmentStarted is triggered and executes TestUnenrollmentPipelineStep. - The user can't unenroll. """ CourseEnrollment.enroll(self.user, self.course.id, mode="no-id-professional") with self.assertRaises(UnenrollmentNotAllowed): CourseEnrollment.unenroll(self.user, self.course.id)
def test_unenrollment_without_filter_configuration(self): """ Test usual unenrollment process without filter's intervention. Expected result: - CourseUnenrollmentStarted does not have any effect on the unenrollment process. - The unenrollment process ends successfully. """ CourseEnrollment.enroll(self.user, self.course.id, mode="audit") CourseEnrollment.unenroll(self.user, self.course.id) self.assertFalse( CourseEnrollment.is_enrolled(self.user, self.course.id))
def test_schedule_is_reset_to_availability_date(self): """ Test that a switch to audit enrollment resets to the availability date, not current time. """ original_start = self.schedule.start_date # Switch to verified, confirm we change start date CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.VERIFIED) self.schedule.refresh_from_db() assert self.schedule.start_date != original_start CourseEnrollment.unenroll(self.user, self.course.id) # Switch back to audit, confirm we change back to original availability date CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.AUDIT) self.schedule.refresh_from_db() assert self.schedule.start_date == original_start
def test_unenrollment_filter_executed(self): """ Test whether the student unenrollment filter is triggered before the user's unenrollment process. Expected result: - CourseUnenrollmentStarted is triggered and executes TestUnenrollmentPipelineStep. - The user's profile has unenrolled_from in its meta field. """ CourseEnrollment.enroll(self.user, self.course.id, mode="audit") CourseEnrollment.unenroll(self.user, self.course.id) self.assertFalse( CourseEnrollment.is_enrolled(self.user, self.course.id))
def test_get_authenticated_user_not_enrolled(self, has_previously_enrolled): if has_previously_enrolled: # Create an enrollment, then unenroll to set is_active to False CourseEnrollment.enroll(self.user, self.course.id) CourseEnrollment.unenroll(self.user, self.course.id) response = self.client.get(self.url) assert response.status_code == 200 course_tools = response.data.get('course_tools') assert len(course_tools) == 0 dates_widget = response.data.get('dates_widget') assert dates_widget date_blocks = dates_widget.get('course_date_blocks') assert all((block.get('title') != '') for block in date_blocks) assert all(block.get('date') for block in date_blocks)
def test_get_course_list(self): """ Test getting courses """ course_location = self.store.make_course_key('Org1', 'Course1', 'Run1') self._create_course_with_access_groups(course_location) # get dashboard courses_list = list(get_course_enrollments(self.student, None, [])) assert len(courses_list) == 1 assert courses_list[0].course_id == course_location CourseEnrollment.unenroll(self.student, course_location) # get dashboard courses_list = list(get_course_enrollments(self.student, None, [])) assert len(courses_list) == 0
def test_enrollment_non_existent_user(self): # Testing enrollment of newly unsaved user (i.e. no database entry) user = User(username="******", email="*****@*****.**") course_id = CourseLocator("edX", "Test101", "2013") assert not CourseEnrollment.is_enrolled(user, course_id) # Unenroll does nothing CourseEnrollment.unenroll(user, course_id) self.assert_no_events_were_emitted() # Implicit save() happens on new User object when enrolling, so this # should still work CourseEnrollment.enroll(user, course_id) assert CourseEnrollment.is_enrolled(user, course_id) self.assert_enrollment_event_was_emitted(user, course_id)
def test_public_course_all_blocks_and_empty_username(self): """ Verify behaviour when specifying both all_blocks and username='', and ensure the response is not cached. """ self.query_params['username'] = '' self.query_params['all_blocks'] = True # Verify response for a regular user. self.verify_response(403, cacheable=False) # Verify response for an unenrolled user. CourseEnrollment.unenroll(self.user, self.course_key) self.verify_response(403, cacheable=False) # Verify response for an anonymous user. self.client.logout() self.verify_response(403, cacheable=False) # Verify response for a staff user. self.client.login(username=self.admin_user.username, password='******') self.verify_response(cacheable=False)
def test_unenrollment_completed_event_emitted(self): """ Test whether the student un-enrollment completed event is sent after the user's unenrollment process. Expected result: - COURSE_UNENROLLMENT_COMPLETED is sent and received by the mocked receiver. - The arguments that the receiver gets are the arguments sent by the event except the metadata generated on the fly. """ enrollment = CourseEnrollment.enroll(self.user, self.course.id) event_receiver = mock.Mock( side_effect=self._event_receiver_side_effect) COURSE_UNENROLLMENT_COMPLETED.connect(event_receiver) CourseEnrollment.unenroll(self.user, self.course.id) self.assertTrue(self.receiver_called) self.assertDictContainsSubset( { "signal": COURSE_UNENROLLMENT_COMPLETED, "sender": None, "enrollment": CourseEnrollmentData( user=UserData( pii=UserPersonalData( username=self.user.username, email=self.user.email, name=self.user.profile.name, ), id=self.user.id, is_active=self.user.is_active, ), course=CourseData( course_key=self.course.id, display_name=self.course.display_name, ), mode=enrollment.mode, is_active=False, creation_date=enrollment.created, ), }, event_receiver.call_args.kwargs)
def test_change_to_default_if_verified_not_active(self): """ Tests that one can renroll for a course if one has already unenrolled """ # enroll student CourseEnrollment.enroll(self.user, self.course.id, mode='verified') # now unenroll student: CourseEnrollment.unenroll(self.user, self.course.id) # check that they are verified but inactive enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( self.user, self.course.id) assert not is_active assert enrollment_mode == 'verified' # now enroll them through the view: response = self._enroll_through_view(self.course) assert response.status_code == 200 enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( self.user, self.course.id) assert is_active assert enrollment_mode == CourseMode.DEFAULT_MODE_SLUG
def unenroll(self, user): """ Un-enroll user from the related course. Args: user (User): User object """ enrollment = CourseEnrollment.objects.filter( user=user, course=self.course).first() if not enrollment: return certificate_info = cert_info(user, enrollment.course_overview) if certificate_info.get('status') in DISABLE_UNENROLL_CERT_STATES: exception_msg = ( f'Cannot un-enroll user: {user} from course: {enrollment.course_overview.display_name_with_default}' ) logger.exception(exception_msg) raise Exception(exception_msg) CourseEnrollment.unenroll(user, self.course)
def test_enrollment_multiple_classes(self): user = User(username="******", email="*****@*****.**") course_id1 = CourseLocator("edX", "Test101", "2013") course_id2 = CourseLocator("MITx", "6.003z", "2012") CourseEnrollment.enroll(user, course_id1) self.assert_enrollment_event_was_emitted(user, course_id1) CourseEnrollment.enroll(user, course_id2) self.assert_enrollment_event_was_emitted(user, course_id2) assert CourseEnrollment.is_enrolled(user, course_id1) assert CourseEnrollment.is_enrolled(user, course_id2) CourseEnrollment.unenroll(user, course_id1) self.assert_unenrollment_event_was_emitted(user, course_id1) assert not CourseEnrollment.is_enrolled(user, course_id1) assert CourseEnrollment.is_enrolled(user, course_id2) CourseEnrollment.unenroll(user, course_id2) self.assert_unenrollment_event_was_emitted(user, course_id2) assert not CourseEnrollment.is_enrolled(user, course_id1) assert not CourseEnrollment.is_enrolled(user, course_id2)
def handle(self, *args, **options): source_key = CourseKey.from_string(options['source_course']) dest_keys = [] for course_key in options['dest_course_list']: dest_keys.append(CourseKey.from_string(course_key)) source_students = User.objects.filter( courseenrollment__course_id=source_key) for user in source_students: with transaction.atomic(): print(f'Moving {user.username}.') # Find the old enrollment. enrollment = CourseEnrollment.objects.get(user=user, course_id=source_key) # Move the Student between the classes. mode = enrollment.mode old_is_active = enrollment.is_active CourseEnrollment.unenroll(user, source_key, skip_refund=True) print('Unenrolled {} from {}'.format(user.username, str(source_key))) for dest_key in dest_keys: if CourseEnrollment.is_enrolled(user, dest_key): # Un Enroll from source course but don't mess # with the enrollment in the destination course. msg = 'Skipping {}, already enrolled in destination course {}' print(msg.format(user.username, str(dest_key))) else: new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode) # Un-enroll from the new course if the user had un-enrolled # form the old course. if not old_is_active: new_enrollment.update_enrollment(is_active=False, skip_refund=True)
def test_enrollment(self): user = User.objects.create_user("joe", "*****@*****.**", "password") course_id = CourseKey.from_string("edX/Test101/2013") course_id_partial = CourseKey.from_string("edX/Test101/") # Test basic enrollment self.assertFalse(CourseEnrollment.is_enrolled(user, course_id)) self.assertFalse( CourseEnrollment.is_enrolled_by_partial(user, course_id_partial)) CourseEnrollment.enroll(user, course_id) self.assertTrue(CourseEnrollment.is_enrolled(user, course_id)) self.assertTrue( CourseEnrollment.is_enrolled_by_partial(user, course_id_partial)) self.assert_enrollment_event_was_emitted(user, course_id) # Enrolling them again should be harmless CourseEnrollment.enroll(user, course_id) self.assertTrue(CourseEnrollment.is_enrolled(user, course_id)) self.assertTrue( CourseEnrollment.is_enrolled_by_partial(user, course_id_partial)) self.assert_no_events_were_emitted() # Now unenroll the user CourseEnrollment.unenroll(user, course_id) self.assertFalse(CourseEnrollment.is_enrolled(user, course_id)) self.assertFalse( CourseEnrollment.is_enrolled_by_partial(user, course_id_partial)) self.assert_unenrollment_event_was_emitted(user, course_id) # Unenrolling them again should also be harmless CourseEnrollment.unenroll(user, course_id) self.assertFalse(CourseEnrollment.is_enrolled(user, course_id)) self.assertFalse( CourseEnrollment.is_enrolled_by_partial(user, course_id_partial)) self.assert_no_events_were_emitted() # The enrollment record should still exist, just be inactive enrollment_record = CourseEnrollment.objects.get(user=user, course_id=course_id) self.assertFalse(enrollment_record.is_active) # Make sure mode is updated properly if user unenrolls & re-enrolls enrollment = CourseEnrollment.enroll(user, course_id, "verified") self.assertEqual(enrollment.mode, "verified") CourseEnrollment.unenroll(user, course_id) enrollment = CourseEnrollment.enroll(user, course_id, "audit") self.assertTrue(CourseEnrollment.is_enrolled(user, course_id)) self.assertEqual(enrollment.mode, "audit")
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"))