def test_enrollment_with_invalid_attr(self): """Check response status is bad request when invalid enrollment attributes are passed """ for mode in [CourseMode.DEFAULT_MODE_SLUG, CourseMode.CREDIT_MODE]: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode, mode_display_name=mode, ) # Create an enrollment self.assert_enrollment_status(as_server=True) # Check that the enrollment is the default. self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG) # Check that the enrollment upgraded to credit. enrollment_attributes = [{ "namespace": "credit", "name": "invalid", "value": "hogwarts", }] self.assert_enrollment_status( as_server=True, mode=CourseMode.CREDIT_MODE, expected_status=status.HTTP_400_BAD_REQUEST, enrollment_attributes=enrollment_attributes ) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
def test_enrollment_with_credit_mode(self): """With the right API key, update an existing enrollment with credit mode and set enrollment attributes. """ for mode in [CourseMode.DEFAULT_MODE_SLUG, CourseMode.CREDIT_MODE]: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode, mode_display_name=mode, ) # Create an enrollment self.assert_enrollment_status(as_server=True) # Check that the enrollment is the default. self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG) # Check that the enrollment upgraded to credit. enrollment_attributes = [{ "namespace": "credit", "name": "provider_id", "value": "hogwarts", }] self.assert_enrollment_status( as_server=True, mode=CourseMode.CREDIT_MODE, expected_status=status.HTTP_200_OK, enrollment_attributes=enrollment_attributes ) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.CREDIT_MODE)
def test_update_enrollment_with_expired_mode_throws_error(self): """Verify that if verified mode is expired than it's enrollment cannot be updated. """ for mode in [CourseMode.DEFAULT_MODE_SLUG, CourseMode.VERIFIED]: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode, mode_display_name=mode, ) # Create an enrollment self.assert_enrollment_status(as_server=True) # Check that the enrollment is the default. self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG) # Change verified mode expiration. mode = CourseMode.objects.get(course_id=self.course.id, mode_slug=CourseMode.VERIFIED) mode.expiration_datetime = datetime.datetime(year=1970, month=1, day=1, tzinfo=pytz.utc) mode.save() self.assert_enrollment_status( as_server=True, mode=CourseMode.VERIFIED, expected_status=status.HTTP_400_BAD_REQUEST ) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
def test_downgrade_enrollment_with_mode(self): """With the right API key, downgrade an existing enrollment with a new mode. """ # Create an honor and verified mode for a course. This allows an update. for mode in [CourseMode.DEFAULT_MODE_SLUG, CourseMode.VERIFIED]: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode, mode_display_name=mode, ) # Create a 'verified' enrollment self.assert_enrollment_status(as_server=True, mode=CourseMode.VERIFIED) # Check that the enrollment is verified. self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.VERIFIED) # Check that the enrollment was downgraded to the default mode. self.assert_enrollment_status( as_server=True, mode=CourseMode.DEFAULT_MODE_SLUG, expected_status=status.HTTP_200_OK ) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.DEFAULT_MODE_SLUG)
def test_choose_mode_audit_enroll_on_get(self): """ Confirms that the learner will be enrolled in Audit track if it is the only possible option """ self.mock_enterprise_learner_api() self.mock_enterprise_course_enrollment_get_api() # Create the course mode audit_mode = 'audit' CourseModeFactory.create(mode_slug=audit_mode, course_id=self.course.id, min_price=0) # 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=[unicode(self.course.id)]) response = self.client.get(choose_track_url) # Assert learner is enrolled in Audit track and sent to the dashboard mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertEquals(mode, audit_mode) self.assertTrue(is_active) redirect_url = reverse('dashboard') self.assertRedirects(response, redirect_url)
def test_transfer_students(self): student = UserFactory() student.set_password(self.PASSWORD) # pylint: disable=E1101 student.save() # pylint: disable=E1101 # Original Course original_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0') course = self._create_course(original_course_location) # Enroll the student in 'verified' CourseEnrollment.enroll(student, course.id, mode="verified") # New Course 1 course_location_one = locator.CourseLocator('Org1', 'Course1', 'Run1') new_course_one = self._create_course(course_location_one) # New Course 2 course_location_two = locator.CourseLocator('Org2', 'Course2', 'Run2') new_course_two = self._create_course(course_location_two) original_key = unicode(course.id) new_key_one = unicode(new_course_one.id) new_key_two = unicode(new_course_two.id) # Run the actual management command transfer_students.Command().handle( source_course=original_key, dest_course_list=new_key_one + "," + new_key_two ) # Confirm the enrollment mode is verified on the new courses, and enrollment is enabled as appropriate. self.assertEquals(('verified', False), CourseEnrollment.enrollment_mode_for_user(student, course.id)) self.assertEquals(('verified', True), CourseEnrollment.enrollment_mode_for_user(student, new_course_one.id)) self.assertEquals(('verified', True), CourseEnrollment.enrollment_mode_for_user(student, new_course_two.id))
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_deactivate_enrollment(self): """With the right API key, deactivate (i.e., unenroll from) an existing enrollment.""" # Create an honor and verified mode for a course. This allows an update. for mode in [CourseMode.HONOR, CourseMode.VERIFIED]: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode, mode_display_name=mode, ) # Create a 'verified' enrollment self.assert_enrollment_status(as_server=True, mode=CourseMode.VERIFIED) # Check that the enrollment is 'verified' and active. self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, CourseMode.VERIFIED) # Verify that a non-Boolean enrollment status is treated as invalid. self.assert_enrollment_status( as_server=True, mode=None, is_active='foo', expected_status=status.HTTP_400_BAD_REQUEST ) # Verify that the enrollment has been deactivated, and that the mode is unchanged. self.assert_enrollment_activation(False) # Verify that enrollment deactivation is idempotent. self.assert_enrollment_activation(False)
def test_enroll(self, course_modes, next_url, enrollment_mode, auto_reg): # Create the course modes (if any) required for this test case for mode_slug in course_modes: CourseModeFactory.create(course_id=self.course.id, mode_slug=mode_slug, mode_display_name=mode_slug) # Reverse the expected next URL, if one is provided # (otherwise, use an empty string, which the JavaScript client # interprets as a redirect to the dashboard) full_url = reverse(next_url, kwargs={"course_id": unicode(self.course.id)}) if next_url else next_url # Enroll in the course and verify the URL we get sent to resp = self._change_enrollment("enroll", auto_reg=auto_reg) self.assertEqual(resp.status_code, 200) self.assertEqual(resp.content, full_url) # TODO (ECOM-16): If auto-registration is enabled, check that we're # storing the auto-reg flag in the user's session if auto_reg: self.assertIn("auto_register", self.client.session) self.assertTrue(self.client.session["auto_register"]) # If we're not expecting to be enrolled, verify that this is the case if enrollment_mode is None: self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) # Otherwise, verify that we're enrolled with the expected course mode else: self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, enrollment_mode)
def test_enroll(self, course_modes, next_url, enrollment_mode): # Create the course modes (if any) required for this test case for mode_slug in course_modes: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode_slug, mode_display_name=mode_slug, ) # Reverse the expected next URL, if one is provided # (otherwise, use an empty string, which the JavaScript client # interprets as a redirect to the dashboard) full_url = ( reverse(next_url, kwargs={'course_id': unicode(self.course.id)}) if next_url else next_url ) # Enroll in the course and verify the URL we get sent to resp = self._change_enrollment('enroll') self.assertEqual(resp.status_code, 200) self.assertEqual(resp.content, full_url) # If we're not expecting to be enrolled, verify that this is the case if enrollment_mode is None: self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) # Otherwise, verify that we're enrolled with the expected course mode else: self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, enrollment_mode)
def test_auto_enroll_step(self, course_modes, enrollment_mode, email_opt_in, email_opt_in_result): # Create the course modes for the test case for mode_slug in course_modes: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode_slug, mode_display_name=mode_slug.capitalize() ) # Simulate the pipeline step, passing in a course ID # to indicate that the user was trying to enroll # when they started the auth process. strategy = self._fake_strategy() strategy.session_set('enroll_course_id', unicode(self.course.id)) strategy.session_set('email_opt_in', email_opt_in) result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=assignment-from-no-return,redundant-keyword-arg self.assertEqual(result, {}) # Check that the user was or was not enrolled # (this will vary based on the course mode) if enrollment_mode is not None: actual_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(actual_mode, enrollment_mode) else: self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id)) # Check that the Email Opt In option was set tag = UserOrgTag.objects.get(user=self.user) self.assertIsNotNone(tag) self.assertEquals(tag.value, email_opt_in_result)
def test_add_entitlement_inactive_audit_enrollment(self, mock_get_course_runs): """ Verify that if an entitlement is added for a user, if the user has an inactive audit enrollment that enrollment is NOT upgraded to the mode of the entitlement and linked to the entitlement. """ course_uuid = uuid.uuid4() entitlement_data = self._get_data_set(self.user, str(course_uuid)) mock_get_course_runs.return_value = [{'key': str(self.course.id)}] # Add an audit course enrollment for user. enrollment = CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.AUDIT) enrollment.update_enrollment(is_active=False) response = self.client.post( self.entitlements_list_url, data=json.dumps(entitlement_data), content_type='application/json', ) assert response.status_code == 201 results = response.data course_entitlement = CourseEntitlement.objects.get( user=self.user, course_uuid=course_uuid ) # Assert that enrollment mode is now verified enrollment_mode, enrollment_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) assert enrollment_mode == CourseMode.AUDIT assert enrollment_active is False assert course_entitlement.enrollment_course_run is None assert results == CourseEntitlementSerializer(course_entitlement).data
def test_enrollment_skipped_if_autoreg(self): # TODO (ECOM-16): Remove once we complete the auto-reg AB test. session = self.client.session session['auto_register'] = True session.save() # Create the course modes for mode in ('audit', 'honor', 'verified'): CourseModeFactory(mode_slug=mode, course_id=self.course.id) # Now enroll in the course CourseEnrollmentFactory( user=self.user, is_active=True, mode="honor", course_id=unicode(self.course.id), ) # Choose the mode (POST request) choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)]) self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['audit']) # Verify that enrollment mode is still honor mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertEqual(mode, "honor") self.assertEqual(is_active, True)
def get_group_for_user(cls, course_key, user, user_partition, **kwargs): # pylint: disable=unused-argument """ Returns the Group from the specified user partition to which the user is assigned, via enrollment mode. If a user is in a Credit mode, the Verified or Professional mode for the course is returned instead. If a course is using the Verified Track Cohorting pilot feature, this method returns None regardless of the user's enrollment mode. """ if is_course_using_cohort_instead(course_key): return None # First, check if we have to deal with masquerading. # If the current user is masquerading as a specific student, use the # same logic as normal to return that student's group. If the current # user is masquerading as a generic student in a specific group, then # return that group. if get_course_masquerade(user, course_key) and not is_masquerading_as_specific_student(user, course_key): return get_masquerading_user_group(course_key, user, user_partition) mode_slug, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_key) if mode_slug and is_active: course_mode = CourseMode.mode_for_course( course_key, mode_slug, modes=CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False), ) if course_mode and CourseMode.is_credit_mode(course_mode): course_mode = CourseMode.verified_mode_for_course(course_key) if not course_mode: course_mode = CourseMode.DEFAULT_MODE return Group(ENROLLMENT_GROUP_IDS[course_mode.slug], unicode(course_mode.name)) else: return None
def test_deactivate_enrollment(self, configured_modes, selected_mode): """With the right API key, deactivate (i.e., unenroll from) an existing enrollment.""" # Configure a set of modes for the course. for mode in configured_modes: CourseModeFactory.create(course_id=self.course.id, mode_slug=mode, mode_display_name=mode) # Create an enrollment with the selected mode. self.assert_enrollment_status(as_server=True, mode=selected_mode) # Check that the enrollment has the correct mode and is active. self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id)) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(course_mode, selected_mode) # Verify that a non-Boolean enrollment status is treated as invalid. self.assert_enrollment_status( as_server=True, mode=None, is_active="foo", expected_status=status.HTTP_400_BAD_REQUEST ) # Verify that the enrollment has been deactivated, and that the mode is unchanged. self.assert_enrollment_activation(False, selected_mode) # Verify that enrollment deactivation is idempotent. self.assert_enrollment_activation(False, selected_mode) # Verify that omitting the mode returns 400 for course configurations # in which the default mode doesn't exist. expected_status = ( status.HTTP_200_OK if CourseMode.DEFAULT_MODE_SLUG in configured_modes else status.HTTP_400_BAD_REQUEST ) self.assert_enrollment_status(as_server=True, is_active=False, expected_status=expected_status)
def test_auto_enroll_step(self, course_modes, enrollment_mode): # Create the course modes for the test case for mode_slug in course_modes: CourseModeFactory.create( course_id=self.course.id, mode_slug=mode_slug, mode_display_name=mode_slug.capitalize() ) # Simulate the pipeline step, passing in a course ID # to indicate that the user was trying to enroll # when they started the auth process. strategy = self._fake_strategy() strategy.session_set('enroll_course_id', unicode(self.course.id)) result = pipeline.change_enrollment(strategy, 1, user=self.user) # pylint: disable=E1111,E1124 self.assertEqual(result, {}) # Check that the user was or was not enrolled # (this will vary based on the course mode) if enrollment_mode is not None: actual_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(actual_mode, enrollment_mode) else: self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
def test_successful_default_enrollment(self): # Create the course modes for mode in (CourseMode.DEFAULT_MODE_SLUG, 'verified'): CourseModeFactory(mode_slug=mode, course_id=self.course.id) # Enroll the user in the default mode (honor) to emulate # automatic enrollment params = { 'enrollment_action': 'enroll', 'course_id': unicode(self.course.id) } self.client.post(reverse('change_enrollment'), params) # Explicitly select the honor mode (POST request) choose_track_url = reverse( 'course_modes_choose', args=[unicode(self.course.id)]) self.client.post( choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[CourseMode.DEFAULT_MODE_SLUG]) # Verify that the user's enrollment remains unchanged mode, is_active = CourseEnrollment.enrollment_mode_for_user( self.user, self.course.id) self.assertEqual(mode, CourseMode.DEFAULT_MODE_SLUG) self.assertEqual(is_active, True)
def test_user_already_enrolled_in_unpaid_mode(self, mock_get_course_runs): course_entitlement = CourseEntitlementFactory.create(user=self.user, mode=CourseMode.VERIFIED) mock_get_course_runs.return_value = self.return_values url = reverse( self.ENTITLEMENTS_ENROLLMENT_NAMESPACE, args=[str(course_entitlement.uuid)] ) CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.AUDIT) data = { 'course_run_id': str(self.course.id) } response = self.client.post( url, data=json.dumps(data), content_type='application/json', ) course_entitlement.refresh_from_db() assert response.status_code == 201 assert CourseEnrollment.is_enrolled(self.user, self.course.id) (enrolled_mode, is_active) = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) assert is_active and (enrolled_mode == course_entitlement.mode) assert course_entitlement.enrollment_course_run is not None
def test_enroll_as_honor(self): """Tests that a student can successfully enroll through this view""" response = self._enroll_through_view(self.course) self.assertEqual(response.status_code, 200) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) self.assertTrue(is_active) self.assertEqual(enrollment_mode, u"honor")
def is_track_ok_for_exam(user, exam): """ Returns whether the user is in an appropriate enrollment mode """ course_id = CourseKey.from_string(exam['course_id']) mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id) return is_active and mode in (CourseMode.VERIFIED, CourseMode.MASTERS, CourseMode.PROFESSIONAL)
def get(self, request, course_id): """ Handle the case where we have a get request """ upgrade = request.GET.get('upgrade', False) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) verify_mode = CourseMode.mode_for_course(course_id, "verified") if verify_mode is None: return redirect(reverse('dashboard')) chosen_price = request.session.get( "donation_for_course", {} ).get( course_id.to_deprecated_string(), verify_mode.min_price ) course = modulestore().get_course(course_id) context = { "course_id": course_id.to_deprecated_string(), "course_modes_choose_url": reverse('course_modes_choose', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, "create_order_url": reverse("verify_student_create_order"), "upgrade": upgrade, } return render_to_response('verify_student/verified.html', context)
def get(self, request, course_id, error=None): """ Displays the course mode choice page """ enrollment_mode = CourseEnrollment.enrollment_mode_for_user(request.user, course_id) upgrade = request.GET.get('upgrade', False) # verified users do not need to register or upgrade if enrollment_mode == 'verified': return redirect(reverse('dashboard')) # registered users who are not trying to upgrade do not need to re-register if enrollment_mode is not None and upgrade is False: return redirect(reverse('dashboard')) modes = CourseMode.modes_for_course_dict(course_id) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(course_id, None) course = course_from_id(course_id) context = { "course_id": course_id, "modes": modes, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "upgrade": upgrade, } if "verified" in modes: context["suggested_prices"] = [decimal.Decimal(x) for x in modes["verified"].suggested_prices.split(",")] context["currency"] = modes["verified"].currency.upper() context["min_price"] = modes["verified"].min_price return render_to_response("course_modes/choose.html", context)
def get(self, request, course_id, error=None): """ Displays the course mode choice page """ if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) modes = CourseMode.modes_for_course_dict(course_id) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(course_id, None) course = course_from_id(course_id) context = { "course_id": course_id, "modes": modes, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, } if "verified" in modes: context["suggested_prices"] = modes["verified"].suggested_prices.split(",") context["currency"] = modes["verified"].currency.upper() context["min_price"] = modes["verified"].min_price return render_to_response("course_modes/choose.html", context)
def get(self, request, course_id): """ Displays the main verification view, which contains three separate steps: - Taking the standard face photo - Taking the id photo - Confirming that the photos and payment price are correct before proceeding to payment """ upgrade = request.GET.get('upgrade', False) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) # If the user has already been verified within the given time period, # redirect straight to the payment -- no need to verify again. if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): return redirect( reverse('verify_student_verified', kwargs={'course_id': course_id.to_deprecated_string()}) + "?upgrade={}".format(upgrade) ) elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) else: # If they haven't completed a verification attempt, we have to # restart with a new one. We can't reuse an older one because we # won't be able to show them their encrypted photo_id -- it's easier # bookkeeping-wise just to start over. progress_state = "start" modes_dict = CourseMode.modes_for_course_dict(course_id) verify_mode = modes_dict.get('verified', None) # if the course doesn't have a verified mode, we want to kick them # from the flow if not verify_mode: return redirect(reverse('dashboard')) if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}): chosen_price = request.session["donation_for_course"][course_id.to_deprecated_string()] else: chosen_price = verify_mode.min_price course = modulestore().get_course(course_id) context = { "progress_state": progress_state, "user_full_name": request.user.profile.name, "course_id": course_id.to_deprecated_string(), "course_modes_choose_url": reverse('course_modes_choose', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "suggested_prices": [ decimal.Decimal(price) for price in verify_mode.suggested_prices.split(",") ], "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, "min_price": verify_mode.min_price, "upgrade": upgrade == u'True', "can_audit": "audit" in modes_dict, } return render_to_response('verify_student/photo_verification.html', context)
def __init__(self, course_id, email): # N.B. retired users are not a concern here because they should be # handled at a higher level (i.e. in enroll_email). Besides, this # class creates readonly objects. exists_user = User.objects.filter(email=email).exists() if exists_user: user = User.objects.get(email=email) mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_id) # is_active is `None` if the user is not enrolled in the course exists_ce = is_active is not None and is_active full_name = user.profile.name ceas = CourseEnrollmentAllowed.for_user(user).filter(course_id=course_id).all() else: mode = None exists_ce = False full_name = None ceas = CourseEnrollmentAllowed.objects.filter(email=email, course_id=course_id).all() exists_allowed = ceas.exists() state_auto_enroll = exists_allowed and ceas[0].auto_enroll self.user = exists_user self.enrollment = exists_ce self.allowed = exists_allowed self.auto_enroll = bool(state_auto_enroll) self.full_name = full_name self.mode = mode
def show_requirements(request, course_id): """ Show the requirements necessary for the verification flow. """ # TODO: seems borked for professional; we're told we need to take photos even if there's a pending verification course_id = CourseKey.from_string(course_id) upgrade = request.GET.get('upgrade', False) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): return redirect( reverse( 'verify_student_verified', kwargs={'course_id': course_id.to_deprecated_string()} ) + "?upgrade={}".format(upgrade) ) upgrade = request.GET.get('upgrade', False) course = modulestore().get_course(course_id) modes_dict = CourseMode.modes_for_course_dict(course_id) context = { "course_id": course_id.to_deprecated_string(), "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id.to_deprecated_string()}), "verify_student_url": reverse('verify_student_verify', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "is_not_active": not request.user.is_active, "upgrade": upgrade == u'True', "modes_dict": modes_dict, } return render_to_response("verify_student/show_requirements.html", context)
def get(self, request, course_id): """ Handle the case where we have a get request """ upgrade = request.GET.get('upgrade', False) if CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == 'verified': return redirect(reverse('dashboard')) verify_mode = CourseMode.mode_for_course(course_id, "verified") if course_id in request.session.get("donation_for_course", {}): chosen_price = request.session["donation_for_course"][course_id] else: chosen_price = verify_mode.min_price.format("{:g}") course = course_from_id(course_id) context = { "course_id": course_id, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, "upgrade": upgrade, } return render_to_response('verify_student/verified.html', context)
def test_confirmation_email_error(self): CourseMode.objects.create( course_id=self.course_key, mode_slug="verified", mode_display_name="Verified", min_price=self.cost ) cart = Order.get_cart_for_user(self.user) CertificateItem.add_to_order(cart, self.course_key, self.cost, 'verified') # Simulate an error when sending the confirmation # email. This should NOT raise an exception. # If it does, then the implicit view-level # transaction could cause a roll-back, effectively # reversing order fulfillment. with patch.object(mail.message.EmailMessage, 'send') as mock_send: mock_send.side_effect = Exception("Kaboom!") cart.purchase() # Verify that the purchase completed successfully self.assertEqual(cart.status, 'purchased') # Verify that the user is enrolled as "verified" mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_key) self.assertTrue(is_active) self.assertEqual(mode, 'verified')
def is_enabled(self): if self.date is None: return False (mode, is_active) = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) if is_active and mode == 'verified': return self.verification_status in ('expired', 'none', 'must_reverify') return False
def description(self): if datetime.now(utc) <= self.date: mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id) if is_active and CourseMode.is_eligible_for_certificate(mode): return _('To earn a certificate, you must complete all requirements before this date.') else: return _('After this date, course content will be archived.') return _('This course is archived, which means you can review course content but it is no longer active.')
def upload_user_grades_csv(_xmodule_instance_args, _entry_id, course_id, _task_input, action_name): # pylint: disable=too-many-statements """ For a given `course_id`, for given usernames generates a grades CSV file, and store using a `ReportStore`. Once created, the files can be accessed by instantiating another `ReportStore` (via `ReportStore.from_config()`) and calling `link_for()` on it. Unenrolled users and unknown usernames are stored in *_err_*.csv This task is very close to the .upload_grades_csv from instructor_tasks.task_helper The difference is that we filter enrolled students against requested usernames and we push info about this into PLP """ start_time = time() start_date = datetime.now(UTC) status_interval = 100 fmt = u'Task: {task_id}, InstructorTask ID: {entry_id}, Course: {course_id}, Input: {task_input}' task_info_string = fmt.format( task_id=_xmodule_instance_args.get('task_id') if _xmodule_instance_args is not None else None, entry_id=_entry_id, course_id=course_id, task_input=_task_input ) TASK_LOG.info(u'%s, Task type: %s, Starting task execution', task_info_string, action_name) extended_kwargs_id = _task_input.get("extended_kwargs_id") extended_kwargs = InstructorTaskExtendedKwargs.get_kwargs_for_id(extended_kwargs_id) usernames = extended_kwargs.get("usernames", None) err_rows = [["id", "username", "error_msg"]] if usernames is None: message = "Error occured during edx task execution: no usersnames in InstructorTaskExtendedKwargs." TASK_LOG.error(u'%s, Task type: %s, ' + message, task_info_string) err_rows.append(["-1", "__", message]) usernames = [] enrolled_students = CourseEnrollment.objects.users_enrolled_in(course_id) enrolled_students = enrolled_students.filter(username__in=usernames) total_enrolled_students = enrolled_students.count() requester_id = _task_input.get("requester_id") task_progress = TaskProgress(action_name, total_enrolled_students, start_time) course = get_course_by_id(course_id) course_is_cohorted = is_course_cohorted(course.id) teams_enabled = course.teams_enabled cohorts_header = ['Cohort Name'] if course_is_cohorted else [] teams_header = ['Team Name'] if teams_enabled else [] experiment_partitions = get_split_user_partitions(course.user_partitions) group_configs_header = [u'Experiment Group ({})'.format(partition.name) for partition in experiment_partitions] certificate_info_header = ['Certificate Eligible', 'Certificate Delivered', 'Certificate Type'] certificate_whitelist = CertificateWhitelist.objects.filter(course_id=course_id, whitelist=True) whitelisted_user_ids = [entry.user_id for entry in certificate_whitelist] # Loop over all our students and build our CSV lists in memory rows = [] current_step = {'step': 'Calculating Grades'} TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students, ) found_students = User.objects.filter(username__in=usernames) # Check invalid usernames if len(found_students)!= len(usernames): found_students_usernames = [x.username for x in found_students] for u in usernames: if u not in found_students_usernames: err_rows.append([-1, u, "invalid_username"]) # Check not enrolled requested students if found_students != enrolled_students: diff = found_students.exclude(id__in=enrolled_students) for u in diff: if u in diff: err_rows.append([u.id, u.username, "enrollment_for_username_not_found"]) total_enrolled_students = enrolled_students.count() student_counter = 0 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Starting grade calculation for total students: %s', task_info_string, action_name, current_step, total_enrolled_students ) graded_assignments = course.grading.graded_assignments(course_id) grade_header = course.grading.grade_header(graded_assignments) rows.append( ["Student ID", "Email", "Username", "Last Name", "First Name", "Second Name", "Grade", "Grade Percent"] + grade_header + cohorts_header + group_configs_header + teams_header + ['Enrollment Track', 'Verification Status'] + certificate_info_header ) for student, course_grade, err_msg in CourseGradeFactory().iter(course, enrolled_students): # Periodically update task status (this is a cache write) if task_progress.attempted % status_interval == 0: task_progress.update_task_state(extra_meta=current_step) task_progress.attempted += 1 # Now add a log entry after each student is graded to get a sense # of the task's progress student_counter += 1 TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation in-progress for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) if not course_grade: # An empty course_grade means we failed to grade a student. task_progress.failed += 1 err_rows.append([student.id, student.username, err_msg]) continue # We were able to successfully grade this student for this course. task_progress.succeeded += 1 cohorts_group_name = [] if course_is_cohorted: group = get_cohort(student, course_id, assign=False) cohorts_group_name.append(group.name if group else '') group_configs_group_names = [] for partition in experiment_partitions: group = PartitionService(course_id).get_group(student, partition, assign=False) group_configs_group_names.append(group.name if group else '') team_name = [] if teams_enabled: try: membership = CourseTeamMembership.objects.get(user=student, team__course_id=course_id) team_name.append(membership.team.name) except CourseTeamMembership.DoesNotExist: team_name.append('') enrollment_mode = CourseEnrollment.enrollment_mode_for_user(student, course_id)[0] verification_status = SoftwareSecurePhotoVerification.verification_status_for_user( student, course_id, enrollment_mode ) certificate_info = certificate_info_for_user( student, course_id, course_grade.letter_grade, student.id in whitelisted_user_ids ) second_name = '' try: up = UserProfile.objects.get(user=student) if up.goals: second_name = json.loads(up.goals).get('second_name', '') except ValueError: pass if certificate_info[0] == 'Y': TASK_LOG.info( u'Student is marked eligible_for_certificate' u'(user=%s, course_id=%s, grade_percent=%s gradecutoffs=%s, allow_certificate=%s, is_whitelisted=%s)', student, course_id, course_grade.percent, course.grade_cutoffs, student.profile.allow_certificate, student.id in whitelisted_user_ids ) grade_results = course.grading.grade_results(graded_assignments, course_grade) grade_results = list(chain.from_iterable(grade_results)) rows.append( [student.id, student.email, student.username, student.last_name, student.first_name, second_name, course_grade.percent, course_grade.percent*100] + grade_results + cohorts_group_name + group_configs_group_names + team_name + [enrollment_mode] + [verification_status] + certificate_info ) TASK_LOG.info( u'%s, Task type: %s, Current step: %s, Grade calculation completed for students: %s/%s', task_info_string, action_name, current_step, student_counter, total_enrolled_students ) # By this point, we've got the rows we're going to stuff into our CSV files. current_step = {'step': 'Uploading CSVs'} task_progress.update_task_state(extra_meta=current_step) TASK_LOG.info(u'%s, Task type: %s, Current step: %s', task_info_string, action_name, current_step) # Perform the actual upload custom_grades_download = get_custom_grade_config() report_hash_unique_hash = hex(random.getrandbits(32))[2:] report_name = 'plp_grade_users_report_{}_id_{}'.format(report_hash_unique_hash, requester_id) err_report_name = 'plp_grade_users_report_err_{}_id_{}'.format(report_hash_unique_hash, requester_id) upload_csv_to_report_store(rows, report_name, course_id, start_date, config_name=custom_grades_download) # If there are any error rows (don't count the header), write them out as well has_errors = len(err_rows) > 1 if has_errors: upload_csv_to_report_store(err_rows, err_report_name, course_id, start_date, config_name=custom_grades_download) callback_url = _task_input.get("callback_url", None) if callback_url: report_store = ReportStore.from_config(config_name=custom_grades_download) files_urls_pairs = report_store.links_for(course_id) find_by_name = lambda name: [url for filename, url in files_urls_pairs if name in filename][0] try: csv_url = find_by_name(report_name) csv_err_url = find_by_name(err_report_name) if has_errors else None PlpApiClient().push_grade_api_result(callback_url, csv_url, csv_err_url) except Exception as e: TASK_LOG.error("Failed push to PLP:{}".format(str(e))) # One last update before we close out... TASK_LOG.info(u'%s, Task type: %s, Finalizing grade task', task_info_string, action_name) return task_progress.update_task_state(extra_meta=current_step)
def test_transfer_students(self): """ Verify the transfer student command works as intended. """ student = UserFactory.create() student.set_password(self.PASSWORD) # pylint: disable=no-member student.save() # pylint: disable=no-member mode = 'verified' # Original Course original_course_location = locator.CourseLocator( 'Org0', 'Course0', 'Run0') course = self._create_course(original_course_location) # Enroll the student in 'verified' CourseEnrollment.enroll(student, course.id, mode="verified") # Create and purchase a verified cert for the original course. self._create_and_purchase_verified(student, course.id) # New Course 1 course_location_one = locator.CourseLocator('Org1', 'Course1', 'Run1') new_course_one = self._create_course(course_location_one) # New Course 2 course_location_two = locator.CourseLocator('Org2', 'Course2', 'Run2') new_course_two = self._create_course(course_location_two) original_key = unicode(course.id) new_key_one = unicode(new_course_one.id) new_key_two = unicode(new_course_two.id) # Run the actual management command transfer_students.Command().handle(source_course=original_key, dest_course_list=new_key_one + "," + new_key_two) self.assertTrue(self.signal_fired) # Confirm the analytics event was emitted. self.mock_tracker.emit.assert_has_calls( # pylint: disable=maybe-no-member [ call(EVENT_NAME_ENROLLMENT_ACTIVATED, { 'course_id': original_key, 'user_id': student.id, 'mode': mode }), call(EVENT_NAME_ENROLLMENT_MODE_CHANGED, { 'course_id': original_key, 'user_id': student.id, 'mode': mode }), call(EVENT_NAME_ENROLLMENT_DEACTIVATED, { 'course_id': original_key, 'user_id': student.id, 'mode': mode }), call(EVENT_NAME_ENROLLMENT_ACTIVATED, { 'course_id': new_key_one, 'user_id': student.id, 'mode': mode }), call(EVENT_NAME_ENROLLMENT_MODE_CHANGED, { 'course_id': new_key_one, 'user_id': student.id, 'mode': mode }), call(EVENT_NAME_ENROLLMENT_ACTIVATED, { 'course_id': new_key_two, 'user_id': student.id, 'mode': mode }), call(EVENT_NAME_ENROLLMENT_MODE_CHANGED, { 'course_id': new_key_two, 'user_id': student.id, 'mode': mode }) ]) self.mock_tracker.reset_mock() # Confirm the enrollment mode is verified on the new courses, and enrollment is enabled as appropriate. self.assertEquals( (mode, False), CourseEnrollment.enrollment_mode_for_user(student, course.id)) self.assertEquals( (mode, True), CourseEnrollment.enrollment_mode_for_user(student, new_course_one.id)) self.assertEquals( (mode, True), CourseEnrollment.enrollment_mode_for_user(student, new_course_two.id)) # Confirm the student has not be refunded. target_certs = CertificateItem.objects.filter(course_id=course.id, user_id=student, status='purchased', mode=mode) self.assertTrue(target_certs[0]) self.assertFalse(target_certs[0].refund_requested_time) self.assertEquals(target_certs[0].order.status, 'purchased')
def get(self, request, course_id): """ Displays the main verification view, which contains three separate steps: - Taking the standard face photo - Taking the id photo - Confirming that the photos and payment price are correct before proceeding to payment """ upgrade = request.GET.get('upgrade', False) course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id) # If the user has already been verified within the given time period, # redirect straight to the payment -- no need to verify again. if SoftwareSecurePhotoVerification.user_has_valid_or_pending(request.user): return redirect( reverse('verify_student_verified', kwargs={'course_id': course_id.to_deprecated_string()}) + "?upgrade={}".format(upgrade) ) elif CourseEnrollment.enrollment_mode_for_user(request.user, course_id) == ('verified', True): return redirect(reverse('dashboard')) else: # If they haven't completed a verification attempt, we have to # restart with a new one. We can't reuse an older one because we # won't be able to show them their encrypted photo_id -- it's easier # bookkeeping-wise just to start over. progress_state = "start" # we prefer professional over verify current_mode = CourseMode.verified_mode_for_course(course_id) # if the course doesn't have a verified mode, we want to kick them # from the flow if not current_mode: return redirect(reverse('dashboard')) if course_id.to_deprecated_string() in request.session.get("donation_for_course", {}): chosen_price = request.session["donation_for_course"][course_id.to_deprecated_string()] else: chosen_price = current_mode.min_price course = modulestore().get_course(course_id) if current_mode.suggested_prices != '': suggested_prices = [ decimal.Decimal(price) for price in current_mode.suggested_prices.split(",") ] else: suggested_prices = [] context = { "progress_state": progress_state, "user_full_name": request.user.profile.name, "course_id": course_id.to_deprecated_string(), "course_modes_choose_url": reverse('course_modes_choose', kwargs={'course_id': course_id.to_deprecated_string()}), "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "suggested_prices": suggested_prices, "currency": current_mode.currency.upper(), "chosen_price": chosen_price, "min_price": current_mode.min_price, "upgrade": upgrade == u'True', "can_audit": CourseMode.mode_for_course(course_id, 'audit') is not None, "modes_dict": CourseMode.modes_for_course_dict(course_id), # TODO (ECOM-16): Remove once the AB test completes "autoreg": request.session.get('auto_register', False), } return render_to_response('verify_student/photo_verification.html', context)
def add_cert(self, student, course_id, course=None): """ Arguments: student - User.object course_id - courseenrollment.course_id (string) Request a new certificate for a student. Will change the certificate status to 'generating'. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the student's status """ VALID_STATUSES = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing ] cert_status = certificate_status_for_student(student, course_id)['status'] new_status = cert_status if cert_status in VALID_STATUSES: # grade the student # re-use the course passed in optionally so we don't have to re-fetch everything # for every student if course is None: course = courses.get_course_by_id(course_id) profile = UserProfile.objects.get(user=student) # Needed self.request.user = student self.request.session = {} grade = grades.grade(student, self.request, course) is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() enrollment_mode = CourseEnrollment.enrollment_mode_for_user( student, course_id) org = course_id.split('/')[0] course_num = course_id.split('/')[1] cert_mode = enrollment_mode if enrollment_mode == GeneratedCertificate.MODES.verified and SoftwareSecurePhotoVerification.user_is_verified( student): template_pdf = "certificate-template-{0}-{1}-verified.pdf".format( org, course_num) elif (enrollment_mode == GeneratedCertificate.MODES.verified and not SoftwareSecurePhotoVerification.user_is_verified(student) ): template_pdf = "certificate-template-{0}-{1}.pdf".format( org, course_num) cert_mode = GeneratedCertificate.MODES.honor else: # honor code and audit students template_pdf = "certificate-template-{0}-{1}.pdf".format( org, course_num) cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile.name if is_whitelisted or grade['grade'] is not None: # check to see whether the student is on the # the embargoed country restricted list # otherwise, put a new certificate request # on the queue if self.restricted.filter(user=student).exists(): new_status = status.restricted cert.status = new_status cert.save() else: key = make_hashkey(random.random()) cert.key = key contents = { 'action': 'create', 'username': student.username, 'course_id': course_id, 'name': profile.name, 'grade': grade['grade'], 'template_pdf': template_pdf, } new_status = status.generating cert.status = new_status cert.save() self._send_to_xqueue(contents, key) else: new_status = status.notpassing cert.status = new_status cert.save() return new_status
def test_transfer_students(self): """ Verify the transfer student command works as intended. """ student = UserFactory.create() student.set_password(self.PASSWORD) student.save() mode = 'verified' # Original Course original_course_location = locator.CourseLocator('Org0', 'Course0', 'Run0') course = self._create_course(original_course_location) # Enroll the student in 'verified' CourseEnrollment.enroll(student, course.id, mode='verified') # Create and purchase a verified cert for the original course. self._create_and_purchase_verified(student, course.id) # New Course 1 course_location_one = locator.CourseLocator('Org1', 'Course1', 'Run1') new_course_one = self._create_course(course_location_one) # New Course 2 course_location_two = locator.CourseLocator('Org2', 'Course2', 'Run2') new_course_two = self._create_course(course_location_two) original_key = text_type(course.id) new_key_one = text_type(new_course_one.id) new_key_two = text_type(new_course_two.id) # Run the actual management command call_command( 'transfer_students', '--from', original_key, '--to', new_key_one, new_key_two, ) self.assertTrue(self.signal_fired) # Confirm the analytics event was emitted. self.mock_tracker.emit.assert_has_calls( [ call( EVENT_NAME_ENROLLMENT_ACTIVATED, {'course_id': original_key, 'user_id': student.id, 'mode': mode} ), call( EVENT_NAME_ENROLLMENT_MODE_CHANGED, {'course_id': original_key, 'user_id': student.id, 'mode': mode} ), call( EVENT_NAME_ENROLLMENT_DEACTIVATED, {'course_id': original_key, 'user_id': student.id, 'mode': mode} ), call( EVENT_NAME_ENROLLMENT_ACTIVATED, {'course_id': new_key_one, 'user_id': student.id, 'mode': mode} ), call( EVENT_NAME_ENROLLMENT_MODE_CHANGED, {'course_id': new_key_one, 'user_id': student.id, 'mode': mode} ), call( EVENT_NAME_ENROLLMENT_ACTIVATED, {'course_id': new_key_two, 'user_id': student.id, 'mode': mode} ), call( EVENT_NAME_ENROLLMENT_MODE_CHANGED, {'course_id': new_key_two, 'user_id': student.id, 'mode': mode} ) ] ) self.mock_tracker.reset_mock() # Confirm the enrollment mode is verified on the new courses, and enrollment is enabled as appropriate. self.assertEqual((mode, False), CourseEnrollment.enrollment_mode_for_user(student, course.id)) self.assertEqual((mode, True), CourseEnrollment.enrollment_mode_for_user(student, new_course_one.id)) self.assertEqual((mode, True), CourseEnrollment.enrollment_mode_for_user(student, new_course_two.id))
def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) # We assume that, if 'professional' is one of the modes, it is the *only* mode. # If we offer more modes alongside 'professional' in the future, this will need to route # to the usual "choose your track" page same is true for no-id-professional mode. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})) # If there isn't a verified mode available, then there's nothing # to do on this page. The user has almost certainly been auto-registered # in the "honor" track by this point, so we send the user # to the dashboard. if not CourseMode.has_verified_mode(modes): return redirect(get_mktg_enroll_success_redirect(course_id)) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(unicode(course_key), None) course = modulestore().get_course(course_key) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for univerity credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False)) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True } if "verified" in modes: context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in modes["verified"].suggested_prices.split(",") if x.strip() ] context["currency"] = modes["verified"].currency.upper() context["min_price"] = modes["verified"].min_price context["verified_name"] = modes["verified"].name context["verified_description"] = modes["verified"].description return render_to_response("course_modes/choose.html", context)
def add_cert(self, student, course_id, course=None, forced_grade=None, template_file=None, generate_pdf=True): """ Request a new certificate for a student. Arguments: student - User.object course_id - courseenrollment.course_id (CourseKey) forced_grade - a string indicating a grade parameter to pass with the certificate request. If this is given, grading will be skipped. generate_pdf - Boolean should a message be sent in queue to generate certificate PDF Will change the certificate status to 'generating' or `downloadable` in case of web view certificates. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student has allow_certificate set to False in the userprofile table the status will change to 'restricted' If a student does not have a passing grade the status will change to status.notpassing Returns the newly created certificate instance """ valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing, status.downloadable, status.auditing, status.audit_passing, status.audit_notpassing, ] cert_status = certificate_status_for_student(student, course_id)['status'] cert = None if cert_status not in valid_statuses: LOGGER.warning( ( u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"the certificate status '%s' is not one of %s." ), student.id, unicode(course_id), cert_status, unicode(valid_statuses) ) return None # The caller can optionally pass a course in to avoid # re-fetching it from Mongo. If they have not provided one, # get it from the modulestore. if course is None: course = modulestore().get_course(course_id, depth=0) profile = UserProfile.objects.get(user=student) profile_name = profile.name # Needed for access control in grading. self.request.user = student self.request.session = {} is_whitelisted = self.whitelist.filter(user=student, course_id=course_id, whitelist=True).exists() grade = grades.grade(student, self.request, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(student, course_id) mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES user_is_verified = SoftwareSecurePhotoVerification.user_is_verified(student) cert_mode = enrollment_mode is_eligible_for_certificate = is_whitelisted or CourseMode.is_eligible_for_certificate(enrollment_mode) unverified = False # For credit mode generate verified certificate if cert_mode == CourseMode.CREDIT_MODE: cert_mode = CourseMode.VERIFIED if template_file is not None: template_pdf = template_file elif mode_is_verified and user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format(id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) if CourseMode.mode_for_course(course_id, CourseMode.HONOR): cert_mode = GeneratedCertificate.MODES.honor else: unverified = True else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format(id=course_id) if forced_grade: grade['grade'] = forced_grade LOGGER.info( ( u"Certificate generated for student %s in the course: %s with template: %s. " u"given template: %s, " u"user is verified: %s, " u"mode is verified: %s" ), student.username, unicode(course_id), template_pdf, template_file, user_is_verified, mode_is_verified ) cert, created = GeneratedCertificate.objects.get_or_create(user=student, course_id=course_id) # pylint: disable=no-member cert.mode = cert_mode cert.user = student cert.grade = grade['percent'] cert.course_id = course_id cert.name = profile_name cert.download_url = '' # Strip HTML from grade range label grade_contents = grade.get('grade', None) try: grade_contents = lxml.html.fromstring(grade_contents).text_content() passing = True except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info( ( u"Could not retrieve grade for student %s " u"in the course '%s' " u"because an exception occurred while parsing the " u"grade contents '%s' as HTML. " u"The exception was: '%s'" ), student.id, unicode(course_id), grade_contents, unicode(exc) ) # Log if the student is whitelisted if is_whitelisted: LOGGER.info( u"Student %s is whitelisted in '%s'", student.id, unicode(course_id) ) passing = True else: passing = False # If this user's enrollment is not eligible to receive a # certificate, mark it as such for reporting and # analytics. Only do this if the certificate is new, or # already marked as ineligible -- we don't want to mark # existing audit certs as ineligible. cutoff = settings.AUDIT_CERT_CUTOFF_DATE if (cutoff and cert.created_date >= cutoff) and not is_eligible_for_certificate: cert.status = CertificateStatuses.audit_passing if passing else CertificateStatuses.audit_notpassing cert.save() LOGGER.info( u"Student %s with enrollment mode %s is not eligible for a certificate.", student.id, enrollment_mode ) return cert # If they are not passing, short-circuit and don't generate cert elif not passing: cert.status = status.notpassing cert.save() LOGGER.info( ( u"Student %s does not have a grade for '%s', " u"so their certificate status has been set to '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, unicode(course_id), cert.status ) return cert # Check to see whether the student is on the the embargoed # country restricted list. If so, they should not receive a # certificate -- set their status to restricted and log it. if self.restricted.filter(user=student).exists(): cert.status = status.restricted cert.save() LOGGER.info( ( u"Student %s is in the embargoed country restricted " u"list, so their certificate status has been set to '%s' " u"for the course '%s'. " u"No certificate generation task was sent to the XQueue." ), student.id, cert.status, unicode(course_id) ) return cert if unverified: cert.status = status.unverified cert.save() LOGGER.info( ( u"User %s has a verified enrollment in course %s " u"but is missing ID verification. " u"Certificate status has been set to unverified" ), student.id, unicode(course_id), ) return cert # Finally, generate the certificate and send it off. return self._generate_cert(cert, course, student, grade_contents, template_pdf, generate_pdf)
def get(self, request, course_id): """ Displays the main verification view, which contains three separate steps: - Taking the standard face photo - Taking the id photo - Confirming that the photos and payment price are correct before proceeding to payment """ upgrade = request.GET.get('upgrade', False) # If the user has already been verified within the given time period, # redirect straight to the payment -- no need to verify again. if SoftwareSecurePhotoVerification.user_has_valid_or_pending( request.user): return redirect( reverse('verify_student_verified', kwargs={'course_id': course_id}) + "?upgrade={}".format(upgrade)) elif CourseEnrollment.enrollment_mode_for_user( request.user, course_id) == 'verified': return redirect(reverse('dashboard')) else: # If they haven't completed a verification attempt, we have to # restart with a new one. We can't reuse an older one because we # won't be able to show them their encrypted photo_id -- it's easier # bookkeeping-wise just to start over. progress_state = "start" verify_mode = CourseMode.mode_for_course(course_id, "verified") # if the course doesn't have a verified mode, we want to kick them # from the flow if not verify_mode: return redirect(reverse('dashboard')) if course_id in request.session.get("donation_for_course", {}): chosen_price = request.session["donation_for_course"][course_id] else: chosen_price = verify_mode.min_price course = course_from_id(course_id) context = { "progress_state": progress_state, "user_full_name": request.user.profile.name, "course_id": course_id, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "purchase_endpoint": get_purchase_endpoint(), "suggested_prices": [ decimal.Decimal(price) for price in verify_mode.suggested_prices.split(",") ], "currency": verify_mode.currency.upper(), "chosen_price": chosen_price, "min_price": verify_mode.min_price, "upgrade": upgrade, } return render_to_response('verify_student/photo_verification.html', context)
def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) ecommerce_service = EcommerceService() # We assume that, if 'professional' is one of the modes, it should be the *only* mode. # If there are both modes, default to non-id-professional. has_enrolled_professional = ( CourseMode.is_professional_slug(enrollment_mode) and is_active) if CourseMode.has_professional_mode( modes) and not has_enrolled_professional: purchase_workflow = request.GET.get("purchase_workflow", "single") verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)}) redirect_url = "{url}?purchase_workflow={workflow}".format( url=verify_url, workflow=purchase_workflow) if ecommerce_service.is_enabled(request.user): professional_mode = modes.get( CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get( CourseMode.PROFESSIONAL) if purchase_workflow == "single" and professional_mode.sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.sku) if purchase_workflow == "bulk" and professional_mode.bulk_sku: redirect_url = ecommerce_service.get_checkout_page_url( professional_mode.bulk_sku) return redirect(redirect_url) course = modulestore().get_course(course_key) # If there isn't a verified mode available, then there's nothing # to do on this page. Send the user to the dashboard. if not CourseMode.has_verified_mode(modes): return redirect(reverse('dashboard')) # If a user has already paid, redirect them to the dashboard. if is_active and (enrollment_mode in CourseMode.VERIFIED_MODES + [CourseMode.NO_ID_PROFESSIONAL_MODE]): # If the course has started redirect to course home instead if course.has_started(): return redirect( reverse('openedx.course_experience.course_home', kwargs={'course_id': course_key})) return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(unicode(course_key), None) if CourseEnrollment.is_enrollment_closed(request.user, course): locale = to_locale(get_language()) enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale) params = urllib.urlencode({'course_closed': enrollment_end_date}) return redirect('{0}?{1}'.format(reverse('dashboard'), params)) # When a credit mode is available, students will be given the option # to upgrade from a verified mode to a credit mode at the end of the course. # This allows students who have completed photo verification to be eligible # for univerity credit. # Since credit isn't one of the selectable options on the track selection page, # we need to check *all* available course modes in order to determine whether # a credit mode is available. If so, then we show slightly different messaging # for the verified track. has_credit_upsell = any( CourseMode.is_credit_mode(mode) for mode in CourseMode.modes_for_course(course_key, only_selectable=False)) course_id = text_type(course_key) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_id}), "modes": modes, "has_credit_upsell": has_credit_upsell, "course_name": course.display_name_with_default_escaped, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "responsive": True, "nav_hidden": True, } context.update( get_experiment_user_metadata_context( course, request.user, )) title_content = _( "Congratulations! You are now enrolled in {course_name}").format( course_name=course.display_name_with_default_escaped) context["title_content"] = title_content if "verified" in modes: verified_mode = modes["verified"] context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in verified_mode.suggested_prices.split(",") if x.strip() ] context["currency"] = verified_mode.currency.upper() context["min_price"] = verified_mode.min_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description if verified_mode.sku: context[ "use_ecommerce_payment_flow"] = ecommerce_service.is_enabled( request.user) context[ "ecommerce_payment_page"] = ecommerce_service.payment_page_url( ) context["sku"] = verified_mode.sku context["bulk_sku"] = verified_mode.bulk_sku context['currency_data'] = [] if waffle.switch_is_active('local_currency'): if 'edx-price-l10n' not in request.COOKIES: currency_data = get_currency_data() try: context['currency_data'] = json.dumps(currency_data) except TypeError: pass return render_to_response("course_modes/choose.html", context)
def get(self, request, course_id, error=None): """Displays the course mode choice page. Args: request (`Request`): The Django Request object. course_id (unicode): The slash-separated course key. Keyword Args: error (unicode): If provided, display this error message on the page. Returns: Response """ course_key = CourseKey.from_string(course_id) # Check whether the user has access to this course # based on country access rules. embargo_redirect = embargo_api.redirect_if_blocked( course_key, user=request.user, ip_address=get_ip(request), url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) modes = CourseMode.modes_for_course_dict(course_key) # We assume that, if 'professional' is one of the modes, it is the *only* mode. # If we offer more modes alongside 'professional' in the future, this will need to route # to the usual "choose your track" page. has_enrolled_professional = (enrollment_mode == "professional" and is_active) if "professional" in modes and not has_enrolled_professional: return redirect( reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})) # If there isn't a verified mode available, then there's nothing # to do on this page. The user has almost certainly been auto-registered # in the "honor" track by this point, so we send the user # to the dashboard. if not CourseMode.has_verified_mode(modes): return redirect(reverse('dashboard')) # If a user has already paid, redirect them to the dashboard. if is_active and enrollment_mode in CourseMode.VERIFIED_MODES: return redirect(reverse('dashboard')) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(unicode(course_key), None) course = modulestore().get_course(course_key) context = { "course_modes_choose_url": reverse("course_modes_choose", kwargs={'course_id': course_key.to_deprecated_string()}), "modes": modes, "course_name": course.display_name_with_default, "course_org": course.display_org_with_default, "course_num": course.display_number_with_default, "chosen_price": chosen_price, "error": error, "can_audit": "audit" in modes, "responsive": True } if "verified" in modes: context["suggested_prices"] = [ decimal.Decimal(x.strip()) for x in modes["verified"].suggested_prices.split(",") if x.strip() ] context["currency"] = modes["verified"].currency.upper() context["min_price"] = modes["verified"].min_price context["verified_name"] = modes["verified"].name context["verified_description"] = modes["verified"].description return render_to_response("course_modes/choose.html", context)