text: """
  Courses application admin
  """
  from itertools import chain
  
  from django import forms
  from django.conf import settings
  from django.contrib import admin
  from django.core.exceptions import PermissionDenied, ValidationError
  from django.db import models as django_models
  from django.db import transaction
  from django.http import HttpResponseBadRequest, HttpResponseForbidden, JsonResponse
  from django.urls import re_path
  from django.utils.decorators import method_decorator
  from django.utils.encoding import force_str
  from django.utils.translation import gettext_lazy as _
  from django.views.decorators.http import require_POST
  
  from cms.admin.placeholderadmin import FrontendEditableAdminMixin
  from cms.api import Page
  from cms.extensions import PageExtensionAdmin
  from cms.utils import page_permissions
  from parler.admin import TranslatableAdmin
  from parler.forms import TranslatableModelForm
  
  from ..core.admin import link_field
  from . import models
  from .fields import CourseRunSplitDateTimeField
  from .forms import AdminCategoryForm, AdminLicenceForm
  from .helpers import snapshot_course
  from .widgets import CourseRunSplitDateTimeWidget
  
  REQUIRE_POST = method_decorator(require_POST)
  
  
  class CategoryAdmin(FrontendEditableAdminMixin, PageExtensionAdmin):
      """Admin class for the Category model"""
  
      form = AdminCategoryForm
  
  
  class CourseRunAdminForm(TranslatableModelForm):
      """Admin form used for frontend editing."""
  
      class Meta:
          model = models.CourseRun
          fields = [
              "direct_course",
              "title",
              "resource_link",
              "start",
              "end",
              "enrollment_start",
              "enrollment_end",
              "languages",
          ]
  
      def __init__(self, *args, **kwargs):
          """
          If the form is instanciated to update an existing course run:
              > show the direct course select box only if the course has one or more snapshots
                and limit choices to either the master course or one of its snapshots
  
          If the form is instanciated to create a new course run and the "Add" form is opened via
          the widget from the course detail page on the frontend, the "direct_course" field receives
          the "id" of the related course as initial value (e.g. happens when you pass it via the
          querystring in Django admin):
              > hide the direct course select box
          """
          super().__init__(*args, **kwargs)
  
          if "direct_course" not in self.fields:
              return
  
          if self.instance.pk:
              course_query = (
                  self.instance.get_course()
                  .get_snapshots(include_self=True)
                  .filter(extended_object__publisher_is_draft=True)
                  .distinct()
              )
          else:
              course_query = models.Course.objects.filter(
                  extended_object__publisher_is_draft=True
              )
  
              # pylint: disable=no-member
              user = self.request.user
              if getattr(settings, "CMS_PERMISSION", False) and not user.is_superuser:
                  course_query = models.Course.objects.filter(
                      django_models.Q(extended_object__pagepermission__user=user)
                      | django_models.Q(
                          extended_object__pagepermission__group__user=user
                      ),
                      extended_object__pagepermission__can_change=True,
                      extended_object__publisher_is_draft=True,
                  )
  
              if kwargs.get("initial", {}).get("direct_course"):
                  self.fields["direct_course"].widget = forms.HiddenInput()
  
          self.fields["direct_course"].choices = chain(
              [("", self.fields["direct_course"].empty_label)],
              [(c.pk, c.extended_object.get_title()) for c in course_query],
          )
  
          if len(course_query) < 2:
              self.fields["direct_course"].widget = forms.HiddenInput()
  
      def clean_direct_course(self):
          """Ensure that the user has the required permissions to change the related course page."""
          course = self.cleaned_data["direct_course"]
          if (
              course
              and getattr(settings, "CMS_PERMISSION", False)
              and not page_permissions.user_can_change_page(
                  self.request.user,  # pylint: disable=no-member
                  course.extended_object,
                  course.extended_object.node.site,
              )
          ):
              raise ValidationError(
                  "You do not have permission to change this course page."
              )
  
          return course
  
  
  class CourseRunAdmin(FrontendEditableAdminMixin, TranslatableAdmin):
      """Admin class for the CourseRun model"""
  
      frontend_editable_fields = (
          "direct_course",
          "title",
          "resource_link",
          "start",
          "end",
          "enrollment_start",
          "enrollment_end",
          "languages",
      )
      list_display = ["id"]
      form = CourseRunAdminForm
      formfield_overrides = {
          django_models.DateTimeField: {
              "form_class": CourseRunSplitDateTimeField,
              "widget": CourseRunSplitDateTimeWidget,
          }
      }
  
      def has_module_permission(self, request):
          """
          Hide course runs from the admin page as frontend editing provides a better experience.
          """
          return False
  
      def get_form(self, request, obj=None, **kwargs):
          """Add request to form so we can validate fields according to the user."""
          form = super().get_form(request, obj=obj, **kwargs)
          form.request = request
          return form
  
      def has_view_permission(self, request, obj=None):
          """Allow view only if the user is allowed to change the related course page."""
          if obj:
              course_page = obj.direct_course.extended_object
              if not course_page.publisher_is_draft:
                  return False
              if getattr(
                  settings, "CMS_PERMISSION", False
              ) and not page_permissions.user_can_change_page(
                  request.user, course_page, course_page.node.site
              ):
                  return False
          return super().has_view_permission(request, obj=obj)
  
      def has_change_permission(self, request, obj=None):
          """Allow change only if the user is allowed to change the related course page."""
          if obj:
              course_page = obj.direct_course.extended_object
              if not course_page.publisher_is_draft:
                  return False
              if getattr(
                  settings, "CMS_PERMISSION", False
              ) and not page_permissions.user_can_change_page(
                  request.user, course_page, course_page.node.site
              ):
                  return False
          return super().has_change_permission(request, obj=obj)
  
      def has_delete_permission(self, request, obj=None):
          """Allow change only if the user is allowed to change the related course page."""
          if obj and getattr(settings, "CMS_PERMISSION", False):
              course_page = obj.direct_course.extended_object
              if not page_permissions.user_can_change_page(
                  request.user, course_page, course_page.node.site
              ):
                  return False
          return super().has_delete_permission(request, obj=obj)
  
      # pylint: disable=no-member
      def save(self, commit=True):
          """
          Trigger check that will mark the related course page dirty if its content has changed
          since it was last saved.
          """
          super().save(commit=commit)
          if commit is True:
              self.instance.mark_course_dirty()
          return self.instance
  
  
  class CourseAdmin(FrontendEditableAdminMixin, PageExtensionAdmin):
      """Admin class for the Course model"""
  
      list_display = ["title", "is_listed"]
      frontend_editable_fields = ("code", "duration", "effort", "is_listed")
  
      # pylint: disable=no-self-use
      def title(self, obj):
          """
          Display the course title as a read-only field from the related page
          """
          return obj.extended_object.get_title()
  
      def get_urls(self):
          """
          Add admin URL to trigger the snapshot of a course.
          """
          url_patterns = super().get_urls()
          return [
              re_path(
                  r"^(?P<course_id>[0-9]+)/snapshot/$",
                  self.admin_site.admin_view(self.snapshot),
                  name="cms_course_snapshot",
              )
          ] + url_patterns
  
      # pylint: disable=unused-argument
      @REQUIRE_POST
      @transaction.atomic
      def snapshot(self, request, course_id, *args, **kwargs):
          """
          Snapshotting a course is making a copy of the course page with all its permissions and
          extensions, and placing the copy as the first child of the page being snapshotted, then
          moving all the course run of the page being snapshotted as children of the new snapshot
          so we keep record of the course as it was when these course runs were played.
          """
          try:
              page = Page.objects.select_related("node__site").get(
                  publisher_is_draft=True, course__id=course_id
              )
          except Page.DoesNotExist:
              return HttpResponseBadRequest(force_str(_("Course could not be found.")))
  
          try:
              new_page = snapshot_course(page, request.user, simulate_only=False)
          except PermissionDenied as context:
              return HttpResponseForbidden(force_str(context))
  
          return JsonResponse({"id": new_page.course.id})
  
  
  class OrganizationAdmin(PageExtensionAdmin):
      """
      Admin class for the Organization model
      """
  
      list_display = ["title", "code"]
  
      # pylint: disable=no-self-use
      def title(self, obj):
          """
          Get the page title from the related page
          """
          return obj.extended_object.get_title()
  
  
  class PageRoleAdmin(admin.ModelAdmin):
      """
      Admin class for the PageRole model.
      """
  
      fields = [
          "role",
          link_field("page"),
          link_field("group", anchor=_("See user group")),
          link_field("folder", anchor=_("See filer folder")),
      ]
      list_display = ["id", "role", "page"]
      list_filter = ["role"]
      search_fields = ("id", "page__title_set__title")
  
      def get_fieldsets(self, request, obj=None):
          fieldsets = super().get_fieldsets(request, obj)
          if not obj:
              fieldsets[0][1]["fields"] = ["role", "page"]
          return fieldsets
  
      def get_readonly_fields(self, request, obj=None):
          """Don't allow modifying the object once created."""
          if obj:
              return self.fields
          return []
  
  
  class PersonAdmin(PageExtensionAdmin):
      """Admin class for the Person model"""
  
      list_display = ["title"]
  
      # pylint: disable=no-self-use
      def title(self, obj):
          """
          Display the person page title as a read-only field from the related page
          """
          return obj.extended_object.get_title()
  
  
  class LicenceAdmin(TranslatableAdmin):
      """
      Admin class for the Licence model
      """
  
      list_display = ["name"]
      form = AdminLicenceForm
  
  
  admin.site.register(models.Category, CategoryAdmin)
  admin.site.register(models.Course, CourseAdmin)
  admin.site.register(models.CourseRun, CourseRunAdmin)
  admin.site.register(models.Licence, LicenceAdmin)
  admin.site.register(models.Organization, OrganizationAdmin)
  admin.site.register(models.PageRole, PageRoleAdmin)
  admin.site.register(models.Person, PersonAdmin)
  