Skip to content
7 changes: 4 additions & 3 deletions .importlinter
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ root_packages =
openedx_learning
openedx_content
openedx_tagging
openedx_catalog
openedx_django_lib
openedx_core
contract_types =
Expand All @@ -21,9 +22,9 @@ contract_types =
name = "top-level source folders are layered correctly"
type = layers
layers =
# Learning-domain features (currently CBE; Learning Pathways to follow).
# May build on content, catalog and tagging. Nothing below may import it:
# in particular, openedx_tagging must never know that CBE exists.
# Learning-domain features (CBE and Pathways). May build on content, catalog and
# tagging. Nothing below may import it: in particular, openedx_tagging must never
# know that CBE exists.
openedx_learning

# Content: authoring-side models and APIs. May reference catalog models
Expand Down
90 changes: 89 additions & 1 deletion src/openedx_catalog/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _

from .models import CatalogCourse, CourseRun
from .models import CatalogCourse, CatalogPathway, CourseRun, PathwayCategory, PathwayEnrollment

if TYPE_CHECKING:

class CatalogCourseWithRunCount(CatalogCourse):
run_count: int

class PathwayCategoryWithPathwayCount(PathwayCategory):
pathway_count: int


class CatalogCourseAdmin(admin.ModelAdmin):
"""
Expand Down Expand Up @@ -110,3 +113,88 @@ def warnings(self, obj: CourseRun) -> str | None:


admin.site.register(CourseRun, CourseRunAdmin)


@admin.register(PathwayCategory)
class PathwayCategoryAdmin(admin.ModelAdmin):
"""
The PathwayCategory model admin.

Renaming a category changes what learners see. It does not change the authoring-side terminology, which is always
"Pathway".
"""

list_display = ["title", "title_plural_display", "category_code", "pathways_summary"]
search_fields = ["title", "title_plural", "category_code"]

def get_readonly_fields(self, request, obj: PathwayCategory | None = None) -> tuple[str, ...]:
if obj: # editing an existing object; the code is what other systems key off
return ("category_code",)
return tuple()

def get_queryset(self, request) -> QuerySet[PathwayCategoryWithPathwayCount]:
"""Add the 'pathway_count' to the list_display queryset"""
qs = super().get_queryset(request)
qs = qs.annotate(pathway_count=Count("pathways"))
return qs

@admin.display(description=_("Plural"))
def title_plural_display(self, obj: PathwayCategory) -> str:
"""Show the plural learners see, whether set explicitly or derived from the title."""
return obj.get_title_plural()

@admin.display(description=_("Pathways"), ordering="pathway_count")
def pathways_summary(self, obj: PathwayCategoryWithPathwayCount) -> str:
"""Link to the catalog pathways using this category"""
if obj.pathway_count == 0:
return "-"
url = reverse("admin:openedx_catalog_catalogpathway_changelist") + f"?category={obj.pk}"
return format_html('<a href="{}">{}</a>', url, obj.pathway_count)


@admin.register(CatalogPathway)
class CatalogPathwayAdmin(admin.ModelAdmin):
"""
The CatalogPathway model admin.

This edits only the catalog half of a Pathway. The Items a learner must complete live on the content side, in the
openedx_learning app, and are versioned there.
"""

list_filter = ["org__short_name", "category"]
list_display = ["title", "category", "org_display", "pathway_code", "key_str", "created_date", "modified"]
search_fields = ["title", "pathway_code"]

def get_readonly_fields(self, request, obj: CatalogPathway | None = None) -> tuple[str, ...]:
if obj: # editing an existing object
return ("org", "pathway_code")
return tuple()

@admin.display(description="Organization", ordering="org__short_name")
def org_display(self, obj: CatalogPathway) -> str:
"""Display the organization, only showing the short_name if different from full name"""
if obj.org.name == obj.org.short_name:
return obj.org.short_name
return str(obj.org)

@admin.display(description=_("Created"), ordering="created")
def created_date(self, obj: CatalogPathway) -> datetime.date:
"""Display the created date without the timestamp"""
return obj.created.date()


@admin.register(PathwayEnrollment)
class PathwayEnrollmentAdmin(admin.ModelAdmin):
"""
The PathwayEnrollment model admin.
"""

list_display = ["user", "catalog_pathway", "is_active", "created_date", "modified"]
list_filter = ["is_active", "catalog_pathway__category"]
# There may be very many users and a fair number of pathways, so don't use <select>
raw_id_fields = ["user", "catalog_pathway"]

@admin.display(description=_("Enrolled"), ordering="created")
def created_date(self, obj: PathwayEnrollment) -> datetime.date:
"""Display the enrollment date without the timestamp"""
return obj.created.date()
3 changes: 2 additions & 1 deletion src/openedx_catalog/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

# Import only the public API methods denoted with __all__
# pylint: disable=wildcard-import
from .api_impl import *
from .api_impl.courses import *
from .api_impl.pathways import *

# You'll also want the models from .models_api
3 changes: 3 additions & 0 deletions src/openedx_catalog/api_impl/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""
Implementation of the `openedx_catalog` API. Import it from `openedx_catalog.api` instead.
"""
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Implementation of the `openedx_catalog` API.
Implementation of the course part of the `openedx_catalog` API: catalog courses and course runs.
"""

import logging
Expand All @@ -9,7 +9,7 @@
from organizations.api import ensure_organization # type: ignore[import]
from organizations.api import exceptions as org_exceptions

from .models import CatalogCourse, CourseRun
from ..models import CatalogCourse, CourseRun

log = logging.getLogger(__name__)

Expand Down
Loading
Loading