Merge pull request #16615 from edx/cale/schedules-admin-improvements
Cale/schedules admin improvements
This commit is contained in:
@@ -1760,7 +1760,7 @@ class CourseEnrollment(models.Model):
|
||||
return None
|
||||
|
||||
try:
|
||||
if not self.schedule:
|
||||
if not self.schedule or not self.schedule.active:
|
||||
return None
|
||||
|
||||
log.debug(
|
||||
|
||||
@@ -12,7 +12,7 @@ from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from uuid import uuid4
|
||||
|
||||
from factory import Factory, Sequence, lazy_attribute_sequence, lazy_attribute
|
||||
from factory import Factory, Sequence, lazy_attribute_sequence, lazy_attribute, Faker
|
||||
from factory.errors import CyclicDefinitionError
|
||||
from mock import patch
|
||||
from nose.tools import assert_less_equal, assert_greater_equal
|
||||
|
||||
@@ -268,6 +268,22 @@ JWT_AUTH.update({
|
||||
'JWT_AUDIENCE': 'lms-key',
|
||||
})
|
||||
|
||||
|
||||
############## Settings for ACE ####################################
|
||||
ACE_ENABLED_CHANNELS = [
|
||||
'file_email'
|
||||
]
|
||||
ACE_ENABLED_POLICIES = [
|
||||
'bulk_email_optout'
|
||||
]
|
||||
ACE_CHANNEL_SAILTHRU_DEBUG = True
|
||||
ACE_CHANNEL_SAILTHRU_TEMPLATE_NAME = 'Automated Communication Engine Email'
|
||||
ACE_CHANNEL_SAILTHRU_API_KEY = None
|
||||
ACE_CHANNEL_SAILTHRU_API_SECRET = None
|
||||
|
||||
ACE_ROUTING_KEY = LOW_PRIORITY_QUEUE
|
||||
|
||||
|
||||
#####################################################################
|
||||
# See if the developer has any local overrides.
|
||||
if os.path.isfile(join(dirname(abspath(__file__)), 'private.py')):
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import functools
|
||||
|
||||
from django.contrib import admin
|
||||
from django import forms
|
||||
from django.db.models import F
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from . import models
|
||||
@@ -9,22 +13,94 @@ class ScheduleExperienceAdminInline(admin.StackedInline):
|
||||
model = models.ScheduleExperience
|
||||
|
||||
|
||||
def _set_experience(db_name, human_name, modeladmin, request, queryset):
|
||||
"""
|
||||
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, "{} 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 = "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):
|
||||
title = _('KnownErrorCases')
|
||||
|
||||
parameter_name = 'error'
|
||||
|
||||
def lookups(self, request, model_admin):
|
||||
return (
|
||||
('schedule_start', _('Schedule start < course start')),
|
||||
)
|
||||
|
||||
def queryset(self, request, queryset):
|
||||
if self.value() == 'schedule_start':
|
||||
return queryset.filter(start__lt=F('enrollment__course__start'))
|
||||
|
||||
|
||||
@admin.register(models.Schedule)
|
||||
class ScheduleAdmin(admin.ModelAdmin):
|
||||
list_display = ('username', 'course_id', 'active', 'start', 'upgrade_deadline')
|
||||
list_display = ('username', 'course_id', 'active', 'start', 'upgrade_deadline', 'experience_display')
|
||||
list_display_links = ('start', 'upgrade_deadline', 'experience_display')
|
||||
list_filter = ('experience__experience_type', 'active', KnownErrorCases)
|
||||
raw_id_fields = ('enrollment',)
|
||||
readonly_fields = ('modified',)
|
||||
search_fields = ('enrollment__user__username', 'enrollment__course_id',)
|
||||
search_fields = ('enrollment__user__username', 'enrollment__course__id',)
|
||||
inlines = (ScheduleExperienceAdminInline,)
|
||||
actions = ['deactivate_schedules', 'activate_schedules'] + experience_actions
|
||||
|
||||
def deactivate_schedules(self, request, queryset):
|
||||
rows_updated = queryset.update(active=False)
|
||||
self.message_user(request, "{} schedule(s) were deactivated".format(rows_updated))
|
||||
deactivate_schedules.short_description = "Deactivate selected schedules"
|
||||
|
||||
def activate_schedules(self, request, queryset):
|
||||
rows_updated = queryset.update(active=True)
|
||||
self.message_user(request, "{} schedule(s) were activated".format(rows_updated))
|
||||
activate_schedules.short_description = "Activate selected schedules"
|
||||
|
||||
def experience_display(self, obj):
|
||||
return obj.experience.get_experience_type_display()
|
||||
experience_display.short_descriptions = _('Experience')
|
||||
|
||||
def username(self, obj):
|
||||
return obj.enrollment.user.username
|
||||
return '<a href="{}">{}</a>'.format(
|
||||
reverse("admin:auth_user_change", args=(obj.enrollment.user.id,)),
|
||||
obj.enrollment.user.username
|
||||
)
|
||||
|
||||
username.allow_tags = True
|
||||
username.short_description = _('Username')
|
||||
|
||||
def course_id(self, obj):
|
||||
return obj.enrollment.course_id
|
||||
return '<a href="{}">{}</a>'.format(
|
||||
reverse("admin:course_overviews_courseoverview_change", args=(
|
||||
obj.enrollment.course_id,
|
||||
)),
|
||||
obj.enrollment.course_id
|
||||
)
|
||||
|
||||
course_id.allow_tags = True
|
||||
course_id.short_description = _('Course ID')
|
||||
|
||||
def get_queryset(self, request):
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import datetime
|
||||
import pytz
|
||||
import factory
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from student.models import CourseEnrollment
|
||||
from django.contrib.sites.models import Site
|
||||
from openedx.core.djangoapps.schedules.models import Schedule, ScheduleConfig, ScheduleExperience
|
||||
from openedx.core.djangoapps.schedules.tests.factories import ScheduleFactory, ScheduleConfigFactory, ScheduleExperienceFactory
|
||||
from student.tests.factories import CourseEnrollmentFactory
|
||||
from xmodule.modulestore.tests.factories import CourseFactory, XMODULE_FACTORY_LOCK
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from xmodule.modulestore.django import modulestore
|
||||
|
||||
|
||||
class ThreeDayNudgeSchedule(ScheduleFactory):
|
||||
start = factory.Faker('date_time_between', start_date='-3d', end_date='-3d', tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
class TenDayNudgeSchedule(ScheduleFactory):
|
||||
start = factory.Faker('date_time_between', start_date='-10d', end_date='-10d', tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
class UpgradeReminderSchedule(ScheduleFactory):
|
||||
start = factory.Faker('past_datetime', tzinfo=pytz.UTC)
|
||||
upgrade_deadline = factory.Faker('date_time_between', start_date='+2d', end_date='+2d', tzinfo=pytz.UTC)
|
||||
|
||||
|
||||
class ContentHighlightSchedule(ScheduleFactory):
|
||||
start = factory.Faker('date_time_between', start_date='-7d', end_date='-7d', tzinfo=pytz.UTC)
|
||||
experience = factory.RelatedFactory(ScheduleExperienceFactory, 'schedule', experience_type=ScheduleExperience.EXPERIENCES.course_updates)
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
A management command that generates schedule objects for all expected schedule email types, so that it is easy to
|
||||
generate test emails of all available types.
|
||||
"""
|
||||
|
||||
def handle(self, *args, **options):
|
||||
courses = modulestore().get_courses()
|
||||
|
||||
# Find the largest auto-generated course, and pick the next sequence id to generate the next
|
||||
# course with.
|
||||
max_org_sequence_id = max(int(course.org[4:]) for course in courses if course.org.startswith('org.'))
|
||||
|
||||
XMODULE_FACTORY_LOCK.enable()
|
||||
CourseFactory.reset_sequence(max_org_sequence_id + 1, force=True)
|
||||
course = CourseFactory(
|
||||
start=datetime.datetime.today() - datetime.timedelta(days=30),
|
||||
end=datetime.datetime.today() + datetime.timedelta(days=30),
|
||||
number=factory.Sequence('schedules_test_course_{}'.format),
|
||||
display_name=factory.Sequence('Schedules Test Course {}'.format),
|
||||
)
|
||||
XMODULE_FACTORY_LOCK.disable()
|
||||
course_overview = CourseOverview.load_from_module_store(course.id)
|
||||
ThreeDayNudgeSchedule.create(enrollment__course=course_overview)
|
||||
TenDayNudgeSchedule.create(enrollment__course=course_overview)
|
||||
UpgradeReminderSchedule.create(enrollment__course=course_overview)
|
||||
ContentHighlightSchedule.create(enrollment__course=course_overview)
|
||||
|
||||
ScheduleConfigFactory.create(site=Site.objects.get(name='example.com'))
|
||||
@@ -122,10 +122,12 @@ class BinnedSchedulesBaseResolver(PrefixedDebugLoggerMixin, RecipientResolver):
|
||||
'enrollment__course',
|
||||
).filter(
|
||||
Q(enrollment__course__end__isnull=True) | Q(
|
||||
enrollment__course__end__gte=self.current_datetime),
|
||||
enrollment__course__end__gte=self.current_datetime
|
||||
),
|
||||
self.experience_filter,
|
||||
enrollment__user__in=users,
|
||||
enrollment__is_active=True,
|
||||
active=True,
|
||||
**schedule_day_equals_target_day_filter
|
||||
).order_by(order_by)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user