Merge pull request #20303 from edx/dcs/masters-access
Automatically add master's to the group access of verified content
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
"""Django admin for course_modes"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import six
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
@@ -22,7 +25,8 @@ from lms.djangoapps.verify_student import models as verification_models
|
||||
from openedx.core.lib.courses import clean_course_id
|
||||
from util.date_utils import get_time_display
|
||||
|
||||
COURSE_MODE_SLUG_CHOICES = [(key, enrollment_mode['display_name']) for key, enrollment_mode in six.iteritems(settings.COURSE_ENROLLMENT_MODES)]
|
||||
COURSE_MODE_SLUG_CHOICES = [(key, enrollment_mode['display_name'])
|
||||
for key, enrollment_mode in six.iteritems(settings.COURSE_ENROLLMENT_MODES)]
|
||||
|
||||
|
||||
class CourseModeForm(forms.ModelForm):
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Course modes API serializers.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"""
|
||||
URL definitions for the course_modes API.
|
||||
"""
|
||||
from django.conf.urls import include, url
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.conf.urls import include, url
|
||||
|
||||
app_name = 'common.djangoapps.course_modes.api'
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
URL definitions for the course_modes v1 API.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Defines the "ReSTful" API for course modes.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
import logging
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"""Django App config for course_modes"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
@@ -7,4 +9,4 @@ class CourseModesConfig(AppConfig):
|
||||
verbose_name = "Course Modes"
|
||||
|
||||
def ready(self):
|
||||
import course_modes.signals # pylint: disable=unused-import
|
||||
import course_modes.signals # pylint: disable=unused-variable
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
""" Helper methods for CourseModes. """
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import six
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
@@ -48,10 +51,10 @@ def enrollment_mode_display(mode, verification_status, course_id):
|
||||
enrollment_value = _("Professional Ed")
|
||||
|
||||
return {
|
||||
'enrollment_title': unicode(enrollment_title),
|
||||
'enrollment_value': unicode(enrollment_value),
|
||||
'enrollment_title': six.text_type(enrollment_title),
|
||||
'enrollment_value': six.text_type(enrollment_value),
|
||||
'show_image': show_image,
|
||||
'image_alt': unicode(image_alt),
|
||||
'image_alt': six.text_type(image_alt),
|
||||
'display_mode': _enrollment_mode_display(mode, verification_status, course_id)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"""
|
||||
Add and create new modes for running courses on this particular LMS
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
from collections import defaultdict, namedtuple
|
||||
from datetime import timedelta
|
||||
|
||||
import six
|
||||
from config_models.models import ConfigurationModel
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
@@ -14,8 +17,8 @@ from django.dispatch import receiver
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from edx_django_utils.cache import RequestCache
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.django.models import CourseKeyField
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.lib.cache_utils import request_cached
|
||||
@@ -59,7 +62,7 @@ class CourseMode(models.Model):
|
||||
|
||||
@course_id.setter
|
||||
def course_id(self, value):
|
||||
if isinstance(value, basestring):
|
||||
if isinstance(value, six.string_types):
|
||||
self._course_id = CourseKey.from_string(value)
|
||||
else:
|
||||
self._course_id = value
|
||||
@@ -296,7 +299,7 @@ class CourseMode(models.Model):
|
||||
mode for mode in modes
|
||||
if mode.expiration_datetime is None or mode.expiration_datetime >= now_dt
|
||||
]
|
||||
for course_id, modes in all_modes.iteritems()
|
||||
for course_id, modes in six.iteritems(all_modes)
|
||||
}
|
||||
|
||||
return (all_modes, unexpired_modes)
|
||||
@@ -910,4 +913,4 @@ class CourseModeExpirationConfig(ConfigurationModel):
|
||||
|
||||
def __unicode__(self):
|
||||
""" Returns the unicode date of the verification window. """
|
||||
return unicode(self.verification_window)
|
||||
return six.text_type(self.verification_window)
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
"""
|
||||
Signal handler for setting default course mode expiration dates
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import logging
|
||||
|
||||
from crum import get_current_user
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch.dispatcher import receiver
|
||||
|
||||
from xmodule.modulestore.django import SignalHandler, modulestore
|
||||
from xmodule.modulestore.exceptions import ItemNotFoundError
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
|
||||
from .models import CourseMode, CourseModeExpirationConfig
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(SignalHandler.course_published)
|
||||
def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument
|
||||
@@ -35,3 +46,38 @@ def _listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable
|
||||
def _should_update_date(verified_mode):
|
||||
""" Returns whether or not the verified mode should be updated. """
|
||||
return not(verified_mode is None or verified_mode.expiration_datetime_is_explicit)
|
||||
|
||||
|
||||
@receiver(post_save, sender=CourseMode)
|
||||
def update_masters_access_course(sender, instance, **kwargs): # pylint: disable=unused-argument
|
||||
"""
|
||||
Update all blocks in the verified content group to include the master's content group
|
||||
"""
|
||||
if instance.mode_slug != CourseMode.MASTERS:
|
||||
return
|
||||
masters_id = getattr(settings, 'COURSE_ENROLLMENT_MODES', {}).get('masters', {}).get('id', None)
|
||||
verified_id = getattr(settings, 'COURSE_ENROLLMENT_MODES', {}).get('verified', {}).get('id', None)
|
||||
if not (masters_id and verified_id):
|
||||
log.error("Missing settings.COURSE_ENROLLMENT_MODES -> verified:%s masters:%s", verified, masters)
|
||||
return
|
||||
|
||||
course_id = instance.course_id
|
||||
user = get_current_user()
|
||||
user_id = user.id if user else None
|
||||
store = modulestore()
|
||||
|
||||
with store.bulk_operations(course_id):
|
||||
try:
|
||||
items = store.get_items(course_id, settings={'group_access': {'$exists': True}}, include_orphans=False)
|
||||
except ItemNotFoundError:
|
||||
return
|
||||
for item in items:
|
||||
group_access = item.group_access
|
||||
enrollment_groups = group_access.get(ENROLLMENT_TRACK_PARTITION_ID, None)
|
||||
if enrollment_groups is not None:
|
||||
if verified_id in enrollment_groups and masters_id not in enrollment_groups:
|
||||
enrollment_groups.append(masters_id)
|
||||
item.group_access = group_access
|
||||
log.info("Publishing %s with Master's group access", item.location)
|
||||
store.update_item(item, user_id)
|
||||
store.publish(item.location, user_id)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""
|
||||
Factories for course mode models.
|
||||
"""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import random
|
||||
|
||||
from factory import lazy_attribute
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""
|
||||
Tests for the course modes Django admin interface.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import ddt
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from pytz import UTC, timezone
|
||||
@@ -12,12 +15,12 @@ from pytz import UTC, timezone
|
||||
from course_modes.admin import CourseModeForm
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
# Technically, we shouldn't be importing verify_student, since it's
|
||||
# defined in the LMS and course_modes is in common. However, the benefits
|
||||
# of putting all this configuration in one place outweigh the downsides.
|
||||
# Once the course admin tool is deployed, we can remove this dependency.
|
||||
from lms.djangoapps.verify_student.models import VerificationDeadline
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from student.tests.factories import UserFactory
|
||||
from util.date_utils import get_time_display
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -44,7 +47,7 @@ class AdminCourseModePageTest(ModuleStoreTestCase):
|
||||
CourseOverview.load_from_module_store(course.id)
|
||||
|
||||
data = {
|
||||
'course': unicode(course.id),
|
||||
'course': six.text_type(course.id),
|
||||
'mode_slug': 'verified',
|
||||
'mode_display_name': 'verified',
|
||||
'min_price': 10,
|
||||
@@ -199,7 +202,7 @@ class AdminCourseModeFormTest(ModuleStoreTestCase):
|
||||
mode_slug=mode,
|
||||
)
|
||||
return CourseModeForm({
|
||||
"course": unicode(self.course.id),
|
||||
"course": six.text_type(self.course.id),
|
||||
"mode_slug": mode,
|
||||
"mode_display_name": mode,
|
||||
"_expiration_datetime": upgrade_deadline,
|
||||
|
||||
@@ -4,6 +4,7 @@ when you run "manage.py test".
|
||||
|
||||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import itertools
|
||||
from datetime import timedelta
|
||||
@@ -14,14 +15,13 @@ from django.test import TestCase, override_settings
|
||||
from django.utils.timezone import now
|
||||
from mock import patch
|
||||
from opaque_keys.edx.locator import CourseLocator
|
||||
from six.moves import zip
|
||||
|
||||
from course_modes.helpers import enrollment_mode_display
|
||||
from course_modes.models import CourseMode, Mode, invalidate_course_mode_cache, get_cosmetic_display_price
|
||||
from course_modes.models import CourseMode, Mode, get_cosmetic_display_price, invalidate_course_mode_cache
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import (
|
||||
ModuleStoreTestCase,
|
||||
)
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -41,6 +41,7 @@ class CourseModeModelTest(TestCase):
|
||||
CourseMode.objects.all().delete()
|
||||
|
||||
def tearDown(self):
|
||||
super(CourseModeModelTest, self).tearDown()
|
||||
invalidate_course_mode_cache(sender=None)
|
||||
|
||||
def create_mode(
|
||||
@@ -398,11 +399,11 @@ class CourseModeModelTest(TestCase):
|
||||
|
||||
# Check the selectable modes, which should exclude credit
|
||||
selectable_modes = CourseMode.modes_for_course_dict(self.course_key)
|
||||
self.assertItemsEqual(selectable_modes.keys(), expected_selectable_modes)
|
||||
self.assertItemsEqual(list(selectable_modes.keys()), expected_selectable_modes)
|
||||
|
||||
# When we get all unexpired modes, we should see credit as well
|
||||
all_modes = CourseMode.modes_for_course_dict(self.course_key, only_selectable=False)
|
||||
self.assertItemsEqual(all_modes.keys(), available_modes)
|
||||
self.assertItemsEqual(list(all_modes.keys()), available_modes)
|
||||
|
||||
def _enrollment_display_modes_dicts(self, dict_type):
|
||||
"""
|
||||
@@ -421,11 +422,11 @@ class CourseModeModelTest(TestCase):
|
||||
'professional']
|
||||
}
|
||||
if dict_type in ['verify_need_to_verify', 'verify_submitted']:
|
||||
return dict(zip(dict_keys, display_values.get('verify_need_to_verify')))
|
||||
return dict(list(zip(dict_keys, display_values.get('verify_need_to_verify'))))
|
||||
elif dict_type is None or dict_type == 'dummy':
|
||||
return dict(zip(dict_keys, display_values.get('verify_none')))
|
||||
return dict(list(zip(dict_keys, display_values.get('verify_none'))))
|
||||
else:
|
||||
return dict(zip(dict_keys, display_values.get(dict_type)))
|
||||
return dict(list(zip(dict_keys, display_values.get(dict_type))))
|
||||
|
||||
def test_expiration_datetime_explicitly_set(self):
|
||||
""" Verify that setting the expiration_date property sets the explicit flag. """
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""
|
||||
Unit tests for the course_mode signals
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@@ -10,8 +11,11 @@ from pytz import UTC
|
||||
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.signals import _listen_for_course_publish
|
||||
from django.conf import settings
|
||||
from xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory
|
||||
from xmodule.partitions.partitions import ENROLLMENT_TRACK_PARTITION_ID
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
@@ -87,3 +91,31 @@ class CourseModeSignalTest(ModuleStoreTestCase):
|
||||
course_mode.refresh_from_db()
|
||||
|
||||
self.assertEqual(course_mode.expiration_datetime, self.end - timedelta(days=verification_window))
|
||||
|
||||
def test_masters_mode(self):
|
||||
# create an xblock with verified group access
|
||||
AUDIT_ID = settings.COURSE_ENROLLMENT_MODES['audit']['id']
|
||||
VERIFIED_ID = settings.COURSE_ENROLLMENT_MODES['verified']['id']
|
||||
MASTERS_ID = settings.COURSE_ENROLLMENT_MODES['masters']['id']
|
||||
verified_section = ItemFactory.create(
|
||||
category="sequential",
|
||||
metadata={'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [VERIFIED_ID]}}
|
||||
)
|
||||
# and a section with no restriction
|
||||
section2 = ItemFactory.create(
|
||||
category="sequential",
|
||||
)
|
||||
section3 = ItemFactory.create(
|
||||
category='sequential',
|
||||
metadata={'group_access': {ENROLLMENT_TRACK_PARTITION_ID: [AUDIT_ID]}}
|
||||
)
|
||||
with self.store.branch_setting(ModuleStoreEnum.Branch.draft_preferred):
|
||||
# create the master's mode. signal will add masters to the verified section
|
||||
self.create_mode('masters', 'masters')
|
||||
verified_section_ret = self.store.get_item(verified_section.location)
|
||||
section2_ret = self.store.get_item(section2.location)
|
||||
section3_ret = self.store.get_item(section3.location)
|
||||
# the verified section will now also be visible to master's
|
||||
assert verified_section_ret.group_access[ENROLLMENT_TRACK_PARTITION_ID] == [VERIFIED_ID, MASTERS_ID]
|
||||
assert section2_ret.group_access == {}
|
||||
assert section3_ret.group_access == {ENROLLMENT_TRACK_PARTITION_ID: [AUDIT_ID]}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
Tests for course_modes views.
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import decimal
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
@@ -10,6 +12,7 @@ import ddt
|
||||
import freezegun
|
||||
import httpretty
|
||||
import pytz
|
||||
import six
|
||||
from django.conf import settings
|
||||
from django.urls import reverse
|
||||
from mock import patch
|
||||
@@ -84,7 +87,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
)
|
||||
|
||||
# Configure whether we're upgrading or not
|
||||
url = reverse('course_modes_choose', args=[unicode(course.id)])
|
||||
url = reverse('course_modes_choose', args=[six.text_type(course.id)])
|
||||
response = self.client.get(url)
|
||||
|
||||
# Check whether we were correctly redirected
|
||||
@@ -111,11 +114,11 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
)
|
||||
|
||||
# Configure whether we're upgrading or not
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
# Check whether we were correctly redirected
|
||||
purchase_workflow = "?purchase_workflow=single"
|
||||
start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow
|
||||
start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow
|
||||
self.assertRedirects(response, start_flow_url)
|
||||
|
||||
def test_no_id_redirect_otto(self):
|
||||
@@ -132,7 +135,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
user=self.user
|
||||
)
|
||||
# Configure whether we're upgrading or not
|
||||
url = reverse('course_modes_choose', args=[unicode(prof_course.id)])
|
||||
url = reverse('course_modes_choose', args=[six.text_type(prof_course.id)])
|
||||
response = self.client.get(url)
|
||||
self.assertRedirects(response, 'http://testserver/test_basket/add/?sku=TEST', fetch_redirect_response=False)
|
||||
ecomm_test_utils.update_commerce_config(enabled=False)
|
||||
@@ -166,7 +169,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
|
||||
# Verify that the prices render correctly
|
||||
response = self.client.get(
|
||||
reverse('course_modes_choose', args=[unicode(self.course.id)]),
|
||||
reverse('course_modes_choose', args=[six.text_type(self.course.id)]),
|
||||
follow=False,
|
||||
)
|
||||
|
||||
@@ -187,7 +190,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
|
||||
# Check whether credit upsell is shown on the page
|
||||
# This should *only* be shown when a credit mode is available
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
|
||||
if show_upsell:
|
||||
@@ -201,13 +204,13 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=1)
|
||||
|
||||
# Go to the "choose your track" page
|
||||
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(choose_track_url)
|
||||
|
||||
# Since the only available track is professional ed, expect that
|
||||
# we're redirected immediately to the start of the payment flow.
|
||||
purchase_workflow = "?purchase_workflow=single"
|
||||
start_flow_url = reverse('verify_student_start_flow', args=[unicode(self.course.id)]) + purchase_workflow
|
||||
start_flow_url = reverse('verify_student_start_flow', args=[six.text_type(self.course.id)]) + purchase_workflow
|
||||
self.assertRedirects(response, start_flow_url)
|
||||
|
||||
# Now enroll in the course
|
||||
@@ -215,7 +218,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
user=self.user,
|
||||
is_active=True,
|
||||
mode=mode,
|
||||
course_id=unicode(self.course.id),
|
||||
course_id=six.text_type(self.course.id),
|
||||
)
|
||||
|
||||
# Expect that this time we're redirected to the dashboard (since we're already registered)
|
||||
@@ -244,7 +247,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id, min_price=min_price)
|
||||
|
||||
# Choose the mode (POST request)
|
||||
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[course_mode])
|
||||
|
||||
# Verify the redirect
|
||||
@@ -253,7 +256,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
elif expected_redirect == 'start-flow':
|
||||
redirect_url = reverse(
|
||||
'verify_student_start_flow',
|
||||
kwargs={'course_id': unicode(self.course.id)}
|
||||
kwargs={'course_id': six.text_type(self.course.id)}
|
||||
)
|
||||
else:
|
||||
self.fail("Must provide a valid redirect URL name")
|
||||
@@ -273,7 +276,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
self.assertIsNone(is_active)
|
||||
|
||||
# Choose the audit mode (POST request)
|
||||
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[audit_mode])
|
||||
|
||||
# Assert learner is enrolled in Audit track post-POST
|
||||
@@ -301,14 +304,14 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
CourseModeFactory.create(mode_slug='verified', course_id=self.course.id, min_price=1)
|
||||
|
||||
# Choose the mode (POST request)
|
||||
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['verified'])
|
||||
|
||||
# Expect that the contribution amount is stored in the user's session
|
||||
self.assertIn('donation_for_course', self.client.session)
|
||||
self.assertIn(unicode(self.course.id), self.client.session['donation_for_course'])
|
||||
self.assertIn(six.text_type(self.course.id), self.client.session['donation_for_course'])
|
||||
|
||||
actual_amount = self.client.session['donation_for_course'][unicode(self.course.id)]
|
||||
actual_amount = self.client.session['donation_for_course'][six.text_type(self.course.id)]
|
||||
expected_amount = decimal.Decimal(self.POST_PARAMS_FOR_COURSE_MODE['verified']['contribution'])
|
||||
self.assertEqual(actual_amount, expected_amount)
|
||||
|
||||
@@ -321,12 +324,12 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
# automatic enrollment
|
||||
params = {
|
||||
'enrollment_action': 'enroll',
|
||||
'course_id': unicode(self.course.id)
|
||||
'course_id': six.text_type(self.course.id)
|
||||
}
|
||||
self.client.post(reverse('change_enrollment'), params)
|
||||
|
||||
# Explicitly select the honor mode (POST request)
|
||||
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE[CourseMode.DEFAULT_MODE_SLUG])
|
||||
|
||||
# Verify that the user's enrollment remains unchanged
|
||||
@@ -340,7 +343,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
# Choose an unsupported mode (POST request)
|
||||
choose_track_url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
choose_track_url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
response = self.client.post(choose_track_url, self.POST_PARAMS_FOR_COURSE_MODE['unsupported'])
|
||||
|
||||
self.assertEqual(400, response.status_code)
|
||||
@@ -348,7 +351,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
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=[unicode(self.course.id)])
|
||||
url = reverse('create_mode', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
@@ -372,7 +375,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
parameters['suggested_prices'] = suggested_prices
|
||||
parameters['currency'] = currency
|
||||
|
||||
url = reverse('create_mode', args=[unicode(self.course.id)])
|
||||
url = reverse('create_mode', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(url, parameters)
|
||||
|
||||
self.assertEquals(response.status_code, 200)
|
||||
@@ -397,7 +400,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
|
||||
def test_multiple_mode_creation(self):
|
||||
# Create an honor mode
|
||||
base_url = reverse('create_mode', args=[unicode(self.course.id)])
|
||||
base_url = reverse('create_mode', args=[six.text_type(self.course.id)])
|
||||
self.client.get(base_url)
|
||||
|
||||
# Excluding the currency parameter implicitly tests the mode creation endpoint's ability to
|
||||
@@ -409,7 +412,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
parameters['suggested_prices'] = '10,20'
|
||||
|
||||
# Create a verified mode
|
||||
url = reverse('create_mode', args=[unicode(self.course.id)])
|
||||
url = reverse('create_mode', args=[six.text_type(self.course.id)])
|
||||
self.client.get(url, parameters)
|
||||
|
||||
honor_mode = Mode(u'honor', u'Honor Code Certificate', 0, '', 'usd', None, None, None, None)
|
||||
@@ -428,7 +431,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
CourseModeFactory.create(mode_slug=mode, course_id=self.course.id)
|
||||
|
||||
# Load the track selection page
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
|
||||
# Verify that the header navigation links are hidden for the edx.org version
|
||||
@@ -445,7 +448,7 @@ class CourseModeViewTest(CatalogIntegrationMixin, UrlResetMixin, ModuleStoreTest
|
||||
self.course.enrollment_end = datetime(2015, 1, 1)
|
||||
modulestore().update_item(self.course, self.user.id)
|
||||
|
||||
url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
response = self.client.get(url)
|
||||
# URL-encoded version of 1/1/15, 12:00 AM
|
||||
redirect_url = reverse('dashboard') + '?course_closed=1%2F1%2F15%2C+12%3A00+AM'
|
||||
@@ -472,7 +475,7 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
self.client.login(username=self.user.username, password="edx")
|
||||
|
||||
# Construct the URL for the track selection page
|
||||
self.url = reverse('course_modes_choose', args=[unicode(self.course.id)])
|
||||
self.url = reverse('course_modes_choose', args=[six.text_type(self.course.id)])
|
||||
|
||||
@patch.dict(settings.FEATURES, {'EMBARGO': True})
|
||||
def test_embargo_restrict(self):
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"""URLs for course_mode API"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import url
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
"""
|
||||
Views for the course_mode module
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
import decimal
|
||||
import json
|
||||
import urllib
|
||||
|
||||
import six
|
||||
import six.moves.urllib.error
|
||||
import six.moves.urllib.parse
|
||||
import six.moves.urllib.request
|
||||
import waffle
|
||||
from babel.dates import format_datetime
|
||||
from django.contrib.auth.decorators import login_required
|
||||
@@ -96,7 +100,7 @@ class ChooseModeView(View):
|
||||
has_enrolled_professional = (CourseMode.is_professional_slug(enrollment_mode) and is_active)
|
||||
if CourseMode.has_professional_mode(modes) and not has_enrolled_professional:
|
||||
purchase_workflow = request.GET.get("purchase_workflow", "single")
|
||||
verify_url = reverse('verify_student_start_flow', kwargs={'course_id': unicode(course_key)})
|
||||
verify_url = reverse('verify_student_start_flow', kwargs={'course_id': six.text_type(course_key)})
|
||||
redirect_url = "{url}?purchase_workflow={workflow}".format(url=verify_url, workflow=purchase_workflow)
|
||||
if ecommerce_service.is_enabled(request.user):
|
||||
professional_mode = modes.get(CourseMode.NO_ID_PROFESSIONAL_MODE) or modes.get(CourseMode.PROFESSIONAL)
|
||||
@@ -121,12 +125,12 @@ class ChooseModeView(View):
|
||||
return redirect(reverse('dashboard'))
|
||||
|
||||
donation_for_course = request.session.get("donation_for_course", {})
|
||||
chosen_price = donation_for_course.get(unicode(course_key), None)
|
||||
chosen_price = donation_for_course.get(six.text_type(course_key), None)
|
||||
|
||||
if CourseEnrollment.is_enrollment_closed(request.user, course):
|
||||
locale = to_locale(get_language())
|
||||
enrollment_end_date = format_datetime(course.enrollment_end, 'short', locale=locale)
|
||||
params = urllib.urlencode({'course_closed': enrollment_end_date})
|
||||
params = six.moves.urllib.parse.urlencode({'course_closed': enrollment_end_date})
|
||||
return redirect('{0}?{1}'.format(reverse('dashboard'), params))
|
||||
|
||||
# When a credit mode is available, students will be given the option
|
||||
@@ -278,13 +282,13 @@ class ChooseModeView(View):
|
||||
return self.get(request, course_id, error=error_msg)
|
||||
|
||||
donation_for_course = request.session.get("donation_for_course", {})
|
||||
donation_for_course[unicode(course_key)] = amount_value
|
||||
donation_for_course[six.text_type(course_key)] = amount_value
|
||||
request.session["donation_for_course"] = donation_for_course
|
||||
|
||||
return redirect(
|
||||
reverse(
|
||||
'verify_student_start_flow',
|
||||
kwargs={'course_id': unicode(course_key)}
|
||||
kwargs={'course_id': six.text_type(course_key)}
|
||||
)
|
||||
)
|
||||
|
||||
@@ -342,7 +346,7 @@ def create_mode(request, course_id):
|
||||
}
|
||||
|
||||
# Try pulling querystring parameters out of the request
|
||||
for parameter, default in PARAMETERS.iteritems():
|
||||
for parameter, default in six.iteritems(PARAMETERS):
|
||||
PARAMETERS[parameter] = request.GET.get(parameter, default)
|
||||
|
||||
# Attempt to create the new mode for the given course
|
||||
|
||||
Reference in New Issue
Block a user