Commit 849ad8df by jsa Committed by Clinton Blackburn

commerce/api: Don’t allow setting expiration of prof modes.

XCOM-497
parent 7d8fb739
...@@ -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"),
......
""" Commerce API v1 view tests. """ """ Commerce API v1 view tests. """
from datetime import datetime from datetime import datetime
import itertools
import json import json
import ddt import ddt
...@@ -158,6 +159,34 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) ...@@ -158,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