def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) student_id = kwargs.get('student_id') if not course_home_mfe_progress_tab_is_active(course_key): raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) is_staff = bool(has_access(request.user, 'staff', course_key)) student = self._get_student_user(request, course_key, student_id, is_staff) username = get_enterprise_learner_generic_name( request) or student.username course = get_course_with_access(student, 'load', course_key, check_if_enrolled=False) course_overview = CourseOverview.get_from_id(course_key) enrollment = CourseEnrollment.get_enrollment(student, course_key) enrollment_mode = getattr(enrollment, 'mode', None) if not (enrollment and enrollment.is_active) and not is_staff: return Response('User not enrolled.', status=401) # The block structure is used for both the course_grade and has_scheduled content fields # So it is called upfront and reused for optimization purposes collected_block_structure = get_block_structure_manager( course_key).get_collected() course_grade = CourseGradeFactory().read( student, collected_block_structure=collected_block_structure) # Get has_scheduled_content data transformers = BlockStructureTransformers() transformers += [ start_date.StartDateTransformer(), ContentTypeGateTransformer() ] usage_key = collected_block_structure.root_block_usage_key course_blocks = get_course_blocks( student, usage_key, transformers=transformers, collected_block_structure=collected_block_structure, include_has_scheduled_content=True) has_scheduled_content = course_blocks.get_xblock_field( usage_key, 'has_scheduled_content') # Get user_has_passing_grade data user_has_passing_grade = False if not student.is_anonymous: user_grade = course_grade.percent user_has_passing_grade = user_grade >= course.lowest_passing_grade descriptor = modulestore().get_course(course_key) grading_policy = descriptor.grading_policy verification_status = IDVerificationService.user_status(student) verification_link = None if verification_status['status'] is None or verification_status[ 'status'] == 'expired': verification_link = IDVerificationService.get_verify_location( course_id=course_key) elif verification_status['status'] == 'must_reverify': verification_link = IDVerificationService.get_verify_location( course_id=course_key) verification_data = { 'link': verification_link, 'status': verification_status['status'], 'status_date': verification_status['status_date'], } access_expiration = get_access_expiration_data(request.user, course_overview) data = { 'access_expiration': access_expiration, 'certificate_data': get_cert_data(student, course, enrollment_mode, course_grade), 'completion_summary': get_course_blocks_completion_summary(course_key, student), 'course_grade': course_grade, 'credit_course_requirements': credit_course_requirements(course_key, student), 'end': course.end, 'enrollment_mode': enrollment_mode, 'grading_policy': grading_policy, 'has_scheduled_content': has_scheduled_content, 'section_scores': list(course_grade.chapter_grades.values()), 'studio_url': get_studio_url(course, 'settings/grading'), 'username': username, 'user_has_passing_grade': user_has_passing_grade, 'verification_data': verification_data, } context = self.get_serializer_context() context['staff_access'] = is_staff context['course_blocks'] = course_blocks context['course_key'] = course_key # course_overview and enrollment will be used by VerifiedModeSerializer context['course_overview'] = course_overview context['enrollment'] = enrollment serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def test_waffle_flag_disabled(self, enrollment_mode): CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) response = self.client.get(self.url) assert response.status_code == 404
def _setup_user(self): self.user = UserFactory.create() CourseEnrollment.enroll(self.user, self.course_key)
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) course_usage_key = modulestore().make_course_usage_key(course_key) if course_home_legacy_is_active(course_key): raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) masquerade_object, request.user = setup_masquerade( request, course_key, staff_access=has_access(request.user, 'staff', course_key), reset_masquerade_data=True, ) user_is_masquerading = is_masquerading( request.user, course_key, course_masquerade=masquerade_object) course_overview = get_course_overview_or_404(course_key) enrollment = CourseEnrollment.get_enrollment(request.user, course_key) enrollment_mode = getattr(enrollment, 'mode', None) allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled( course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] if course_home_legacy_is_active(course.id): dates_tab_link = request.build_absolute_uri( reverse('dates', args=[course.id])) else: dates_tab_link = get_learning_mfe_home_url(course_key=course.id, view_name='dates') # Set all of the defaults access_expiration = None cert_data = None course_blocks = None course_goals = {'goal_options': [], 'selected_goal': None} course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) dates_widget = { 'course_date_blocks': [], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } enroll_alert = { 'can_enroll': True, 'extra_text': None, } handouts_html = None offer_data = None resume_course = { 'has_visited_course': False, 'url': None, } welcome_message_html = None is_enrolled = enrollment and enrollment.is_active is_staff = bool(has_access(request.user, 'staff', course_key)) show_enrolled = is_enrolled or is_staff if show_enrolled: course_blocks = get_course_outline_block_tree( request, course_key_string, request.user) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) dates_widget['course_date_blocks'] = [ block for block in date_blocks if not isinstance(block, TodaysDate) ] handouts_html = get_course_info_section(request, request.user, course, 'handouts') welcome_message_html = get_current_update_for_user(request, course) offer_data = generate_offer_data(request.user, course_overview) access_expiration = get_access_expiration_data( request.user, course_overview) cert_data = get_cert_data(request.user, course, enrollment.mode) if is_enrolled else None if COURSE_GOALS_NUMBER_OF_DAYS_GOALS.is_enabled(): if (is_enrolled and ENABLE_COURSE_GOALS.is_enabled(course_key)): course_goals = {'selected_goal': None} selected_goal = get_course_goal(request.user, course_key) if selected_goal: course_goals['selected_goal'] = { 'days_per_week': selected_goal.days_per_week, 'subscribed_to_reminders': selected_goal.subscribed_to_reminders, } else: # Only show the set course goal message for enrolled, unverified # users in a course that allows for verified statuses. is_already_verified = CourseEnrollment.is_enrolled_as_verified( request.user, course_key) if not is_already_verified and has_course_goal_permission( request, course_key_string, {'is_enrolled': is_enrolled}): course_goals = { 'goal_options': valid_course_goals_ordered(include_unsure=True), 'selected_goal': None } selected_goal = get_course_goal(request.user, course_key) if selected_goal: course_goals['selected_goal'] = { 'key': selected_goal.goal_key, 'text': get_course_goal_text(selected_goal.goal_key), } try: resume_block = get_key_to_last_completed_block( request.user, course.id) resume_course['has_visited_course'] = True resume_path = reverse('jump_to', kwargs={ 'course_id': course_key_string, 'location': str(resume_block) }) resume_course['url'] = request.build_absolute_uri(resume_path) except UnavailableCompletionData: start_block = get_start_block(course_blocks) resume_course['url'] = start_block['lms_web_url'] elif allow_public_outline or allow_public or user_is_masquerading: course_blocks = get_course_outline_block_tree( request, course_key_string, None) if allow_public or user_is_masquerading: handouts_html = get_course_info_section( request, request.user, course, 'handouts') if not show_enrolled: if CourseMode.is_masters_only(course_key): enroll_alert['can_enroll'] = False enroll_alert['extra_text'] = _( 'Please contact your degree administrator or ' '{platform_name} Support if you have questions.').format( platform_name=settings.PLATFORM_NAME) elif course.invitation_only: enroll_alert['can_enroll'] = False # Sometimes there are sequences returned by Course Blocks that we # don't actually want to show to the user, such as when a sequence is # composed entirely of units that the user can't access. The Learning # Sequences API knows how to roll this up, so we use it determine which # sequences we should remove from course_blocks. # # The long term goal is to remove the Course Blocks API call entirely, # so this is a tiny first step in that migration. if course_blocks and learning_sequences_api_available(course_key): user_course_outline = get_user_course_outline( course_key, request.user, datetime.now(tz=timezone.utc)) available_seq_ids = { str(usage_key) for usage_key in user_course_outline.sequences } # course_blocks is a reference to the root of the course, so we go # through the chapters (sections) to look for sequences to remove. for chapter_data in course_blocks['children']: chapter_data['children'] = [ seq_data for seq_data in chapter_data['children'] if (seq_data['id'] in available_seq_ids or # Edge case: Sometimes we have weird course structures. # We expect only sequentials here, but if there is # another type, just skip it (don't filter it out). seq_data['type'] != 'sequential') ] if 'children' in chapter_data else [] user_has_passing_grade = False if not request.user.is_anonymous: user_grade = CourseGradeFactory().read(request.user, course) if user_grade: user_has_passing_grade = user_grade.passed data = { 'access_expiration': access_expiration, 'cert_data': cert_data, 'course_blocks': course_blocks, 'course_goals': course_goals, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enroll_alert': enroll_alert, 'enrollment_mode': enrollment_mode, 'handouts_html': handouts_html, 'has_ended': course.has_ended(), 'offer': offer_data, 'resume_course': resume_course, 'user_has_passing_grade': user_has_passing_grade, 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_overview'] = course_overview context['enable_links'] = show_enrolled or allow_public context['enrollment'] = enrollment serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def assert_enrollment(self, mode): """ Assert that the student's enrollment has the correct mode. """ enrollment = CourseEnrollment.get_enrollment(self.student, self.course.id) self.assertEqual(enrollment.mode, mode)
def setUp(self): super().setUp() self.user = UserFactory.create() self.set_up_course() CourseEnrollment.enroll(self.user, self.course.id)
def assert_enrollment_status_hash_cached(self, user, expected_value): assert cache.get( CourseEnrollment.enrollment_status_hash_cache_key( user)) == expected_value
def test_not_enrolled_non_public_course(self): """ Verify behaviour when accessing course blocks for a non-public course as a user not enrolled in course. """ CourseEnrollment.unenroll(self.user, self.course_key) self.verify_response(403)
def test_display_upgrade_message_if_audit_and_deadline_not_passed(self): CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) self.assert_upgrade_message_displayed()
def test_streak_data_in_response(self): """ Test that metadata endpoint returns data for the streak celebration """ CourseEnrollment.enroll(self.user, self.course.id, 'audit') response = self.client.get(self.url, content_type='application/json') celebrations = response.json()['celebrations'] assert 'streak_length_to_celebrate' in celebrations
def setUp(self): super().setUp() self.enrollment = CourseEnrollment.enroll(self.user, self.course.id, 'verified')
def test_course_metadata(self, logged_in, enrollment_mode, enable_anonymous, is_microfrontend_enabled_for_user): is_microfrontend_enabled_for_user.return_value = True check_public_access = mock.Mock() check_public_access.return_value = enable_anonymous with mock.patch( 'lms.djangoapps.courseware.access_utils.check_public_access', check_public_access): if not logged_in: self.client.logout() if enrollment_mode: CourseEnrollment.enroll(self.user, self.course.id, enrollment_mode) if enrollment_mode == 'verified': cert = GeneratedCertificateFactory.create( user=self.user, course_id=self.course.id, status='downloadable', mode='verified', ) response = self.client.get(self.url) assert response.status_code == 200 if enrollment_mode: enrollment = response.data['enrollment'] assert enrollment_mode == enrollment['mode'] assert enrollment['is_active'] assert len(response.data['tabs']) == 6 found = False for tab in response.data['tabs']: if tab['type'] == 'external_link': assert tab[ 'url'] != 'http://hidden.com', "Hidden tab is not hidden" if tab['url'] == 'http://zombo.com': found = True assert found, 'external link not in course tabs' assert not response.data['user_has_passing_grade'] if enrollment_mode == 'audit': assert response.data['verify_identity_url'] is None assert response.data['verification_status'] == 'none' # lint-amnesty, pylint: disable=literal-comparison assert response.data['linkedin_add_to_profile_url'] is None else: assert response.data['certificate_data'][ 'cert_status'] == 'earned_but_not_available' expected_verify_identity_url = IDVerificationService.get_verify_location( course_id=self.course.id) # The response contains an absolute URL so this is only checking the path of the final assert expected_verify_identity_url in response.data[ 'verify_identity_url'] assert response.data['verification_status'] == 'none' # lint-amnesty, pylint: disable=literal-comparison request = RequestFactory().request() cert_url = get_certificate_url(course_id=self.course.id, uuid=cert.verify_uuid) linkedin_url_params = { 'name': '{platform_name} Verified Certificate for {course_name}' .format( platform_name=settings.PLATFORM_NAME, course_name=self.course.display_name, ), 'certUrl': request.build_absolute_uri(cert_url), # default value from the LinkedInAddToProfileConfigurationFactory company_identifier 'organizationId': 1337, 'certId': cert.verify_uuid, 'issueYear': cert.created_date.year, 'issueMonth': cert.created_date.month, } expected_linkedin_url = ( 'https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME&{params}' .format(params=urlencode(linkedin_url_params))) assert response.data[ 'linkedin_add_to_profile_url'] == expected_linkedin_url elif enable_anonymous and not logged_in: # multiple checks use this handler check_public_access.assert_called() assert response.data['enrollment']['mode'] is None assert response.data['can_load_courseware']['has_access'] else: assert not response.data['can_load_courseware']['has_access']
def _attach_course_run_is_enrolled(self, run_mode): run_mode['is_enrolled'] = CourseEnrollment.is_enrolled( self.user, self.course_run_key)
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) course_usage_key = modulestore().make_course_usage_key(course_key) if not course_home_mfe_outline_tab_is_active(course_key): raise Http404 # Enable NR tracing for this view based on course monitoring_utils.set_custom_attribute('course_id', course_key_string) monitoring_utils.set_custom_attribute('user_id', request.user.id) monitoring_utils.set_custom_attribute('is_staff', request.user.is_staff) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=False) _masquerade, request.user = setup_masquerade( request, course_key, staff_access=has_access(request.user, 'staff', course_key), reset_masquerade_data=True, ) course_overview = CourseOverview.get_from_id(course_key) enrollment = CourseEnrollment.get_enrollment(request.user, course_key) allow_anonymous = COURSE_ENABLE_UNENROLLED_ACCESS_FLAG.is_enabled( course_key) allow_public = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC allow_public_outline = allow_anonymous and course.course_visibility == COURSE_VISIBILITY_PUBLIC_OUTLINE # User locale settings user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] dates_tab_link = request.build_absolute_uri( reverse('dates', args=[course.id])) if course_home_mfe_dates_tab_is_active(course.id): dates_tab_link = get_microfrontend_url(course_key=course.id, view_name='dates') # Set all of the defaults access_expiration = None course_blocks = None course_goals = {'goal_options': [], 'selected_goal': None} course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) dates_widget = { 'course_date_blocks': [], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } enroll_alert = { 'can_enroll': True, 'extra_text': None, } handouts_html = None offer_data = None resume_course = { 'has_visited_course': False, 'url': None, } welcome_message_html = None is_enrolled = enrollment and enrollment.is_active is_staff = bool(has_access(request.user, 'staff', course_key)) show_enrolled = is_enrolled or is_staff if show_enrolled: course_blocks = get_course_outline_block_tree( request, course_key_string, request.user) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) dates_widget['course_date_blocks'] = [ block for block in date_blocks if not isinstance(block, TodaysDate) ] handouts_html = get_course_info_section(request, request.user, course, 'handouts') welcome_message_html = get_current_update_for_user(request, course) offer_data = generate_offer_data(request.user, course_overview) access_expiration = get_access_expiration_data( request.user, course_overview) # Only show the set course goal message for enrolled, unverified # users in a course that allows for verified statuses. is_already_verified = CourseEnrollment.is_enrolled_as_verified( request.user, course_key) if not is_already_verified and has_course_goal_permission( request, course_key_string, {'is_enrolled': is_enrolled}): course_goals = { 'goal_options': valid_course_goals_ordered(include_unsure=True), 'selected_goal': None } selected_goal = get_course_goal(request.user, course_key) if selected_goal: course_goals['selected_goal'] = { 'key': selected_goal.goal_key, 'text': get_course_goal_text(selected_goal.goal_key), } try: resume_block = get_key_to_last_completed_block( request.user, course.id) resume_course['has_visited_course'] = True resume_path = reverse('jump_to', kwargs={ 'course_id': course_key_string, 'location': str(resume_block) }) resume_course['url'] = request.build_absolute_uri(resume_path) except UnavailableCompletionData: start_block = get_start_block(course_blocks) resume_course['url'] = start_block['lms_web_url'] elif allow_public_outline or allow_public: course_blocks = get_course_outline_block_tree( request, course_key_string, None) if allow_public: handouts_html = get_course_info_section( request, request.user, course, 'handouts') if not show_enrolled: if CourseMode.is_masters_only(course_key): enroll_alert['can_enroll'] = False enroll_alert['extra_text'] = _( 'Please contact your degree administrator or ' 'edX Support if you have questions.') elif course.invitation_only: enroll_alert['can_enroll'] = False data = { 'access_expiration': access_expiration, 'course_blocks': course_blocks, 'course_goals': course_goals, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enroll_alert': enroll_alert, 'handouts_html': handouts_html, 'has_ended': course.has_ended(), 'offer': offer_data, 'resume_course': resume_course, 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_overview'] = course_overview context['enable_links'] = show_enrolled or allow_public context['enrollment'] = enrollment serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def setUpTestData(cls): # lint-amnesty, pylint: disable=super-method-not-called """Set up and enroll our fake user in the course.""" cls.user = UserFactory(password=TEST_PASSWORD) CourseEnrollment.enroll(cls.user, cls.course.id) cls.site = Site.objects.get_current()
def setUpTestData(cls): """Set up and enroll our fake user in the course.""" super(CourseHomePageTestCase, cls).setUpTestData() cls.staff_user = StaffFactory(course_key=cls.course.id, password=TEST_PASSWORD) cls.user = UserFactory(password=TEST_PASSWORD) CourseEnrollment.enroll(cls.user, cls.course.id)
def setUp(self): super().setUp() self.users = [UserFactory.create() for _ in range(12)] self.set_up_course() for user in self.users: CourseEnrollment.enroll(user, self.course.id)
def test_no_upgrade_message_if_verified_track(self): CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) self.assert_upgrade_message_not_displayed()
def test_enrollment_status_hash_cache_key(self): username = '******' user = UserFactory(username=username) expected = 'enrollment_status_hash_' + username assert CourseEnrollment.enrollment_status_hash_cache_key( user) == expected
def test_no_upgrade_message_if_flag_disabled(self): self.flag.everyone = False self.flag.save() CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) # lint-amnesty, pylint: disable=no-member self.assert_upgrade_message_not_displayed()
def post(self, request, *args, **kwargs): # lint-amnesty, pylint: disable=unused-argument """ Attempt to enroll the user. """ user = request.user valid, course_key, error = self._is_data_valid(request) if not valid: return DetailResponse(error, status=HTTP_406_NOT_ACCEPTABLE) embargo_response = embargo_api.get_embargo_response( request, course_key, user) if embargo_response: return embargo_response # Don't do anything if an enrollment already exists course_id = six.text_type(course_key) enrollment = CourseEnrollment.get_enrollment(user, course_key) if enrollment and enrollment.is_active: msg = Messages.ENROLLMENT_EXISTS.format(course_id=course_id, username=user.username) return DetailResponse(msg, status=HTTP_409_CONFLICT) # Check to see if enrollment for this course is closed. course = courses.get_course(course_key) if CourseEnrollment.is_enrollment_closed(user, course): msg = Messages.ENROLLMENT_CLOSED.format(course_id=course_id) log.info(u'Unable to enroll user %s in closed course %s.', user.id, course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE) # If there is no audit or honor course mode, this most likely # a Prof-Ed course. Return an error so that the JS redirects # to track selection. honor_mode = CourseMode.mode_for_course(course_key, CourseMode.HONOR) audit_mode = CourseMode.mode_for_course(course_key, CourseMode.AUDIT) # Check to see if the User has an entitlement and enroll them if they have one for this course if CourseEntitlement.check_for_existing_entitlement_and_enroll( user=user, course_run_key=course_key): return JsonResponse( { 'redirect_destination': reverse('courseware', args=[six.text_type(course_id)]), }, ) # Accept either honor or audit as an enrollment mode to # maintain backwards compatibility with existing courses default_enrollment_mode = audit_mode or honor_mode course_name = None course_announcement = None if course is not None: course_name = course.display_name course_announcement = course.announcement if default_enrollment_mode: msg = Messages.ENROLL_DIRECTLY.format(username=user.username, course_id=course_id) if not default_enrollment_mode.sku: # If there are no course modes with SKUs, return a different message. msg = Messages.NO_SKU_ENROLLED.format( enrollment_mode=default_enrollment_mode.slug, course_id=course_id, course_name=course_name, username=user.username, announcement=course_announcement) log.info(msg) self._enroll(course_key, user, default_enrollment_mode.slug) mode = CourseMode.AUDIT if audit_mode else CourseMode.HONOR SAILTHRU_AUDIT_PURCHASE.send(sender=None, user=user, mode=mode, course_id=course_id) self._handle_marketing_opt_in(request, course_key, user) return DetailResponse(msg) else: msg = Messages.NO_DEFAULT_ENROLLMENT_MODE.format( course_id=course_id) return DetailResponse(msg, status=HTTP_406_NOT_ACCEPTABLE)
def test_display_upgrade_message_if_audit_and_deadline_not_passed(self): CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) # lint-amnesty, pylint: disable=no-member self.assert_upgrade_message_displayed()
def create_credit_request(course_key, provider_id, username): """ Initiate a request for credit from a credit provider. This will return the parameters that the user's browser will need to POST to the credit provider. It does NOT calculate the signature. Only users who are eligible for credit (have satisfied all credit requirements) are allowed to make requests. A provider can be configured either with *integration enabled* or not. If automatic integration is disabled, this method will simply return a URL to the credit provider and method set to "GET", so the student can visit the URL and request credit directly. No database record will be created to track these requests. If automatic integration *is* enabled, then this will also return the parameters that the user's browser will need to POST to the credit provider. These parameters will be digitally signed using a secret key shared with the credit provider. A database record will be created to track the request with a 32-character UUID. The returned dictionary can be used by the user's browser to send a POST request to the credit provider. If a pending request already exists, this function should return a request description with the same UUID. (Other parameters, such as the user's full name may be different than the original request). If a completed request (either accepted or rejected) already exists, this function will raise an exception. Users are not allowed to make additional requests once a request has been completed. Arguments: course_key (CourseKey): The identifier for the course. provider_id (str): The identifier of the credit provider. username (str): The user initiating the request. Returns: dict Raises: UserIsNotEligible: The user has not satisfied eligibility requirements for credit. CreditProviderNotConfigured: The credit provider has not been configured for this course. RequestAlreadyCompleted: The user has already submitted a request and received a response from the credit provider. Example Usage: >>> create_credit_request(course.id, "hogwarts", "ron") { "url": "https://credit.example.com/request", "method": "POST", "parameters": { "request_uuid": "557168d0f7664fe59097106c67c3f847", "timestamp": 1434631630, "course_org": "HogwartsX", "course_num": "Potions101", "course_run": "1T2015", "final_grade": "0.95", "user_username": "******", "user_email": "*****@*****.**", "user_full_name": "Ron Weasley", "user_mailing_address": "", "user_country": "US", "signature": "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI=" } } """ try: user_eligibility = CreditEligibility.objects.select_related( 'course').get(username=username, course__course_key=course_key) credit_course = user_eligibility.course credit_provider = CreditProvider.objects.get(provider_id=provider_id) except CreditEligibility.DoesNotExist: log.warning( u'User "%s" tried to initiate a request for credit in course "%s", ' u'but the user is not eligible for credit', username, course_key) raise UserIsNotEligible # lint-amnesty, pylint: disable=raise-missing-from except CreditProvider.DoesNotExist: log.error(u'Credit provider with ID "%s" has not been configured.', provider_id) raise CreditProviderNotConfigured # lint-amnesty, pylint: disable=raise-missing-from # Check if we've enabled automatic integration with the credit # provider. If not, we'll show the user a link to a URL # where the user can request credit directly from the provider. # Note that we do NOT track these requests in our database, # since the state would always be "pending" (we never hear back). if not credit_provider.enable_integration: return { "url": credit_provider.provider_url, "method": "GET", "parameters": {} } else: # If automatic credit integration is enabled, then try # to retrieve the shared signature *before* creating the request. # That way, if there's a misconfiguration, we won't have requests # in our system that we know weren't sent to the provider. shared_secret_key = get_shared_secret_key(credit_provider.provider_id) check_keys_exist(shared_secret_key, credit_provider.provider_id) if isinstance(shared_secret_key, list): # if keys exist, and keys are stored as a list # then we know at least 1 is available for [0] shared_secret_key = [key for key in shared_secret_key if key][0] # Initiate a new request if one has not already been created credit_request, created = CreditRequest.objects.get_or_create( course=credit_course, provider=credit_provider, username=username, ) # Check whether we've already gotten a response for a request, # If so, we're not allowed to issue any further requests. # Skip checking the status if we know that we just created this record. if not created and credit_request.status != "pending": log.warning(( u'Cannot initiate credit request because the request with UUID "%s" ' u'exists with status "%s"'), credit_request.uuid, credit_request.status) raise RequestAlreadyCompleted if created: credit_request.uuid = uuid.uuid4().hex # Retrieve user account and profile info user = User.objects.select_related('profile').get(username=username) # Retrieve the final grade from the eligibility table try: final_grade = CreditRequirementStatus.objects.get( username=username, requirement__namespace="grade", requirement__name="grade", requirement__course__course_key=course_key, status="satisfied").reason["final_grade"] # NOTE (CCB): Limiting the grade to seven characters is a hack for ASU. if len(six.text_type(final_grade)) > 7: final_grade = u'{:.5f}'.format(final_grade) else: final_grade = six.text_type(final_grade) except (CreditRequirementStatus.DoesNotExist, TypeError, KeyError): msg = u'Could not retrieve final grade from the credit eligibility table for ' \ u'user [{user_id}] in course [{course_key}].'.format(user_id=user.id, course_key=course_key) log.exception(msg) raise UserIsNotEligible(msg) # lint-amnesty, pylint: disable=raise-missing-from # Getting the students's enrollment date course_enrollment = CourseEnrollment.get_enrollment(user, course_key) enrollment_date = course_enrollment.created if course_enrollment else "" # Getting the student's course completion date completion_date = get_last_exam_completion_date(course_key, username) parameters = { "request_uuid": credit_request.uuid, "timestamp": to_timestamp(datetime.datetime.now(pytz.UTC)), "course_org": course_key.org, "course_num": course_key.course, "course_run": course_key.run, "enrollment_timestamp": to_timestamp(enrollment_date) if enrollment_date else "", "course_completion_timestamp": to_timestamp(completion_date) if completion_date else "", "final_grade": final_grade, "user_username": user.username, "user_email": user.email, "user_full_name": user.profile.name, "user_mailing_address": "", "user_country": (user.profile.country.code if user.profile.country.code is not None else ""), } credit_request.parameters = parameters credit_request.save() if created: log.info(u'Created new request for credit with UUID "%s"', credit_request.uuid) else: log.info( u'Updated request for credit with UUID "%s" so the user can re-issue the request', credit_request.uuid) # Sign the parameters using a secret key we share with the credit provider. parameters["signature"] = signature(parameters, shared_secret_key) return { "url": credit_provider.provider_url, "method": "POST", "parameters": parameters }
def test_course_messaging(self): """ Ensure that the following four use cases work as expected 1) Anonymous users are shown a course message linking them to the login page 2) Unenrolled users are shown a course message allowing them to enroll 3) Enrolled users who show up on the course page after the course has begun are not shown a course message. 4) Enrolled users who show up on the course page after the course has begun will see the course expiration banner if course duration limits are on for the course. 5) Enrolled users who show up on the course page before the course begins are shown a message explaining when the course starts as well as a call to action button that allows them to add a calendar event. """ # Verify that anonymous users are shown a login link in the course message url = course_home_url(self.course) response = self.client.get(url) self.assertContains(response, TEST_COURSE_HOME_MESSAGE) self.assertContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS) # Verify that unenrolled users are shown an enroll call to action message user = self.create_user_for_course(self.course, CourseUserType.UNENROLLED) url = course_home_url(self.course) response = self.client.get(url) self.assertContains(response, TEST_COURSE_HOME_MESSAGE) self.assertContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED) # Verify that enrolled users are not shown any state warning message when enrolled and course has begun. CourseEnrollment.enroll(user, self.course.id) url = course_home_url(self.course) response = self.client.get(url) self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_ANONYMOUS) self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_UNENROLLED) self.assertNotContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START) # Verify that enrolled users are shown the course expiration banner if content gating is enabled # We use .save() explicitly here (rather than .objects.create) in order to force the # cache to refresh. config = CourseDurationLimitConfig(course=CourseOverview.get_from_id( self.course.id), enabled=True, enabled_as_of=datetime(2018, 1, 1, tzinfo=UTC)) config.save() url = course_home_url(self.course) response = self.client.get(url) bannerText = get_expiration_banner_text(user, self.course) self.assertContains(response, bannerText, html=True) # Verify that enrolled users are not shown the course expiration banner if content gating is disabled config.enabled = False config.save() url = course_home_url(self.course) response = self.client.get(url) bannerText = get_expiration_banner_text(user, self.course) self.assertNotContains(response, bannerText, html=True) # Verify that enrolled users are shown 'days until start' message before start date future_course = self.create_future_course() CourseEnrollment.enroll(user, future_course.id) url = course_home_url(future_course) response = self.client.get(url) self.assertContains(response, TEST_COURSE_HOME_MESSAGE) self.assertContains(response, TEST_COURSE_HOME_MESSAGE_PRE_START)
def test_handouts(self): CourseEnrollment.enroll(self.user, self.course.id) self.store.create_item(self.user.id, self.course.id, 'course_info', 'handouts', fields={'data': '<p>Hi</p>'}) assert self.client.get(self.url).data['handouts_html'] == '<p>Hi</p>'
def test_no_upgrade_message_if_not_enrolled(self): assert len(CourseEnrollment.enrollments_for_user(self.user)) == 0 self.assert_upgrade_message_not_displayed()
def test_verified_mode(self): enrollment = CourseEnrollment.enroll(self.user, self.course.id) CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2018, 1, 1)) response = self.client.get(self.url) assert response.data['verified_mode'] == {'access_expiration_date': (enrollment.created + MIN_DURATION), 'currency': 'USD', 'currency_symbol': '$', 'price': 149, 'sku': 'ABCD1234', 'upgrade_url': '/dashboard'}
def test_no_upgrade_message_if_verified_track(self): CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) # lint-amnesty, pylint: disable=no-member self.assert_upgrade_message_not_displayed()
def title(self): enrollment = CourseEnrollment.get_enrollment(self.user, self.course_id) if enrollment and self.course.end and enrollment.created > self.course.end: return ugettext_lazy('Enrollment Date') return ugettext_lazy('Course Starts')
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. The course must not be a CCX. Certificate must be in the 'unavailable', 'error', 'deleted' or 'generating' state. If a student has a passing grade or is in the whitelist table for the course a request will be made for a new cert. If a student 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 """ if hasattr(course_id, 'ccx'): LOGGER.warning( (u"Cannot create certificate generation task for user %s " u"in the course '%s'; " u"certificates are not allowed for CCX courses."), student.id, six.text_type(course_id)) return None valid_statuses = [ status.generating, status.unavailable, status.deleted, status.error, status.notpassing, status.downloadable, status.auditing, status.audit_passing, status.audit_notpassing, status.unverified, ] cert_status_dict = certificate_status_for_student(student, course_id) cert_status = cert_status_dict.get('status') download_url = cert_status_dict.get('download_url') cert = None if download_url: self._log_pdf_cert_generation_discontinued_warning( student.id, course_id, cert_status, download_url) return None if cert_status not in valid_statuses: LOGGER.warning( (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, six.text_type(course_id), cert_status, six.text_type(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() course_grade = CourseGradeFactory().read(student, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( student, course_id) mode_is_verified = enrollment_mode in GeneratedCertificate.VERIFIED_CERTS_MODES user_is_verified = IDVerificationService.user_is_verified(student) cert_mode = enrollment_mode is_eligible_for_certificate = CourseMode.is_eligible_for_certificate( enrollment_mode, cert_status) if is_whitelisted and not is_eligible_for_certificate: # check if audit certificates are enabled for audit mode is_eligible_for_certificate = enrollment_mode != CourseMode.AUDIT or \ not settings.FEATURES['DISABLE_AUDIT_CERTIFICATES'] unverified = False # For credit mode generate verified certificate if cert_mode in (CourseMode.CREDIT_MODE, CourseMode.MASTERS): cert_mode = CourseMode.VERIFIED if template_file is not None: template_pdf = template_file elif mode_is_verified and user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}-verified.pdf".format( id=course_id) elif mode_is_verified and not user_is_verified: template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) if CourseMode.mode_for_course(course_id, CourseMode.HONOR): cert_mode = GeneratedCertificate.MODES.honor else: unverified = True else: # honor code and audit students template_pdf = "certificate-template-{id.org}-{id.course}.pdf".format( id=course_id) LOGGER.info(( 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," u"generate_pdf is: %s"), student.username, six.text_type(course_id), template_pdf, template_file, user_is_verified, mode_is_verified, generate_pdf) cert, created = GeneratedCertificate.objects.get_or_create( user=student, course_id=course_id) cert.mode = cert_mode cert.user = student cert.grade = course_grade.percent cert.course_id = course_id cert.name = profile_name cert.download_url = '' # Strip HTML from grade range label grade_contents = forced_grade or course_grade.letter_grade 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, six.text_type(course_id), grade_contents, six.text_type(exc)) # Log if the student is whitelisted if is_whitelisted: LOGGER.info(u"Student %s is whitelisted in '%s'", student.id, six.text_type(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 = status.audit_passing if passing else status.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, six.text_type(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, six.text_type(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, six.text_type(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)