def access_denied_message(self, block_key, user, user_group, allowed_groups): course_key = block_key.course_key modes = CourseMode.modes_for_course_dict(course_key) verified_mode = modes.get(CourseMode.VERIFIED) if (verified_mode is None or user_group == FULL_ACCESS or user_group in allowed_groups): return None request = crum.get_current_request() if request and is_request_from_mobile_app(request): return _("Graded assessments are available to Verified Track learners.") else: return _("Graded assessments are available to Verified Track learners. Upgrade to Unlock.")
def unlink_program_enrollment(program_enrollment): """ Unlinks CourseEnrollments from the ProgramEnrollment by doing the following for each ProgramCourseEnrollment associated with the Program Enrollment. 1. unenrolling the corresponding user from the course 2. moving the user into the audit track, if the track exists 3. removing the link between the ProgramCourseEnrollment and the CourseEnrollment Arguments: program_enrollment: the ProgramEnrollment object """ program_course_enrollments = program_enrollment.program_course_enrollments.all( ) for pce in program_course_enrollments: course_key = pce.course_enrollment.course.id modes = CourseMode.modes_for_course_dict(course_key) update_enrollment_kwargs = { 'is_active': False, 'skip_refund': True, } if CourseMode.contains_audit_mode(modes): # if the course contains an audit mode, move the # learner's enrollment into the audit mode update_enrollment_kwargs['mode'] = 'audit' # deactive the learner's course enrollment and move them into the # audit track, if it exists pce.course_enrollment.update_enrollment(**update_enrollment_kwargs) # sever ties to the user from the ProgramCourseEnrollment pce.course_enrollment = None pce.save() program_enrollment.user = None program_enrollment.save()
def test_default_mode_creation(self): # Hit the mode creation endpoint with no querystring params, to create an honor mode url = reverse('create_mode', args=[str(self.course.id)]) response = self.client.get(url) assert response.status_code == 200 expected_mode = [ Mode('honor', 'Honor Code Certificate', 0, '', 'usd', None, None, None, None) ] course_mode = CourseMode.modes_for_course(self.course.id) assert course_mode == expected_mode
def test_update_professional_expiration(self, mode_slug, expiration_datetime_name): """ Verify that pushing a mode with a professional certificate and an expiration datetime will be rejected (this is not allowed). """ expiration_datetime = self.DATES[expiration_datetime_name] mode = self._serialize_course_mode( CourseMode(mode_slug=mode_slug, min_price=500, currency='USD', sku='ABC123', bulk_sku='BULK-ABC123', expiration_datetime=expiration_datetime)) course_id = str(self.course.id) payload = {'id': course_id, 'modes': [mode]} path = reverse('commerce_api:v1:courses:retrieve_update', args=[course_id]) expected_status = 400 if CourseMode.is_professional_slug( mode_slug) and expiration_datetime is not None else 200 response = self.client.put(path, json.dumps(payload), content_type=JSON_CONTENT_TYPE) assert response.status_code == expected_status
def test_default_mode_creation(self): # Hit the mode creation endpoint with no querystring params, to create an honor mode url = reverse('create_mode', args=[six.text_type(self.course.id)]) response = self.client.get(url) self.assertEqual(response.status_code, 200) expected_mode = [ Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None) ] course_mode = CourseMode.modes_for_course(self.course.id) self.assertEqual(course_mode, expected_mode)
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"], six.text_type(course_mode.name)) else: return None
def test_course_without_sku_honor(self): """ If the course does not have an SKU and has an honor mode, the user should be enrolled as honor. This ensures backwards compatibility with courses existing before the removal of honor certificates. """ # Remove all existing course modes CourseMode.objects.filter(course_id=self.course.id).delete() # Ensure that honor mode exists CourseMode(mode_slug=CourseMode.HONOR, mode_display_name="Honor Cert", course_id=self.course.id).save() # We should be enrolled in honor mode self._test_course_without_sku(enrollment_mode=CourseMode.HONOR)
def test_min_course_price_for_currency(self): """ Get the min course price for a course according to currency """ # no modes, should get 0 assert 0 == CourseMode.min_course_price_for_currency( self.course_key, 'usd') # create some modes mode1 = Mode('honor', 'Honor Code Certificate', 10, '', 'usd', None, None, None, None) mode2 = Mode('verified', 'Verified Certificate', 20, '', 'usd', None, None, None, None) mode3 = Mode('honor', 'Honor Code Certificate', 80, '', 'cny', None, None, None, None) set_modes = [mode1, mode2, mode3] for mode in set_modes: self.create_mode(mode.slug, mode.name, mode.min_price, mode.suggested_prices, mode.currency) assert 10 == CourseMode.min_course_price_for_currency( self.course_key, 'usd') assert 80 == CourseMode.min_course_price_for_currency( self.course_key, 'cny')
def test_invalid_mode_expiration(self, mode_slug, exp_dt_name): exp_dt = self.DATES[exp_dt_name] is_error_expected = CourseMode.is_professional_slug( mode_slug) and exp_dt is not None try: self.create_mode(mode_slug=mode_slug, mode_name=mode_slug.title(), expiration_datetime=exp_dt, min_price=10) assert not is_error_expected, 'Expected a ValidationError to be thrown.' except ValidationError as exc: assert is_error_expected, 'Did not expect a ValidationError to be thrown.' assert exc.messages == [ 'Professional education modes are not allowed to have expiration_datetime set.' ]
def _section_send_email(course, access): """ Provide data for the corresponding bulk email section """ course_key = course.id # Monkey-patch applicable_aside_types to return no asides for the duration of this render with patch.object(course.runtime, 'applicable_aside_types', null_applicable_aside_types): # This HtmlBlock is only being used to generate a nice text editor. html_module = HtmlBlock( course.system, DictFieldData({'data': ''}), ScopeIds(None, None, None, course_key.make_usage_key('html', 'fake')) ) fragment = course.system.render(html_module, 'studio_view') fragment = wrap_xblock( 'LmsRuntime', html_module, 'studio_view', fragment, None, extra_data={"course-id": str(course_key)}, usage_id_serializer=lambda usage_id: quote_slashes(str(usage_id)), # Generate a new request_token here at random, because this module isn't connected to any other # xblock rendering. request_token=uuid.uuid1().hex ) cohorts = [] if is_course_cohorted(course_key): cohorts = get_course_cohorts(course) course_modes = [] if not VerifiedTrackCohortedCourse.is_verified_track_cohort_enabled(course_key): course_modes = CourseMode.modes_for_course(course_key, include_expired=True, only_selectable=False) email_editor = fragment.content section_data = { 'section_key': 'send_email', 'section_display_name': _('Email'), 'access': access, 'send_email': reverse('send_email', kwargs={'course_id': str(course_key)}), 'editor': email_editor, 'cohorts': cohorts, 'course_modes': course_modes, 'default_cohort_name': DEFAULT_COHORT_NAME, 'list_instructor_tasks_url': reverse( 'list_instructor_tasks', kwargs={'course_id': str(course_key)} ), 'email_background_tasks_url': reverse( 'list_background_email_tasks', kwargs={'course_id': str(course_key)} ), 'email_content_history_url': reverse( 'list_email_content', kwargs={'course_id': str(course_key)} ), } return section_data
def _get_update_response_and_expected_data(self, mode_expiration, verification_deadline): """ Returns expected data and response for course update. """ expected_course_mode = CourseMode( mode_slug=u'verified', min_price=200, currency=u'USD', sku=u'ABC123', bulk_sku=u'BULK-ABC123', expiration_datetime=mode_expiration ) expected = self._serialize_course(self.course, [expected_course_mode], verification_deadline) # Sanity check: The API should return HTTP status 200 for updates response = self.client.put(self.path, json.dumps(expected), content_type=JSON_CONTENT_TYPE) return response, expected
def is_course_run_entitlement_fulfillable( course_run_key, entitlement, compare_date=timezone.now(), ): """ Checks that the current run meets the following criteria for an entitlement 1) A User can enroll in or is currently enrolled 2) A User can upgrade to the entitlement mode Arguments: course_run_key (CourseKey): The id of the Course run that is being checked. entitlement: The Entitlement that we are checking against. compare_date: The date and time that we are comparing against. Defaults to timezone.now() Returns: bool: True if the Course Run is fullfillable for the CourseEntitlement. """ try: course_overview = CourseOverview.get_from_id(course_run_key) except CourseOverview.DoesNotExist: log.error(( u'There is no CourseOverview entry available for {course_run_id}, ' u'course run cannot be applied to entitlement').format( course_run_id=str(course_run_key))) return False # Verify that the course run can currently be enrolled enrollment_start = course_overview.enrollment_start enrollment_end = course_overview.enrollment_end can_enroll = ((not enrollment_start or enrollment_start < compare_date) and (not enrollment_end or enrollment_end > compare_date)) # Is the user already enrolled in the Course Run is_enrolled = CourseEnrollment.is_enrolled(entitlement.user, course_run_key) # Ensure the course run is upgradeable and the mode matches the entitlement's mode unexpired_paid_modes = [ mode.slug for mode in CourseMode.paid_modes_for_course(course_run_key) ] can_upgrade = unexpired_paid_modes and entitlement.mode in unexpired_paid_modes return course_overview.start and can_upgrade and (is_enrolled or can_enroll)
def get_course_modes(course_key): """ Returns a list of all modes including expired modes for a given course id Arguments: course_id (CourseKey): Search for course modes for this course. Returns: list of `Mode` """ course_modes = CourseMode.modes_for_course( course_key, include_expired=True, only_selectable=False, ) return [ModeSerializer(mode).data for mode in course_modes]
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 update(self, attrs): """ Update the model with external data (usually passed via API call). """ # There are possible downstream effects of settings self.verification_deadline to null, # so don't assign it a value here unless it is specifically included in attrs. if 'verification_deadline' in attrs: self.verification_deadline = attrs.get('verification_deadline') existing_modes = {mode.mode_slug: mode for mode in self.modes} merged_modes = set() merged_mode_keys = set() for posted_mode in attrs.get('modes', []): merged_mode = existing_modes.get(posted_mode.mode_slug, CourseMode()) merged_mode.course_id = self.id merged_mode.mode_slug = posted_mode.mode_slug merged_mode.mode_display_name = posted_mode.mode_slug merged_mode.min_price = posted_mode.min_price merged_mode.currency = posted_mode.currency merged_mode.sku = posted_mode.sku merged_mode.bulk_sku = posted_mode.bulk_sku merged_mode.expiration_datetime = posted_mode.expiration_datetime merged_mode.save() merged_modes.add(merged_mode) merged_mode_keys.add(merged_mode.mode_slug) # Masters degrees are not sold through the eCommerce site. # So, Masters course modes are not included in PUT calls to this API, # and their omission which would normally cause them to be deleted. # We don't want that to happen, but for the time being, # we cannot include in Masters modes in the PUT calls from eCommerce. # So, here's hack to handle Masters course modes, along with any other # modes that end up in that boat. MODES_TO_NOT_DELETE = { CourseMode.MASTERS, } modes_to_delete = set(existing_modes.keys()) - merged_mode_keys modes_to_delete -= MODES_TO_NOT_DELETE self._deleted_modes = [ existing_modes[mode] for mode in modes_to_delete ] self.modes = list(merged_modes)
def groups(self): """ Return the groups (based on CourseModes) for the course associated with this EnrollmentTrackUserPartition instance. Note that only groups based on selectable CourseModes are returned (which means that Credit will never be returned). If a course is using the Verified Track Cohorting pilot feature, this method returns an empty array regardless of registered CourseModes. """ course_key = CourseKey.from_string(self.parameters["course_id"]) if is_course_using_cohort_instead(course_key): return [] return [ Group(ENROLLMENT_GROUP_IDS[mode.slug]["id"], six.text_type(mode.name)) for mode in CourseMode.modes_for_course(course_key, include_expired=True) ]
def serialize_upgrade_info(user, course_overview, enrollment): """ Return verified mode upgrade information, or None. This is used in a few API views to provide consistent upgrade info to frontends. """ if not can_show_verified_upgrade(user, enrollment): return None mode = CourseMode.verified_mode_for_course(course=course_overview) return { 'access_expiration_date': get_user_course_expiration_date(user, course_overview), 'currency': mode.currency.upper(), 'currency_symbol': get_currency_symbol(mode.currency.upper()), 'price': mode.min_price, 'sku': mode.sku, 'upgrade_url': verified_upgrade_deadline_link(user, course_overview), }
def certificate_status(generated_certificate): """ This returns a dictionary with a key for status, and other information. If the status is "downloadable", the dictionary also contains "download_url". If the student has been graded, the dictionary also contains their grade for the course with the key "grade". """ # Import here instead of top of file since this module gets imported before # the course_modes app is loaded, resulting in a Django deprecation warning. from common.djangoapps.course_modes.models import CourseMode # pylint: disable=redefined-outer-name, reimported if generated_certificate: cert_status = { 'status': generated_certificate.status, 'mode': generated_certificate.mode, 'uuid': generated_certificate.verify_uuid, } if generated_certificate.grade: cert_status['grade'] = generated_certificate.grade if generated_certificate.mode == 'audit': course_mode_slugs = [ mode.slug for mode in CourseMode.modes_for_course( generated_certificate.course_id) ] # Short term fix to make sure old audit users with certs still see their certs # only do this if there if no honor mode if 'honor' not in course_mode_slugs: cert_status['status'] = CertificateStatuses.auditing return cert_status if generated_certificate.status == CertificateStatuses.downloadable: cert_status['download_url'] = generated_certificate.download_url return cert_status else: return { 'status': CertificateStatuses.unavailable, 'mode': GeneratedCertificate.MODES.honor, 'uuid': None }
def get_celebrations_dict(user, enrollment, course, browser_timezone): """ Returns a dict of celebrations that should be performed. """ if not enrollment: return { 'first_section': False, 'streak_length_to_celebrate': None, 'streak_discount_enabled': False, } streak_length_to_celebrate = UserCelebration.perform_streak_updates( user, course.id, browser_timezone) celebrations = { 'first_section': CourseEnrollmentCelebration.should_celebrate_first_section(enrollment), 'streak_length_to_celebrate': streak_length_to_celebrate, 'streak_discount_enabled': False, } if streak_length_to_celebrate: # We only want to offer the streak discount # if the course has not ended, is upgradeable and the user is not an enterprise learner if can_show_streak_discount_coupon(user, course): # Send course streak coupon event course_key = str(course.id) modes_dict = CourseMode.modes_for_course_dict( course_id=course_key, include_expired=False) verified_mode = modes_dict.get('verified', None) if verified_mode: celebrations['streak_discount_enabled'] = True segment.track( user_id=user.id, event_name='edx.bi.course.streak_discount_enabled', properties={ 'course_id': str(course_key), 'sku': verified_mode.sku, }) return celebrations
def test_create_with_non_existent_course(self): """ Verify the API does not allow data to be created for courses that do not exist. """ permissions = Permission.objects.filter( name__in=('Can add course mode', 'Can change course mode')) for permission in permissions: self.user.user_permissions.add(permission) expected_modes = [ CourseMode(mode_slug=CourseMode.DEFAULT_MODE_SLUG, min_price=150, currency=u'USD', sku=u'ABC123', bulk_sku=u'BULK-ABC123') ] course_key = 'non/existing/key' course_dict = { u'id': six.text_type(course_key), u'name': six.text_type('Non Existing Course'), u'verification_deadline': None, u'modes': [self._serialize_course_mode(mode) for mode in expected_modes] } path = reverse('commerce_api:v1:courses:retrieve_update', args=[six.text_type(course_key)]) response = self.client.put(path, json.dumps(course_dict), content_type=JSON_CONTENT_TYPE) self.assertEqual(response.status_code, 400) expected_dict = { 'id': [u'Course {} does not exist.'.format(course_key)] } self.assertDictEqual(expected_dict, json.loads(response.content.decode('utf-8')))
def test_invalid_mode_expiration(self, mode_slug, exp_dt_name): exp_dt = self.DATES[exp_dt_name] is_error_expected = CourseMode.is_professional_slug( mode_slug) and exp_dt is not None try: self.create_mode(mode_slug=mode_slug, mode_name=mode_slug.title(), expiration_datetime=exp_dt, min_price=10) self.assertFalse(is_error_expected, "Expected a ValidationError to be thrown.") except ValidationError as exc: self.assertTrue(is_error_expected, "Did not expect a ValidationError to be thrown.") self.assertEqual( exc.messages, [ u"Professional education modes are not allowed to have expiration_datetime set." ], )
def _calculate_upgrade_deadline(course_id, content_availability_date): # lint-amnesty, pylint: disable=missing-function-docstring upgrade_deadline = None delta = _get_upgrade_deadline_delta_setting(course_id) if delta is not None: upgrade_deadline = content_availability_date + datetime.timedelta(days=delta) if upgrade_deadline is not None: # The content availability-based deadline should never occur # after the verified mode's expiration date, if one is set. try: verified_mode = CourseMode.verified_mode_for_course(course_id) except CourseMode.DoesNotExist: pass else: if verified_mode: course_mode_upgrade_deadline = verified_mode.expiration_datetime if course_mode_upgrade_deadline is not None: upgrade_deadline = min(upgrade_deadline, course_mode_upgrade_deadline) return upgrade_deadline
def test_all_modes_for_courses(self): now_dt = now() future = now_dt + timedelta(days=1) past = now_dt - timedelta(days=1) # Unexpired, no expiration date CourseModeFactory.create( course_id=self.course_key, mode_display_name="Honor No Expiration", mode_slug="honor_no_expiration", expiration_datetime=None ) # Unexpired, expiration date in future CourseModeFactory.create( course_id=self.course_key, mode_display_name="Honor Not Expired", mode_slug="honor_not_expired", expiration_datetime=future ) # Expired CourseModeFactory.create( course_id=self.course_key, mode_display_name="Verified Expired", mode_slug="verified_expired", expiration_datetime=past ) # We should get all of these back when querying for *all* course modes, # including ones that have expired. other_course_key = CourseLocator(org="not", course="a", run="course") all_modes = CourseMode.all_modes_for_courses([self.course_key, other_course_key]) assert len(all_modes[self.course_key]) == 3 assert all_modes[self.course_key][0].name == 'Honor No Expiration' assert all_modes[self.course_key][1].name == 'Honor Not Expired' assert all_modes[self.course_key][2].name == 'Verified Expired' # Check that we get a default mode for when no course mode is available assert len(all_modes[other_course_key]) == 1 assert all_modes[other_course_key][0] == CourseMode.DEFAULT_MODE
def _display_steps(self, always_show_payment, already_verified, already_paid, course_mode): """Determine which steps to display to the user. Includes all steps by default, but removes steps if the user has already completed them. Arguments: always_show_payment (bool): If True, display the payment steps even if the user has already paid. already_verified (bool): Whether the user has submitted a verification request recently. already_paid (bool): Whether the user is enrolled in a paid course mode. Returns: list """ display_steps = self.ALL_STEPS remove_steps = set() if already_verified or not CourseMode.is_verified_mode(course_mode): remove_steps |= set(self.VERIFICATION_STEPS) if already_paid and not always_show_payment: remove_steps |= set(self.PAYMENT_STEPS) else: # The "make payment" step doubles as an intro step, # so if we're showing the payment step, hide the intro step. remove_steps |= set([self.INTRO_STEP]) return [ { 'name': step, 'title': six.text_type(self.STEP_TITLES[step]), } for step in display_steps if step not in remove_steps ]
def _enrollment_mode_display(enrollment_mode, course_id): """Checking enrollment mode and status and returns the display mode Args: enrollment_mode (str): enrollment mode. verification_status (str) : verification status of student Returns: display_mode (str) : display mode for certs """ course_mode_slugs = [mode.slug for mode in CourseMode.modes_for_course(course_id)] if enrollment_mode == CourseMode.VERIFIED: display_mode = DISPLAY_VERIFIED if DISPLAY_HONOR in course_mode_slugs: display_mode = DISPLAY_HONOR elif enrollment_mode in [CourseMode.PROFESSIONAL, CourseMode.NO_ID_PROFESSIONAL_MODE]: display_mode = DISPLAY_PROFESSIONAL else: display_mode = enrollment_mode return display_mode
def get_user_course_duration(user, course): """ Return a timedelta measuring the duration of the course for a particular user. Business Logic: - Course access duration is bounded by the min and max duration. - If course fields are missing, default course access duration to MIN_DURATION. """ if not CourseDurationLimitConfig.enabled_for_enrollment(user, course): return None enrollment = CourseEnrollment.get_enrollment(user, course.id) if enrollment is None or enrollment.mode != CourseMode.AUDIT: return None verified_mode = CourseMode.verified_mode_for_course(course=course, include_expired=True) if not verified_mode: return None return get_expected_duration(course.id)
def set_entitlement_policy(entitlement, site): """ Assign the appropriate CourseEntitlementPolicy to the given CourseEntitlement based on its mode and site. Arguments: entitlement: Course Entitlement object site: string representation of a Site object Notes: Site-specific, mode-agnostic policies take precedence over mode-specific, site-agnostic policies. If no appropriate CourseEntitlementPolicy is found, the default CourseEntitlementPolicy is assigned. """ policy_mode = entitlement.mode if CourseMode.is_professional_slug(policy_mode): policy_mode = CourseMode.PROFESSIONAL filter_query = (Q(site=site) | Q(site__isnull=True)) & ( Q(mode=policy_mode) | Q(mode__isnull=True)) policy = CourseEntitlementPolicy.objects.filter(filter_query).order_by( '-site', '-mode').first() entitlement.policy = policy if policy else None entitlement.save()
def test_verified_mode_creation(self, mode_slug, mode_display_name, min_price, suggested_prices, currency): parameters = {} parameters['mode_slug'] = mode_slug parameters['mode_display_name'] = mode_display_name parameters['min_price'] = min_price parameters['suggested_prices'] = suggested_prices parameters['currency'] = currency url = reverse('create_mode', args=[str(self.course.id)]) response = self.client.get(url, parameters) assert response.status_code == 200 expected_mode = [ Mode(mode_slug, mode_display_name, min_price, suggested_prices, currency, None, None, None, None) ] course_mode = CourseMode.modes_for_course(self.course.id) assert course_mode == expected_mode
def test_update_overwrite(self): """ Verify that data submitted via PUT overwrites/deletes modes that are not included in the body of the request, EXCEPT the Masters mode, which it leaves alone. """ existing_mode = self.course_mode existing_masters_mode = CourseMode.objects.create( course_id=self.course.id, mode_slug=u'masters', min_price=10000, currency=u'USD', sku=u'DEF456', bulk_sku=u'BULK-DEF456' ) new_mode = CourseMode( course_id=self.course.id, mode_slug=u'credit', min_price=500, currency=u'USD', sku=u'ABC123', bulk_sku=u'BULK-ABC123' ) path = reverse('commerce_api:v1:courses:retrieve_update', args=[six.text_type(self.course.id)]) data = json.dumps(self._serialize_course(self.course, [new_mode])) response = self.client.put(path, data, content_type=JSON_CONTENT_TYPE) self.assertEqual(response.status_code, 200) # Check modes list in response, disregarding its order. expected_dict = self._serialize_course(self.course, [new_mode]) expected_items = expected_dict['modes'] actual_items = json.loads(response.content.decode('utf-8'))['modes'] self.assertCountEqual(actual_items, expected_items) # The existing non-Masters CourseMode should have been removed. self.assertFalse(CourseMode.objects.filter(id=existing_mode.id).exists()) # The existing Masters course mode should remain. self.assertTrue(CourseMode.objects.filter(id=existing_masters_mode.id).exists())
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 ''