Remove course_id field from CourseMode.

Handle this change appropriately in CourseModeFactory.
This commit is contained in:
Diana Huang
2020-04-03 11:31:49 -04:00
parent 09d814f7fd
commit 99863aebff
9 changed files with 69 additions and 55 deletions

View File

@@ -29,6 +29,7 @@ from contentstore.signals.handlers import listen_for_course_publish, listen_for_
from contentstore.tests.utils import CourseTestCase
from contentstore.utils import reverse_course_url, reverse_usage_url
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.models.course_details import CourseDetails
from xmodule.library_tools import normalize_key_for_search
from xmodule.modulestore import ModuleStoreEnum
@@ -473,14 +474,14 @@ class TestCoursewareSearchIndexer(MixedWithOptionsTestCase):
def _test_course_about_mode_index(self, store):
""" Test that informational properties in the course modes store end up in the course_info index """
honour_mode = CourseMode(
course_id=six.text_type(self.course.id),
honour_mode = CourseModeFactory(
course_id=self.course.id,
mode_slug=CourseMode.HONOR,
mode_display_name=CourseMode.HONOR
)
honour_mode.save()
verified_mode = CourseMode(
course_id=six.text_type(self.course.id),
verified_mode = CourseModeFactory(
course_id=self.course.id,
mode_slug=CourseMode.VERIFIED,
mode_display_name=CourseMode.VERIFIED,
min_price=1

View File

@@ -6,6 +6,7 @@ Course modes API serializers.
from rest_framework import serializers
from course_modes.models import CourseMode
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
class CourseModeSerializer(serializers.Serializer):
@@ -37,6 +38,11 @@ class CourseModeSerializer(serializers.Serializer):
This method must be implemented for use in our
ListCreateAPIView.
"""
if 'course_id' in validated_data:
course_overview = CourseOverview.get_from_id(validated_data['course_id'])
del validated_data['course_id']
validated_data['course'] = course_overview
return CourseMode.objects.create(**validated_data)
def update(self, instance, validated_data):

View File

@@ -21,6 +21,7 @@ from course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
from openedx.core.djangoapps.user_authn.tests.utils import JWT_AUTH_TYPES, AuthAndScopesTestMixin, AuthType
from student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
@ddt.ddt
@@ -96,7 +97,7 @@ class CourseModesViewTestBase(AuthAndScopesTestMixin):
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestCourseModesListViews(CourseModesViewTestBase, APITestCase):
class TestCourseModesListViews(CourseModesViewTestBase, ModuleStoreTestCase, APITestCase):
"""
Tests for the course modes list/create API endpoints.
"""

View File

@@ -6,6 +6,8 @@ Add and create new modes for running courses on this particular LMS
from collections import defaultdict, namedtuple
from datetime import timedelta
import inspect
import logging
import six
from config_models.models import ConfigurationModel
from django.conf import settings
@@ -25,6 +27,8 @@ from simple_history.models import HistoricalRecords
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.lib.cache_utils import request_cached
log = logging.getLogger(__name__)
Mode = namedtuple('Mode',
[
'slug',
@@ -54,19 +58,6 @@ class CourseMode(models.Model):
on_delete=models.DO_NOTHING,
)
# Django sets the `course_id` property in __init__ with the value from the database
# This pair of properties converts that into a proper CourseKey
@property
def course_id(self):
return self._course_id
@course_id.setter
def course_id(self, value):
if isinstance(value, six.string_types):
self._course_id = CourseKey.from_string(value)
else:
self._course_id = value
# the reference to this mode that can be used by Enrollments to generate
# similar behavior for the same slug across courses
mode_slug = models.CharField(max_length=100, verbose_name=_("Mode"))
@@ -199,6 +190,16 @@ class CourseMode(models.Model):
app_label = "course_modes"
unique_together = ('course', 'mode_slug', 'currency')
def __init__(self, *args, **kwargs):
if 'course_id' in kwargs:
course_id = kwargs['course_id']
if isinstance(course_id, str):
kwargs['course_id'] = CourseKey.from_string(course_id)
call_location = "\n".join("%30s : %s:%d" % (t[3], t[1], t[2]) for t in inspect.stack()[::-1])
log.warning("Forced to coerce course_id in CourseMode instantiation: %s", call_location)
super(CourseMode, self).__init__(*args, **kwargs)
def clean(self):
"""
Object-level validation - implemented in this method so DRF serializers

View File

@@ -4,12 +4,15 @@ Factories for course mode models.
import random
import six
from factory import lazy_attribute
from factory.django import DjangoModelFactory
from opaque_keys.edx.locator import CourseLocator
from opaque_keys.edx.keys import CourseKey
from course_modes.models import CourseMode
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
# Factories are self documenting
@@ -18,12 +21,41 @@ class CourseModeFactory(DjangoModelFactory):
class Meta(object):
model = CourseMode
course_id = CourseLocator('MITx', '999', 'Robot_Super_Course')
mode_slug = CourseMode.DEFAULT_MODE_SLUG
currency = 'usd'
expiration_datetime = None
suggested_prices = ''
@classmethod
def _create(cls, model_class, *args, **kwargs):
manager = cls._get_manager(model_class)
course_kwargs = {}
for key in list(kwargs):
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
course_kwargs.setdefault('id', course_id)
if course_id is not None:
if isinstance(course_id, six.string_types):
course_id = CourseKey.from_string(course_id)
course_kwargs['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
del kwargs['course_id']
return manager.create(*args, **kwargs)
@lazy_attribute
def min_price(self):
if CourseMode.is_verified_slug(self.mode_slug):

View File

@@ -675,7 +675,7 @@ class CertificateGenerationEnabledTest(EventTestMixin, TestCase):
self.assertEqual(expect_enabled, actual_enabled)
class GenerateExampleCertificatesTest(TestCase):
class GenerateExampleCertificatesTest(ModuleStoreTestCase):
"""Test generation of example certificates. """
COURSE_KEY = CourseLocator(org='test', course='test', run='test')
@@ -739,7 +739,7 @@ class GenerateExampleCertificatesTest(TestCase):
@override_settings(FEATURES=FEATURES_WITH_CERTS_ENABLED)
class CertificatesBrandingTest(TestCase):
class CertificatesBrandingTest(ModuleStoreTestCase):
"""Test certificates branding. """
COURSE_KEY = CourseLocator(org='test', course='test', run='test')

View File

@@ -836,35 +836,6 @@ class ViewsTestCase(BaseViewsTestCase):
self.assertNotContains(response, str(course.id))
@patch.object(CourseOverview, 'load_from_module_store', return_value=None)
def test_financial_assistance_form_missing_course_overview(self, _mock_course_overview):
"""
Verify that learners can not get financial aid for the courses with no
course overview.
"""
# Create course
course = CourseFactory.create().id
# Create Course Modes
CourseModeFactory.create(mode_slug=CourseMode.AUDIT, course_id=course)
CourseModeFactory.create(mode_slug=CourseMode.VERIFIED, course_id=course)
# Enroll user in the course
# Don't use the CourseEnrollmentFactory since it ensures a CourseOverview is available
enrollment = CourseEnrollment.objects.create(
course_id=course,
user=self.user,
mode=CourseMode.AUDIT,
)
self.assertEqual(enrollment.course_overview, None)
url = reverse('financial_assistance_form')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertNotContains(response, str(course))
def test_financial_assistance_form(self):
"""Verify that learner can get the financial aid for the course in which
he/she is enrolled in audit mode whereas the course provide verified mode.

View File

@@ -11,6 +11,7 @@ from django.urls import reverse
from six import text_type
from course_modes.models import CourseMode
from course_modes.tests.factories import CourseModeFactory
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from shoppingcart.models import Coupon, CourseRegistrationCode
from student.roles import CourseFinanceAdminRole
@@ -39,7 +40,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase):
# Create instructor account
self.instructor = AdminFactory.create()
self.client.login(username=self.instructor.username, password="test")
mode = CourseMode(
mode = CourseModeFactory(
course_id=text_type(self.course.id), mode_slug='honor',
mode_display_name='honor', min_price=10, currency='usd'
)
@@ -118,7 +119,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase):
price = 200
# course B
course2 = CourseFactory.create(org='EDX', display_name='test_course', number='100')
mode = CourseMode(
mode = CourseModeFactory(
course_id=text_type(course2.id), mode_slug='honor',
mode_display_name='honor', min_price=30, currency='usd'
)
@@ -365,7 +366,7 @@ class TestECommerceDashboardViews(SiteMixin, SharedModuleStoreTestCase):
# Change honor mode to verified.
original_mode = CourseMode.objects.get(course_id=self.course.id, mode_slug='honor')
original_mode.delete()
new_mode = CourseMode(
new_mode = CourseModeFactory(
course_id=six.text_type(self.course.id), mode_slug='verified',
mode_display_name='verified', min_price=10, currency='usd'
)

View File

@@ -1271,7 +1271,8 @@ class TestCheckoutWithEcommerceService(ModuleStoreTestCase):
ecommerce api, we correctly call to that api to create a basket.
"""
user = UserFactory.create(username="test-username")
course_mode = CourseModeFactory.create(sku="test-sku").to_tuple()
course_id = 'edX/test/test_run'
course_mode = CourseModeFactory.create(course_id=course_id, sku="test-sku").to_tuple()
expected_payment_data = {'foo': 'bar'}
# mock out the payment processors endpoint
httpretty.register_uri(