Files
edx-platform/openedx/core/djangoapps/schedules/admin.py
Michael Terry 74887aa216 feat: turn on schedule creation by default
This commit removes several waffle toggles that have been enabled
on edx.org for years. It's time to remove the rollout gating for
these features and enable them by default.

This doesn't directly change any behavior. But it does create new
database objects by default now and allows for enabling other
schedule based features more easily.

Specifically, the following toggles were affected.

schedules.create_schedules_for_course
- Waffle flag removed as always-enabled
- We now always create a schedule when an enrollment is created

schedules.send_updates_for_course
- Waffle flag removed as always-enabled
- Course update emails are sent as long as the ScheduleConfig
  allows it.
- This is not a change in default behavior, because ScheduleConfig
  is off by default.

dynamic_pacing.studio_course_update
- Waffle switch removed as always-enabled
- Course teams can now always edit course updates directly in Studio

ScheduleConfig.create_schedules
ScheduleConfig.hold_back_ratio
- Model fields for rolling out the schedules feature
- Schedules are now always created
- This commit only removes references to these fields, they still
  exist in the database. A future commit will remove them entirely

This commit also adds a new has_highlights field to CourseOverview.
This is used to cache whether a course has highlights, used to
decide which course update email behavior they get. Previously every
enrollment had to dig into the modulestore to determine that.
2021-02-23 12:34:02 -05:00

182 lines
6.2 KiB
Python

# lint-amnesty, pylint: disable=missing-module-docstring
import functools
import six
from django import forms
from django.contrib import admin
from django.db.models import F
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _
from opaque_keys.edx.keys import CourseKey
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from . import models
class ScheduleExperienceAdminInline(admin.StackedInline):
model = models.ScheduleExperience
def _set_experience(db_name, human_name, modeladmin, request, queryset): # lint-amnesty, pylint: disable=redefined-outer-name
"""
A django action which will set all selected schedules to the supplied experience.
The intended usage is with functools.partial to generate the action for each experience type
dynamically.
Arguments:
db_name: the database name of the experience being selected
human_name: the human name of the experience being selected
modeladmin: The ModelAdmin subclass, passed by django as part of the standard Action interface
request: The current request, passed by django as part of the standard Action interface
queryset: The queryset selecting schedules, passed by django as part of the standard Action interface
"""
rows_updated = models.ScheduleExperience.objects.filter(
schedule__in=list(queryset)
).update(
experience_type=db_name
)
modeladmin.message_user(
request,
u"{} schedule(s) were changed to use the {} experience".format(
rows_updated,
human_name,
)
)
# Generate a list of all "set_experience_to_X" actions
experience_actions = []
for (db_name, human_name) in models.ScheduleExperience.EXPERIENCES:
partial = functools.partial(_set_experience, db_name, human_name)
partial.short_description = u"Convert the selected schedules to the {} experience".format(human_name)
partial.__name__ = "set_experience_to_{}".format(db_name)
experience_actions.append(partial)
class KnownErrorCases(admin.SimpleListFilter):
"""
Filter schedules by a list of known error cases.
"""
title = _('Known Error Case')
parameter_name = 'error'
def lookups(self, request, model_admin):
return (
('schedule_start_date', _('Schedule start < course start')),
)
def queryset(self, request, queryset):
if self.value() == 'schedule_start_date':
return queryset.filter(start_date__lt=F('enrollment__course__start'))
class CourseIdFilter(admin.SimpleListFilter):
"""
Filter schedules to by course id using a dropdown list.
"""
template = "dropdown_filter.html"
title = _("Course Id")
parameter_name = "course_id"
def __init__(self, request, params, model, model_admin):
super(CourseIdFilter, self).__init__(request, params, model, model_admin) # lint-amnesty, pylint: disable=super-with-arguments
self.unused_parameters = params.copy()
self.unused_parameters.pop(self.parameter_name, None)
def value(self):
value = super(CourseIdFilter, self).value() # lint-amnesty, pylint: disable=super-with-arguments
if value == "None" or value is None:
return None
else:
return CourseKey.from_string(value)
def lookups(self, request, model_admin):
return (
(overview.id, six.text_type(overview.id)) for overview in CourseOverview.objects.all().order_by('id')
)
def queryset(self, request, queryset):
value = self.value()
if value is None:
return queryset
else:
return queryset.filter(enrollment__course_id=value)
def choices(self, changelist):
yield {
'selected': self.value() is None,
'value': None,
'display': _('All'),
}
for lookup, title in self.lookup_choices:
yield {
'selected': self.value() == lookup,
'value': six.text_type(lookup),
'display': title,
}
@admin.register(models.Schedule)
class ScheduleAdmin(admin.ModelAdmin): # lint-amnesty, pylint: disable=missing-class-docstring
list_display = ('username', 'course_id', 'start_date', 'upgrade_deadline', 'experience_display')
list_display_links = ('start_date', 'upgrade_deadline', 'experience_display')
list_filter = (
CourseIdFilter,
'experience__experience_type',
KnownErrorCases
)
raw_id_fields = ('enrollment',)
readonly_fields = ('modified',)
search_fields = ('enrollment__user__username',)
inlines = (ScheduleExperienceAdminInline,)
actions = experience_actions
def experience_display(self, obj):
return obj.experience.get_experience_type_display()
experience_display.short_descriptions = _('Experience')
def username(self, obj):
return format_html(
'<a href="{}">{}</a>',
reverse("admin:auth_user_change", args=(obj.enrollment.user.id,)),
obj.enrollment.user.username
)
username.short_description = _('Username')
def course_id(self, obj):
return format_html(
'<a href="{}">{}</a>',
reverse("admin:course_overviews_courseoverview_change", args=(
obj.enrollment.course_id,
)),
obj.enrollment.course_id
)
course_id.short_description = _('Course ID')
def get_queryset(self, request):
qs = super(ScheduleAdmin, self).get_queryset(request) # lint-amnesty, pylint: disable=super-with-arguments
qs = qs.select_related('enrollment', 'enrollment__user')
return qs
class ScheduleConfigAdminForm(forms.ModelForm): # lint-amnesty, pylint: disable=missing-class-docstring
pass
@admin.register(models.ScheduleConfig)
class ScheduleConfigAdmin(admin.ModelAdmin): # lint-amnesty, pylint: disable=missing-class-docstring
search_fields = ('site',)
list_display = (
'site',
'enqueue_recurring_nudge', 'deliver_recurring_nudge',
'enqueue_upgrade_reminder', 'deliver_upgrade_reminder',
'enqueue_course_update', 'deliver_course_update',
)
form = ScheduleConfigAdminForm