diff --git a/lms/djangoapps/commerce/api/v1/models.py b/lms/djangoapps/commerce/api/v1/models.py index 4d7c7a0579..f6d547203a 100644 --- a/lms/djangoapps/commerce/api/v1/models.py +++ b/lms/djangoapps/commerce/api/v1/models.py @@ -12,6 +12,8 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi log = logging.getLogger(__name__) +UNDEFINED = object() + class Course(object): """ Pseudo-course model used to group CourseMode objects. """ @@ -19,10 +21,12 @@ class Course(object): modes = None _deleted_modes = None - def __init__(self, id, modes, verification_deadline=None): # pylint: disable=redefined-builtin + def __init__(self, id, modes, **kwargs): # pylint: disable=redefined-builtin self.id = CourseKey.from_string(unicode(id)) # pylint: disable=invalid-name self.modes = list(modes) - self.verification_deadline = verification_deadline + self.verification_deadline = UNDEFINED + if 'verification_deadline' in kwargs: + self.verification_deadline = kwargs['verification_deadline'] self._deleted_modes = [] @property @@ -59,8 +63,10 @@ class Course(object): def save(self, *args, **kwargs): # pylint: disable=unused-argument """ Save the CourseMode objects to the database. """ - # Override the verification deadline for the course (not the individual modes) - VerificationDeadline.set_deadline(self.id, self.verification_deadline, is_explicit=True) + if self.verification_deadline is not UNDEFINED: + # Override the verification deadline for the course (not the individual modes) + # This will delete verification deadlines for the course if self.verification_deadline is null + VerificationDeadline.set_deadline(self.id, self.verification_deadline, is_explicit=True) for mode in self.modes: mode.course_id = self.id @@ -73,7 +79,10 @@ class Course(object): def update(self, attrs): """ Update the model with external data (usually passed via API call). """ - self.verification_deadline = attrs.get('verification_deadline') + # 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() diff --git a/lms/djangoapps/commerce/api/v1/serializers.py b/lms/djangoapps/commerce/api/v1/serializers.py index 8cf1756eb9..687fa610ec 100644 --- a/lms/djangoapps/commerce/api/v1/serializers.py +++ b/lms/djangoapps/commerce/api/v1/serializers.py @@ -10,7 +10,7 @@ from rest_framework import serializers from course_modes.models import CourseMode from xmodule.modulestore.django import modulestore -from .models import Course +from .models import Course, UNDEFINED class CourseModeSerializer(serializers.ModelSerializer): @@ -56,11 +56,22 @@ def validate_course_id(course_id): ) +class PossiblyUndefinedDateTimeField(serializers.DateTimeField): + """ + We need a DateTime serializer that can deal with the non-JSON-serializable + UNDEFINED object. + """ + def to_representation(self, value): + if value is UNDEFINED: + return None + return super(PossiblyUndefinedDateTimeField, self).to_representation(value) + + class CourseSerializer(serializers.Serializer): """ Course serializer. """ id = serializers.CharField(validators=[validate_course_id]) # pylint: disable=invalid-name name = serializers.CharField(read_only=True) - verification_deadline = serializers.DateTimeField(format=None, allow_null=True, required=False) + verification_deadline = PossiblyUndefinedDateTimeField(format=None, allow_null=True, required=False) modes = CourseModeSerializer(many=True) def validate(self, attrs): @@ -87,11 +98,23 @@ class CourseSerializer(serializers.Serializer): return attrs def create(self, validated_data): - """Create course modes for a course. """ + """ + Create course modes for a course. + + arguments: + validated_data: The result of self.validate() - a dictionary containing 'id', 'modes', and optionally + a 'verification_deadline` key. + returns: + A ``commerce.api.v1.models.Course`` object. + """ + kwargs = {} + if 'verification_deadline' in validated_data: + kwargs['verification_deadline'] = validated_data['verification_deadline'] + course = Course( validated_data["id"], self._new_course_mode_models(validated_data["modes"]), - verification_deadline=validated_data["verification_deadline"] + **kwargs ) course.save() return course diff --git a/lms/djangoapps/commerce/api/v1/tests/test_views.py b/lms/djangoapps/commerce/api/v1/tests/test_views.py index 5e6055570e..be0c17ec8c 100644 --- a/lms/djangoapps/commerce/api/v1/tests/test_views.py +++ b/lms/djangoapps/commerce/api/v1/tests/test_views.py @@ -128,8 +128,9 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) @ddt.data('post', 'put') def test_authorization_required(self, method): - self.user.user_permissions.clear() """ Verify create/edit operations require appropriate permissions. """ + self.user.user_permissions.clear() + response = getattr(self.client, method)(self.path, content_type=JSON_CONTENT_TYPE) self.assertEqual(response.status_code, 403) @@ -236,6 +237,32 @@ class CourseRetrieveUpdateViewTests(CourseApiViewTestMixin, ModuleStoreTestCase) self.assertEqual(response.status_code, 200) self.assertIsNone(VerificationDeadline.deadline_for_course(self.course.id)) + def test_update_verification_deadline_left_alone(self): + """ + When the course's verification deadline is set and an update request doesn't + include it, we should take no action on it. + """ + verification_deadline = datetime(year=1915, month=5, day=7, tzinfo=pytz.utc) + response, __ = self._get_update_response_and_expected_data(None, verification_deadline) + self.assertEqual(VerificationDeadline.deadline_for_course(self.course.id), verification_deadline) + + verified_mode = CourseMode( + mode_slug=u'verified', + min_price=200, + currency=u'USD', + sku=u'ABC123', + bulk_sku=u'BULK-ABC123', + expiration_datetime=None + ) + updated_data = self._serialize_course(self.course, [verified_mode], None) + # don't include the verification_deadline key in the PUT request + updated_data.pop('verification_deadline', None) + + response = self.client.put(self.path, json.dumps(updated_data), content_type=JSON_CONTENT_TYPE) + + self.assertEqual(response.status_code, 200) + self.assertEqual(VerificationDeadline.deadline_for_course(self.course.id), verification_deadline) + def test_remove_upgrade_deadline(self): """ Verify that course mode upgrade deadlines can be removed through the API.