""" Tests for the course modes Django admin interface. """ import unittest from datetime import datetime, timedelta import ddt from django.conf import settings from django.core.urlresolvers import reverse from pytz import UTC, timezone from course_modes.admin import CourseModeForm from course_modes.models import CourseMode from course_modes.tests.factories import CourseModeFactory # 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 student.tests.factories import UserFactory from util.date_utils import get_time_display from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase from xmodule.modulestore.tests.factories import CourseFactory # We can only test this in the LMS because the course modes admin relies # on verify student, which is not an installed app in Studio, so the verification # deadline table will not be created. @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') class AdminCourseModePageTest(ModuleStoreTestCase): """ Test the course modes Django admin interface. """ def test_expiration_timezone(self): # Test that expiration datetimes are saved and retrieved with the timezone set to UTC. # This verifies the fix for a bug in which the date displayed to users was different # than the date in Django admin. user = UserFactory.create(is_staff=True, is_superuser=True) user.save() course = CourseFactory.create() expiration = datetime(2015, 10, 20, 1, 10, 23, tzinfo=timezone(settings.TIME_ZONE)) data = { 'course_id': unicode(course.id), 'mode_slug': 'verified', 'mode_display_name': 'verified', 'min_price': 10, 'currency': 'usd', '_expiration_datetime_0': expiration.date(), # due to django admin datetime widget passing as separate vals '_expiration_datetime_1': expiration.time(), } self.client.login(username=user.username, password='test') # Create a new course mode from django admin page response = self.client.post(reverse('admin:course_modes_coursemode_add'), data=data) self.assertRedirects(response, reverse('admin:course_modes_coursemode_changelist')) # Verify that datetime is appears on list page response = self.client.get(reverse('admin:course_modes_coursemode_changelist')) self.assertContains(response, get_time_display(expiration, '%B %d, %Y, %H:%M %p')) # Verify that on the edit page the datetime value appears as UTC. resp = self.client.get(reverse('admin:course_modes_coursemode_change', args=(1,))) self.assertContains(resp, expiration.date()) self.assertContains(resp, expiration.time()) # Verify that the expiration datetime is the same as what we set # (hasn't changed because of a timezone translation). course_mode = CourseMode.objects.get(pk=1) self.assertEqual(course_mode.expiration_datetime.replace(tzinfo=None), expiration.replace(tzinfo=None)) @unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @ddt.ddt class AdminCourseModeFormTest(ModuleStoreTestCase): """ Test the course modes Django admin form validation and saving. """ UPGRADE_DEADLINE = datetime.now(UTC) VERIFICATION_DEADLINE = UPGRADE_DEADLINE + timedelta(days=5) def setUp(self): """ Create a test course. """ super(AdminCourseModeFormTest, self).setUp() self.course = CourseFactory.create() @ddt.data( ("honor", False), ("verified", True), ("professional", True), ("no-id-professional", False), ("credit", False), ) @ddt.unpack def test_load_verification_deadline(self, mode, expect_deadline): # Configure a verification deadline for the course VerificationDeadline.set_deadline(self.course.id, self.VERIFICATION_DEADLINE) # Configure a course mode with both an upgrade and verification deadline # and load the form to edit it. deadline = self.UPGRADE_DEADLINE if mode == "verified" else None form = self._admin_form(mode, upgrade_deadline=deadline) # Check that the verification deadline is loaded, # but ONLY for verified modes. loaded_deadline = form.initial.get("verification_deadline") if expect_deadline: self.assertEqual( loaded_deadline.replace(tzinfo=None), self.VERIFICATION_DEADLINE.replace(tzinfo=None) ) else: self.assertIs(loaded_deadline, None) @ddt.data("verified", "professional") def test_set_verification_deadline(self, course_mode): # Configure a verification deadline for the course VerificationDeadline.set_deadline(self.course.id, self.VERIFICATION_DEADLINE) # Create the course mode Django admin form form = self._admin_form(course_mode) # Update the verification deadline form data # We need to set the date and time fields separately, since they're # displayed as separate widgets in the form. new_deadline = (self.VERIFICATION_DEADLINE + timedelta(days=1)).replace(microsecond=0) self._set_form_verification_deadline(form, new_deadline) form.save() # Check that the deadline was updated updated_deadline = VerificationDeadline.deadline_for_course(self.course.id) self.assertEqual(updated_deadline, new_deadline) def test_disable_verification_deadline(self): # Configure a verification deadline for the course VerificationDeadline.set_deadline(self.course.id, self.VERIFICATION_DEADLINE) # Create the course mode Django admin form form = self._admin_form("verified", upgrade_deadline=self.UPGRADE_DEADLINE) # Use the form to disable the verification deadline self._set_form_verification_deadline(form, None) form.save() # Check that the deadline was disabled self.assertIs(VerificationDeadline.deadline_for_course(self.course.id), None) @ddt.data("honor", "professional", "no-id-professional", "credit") def test_validate_upgrade_deadline_only_for_verified(self, course_mode): # Only the verified mode should have an upgrade deadline, so any other course # mode that has an upgrade deadline set should cause a validation error form = self._admin_form(course_mode, upgrade_deadline=self.UPGRADE_DEADLINE) self._assert_form_has_error(form, ( 'Only the "verified" mode can have an upgrade deadline. ' 'For other modes, please set the enrollment end date in Studio.' )) @ddt.data("honor", "no-id-professional", "credit") def test_validate_verification_deadline_only_for_verified(self, course_mode): # Only the verified mode should have a verification deadline set. # Any other course mode should raise a validation error if a deadline is set. form = self._admin_form(course_mode) self._set_form_verification_deadline(form, self.VERIFICATION_DEADLINE) self._assert_form_has_error(form, "Verification deadline can be set only for verified modes.") def test_verification_deadline_after_upgrade_deadline(self): form = self._admin_form("verified", upgrade_deadline=self.UPGRADE_DEADLINE) before_upgrade = self.UPGRADE_DEADLINE - timedelta(days=1) self._set_form_verification_deadline(form, before_upgrade) self._assert_form_has_error(form, "Verification deadline must be after the upgrade deadline.") def _configure(self, mode, upgrade_deadline=None, verification_deadline=None): """Configure course modes and deadlines. """ course_mode = CourseModeFactory.create( mode_slug=mode, mode_display_name=mode, ) if upgrade_deadline is not None: course_mode.upgrade_deadline = upgrade_deadline course_mode.save() VerificationDeadline.set_deadline(self.course.id, verification_deadline) return CourseModeForm(instance=course_mode) def _admin_form(self, mode, upgrade_deadline=None): """Load the course mode admin form. """ course_mode = CourseModeFactory.create( course_id=self.course.id, mode_slug=mode, ) return CourseModeForm({ "course_id": unicode(self.course.id), "mode_slug": mode, "mode_display_name": mode, "_expiration_datetime": upgrade_deadline, "currency": "usd", "min_price": 10, }, instance=course_mode) def _set_form_verification_deadline(self, form, deadline): """Set the verification deadline on the course mode admin form. """ date_str = deadline.strftime("%Y-%m-%d") if deadline else None time_str = deadline.strftime("%H:%M:%S") if deadline else None form.data["verification_deadline_0"] = date_str form.data["verification_deadline_1"] = time_str def _assert_form_has_error(self, form, error): """Check that a form has a validation error. """ validation_errors = form.errors.get("__all__", []) self.assertIn(error, validation_errors)