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
13+ from openedx_learning .lib .admin_utils import ReadOnlyModelAdmin , model_detail_link , one_to_one_related_model_html
1014
1115from .models import (
16+ Container ,
17+ ContainerVersion ,
1218 DraftChangeLog ,
1319 DraftChangeLogRecord ,
20+ EntityList ,
21+ EntityListRow ,
1422 LearningPackage ,
1523 PublishableEntity ,
1624 PublishLog ,
@@ -122,6 +130,12 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
122130 "can_stand_alone" ,
123131 ]
124132
133+ def draft_version (self , entity : PublishableEntity ):
134+ return entity .draft .version .version_num if entity .draft .version else None
135+
136+ def published_version (self , entity : PublishableEntity ):
137+ return entity .published .version .version_num if entity .published and entity .published .version else None
138+
125139 def get_queryset (self , request ):
126140 queryset = super ().get_queryset (request )
127141 return queryset .select_related (
@@ -131,16 +145,6 @@ def get_queryset(self, request):
131145 def see_also (self , entity ):
132146 return one_to_one_related_model_html (entity )
133147
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-
144148
145149@admin .register (Published )
146150class PublishedAdmin (ReadOnlyModelAdmin ):
@@ -229,7 +233,7 @@ class DraftChangeSetAdmin(ReadOnlyModelAdmin):
229233 """
230234 inlines = [DraftChangeLogRecordTabularInline ]
231235 fields = (
232- "uuid " ,
236+ "pk " ,
233237 "learning_package" ,
234238 "num_changes" ,
235239 "changed_at" ,
@@ -246,3 +250,264 @@ def get_queryset(self, request):
246250 queryset = super ().get_queryset (request )
247251 return queryset .select_related ("learning_package" , "changed_by" ) \
248252 .annotate (num_changes = Count ("records" ))
253+
254+
255+ def _entity_list_detail_link (el : EntityList ) -> SafeText :
256+ """
257+ A link to the detail page for an EntityList which includes its PK and length.
258+ """
259+ num_rows = el .entitylistrow_set .count ()
260+ rows_noun = "row" if num_rows == 1 else "rows"
261+ return model_detail_link (el , f"EntityList #{ el .pk } with { num_rows } { rows_noun } " )
262+
263+
264+ class ContainerVersionInlineForContainer (admin .TabularInline ):
265+ """
266+ Inline admin view of ContainerVersions in a given Container
267+ """
268+ model = ContainerVersion
269+ ordering = ["-publishable_entity_version__version_num" ]
270+ fields = [
271+ "pk" ,
272+ "version_num" ,
273+ "title" ,
274+ "children" ,
275+ "created" ,
276+ "created_by" ,
277+ ]
278+ readonly_fields = fields # type: ignore[assignment]
279+ extra = 0
280+
281+ def get_queryset (self , request ):
282+ return super ().get_queryset (request ).select_related (
283+ "publishable_entity_version"
284+ )
285+
286+ def children (self , obj : ContainerVersion ):
287+ return _entity_list_detail_link (obj .entity_list )
288+
289+
290+ @admin .register (Container )
291+ class ContainerAdmin (ReadOnlyModelAdmin ):
292+ """
293+ Django admin configuration for Container
294+ """
295+ list_display = ("key" , "created" , "draft" , "published" , "see_also" )
296+ fields = [
297+ "pk" ,
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 learning_package (self , obj : Container ) -> SafeText :
312+ return model_detail_link (
313+ obj .publishable_entity .learning_package ,
314+ obj .publishable_entity .learning_package .key ,
315+ )
316+
317+ def get_queryset (self , request ):
318+ return super ().get_queryset (request ).select_related (
319+ "publishable_entity" ,
320+ "publishable_entity__learning_package" ,
321+ "publishable_entity__published__version" ,
322+ "publishable_entity__draft__version" ,
323+ )
324+
325+ def draft (self , obj : Container ) -> str :
326+ """
327+ Link to this Container's draft ContainerVersion
328+ """
329+ if draft := obj .versioning .draft :
330+ return format_html (
331+ 'Version {} "{}" ({})' , draft .version_num , draft .title , _entity_list_detail_link (draft .entity_list )
332+ )
333+ return "-"
334+
335+ def published (self , obj : Container ) -> str :
336+ """
337+ Link to this Container's published ContainerVersion
338+ """
339+ if published := obj .versioning .published :
340+ return format_html (
341+ 'Version {} "{}" ({})' ,
342+ published .version_num ,
343+ published .title ,
344+ _entity_list_detail_link (published .entity_list ),
345+ )
346+ return "-"
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 ) -> str :
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 "-"
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+ "pk" ,
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