diff --git a/enterprise_catalog/apps/catalog/admin.py b/enterprise_catalog/apps/catalog/admin.py index a7217f27..415e3acb 100644 --- a/enterprise_catalog/apps/catalog/admin.py +++ b/enterprise_catalog/apps/catalog/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin from django.urls import reverse -from django.utils.html import format_html +from django.utils.html import format_html, format_html_join +from django.utils.safestring import mark_safe from edx_rbac.admin import UserRoleAssignmentAdmin from enterprise_catalog.apps.catalog.constants import ( @@ -15,9 +16,20 @@ ContentMetadata, EnterpriseCatalog, EnterpriseCatalogRoleAssignment, + RestrictedCourseMetadata, + RestrictedRunAllowedForRestrictedCourse, ) +def _html_list_from_objects(objs, viewname, str_callback=None): + str_callback = str_callback or str + return format_html_join( + sep=mark_safe('
'), + format_string='{}', + args_generator=((reverse(viewname, args=[obj.pk]), str_callback(obj)) for obj in objs), + ) + + class UnchangeableMixin(admin.ModelAdmin): """ Mixin for disabling changing models through the admin @@ -59,17 +71,99 @@ class ContentMetadataAdmin(UnchangeableMixin): ) readonly_fields = ( 'associated_content_metadata', - 'catalog_queries', - 'get_catalog', + 'get_catalog_queries', + 'get_catalogs', + 'get_restricted_courses', 'modified', ) + exclude = ( + 'catalog_queries', + ) + + @admin.display(description='Catalog Queries') + def get_catalog_queries(self, obj): + catalog_queries = obj.catalog_queries.all() + return _html_list_from_objects( + objs=catalog_queries, + viewname="admin:catalog_catalogquery_change", + str_callback=lambda cq: cq.short_str_for_listings(), + ) @admin.display(description='Enterprise Catalogs') - def get_catalog(self, obj): + def get_catalogs(self, obj): catalogs = EnterpriseCatalog.objects.filter( catalog_query_id__in=obj.catalog_queries.all().values_list('id') ) - return f"{list(catalogs)}" + return _html_list_from_objects(catalogs, "admin:catalog_enterprisecatalog_change") + + @admin.display(description='Restricted For Courses') + def get_restricted_courses(self, obj): + restricted_runs_allowed_for_restricted_course = RestrictedRunAllowedForRestrictedCourse.objects.select_related( + 'course', + ).filter( + run=obj, + ) + restricted_courses = (relationship.course for relationship in restricted_runs_allowed_for_restricted_course) + return _html_list_from_objects(restricted_courses, "admin:catalog_contentmetadata_change") + + +@admin.register(RestrictedCourseMetadata) +class RestrictedCourseMetadataAdmin(UnchangeableMixin): + """ Admin configuration for the custom RestrictedCourseMetadata model. """ + list_display = ( + 'content_key', + 'get_catalog_query_for_list', + 'get_unrestricted_parent', + ) + search_fields = ( + 'content_key', + 'catalog_query', + ) + readonly_fields = ( + 'get_catalog_query', + 'get_catalogs', + 'get_restricted_runs_allowed', + 'modified', + ) + exclude = ( + 'catalog_query', + ) + + @admin.display( + description='Catalog Query' + ) + def get_catalog_query_for_list(self, obj): + link = reverse("admin:catalog_catalogquery_change", args=[obj.catalog_query.id]) + return format_html('{}', link, obj.catalog_query.short_str_for_listings()) + + @admin.display( + description='Catalog Query' + ) + def get_catalog_query(self, obj): + link = reverse("admin:catalog_catalogquery_change", args=[obj.catalog_query.id]) + return format_html('{}', link, obj.catalog_query.pretty_print_content_filter()) + + @admin.display( + description='Unrestricted Parent' + ) + def get_unrestricted_parent(self, obj): + link = reverse("admin:catalog_contentmetadata_change", args=[obj.unrestricted_parent.id]) + return format_html('{}', link, str(obj.unrestricted_parent)) + + @admin.display(description='Enterprise Catalogs') + def get_catalogs(self, obj): + catalogs = EnterpriseCatalog.objects.filter(catalog_query=obj.catalog_query) + return _html_list_from_objects(catalogs, "admin:catalog_enterprisecatalog_change") + + @admin.display(description='Restricted Runs Allowed') + def get_restricted_runs_allowed(self, obj): + restricted_runs_allowed_for_restricted_course = RestrictedRunAllowedForRestrictedCourse.objects.select_related( + 'run', + ).filter( + course=obj, + ) + restricted_runs = (relationship.run for relationship in restricted_runs_allowed_for_restricted_course) + return _html_list_from_objects(restricted_runs, "admin:catalog_contentmetadata_change") @admin.register(CatalogQuery) diff --git a/enterprise_catalog/apps/catalog/migrations/0042_alter_restrictedcoursemetadata_display_name.py b/enterprise_catalog/apps/catalog/migrations/0042_alter_restrictedcoursemetadata_display_name.py new file mode 100644 index 00000000..d3ea1168 --- /dev/null +++ b/enterprise_catalog/apps/catalog/migrations/0042_alter_restrictedcoursemetadata_display_name.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-10-11 00:52 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('catalog', '0041_restrictedcoursemetadata_and_more'), + ] + + operations = [ + migrations.AlterModelOptions( + name='historicalrestrictedcoursemetadata', + options={'get_latest_by': ('history_date', 'history_id'), 'ordering': ('-history_date', '-history_id'), 'verbose_name': 'historical Restricted Course Metadata', 'verbose_name_plural': 'historical Restricted Course Metadata'}, + ), + migrations.AlterModelOptions( + name='restrictedcoursemetadata', + options={'verbose_name': 'Restricted Course Metadata', 'verbose_name_plural': 'Restricted Course Metadata'}, + ), + ] diff --git a/enterprise_catalog/apps/catalog/models.py b/enterprise_catalog/apps/catalog/models.py index 8d561bf5..aac2fb10 100644 --- a/enterprise_catalog/apps/catalog/models.py +++ b/enterprise_catalog/apps/catalog/models.py @@ -166,6 +166,14 @@ def __str__(self): f"and content_filter '{self.pretty_print_content_filter()}'>" ) + def short_str_for_listings(self): + """ + Return *short* human-readable string representation for listings. + """ + return ( + f"" + ) + class EnterpriseCatalog(TimeStampedModel): """ @@ -715,11 +723,7 @@ def __str__(self): """ Return human-readable string representation. """ - return ( - "".format( - content_key=self.content_key - ) - ) + return f"<{self.__class__.__name__} for '{self.content_key}'>" class ContentMetadata(BaseContentMetadata): @@ -775,8 +779,8 @@ class RestrictedCourseMetadata(BaseContentMetadata): .. no_pii: """ class Meta: - verbose_name = _("Restricted Content Metadata") - verbose_name_plural = _("Restricted Content Metadata") + verbose_name = _("Restricted Course Metadata") + verbose_name_plural = _("Restricted Course Metadata") app_label = 'catalog' unique_together = ('content_key', 'catalog_query') @@ -810,6 +814,12 @@ class Meta: ) history = HistoricalRecords() + def __str__(self): + """ + Return human-readable string representation. + """ + return f"<{self.__class__.__name__} for '{self.content_key}' and CatalogQuery ({self.catalog_query.id})>" + class RestrictedRunAllowedForRestrictedCourse(TimeStampedModel): """