Files
edx-platform/cms/djangoapps/contentstore/admin.py
Navin Karkera 1cd73d1b96 feat: support for syncing units from libraries to courses (#36553)
* feat: library unit sync
* feat: create component link only for component xblocks
* feat: container link model
* feat: update downstream api views
* feat: delete extra components in container on sync (not working)
* fix: duplicate definitions of LibraryXBlockMetadata
* test: add a new integration test suite for syncing
* feat: partially implement container+child syncing
* fix: blockserializer wasn't always serializing all HTML block fields
* feat: handle reorder, addition and deletion of components in sync

Updates children components of unit in course based on upstream unit,
deletes removed component, adds new ones and updates order as per
upstream.

* feat: return unit upstreamInfo and disallow edits to units in courses that are sourced from a library (#773)
* feat: Add upstream_info to unit
* feat: disallow edits to units in courses that are sourced from a library (#774)

---------

Co-authored-by: Jillian Vogel <jill@opencraft.com>
Co-authored-by: Rômulo Penido <romulo.penido@gmail.com>

* docs: capitalization of XBlock

Co-authored-by: David Ormsbee <dave@axim.org>

* refactor: (minor) change python property name to reflect type better

* fix: lots of "Tried to inspect a missing...upstream link" warnings

when viewing a unit in Studio

* docs: mention potential REST API for future refactor

* fix: check if upstream actually exists before making unit read-only

* chore: fix camel-case var

* fix: test failure when mocked XBlock doesn't have UpstreamSyncMixin

---------

Co-authored-by: Braden MacDonald <braden@opencraft.com>
Co-authored-by: Chris Chávez <xnpiochv@gmail.com>
Co-authored-by: Jillian Vogel <jill@opencraft.com>
Co-authored-by: Rômulo Penido <romulo.penido@gmail.com>
Co-authored-by: Braden MacDonald <mail@bradenm.com>
Co-authored-by: David Ormsbee <dave@axim.org>
2025-04-24 11:41:47 -07:00

199 lines
6.2 KiB
Python

"""
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 gettext as _
from edx_django_utils.admin.mixins import ReadOnlyAdminMixin
from cms.djangoapps.contentstore.models import (
BackfillCourseTabsConfig,
CleanStaleCertificateAvailabilityDatesConfig,
ComponentLink,
ContainerLink,
LearningContextLinksStatus,
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_all_outlines_from_modulestore_task, update_outline_from_modulestore_task
log = logging.getLogger(__name__)
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)
class CleanStaleCertificateAvailabilityDatesConfigAdmin(ConfigurationModelAdmin):
pass
@admin.register(ComponentLink)
class ComponentLinkAdmin(admin.ModelAdmin):
"""
ComponentLink admin.
"""
fields = (
"uuid",
"upstream_block",
"upstream_usage_key",
"upstream_context_key",
"downstream_usage_key",
"downstream_context_key",
"version_synced",
"version_declined",
"created",
"updated",
)
readonly_fields = fields
list_display = [
"upstream_block",
"upstream_usage_key",
"downstream_usage_key",
"version_synced",
"updated",
]
search_fields = [
"upstream_usage_key",
"upstream_context_key",
"downstream_usage_key",
"downstream_context_key",
]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(ContainerLink)
class ContainerLinkAdmin(admin.ModelAdmin):
"""
ContainerLink admin.
"""
fields = (
"uuid",
"upstream_container",
"upstream_container_key",
"upstream_context_key",
"downstream_usage_key",
"downstream_context_key",
"version_synced",
"version_declined",
"created",
"updated",
)
readonly_fields = fields
list_display = [
"upstream_container",
"upstream_container_key",
"downstream_usage_key",
"version_synced",
"updated",
]
search_fields = [
"upstream_container_key",
"upstream_context_key",
"downstream_usage_key",
"downstream_context_key",
]
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
@admin.register(LearningContextLinksStatus)
class LearningContextLinksStatusAdmin(admin.ModelAdmin):
"""
LearningContextLinksStatus admin.
"""
fields = (
"context_key",
"status",
"created",
"updated",
)
readonly_fields = ("created", "updated")
list_display = (
"context_key",
"status",
"created",
"updated",
)
def has_add_permission(self, request):
return False
def has_change_permission(self, request, obj=None):
return False
admin.site.register(BackfillCourseTabsConfig, ConfigurationModelAdmin)
admin.site.register(VideoUploadConfig, ConfigurationModelAdmin)
admin.site.register(CourseOutlineRegenerate, CourseOutlineRegenerateAdmin)
admin.site.register(CleanStaleCertificateAvailabilityDatesConfig, ConfigurationModelAdmin)