Files
edx-platform/common/djangoapps/course_modes/admin.py
2017-03-30 12:24:48 -04:00

217 lines
9.0 KiB
Python

"""
Django admin page for course modes
"""
from django.conf import settings
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.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, CourseModeExpirationConfig
# 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 lms.djangoapps.verify_student import models as verification_models
COURSE_MODE_SLUG_CHOICES = [(mode_slug, mode_slug) for mode_slug in settings.COURSE_ENROLLMENT_MODES]
class CourseModeForm(forms.ModelForm):
"""
Admin form for adding a course mode.
"""
class Meta(object):
model = CourseMode
fields = '__all__'
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: # pylint: disable=protected-access
# 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( # pylint: disable=protected-access
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']
try:
course_key = CourseKey.from_string(course_id)
except InvalidKeyError:
try:
course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id)
except InvalidKeyError:
raise forms.ValidationError("Cannot make a valid CourseKey from id {}!".format(course_id))
if not modulestore().has_course(course_key):
raise forms.ValidationError("Cannot find course with id {} in the modulestore".format(course_id))
return course_key
def clean__expiration_datetime(self):
"""
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
fields = (
'course_id',
'mode_slug',
'mode_display_name',
'min_price',
'currency',
'_expiration_datetime',
'verification_deadline',
'sku',
'bulk_sku'
)
search_fields = ('course_id',)
list_display = (
'id',
'course_id',
'mode_slug',
'min_price',
'expiration_datetime_custom',
'sku',
'bulk_sku'
)
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')
# 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"
class CourseModeExpirationConfigAdmin(admin.ModelAdmin):
"""Admin interface for the course mode auto expiration configuration. """
class Meta(object):
model = CourseModeExpirationConfig
admin.site.register(CourseMode, CourseModeAdmin)
admin.site.register(CourseModeExpirationConfig, CourseModeExpirationConfigAdmin)