feat: Add Django admin interface to backfill course outlines. (#27749)
TNL-8080
This commit is contained in:
@@ -2,10 +2,111 @@
|
||||
Admin site bindings for contentstore
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
from config_models.admin import ConfigurationModelAdmin
|
||||
from django.contrib import admin
|
||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||
from django.utils.translation import ugettext as _
|
||||
|
||||
from cms.djangoapps.contentstore.models import VideoUploadConfig
|
||||
from cms.djangoapps.contentstore.outlines_regenerate import CourseOutlineRegenerate
|
||||
from openedx.core.djangoapps.content.learning_sequences.api import key_supports_outlines
|
||||
|
||||
from .tasks import update_outline_from_modulestore_task, update_all_outlines_from_modulestore_task
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReadOnlyAdminMixin(object):
|
||||
"""
|
||||
Disables all editing capabilities for the admin's model.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.list_display_links = None
|
||||
self.readonly_fields = [f.name for f in self.model._meta.get_fields()]
|
||||
|
||||
def get_actions(self, request):
|
||||
actions = super().get_actions(request)
|
||||
if 'delete_selected' in actions:
|
||||
del actions["delete_selected"]
|
||||
return actions
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
def has_delete_permission(self, request, obj=None): # pylint: disable=unused-argument
|
||||
return False
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
pass
|
||||
|
||||
def delete_model(self, request, obj):
|
||||
pass
|
||||
|
||||
def save_related(self, request, form, formsets, change):
|
||||
pass
|
||||
|
||||
|
||||
def regenerate_course_outlines_subset(modeladmin, request, queryset):
|
||||
"""
|
||||
Create a celery task to regenerate a single course outline for each passed-in course key.
|
||||
|
||||
If the number of passed-in course keys is above a threshold, then instead create a celery task which
|
||||
will then create a celery task to regenerate a single course outline for each passed-in course key.
|
||||
"""
|
||||
all_course_keys_qs = queryset.values_list('id', flat=True)
|
||||
|
||||
# Create a separate celery task for each course outline requested.
|
||||
regenerates = 0
|
||||
for course_key in all_course_keys_qs:
|
||||
if key_supports_outlines(course_key):
|
||||
log.info("Queuing outline creation for %s", course_key)
|
||||
update_outline_from_modulestore_task.delay(str(course_key))
|
||||
regenerates += 1
|
||||
else:
|
||||
log.info("Outlines not supported for %s - skipping", course_key)
|
||||
msg = _("Number of course outline regenerations successfully requested: {regenerates}").format(
|
||||
regenerates=regenerates
|
||||
)
|
||||
modeladmin.message_user(request, msg)
|
||||
regenerate_course_outlines_subset.short_description = _("Regenerate selected course outlines")
|
||||
|
||||
|
||||
def regenerate_course_outlines_all(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
"""
|
||||
Custom admin action which regenerates *all* the course outlines - no matter which CourseOverviews are selected.
|
||||
"""
|
||||
update_all_outlines_from_modulestore_task.delay()
|
||||
modeladmin.message_user(request, _("All course outline regenerations successfully requested."))
|
||||
regenerate_course_outlines_all.short_description = _("Regenerate *all* course outlines")
|
||||
|
||||
|
||||
class CourseOutlineRegenerateAdmin(ReadOnlyAdminMixin, admin.ModelAdmin):
|
||||
"""
|
||||
Regenerates the course outline for each selected course key.
|
||||
"""
|
||||
list_display = ['id']
|
||||
ordering = ['id']
|
||||
search_fields = ['id']
|
||||
|
||||
actions = [regenerate_course_outlines_subset, regenerate_course_outlines_all]
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
"""
|
||||
Overrides the admin's changelist_view & selects at least one of the CourseOverviews
|
||||
when the custom regenerate_course_outlines_all action is selected.
|
||||
"""
|
||||
if 'action' in request.POST and request.POST['action'] == 'regenerate_course_outlines_all':
|
||||
# Slight hack: Ensure that at least one CourseOverview course key is selected.
|
||||
# The selection will be ignored, but the action will fail if *nothing* is selected.
|
||||
post = request.POST.copy()
|
||||
post.setlist(ACTION_CHECKBOX_NAME, self.model.get_course_outline_ids()[:1])
|
||||
request._set_post(post) # pylint: disable=protected-access
|
||||
return super().changelist_view(request, extra_context)
|
||||
|
||||
|
||||
admin.site.register(VideoUploadConfig, ConfigurationModelAdmin)
|
||||
admin.site.register(CourseOutlineRegenerate, CourseOutlineRegenerateAdmin)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Generated by Django 2.2.20 on 2021-05-27 17:07
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('course_overviews', '0024_overview_adds_has_highlights'),
|
||||
('contentstore', '0005_add_enable_checklists_quality_waffle_flag'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='CourseOutlineRegenerate',
|
||||
fields=[
|
||||
],
|
||||
options={
|
||||
'proxy': True,
|
||||
'indexes': [],
|
||||
'constraints': [],
|
||||
},
|
||||
bases=('course_overviews.courseoverview',),
|
||||
),
|
||||
]
|
||||
28
cms/djangoapps/contentstore/outlines_regenerate.py
Normal file
28
cms/djangoapps/contentstore/outlines_regenerate.py
Normal file
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Defines a proxy model to enable a Django admin interface to trigger asynch
|
||||
tasks which regenerates course outline data.
|
||||
"""
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
|
||||
|
||||
class CourseOutlineRegenerate(CourseOverview):
|
||||
"""
|
||||
Proxy model for CourseOverview.
|
||||
|
||||
Does *not* create/update/delete CourseOverview objects - only reads the objects.
|
||||
Uses the course IDs of the CourseOverview objects to determine which course
|
||||
outlines to regenerate.
|
||||
"""
|
||||
class Meta:
|
||||
proxy = True
|
||||
|
||||
def __str__(self):
|
||||
"""Represent ourselves with the course key."""
|
||||
return str(self.id)
|
||||
|
||||
@classmethod
|
||||
def get_course_outline_ids(cls):
|
||||
"""
|
||||
Returns all the CourseOverview object ids.
|
||||
"""
|
||||
return cls.objects.values_list('id', flat=True)
|
||||
@@ -65,6 +65,7 @@ from xmodule.modulestore.xml_importer import import_course_from_xml, import_libr
|
||||
|
||||
from .exceptions import CourseImportException
|
||||
from .outlines import update_outline_from_modulestore
|
||||
from .outlines_regenerate import CourseOutlineRegenerate
|
||||
from .toggles import bypass_olx_failure_enabled
|
||||
from .utils import course_import_olx_validation_is_enabled
|
||||
|
||||
@@ -668,7 +669,34 @@ def import_olx(self, user_id, course_key_string, archive_path, archive_name, lan
|
||||
|
||||
@shared_task
|
||||
@set_code_owner_attribute
|
||||
def update_outline_from_modulestore_task(course_key_str):
|
||||
def update_all_outlines_from_modulestore_task():
|
||||
"""
|
||||
Celery task that creates multiple celery tasks - one per learning_sequence course outline
|
||||
to regenerate. The list of course keys to regenerate comes from the proxy model itself.
|
||||
"""
|
||||
course_key_list = [str(course_key) for course_key in CourseOutlineRegenerate.get_course_outline_ids()]
|
||||
for course_key_str in course_key_list:
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_key_str)
|
||||
if not key_supports_outlines(course_key):
|
||||
LOGGER.warning(
|
||||
(
|
||||
"update_multiple_outlines_from_modulestore_task called for course key"
|
||||
" %s, which does not support learning_sequence outlines."
|
||||
),
|
||||
course_key_str
|
||||
)
|
||||
return
|
||||
|
||||
update_outline_from_modulestore_task.delay(course_key_str)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
# Swallow the exception to continue the loop through course keys - but log it.
|
||||
LOGGER.exception("Could not create course outline for course %s", course_key_str)
|
||||
|
||||
|
||||
@shared_task
|
||||
@set_code_owner_attribute
|
||||
def update_outline_from_modulestore_task(course_key_str: str):
|
||||
"""
|
||||
Celery task that creates a learning_sequence course outline.
|
||||
"""
|
||||
@@ -685,7 +713,7 @@ def update_outline_from_modulestore_task(course_key_str):
|
||||
return
|
||||
|
||||
update_outline_from_modulestore(course_key)
|
||||
except Exception: # pylint disable=broad-except
|
||||
except Exception: # pylint: disable=broad-except
|
||||
LOGGER.exception("Could not create course outline for course %s", course_key_str)
|
||||
raise # Re-raise so that errors are noted in reporting.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user