* mattdrayer: Add bulk checkout link to course views * asadiqbal08: MAYN-225 replace the "Verify Now" button by the "Go to Dashboard" button in case of themed sites. * mattdrayer: Add bulk_sku check in courseware.views
221 lines
9.4 KiB
Python
221 lines
9.4 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
|
|
|
|
|
|
class CourseModeForm(forms.ModelForm):
|
|
|
|
class Meta(object):
|
|
model = CourseMode
|
|
fields = '__all__'
|
|
|
|
COURSE_MODE_SLUG_CHOICES = (
|
|
[(CourseMode.DEFAULT_MODE_SLUG, CourseMode.DEFAULT_MODE_SLUG)] +
|
|
[(mode_slug, mode_slug) for mode_slug in CourseMode.VERIFIED_MODES] +
|
|
[(CourseMode.NO_ID_PROFESSIONAL_MODE, CourseMode.NO_ID_PROFESSIONAL_MODE)] +
|
|
[(mode_slug, mode_slug) for mode_slug in CourseMode.CREDIT_MODES] +
|
|
# need to keep legacy modes around for awhile
|
|
[(CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG, CourseMode.DEFAULT_SHOPPINGCART_MODE_SLUG)]
|
|
)
|
|
|
|
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)
|