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:
Will Daly
2015-07-09 13:18:36 -07:00
parent 80cf4d6ecb
commit 25fa2ffc2d
15 changed files with 1181 additions and 195 deletions

View File

@@ -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)

View File

@@ -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}

View 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)

View File

@@ -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))

View File

@@ -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)

View File

@@ -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",

View File

@@ -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