33"""
44from __future__ import annotations
55
6+ import functools
7+
68from django .contrib import admin
79from django .db .models import Count
10+ from django .utils .html import format_html
11+ from django .utils .safestring import SafeText
812
9- from openedx_learning .lib .admin_utils import ReadOnlyModelAdmin , one_to_one_related_model_html
10-
13+ from openedx_learning .lib .admin_utils import (
14+ ReadOnlyModelAdmin ,
15+ model_detail_link ,
16+ one_to_one_related_model_html ,
17+ )
1118from .models import (
1219 DraftChangeLog ,
1320 DraftChangeLogRecord ,
21+ EntityList ,
22+ EntityListRow ,
1423 LearningPackage ,
1524 PublishableEntity ,
1625 PublishLog ,
1726 PublishLogRecord ,
27+ Container ,
28+ ContainerVersion ,
1829)
1930from .models .publish_log import Published
2031
@@ -122,6 +133,12 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
122133 "can_stand_alone" ,
123134 ]
124135
136+ def draft_version (self , entity : PublishableEntity ):
137+ return entity .draft .version .version_num if entity .draft .version else None
138+
139+ def published_version (self , entity : PublishableEntity ):
140+ return entity .published .version .version_num if entity .published and entity .published .version else None
141+
125142 def get_queryset (self , request ):
126143 queryset = super ().get_queryset (request )
127144 return queryset .select_related (
@@ -131,16 +148,6 @@ def get_queryset(self, request):
131148 def see_also (self , entity ):
132149 return one_to_one_related_model_html (entity )
133150
134- def draft_version (self , entity ):
135- if entity .draft .version :
136- return entity .draft .version .version_num
137- return None
138-
139- def published_version (self , entity ):
140- if entity .published .version :
141- return entity .published .version .version_num
142- return None
143-
144151
145152@admin .register (Published )
146153class PublishedAdmin (ReadOnlyModelAdmin ):
@@ -246,3 +253,261 @@ def get_queryset(self, request):
246253 queryset = super ().get_queryset (request )
247254 return queryset .select_related ("learning_package" , "changed_by" ) \
248255 .annotate (num_changes = Count ("records" ))
256+
257+
258+ def _entity_list_detail_link (el : EntityList ) -> SafeText :
259+ """
260+ A link to the detail page for an EntityList which includes its PK and length.
261+ """
262+ return model_detail_link (el , f"EntityList #{ el .pk } with { el .entitylistrow_set .count ()} row(s)" )
263+
264+
265+ class ContainerVersionInlineForContainer (admin .TabularInline ):
266+ """
267+ Inline admin view of ContainerVersions in a given Container
268+ """
269+ model = ContainerVersion
270+ ordering = ["-publishable_entity_version__version_num" ]
271+ fields = [
272+ "uuid" ,
273+ "version_num" ,
274+ "title" ,
275+ "children" ,
276+ "created" ,
277+ "created_by" ,
278+ ]
279+ readonly_fields = fields # type: ignore[assignment]
280+ extra = 0
281+
282+ def get_queryset (self , request ):
283+ return super ().get_queryset (request ).select_related (
284+ "publishable_entity_version"
285+ )
286+
287+ def children (self , obj : ContainerVersion ):
288+ return _entity_list_detail_link (obj .entity_list )
289+
290+
291+ @admin .register (Container )
292+ class ContainerAdmin (ReadOnlyModelAdmin ):
293+ """
294+ Django admin configuration for Container
295+ """
296+ list_display = ("key" , "uuid" , "created" , "draft" , "published" , "see_also" )
297+ fields = [
298+ "publishable_entity" ,
299+ "learning_package" ,
300+ "draft" ,
301+ "published" ,
302+ "created" ,
303+ "created_by" ,
304+ "see_also" ,
305+ "most_recent_parent_entity_list" ,
306+ ]
307+ readonly_fields = fields # type: ignore[assignment]
308+ search_fields = ["publishable_entity__uuid" , "publishable_entity__key" ]
309+ inlines = [ContainerVersionInlineForContainer ]
310+
311+ def uuid (self , obj : Container ) -> SafeText :
312+ return model_detail_link (obj , obj .uuid )
313+
314+ def learning_package (self , obj : Container ) -> SafeText :
315+ return model_detail_link (
316+ obj .publishable_entity .learning_package ,
317+ obj .publishable_entity .learning_package .key ,
318+ )
319+
320+ def get_queryset (self , request ):
321+ return super ().get_queryset (request ).select_related (
322+ "publishable_entity" ,
323+ "publishable_entity__learning_package" ,
324+ "publishable_entity__published__version" ,
325+ "publishable_entity__draft__version" ,
326+ )
327+
328+ def draft (self , obj : Container ) -> SafeText :
329+ """
330+ Link to this Container's draft ContainerVersion
331+ """
332+ if draft := obj .versioning .draft :
333+ return format_html (
334+ "Version {} ({})" , draft .version_num , _entity_list_detail_link (draft .entity_list )
335+ )
336+ return SafeText ("-" )
337+
338+ def published (self , obj : Container ) -> SafeText :
339+ """
340+ Link to this Container's published ContainerVersion
341+ """
342+ if published := obj .versioning .published :
343+ return format_html (
344+ "Version {} ({})" , published .version_num , _entity_list_detail_link (published .entity_list )
345+ )
346+ return SafeText ("-" )
347+
348+ def see_also (self , obj : Container ):
349+ return one_to_one_related_model_html (obj )
350+
351+ def most_recent_parent_entity_list (self , obj : Container ) -> SafeText :
352+ if latest_row := EntityListRow .objects .filter (entity_id = obj .publishable_entity_id ).order_by ("-pk" ).first ():
353+ return _entity_list_detail_link (latest_row .entity_list )
354+ return SafeText ("-" )
355+
356+
357+ class ContainerVersionInlineForEntityList (admin .TabularInline ):
358+ """
359+ Inline admin view of ContainerVersions which use a given EntityList
360+ """
361+ model = ContainerVersion
362+ verbose_name = "Container Version that references this Entity List"
363+ verbose_name_plural = "Container Versions that reference this Entity List"
364+ ordering = ["-pk" ] # Newest first
365+ fields = [
366+ "uuid" ,
367+ "version_num" ,
368+ "container_key" ,
369+ "title" ,
370+ "created" ,
371+ "created_by" ,
372+ ]
373+ readonly_fields = fields # type: ignore[assignment]
374+ extra = 0
375+
376+ def get_queryset (self , request ):
377+ return super ().get_queryset (request ).select_related (
378+ "container" ,
379+ "container__publishable_entity" ,
380+ "publishable_entity_version" ,
381+ )
382+
383+ def container_key (self , obj : ContainerVersion ) -> SafeText :
384+ return model_detail_link (obj .container , obj .container .key )
385+
386+
387+ class EntityListRowInline (admin .TabularInline ):
388+ """
389+ Table of entity rows in the entitylist admin
390+ """
391+ model = EntityListRow
392+ readonly_fields = [
393+ "order_num" ,
394+ "pinned_version_num" ,
395+ "entity_models" ,
396+ "container_models" ,
397+ "container_children" ,
398+ ]
399+ fields = readonly_fields # type: ignore[assignment]
400+
401+ def get_queryset (self , request ):
402+ return super ().get_queryset (request ).select_related (
403+ "entity" ,
404+ "entity_version" ,
405+ )
406+
407+ def pinned_version_num (self , obj : EntityListRow ):
408+ return str (obj .entity_version .version_num ) if obj .entity_version else "(Unpinned)"
409+
410+ def entity_models (self , obj : EntityListRow ):
411+ return format_html (
412+ "{}<ul>{}</ul>" ,
413+ model_detail_link (obj .entity , obj .entity .key ),
414+ one_to_one_related_model_html (obj .entity ),
415+ )
416+
417+ def container_models (self , obj : EntityListRow ) -> SafeText :
418+ if not hasattr (obj .entity , "container" ):
419+ return SafeText ("(Not a Container)" )
420+ return format_html (
421+ "{}<ul>{}</ul>" ,
422+ model_detail_link (obj .entity .container , str (obj .entity .container )),
423+ one_to_one_related_model_html (obj .entity .container ),
424+ )
425+
426+ def container_children (self , obj : EntityListRow ) -> SafeText :
427+ """
428+ If this row holds a Container, then link *its* EntityList, allowing easy hierarchy browsing.
429+
430+ When determining which ContainerVersion to grab the EntityList from, prefer the pinned
431+ version if there is one; otherwise use the Draft version.
432+ """
433+ if not hasattr (obj .entity , "container" ):
434+ return SafeText ("(Not a Container)" )
435+ child_container_version : ContainerVersion = (
436+ obj .entity_version .containerversion
437+ if obj .entity_version
438+ else obj .entity .container .versioning .draft
439+ )
440+ return _entity_list_detail_link (child_container_version .entity_list )
441+
442+
443+ @admin .register (EntityList )
444+ class EntityListAdmin (ReadOnlyModelAdmin ):
445+ """
446+ Django admin configuration for EntityList
447+ """
448+ list_display = [
449+ "entity_list" ,
450+ "row_count" ,
451+ "recent_container_version_num" ,
452+ "recent_container" ,
453+ "recent_container_package"
454+ ]
455+ inlines = [ContainerVersionInlineForEntityList , EntityListRowInline ]
456+
457+ def entity_list (self , obj : EntityList ) -> SafeText :
458+ return model_detail_link (obj , f"EntityList #{ obj .pk } " )
459+
460+ def row_count (self , obj : EntityList ) -> int :
461+ return obj .entitylistrow_set .count ()
462+
463+ def recent_container_version_num (self , obj : EntityList ) -> str :
464+ """
465+ Number of the newest ContainerVersion that references this EntityList
466+ """
467+ if latest := _latest_container_version (obj ):
468+ return f"Version { latest .version_num } "
469+ else :
470+ return "-"
471+
472+ def recent_container (self , obj : EntityList ) -> SafeText | None :
473+ """
474+ Link to the Container of the newest ContainerVersion that references this EntityList
475+ """
476+ if latest := _latest_container_version (obj ):
477+ return format_html ("of: {}" , model_detail_link (latest .container , latest .container .key ))
478+ else :
479+ return None
480+
481+ def recent_container_package (self , obj : EntityList ) -> SafeText | None :
482+ """
483+ Link to the LearningPackage of the newest ContainerVersion that references this EntityList
484+ """
485+ if latest := _latest_container_version (obj ):
486+ return format_html (
487+ "in: {}" ,
488+ model_detail_link (
489+ latest .container .publishable_entity .learning_package ,
490+ latest .container .publishable_entity .learning_package .key
491+ )
492+ )
493+ else :
494+ return None
495+
496+ # We'd like it to appear as if these three columns are just a single
497+ # nicely-formatted column, so only give the left one a description.
498+ recent_container_version_num .short_description = ( # type: ignore[attr-defined]
499+ "Most recent container version using this entity list"
500+ )
501+ recent_container .short_description = "" # type: ignore[attr-defined]
502+ recent_container_package .short_description = "" # type: ignore[attr-defined]
503+
504+
505+ @functools .cache
506+ def _latest_container_version (obj : EntityList ) -> ContainerVersion | None :
507+ """
508+ Any given EntityList can be used by multiple ContainerVersion (which may even
509+ span multiple Containers). We only have space here to show one ContainerVersion
510+ easily, so let's show the one that's most likely to be interesting to the Django
511+ admin user: the most-recently-created one.
512+ """
513+ return obj .container_versions .order_by ("-pk" ).first ()
0 commit comments