def test_add_entitlement_and_upgrade_audit_enrollment_with_dynamic_deadline( self, mock_get_course_runs): """ Verify that if an entitlement is added for a user, if the user has one upgradeable enrollment that enrollment is upgraded to the mode of the entitlement and linked to the entitlement regardless of dynamic upgrade deadline being set. """ DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True) course = CourseFactory.create(self_paced=True) course_uuid = uuid.uuid4() course_mode = CourseModeFactory( course_id=course.id, mode_slug=CourseMode.VERIFIED, # This must be in the future to ensure it is returned by downstream code. expiration_datetime=now() + timedelta(days=1)) # Set up Entitlement entitlement_data = self._get_data_set(self.user, str(course_uuid)) mock_get_course_runs.return_value = [{'key': str(course.id)}] # Add an audit course enrollment for user. enrollment = CourseEnrollment.enroll(self.user, course.id, mode=CourseMode.AUDIT) # Set an upgrade schedule so that dynamic upgrade deadlines are used ScheduleFactory.create( enrollment=enrollment, upgrade_deadline=course_mode.expiration_datetime + timedelta(days=-3)) # The upgrade should complete and ignore the deadline 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 = CourseEnrollment.enrollment_mode_for_user( self.user, course.id)[0] assert enrollment_mode == course_entitlement.mode assert course_entitlement.enrollment_course_run == enrollment assert results == CourseEntitlementSerializer(course_entitlement).data
def invalidate(self, mode=None, source=None): """ Invalidate Generated Certificate by marking it 'unavailable'. For additional information see the `_revoke_certificate()` function. Args: mode (String) - learner's current enrollment mode. May be none as the caller likely does not need to evaluate the mode before deciding to invalidate the cert. source (String) - source requesting invalidation of the certificate for tracking purposes """ if not mode: mode, __ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_id) log.info(f'Marking certificate as unavailable for {self.user.id} : {self.course_id} with mode {mode} from ' f'source {source}') self._revoke_certificate(status=CertificateStatuses.unavailable, mode=mode, source=source)
def _generate_certificate(user, course_key): """ Generate a certificate for this user, in this course run. """ # Retrieve the existing certificate for the learner if it exists existing_certificate = GeneratedCertificate.certificate_for_student( user, course_key) profile = UserProfile.objects.get(user=user) profile_name = profile.name course = modulestore().get_course(course_key, depth=0) course_grade = CourseGradeFactory().read(user, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( user, course_key) # Retain the `verify_uuid` from an existing certificate if possible, this will make it possible for the learner to # keep the existing URL to their certificate if existing_certificate and existing_certificate.verify_uuid: uuid = existing_certificate.verify_uuid else: uuid = uuid4().hex cert, created = GeneratedCertificate.objects.update_or_create( user=user, course_id=course_key, defaults={ 'user': user, 'course_id': course_key, 'mode': enrollment_mode, 'name': profile_name, 'status': CertificateStatuses.downloadable, 'grade': course_grade.percent, 'download_url': '', 'key': '', 'verify_uuid': uuid, 'error_reason': '' }) if created: created_msg = 'Certificate was created.' else: created_msg = 'Certificate already existed and was updated.' log.info( f'Generated certificate with status {cert.status} for {user.id} : {course_key}. {created_msg}' ) return cert
def _attach_course_run_upgrade_url(self, run_mode): required_mode_slug = run_mode['type'] enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_run_key) is_mode_mismatch = required_mode_slug != enrolled_mode_slug is_upgrade_required = is_mode_mismatch and CourseEnrollment.is_enrolled(self.user, self.course_run_key) if is_upgrade_required: # Requires that the ecommerce service be in use. required_mode = CourseMode.mode_for_course(self.course_run_key, required_mode_slug) ecommerce = EcommerceService() sku = getattr(required_mode, 'sku', None) if ecommerce.is_enabled(self.user) and sku: run_mode['upgrade_url'] = ecommerce.get_checkout_page_url(required_mode.sku) else: run_mode['upgrade_url'] = None else: run_mode['upgrade_url'] = None
def _can_generate_certificate_common(user, course_key): """ Check if a course certificate can be generated (created if it doesn't already exist, or updated if it does exist) for this user, in this course run. This method contains checks that are common to both allowlist and V2 regular course certificates. """ if CertificateInvalidation.has_certificate_invalidation(user, course_key): # The invalidation list prevents certificate generation log.info( f'{user.id} : {course_key} is on the certificate invalidation list. Certificate cannot be generated.' ) return False enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( user, course_key) if enrollment_mode is None: log.info( f'{user.id} : {course_key} does not have an enrollment. Certificate cannot be generated.' ) return False if not modes_api.is_eligible_for_certificate(enrollment_mode): log.info( f'{user.id} : {course_key} has an enrollment mode of {enrollment_mode}, which is not eligible for a ' f'certificate. Certificate cannot be generated.') return False if not IDVerificationService.user_is_verified(user): log.info( f'{user.id} does not have a verified id. Certificate cannot be generated for {course_key}.' ) return False if not _can_generate_certificate_for_status(user, course_key): return False course = _get_course(course_key) if not has_html_certificates_enabled(course): log.info( f'{course_key} does not have HTML certificates enabled. Certificate cannot be generated for ' f'{user.id}.') return False return 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): # We want the verified track even if the upgrade deadline has passed, since we # are determining what content to show the user, not whether the user can enroll # in the verified track. course_mode = CourseMode.verified_mode_for_course( course_key, include_expired=True) if not course_mode: course_mode = CourseMode.DEFAULT_MODE return Group(ENROLLMENT_GROUP_IDS[course_mode.slug]["id"], str(course_mode.name)) else: return None
def test_enroll(self, course_modes, enrollment_mode): # Create the course modes (if any) required for this test case self._create_course_modes(course_modes) enrollment = data.create_course_enrollment( self.user.username, six.text_type(self.course.id), enrollment_mode, True) assert CourseEnrollment.is_enrolled(self.user, self.course.id) course_mode, is_active = CourseEnrollment.enrollment_mode_for_user( self.user, self.course.id) assert is_active assert course_mode == enrollment_mode # Confirm the returned enrollment and the data match up. assert course_mode == enrollment['mode'] assert is_active == enrollment['is_active'] assert self.course.display_name_with_default == enrollment[ 'course_details']['course_name']
def validate_user_enrollment_is_valid(self, user, supplied_enrollment): """ Invalid states: user not enrolled in course enrollment mode from csv doesn't match actual user enrollment """ actual_enrollment_mode, user_enrolled = CourseEnrollment.enrollment_mode_for_user( user, self.course.id) if not user_enrolled: self.validation_errors.append('User ' + user.username + ' is not enrolled in this course.') return False if actual_enrollment_mode != supplied_enrollment.strip(): self.validation_errors.append('User ' + user.username + ' enrollment mismatch.') return False self.user_to_actual_enrollment_mode[user.id] = actual_enrollment_mode return True
def _can_set_cert_status_common(user, course_key): """ Determine whether we can set a custom (non-downloadable) cert status """ if _is_cert_downloadable(user, course_key): return False enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(user, course_key) if enrollment_mode is None: return False if not modes_api.is_eligible_for_certificate(enrollment_mode): return False course_overview = get_course_overview(course_key) if not has_html_certificates_enabled_from_course_overview(course_overview): return False return True
def _generate_certificate(user, course_id): """ Generate a certificate for this user, in this course run. """ profile = UserProfile.objects.get(user=user) profile_name = profile.name course = modulestore().get_course(course_id, depth=0) course_grade = CourseGradeFactory().read(user, course) enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(user, course_id) key = make_hashkey(random.random()) uuid = uuid4().hex cert, created = GeneratedCertificate.objects.update_or_create( user=user, course_id=course_id, defaults={ 'user': user, 'course_id': course_id, 'mode': enrollment_mode, 'name': profile_name, 'status': CertificateStatuses.downloadable, 'grade': course_grade.percent, 'download_url': '', 'key': key, 'verify_uuid': uuid } ) if created: created_msg = 'Certificate was created.' else: created_msg = 'Certificate already existed and was updated.' log.info( 'Generated certificate with status {status} for {user} : {course}. {created_msg}'.format( status=cert.status, user=cert.user.id, course=cert.course_id, created_msg=created_msg )) return cert
def test_successful_default_enrollment(self): # Create the course modes for mode in (CourseMode.DEFAULT_MODE_SLUG, 'verified'): CourseModeFactory.create(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': str(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=[str(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) assert mode == CourseMode.DEFAULT_MODE_SLUG assert is_active is True
def description(self): """ Returns a description for what experience changes a learner encounters when the course end date passes. Note that this currently contains 4 scenarios: 1. End date is in the future and learner is enrolled in a certificate earning mode 2. End date is in the future and learner is not enrolled at all or not enrolled in a certificate earning mode 3. End date is in the past 4. End date does not exist (and now neither does the description) """ if self.date and self.current_time <= 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 _('After this date, the course will be archived, which means you can review the ' 'course content but can no longer participate in graded assignments or work towards earning ' 'a certificate.') else: return _('After the course ends, the course content will be archived and no longer active.') elif self.date: return _('This course is archived, which means you can review course content but it is no longer active.') else: return ''
def test_already_enrolled_course_ended(self, mock_get_course_runs): """ Test that already enrolled user can still select a session while course has ended but upgrade deadline is in future. """ course_entitlement = CourseEntitlementFactory.create( user=self.user, mode=CourseMode.VERIFIED) mock_get_course_runs.return_value = self.return_values # Setup enrollment period to be in the past utc_now = datetime.now(UTC) self.course.start = utc_now - timedelta(days=15) self.course.end = utc_now - timedelta(days=1) self.course = self.update_course(self.course, self.user.id) CourseOverview.update_select_courses([self.course.id], force_update=True) CourseEnrollment.enroll(self.user, self.course.id, mode=CourseMode.AUDIT) url = reverse(self.ENTITLEMENTS_ENROLLMENT_NAMESPACE, args=[str(course_entitlement.uuid)]) 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 _listen_for_failing_grade(sender, user, course_id, grade, **kwargs): # pylint: disable=unused-argument """ Listen for a signal indicating that the user has failed a course run. If needed, mark the certificate as notpassing. """ if is_on_certificate_allowlist(user, course_id): log.info( f'User {user.id} is on the allowlist for {course_id}. The failing grade will not affect the ' f'certificate.') return cert = GeneratedCertificate.certificate_for_student(user, course_id) if cert is not None: if CertificateStatuses.is_passing_status(cert.status): enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( user, course_id) cert.mark_notpassing(mode=enrollment_mode, grade=grade.percent, source='notpassing_signal') log.info( f'Certificate marked not passing for {user.id} : {course_id} via failing grade' )
def certificate_info_for_user(user, course_id, grade, user_is_allowlisted, user_certificate): """ Returns the certificate info for a user for grade report. """ from common.djangoapps.student.models import CourseEnrollment certificate_is_delivered = 'N' certificate_type = 'N/A' status = certificate_status(user_certificate) certificate_generated = status['status'] == CertificateStatuses.downloadable can_have_certificate = CourseOverview.get_from_id(course_id).may_certify() enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user(user, course_id) mode_is_verified = enrollment_mode in CourseMode.VERIFIED_MODES user_is_verified = grade is not None and mode_is_verified eligible_for_certificate = 'Y' if (user_is_allowlisted or user_is_verified or certificate_generated) \ else 'N' if certificate_generated and can_have_certificate: certificate_is_delivered = 'Y' certificate_type = status['mode'] return [eligible_for_certificate, certificate_is_delivered, certificate_type]
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)}] # pylint: disable=no-member # Add an audit course enrollment for user. enrollment = CourseEnrollment.enroll( self.user, self.course.id, # pylint: disable=no-member 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 # pylint: disable=no-member ) 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 _check_enrollment(self, user, course_key): """Check whether the user has an active enrollment and has paid. If a user is enrolled in a paid course mode, we assume that the user has paid. Arguments: user (User): The user to check. course_key (CourseKey): The key of the course to check. Returns: Tuple `(has_paid, is_active)` indicating whether the user has paid and whether the user has an active account. """ enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user(user, course_key) has_paid = False if enrollment_mode is not None and is_active: all_modes = CourseMode.modes_for_course_dict(course_key, include_expired=True) course_mode = all_modes.get(enrollment_mode) has_paid = (course_mode and course_mode.min_price > 0) return (has_paid, bool(is_active))
def get_fulfillable_course_runs_for_entitlement(entitlement, course_runs): """ Looks through the list of course runs and returns the course runs that can be applied to the entitlement. Args: entitlement (CourseEntitlement): The CourseEntitlement to which a course run is to be applied. course_runs (list): List of course run that we would like to apply to the entitlement. Return: list: A list of sessions that a user can apply to the provided entitlement. """ enrollable_sessions = [] # Only retrieve list of published course runs that can still be enrolled and upgraded search_time = datetime.datetime.now(UTC) for course_run in course_runs: course_id = CourseKey.from_string(course_run.get('key')) (user_enrollment_mode, is_active) = CourseEnrollment.enrollment_mode_for_user( user=entitlement.user, course_id=course_id) is_enrolled_in_mode = is_active and (user_enrollment_mode == entitlement.mode) if (is_enrolled_in_mode and entitlement.enrollment_course_run and course_id == entitlement.enrollment_course_run.course_id): # User is enrolled in the course so we should include it in the list of enrollable sessions always # this will ensure it is available for the UI enrollable_sessions.append(course_run) elif not is_enrolled_in_mode and is_course_run_entitlement_fulfillable( course_id, entitlement, search_time): enrollable_sessions.append(course_run) enrollable_sessions.sort(key=lambda session: session.get('start')) return enrollable_sessions
def fire_ungenerated_certificate_task(user, course_key, expected_verification_status=None): """ Helper function to fire certificate generation task. Auto-generation of certificates is available for following course modes: 1- VERIFIED 2- CREDIT_MODE 3- PROFESSIONAL 4- NO_ID_PROFESSIONAL_MODE Certificate generation task is fired to either generate a certificate when there is no generated certificate for user in a particular course or update a certificate if it has 'unverified' status. Task is fired to attempt an update to a certificate with 'unverified' status as this method is called when a user is successfully verified, any certificate associated with such user can now be verified. NOTE: Purpose of restricting other course modes (HONOR and AUDIT) from auto-generation is to reduce traffic to workers. """ message = u'Entered into Ungenerated Certificate task for {user} : {course}' log.info(message.format(user=user.id, course=course_key)) if is_using_certificate_allowlist_and_is_on_allowlist(user, course_key): log.info( '{course} is using allowlist certificates, and the user {user} is on its allowlist. Attempt will be ' 'made to generate an allowlist certificate.'.format( course=course_key, user=user.id)) generate_allowlist_certificate_task(user, course_key) return True log.info( '{course} is not using allowlist certificates (or user {user} is not on its allowlist). The normal ' 'generation logic will be followed.'.format(course=course_key, user=user.id)) allowed_enrollment_modes_list = [ CourseMode.VERIFIED, CourseMode.CREDIT_MODE, CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.MASTERS, CourseMode.EXECUTIVE_EDUCATION, ] enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( user, course_key) cert = GeneratedCertificate.certificate_for_student(user, course_key) generate_learner_certificate = ( enrollment_mode in allowed_enrollment_modes_list and (cert is None or cert.status == 'unverified')) if generate_learner_certificate: kwargs = { 'student': six.text_type(user.id), 'course_key': six.text_type(course_key) } if expected_verification_status: kwargs['expected_verification_status'] = six.text_type( expected_verification_status) generate_certificate.apply_async(countdown=CERTIFICATE_DELAY_SECONDS, kwargs=kwargs) return True message = u'Certificate Generation task failed for {user} : {course}' log.info(message.format(user=user.id, course=course_key)) return False
def is_allowed(self): mode, is_active = CourseEnrollment.enrollment_mode_for_user( self.user, self.course_id) return (is_active and mode == 'verified' and self.verification_status in ('expired', 'none', 'must_reverify') and not settings.FEATURES.get('ENABLE_INTEGRITY_SIGNATURE'))
def set_due_date_extension(course, unit, student, due_date, actor=None, reason=''): """ Sets a due date extension. Raises: DashboardError if the unit or extended, due date is invalid or user is not enrolled in the course. """ mode, __ = CourseEnrollment.enrollment_mode_for_user( user=student, course_id=six.text_type(course.id)) if not mode: raise DashboardError( _("Could not find student enrollment in the course.")) # We normally set dates at the subsection level. But technically dates can be anywhere down the tree (and # usually are in self paced courses, where the subsection date gets propagated down). # So find all children that we need to set the date on, then set those dates. course_dates = api.get_dates_for_course(course.id, user=student) blocks_to_set = { unit } # always include the requested unit, even if it doesn't appear to have a due date now def visit(node): """ Visit a node. Checks to see if node has a due date and appends to `blocks_to_set` if it does. And recurses into children to search for nodes with due dates. """ if (node.location, 'due') in course_dates: blocks_to_set.add(node) for child in node.get_children(): visit(child) visit(unit) for block in blocks_to_set: if due_date: try: api.set_date_for_block(course.id, block.location, 'due', due_date, user=student, reason=reason, actor=actor) except api.MissingDateError: raise DashboardError( _(u"Unit {0} has no due date to extend.").format( unit.location)) except api.InvalidDateError: raise DashboardError( _("An extended due date must be later than the original due date." )) else: api.set_date_for_block(course.id, block.location, 'due', None, user=student, reason=reason, actor=actor)
def get(self, request, course_id, error=None): # lint-amnesty, pylint: disable=too-many-statements """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_client_ip(request)[0], url=request.path) if embargo_redirect: return redirect(embargo_redirect) enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) increment('track-selection.{}.{}'.format( enrollment_mode, 'active' if is_active else 'inactive')) increment('track-selection.views') if enrollment_mode is None: LOG.info( 'Rendering track selection for unenrolled user, referred by %s', request.META.get('HTTP_REFERER')) 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 'no-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") redirect_url = IDVerificationService.get_verify_location( course_id=course_key) 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 self._redirect_to_course_or_dashboard( course, course_key, request.user) # 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 self._redirect_to_course_or_dashboard( course, course_key, request.user) donation_for_course = request.session.get("donation_for_course", {}) chosen_price = donation_for_course.get(str(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 = six.moves.urllib.parse.urlencode( {'course_closed': enrollment_end_date}) return redirect('{}?{}'.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 university 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 = str(course_key) gated_content = ContentTypeGatingConfig.enabled_for_enrollment( user=request.user, course_key=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, "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, "content_gating_enabled": gated_content, "course_duration_limit_enabled": CourseDurationLimitConfig.enabled_for_enrollment( request.user, course), } context.update( get_experiment_user_metadata_context( course, request.user, )) title_content = '' if enrollment_mode: title_content = _( "Congratulations! You are now enrolled in {course_name}" ).format(course_name=course.display_name_with_default) 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() ] price_before_discount = verified_mode.min_price course_price = price_before_discount enterprise_customer = enterprise_customer_for_request(request) LOG.info( '[e-commerce calculate API] Going to hit the API for user [%s] linked to [%s] enterprise', request.user.username, enterprise_customer.get('name') if isinstance( enterprise_customer, dict) else None # Test Purpose ) if enterprise_customer and verified_mode.sku: course_price = get_course_final_price(request.user, verified_mode.sku, price_before_discount) context["currency"] = verified_mode.currency.upper() context["currency_symbol"] = get_currency_symbol( verified_mode.currency.upper()) context["min_price"] = course_price context["verified_name"] = verified_mode.name context["verified_description"] = verified_mode.description # if course_price is equal to price_before_discount then user doesn't entitle to any discount. if course_price != price_before_discount: context["price_before_discount"] = price_before_discount 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 language = get_language() context['track_links'] = get_verified_track_links(language) duration = get_user_course_duration(request.user, course) deadline = duration and get_user_course_expiration_date( request.user, course) if deadline: formatted_audit_access_date = strftime_localized_html( deadline, 'SHORT_DATE') context['audit_access_deadline'] = formatted_audit_access_date fbe_is_on = deadline and gated_content # Route to correct Track Selection page. # REV-2133 TODO Value Prop: remove waffle flag after testing is completed # and happy path version is ready to be rolled out to all users. if VALUE_PROP_TRACK_SELECTION_FLAG.is_enabled(): if not error: # TODO: Remove by executing REV-2355 if not enterprise_customer_for_request( request): # TODO: Remove by executing REV-2342 if fbe_is_on: return render_to_response("course_modes/fbe.html", context) else: return render_to_response("course_modes/unfbe.html", context) # If error or enterprise_customer, failover to old choose.html page return render_to_response("course_modes/choose.html", context)
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) 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) _, request.user = setup_masquerade(request, course_key, staff_access=has_access( request.user, 'staff', course_key), reset_masquerade_data=True) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) # 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( request.user, collected_block_structure=collected_block_structure) # Get has_scheduled_content data transformers = BlockStructureTransformers() transformers += [start_date.StartDateTransformer()] usage_key = collected_block_structure.root_block_usage_key course_blocks = get_course_blocks( request.user, 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 request.user.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(request.user) 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'], } data = { 'end': course.end, 'user_has_passing_grade': user_has_passing_grade, 'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade), 'completion_summary': get_course_blocks_completion_summary(course_key, request.user), 'course_grade': course_grade, 'has_scheduled_content': has_scheduled_content, 'section_scores': course_grade.chapter_grades.values(), 'enrollment_mode': enrollment_mode, 'grading_policy': grading_policy, 'studio_url': get_studio_url(course, 'settings/grading'), 'verification_data': verification_data, } context = self.get_serializer_context() context['staff_access'] = bool( has_access(request.user, 'staff', course)) context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def add_cert(self, student, course_id, 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 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( ("Cannot create certificate generation task for user %s " "in the course '%s'; " "certificates are not allowed for CCX courses."), student.id, str(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( ("Cannot create certificate generation task for user %s " "in the course '%s'; " "the certificate status '%s' is not one of %s."), student.id, str(course_id), cert_status, str(valid_statuses)) return None 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_key=course_id) 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 = modes_api.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(( "Certificate generated for student %s in the course: %s with template: %s. " "given template: %s, " "user is verified: %s, " "mode is verified: %s," "generate_pdf is: %s"), student.username, str(course_id), template_pdf, template_file, user_is_verified, mode_is_verified, generate_pdf) cert, __ = 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 passing = False try: grade_contents = lxml.html.fromstring( grade_contents).text_content() passing = True except (TypeError, XMLSyntaxError, ParserError) as exc: LOGGER.info(("Could not retrieve grade for student %s " "in the course '%s' " "because an exception occurred while parsing the " "grade contents '%s' as HTML. " "The exception was: '%s'"), student.id, str(course_id), grade_contents, str(exc)) # Check if the student is whitelisted if is_whitelisted: LOGGER.info("Student %s is whitelisted in '%s'", student.id, str(course_id)) passing = True # 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( "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( ("Student %s does not have a grade for '%s', " "so their certificate status has been set to '%s'. " "No certificate generation task was sent to the XQueue."), student.id, str(course_id), cert.status) return cert if unverified: cert.status = status.unverified cert.save() LOGGER.info( ("User %s has a verified enrollment in course %s " "but is missing ID verification. " "Certificate status has been set to unverified"), student.id, str(course_id), ) return cert # Finally, generate the certificate and send it off. return self._generate_cert(cert, student, grade_contents, template_pdf, generate_pdf)
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) # 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) _, request.user = setup_masquerade(request, course_key, staff_access=has_access( request.user, 'staff', course_key), reset_masquerade_data=True) user_timezone_locale = user_timezone_locale_prefs(request) user_timezone = user_timezone_locale['user_timezone'] transformers = BlockStructureTransformers() transformers += course_blocks_api.get_course_block_access_transformers( request.user) transformers += [ BlocksAPITransformer(None, None, depth=3), ] course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) course_grade = CourseGradeFactory().read(request.user, course) courseware_summary = course_grade.chapter_grades.values() verification_status = IDVerificationService.user_status(request.user) 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'], } data = { 'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade), 'courseware_summary': courseware_summary, 'credit_course_requirements': credit_course_requirements(course_key, request.user), 'credit_support_url': CREDIT_SUPPORT_URL, 'enrollment_mode': enrollment_mode, 'studio_url': get_studio_url(course, 'settings/grading'), 'user_timezone': user_timezone, 'verification_data': verification_data, } context = self.get_serializer_context() context['staff_access'] = bool( has_access(request.user, 'staff', course)) context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def set_credit_requirement_status(user, course_key, req_namespace, req_name, status="satisfied", reason=None): """ Update the user's requirement status. This will record whether the user satisfied or failed a particular requirement in a course. If the user has satisfied all requirements, the user will be marked as eligible for credit in the course. Args: user(User): User object to set credit requirement for. course_key (CourseKey): Identifier for the course associated with the requirement. req_namespace (str): Namespace of the requirement (e.g. "grade" or "reverification") req_name (str): Name of the requirement (e.g. "grade" or the location of the ICRV XBlock) Keyword Arguments: status (str): Status of the requirement (either "satisfied" or "failed") reason (dict): Reason of the status """ # Check whether user has credit eligible enrollment. enrollment_mode, is_active = CourseEnrollment.enrollment_mode_for_user( user, course_key) has_credit_eligible_enrollment = ( CourseMode.is_credit_eligible_slug(enrollment_mode) and is_active) # Refuse to set status of requirement if the user enrollment is not credit eligible. if not has_credit_eligible_enrollment: return # Do not allow students who have requested credit to change their eligibility if CreditRequest.get_user_request_status(user.username, course_key): log.info( u'Refusing to set status of requirement with namespace "%s" and name "%s" because the ' u'user "%s" has already requested credit for the course "%s".', req_namespace, req_name, user.username, course_key) return # Do not allow a student who has earned eligibility to un-earn eligibility eligible_before_update = CreditEligibility.is_user_eligible_for_credit( course_key, user.username) if eligible_before_update and status == 'failed': log.info( u'Refusing to set status of requirement with namespace "%s" and name "%s" to "failed" because the ' u'user "%s" is already eligible for credit in the course "%s".', req_namespace, req_name, user.username, course_key) return # Retrieve all credit requirements for the course # We retrieve all of them to avoid making a second query later when # we need to check whether all requirements have been satisfied. reqs = CreditRequirement.get_course_requirements(course_key) # Find the requirement we're trying to set req_to_update = next( (req for req in reqs if req.namespace == req_namespace and req.name == req_name), None) # If we can't find the requirement, then the most likely explanation # is that there was a lag updating the credit requirements after the course # was published. We *could* attempt to create the requirement here, # but that could cause serious performance issues if many users attempt to # lock the row at the same time. # Instead, we skip updating the requirement and log an error. if req_to_update is None: log.error( (u'Could not update credit requirement in course "%s" ' u'with namespace "%s" and name "%s" ' u'because the requirement does not exist. ' u'The user "%s" should have had their status updated to "%s".'), six.text_type(course_key), req_namespace, req_name, user.username, status) return # Update the requirement status CreditRequirementStatus.add_or_update_requirement_status(user.username, req_to_update, status=status, reason=reason) # If we're marking this requirement as "satisfied", there's a chance that the user has met all eligibility # requirements, and should be notified. However, if the user was already eligible, do not send another notification. if status == "satisfied" and not eligible_before_update: is_eligible, eligibility_record_created = CreditEligibility.update_eligibility( reqs, user.username, course_key) if eligibility_record_created and is_eligible: try: send_credit_notifications(user.username, course_key) except Exception: # pylint: disable=broad-except log.exception("Error sending email")
def get(self, request, *args, **kwargs): course_key_string = kwargs.get('course_key_string') course_key = CourseKey.from_string(course_key_string) 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) _, request.user = setup_masquerade(request, course_key, staff_access=has_access( request.user, 'staff', course_key), reset_masquerade_data=True) course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) enrollment_mode, _ = CourseEnrollment.enrollment_mode_for_user( request.user, course_key) course_grade = CourseGradeFactory().read(request.user, course)\ descriptor = modulestore().get_course(course_key) grading_policy = descriptor.grading_policy verification_status = IDVerificationService.user_status(request.user) 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'], } data = { 'certificate_data': get_cert_data(request.user, course, enrollment_mode, course_grade), 'completion_summary': get_course_blocks_completion_summary(course_key, request.user), 'course_grade': course_grade, 'section_scores': course_grade.chapter_grades.values(), 'enrollment_mode': enrollment_mode, 'grading_policy': grading_policy, 'studio_url': get_studio_url(course, 'settings/grading'), 'verification_data': verification_data, } context = self.get_serializer_context() context['staff_access'] = bool( has_access(request.user, 'staff', course)) context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def is_allowed(self): mode, is_active = CourseEnrollment.enrollment_mode_for_user( self.user, self.course_id) return (is_active and mode == 'verified' and self.verification_status in ('expired', 'none', 'must_reverify'))
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 _can_generate_v2_certificate(user, course_key): """ Check if a v2 course certificate can be generated (created if it doesn't already exist, or updated if it does exist) for this user, in this course run. """ if not _is_using_v2_course_certificates(course_key): # This course run is not using the v2 course certificate feature log.info( f'{course_key} is not using v2 course certificates. Certificate cannot be generated.' ) return False if not auto_certificate_generation_enabled(): # Automatic certificate generation is globally disabled log.info( f'Automatic certificate generation is globally disabled. Certificate cannot be generated for ' f'{user.id} : {course_key}.') return False if CertificateInvalidation.has_certificate_invalidation(user, course_key): # The invalidation list prevents certificate generation log.info( f'{user.id} : {course_key} is on the certificate invalidation list. Certificate cannot be generated.' ) return False enrollment_mode, __ = CourseEnrollment.enrollment_mode_for_user( user, course_key) if enrollment_mode is None: log.info( f'{user.id} : {course_key} does not have an enrollment. Certificate cannot be generated.' ) return False if not modes_api.is_eligible_for_certificate(enrollment_mode): log.info( f'{user.id} : {course_key} has an enrollment mode of {enrollment_mode}, which is not eligible for an ' f'allowlist certificate. Certificate cannot be generated.') return False if not IDVerificationService.user_is_verified(user): log.info( f'{user.id} does not have a verified id. Certificate cannot be generated for {course_key}.' ) return False if _is_ccx_course(course_key): log.info( f'{course_key} is a CCX course. Certificate cannot be generated for {user.id}.' ) return False course = modulestore().get_course(course_key, depth=0) if _is_beta_tester(user, course): log.info( f'{user.id} is a beta tester in {course_key}. Certificate cannot be generated.' ) return False if not _has_passing_grade(user, course): log.info( f'{user.id} does not have a passing grade in {course_key}. Certificate cannot be generated.' ) return False if not _can_generate_certificate_for_status(user, course_key): return False log.info(f'V2 certificate can be generated for {user.id} : {course_key}') return True