Commit 3a1f9bd8 by Jim Abramson

Merge pull request #9084 from edx/jsa/xcom-497

commerce/api: Don’t allow setting expiration of prof modes.
parents 8d9e9076 03717f39
...@@ -4,6 +4,7 @@ Add and create new modes for running courses on this particular LMS ...@@ -4,6 +4,7 @@ Add and create new modes for running courses on this particular LMS
import pytz import pytz
from datetime import datetime from datetime import datetime
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from collections import namedtuple, defaultdict from collections import namedtuple, defaultdict
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
...@@ -106,8 +107,19 @@ class CourseMode(models.Model): ...@@ -106,8 +107,19 @@ class CourseMode(models.Model):
""" meta attributes of this model """ """ meta attributes of this model """
unique_together = ('course_id', 'mode_slug', 'currency') unique_together = ('course_id', 'mode_slug', 'currency')
def clean(self):
"""
Object-level validation - implemented in this method so DRF serializers
catch errors in advance of a save() attempt.
"""
if self.is_professional_slug(self.mode_slug) and self.expiration_datetime is not None:
raise ValidationError(
_(u"Professional education modes are not allowed to have expiration_datetime set.")
)
def save(self, force_insert=False, force_update=False, using=None): def save(self, force_insert=False, force_update=False, using=None):
# Ensure currency is always lowercase. # Ensure currency is always lowercase.
self.clean() # ensure object-level validation is performed before we save.
self.currency = self.currency.lower() self.currency = self.currency.lower()
super(CourseMode, self).save(force_insert, force_update, using) super(CourseMode, self).save(force_insert, force_update, using)
......
...@@ -6,12 +6,15 @@ Replace this with more appropriate tests for your application. ...@@ -6,12 +6,15 @@ Replace this with more appropriate tests for your application.
""" """
from datetime import datetime, timedelta from datetime import datetime, timedelta
import pytz import itertools
import ddt
import ddt
from django.core.exceptions import ValidationError
from django.test import TestCase
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from opaque_keys.edx.locator import CourseLocator from opaque_keys.edx.locator import CourseLocator
from django.test import TestCase import pytz
from course_modes.models import CourseMode, Mode from course_modes.models import CourseMode, Mode
...@@ -26,7 +29,15 @@ class CourseModeModelTest(TestCase): ...@@ -26,7 +29,15 @@ class CourseModeModelTest(TestCase):
self.course_key = SlashSeparatedCourseKey('Test', 'TestCourse', 'TestCourseRun') self.course_key = SlashSeparatedCourseKey('Test', 'TestCourse', 'TestCourseRun')
CourseMode.objects.all().delete() CourseMode.objects.all().delete()
def create_mode(self, mode_slug, mode_name, min_price=0, suggested_prices='', currency='usd'): def create_mode(
self,
mode_slug,
mode_name,
min_price=0,
suggested_prices='',
currency='usd',
expiration_datetime=None,
):
""" """
Create a new course mode Create a new course mode
""" """
...@@ -37,6 +48,7 @@ class CourseModeModelTest(TestCase): ...@@ -37,6 +48,7 @@ class CourseModeModelTest(TestCase):
min_price=min_price, min_price=min_price,
suggested_prices=suggested_prices, suggested_prices=suggested_prices,
currency=currency, currency=currency,
expiration_datetime=expiration_datetime,
) )
def test_save(self): def test_save(self):
...@@ -264,6 +276,29 @@ class CourseModeModelTest(TestCase): ...@@ -264,6 +276,29 @@ class CourseModeModelTest(TestCase):
else: else:
self.assertFalse(CourseMode.is_verified_slug(mode_slug)) self.assertFalse(CourseMode.is_verified_slug(mode_slug))
@ddt.data(*itertools.product(
(
CourseMode.HONOR,
CourseMode.AUDIT,
CourseMode.VERIFIED,
CourseMode.PROFESSIONAL,
CourseMode.NO_ID_PROFESSIONAL_MODE
),
(datetime.now(), None),
))
@ddt.unpack
def test_invalid_mode_expiration(self, mode_slug, exp_dt):
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)
self.assertFalse(is_error_expected, "Expected a ValidationError to be thrown.")
except ValidationError, 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."],
)
@ddt.data( @ddt.data(
("verified", "verify_need_to_verify"), ("verified", "verify_need_to_verify"),
("verified", "verify_submitted"), ("verified", "verify_submitted"),
......
...@@ -66,6 +66,7 @@ class Course(object): ...@@ -66,6 +66,7 @@ class Course(object):
merged_mode.min_price = posted_mode.min_price merged_mode.min_price = posted_mode.min_price
merged_mode.currency = posted_mode.currency merged_mode.currency = posted_mode.currency
merged_mode.sku = posted_mode.sku merged_mode.sku = posted_mode.sku
merged_mode.expiration_datetime = posted_mode.expiration_datetime
merged_modes.add(merged_mode) merged_modes.add(merged_mode)
merged_mode_keys.add(merged_mode.mode_slug) merged_mode_keys.add(merged_mode.mode_slug)
......
""" Commerce API v1 view tests. """ """ Commerce API v1 view tests. """
from datetime import datetime
import itertools
import json import json
import ddt import ddt
...@@ -6,6 +8,7 @@ from django.conf import settings ...@@ -6,6 +8,7 @@ from django.conf import settings
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test.utils import override_settings from django.test.utils import override_settings
from rest_framework.utils.encoders import JSONEncoder
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory from xmodule.modulestore.tests.factories import CourseFactory
...@@ -31,12 +34,17 @@ class CourseApiViewTestMixin(object): ...@@ -31,12 +34,17 @@ class CourseApiViewTestMixin(object):
@staticmethod @staticmethod
def _serialize_course_mode(course_mode): def _serialize_course_mode(course_mode):
""" Serialize a CourseMode to a dict. """ """ Serialize a CourseMode to a dict. """
# encode the datetime (if nonempty) using DRF's encoder, simplifying
# equality assertions.
expires = course_mode.expiration_datetime
if expires is not None:
expires = JSONEncoder().default(expires)
return { return {
u'name': course_mode.mode_slug, u'name': course_mode.mode_slug,
u'currency': course_mode.currency.lower(), u'currency': course_mode.currency.lower(),
u'price': course_mode.min_price, u'price': course_mode.min_price,
u'sku': course_mode.sku, u'sku': course_mode.sku,
u'expires': course_mode.expiration_datetime, u'expires': expires,
} }
...@@ -112,7 +120,14 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) ...@@ -112,7 +120,14 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
""" Verify the view supports updating a course. """ """ Verify the view supports updating a course. """
permission = Permission.objects.get(name='Can change course mode') permission = Permission.objects.get(name='Can change course mode')
self.user.user_permissions.add(permission) self.user.user_permissions.add(permission)
expected_course_mode = CourseMode(mode_slug=u'verified', min_price=200, currency=u'USD', sku=u'ABC123') expiration_datetime = datetime.now()
expected_course_mode = CourseMode(
mode_slug=u'verified',
min_price=200,
currency=u'USD',
sku=u'ABC123',
expiration_datetime=expiration_datetime
)
expected = { expected = {
u'id': unicode(self.course.id), u'id': unicode(self.course.id),
u'modes': [self._serialize_course_mode(expected_course_mode)] u'modes': [self._serialize_course_mode(expected_course_mode)]
...@@ -144,6 +159,34 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) ...@@ -144,6 +159,34 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase)
# The existing CourseMode should have been removed. # The existing CourseMode should have been removed.
self.assertFalse(CourseMode.objects.filter(id=self.course_mode.id).exists()) self.assertFalse(CourseMode.objects.filter(id=self.course_mode.id).exists())
@ddt.data(*itertools.product(
('honor', 'audit', 'verified', 'professional', 'no-id-professional'),
(datetime.now(), None),
))
@ddt.unpack
def test_update_professional_expiration(self, mode_slug, expiration_datetime):
""" Verify that pushing a mode with a professional certificate and an expiration datetime
will be rejected (this is not allowed). """
permission = Permission.objects.get(name='Can change course mode')
self.user.user_permissions.add(permission)
mode = self._serialize_course_mode(
CourseMode(
mode_slug=mode_slug,
min_price=500,
currency=u'USD',
sku=u'ABC123',
expiration_datetime=expiration_datetime
)
)
course_id = unicode(self.course.id)
payload = {u'id': course_id, u'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)
self.assertEqual(response.status_code, expected_status)
def assert_can_create_course(self, **request_kwargs): def assert_can_create_course(self, **request_kwargs):
""" Verify a course can be created by the view. """ """ Verify a course can be created by the view. """
course = CourseFactory.create() course = CourseFactory.create()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment