Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion openedx_learning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Open edX Learning ("Learning Core").
"""

__version__ = "0.26.0"
__version__ = "0.27.0"
289 changes: 277 additions & 12 deletions openedx_learning/apps/authoring/publishing/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,22 @@
"""
from __future__ import annotations

import functools

from django.contrib import admin
from django.db.models import Count
from django.utils.html import format_html
from django.utils.safestring import SafeText

from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, one_to_one_related_model_html
from openedx_learning.lib.admin_utils import ReadOnlyModelAdmin, model_detail_link, one_to_one_related_model_html

from .models import (
Container,
ContainerVersion,
DraftChangeLog,
DraftChangeLogRecord,
EntityList,
EntityListRow,
LearningPackage,
PublishableEntity,
PublishLog,
Expand Down Expand Up @@ -122,6 +130,12 @@ class PublishableEntityAdmin(ReadOnlyModelAdmin):
"can_stand_alone",
]

def draft_version(self, entity: PublishableEntity):
return entity.draft.version.version_num if entity.draft.version else None

def published_version(self, entity: PublishableEntity):
return entity.published.version.version_num if entity.published and entity.published.version else None

def get_queryset(self, request):
queryset = super().get_queryset(request)
return queryset.select_related(
Expand All @@ -131,16 +145,6 @@ def get_queryset(self, request):
def see_also(self, entity):
return one_to_one_related_model_html(entity)

def draft_version(self, entity):
if entity.draft.version:
return entity.draft.version.version_num
return None

def published_version(self, entity):
if entity.published.version:
return entity.published.version.version_num
return None


@admin.register(Published)
class PublishedAdmin(ReadOnlyModelAdmin):
Expand Down Expand Up @@ -229,7 +233,7 @@ class DraftChangeSetAdmin(ReadOnlyModelAdmin):
"""
inlines = [DraftChangeLogRecordTabularInline]
fields = (
"uuid",
"pk",
"learning_package",
"num_changes",
"changed_at",
Expand All @@ -246,3 +250,264 @@ def get_queryset(self, request):
queryset = super().get_queryset(request)
return queryset.select_related("learning_package", "changed_by") \
.annotate(num_changes=Count("records"))


def _entity_list_detail_link(el: EntityList) -> SafeText:
"""
A link to the detail page for an EntityList which includes its PK and length.
"""
num_rows = el.entitylistrow_set.count()
rows_noun = "row" if num_rows == 1 else "rows"
return model_detail_link(el, f"EntityList #{el.pk} with {num_rows} {rows_noun}")


class ContainerVersionInlineForContainer(admin.TabularInline):
"""
Inline admin view of ContainerVersions in a given Container
"""
model = ContainerVersion
ordering = ["-publishable_entity_version__version_num"]
fields = [
"pk",
"version_num",
"title",
"children",
"created",
"created_by",
]
readonly_fields = fields # type: ignore[assignment]
extra = 0

def get_queryset(self, request):
return super().get_queryset(request).select_related(
"publishable_entity_version"
)

def children(self, obj: ContainerVersion):
return _entity_list_detail_link(obj.entity_list)


@admin.register(Container)
class ContainerAdmin(ReadOnlyModelAdmin):
"""
Django admin configuration for Container
"""
list_display = ("key", "created", "draft", "published", "see_also")
fields = [
"pk",
"publishable_entity",
"learning_package",
"draft",
"published",
"created",
"created_by",
"see_also",
"most_recent_parent_entity_list",
]
readonly_fields = fields # type: ignore[assignment]
search_fields = ["publishable_entity__uuid", "publishable_entity__key"]
inlines = [ContainerVersionInlineForContainer]

def learning_package(self, obj: Container) -> SafeText:
return model_detail_link(
obj.publishable_entity.learning_package,
obj.publishable_entity.learning_package.key,
)

def get_queryset(self, request):
return super().get_queryset(request).select_related(
"publishable_entity",
"publishable_entity__learning_package",
"publishable_entity__published__version",
"publishable_entity__draft__version",
)

def draft(self, obj: Container) -> str:
"""
Link to this Container's draft ContainerVersion
"""
if draft := obj.versioning.draft:
return format_html(
'Version {} "{}" ({})', draft.version_num, draft.title, _entity_list_detail_link(draft.entity_list)
)
return "-"

def published(self, obj: Container) -> str:
"""
Link to this Container's published ContainerVersion
"""
if published := obj.versioning.published:
return format_html(
'Version {} "{}" ({})',
published.version_num,
published.title,
_entity_list_detail_link(published.entity_list),
)
return "-"

def see_also(self, obj: Container):
return one_to_one_related_model_html(obj)

def most_recent_parent_entity_list(self, obj: Container) -> str:
if latest_row := EntityListRow.objects.filter(entity_id=obj.publishable_entity_id).order_by("-pk").first():
return _entity_list_detail_link(latest_row.entity_list)
return "-"


class ContainerVersionInlineForEntityList(admin.TabularInline):
"""
Inline admin view of ContainerVersions which use a given EntityList
"""
model = ContainerVersion
verbose_name = "Container Version that references this Entity List"
verbose_name_plural = "Container Versions that reference this Entity List"
ordering = ["-pk"] # Newest first
fields = [
"pk",
"version_num",
"container_key",
"title",
"created",
"created_by",
]
readonly_fields = fields # type: ignore[assignment]
extra = 0

def get_queryset(self, request):
return super().get_queryset(request).select_related(
"container",
"container__publishable_entity",
"publishable_entity_version",
)

def container_key(self, obj: ContainerVersion) -> SafeText:
return model_detail_link(obj.container, obj.container.key)


class EntityListRowInline(admin.TabularInline):
"""
Table of entity rows in the entitylist admin
"""
model = EntityListRow
readonly_fields = [
"order_num",
"pinned_version_num",
"entity_models",
"container_models",
"container_children",
]
fields = readonly_fields # type: ignore[assignment]

def get_queryset(self, request):
return super().get_queryset(request).select_related(
"entity",
"entity_version",
)

def pinned_version_num(self, obj: EntityListRow):
return str(obj.entity_version.version_num) if obj.entity_version else "(Unpinned)"

def entity_models(self, obj: EntityListRow):
return format_html(
"{}<ul>{}</ul>",
model_detail_link(obj.entity, obj.entity.key),
one_to_one_related_model_html(obj.entity),
)

def container_models(self, obj: EntityListRow) -> SafeText:
if not hasattr(obj.entity, "container"):
return SafeText("(Not a Container)")
return format_html(
"{}<ul>{}</ul>",
model_detail_link(obj.entity.container, str(obj.entity.container)),
one_to_one_related_model_html(obj.entity.container),
)

def container_children(self, obj: EntityListRow) -> SafeText:
"""
If this row holds a Container, then link *its* EntityList, allowing easy hierarchy browsing.

When determining which ContainerVersion to grab the EntityList from, prefer the pinned
version if there is one; otherwise use the Draft version.
"""
if not hasattr(obj.entity, "container"):
return SafeText("(Not a Container)")
child_container_version: ContainerVersion = (
obj.entity_version.containerversion
if obj.entity_version
else obj.entity.container.versioning.draft
)
return _entity_list_detail_link(child_container_version.entity_list)


@admin.register(EntityList)
class EntityListAdmin(ReadOnlyModelAdmin):
"""
Django admin configuration for EntityList
"""
list_display = [
"entity_list",
"row_count",
"recent_container_version_num",
"recent_container",
"recent_container_package"
]
inlines = [ContainerVersionInlineForEntityList, EntityListRowInline]

def entity_list(self, obj: EntityList) -> SafeText:
return model_detail_link(obj, f"EntityList #{obj.pk}")

def row_count(self, obj: EntityList) -> int:
return obj.entitylistrow_set.count()

def recent_container_version_num(self, obj: EntityList) -> str:
"""
Number of the newest ContainerVersion that references this EntityList
"""
if latest := _latest_container_version(obj):
return f"Version {latest.version_num}"
else:
return "-"

def recent_container(self, obj: EntityList) -> SafeText | None:
"""
Link to the Container of the newest ContainerVersion that references this EntityList
"""
if latest := _latest_container_version(obj):
return format_html("of: {}", model_detail_link(latest.container, latest.container.key))
else:
return None

def recent_container_package(self, obj: EntityList) -> SafeText | None:
"""
Link to the LearningPackage of the newest ContainerVersion that references this EntityList
"""
if latest := _latest_container_version(obj):
return format_html(
"in: {}",
model_detail_link(
latest.container.publishable_entity.learning_package,
latest.container.publishable_entity.learning_package.key
)
)
else:
return None

# We'd like it to appear as if these three columns are just a single
# nicely-formatted column, so only give the left one a description.
recent_container_version_num.short_description = ( # type: ignore[attr-defined]
"Most recent container version using this entity list"
)
recent_container.short_description = "" # type: ignore[attr-defined]
recent_container_package.short_description = "" # type: ignore[attr-defined]


@functools.cache
def _latest_container_version(obj: EntityList) -> ContainerVersion | None:
"""
Any given EntityList can be used by multiple ContainerVersion (which may even
span multiple Containers). We only have space here to show one ContainerVersion
easily, so let's show the one that's most likely to be interesting to the Django
admin user: the most-recently-created one.
"""
return obj.container_versions.order_by("-pk").first()
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,9 @@ class PublishableEntityVersion(models.Model):
blank=True,
)

def __str__(self):
return f"{self.entity.key} @ v{self.version_num} - {self.title}"

class Meta:
constraints = [
# Prevent the situation where we have multiple
Expand Down Expand Up @@ -303,6 +306,9 @@ def created(self) -> datetime:
def created_by(self):
return self.publishable_entity.created_by

def __str__(self) -> str:
return str(self.publishable_entity)

class Meta:
abstract = True

Expand Down Expand Up @@ -570,10 +576,17 @@ def title(self) -> str:
def created(self) -> datetime:
return self.publishable_entity_version.created

@property
def created_by(self):
return self.publishable_entity_version.created_by

@property
def version_num(self) -> int:
return self.publishable_entity_version.version_num

def __str__(self) -> str:
return str(self.publishable_entity_version)

class Meta:
abstract = True

Expand Down
Loading