Separate verification deadline from upgrade deadline
* Add verification deadline model. * Populate verification deadlines from course modes table. * Update student dashboard to use verification deadlines. * Update pay-and-verify view to use verification deadlines. * Simplify Django admin for course modes and add validation. * Add verification deadline to Django admin for course modes. * Add UI for when the upgrade deadline is missed in the pay-and-verify flow.
This commit is contained in:
@@ -2,16 +2,31 @@
|
||||
Django admin page for course modes
|
||||
"""
|
||||
from django.conf import settings
|
||||
from pytz import timezone, UTC
|
||||
from ratelimitbackend import admin
|
||||
from course_modes.models import CourseMode
|
||||
from django import forms
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.contrib import admin
|
||||
|
||||
from pytz import timezone, UTC
|
||||
|
||||
from opaque_keys import InvalidKeyError
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from opaque_keys.edx.locations import SlashSeparatedCourseKey
|
||||
from opaque_keys import InvalidKeyError
|
||||
|
||||
from util.date_utils import get_time_display
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
# Technically, we shouldn't be doing this, since verify_student is defined
|
||||
# in LMS, and course_modes is defined in common.
|
||||
#
|
||||
# Once we move the responsibility for administering course modes into
|
||||
# the Course Admin tool, we can remove this dependency and expose
|
||||
# verification deadlines as a separate Django model admin.
|
||||
#
|
||||
# The admin page will work in both LMS and Studio,
|
||||
# but the test suite for Studio will fail because
|
||||
# the verification deadline table won't exist.
|
||||
from verify_student import models as verification_models # pylint: disable=import-error
|
||||
|
||||
|
||||
class CourseModeForm(forms.ModelForm):
|
||||
@@ -26,7 +41,45 @@ class CourseModeForm(forms.ModelForm):
|
||||
[(mode_slug, mode_slug) for mode_slug in CourseMode.CREDIT_MODES]
|
||||
)
|
||||
|
||||
mode_slug = forms.ChoiceField(choices=COURSE_MODE_SLUG_CHOICES)
|
||||
mode_slug = forms.ChoiceField(choices=COURSE_MODE_SLUG_CHOICES, label=_("Mode"))
|
||||
|
||||
# The verification deadline is stored outside the course mode in the verify_student app.
|
||||
# (we used to use the course mode expiration_datetime as both an upgrade and verification deadline).
|
||||
# In order to make this transition easier, we include the verification deadline as a custom field
|
||||
# in the course mode admin form. Longer term, we will deprecate the course mode Django admin
|
||||
# form in favor of an external Course Administration Tool.
|
||||
verification_deadline = forms.SplitDateTimeField(
|
||||
label=_("Verification Deadline"),
|
||||
required=False,
|
||||
help_text=_(
|
||||
"OPTIONAL: After this date/time, users will no longer be able to submit photos for verification. "
|
||||
"This appies ONLY to modes that require verification."
|
||||
),
|
||||
widget=admin.widgets.AdminSplitDateTime,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CourseModeForm, self).__init__(*args, **kwargs)
|
||||
|
||||
default_tz = timezone(settings.TIME_ZONE)
|
||||
|
||||
if self.instance.expiration_datetime:
|
||||
# django admin is using default timezone. To avoid time conversion from db to form
|
||||
# convert the UTC object to naive and then localize with default timezone.
|
||||
expiration_datetime = self.instance.expiration_datetime.replace(tzinfo=None)
|
||||
self.initial["expiration_datetime"] = default_tz.localize(expiration_datetime)
|
||||
|
||||
# Load the verification deadline
|
||||
# Since this is stored on a model in verify student, we need to load it from there.
|
||||
# We need to munge the timezone a bit to get Django admin to display it without converting
|
||||
# it to the user's timezone. We'll make sure we write it back to the database with the timezone
|
||||
# set to UTC later.
|
||||
if self.instance.course_id and self.instance.mode_slug in CourseMode.VERIFIED_MODES:
|
||||
deadline = verification_models.VerificationDeadline.deadline_for_course(self.instance.course_id)
|
||||
self.initial["verification_deadline"] = (
|
||||
default_tz.localize(deadline.replace(tzinfo=None))
|
||||
if deadline is not None else None
|
||||
)
|
||||
|
||||
def clean_course_id(self):
|
||||
course_id = self.cleaned_data['course_id']
|
||||
@@ -43,38 +96,111 @@ class CourseModeForm(forms.ModelForm):
|
||||
|
||||
return course_key
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(CourseModeForm, self).__init__(*args, **kwargs)
|
||||
if self.instance.expiration_datetime:
|
||||
default_tz = timezone(settings.TIME_ZONE)
|
||||
# django admin is using default timezone. To avoid time conversion from db to form
|
||||
# convert the UTC object to naive and then localize with default timezone.
|
||||
expiration_datetime = self.instance.expiration_datetime.replace(tzinfo=None)
|
||||
self.initial['expiration_datetime'] = default_tz.localize(expiration_datetime)
|
||||
|
||||
def clean_expiration_datetime(self):
|
||||
"""changing the tzinfo for a given datetime object"""
|
||||
"""
|
||||
Ensure that the expiration datetime we save uses the UTC timezone.
|
||||
"""
|
||||
# django admin saving the date with default timezone to avoid time conversion from form to db
|
||||
# changes its tzinfo to UTC
|
||||
if self.cleaned_data.get("expiration_datetime"):
|
||||
return self.cleaned_data.get("expiration_datetime").replace(tzinfo=UTC)
|
||||
|
||||
def clean_verification_deadline(self):
|
||||
"""
|
||||
Ensure that the verification deadline we save uses the UTC timezone.
|
||||
"""
|
||||
if self.cleaned_data.get("verification_deadline"):
|
||||
return self.cleaned_data.get("verification_deadline").replace(tzinfo=UTC)
|
||||
|
||||
def clean(self):
|
||||
"""
|
||||
Clean the form fields.
|
||||
This is the place to perform checks that involve multiple form fields.
|
||||
"""
|
||||
cleaned_data = super(CourseModeForm, self).clean()
|
||||
mode_slug = cleaned_data.get("mode_slug")
|
||||
upgrade_deadline = cleaned_data.get("expiration_datetime")
|
||||
verification_deadline = cleaned_data.get("verification_deadline")
|
||||
|
||||
# Allow upgrade deadlines ONLY for the "verified" mode
|
||||
# This avoids a nasty error condition in which the upgrade deadline is set
|
||||
# for a professional education course before the enrollment end date.
|
||||
# When this happens, the course mode expires and students are able to enroll
|
||||
# in the course for free. To avoid this, we explicitly prevent admins from
|
||||
# setting an upgrade deadline for any mode except "verified" (which has an upgrade path).
|
||||
if upgrade_deadline is not None and mode_slug != CourseMode.VERIFIED:
|
||||
raise forms.ValidationError(
|
||||
'Only the "verified" mode can have an upgrade deadline. '
|
||||
'For other modes, please set the enrollment end date in Studio.'
|
||||
)
|
||||
|
||||
# Verification deadlines are allowed only for verified modes
|
||||
if verification_deadline is not None and mode_slug not in CourseMode.VERIFIED_MODES:
|
||||
raise forms.ValidationError("Verification deadline can be set only for verified modes.")
|
||||
|
||||
# Verification deadline must be after the upgrade deadline,
|
||||
# if an upgrade deadline is set.
|
||||
# There are cases in which we might want to set a verification deadline,
|
||||
# but not an upgrade deadline (for example, a professional education course that requires verification).
|
||||
if verification_deadline is not None:
|
||||
if upgrade_deadline is not None and verification_deadline < upgrade_deadline:
|
||||
raise forms.ValidationError("Verification deadline must be after the upgrade deadline.")
|
||||
|
||||
return cleaned_data
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
Save the form data.
|
||||
"""
|
||||
# Trigger validation so we can access cleaned data
|
||||
if self.is_valid():
|
||||
course_key = self.cleaned_data.get("course_id")
|
||||
verification_deadline = self.cleaned_data.get("verification_deadline")
|
||||
mode_slug = self.cleaned_data.get("mode_slug")
|
||||
|
||||
# Since the verification deadline is stored in a separate model,
|
||||
# we need to handle saving this ourselves.
|
||||
# Note that verification deadline can be `None` here if
|
||||
# the deadline is being disabled.
|
||||
if course_key is not None and mode_slug in CourseMode.VERIFIED_MODES:
|
||||
verification_models.VerificationDeadline.set_deadline(course_key, verification_deadline)
|
||||
|
||||
return super(CourseModeForm, self).save(commit=commit)
|
||||
|
||||
|
||||
class CourseModeAdmin(admin.ModelAdmin):
|
||||
"""Admin for course modes"""
|
||||
form = CourseModeForm
|
||||
search_fields = ('course_id',)
|
||||
list_display = (
|
||||
'id', 'course_id', 'mode_slug', 'mode_display_name', 'min_price',
|
||||
'currency', 'expiration_date', 'expiration_datetime_custom', 'sku'
|
||||
|
||||
fields = (
|
||||
'course_id',
|
||||
'mode_slug',
|
||||
'mode_display_name',
|
||||
'min_price',
|
||||
'currency',
|
||||
'expiration_datetime',
|
||||
'verification_deadline',
|
||||
'sku'
|
||||
)
|
||||
|
||||
search_fields = ('course_id',)
|
||||
|
||||
list_display = (
|
||||
'id',
|
||||
'course_id',
|
||||
'mode_slug',
|
||||
'min_price',
|
||||
'expiration_datetime_custom',
|
||||
'sku'
|
||||
)
|
||||
exclude = ('suggested_prices',)
|
||||
|
||||
def expiration_datetime_custom(self, obj):
|
||||
"""adding custom column to show the expiry_datetime"""
|
||||
if obj.expiration_datetime:
|
||||
return get_time_display(obj.expiration_datetime, '%B %d, %Y, %H:%M %p')
|
||||
|
||||
expiration_datetime_custom.short_description = "Expiration Datetime"
|
||||
# Display a more user-friendly name for the custom expiration datetime field
|
||||
# in the Django admin list view.
|
||||
expiration_datetime_custom.short_description = "Upgrade Deadline"
|
||||
|
||||
admin.site.register(CourseMode, CourseModeAdmin)
|
||||
|
||||
@@ -30,28 +30,45 @@ class CourseMode(models.Model):
|
||||
|
||||
"""
|
||||
# the course that this mode is attached to
|
||||
course_id = CourseKeyField(max_length=255, db_index=True)
|
||||
course_id = CourseKeyField(max_length=255, db_index=True, verbose_name=_("Course"))
|
||||
|
||||
# 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)
|
||||
mode_slug = models.CharField(max_length=100, verbose_name=_("Mode"))
|
||||
|
||||
# The 'pretty' name that can be translated and displayed
|
||||
mode_display_name = models.CharField(max_length=255)
|
||||
mode_display_name = models.CharField(max_length=255, verbose_name=_("Display Name"))
|
||||
|
||||
# minimum price in USD that we would like to charge for this mode of the course
|
||||
min_price = models.IntegerField(default=0)
|
||||
|
||||
# the suggested prices for this mode
|
||||
suggested_prices = models.CommaSeparatedIntegerField(max_length=255, blank=True, default='')
|
||||
# The price in USD that we would like to charge for this mode of the course
|
||||
# Historical note: We used to allow users to choose from several prices, but later
|
||||
# switched to using a single price. Although this field is called `min_price`, it is
|
||||
# really just the price of the course.
|
||||
min_price = models.IntegerField(default=0, verbose_name=_("Price"))
|
||||
|
||||
# the currency these prices are in, using lower case ISO currency codes
|
||||
currency = models.CharField(default="usd", max_length=8)
|
||||
|
||||
# turn this mode off after the given expiration date
|
||||
# The datetime at which the course mode will expire.
|
||||
# This is used to implement "upgrade" deadlines.
|
||||
# For example, if there is a verified mode that expires on 1/1/2015,
|
||||
# then users will be able to upgrade into the verified mode before that date.
|
||||
# Once the date passes, users will no longer be able to enroll as verified.
|
||||
expiration_datetime = models.DateTimeField(
|
||||
default=None, null=True, blank=True,
|
||||
verbose_name=_(u"Upgrade Deadline"),
|
||||
help_text=_(
|
||||
u"OPTIONAL: After this date/time, users will no longer be able to enroll in this mode. "
|
||||
u"Leave this blank if users can enroll in this mode until enrollment closes for the course."
|
||||
),
|
||||
)
|
||||
|
||||
# DEPRECATED: the `expiration_date` field has been replaced by `expiration_datetime`
|
||||
expiration_date = models.DateField(default=None, null=True, blank=True)
|
||||
|
||||
expiration_datetime = models.DateTimeField(default=None, null=True, blank=True)
|
||||
# DEPRECATED: the suggested prices for this mode
|
||||
# We used to allow users to choose from a set of prices, but we now allow only
|
||||
# a single price. This field has been deprecated by `min_price`
|
||||
suggested_prices = models.CommaSeparatedIntegerField(max_length=255, blank=True, default='')
|
||||
|
||||
# optional description override
|
||||
# WARNING: will not be localized
|
||||
@@ -63,7 +80,10 @@ class CourseMode(models.Model):
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name="SKU",
|
||||
help_text="This is the SKU (stock keeping unit) of this mode in the external ecommerce service."
|
||||
help_text=_(
|
||||
u"OPTIONAL: This is the SKU (stock keeping unit) of this mode in the external ecommerce service. "
|
||||
u"Leave this blank if the course has not yet been migrated to the ecommerce service."
|
||||
)
|
||||
)
|
||||
|
||||
HONOR = 'honor'
|
||||
@@ -217,7 +237,7 @@ class CourseMode(models.Model):
|
||||
return modes
|
||||
|
||||
@classmethod
|
||||
def modes_for_course_dict(cls, course_id, modes=None, only_selectable=True):
|
||||
def modes_for_course_dict(cls, course_id, modes=None, **kwargs):
|
||||
"""Returns the non-expired modes for a particular course.
|
||||
|
||||
Arguments:
|
||||
@@ -228,6 +248,9 @@ class CourseMode(models.Model):
|
||||
of course modes. This can be used to avoid an additional
|
||||
database query if you have already loaded the modes list.
|
||||
|
||||
include_expired (bool): If True, expired course modes will be included
|
||||
in the returned values. If False, these modes will be omitted.
|
||||
|
||||
only_selectable (bool): If True, include only modes that are shown
|
||||
to users on the track selection page. (Currently, "credit" modes
|
||||
aren't available to users until they complete the course, so
|
||||
@@ -238,7 +261,7 @@ class CourseMode(models.Model):
|
||||
|
||||
"""
|
||||
if modes is None:
|
||||
modes = cls.modes_for_course(course_id, only_selectable=only_selectable)
|
||||
modes = cls.modes_for_course(course_id, **kwargs)
|
||||
|
||||
return {mode.slug: mode for mode in modes}
|
||||
|
||||
|
||||
220
common/djangoapps/course_modes/tests/test_admin.py
Normal file
220
common/djangoapps/course_modes/tests/test_admin.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""
|
||||
Tests for the course modes Django admin interface.
|
||||
"""
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import ddt
|
||||
from pytz import timezone, UTC
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from util.date_utils import get_time_display
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from student.tests.factories import UserFactory
|
||||
from course_modes.models import CourseMode
|
||||
from course_modes.admin import CourseModeForm
|
||||
|
||||
# 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 verify_student.models import VerificationDeadline # pylint: disable=import-error
|
||||
|
||||
|
||||
# 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 seperate 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 = CourseMode.objects.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 = CourseMode.objects.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)
|
||||
@@ -4,12 +4,9 @@ import ddt
|
||||
from mock import patch
|
||||
from django.conf import settings
|
||||
from django.core.urlresolvers import reverse
|
||||
from pytz import timezone
|
||||
from datetime import datetime
|
||||
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
|
||||
from util.date_utils import get_time_display
|
||||
from util.testing import UrlResetMixin
|
||||
from embargo.test_utils import restrict_course
|
||||
from xmodule.modulestore.tests.factories import CourseFactory
|
||||
@@ -371,45 +368,3 @@ class TrackSelectionEmbargoTest(UrlResetMixin, ModuleStoreTestCase):
|
||||
def test_embargo_allow(self):
|
||||
response = self.client.get(self.url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
|
||||
class AdminCourseModePageTest(ModuleStoreTestCase):
|
||||
"""Test the django admin course mode form saving data in db without any conversion
|
||||
properly converts the tzinfo from default zone to utc
|
||||
"""
|
||||
|
||||
def test_save_valid_data(self):
|
||||
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': 'professional',
|
||||
'mode_display_name': 'professional',
|
||||
'min_price': 10,
|
||||
'currency': 'usd',
|
||||
'expiration_datetime_0': expiration.date(), # due to django admin datetime widget passing as seperate vals
|
||||
'expiration_datetime_1': expiration.time(),
|
||||
|
||||
}
|
||||
|
||||
self.client.login(username=user.username, password='test')
|
||||
|
||||
# creating 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'))
|
||||
|
||||
# verifying that datetime is appearing 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'))
|
||||
|
||||
# verifiying the on edit page datetime value appearing without any modifications
|
||||
resp = self.client.get(reverse('admin:course_modes_coursemode_change', args=(1,)))
|
||||
self.assertContains(resp, expiration.date())
|
||||
self.assertContains(resp, expiration.time())
|
||||
|
||||
# checking the values in db. comparing values without tzinfo
|
||||
course_mode = CourseMode.objects.get(pk=1)
|
||||
self.assertEqual(course_mode.expiration_datetime.replace(tzinfo=None), expiration.replace(tzinfo=None))
|
||||
|
||||
@@ -6,7 +6,7 @@ from pytz import UTC
|
||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||
|
||||
import third_party_auth
|
||||
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error
|
||||
from verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification # pylint: disable=import-error
|
||||
from course_modes.models import CourseMode
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ VERIFY_STATUS_MISSED_DEADLINE = "verify_missed_deadline"
|
||||
VERIFY_STATUS_NEED_TO_REVERIFY = "verify_need_to_reverify"
|
||||
|
||||
|
||||
def check_verify_status_by_course(user, course_enrollments, all_course_modes):
|
||||
def check_verify_status_by_course(user, course_enrollments):
|
||||
"""
|
||||
Determine the per-course verification statuses for a given user.
|
||||
|
||||
@@ -44,8 +44,6 @@ def check_verify_status_by_course(user, course_enrollments, all_course_modes):
|
||||
Arguments:
|
||||
user (User): The currently logged-in user.
|
||||
course_enrollments (list[CourseEnrollment]): The courses the user is enrolled in.
|
||||
all_course_modes (list): List of all course modes for the student's enrolled courses,
|
||||
including modes that have expired.
|
||||
|
||||
Returns:
|
||||
dict: Mapping of course keys verification status dictionaries.
|
||||
@@ -69,24 +67,21 @@ def check_verify_status_by_course(user, course_enrollments, all_course_modes):
|
||||
user, queryset=verifications
|
||||
)
|
||||
|
||||
# Retrieve verification deadlines for the enrolled courses
|
||||
enrolled_course_keys = [enrollment.course_id for enrollment in course_enrollments]
|
||||
course_deadlines = VerificationDeadline.deadlines_for_courses(enrolled_course_keys)
|
||||
|
||||
recent_verification_datetime = None
|
||||
|
||||
for enrollment in course_enrollments:
|
||||
|
||||
# Get the verified mode (if any) for this course
|
||||
# We pass in the course modes we have already loaded to avoid
|
||||
# another database hit, as well as to ensure that expired
|
||||
# course modes are included in the search.
|
||||
verified_mode = CourseMode.verified_mode_for_course(
|
||||
enrollment.course_id,
|
||||
modes=all_course_modes[enrollment.course_id]
|
||||
)
|
||||
# If the user hasn't enrolled as verified, then the course
|
||||
# won't display state related to its verification status.
|
||||
if enrollment.mode in CourseMode.VERIFIED_MODES:
|
||||
|
||||
# If no verified mode has ever been offered, or the user hasn't enrolled
|
||||
# as verified, then the course won't display state related to its
|
||||
# verification status.
|
||||
if verified_mode is not None and enrollment.mode in CourseMode.VERIFIED_MODES:
|
||||
deadline = verified_mode.expiration_datetime
|
||||
# Retrieve the verification deadline associated with the course.
|
||||
# This could be None if the course doesn't have a deadline.
|
||||
deadline = course_deadlines.get(enrollment.course_id)
|
||||
|
||||
relevant_verification = SoftwareSecurePhotoVerification.verification_for_datetime(deadline, verifications)
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
from student.tests.factories import UserFactory, CourseEnrollmentFactory
|
||||
from course_modes.tests.factories import CourseModeFactory
|
||||
from verify_student.models import SoftwareSecurePhotoVerification # pylint: disable=import-error
|
||||
from verify_student.models import VerificationDeadline, SoftwareSecurePhotoVerification # pylint: disable=import-error
|
||||
from util.testing import UrlResetMixin
|
||||
|
||||
|
||||
@@ -61,9 +61,11 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
mode="verified"
|
||||
)
|
||||
|
||||
# The default course has no verified mode,
|
||||
# so no verification status should be displayed
|
||||
self._assert_course_verification_status(None)
|
||||
# Continue to show the student as needing to verify.
|
||||
# The student is enrolled as verified, so we might as well let them
|
||||
# complete verification. We'd need to change their enrollment mode
|
||||
# anyway to ensure that the student is issued the correct kind of certificate.
|
||||
self._assert_course_verification_status(VERIFY_STATUS_NEED_TO_VERIFY)
|
||||
|
||||
def test_need_to_verify_no_expiration(self):
|
||||
self._setup_mode_and_enrollment(None, "verified")
|
||||
@@ -285,6 +287,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
user=self.user,
|
||||
mode=enrollment_mode
|
||||
)
|
||||
VerificationDeadline.set_deadline(self.course.id, deadline)
|
||||
|
||||
BANNER_ALT_MESSAGES = {
|
||||
None: "Honor",
|
||||
|
||||
@@ -533,7 +533,7 @@ def dashboard(request):
|
||||
|
||||
# Retrieve the course modes for each course
|
||||
enrolled_course_ids = [enrollment.course_id for enrollment in course_enrollments]
|
||||
all_course_modes, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(enrolled_course_ids)
|
||||
__, unexpired_course_modes = CourseMode.all_and_unexpired_modes_for_courses(enrolled_course_ids)
|
||||
course_modes_by_course = {
|
||||
course_id: {
|
||||
mode.slug: mode
|
||||
@@ -596,11 +596,7 @@ def dashboard(request):
|
||||
#
|
||||
# If a course is not included in this dictionary,
|
||||
# there is no verification messaging to display.
|
||||
verify_status_by_course = check_verify_status_by_course(
|
||||
user,
|
||||
course_enrollments,
|
||||
all_course_modes
|
||||
)
|
||||
verify_status_by_course = check_verify_status_by_course(user, course_enrollments)
|
||||
cert_statuses = {
|
||||
enrollment.course_id: cert_info(request.user, enrollment.course_overview, enrollment.mode)
|
||||
for enrollment in course_enrollments
|
||||
|
||||
Reference in New Issue
Block a user