def handle(self, *args, **options): course_keys = [] if options['all']: course_keys = [course.id for course in modulestore().get_courses()] else: if len(args) < 1: raise CommandError('At least one course or --all must be specified.') try: course_keys = [CourseKey.from_string(arg) for arg in args] except InvalidKeyError: log.fatal('Invalid key specified.') if not course_keys: log.fatal('No courses specified.') log.info('Generating course overview for %d courses.', len(course_keys)) log.debug('Generating course overview(s) for the following courses: %s', course_keys) for course_key in course_keys: try: CourseOverview.get_from_id(course_key) except Exception as ex: # pylint: disable=broad-except log.exception('An error occurred while generating course overview for %s: %s', unicode( course_key), ex.message) log.info('Finished generating course overviews.')
def setUp(self): super(TestSyncCourseRunsCommand, self).setUp() # create mongo course self.course = CourseFactory.create() # load this course into course overview CourseOverview.get_from_id(self.course.id) # create a catalog course run with the same course id. self.catalog_course_run = CourseRunFactory( key=unicode(self.course.id), marketing_url='test_marketing_url' )
def course_run(self): """ The course run specified by the `course_id` URL parameter. """ try: CourseOverview.get_from_id(self.course_key) except CourseOverview.DoesNotExist: raise Http404() for course in self.program["courses"]: for course_run in course["course_runs"]: if self.course_key == CourseKey.from_string(course_run["key"]): return course_run raise Http404()
def generate_certificate_for_user(request): """ Generate certificates for a user. This is meant to be used by support staff through the UI in lms/djangoapps/support Arguments: request (HttpRequest): The request object Returns: HttpResponse Example Usage: POST /certificates/generate * username: "******" * course_key: "edX/DemoX/Demo_Course" Response: 200 OK """ # Check the POST parameters, returning a 400 response if they're not valid. params, response = _validate_post_params(request.POST) if response is not None: return response try: # Check that the course exists CourseOverview.get_from_id(params["course_key"]) except CourseOverview.DoesNotExist: msg = _("The course {course_key} does not exist").format(course_key=params["course_key"]) return HttpResponseBadRequest(msg) else: # Check that the user is enrolled in the course if not CourseEnrollment.is_enrolled(params["user"], params["course_key"]): msg = _("User {username} is not enrolled in the course {course_key}").format( username=params["user"].username, course_key=params["course_key"] ) return HttpResponseBadRequest(msg) # Attempt to generate certificate generate_certificates_for_students( request, params["course_key"], student_set="specific_student", specific_student_id=params["user"].id ) return HttpResponse(200)
def _create(cls, model_class, *args, **kwargs): manager = cls._get_manager(model_class) course_kwargs = {} for key in kwargs.keys(): if key.startswith('course__'): course_kwargs[key.split('__')[1]] = kwargs.pop(key) if 'course' not in kwargs: course_id = kwargs.get('course_id') course_overview = None if course_id is not None: if isinstance(course_id, six.string_types): course_id = CourseKey.from_string(course_id) course_kwargs.setdefault('id', course_id) try: course_overview = CourseOverview.get_from_id(course_id) except CourseOverview.DoesNotExist: pass if course_overview is None: course_overview = CourseOverviewFactory(**course_kwargs) kwargs['course'] = course_overview return manager.create(*args, **kwargs)
def _assert_supplemented(self, actual, **kwargs): """DRY helper used to verify that program data is extended correctly.""" course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member run_mode = dict( factories.RunMode( certificate_url=None, course_image_url=course_overview.course_image_url, course_key=unicode(self.course.id), # pylint: disable=no-member course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member end_date=strftime_localized(self.course.end, 'SHORT_DATE'), enrollment_open_date=strftime_localized(utils.DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'), is_course_ended=self.course.end < timezone.now(), is_enrolled=False, is_enrollment_open=True, marketing_url=MARKETING_URL, start_date=strftime_localized(self.course.start, 'SHORT_DATE'), upgrade_url=None, ), **kwargs ) course_code = factories.CourseCode(display_name=self.course_code['display_name'], run_modes=[run_mode]) expected = copy.deepcopy(self.program) expected['course_codes'] = [course_code] self.assertEqual(actual, expected)
def test_masquerade_expired(self, mock_get_course_run_details): mock_get_course_run_details.return_value = {'weeks_to_complete': 1} audit_student = UserFactory(username='******') enrollment = CourseEnrollmentFactory.create( user=audit_student, course_id=self.course.id, mode='audit', ) enrollment.created = self.course.start enrollment.save() CourseDurationLimitConfig.objects.create( enabled=True, course=CourseOverview.get_from_id(self.course.id), enabled_as_of=self.course.start, ) instructor = UserFactory.create(username='******') CourseEnrollmentFactory.create( user=instructor, course_id=self.course.id, mode='audit' ) CourseInstructorRole(self.course.id).add_users(instructor) self.client.login(username=instructor.username, password='******') self.update_masquerade(username='******') course_home_url = reverse('openedx.course_experience.course_home', args=[six.text_type(self.course.id)]) response = self.client.get(course_home_url, follow=True) self.assertEqual(response.status_code, 200) self.assertItemsEqual(response.redirect_chain, []) banner_text = 'This learner does not have access to this course. Their access expired on' self.assertIn(banner_text, response.content)
def test_course_catalog_access_num_queries(self, user_attr_name, action, course_attr_name): course = getattr(self, course_attr_name) # get a fresh user object that won't have any cached role information if user_attr_name == 'user_anonymous': user = AnonymousUserFactory() else: user = getattr(self, user_attr_name) user = User.objects.get(id=user.id) if (user_attr_name == 'user_staff' and action == 'see_exists' and course_attr_name in ['course_default', 'course_not_started']): # checks staff role num_queries = 1 elif user_attr_name == 'user_normal' and action == 'see_exists' and course_attr_name != 'course_started': # checks staff role and enrollment data num_queries = 2 else: num_queries = 0 course_overview = CourseOverview.get_from_id(course.id) with self.assertNumQueries(num_queries, table_blacklist=QUERY_COUNT_TABLE_BLACKLIST): bool(access.has_access(user, action, course_overview, course_key=course.id))
def supplement_program_data(program_data, user): """Supplement program course codes with CourseOverview and CourseEnrollment data. Arguments: program_data (dict): Representation of a program. user (User): The user whose enrollments to inspect. """ for course_code in program_data['course_codes']: for run_mode in course_code['run_modes']: course_key = CourseKey.from_string(run_mode['course_key']) course_overview = CourseOverview.get_from_id(course_key) run_mode['course_url'] = reverse('course_root', args=[course_key]) run_mode['course_image_url'] = course_overview.course_image_url human_friendly_format = '%x' start_date = course_overview.start or DEFAULT_START_DATE end_date = course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC) run_mode['start_date'] = start_date.strftime(human_friendly_format) run_mode['end_date'] = end_date.strftime(human_friendly_format) run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(user, course_key) enrollment_start = course_overview.enrollment_start or datetime.datetime.min.replace(tzinfo=pytz.UTC) enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC) is_enrollment_open = enrollment_start <= timezone.now() < enrollment_end run_mode['is_enrollment_open'] = is_enrollment_open # TODO: Currently unavailable on LMS. run_mode['marketing_url'] = '' return program_data
def test_student_has_access(self): """ Tests course student have right access to content w/o preview. """ course_key = self.course.id chapter = ItemFactory.create(category="chapter", parent_location=self.course.location) overview = CourseOverview.get_from_id(course_key) # Enroll student to the course CourseEnrollmentFactory(user=self.student, course_id=self.course.id) modules = [ self.course, overview, chapter, ] with patch('courseware.access.in_preview_mode') as mock_preview: mock_preview.return_value = False for obj in modules: self.assertTrue(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id))) with patch('courseware.access.in_preview_mode') as mock_preview: mock_preview.return_value = True for obj in modules: self.assertFalse(bool(access.has_access(self.student, 'load', obj, course_key=self.course.id)))
def test_get_course_details_course_dates(self, start_datetime, end_datetime, expected_start, expected_end): course = CourseFactory.create(start=start_datetime, end=end_datetime) # Load a CourseOverview. This initial load should result in a cache # miss; the modulestore is queried and course metadata is cached. __ = CourseOverview.get_from_id(course.id) self.assert_enrollment_status(course_id=unicode(course.id)) # Check course details url = reverse('courseenrollmentdetails', kwargs={"course_id": unicode(course.id)}) resp = self.client.get(url) self.assertEqual(resp.status_code, status.HTTP_200_OK) data = json.loads(resp.content) self.assertEqual(data['course_start'], expected_start) self.assertEqual(data['course_end'], expected_end) # Check enrollment course details url = reverse('courseenrollment', kwargs={"course_id": unicode(course.id)}) resp = self.client.get(url) self.assertEqual(resp.status_code, status.HTTP_200_OK) data = json.loads(resp.content) self.assertEqual(data['course_details']['course_start'], expected_start) self.assertEqual(data['course_details']['course_end'], expected_end) # Check enrollment list course details resp = self.client.get(reverse('courseenrollments')) self.assertEqual(resp.status_code, status.HTTP_200_OK) data = json.loads(resp.content) self.assertEqual(data[0]['course_details']['course_start'], expected_start) self.assertEqual(data[0]['course_details']['course_end'], expected_end)
def test_expired_course(self): """ Ensure that a user accessing an expired course sees a redirect to the student dashboard, not a 404. """ CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime(2010, 1, 1)) course = CourseFactory.create(start=THREE_YEARS_AGO) url = course_home_url(course) for mode in [CourseMode.AUDIT, CourseMode.VERIFIED]: CourseModeFactory.create(course_id=course.id, mode_slug=mode) # assert that an if an expired audit user tries to access the course they are redirected to the dashboard audit_user = UserFactory(password=self.TEST_PASSWORD) self.client.login(username=audit_user.username, password=self.TEST_PASSWORD) audit_enrollment = CourseEnrollment.enroll(audit_user, course.id, mode=CourseMode.AUDIT) ScheduleFactory(start=THREE_YEARS_AGO, enrollment=audit_enrollment) response = self.client.get(url) expiration_date = strftime_localized(course.start + timedelta(weeks=4), 'SHORT_DATE') expected_params = QueryDict(mutable=True) course_name = CourseOverview.get_from_id(course.id).display_name_with_default expected_params['access_response_error'] = 'Access to {run} expired on {expiration_date}'.format( run=course_name, expiration_date=expiration_date ) expected_url = '{url}?{params}'.format( url=reverse('dashboard'), params=expected_params.urlencode() ) self.assertRedirects(response, expected_url)
def certificate_downloadable_status(student, course_key): """ Check the student existing certificates against a given course. if status is not generating and not downloadable or error then user can view the generate button. Args: student (user object): logged-in user course_key (CourseKey): ID associated with the course Returns: Dict containing student passed status also download url, uuid for cert if available """ current_status = certificate_status_for_student(student, course_key) # If the certificate status is an error user should view that status is "generating". # On the back-end, need to monitor those errors and re-submit the task. response_data = { 'is_downloadable': False, 'is_generating': True if current_status['status'] in [CertificateStatuses.generating, CertificateStatuses.error] else False, 'is_unverified': True if current_status['status'] == CertificateStatuses.unverified else False, 'download_url': None, 'uuid': None, } may_view_certificate = CourseOverview.get_from_id(course_key).may_certify() if current_status['status'] == CertificateStatuses.downloadable and may_view_certificate: response_data['is_downloadable'] = True response_data['download_url'] = current_status['download_url'] or get_certificate_url(student.id, course_key) response_data['uuid'] = current_status['uuid'] return response_data
def setUp(self): """ Create a course and user, then log in. """ super(EnrollmentTest, self).setUp() self.rate_limit_config = RateLimitConfiguration.current() self.rate_limit_config.enabled = False self.rate_limit_config.save() throttle = EnrollmentUserThrottle() self.rate_limit, rate_duration = throttle.parse_rate(throttle.rate) self.course = CourseFactory.create() # Load a CourseOverview. This initial load should result in a cache # miss; the modulestore is queried and course metadata is cached. __ = CourseOverview.get_from_id(self.course.id) self.user = UserFactory.create( username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD, ) self.other_user = UserFactory.create( username=self.OTHER_USERNAME, email=self.OTHER_EMAIL, password=self.PASSWORD, ) self.client.login(username=self.USERNAME, password=self.PASSWORD)
def get_course_enrollment_info(course_id, include_expired=False): """Returns all course enrollment information for the given course. Based on the course id, return all related course information. Args: course_id (str): The course to retrieve enrollment information for. include_expired (bool): Boolean denoting whether expired course modes should be included in the returned JSON data. Returns: A serializable dictionary representing the course's enrollment information. Raises: CourseNotFoundError """ course_key = CourseKey.from_string(course_id) try: course = CourseOverview.get_from_id(course_key) except CourseOverview.DoesNotExist: msg = u"Requested enrollment information for unknown course {course}".format(course=course_id) log.warning(msg) raise CourseNotFoundError(msg) else: return CourseSerializer(course, include_expired=include_expired).data
def get_days_until_expiration(self, entitlement): """ Returns an integer of number of days until the entitlement expires. Includes the logic for regaining an entitlement. """ now_timestamp = now() expiry_date = entitlement.created + self.expiration_period days_until_expiry = (expiry_date - now_timestamp).days if not entitlement.enrollment_course_run: return days_until_expiry course_overview = CourseOverview.get_from_id(entitlement.enrollment_course_run.course_id) # Compute the days left for the regain days_since_course_start = (now_timestamp - course_overview.start).days days_since_enrollment = (now_timestamp - entitlement.enrollment_course_run.created).days days_since_entitlement_created = (now_timestamp - entitlement.created).days # We want to return whichever days value is less since it is then the more recent one days_until_regain_ends = (self.regain_period.days - # pylint: disable=no-member min(days_since_course_start, days_since_enrollment, days_since_entitlement_created)) # If the base days until expiration is less than the days until the regain period ends, use that instead if days_until_expiry < days_until_regain_ends: return days_until_expiry return days_until_regain_ends # pylint: disable=no-member
def delete(self, request, ccx_course_id=None): # pylint: disable=unused-argument """ Deletes a CCX course. Args: request (Request): Django request object. ccx_course_id (string): URI element specifying the CCX course location. """ ccx_course_object, ccx_course_key, error_code, http_status = self.get_object(ccx_course_id, is_ccx=True) if ccx_course_object is None: return Response( status=http_status, data={ 'error_code': error_code } ) ccx_course_overview = CourseOverview.get_from_id(ccx_course_key) # clean everything up with a single transaction with transaction.atomic(): CcxFieldOverride.objects.filter(ccx=ccx_course_object).delete() # remove all users enrolled in the CCX from the CourseEnrollment model CourseEnrollment.objects.filter(course_id=ccx_course_key).delete() ccx_course_overview.delete() ccx_course_object.delete() return Response( status=status.HTTP_204_NO_CONTENT, )
def test_generate_force_update(self): self.command.handle(all=True) # update each course updated_course_name = u'test_generate_course_overview.course_edit' for course_key in (self.course_key_1, self.course_key_2): course = self.store.get_course(course_key) course.display_name = updated_course_name self.store.update_item(course, self.user.id) # force_update course_key_1, but not course_key_2 self.command.handle(unicode(self.course_key_1), all=False, force_update=True) self.command.handle(unicode(self.course_key_2), all=False, force_update=False) self.assertEquals(CourseOverview.get_from_id(self.course_key_1).display_name, updated_course_name) self.assertNotEquals(CourseOverview.get_from_id(self.course_key_2).display_name, updated_course_name)
def test_enrollments_not_deleted(self): """ Recreating a CourseOverview with an outdated version should not delete the associated enrollment. """ course = CourseFactory(self_paced=True) 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=datetime.datetime.now(pytz.UTC) + datetime.timedelta(days=30), ) # Create a CourseOverview with an outdated version course_overview = CourseOverview.load_from_module_store(course.id) course_overview.version = CourseOverview.VERSION - 1 course_overview.save() # Create an inactive enrollment with this course overview enrollment = CourseEnrollmentFactory( user=self.user, course_id=course.id, mode=CourseMode.AUDIT, course=course_overview, ) # Re-fetch the CourseOverview record. # As a side effect, this will recreate the record, and update the version. course_overview_new = CourseOverview.get_from_id(course.id) self.assertEqual(course_overview_new.version, CourseOverview.VERSION) # Ensure that the enrollment record was unchanged during this re-creation enrollment_refetched = CourseEnrollment.objects.filter(id=enrollment.id) self.assertTrue(enrollment_refetched.exists()) self.assertEqual(enrollment_refetched.all()[0], enrollment)
def are_grades_frozen(course_key): """ Returns whether grades are frozen for the given course. """ if waffle_flags()[ENFORCE_FREEZE_GRADE_AFTER_COURSE_END].is_enabled(course_key): course = CourseOverview.get_from_id(course_key) if course.end: freeze_grade_date = course.end + timedelta(30) now = timezone.now() return now > freeze_grade_date
def test_course_overview_unsupported_action(self): """ Check that calling has_access with an unsupported action raises a ValueError. """ overview = CourseOverview.get_from_id(self.course_default.id) with self.assertRaises(ValueError): access.has_access(self.user, "_non_existent_action", overview)
def ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, coach): """ Function to enroll/add or unenroll/revoke students. This function exists for backwards compatibility: in CCX there are two different views to manage students that used to implement a different logic. Now the logic has been reconciled at the point that this function can be used by both. The two different views can be merged after some UI refactoring. Arguments: action (str): type of action to perform (add, Enroll, revoke, Unenroll) identifiers (list): list of students username/email email_students (bool): Flag to send an email to students course_key (CCXLocator): a CCX course key email_params (dict): dictionary of settings for the email to be sent coach (User): ccx coach Returns: list: list of error """ errors = [] if action == 'Enroll' or action == 'add': ccx_course_overview = CourseOverview.get_from_id(course_key) course_locator = course_key.to_course_locator() staff = CourseStaffRole(course_locator).users_with_role() admins = CourseInstructorRole(course_locator).users_with_role() for identifier in identifiers: must_enroll = False try: email, student = get_valid_student_with_email(identifier) if student: must_enroll = student in staff or student in admins or student == coach except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue if CourseEnrollment.objects.is_course_full(ccx_course_overview) and not must_enroll: error = _('The course is full: the limit is {max_student_enrollments_allowed}').format( max_student_enrollments_allowed=ccx_course_overview.max_student_enrollments_allowed) log.info("%s", error) errors.append(error) break enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params) elif action == 'Unenroll' or action == 'revoke': for identifier in identifiers: try: email, __ = get_valid_student_with_email(identifier) except CCXUserValidationException as exp: log.info("%s", exp) errors.append("{0}".format(exp)) continue unenroll_email(course_key, email, email_students=email_students, email_params=email_params) return errors
def test_date_with_self_paced_with_enrollment_before_course_start(self): """ Enrolling before a course begins should result in the upgrade deadline being set relative to the course start date. """ global_config = DynamicUpgradeDeadlineConfiguration.objects.create(enabled=True) course = create_self_paced_course_run(days_till_start=3) overview = CourseOverview.get_from_id(course.id) expected = overview.start + timedelta(days=global_config.deadline_days) enrollment = CourseEnrollmentFactory(course_id=course.id, mode=CourseMode.AUDIT) block = VerifiedUpgradeDeadlineDate(course, enrollment.user) self.assertEqual(block.date, expected)
def _extend_course_runs(self): """Execute course run data handlers.""" for course in self.data['courses']: for course_run in course['course_runs']: # State to be shared across handlers. self.course_run_key = CourseKey.from_string(course_run['key']) self.course_overview = CourseOverview.get_from_id(self.course_run_key) self.enrollment_start = self.course_overview.enrollment_start or DEFAULT_ENROLLMENT_START_DATE self._execute('_attach_course_run', course_run)
def _listen_for_certificate_whitelist_append(sender, instance, **kwargs): # pylint: disable=unused-argument course = CourseOverview.get_from_id(instance.course_id) if not auto_certificate_generation_enabled(): return fire_ungenerated_certificate_task(instance.user, instance.course_id) log.info(u'Certificate generation task initiated for {user} : {course} via whitelist'.format( user=instance.user.id, course=instance.course_id ))
def _extend_run_modes(self): """Execute run mode data handlers.""" for course_code in self.data['course_codes']: for run_mode in course_code['run_modes']: # State to be shared across handlers. self.course_key = CourseKey.from_string(run_mode['course_key']) self.course_overview = CourseOverview.get_from_id(self.course_key) self.enrollment_start = self.course_overview.enrollment_start or DEFAULT_ENROLLMENT_START_DATE self._execute('_attach_run_mode', run_mode)
def test_content_availability_date(self, mock_get_course_run_details): """ Content availability date is course start date or enrollment date, whichever is later. """ access_duration = timedelta(weeks=7) mock_get_course_run_details.return_value = {'weeks_to_complete': 7} # Content availability date is enrollment date start_date = now() - timedelta(weeks=10) past_course = CourseFactory(start=start_date) enrollment = CourseEnrollment.enroll(self.user, past_course.id, CourseMode.AUDIT) result = get_user_course_expiration_date( self.user, CourseOverview.get_from_id(past_course.id), ) self.assertEqual(result, None) add_course_mode(past_course, upgrade_deadline_expired=False) result = get_user_course_expiration_date( self.user, CourseOverview.get_from_id(past_course.id), ) content_availability_date = enrollment.created self.assertEqual(result, content_availability_date + access_duration) # Content availability date is course start date start_date = now() + timedelta(weeks=10) future_course = CourseFactory(start=start_date) enrollment = CourseEnrollment.enroll(self.user, future_course.id, CourseMode.AUDIT) result = get_user_course_expiration_date( self.user, CourseOverview.get_from_id(future_course.id), ) self.assertEqual(result, None) add_course_mode(future_course, upgrade_deadline_expired=False) result = get_user_course_expiration_date( self.user, CourseOverview.get_from_id(future_course.id), ) content_availability_date = start_date.replace(microsecond=0) self.assertEqual(result, content_availability_date + access_duration)
def name(self): """ Return course name. """ course_id = CourseKey.from_string(unicode(self.id)) try: return CourseOverview.get_from_id(course_id).display_name except CourseOverview.DoesNotExist: # NOTE (CCB): Ideally, the course modes table should only contain data for courses that exist in # modulestore. If that is not the case, say for local development/testing, carry on without failure. log.warning('Failed to retrieve CourseOverview for [%s]. Using empty course name.', course_id) return None
def get(self, request, course_key_string): """ Return the discount percent, if the user has appropriate permissions. """ course_key = CourseKey.from_string(course_key_string) course = CourseOverview.get_from_id(course_key) discount_applicable = can_receive_discount(user=request.user, course=course) discount_percent = discount_percentage() payload = {'discount_applicable': discount_applicable, 'discount_percent': discount_percent} return Response({ 'discount_applicable': discount_applicable, 'jwt': create_jwt_for_user(request.user, additional_claims=payload)})
def setUp(self): """ Create a course and user, then log in. """ super(EnrollmentEmbargoTest, self).setUp() self.course = CourseFactory.create() # Load a CourseOverview. This initial load should result in a cache # miss; the modulestore is queried and course metadata is cached. __ = CourseOverview.get_from_id(self.course.id) self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD) self.client.login(username=self.USERNAME, password=self.PASSWORD) self.url = reverse('courseenrollments')
def has_html_certificates_enabled(course_key, course=None): """ Determine if a course has html certificates enabled. Arguments: course_key (CourseKey|str): A course key or a string representation of one. course (CourseDescriptor|CourseOverview): A course. """ # If the feature is disabled, then immediately return a False if not settings.FEATURES.get('CERTIFICATES_HTML_VIEW', False): return False # If we don't have a course object, we'll need to assemble one if not course: # Initialize a course key if necessary if not isinstance(course_key, CourseKey): try: course_key = CourseKey.from_string(course_key) except InvalidKeyError: log.warning( ('Unable to parse course_key "%s"', course_key), exc_info=True ) return False # Pull the course data from the cache try: course = CourseOverview.get_from_id(course_key) except: # pylint: disable=bare-except log.warning( ('Unable to load CourseOverview object for course_key "%s"', unicode(course_key)), exc_info=True ) # Return the flag on the course object return course.cert_html_view_enabled if course else False
def course_expiration_wrapper(user, block, view, frag, context): # pylint: disable=W0613 """ An XBlock wrapper that prepends a message to the beginning of a vertical if a user's course is about to expire. """ if block.category != "vertical": return frag course = CourseOverview.get_from_id(block.course_id) course_expiration_fragment = generate_course_expired_fragment(user, course) if not course_expiration_fragment: return frag # Course content must be escaped to render correctly due to the way the # way the XBlock rendering works. Transforming the safe markup to unicode # escapes correctly. course_expiration_fragment.content = unicode( course_expiration_fragment.content) course_expiration_fragment.add_content(frag.content) course_expiration_fragment.add_fragment_resources(frag) return course_expiration_fragment
def test_masquerade(self, masquerade_config, show_expiration_banner, mock_get_course_run_details): mock_get_course_run_details.return_value = {'weeks_to_complete': 12} audit_student = UserFactory(username='******') CourseEnrollmentFactory.create(user=audit_student, course_id=self.course.id, mode='audit') verified_student = UserFactory(username='******') CourseEnrollmentFactory.create(user=verified_student, course_id=self.course.id, mode='verified') CourseDurationLimitConfig.objects.create( enabled=True, course=CourseOverview.get_from_id(self.course.id), enabled_as_of=self.course.start, ) instructor = UserFactory.create(username='******') CourseEnrollmentFactory.create(user=instructor, course_id=self.course.id, mode='audit') CourseInstructorRole(self.course.id).add_users(instructor) self.client.login(username=instructor.username, password='******') self.update_masquerade(**masquerade_config) course_home_url = reverse('openedx.course_experience.course_home', args=[unicode(self.course.id)]) response = self.client.get(course_home_url, follow=True) self.assertEqual(response.status_code, 200) self.assertItemsEqual(response.redirect_chain, []) banner_text = 'You lose all access to this course, including your progress,' if show_expiration_banner: self.assertIn(banner_text, response.content) else: self.assertNotIn(banner_text, response.content)
def test_student_has_access(self): """ Tests course student have right access to content w/o preview. """ course_key = self.course.id chapter = ItemFactory.create(category="chapter", parent_location=self.course.location) overview = CourseOverview.get_from_id(course_key) # Enroll student to the course CourseEnrollmentFactory(user=self.student, course_id=self.course.id) modules = [ self.course, overview, chapter, ] with patch('courseware.access.in_preview_mode') as mock_preview: mock_preview.return_value = False for obj in modules: self.assertTrue( bool( access.has_access(self.student, 'load', obj, course_key=self.course.id))) with patch('courseware.access.in_preview_mode') as mock_preview: mock_preview.return_value = True for obj in modules: self.assertFalse( bool( access.has_access(self.student, 'load', obj, course_key=self.course.id)))
def test_expired_course(self): """ Ensure that a user accessing an expired course sees a redirect to the student dashboard, not a 404. """ CourseDurationLimitConfig.objects.create(enabled=True, enabled_as_of=datetime( 2010, 1, 1)) course = CourseFactory.create(start=THREE_YEARS_AGO) url = course_home_url(course) for mode in [CourseMode.AUDIT, CourseMode.VERIFIED]: CourseModeFactory.create(course_id=course.id, mode_slug=mode) # assert that an if an expired audit user tries to access the course they are redirected to the dashboard audit_user = UserFactory(password=self.TEST_PASSWORD) self.client.login(username=audit_user.username, password=self.TEST_PASSWORD) audit_enrollment = CourseEnrollment.enroll(audit_user, course.id, mode=CourseMode.AUDIT) ScheduleFactory(start=THREE_YEARS_AGO, enrollment=audit_enrollment) response = self.client.get(url) expiration_date = strftime_localized(course.start + timedelta(weeks=4), u'%b. %-d, %Y') expected_params = QueryDict(mutable=True) course_name = CourseOverview.get_from_id( course.id).display_name_with_default expected_params[ 'access_response_error'] = u'Access to {run} expired on {expiration_date}'.format( run=course_name, expiration_date=expiration_date) expected_url = '{url}?{params}'.format( url=reverse('dashboard'), params=expected_params.urlencode()) self.assertRedirects(response, expected_url)
def _assert_supplemented(self, actual, is_enrolled=False, is_enrollment_open=True): """DRY helper used to verify that program data is extended correctly.""" course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member run_mode = factories.RunMode( course_key=unicode(self.course.id), # pylint: disable=no-member course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member course_image_url=course_overview.course_image_url, start_date=self.course.start.strftime(self.human_friendly_format), end_date=self.course.end.strftime(self.human_friendly_format), is_enrolled=is_enrolled, is_enrollment_open=is_enrollment_open, marketing_url='', ) course_code = factories.CourseCode( display_name=self.course_code['display_name'], run_modes=[run_mode]) expected = copy.deepcopy(self.program) expected['course_codes'] = [course_code] self.assertEqual(actual, expected)
def get(self, request, course_key_string, user_id): """ Return the discount percent, if the user has appropriate permissions. """ course_key = CourseKey.from_string(course_key_string) course = CourseOverview.get_from_id(course_key) user = User.objects.get(id=user_id) # Below code in try/except is temporarily replacing this call # discount_applicable = can_receive_discount(user=user, course=course) # Only show a discount on the order if the last basket loaded for this course had a discount # Do not check any of the discount requirements try: discount_applicable = ExperimentData.objects.get( user=user, experiment_id=REV1008_EXPERIMENT_ID, key='discount_' + str(course) ).value == 'True' except ExperimentData.DoesNotExist: discount_applicable = False discount_percent = discount_percentage(course) payload = {'discount_applicable': discount_applicable, 'discount_percent': discount_percent} return Response({ 'discount_applicable': discount_applicable, 'jwt': create_jwt_for_user(request.user, additional_claims=payload)})
def test_all_courses_with_weeks_to_complete( self, weeks_to_complete, access_duration, self_paced, mock_get_course_run_details, ): """ Test that access_duration for a course is equal to the value of the weeks_to_complete field in discovery. If weeks_to_complete is None, access_duration will be the MIN_DURATION constant. """ if self_paced: self.course.self_paced = True mock_get_course_run_details.return_value = { 'weeks_to_complete': weeks_to_complete } enrollment = CourseEnrollment.enroll(self.user, self.course.id, CourseMode.AUDIT) result = get_user_course_expiration_date( self.user, CourseOverview.get_from_id(self.course.id), ) self.assertEqual(result, enrollment.created + access_duration)
def get_days_until_expiration(self, entitlement): """ Returns an integer of number of days until the entitlement expires. Includes the logic for regaining an entitlement. """ now = datetime.now(tz=pytz.UTC) expiry_date = entitlement.created + self.expiration_period days_until_expiry = (expiry_date - now).days if not entitlement.enrollment_course_run: return days_until_expiry course_overview = CourseOverview.get_from_id(entitlement.enrollment_course_run.course_id) # Compute the days left for the regain days_since_course_start = (now - course_overview.start).days days_since_enrollment = (now - entitlement.enrollment_course_run.created).days # We want to return whichever days value is less since it is then the more recent one days_until_regain_ends = (self.regain_period.days - # pylint: disable=no-member min(days_since_course_start, days_since_enrollment)) # If the base days until expiration is less than the days until the regain period ends, use that instead if days_until_expiry < days_until_regain_ends: return days_until_expiry return days_until_regain_ends # pylint: disable=no-member
def course_overview(self): return CourseOverview.get_from_id(self.course_id)
def test_enrollment_mode(self): """Tests that verified enrollments do not have an expiration""" CourseEnrollment.enroll(self.user, self.course.id, CourseMode.VERIFIED) result = get_user_course_expiration_date(self.user, CourseOverview.get_from_id(self.course.id)) self.assertEqual(result, None)
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 is_enrolled = enrollment and enrollment.is_active is_staff = bool(has_access(request.user, 'staff', course_key)) show_enrolled = is_enrolled or is_staff show_handouts = show_enrolled or allow_public handouts_html = get_course_info_section( request, request.user, course, 'handouts') if show_handouts else '' # TODO: TNL-7185 Legacy: Refactor to return the offer & expired data and format the message in the MFE offer_html = show_enrolled and generate_offer_html( request.user, course_overview) course_expired_html = show_enrolled and generate_course_expired_message( request.user, course_overview) welcome_message_html = None if show_enrolled: if LATEST_UPDATE_FLAG.is_enabled(course_key): welcome_message_html = LatestUpdateFragmentView( ).latest_update_html(request, course) elif get_course_tag(request.user, course_key, PREFERENCE_KEY) != 'False': welcome_message_html = WelcomeMessageFragmentView( ).welcome_message_html(request, course) enroll_alert = { 'can_enroll': True, 'extra_text': None, } 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 course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) # 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') course_blocks = None if show_enrolled or allow_public or allow_public_outline: outline_user = request.user if show_enrolled else None course_blocks = get_course_outline_block_tree( request, course_key_string, outline_user) resume_course = { 'has_visited_course': False, 'url': None, } if show_enrolled: try: resume_block = get_key_to_last_completed_block( request.user, course.id) resume_course['has_visited_course'] = True except UnavailableCompletionData: resume_block = course_usage_key resume_path = reverse('jump_to', kwargs={ 'course_id': course_key_string, 'location': str(resume_block) }) resume_course['url'] = request.build_absolute_uri(resume_path) dates_widget = { 'course_date_blocks': [ block for block in date_blocks if not isinstance(block, TodaysDate) ], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } # 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), } else: course_goals = {'goal_options': [], 'selected_goal': None} data = { 'course_blocks': course_blocks, 'course_expired_html': course_expired_html or None, 'course_goals': course_goals, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enroll_alert': enroll_alert, 'handouts_html': handouts_html or None, 'has_ended': course.has_ended(), 'offer_html': offer_html or None, 'resume_course': resume_course, 'welcome_message_html': welcome_message_html or None, } context = self.get_serializer_context() context['course_key'] = course_key context['enable_links'] = show_enrolled or allow_public serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
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_metric('course_id', course_key_string) monitoring_utils.set_custom_metric('user_id', request.user.id) monitoring_utils.set_custom_metric('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 is_enrolled = enrollment and enrollment.is_active is_staff = has_access(request.user, 'staff', course_key) show_enrolled = is_enrolled or is_staff show_handouts = show_enrolled or allow_public handouts_html = get_course_info_section( request, request.user, course, 'handouts') if show_handouts else '' # TODO: TNL-7185 Legacy: Refactor to return the offer & expired data and format the message in the MFE offer_html = generate_offer_html(request.user, course_overview) course_expired_html = generate_course_expired_message( request.user, course_overview) welcome_message_html = None if get_course_tag(request.user, course_key, PREFERENCE_KEY) != 'False': if LATEST_UPDATE_FLAG.is_enabled(course_key): welcome_message_html = LatestUpdateFragmentView( ).latest_update_html(request, course) else: welcome_message_html = WelcomeMessageFragmentView( ).welcome_message_html(request, course) enroll_alert = { 'can_enroll': True, 'extra_text': None, } 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 course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) date_blocks = get_course_date_blocks(course, request.user, request, num_assignments=1) # 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') transformers = BlockStructureTransformers() transformers += get_course_block_access_transformers(request.user) transformers += [ BlocksAPITransformer(None, None, depth=3), ] course_blocks = get_course_blocks(request.user, course_usage_key, transformers, include_completion=True) dates_widget = { 'course_date_blocks': [ block for block in date_blocks if not isinstance(block, TodaysDate) ], 'dates_tab_link': dates_tab_link, 'user_timezone': user_timezone, } data = { 'course_blocks': course_blocks, 'course_expired_html': course_expired_html, 'course_tools': course_tools, 'dates_widget': dates_widget, 'enroll_alert': enroll_alert, 'handouts_html': handouts_html, 'offer_html': offer_html, 'welcome_message_html': welcome_message_html, } context = self.get_serializer_context() context['course_key'] = course_key serializer = self.get_serializer_class()(data, context=context) return Response(serializer.data)
def search_certificates(request): """ Search for certificates for a particular user OR along with the given course. Supports search by either username or email address along with course id. First filter the records for the given username/email and then filter against the given course id (if given). Show the 'Regenerate' button if a record found in 'generatedcertificate' model otherwise it will show the Generate button. Arguments: request (HttpRequest): The request object. Returns: JsonResponse Example Usage: GET /certificates/[email protected] GET /certificates/[email protected]&course_id=xyz Response: 200 OK Content-Type: application/json [ { "username": "******", "course_key": "edX/DemoX/Demo_Course", "type": "verified", "status": "downloadable", "download_url": "http://www.example.com/cert.pdf", "grade": "0.98", "created": 2015-07-31T00:00:00Z, "modified": 2015-07-31T00:00:00Z } ] """ unbleached_filter = urllib.parse.unquote( urllib.parse.quote_plus(request.GET.get("user", ""))) user_filter = bleach.clean(unbleached_filter) if not user_filter: msg = _("user is not given.") return HttpResponseBadRequest(msg) try: user = User.objects.get(Q(email=user_filter) | Q(username=user_filter)) except User.DoesNotExist: return HttpResponseBadRequest( _("user '{user}' does not exist").format(user=user_filter)) certificates = get_certificates_for_user(user.username) for cert in certificates: cert["course_key"] = str(cert["course_key"]) cert["created"] = cert["created"].isoformat() cert["modified"] = cert["modified"].isoformat() cert["regenerate"] = not cert['is_pdf_certificate'] course_id = urllib.parse.quote_plus(request.GET.get("course_id", ""), safe=':/') if course_id: try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: return HttpResponseBadRequest( _("Course id '{course_id}' is not valid").format( course_id=course_id)) else: try: if CourseOverview.get_from_id(course_key): certificates = [ certificate for certificate in certificates if certificate['course_key'] == course_id ] if not certificates: return JsonResponse([{ 'username': user.username, 'course_key': course_id, 'regenerate': False }]) except CourseOverview.DoesNotExist: msg = _( "The course does not exist against the given key '{course_key}'" ).format(course_key=course_key) return HttpResponseBadRequest(msg) return JsonResponse(certificates)
def render_to_fragment(self, request, course_id=None, **kwargs): # lint-amnesty, pylint: disable=arguments-differ, too-many-statements """ Renders the course's home page as a fragment. """ course_key = CourseKey.from_string(course_id) course = get_course_with_access(request.user, 'load', course_key) # Render the course dates as a fragment dates_fragment = CourseDatesFragmentView().render_to_fragment( request, course_id=course_id, **kwargs) # Render the full content to enrolled users, as well as to course and global staff. # Unenrolled users who are not course or global staff are given only a subset. enrollment = CourseEnrollment.get_enrollment(request.user, course_key) user_access = { 'is_anonymous': request.user.is_anonymous, 'is_enrolled': enrollment and enrollment.is_active, 'is_staff': has_access(request.user, 'staff', 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 # Set all the fragments outline_fragment = None update_message_fragment = None course_sock_fragment = None offer_banner_fragment = None course_expiration_fragment = None has_visited_course = None resume_course_url = None handouts_html = None course_overview = CourseOverview.get_from_id(course.id) if user_access['is_enrolled'] or user_access['is_staff']: outline_fragment = CourseOutlineFragmentView().render_to_fragment( request, course_id=course_id, **kwargs) if LATEST_UPDATE_FLAG.is_enabled(course_key): update_message_fragment = LatestUpdateFragmentView( ).render_to_fragment(request, course_id=course_id, **kwargs) else: update_message_fragment = WelcomeMessageFragmentView( ).render_to_fragment(request, course_id=course_id, **kwargs) course_sock_fragment = CourseSockFragmentView().render_to_fragment( request, course=course, **kwargs) has_visited_course, resume_course_url = self._get_resume_course_info( request, course_id) handouts_html = self._get_course_handouts(request, course) offer_banner_fragment = get_first_purchase_offer_banner_fragment( request.user, course_overview) course_expiration_fragment = generate_course_expired_fragment( request.user, course_overview) elif allow_public_outline or allow_public: outline_fragment = CourseOutlineFragmentView().render_to_fragment( request, course_id=course_id, user_is_enrolled=False, **kwargs) course_sock_fragment = CourseSockFragmentView().render_to_fragment( request, course=course, **kwargs) if allow_public: handouts_html = self._get_course_handouts(request, course) else: # Redirect the user to the dashboard if they are not enrolled and # this is a course that does not support direct enrollment. if not can_self_enroll_in_course(course_key): raise CourseAccessRedirect(reverse('dashboard')) # Get the course tools enabled for this user and course course_tools = CourseToolsPluginManager.get_enabled_course_tools( request, course_key) # Check if the user can access the course goal functionality has_goal_permission = has_course_goal_permission( request, course_id, user_access) # Grab the current course goal and the acceptable course goal keys mapped to translated values current_goal = get_course_goal(request.user, course_key) goal_options = get_course_goal_options() # Get the course goals api endpoint goal_api_url = get_goal_api_url(request) # Grab the course home messages fragment to render any relevant django messages course_home_message_fragment = CourseHomeMessageFragmentView( ).render_to_fragment(request, course_id=course_id, user_access=user_access, **kwargs) # Get info for upgrade messaging upgrade_price = None upgrade_url = None has_discount = False # TODO Add switch to control deployment if SHOW_UPGRADE_MSG_ON_COURSE_HOME.is_enabled( course_key) and can_show_verified_upgrade( request.user, enrollment, course): upgrade_url = verified_upgrade_deadline_link(request.user, course_id=course_key) upgrade_price, has_discount = format_strikeout_price( request.user, course_overview) show_search = ( settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH') or (settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH_FOR_COURSE_STAFF') and user_access['is_staff'])) # Render the course home fragment context = { 'request': request, 'csrf': csrf(request)['csrf_token'], 'course': course, 'course_key': course_key, 'outline_fragment': outline_fragment, 'handouts_html': handouts_html, 'course_home_message_fragment': course_home_message_fragment, 'offer_banner_fragment': offer_banner_fragment, 'course_expiration_fragment': course_expiration_fragment, 'has_visited_course': has_visited_course, 'resume_course_url': resume_course_url, 'course_tools': course_tools, 'dates_fragment': dates_fragment, 'username': request.user.username, 'goal_api_url': goal_api_url, 'has_goal_permission': has_goal_permission, 'goal_options': goal_options, 'current_goal': current_goal, 'update_message_fragment': update_message_fragment, 'course_sock_fragment': course_sock_fragment, 'disable_courseware_js': True, 'uses_bootstrap': True, 'upgrade_price': upgrade_price, 'upgrade_url': upgrade_url, 'has_discount': has_discount, 'show_search': show_search, } html = render_to_string('course_experience/course-home-fragment.html', context) return Fragment(html)
def _get_result(self, course): """ Return the CourseSerializer for the specified course. """ course_overview = CourseOverview.get_from_id(course.id) return self.serializer_class(course_overview, context={'request': self._get_request()}).data
def _course_from_key(course_key): return CourseOverview.get_from_id(_safe_course_key(course_key))
def ccx_students_enrolling_center(action, identifiers, email_students, course_key, email_params, coach): """ Function to enroll or unenroll/revoke students. Arguments: action (str): type of action to perform (Enroll, Unenroll/revoke) identifiers (list): list of students username/email email_students (bool): Flag to send an email to students course_key (CCXLocator): a CCX course key email_params (dict): dictionary of settings for the email to be sent coach (User): ccx coach Returns: list: list of error """ errors = [] if action == 'Enroll': ccx_course_overview = CourseOverview.get_from_id(course_key) course_locator = course_key.to_course_locator() staff = CourseStaffRole(course_locator).users_with_role() admins = CourseInstructorRole(course_locator).users_with_role() for identifier in identifiers: must_enroll = False try: email, student = get_valid_student_with_email(identifier) if student: must_enroll = student in staff or student in admins or student == coach except CCXUserValidationException as exp: log.info("%s", exp) errors.append(f"{exp}") continue if CourseEnrollment.objects.is_course_full( ccx_course_overview) and not must_enroll: error = _( 'The course is full: the limit is {max_student_enrollments_allowed}' ).format(max_student_enrollments_allowed=ccx_course_overview. max_student_enrollments_allowed) log.info("%s", error) errors.append(error) break enroll_email(course_key, email, auto_enroll=True, email_students=email_students, email_params=email_params) elif action == 'Unenroll' or action == 'revoke': # lint-amnesty, pylint: disable=consider-using-in for identifier in identifiers: try: email, __ = get_valid_student_with_email(identifier) except CCXUserValidationException as exp: log.info("%s", exp) errors.append(f"{exp}") continue unenroll_email(course_key, email, email_students=email_students, email_params=email_params) return errors
def get(self, request): """ Return the if the course should be upsold in the mobile app, if the user has appropriate permissions. """ if not MOBILE_UPSELL_FLAG.is_enabled(): return Response({ 'show_upsell': False, 'upsell_flag': False, }) course_id = request.GET.get('course_id') try: course_key = CourseKey.from_string(course_id) except InvalidKeyError: return HttpResponseBadRequest("Missing or invalid course_id") course = CourseOverview.get_from_id(course_key) if not course.has_started() or course.has_ended(): return Response({ 'show_upsell': False, 'upsell_flag': MOBILE_UPSELL_FLAG.is_enabled(), 'course_running': False, }) user = request.user try: enrollment = CourseEnrollment.objects.select_related('course').get( user_id=user.id, course_id=course.id) user_upsell = can_show_verified_upgrade(user, enrollment) except CourseEnrollment.DoesNotExist: user_upsell = True basket_url = EcommerceService().upgrade_url(user, course.id) upgrade_price = six.text_type( get_cosmetic_verified_display_price(course)) could_upsell = bool(user_upsell and basket_url) bucket = stable_bucketing_hash_group(MOBILE_UPSELL_EXPERIMENT, 2, user) if could_upsell and hasattr( request, 'session') and MOBILE_UPSELL_EXPERIMENT not in request.session: properties = { 'site': request.site.domain, 'app_label': 'experiments', 'bucket': bucket, 'experiment': 'REV-934', } segment.track( user_id=user.id, event_name='edx.bi.experiment.user.bucketed', properties=properties, ) # Mark that we've recorded this bucketing, so that we don't do it again this session request.session[MOBILE_UPSELL_EXPERIMENT] = True show_upsell = bool(bucket != 0 and could_upsell) if show_upsell: return Response({ 'show_upsell': show_upsell, 'price': upgrade_price, 'basket_url': basket_url, }) else: return Response({ 'show_upsell': show_upsell, 'upsell_flag': MOBILE_UPSELL_FLAG.is_enabled(), 'experiment_bucket': bucket, 'user_upsell': user_upsell, 'basket_url': basket_url, })
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)) 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 enable_self_generated_certs(sender, course_key, **kwargs): # pylint: disable=unused-argument """ Enable/disable the self-generated certificates according to course-pacing. """ course = CourseOverview.get_from_id(course_key) toggle_self_generated_certs.delay(unicode(course_key), course.self_paced)
def post(self, request): """ **Use Case** * Send favorite course through email to user for later learning. **Example Request for course** POST /api/v1/save/course/ **Example POST Request for course** { "email": "*****@*****.**", "course_id": "course-v1:edX+DemoX+2021", "marketing_url": "https://test.com", "org_img_url": "https://test.com/logo.png", "weeks_to_complete": 7, "min_effort": 4, "max_effort": 5, } """ user = request.user data = request.data course_id = data.get('course_id') email = data.get('email') org_img_url = data.get('org_img_url') marketing_url = data.get('marketing_url') weeks_to_complete = data.get('weeks_to_complete', 0) min_effort = data.get('min_effort', 0) max_effort = data.get('max_effort', 0) user_id = request.user.id pref_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME, 'en') send_to_self = bool(not request.user.is_anonymous and request.user.email == email) if getattr(request, 'limited', False): return Response({'error_code': 'rate-limited'}, status=403) if get_email_validation_error(email): return Response({'error_code': 'incorrect-email'}, status=400) try: course_key = CourseKey.from_string(course_id) course = CourseOverview.get_from_id(course_key) except InvalidKeyError: return Response({'error_code': 'invalid-course-key'}, status=400) except CourseOverview.DoesNotExist: return Response({'error_code': 'course-not-found'}, status=404) SavedCourse.objects.update_or_create( user_id=user.id, email=email, course_id=course_id, org_img_url=org_img_url, marketing_url=marketing_url, weeks_to_complete=weeks_to_complete, min_effort=min_effort, max_effort=max_effort, reminder_email_sent=False, ) course_data = { 'course': course, 'send_to_self': send_to_self, 'user_id': user_id, 'pref-lang': pref_lang, 'org_img_url': org_img_url, 'marketing_url': marketing_url, 'weeks_to_complete': weeks_to_complete, 'min_effort': min_effort, 'max_effort': max_effort, 'type': 'course', 'reminder': False, 'braze_event': USER_SEND_SAVE_FOR_LATER_EMAIL, } if send_email(email, course_data): return Response({'result': 'success'}, status=200) else: return Response({'error_code': 'email-not-send'}, status=400)
def is_enabled(cls, course_key: CourseKey) -> bool: """ Get calculator enabled status from course overview model. """ return CourseOverview.get_from_id(course_key).show_calculator
def award_course_certificate(self, username, course_run_key): """ This task is designed to be called whenever a student GeneratedCertificate is updated. It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save. """ LOGGER.info('Running task award_course_certificate for username %s', username) countdown = 2**self.request.retries # If the credentials config model is disabled for this # feature, it may indicate a condition where processing of such tasks # has been temporarily disabled. Since this is a recoverable situation, # mark this task for retry instead of failing it altogether. if not CredentialsApiConfig.current().is_learner_issuance_enabled: LOGGER.warning( 'Task award_course_certificate cannot be executed when credentials issuance is disabled in API config', ) raise self.retry(countdown=countdown, max_retries=MAX_RETRIES) try: course_key = CourseKey.from_string(course_run_key) try: user = User.objects.get(username=username) except User.DoesNotExist: LOGGER.exception( 'Task award_course_certificate was called with invalid username %s', username) # Don't retry for this case - just conclude the task. return # Get the cert for the course key and username if it's both passing and available in professional/verified try: certificate = GeneratedCertificate.eligible_certificates.get( user=user.id, course_id=course_key) except GeneratedCertificate.DoesNotExist: LOGGER.exception( 'Task award_course_certificate was called without Certificate found for %s to user %s', course_key, username) return if certificate.mode in CourseMode.CREDIT_ELIGIBLE_MODES + CourseMode.CREDIT_MODES: try: course_overview = CourseOverview.get_from_id(course_key) except (CourseOverview.DoesNotExist, IOError): LOGGER.exception( 'Task award_course_certificate was called without course overview data for course %s', course_key) return credentials_client = get_credentials_api_client( User.objects.get( username=settings.CREDENTIALS_SERVICE_USERNAME), org=course_key.org, ) # FIXME This may result in visible dates that do not update alongside the Course Overview if that changes # This is a known limitation of this implementation and was chosen to reduce the amount of replication, # endpoints, celery tasks, and jenkins jobs that needed to be written for this functionality visible_date = available_date_for_certificate( course_overview, certificate) post_course_certificate(credentials_client, username, certificate, visible_date) LOGGER.info('Awarded certificate for course %s to user %s', course_key, username) except Exception as exc: LOGGER.exception( 'Failed to determine course certificates to be awarded for user %s', username) raise self.retry(exc=exc, countdown=countdown, max_retries=MAX_RETRIES)
def is_enabled(cls, course_key: CourseKey) -> bool: """ The progress course status is stored in the course module. """ return not CourseOverview.get_from_id(course_key).hide_progress_tab
def award_course_certificate(self, username, course_run_key, certificate_available_date=None): """ This task is designed to be called whenever a student GeneratedCertificate is updated. It can be called independently for a username and a course_run, but is invoked on each GeneratedCertificate.save. Arguments: username (str): The user to award the Credentials course cert to course_run_key (str): The course run key to award the certificate for certificate_available_date (str): A string representation of the datetime for when to make the certificate available to the user. If not provided, it will calculate the date. """ def _retry_with_custom_exception(username, course_run_key, reason, countdown): exception = MaxRetriesExceededError( f"Failed to award course certificate for user {username} for course {course_run_key}. Reason: {reason}" ) return self.retry( exc=exception, countdown=countdown, max_retries=MAX_RETRIES ) LOGGER.info(f"Running task award_course_certificate for username {username}") countdown = 2 ** self.request.retries # If the credentials config model is disabled for this # feature, it may indicate a condition where processing of such tasks # has been temporarily disabled. Since this is a recoverable situation, # mark this task for retry instead of failing it altogether. if not CredentialsApiConfig.current().is_learner_issuance_enabled: error_msg = ( "Task award_course_certificate cannot be executed when credentials issuance is disabled in API config" ) LOGGER.warning(error_msg) raise _retry_with_custom_exception( username=username, course_run_key=course_run_key, reason=error_msg, countdown=countdown ) try: course_key = CourseKey.from_string(course_run_key) try: user = User.objects.get(username=username) except User.DoesNotExist: LOGGER.exception(f"Task award_course_certificate was called with invalid username {username}") # Don't retry for this case - just conclude the task. return # Get the cert for the course key and username if it's both passing and available in professional/verified try: certificate = GeneratedCertificate.eligible_certificates.get( user=user.id, course_id=course_key ) except GeneratedCertificate.DoesNotExist: LOGGER.exception( "Task award_course_certificate was called without Certificate found " f"for {course_key} to user {username}" ) return if certificate.mode in CourseMode.CERTIFICATE_RELEVANT_MODES: try: course_overview = CourseOverview.get_from_id(course_key) except (CourseOverview.DoesNotExist, IOError): LOGGER.exception( f"Task award_course_certificate was called without course overview data for course {course_key}" ) return credentials_client = get_credentials_api_client(User.objects.get( username=settings.CREDENTIALS_SERVICE_USERNAME), org=course_key.org, ) # Date is being passed via JSON and is encoded in the EMCA date time string format. The rest of the code # expects a datetime. if certificate_available_date: certificate_available_date = datetime.strptime(certificate_available_date, VISIBLE_DATE_FORMAT) # Even in the cases where this task is called with a certificate_available_date, we still need to retrieve # the course overview because it's required to determine if we should use the certificate_available_date or # the certs modified date visible_date = available_date_for_certificate( course_overview, certificate, certificate_available_date=certificate_available_date ) LOGGER.info( "Task award_course_certificate will award certificate for course " f"{course_key} with a visible date of {visible_date}" ) post_course_certificate(credentials_client, username, certificate, visible_date) LOGGER.info(f"Awarded certificate for course {course_key} to user {username}") except Exception as exc: error_msg = f"Failed to determine course certificates to be awarded for user {username}." LOGGER.exception(error_msg) raise _retry_with_custom_exception( username=username, course_run_key=course_run_key, reason=error_msg, countdown=countdown ) from exc
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_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 = 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_learning_mfe_home_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 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 ' '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 get(self, request, course_id, chapter=None, section=None, position=None): """ Displays courseware accordion and associated content. If course, chapter, and section are all specified, renders the page, or returns an error if they are invalid. If section is not specified, displays the accordion opened to the right chapter. If neither chapter or section are specified, displays the user's most recent chapter, or the first chapter if this is the user's first visit. Arguments: request: HTTP request course_id (unicode): course id chapter (unicode): chapter url_name section (unicode): section url_name position (unicode): position in module, eg of <sequential> module """ self.course_key = CourseKey.from_string(course_id) if not (request.user.is_authenticated or self.enable_unenrolled_access): return redirect_to_login(request.get_full_path()) self.original_chapter_url_name = chapter self.original_section_url_name = section self.chapter_url_name = chapter self.section_url_name = section self.position = position self.chapter, self.section = None, None self.course = None self.url = request.path try: set_custom_attributes_for_course_key(self.course_key) self._clean_position() with modulestore().bulk_operations(self.course_key): self.view = STUDENT_VIEW self.course = get_course_with_access( request.user, 'load', self.course_key, depth=CONTENT_DEPTH, check_if_enrolled=True, check_if_authenticated=True) self.course_overview = CourseOverview.get_from_id( self.course.id) self.is_staff = has_access(request.user, 'staff', self.course) # There's only one situation where we want to show the public view if (not self.is_staff and self.enable_unenrolled_access and self.course.course_visibility == COURSE_VISIBILITY_PUBLIC and not CourseEnrollment.is_enrolled( request.user, self.course_key)): self.view = PUBLIC_VIEW self.can_masquerade = request.user.has_perm( MASQUERADE_AS_STUDENT, self.course) self._setup_masquerade_for_effective_user() return self.render(request) except Exception as exception: # pylint: disable=broad-except return CourseTabView.handle_exceptions(request, self.course_key, self.course, exception)
def is_enabled(cls, course, user=None): """Returns true if this tab is enabled.""" # We want to only limit this feature to instructor led courses for now (and limit to relative dates experiment) return not CourseOverview.get_from_id( course.id).self_paced and RELATIVE_DATES_FLAG.is_enabled(course.id)
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 = 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'] 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 # 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 ' 'edX Support if you have questions.') 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, request.user): 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 [] 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, '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)