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 test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap( external_id="*****@*****.**", external_email="", external_domain="shib:https://idp.stanford.edu/", external_credentials="", internal_password="******", user=student, ) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org="Stanford", number="123", display_name="Shib Only", enrollment_domain="shib:https://idp.stanford.edu/", user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() request_kwargs = { "path": "/shib-login/", "data": { "enrollment_action": "enroll", "course_id": course.id.to_deprecated_string(), "next": "/testredirect", }, "follow": False, "REMOTE_USER": "******", "Shib-Identity-Provider": "https://idp.stanford.edu/", } response = self.client.get(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response["location"], "http://testserver/testredirect") # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage) self.client.logout() CourseEnrollment.unenroll(student, course.id) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) response = self.client.post(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response["location"], "http://testserver/testredirect") # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
def test_refund_cert_callback_before_expiration(self): # If the expiration date has not yet passed on a verified mode, the user can be refunded many_days = datetime.timedelta(days=60) course = CourseFactory.create() self.course_key = course.id course_mode = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=(datetime.datetime.now(pytz.utc) + many_days)) course_mode.save() # need to prevent analytics errors from appearing in stderr with patch('sys.stderr', sys.stdout.write): CourseEnrollment.enroll(self.user, self.course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') cart.purchase() CourseEnrollment.unenroll(self.user, self.course_key) target_certs = CertificateItem.objects.filter(course_id=self.course_key, user_id=self.user, status='refunded', mode='verified') self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, 'refunded') self._assert_refund_tracked()
def test_refund_cert_callback_before_expiration(self): # If the expiration date has not yet passed on a verified mode, the user can be refunded many_days = datetime.timedelta(days=60) course = CourseFactory.create(org="refund_before_expiration", number="test", display_name="one") course_key = course.id course_mode = CourseMode( course_id=course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=(datetime.datetime.now(pytz.utc) + many_days), ) course_mode.save() CourseEnrollment.enroll(self.user, course_key, "verified") cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, course_key, self.cost, "verified") cart.purchase() CourseEnrollment.unenroll(self.user, course_key) target_certs = CertificateItem.objects.filter( course_id=course_key, user_id=self.user, status="refunded", mode="verified" ) self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, "refunded")
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 perform_destroy(self, instance): """ This method is an override and is called by the DELETE method """ save_model = False if instance.expired_at is None: instance.expired_at = timezone.now() log.info('Set expired_at to [%s] for course entitlement [%s]', instance.expired_at, instance.uuid) save_model = True if instance.enrollment_course_run is not None: CourseEnrollment.unenroll( user=instance.user, course_id=instance.enrollment_course_run.course_id, skip_refund=True ) enrollment = instance.enrollment_course_run instance.enrollment_course_run = None save_model = True log.info( 'Unenrolled user [%s] from course run [%s] as part of revocation of course entitlement [%s]', instance.user.username, enrollment.course_id, instance.uuid ) if save_model: instance.save()
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) self.assertIsNone(mode) self.assertIsNone(is_active) # Choose the audit mode (POST request) choose_track_url = reverse('course_modes_choose', args=[six.text_type(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) self.assertEqual(mode, audit_mode) self.assertTrue(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) self.assertEqual(mode, audit_mode) self.assertFalse(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) self.assertEqual(mode, audit_mode) self.assertTrue(is_active)
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 handle(self, *args, **options): csv_path = options['csv_path'] with open(csv_path) as csvfile: reader = unicodecsv.DictReader(csvfile) for row in reader: username = row['username'] email = row['email'] course_key = row['course_id'] try: user = User.objects.get(Q(username=username) | Q(email=email)) except ObjectDoesNotExist: user = None msg = 'User with username {} or email {} does not exist'.format(username, email) logger.warning(msg) try: course_id = CourseKey.from_string(course_key) except InvalidKeyError: course_id = None msg = 'Invalid course id {course_id}, skipping un-enrollement for {username}, {email}'.format(**row) logger.warning(msg) if user and course_id: enrollment = CourseEnrollment.get_enrollment(user, course_id) if not enrollment: msg = 'Enrollment for the user {} in course {} does not exist!'.format(username, course_key) logger.info(msg) else: try: CourseEnrollment.unenroll(user, course_id, skip_refund=True) except Exception as err: msg = 'Error un-enrolling User {} from course {}: '.format(username, course_key, err) logger.error(msg, exc_info=True)
def test_certificate_exception_user_not_enrolled_error(self): """ Test certificates exception addition api endpoint returns failure when called with username/email that is not enrolled in the given course. """ # Un-enroll student from the course CourseEnrollment.unenroll(self.user, self.course.id) response = self.client.post( self.url, data=json.dumps(self.certificate_exception), content_type='application/json' ) # Assert 400 status code in response self.assertEqual(response.status_code, 400) res_json = json.loads(response.content) # Assert Request not successful self.assertFalse(res_json['success']) # Assert Error Message self.assertEqual( res_json['message'], "{user} is not enrolled in this course. Please check your spelling and retry.".format( user=self.certificate_exception['user_name'] ) )
def handle(self, *args, **options): source = options["source_course"] dest = options["dest_course"] source_students = User.objects.filter(courseenrollment__course_id=source) for user in source_students: print("Moving {}.".format(user.username)) # Find the old enrollment. enrollment = CourseEnrollment.objects.get(user=user, course_id=source) # Move the Student between the classes. mode = enrollment.mode old_is_active = enrollment.is_active CourseEnrollment.unenroll(user, source) new_enrollment = CourseEnrollment.enroll(user, dest, mode=mode) # Unenroll from the new coures if the user had unenrolled # form the old course. if not old_is_active: new_enrollment.update_enrollment(is_active=False) if mode == "verified": try: certificate_item = CertificateItem.objects.get(course_id=source, course_enrollment=enrollment) except CertificateItem.DoesNotExist: print("No certificate for {}".format(user)) continue certificate_item.course_id = dest certificate_item.course_enrollment = new_enrollment certificate_item.save()
def test_enrollment(self): user = User.objects.create_user("joe", "*****@*****.**", "password") course_id = "edX/Test101/2013" course_id_partial = "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)) # 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)) # 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)) # 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)) # 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)
def test_refund_cert_callback_before_expiration_email(self): """ Test that refund emails are being sent correctly. """ course = CourseFactory.create() course_key = course.id many_days = datetime.timedelta(days=60) course_mode = CourseMode(course_id=course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost, expiration_datetime=datetime.datetime.now(pytz.utc) + many_days) course_mode.save() CourseEnrollment.enroll(self.user, course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, course_key, self.cost, 'verified') cart.purchase() mail.outbox = [] with patch('shoppingcart.models.log.error') as mock_error_logger: CourseEnrollment.unenroll(self.user, course_key) self.assertFalse(mock_error_logger.called) self.assertEquals(len(mail.outbox), 1) self.assertEquals('[Refund] User-Requested Refund', mail.outbox[0].subject) self.assertEquals(settings.PAYMENT_SUPPORT_EMAIL, mail.outbox[0].from_email) self.assertIn('has requested a refund on Order', mail.outbox[0].body)
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 self.assertIsNone(cea.user) user1 = UserFactory.create(username="******", email="*****@*****.**", password="******") user2 = UserFactory.create(username="******", email="*****@*****.**", password="******") self.assertFalse( CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists() ) user1.email = '*****@*****.**' user1.save() CourseEnrollment.enroll(user1, self.course.id, check_access=True) self.assertTrue( CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists() ) # The CEA is now linked cea.refresh_from_db() self.assertEqual(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 self.assertRaises(EnrollmentClosedError): CourseEnrollment.enroll(user2, self.course.id, check_access=True) # CEA still linked to user1. Also after unenrolling cea.refresh_from_db() self.assertEqual(cea.user, user1) CourseEnrollment.unenroll(user1, self.course.id) cea.refresh_from_db() self.assertEqual(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() self.assertEqual(cea.user, user1)
def _course_unenroll(self,username,course_id): try: user = User.objects.get(username=username) course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) CourseEnrollment.unenroll(user, course_key) return True except: return False
def test_enrollment_limit_by_domain(self): """ Tests that the enrollmentDomain setting is properly limiting enrollment to those who have the proper external auth """ # create 2 course, one with limited enrollment one without shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/' self.store.update_item(shib_course, '**replace_user**') open_enroll_course = CourseFactory.create(org='MITx', number='999', display_name='Robot Super Course') open_enroll_course.enrollment_domain = '' self.store.update_item(open_enroll_course, '**replace_user**') # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth shib_student = UserFactory.create() shib_student.save() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=shib_student) extauth.save() other_ext_student = UserFactory.create() other_ext_student.username = "******" other_ext_student.email = "*****@*****.**" other_ext_student.save() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://other.edu/', external_credentials="", user=other_ext_student) extauth.save() int_student = UserFactory.create() int_student.username = "******" int_student.email = "*****@*****.**" int_student.save() # Tests the two case for courses, limited and not for course in [shib_course, open_enroll_course]: for student in [shib_student, other_ext_student, int_student]: request = self.request_factory.post('/change_enrollment') request.POST.update({'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string()}) request.user = student response = change_enrollment(request) # If course is not limited or student has correct shib extauth then enrollment should be allowed if course is open_enroll_course or student is shib_student: self.assertEqual(response.status_code, 200) self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up CourseEnrollment.unenroll(student, course.id) else: self.assertEqual(response.status_code, 400) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id))
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 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, unicode(self.course.id)))
def test_refund_cert_callback_no_expiration(self): # When there is no expiration date on a verified mode, the user can always get a refund CourseEnrollment.enroll(self.user, self.course_id, 'verified') cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_id, self.cost, 'verified') cart.purchase() CourseEnrollment.unenroll(self.user, self.course_id) target_certs = CertificateItem.objects.filter(course_id=self.course_id, user_id=self.user, status='refunded', mode='verified') self.assertTrue(target_certs[0])
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 cm_unenroll_user(request): """ This is a hard unenroll as opposed to the regular soft unenroll that edx uses internally. """ response_format = request.REQUEST.get('format','html') if response_format == 'json' or 'application/json' in request.META.get('HTTP/ACCEPT', 'application/json'): if request.method == 'POST': if validate_token(request.body, request) is False: log.warn("Unauthorized access made by course: %s, user: %s", request.json.get('email'), request.json.get('course_id')) return HttpResponse('Unauthorized', status=401) if 'email' not in request.json or 'course_id' not in request.json: log.error("Incomplete request.") return HttpResponse(content=json.dumps({'errors':'Missing params'}), \ content_type = 'application/json', status=400) try: log.info("Unenrolling user: %s from course: %s" % (request.json.get('email'), request.json.get('course_id'))) user = User.objects.get(email=request.json.get('email')) request.user = user except User.DoesNotExist: log.info("Unknown user : %s from course %s", request.json.get('email'), request.json.get('course_id')) return HttpResponse(content=json.dumps({'errors': 'Unknown user'}), status=500, content_type='application/json') course_id = request.json.get('course_id') #locator = loc_mapper().translate_location(course_id, CourseDescriptor.id_to_location(course_id)) role = request.json.get('role') if role == 'staff': try: try_remove_instructor(request, get_key_from_course_id(course_id), user) log.info("staff unenrolled: %s", str(user)) except CannotOrphanCourse as oops: log.warn("last course admin removal attempted: %s", str(user)) return JsonResponse(oops.msg, 400) else: log.info("student unenroll: %s", str(user)) try: CourseEnrollment.unenroll(user, get_key_from_course_id(request.json.get('course_id'))) status_code = 200 except DatabaseError: # this is just for tests. This error will never be hit in production # should probably consider moving cm_plugin out of common too. log.error("Unenroll failed for user: %s from course: %s", request.json.get('email'), request.json.get('course_id')) pass status_code = 200 content = {'success':'ok'} log.info("Unenrolled user: %s from course: %s", request.json.get('email'), request.json.get('course_id')) return HttpResponse(content=json.dumps(content), status=status_code, content_type='application/json') else: return HttpResponse(content=json.dumps({}), status=404, content_type='application/json') else: return HttpResponse(content=json.dumps({}), status=404, content_type='application/json')
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() request_kwargs = {'path': '/shib-login/', 'data': {'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string(), 'next': '/testredirect'}, 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/'} response = self.client.get(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage) self.client.logout() CourseEnrollment.unenroll(student, course.id) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) response = self.client.post(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, 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) self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) self.assertIsNotNone(get_enrollment(self.user.username, unicode(self.course.id))) with mock_create_basket(): self._test_successful_ecommerce_api_call(False)
def test_refund_cert_callback_no_expiration(self): # When there is no expiration date on a verified mode, the user can always get a refund CourseEnrollment.enroll(self.user, self.course_key, "verified") cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, "verified") cart.purchase() CourseEnrollment.unenroll(self.user, self.course_key) target_certs = CertificateItem.objects.filter( course_id=self.course_key, user_id=self.user, status="refunded", mode="verified" ) self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, "refunded")
def test_enrollment_non_existent_user(self): # Testing enrollment of newly unsaved user (i.e. no database entry) user = User(username="******", email="*****@*****.**") course_id = "edX/Test101/2013" self.assertFalse(CourseEnrollment.is_enrolled(user, course_id)) # Unenroll does nothing CourseEnrollment.unenroll(user, course_id) # Implicit save() happens on new User object when enrolling, so this # should still work CourseEnrollment.enroll(user, course_id) self.assertTrue(CourseEnrollment.is_enrolled(user, course_id))
def handle(self, *args, **options): source_key = SlashSeparatedCourseKey.from_deprecated_string(options['source_course']) dest_key = SlashSeparatedCourseKey.from_deprecated_string(options['dest_course']) source_students = User.objects.filter( courseenrollment__course_id=source_key ) for user in source_students: if CourseEnrollment.is_enrolled(user, dest_key): # Un Enroll from source course but don't mess # with the enrollment in the destination course. CourseEnrollment.unenroll(user, source_key) print("Unenrolled {} from {}".format(user.username, source_key.to_deprecated_string())) msg = "Skipping {}, already enrolled in destination course {}" print(msg.format(user.username, dest_key.to_deprecated_string())) continue print("Moving {}.".format(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) new_enrollment = CourseEnrollment.enroll(user, dest_key, mode=mode) # Unenroll from the new coures if the user had unenrolled # form the old course. if not old_is_active: new_enrollment.update_enrollment(is_active=False) if mode == 'verified': try: certificate_item = CertificateItem.objects.get( course_id=source_key, course_enrollment=enrollment ) except CertificateItem.DoesNotExist: print("No certificate for {}".format(user)) continue certificate_item.course_id = dest_key certificate_item.course_enrollment = new_enrollment certificate_item.save()
def handle(self, *args, **options): source_key = CourseKey.from_string(options.get('source_course', '')) dest_keys = [] for course_key in options.get('dest_course_list', '').split(','): dest_keys.append(CourseKey.from_string(course_key)) if not source_key or not dest_keys: raise TransferStudentError(u"Must have a source course and destination course specified.") tc_option = options.get('transfer_certificates', '') transfer_certificates = ('true' == tc_option.lower()) if tc_option else False if transfer_certificates and len(dest_keys) != 1: raise TransferStudentError(u"Cannot transfer certificate items from one course to many.") source_students = User.objects.filter( courseenrollment__course_id=source_key ) for user in source_students: with transaction.atomic(): print "Moving {}.".format(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 u"Unenrolled {} from {}".format(user.username, unicode(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 = u"Skipping {}, already enrolled in destination course {}" print msg.format(user.username, unicode(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) if transfer_certificates: self._transfer_certificate_item(source_key, enrollment, user, dest_keys, new_enrollment)
def test_refund_cert_callback_no_expiration(self): # When there is no expiration date on a verified mode, the user can always get a refund # need to prevent analytics errors from appearing in stderr with patch('sys.stderr', sys.stdout.write): CourseEnrollment.enroll(self.user, self.course_key, 'verified') cart = Order.get_cart_for_user(user=self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') cart.purchase() CourseEnrollment.unenroll(self.user, self.course_key) target_certs = CertificateItem.objects.filter(course_id=self.course_key, user_id=self.user, status='refunded', mode='verified') self.assertTrue(target_certs[0]) self.assertTrue(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, 'refunded') self._assert_refund_tracked()
def test_enrollment_non_existent_user(self): # Testing enrollment of newly unsaved user (i.e. no database entry) user = User(username="******", email="*****@*****.**") course_id = SlashSeparatedCourseKey("edX", "Test101", "2013") self.assertFalse(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) self.assertTrue(CourseEnrollment.is_enrolled(user, course_id)) self.assert_enrollment_event_was_emitted(user, course_id)
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_enrollment_pairs(self.student, None, [])) self.assertEqual(len(courses_list), 1) self.assertEqual(courses_list[0][0].id, course_location) CourseEnrollment.unenroll(self.student, course_location) # get dashboard courses_list = list(get_course_enrollment_pairs(self.student, None, [])) self.assertEqual(len(courses_list), 0)
def test_unenrolled_student(self): CourseEnrollment.unenroll(self.student, self.course.id) self.assert_raises_permission_denied()
def test_unenrolled_staff(self): CourseEnrollment.unenroll(self.staff, self.course.id) self.initial = {'requesting_user': self.staff} self.form_data['username'] = self.staff.username self.get_form(expected_valid=True)
def test_unenrolled_student_by_staff(self): CourseEnrollment.unenroll(self.student, self.course.id) self.initial = {'requesting_user': self.staff} self.get_form(expected_valid=True)
def test_course_info_unenrolled(self): self._set_up_course(False, False, False) course_id = self.courses[0].id CourseEnrollment.unenroll(self.user, course_id) result = self._get_detail() self.assertNotIn(six.text_type(course_id), result["course_info"])
def populate_user(user, authentication_response): attr = authentication_response.find(CAS + 'authenticationSuccess/' + CAS + 'attributes', namespaces=NSMAP) if attr is not None: staff_flag = attr.find(CAS + 'is_staff', NSMAP) if staff_flag is not None: user.is_staff = (staff_flag.text or '').upper() == 'TRUE' superuser_flag = attr.find(CAS + 'is_superuser', NSMAP) if superuser_flag is not None: user.is_superuser = (superuser_flag.text or '').upper() == 'TRUE' active_flag = attr.find(CAS + 'is_active', NSMAP) if active_flag is not None: user.is_active = (active_flag.text or '').upper() == 'TRUE' # Limiting by maximum lengths. # Max length of firstname/lastname is 30. # Max length of a email is 75. first_name = attr.find(CAS + 'givenName', NSMAP) if first_name is not None: user.first_name = (first_name.text or '')[0:30] last_name = attr.find(CAS + 'sn', NSMAP) if last_name is not None: user.last_name = (last_name.text or '')[0:30] email = attr.find(CAS + 'email', NSMAP) if email is not None: user.email = (email.text or '')[0:75] # Here we handle things that go into UserProfile instead. # This is a dirty hack and you shouldn't do that. # However, I don't think it's going to work when imported outside of the function body. from student.models import UserProfile # Make the user's password unusable. But only if they don't have an unusable password already, # to prevent SessionAuthenticationMiddleware from logging them out because their password changed. if user.has_usable_password(): user.set_unusable_password() user.save() # If the user doesn't yet have a profile, it means it's a new one and we need to create it a profile. # but we need to save the user first. user_profile, created = UserProfile.objects.get_or_create( user=user, defaults={'name': user.username}) # There should be more variables, but let's settle on the actual model first. full_name = attr.find(CAS + 'fullName', NSMAP) if full_name is not None: user_profile.name = full_name.text or '' user_profile.save() # Now the really fun bit. Signing the user up for courses given. coursetag = attr.find(CAS + 'courses', NSMAP) from student.models import CourseEnrollment from opaque_keys.edx.locator import CourseLocator from opaque_keys import InvalidKeyError from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError if coursetag is not None: try: courses = json.loads(coursetag.text) assert isinstance(courses, list) except (ValueError, AssertionError): # We failed to parse the tag and get a list, so we leave. log.error("Course list failed to parse.") return # We got a list. Compare it to existing enrollments. existing_enrollments = CourseEnrollment.objects.filter( user=user, is_active=True).values_list('course_id', flat=True) for course in courses: if course and not course in existing_enrollments: try: locator = CourseLocator.from_string(course) except (InvalidKeyError, AttributeError) as e: log.error( "Invalid course identifier {}".format(course)) continue try: course = modulestore().get_course(locator) except ItemNotFoundError: log.error("Course {} does not exist.".format(course)) continue CourseEnrollment.enroll(user, locator) # Now we need to unsub the user from courses for which they are not enrolled. for course in existing_enrollments: if not course in courses: try: locator = CourseLocator.from_string(course) except (InvalidKeyError, AttributeError) as e: log.error( "Invalid course identifier {} in existing enrollments." .format(course)) continue CourseEnrollment.unenroll(user, locator) # Now implement CourseEnrollmentAllowed objects, because otherwise they will only ever fire when # users click a link in the registration email -- which can never happen here. # Considering the new setup, I doubt this will ever be useful. if created: from student.models import CourseEnrollmentAllowed for cea in CourseEnrollmentAllowed.objects.filter( email=user.email, auto_enroll=True): CourseEnrollment.enroll(user, cea.course_id) # Now, deal with course administration packets. course_admin_tag = attr.find(CAS + 'course_administration_update', NSMAP) if course_admin_tag is not None: try: courses = json.loads(course_admin_tag.text) assert isinstance(courses, dict) except (ValueError, AssertionError): # We failed to parse the tag, so we leave. log.error( "Could not parse course administration block: <<{}>>". format(course_admin_tag.text)) return from instructor.access import list_with_level, allow_access, revoke_access from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA from django.contrib.auth.models import User for course_id, admin_block in courses.iteritems(): try: locator = CourseLocator.from_string(course_id) except (InvalidKeyError, AttributeError) as e: log.error("Invalid course identifier {}".format(course_id)) continue try: course = modulestore().get_course(locator) except ItemNotFoundError: log.error("Course {} does not exist.".format(course_id)) continue if not course: continue # Course roles are relatively easy. for block_name, role in [('admin', 'instructor'), ('staff', 'staff'), ('beta', 'beta')]: role_list = admin_block.get(block_name, []) existing = list_with_level(course, role) for username in role_list: try: user = User.objects.get(username=username) except User.DoesNotExist: continue if not user in existing: allow_access(course, user, role) try: CourseEnrollment.enroll(user, locator) except: pass for user in existing: if not user.username in role_list: revoke_access(course, user, role) # Forum roles, considerably different. for block_name, rolename in [ ('forum_admin', FORUM_ROLE_ADMINISTRATOR), ('forum_moderator', FORUM_ROLE_MODERATOR), ('forum_assistant', FORUM_ROLE_COMMUNITY_TA) ]: role_list = admin_block.get(block_name, []) try: role = Role.objects.get(course_id=locator, name=rolename) except Role.DoesNotExist: continue existing = role.users.all() for user in existing: if not user.username in role_list: role.users.remove(user) for username in role_list: try: user = User.objects.get(username=username) except User.DoesNotExist: continue if not user in existing: role.users.add(user) try: CourseEnrollment.enroll(user, locator) except: pass pass
def test_not_enrolled(self): CourseEnrollment.unenroll(self.user, self.course_key) self.verify_response(403)
def _do_unenroll_students(course_key, students, email_students=False): """ Do the actual work of un-enrolling multiple students, presented as a string of emails separated by commas or returns `course_key` is id of course (a `str`) `students` is string of student emails separated by commas or returns (a `str`) `email_students` is user input preference (a `boolean`) """ old_students, __ = get_and_clean_student_list(students) status = dict([x, 'unprocessed'] for x in old_students) stripped_site_name = microsite.get_value( 'SITE_NAME', settings.SITE_NAME ) if email_students: course = modulestore().get_course(course_key) # Composition of email data = { 'site_name': stripped_site_name, 'course': course } for student in old_students: isok = False cea = CourseEnrollmentAllowed.objects.filter(course_id=course_key, email=student) # Will be 0 or 1 records as there is a unique key on email + course_id if cea: cea[0].delete() status[student] = "un-enrolled" isok = True try: user = User.objects.get(email=student) except User.DoesNotExist: if isok and email_students: # User was allowed to join but had not signed up yet data['email_address'] = student data['message'] = 'allowed_unenroll' send_mail_ret = send_mail_to_student(student, data) status[student] += (', email sent' if send_mail_ret else '') continue # Will be 0 or 1 records as there is a unique key on user + course_id if CourseEnrollment.is_enrolled(user, course_key): try: CourseEnrollment.unenroll(user, course_key) status[student] = "un-enrolled" if email_students: # User was enrolled data['email_address'] = student data['full_name'] = user.profile.name data['message'] = 'enrolled_unenroll' send_mail_ret = send_mail_to_student(student, data) status[student] += (', email sent' if send_mail_ret else '') except Exception: # pylint: disable=broad-except if not isok: status[student] = "Error! Failed to un-enroll" datatable = {'header': ['StudentEmail', 'action']} datatable['data'] = [[x, status[x]] for x in sorted(status)] datatable['title'] = _('Un-enrollment of students') return dict(datatable=datatable)
def setUp(self): super(ReportTypeTests, self).setUp() # Need to make a *lot* of users for this one self.first_verified_user = UserFactory.create(profile__name="John Doe") self.second_verified_user = UserFactory.create(profile__name="Jane Deer") self.first_audit_user = UserFactory.create(profile__name="Joe Miller") self.second_audit_user = UserFactory.create(profile__name="Simon Blackquill") self.third_audit_user = UserFactory.create(profile__name="Super Mario") self.honor_user = UserFactory.create(profile__name="Princess Peach") self.first_refund_user = UserFactory.create(profile__name="King Bowsér") self.second_refund_user = UserFactory.create(profile__name="Súsan Smith") # Two are verified, three are audit, one honor self.cost = 40 self.course = CourseFactory.create(org='MITx', number='999', display_name=u'Robot Super Course') self.course_key = self.course.id settings.COURSE_LISTINGS['default'] = [self.course_key.to_deprecated_string()] course_mode = CourseMode(course_id=self.course_key, mode_slug="honor", mode_display_name="honor cert", min_price=self.cost) course_mode.save() course_mode2 = CourseMode(course_id=self.course_key, mode_slug="verified", mode_display_name="verified cert", min_price=self.cost) course_mode2.save() # User 1 & 2 will be verified self.cart1 = Order.get_cart_for_user(self.first_verified_user) CertificateItem.add_to_order(self.cart1, self.course_key, self.cost, 'verified') self.cart1.purchase() self.cart2 = Order.get_cart_for_user(self.second_verified_user) CertificateItem.add_to_order(self.cart2, self.course_key, self.cost, 'verified') self.cart2.purchase() # Users 3, 4, and 5 are audit CourseEnrollment.enroll(self.first_audit_user, self.course_key, "audit") CourseEnrollment.enroll(self.second_audit_user, self.course_key, "audit") CourseEnrollment.enroll(self.third_audit_user, self.course_key, "audit") # User 6 is honor CourseEnrollment.enroll(self.honor_user, self.course_key, "honor") self.now = datetime.datetime.now(pytz.UTC) # Users 7 & 8 are refunds self.cart = Order.get_cart_for_user(self.first_refund_user) CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') self.cart.purchase() CourseEnrollment.unenroll(self.first_refund_user, self.course_key) self.cart = Order.get_cart_for_user(self.second_refund_user) CertificateItem.add_to_order(self.cart, self.course_key, self.cost, 'verified') self.cart.purchase(self.second_refund_user, self.course_key) CourseEnrollment.unenroll(self.second_refund_user, self.course_key) self.test_time = datetime.datetime.now(pytz.UTC) first_refund = CertificateItem.objects.get(id=3) first_refund.fulfilled_time = self.test_time first_refund.refund_requested_time = self.test_time first_refund.save() second_refund = CertificateItem.objects.get(id=4) second_refund.fulfilled_time = self.test_time second_refund.refund_requested_time = self.test_time second_refund.save() self.CORRECT_REFUND_REPORT_CSV = dedent(""" Order Number,Customer Name,Date of Original Transaction,Date of Refund,Amount of Refund,Service Fees (if any) 3,King Bowsér,{time_str},{time_str},40,0 4,Súsan Smith,{time_str},{time_str},40,0 """.format(time_str=str(self.test_time))) self.CORRECT_CERT_STATUS_CSV = dedent(""" University,Course,Course Announce Date,Course Start Date,Course Registration Close Date,Course Registration Period,Total Enrolled,Audit Enrollment,Honor Code Enrollment,Verified Enrollment,Gross Revenue,Gross Revenue over the Minimum,Number of Verified Students Contributing More than the Minimum,Number of Refunds,Dollars Refunded MITx,999 Robot Super Course,,,,,6,3,1,2,80.00,0.00,0,2,80.00 """.format(time_str=str(self.test_time))) self.CORRECT_UNI_REVENUE_SHARE_CSV = dedent(""" University,Course,Number of Transactions,Total Payments Collected,Service Fees (if any),Number of Successful Refunds,Total Amount of Refunds MITx,999 Robot Super Course,6,80.00,0.00,2,80.00 """.format(time_str=str(self.test_time)))
def tearDown(self): CourseEnrollment.unenroll(self.user, self.course.id) super(CourseExpirationTestCase, self).tearDown()
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( u"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_metric('course_id', text_type(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(u"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_ip(request), 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=[unicode(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': text_type(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 unenroll(self, course_id=None): """Unenroll test user in test course.""" CourseEnrollment.unenroll(self.user, course_id or self.course.id)
def test_enrollment_limit_by_domain(self): """ Tests that the enrollmentDomain setting is properly limiting enrollment to those who have the proper external auth """ # create 2 course, one with limited enrollment one without shib_course = CourseFactory.create(org='Stanford', number='123', display_name='Shib Only') shib_course.enrollment_domain = 'shib:https://idp.stanford.edu/' self.store.update_item(shib_course, '**replace_user**') open_enroll_course = CourseFactory.create( org='MITx', number='999', display_name='Robot Super Course') open_enroll_course.enrollment_domain = '' self.store.update_item(open_enroll_course, '**replace_user**') # create 3 kinds of students, external_auth matching shib_course, external_auth not matching, no external auth shib_student = UserFactory.create() shib_student.save() extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", user=shib_student) extauth.save() other_ext_student = UserFactory.create() other_ext_student.username = "******" other_ext_student.email = "*****@*****.**" other_ext_student.save() extauth = ExternalAuthMap(external_id='*****@*****.**', external_email='', external_domain='shib:https://other.edu/', external_credentials="", user=other_ext_student) extauth.save() int_student = UserFactory.create() int_student.username = "******" int_student.email = "*****@*****.**" int_student.save() # Tests the two case for courses, limited and not for course in [shib_course, open_enroll_course]: for student in [shib_student, other_ext_student, int_student]: request = self.request_factory.post('/change_enrollment') request.POST.update({ 'enrollment_action': 'enroll', 'course_id': course.id }) request.user = student response = change_enrollment(request) # If course is not limited or student has correct shib extauth then enrollment should be allowed if course is open_enroll_course or student is shib_student: self.assertEqual(response.status_code, 200) self.assertTrue( CourseEnrollment.is_enrolled(student, course.id)) # Clean up CourseEnrollment.unenroll(student, course.id) else: self.assertEqual(response.status_code, 400) self.assertFalse( CourseEnrollment.is_enrolled(student, course.id))
def test_refund_cert_no_cert_exists(self): # If there is no paid certificate, the refund callback should return nothing CourseEnrollment.enroll(self.user, self.course_id, 'verified') ret_val = CourseEnrollment.unenroll(self.user, self.course_id) self.assertFalse(ret_val)
def mobile_change_enrollment(request): """ Modify the enrollment status for the logged-in user. 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 or as a post-login/registration helper, so the error messages in the responses should never actually be user-visible. """ user = request.user action = request.POST.get("enrollment_action") course_id = request.POST.get("course_id") if course_id is None: return HttpResponseBadRequest(_("Course id not specified")) if not user.is_authenticated(): return HttpResponseForbidden() 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 try: course = course_from_id(course_id) except ItemNotFoundError: log.warning("User {0} tried to enroll in non-existent course {1}" .format(user.username, course_id)) return HttpResponseBadRequest(_("Course id is invalid")) if not has_access(user, course, 'enroll'): return HttpResponseBadRequest(_("Enrollment is closed")) # see if we have already filled up all allowed enrollments is_course_full = CourseEnrollment.is_course_full(course) if is_course_full: return HttpResponseBadRequest(_("Course is full")) # If this course is available in multiple modes, redirect them to a page # where they can choose which mode they want. available_modes = CourseMode.modes_for_course(course_id) if len(available_modes) > 1: return HttpResponse( reverse("course_modes_choose", kwargs={'course_id': course_id}) ) current_mode = available_modes[0] course_id_dict = Location.parse_course_id(course_id) dog_stats_api.increment( "common.student.enrollment", tags=[u"org:{org}".format(**course_id_dict), u"course:{course}".format(**course_id_dict), u"run:{name}".format(**course_id_dict)] ) CourseEnrollment.enroll(user, course.id, mode=current_mode.slug) return HttpResponse('about') elif action == "add_to_cart": # Pass the request handling to shoppingcart.views # The view in shoppingcart.views performs error handling and logs different errors. But this elif clause # is only used in the "auto-add after user reg/login" case, i.e. it's always wrapped in try_change_enrollment. # This means there's no good way to display error messages to the user. So we log the errors and send # the user to the shopping cart page always, where they can reasonably discern the status of their cart, # whether things got added, etc shoppingcart.views.add_course_to_cart(request, course_id) return HttpResponse( reverse("shoppingcart.views.show_cart") ) elif action == "unenroll": if not CourseEnrollment.is_enrolled(user, course_id): return HttpResponseBadRequest(_("You are not enrolled in this course")) CourseEnrollment.unenroll(user, course_id) course_id_dict = Location.parse_course_id(course_id) dog_stats_api.increment( "common.student.unenrollment", tags=[u"org:{org}".format(**course_id_dict), u"course:{course}".format(**course_id_dict), u"run:{name}".format(**course_id_dict)] ) return HttpResponse() else: return HttpResponseBadRequest(_("Enrollment action is invalid"))
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 self.assertIsNone(cea.user) user1 = UserFactory.create(username="******", email="*****@*****.**", password="******") user2 = UserFactory.create(username="******", email="*****@*****.**", password="******") self.assertFalse( CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists()) user1.email = '*****@*****.**' user1.save() CourseEnrollment.enroll(user1, self.course.id, check_access=True) self.assertTrue( CourseEnrollment.objects.filter(course_id=self.course.id, user=user1).exists()) # The CEA is now linked cea.refresh_from_db() self.assertEqual(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 self.assertRaises(EnrollmentClosedError): CourseEnrollment.enroll(user2, self.course.id, check_access=True) # CEA still linked to user1. Also after unenrolling cea.refresh_from_db() self.assertEqual(cea.user, user1) CourseEnrollment.unenroll(user1, self.course.id) cea.refresh_from_db() self.assertEqual(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() self.assertEqual(cea.user, user1)
def test_shib_login_enrollment(self): """ A functionality test that a student with an existing shib login can auto-enroll in a class with GET or POST params. Also tests the direction functionality of the 'next' GET/POST param """ student = UserFactory.create() extauth = ExternalAuthMap( external_id='*****@*****.**', external_email='', external_domain='shib:https://idp.stanford.edu/', external_credentials="", internal_password="******", user=student) student.set_password("password") student.save() extauth.save() course = CourseFactory.create( org='Stanford', number='123', display_name='Shib Only', enrollment_domain='shib:https://idp.stanford.edu/', user_id=self.test_user_id, ) # use django test client for sessions and url processing # no enrollment before trying self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) self.client.logout() request_kwargs = { 'path': '/shib-login/', 'data': { 'enrollment_action': 'enroll', 'course_id': course.id.to_deprecated_string(), 'next': '/testredirect' }, 'follow': False, 'REMOTE_USER': '******', 'Shib-Identity-Provider': 'https://idp.stanford.edu/' } response = self.client.get(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id)) # Clean up and try again with POST (doesn't happen with real production shib, doing this for test coverage) self.client.logout() CourseEnrollment.unenroll(student, course.id) self.assertFalse(CourseEnrollment.is_enrolled(student, course.id)) response = self.client.post(**request_kwargs) # successful login is a redirect to "/" self.assertEqual(response.status_code, 302) self.assertEqual(response['location'], 'http://testserver/testredirect') # now there is enrollment self.assertTrue(CourseEnrollment.is_enrolled(student, course.id))
def _unenroll_entitlement(self, entitlement, course_run_key, user): """ Internal method to handle the details of Unenrolling a User in a Course Run. """ CourseEnrollment.unenroll(user, course_run_key, skip_refund=True) entitlement.set_enrollment(None)