Merged in hotfix/MS-932-WagtailCustomization (pull request #147)

Hotfix/MS-932 WagtailCustomization
This commit is contained in:
Lorenz Padberg 2024-04-15 13:04:27 +00:00 committed by Ramon Wenger
commit 000192ba36
11 changed files with 200 additions and 161 deletions

View File

@ -1,6 +1,6 @@
from django.db import models from django.db import models
from django.utils.text import slugify from django.utils.text import slugify
from wagtail.admin.panels import FieldPanel from wagtail.admin.panels import FieldPanel, TitleFieldPanel
from wagtail.fields import RichTextField, StreamField from wagtail.fields import RichTextField, StreamField
from wagtail.images.blocks import ImageChooserBlock from wagtail.images.blocks import ImageChooserBlock
@ -110,7 +110,7 @@ class BasicKnowledge(StrictHierarchyPage):
) )
content_panels = [ content_panels = [
FieldPanel("title", classname="full title"), TitleFieldPanel("title", classname="full title"),
FieldPanel("new_type"), FieldPanel("new_type"),
FieldPanel("intro"), FieldPanel("intro"),
FieldPanel("contents"), FieldPanel("contents"),

View File

@ -1,6 +1,6 @@
import logging import logging
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList from wagtail.admin.panels import TabbedInterface, ObjectList, TitleFieldPanel
from core.wagtail_utils import StrictHierarchyPage, get_default_settings from core.wagtail_utils import StrictHierarchyPage, get_default_settings
@ -13,7 +13,7 @@ class Book(StrictHierarchyPage):
verbose_name_plural = 'Bücher' verbose_name_plural = 'Bücher'
content_panels = [ content_panels = [
FieldPanel('title', classname="full title") TitleFieldPanel('title', classname="full title")
] ]
edit_handler = TabbedInterface([ edit_handler = TabbedInterface([

View File

@ -1,7 +1,7 @@
import logging import logging
from django.db import models from django.db import models
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList, TitleFieldPanel
from core.wagtail_utils import StrictHierarchyPage, get_default_settings from core.wagtail_utils import StrictHierarchyPage, get_default_settings
from users.models import SchoolClass from users.models import SchoolClass
@ -18,7 +18,7 @@ class Chapter(StrictHierarchyPage, GraphqlNodeMixin):
description = models.TextField(blank=True) description = models.TextField(blank=True)
content_panels = [ content_panels = [
FieldPanel("title", classname="full title"), TitleFieldPanel("title", classname="full title"),
FieldPanel("description", classname="full description"), FieldPanel("description", classname="full description"),
] ]

View File

@ -3,6 +3,7 @@ from wagtail.admin.panels import (
FieldPanel, FieldPanel,
TabbedInterface, TabbedInterface,
ObjectList, ObjectList,
TitleFieldPanel,
) )
from wagtail.blocks import StreamBlock from wagtail.blocks import StreamBlock
from wagtail.fields import StreamField from wagtail.fields import StreamField
@ -140,7 +141,7 @@ class ContentBlock(StrictHierarchyPage, GraphqlNodeMixin):
type = models.CharField(max_length=100, choices=TYPE_CHOICES, default=NORMAL) type = models.CharField(max_length=100, choices=TYPE_CHOICES, default=NORMAL)
content_panels = [ content_panels = [
FieldPanel("title", classname="full title"), TitleFieldPanel("title", classname="full title"),
FieldPanel("type"), FieldPanel("type"),
FieldPanel("contents"), FieldPanel("contents"),
] ]

View File

@ -1,14 +1,16 @@
from django import forms from core.constants import DEFAULT_RICH_TEXT_FEATURES
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from wagtail.admin.forms import WagtailAdminPageForm
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList
from wagtail.fields import RichTextField
from core.constants import DEFAULT_RICH_TEXT_FEATURES
from core.wagtail_utils import StrictHierarchyPage, get_default_settings
from users.models import SchoolClass from users.models import SchoolClass
from wagtail.admin.panels import (
FieldPanel,
ObjectList,
TabbedInterface,
TitleFieldPanel,
)
from wagtail.fields import RichTextField
EXACT = "exact" EXACT = "exact"
@ -51,20 +53,23 @@ class ModuleCategory(models.Model):
return f"{self.name}" return f"{self.name}"
class ModulePageForm(WagtailAdminPageForm): # Commented out since that check is not necessary if a slug is chosen that is already in use
def clean(self): # a new one will be generated
cleaned_data = super().clean() # TODO: remove after pullrequest is merged
if "slug" in self.cleaned_data: # class ModulePageForm(WagtailAdminPageForm):
page_slug = cleaned_data["slug"] # def clean(self):
if not Module._slug_is_available(page_slug, self.instance): # cleaned_data = super().clean()
self.add_error( # if "slug" in self.cleaned_data and "id" in self.cleaned_data:
"slug", # page_slug = cleaned_data["slug"]
forms.ValidationError( # if not Module._slug_is_available(page_slug, self.parent_page, self.instance):
_("The slug '%(page_slug)s' is already in use") # self.add_error(
% {"page_slug": page_slug} # "slug",
), # forms.ValidationError(
) # _("The slug '%(page_slug)s' is already in use")
return cleaned_data # % {"page_slug": page_slug}
# ),
# )
# return cleaned_data
class Module(StrictHierarchyPage): class Module(StrictHierarchyPage):
@ -97,7 +102,7 @@ class Module(StrictHierarchyPage):
solutions_enabled_for = models.ManyToManyField(SchoolClass) solutions_enabled_for = models.ManyToManyField(SchoolClass)
content_panels = [ content_panels = [
FieldPanel("title", classname="full title"), TitleFieldPanel("title", classname="full title"),
FieldPanel("meta_title", classname="full title"), FieldPanel("meta_title", classname="full title"),
FieldPanel("level"), FieldPanel("level"),
FieldPanel("category"), FieldPanel("category"),
@ -106,7 +111,8 @@ class Module(StrictHierarchyPage):
FieldPanel("teaser"), FieldPanel("teaser"),
FieldPanel("intro"), FieldPanel("intro"),
] ]
base_form_class = ModulePageForm # TODO remove after pullrequest is merged
# base_form_class = ModulePageForm
edit_handler = TabbedInterface( edit_handler = TabbedInterface(
[ObjectList(content_panels, heading="Content"), get_default_settings()] [ObjectList(content_panels, heading="Content"), get_default_settings()]
@ -181,11 +187,44 @@ class Module(StrictHierarchyPage):
return f"{self.meta_title} - {self.title}" return f"{self.meta_title} - {self.title}"
@staticmethod @staticmethod
def _slug_is_available(slug, page): def _slug_is_available(slug, parent_page, page=None):
# modeled after `Page._slug_is_available` """
modules = Module.objects.filter(slug=slug).not_page(page)
return not modules.exists() # modeled after `Page._slug_is_available`
Determine whether the given slug is available for use on a child page of
parent_page. If 'page' is passed, the slug is intended for use on that page
(and so it will be excluded from the duplicate check).
"""
if parent_page is None:
# the root page's slug can be whatever it likes...
return True
modules = Module.objects.all()
if page:
modules = modules.not_page(page)
return not modules.filter(slug=slug).exists()
def _get_autogenerated_slug(self, base_slug):
# modeled after `Page._get_autogenerated_slug`
candidate_slug = base_slug
suffix = 1
parent_page = self.get_parent()
while not self._slug_is_available(candidate_slug, parent_page, self):
# try with incrementing suffix until we find a slug which is available
suffix += 1
candidate_slug = "%s-%d" % (base_slug, suffix)
return candidate_slug
def full_clean(self, *args, **kwargs):
super().full_clean(*args, **kwargs)
# Always create a slug if it is not available
# todo: do we really want to do this? this will silently change a slug if the users sets one that already exists, which probably isn't what they expect
if not self._slug_is_available(self.slug, self.get_parent(), self):
self.slug = self._get_autogenerated_slug(self.slug)
class RecentModule(models.Model): class RecentModule(models.Model):

View File

@ -1,7 +1,7 @@
import logging import logging
from django.db import models from django.db import models
from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList from wagtail.admin.panels import FieldPanel, TabbedInterface, ObjectList, TitleFieldPanel
from wagtail.fields import RichTextField from wagtail.fields import RichTextField
from core.constants import DEFAULT_RICH_TEXT_FEATURES from core.constants import DEFAULT_RICH_TEXT_FEATURES
@ -22,7 +22,7 @@ class Topic(StrictHierarchyPage):
instructions = models.CharField(max_length=255, blank=True, null=True, default=None) instructions = models.CharField(max_length=255, blank=True, null=True, default=None)
content_panels = [ content_panels = [
FieldPanel('title', classname="full title"), TitleFieldPanel('title', classname="full title"),
FieldPanel('order'), FieldPanel('order'),
FieldPanel('teaser'), FieldPanel('teaser'),
FieldPanel('vimeo_id'), FieldPanel('vimeo_id'),

View File

@ -0,0 +1,30 @@
from books.factories import ModuleFactory
from books.models import Chapter
from core.tests.base_test import SkillboxTestCase
from django.test.client import Client
class TestChapterCreation(SkillboxTestCase):
"""Test created for Issue MS-932"""
def setUp(self) -> None:
self.createDefault()
self.module = ModuleFactory(slug="my-module")
self.Client = Client()
self.client.login(username="admin", password="test")
def test_create_chapter_creates_slug_automatically(self):
new_chapter = Chapter(title="New Chapter")
self.module.add_child(instance=new_chapter)
new_chapter.save()
self.assertEqual("new-chapter", new_chapter.slug)
def test_create_chapter_creates_slug_automatically_if_existing(self):
new_chapter = Chapter(title="New Chapter")
self.module.add_child(instance=new_chapter)
new_chapter.save()
self.assertEqual("new-chapter", new_chapter.slug)
new_chapter2 = Chapter(title="New Chapter")
self.module.add_child(instance=new_chapter2)
new_chapter2.save()
self.assertEqual("new-chapter-2", new_chapter2.slug)

View File

@ -0,0 +1,32 @@
from django.test import TestCase, RequestFactory
from unittest import skip
from graphene.test import Client
from graphql_relay import to_global_id
from api.schema import schema
from api.utils import get_object
from books.models import ContentBlock, Chapter
from books.factories import ModuleFactory, ModuleLevelFactory, TopicFactory
from core.factories import UserFactory
from users.models import User
class TestModuleCreation(TestCase):
"""
Since the modules url in the frontend is not /topic/module but /module the slug has to be unique.
This test checks if the slug is generated correctly.
"""
def test_create_new_module_generates_slug(self):
topic = TopicFactory(title="Berufslehre")
module = ModuleFactory(title="Modul 1", parent=topic)
self.assertEqual("modul-1", module.slug)
def test_create_new_module_different_topic(self):
topic = TopicFactory(title="Berufslehre")
module = ModuleFactory(title="Modul 1", parent=topic)
topic2 = TopicFactory(title="Geld und Macht")
module2 = ModuleFactory(title="Modul 1", parent=topic2)
self.assertEqual("modul-1", module.slug)
self.assertEqual("modul-1-2", module2.slug, )

View File

@ -1,124 +0,0 @@
{# This template is overwritten to create a custom cms ui for the model "chapter" to improve navigation experience.#}
{# See MS-538#}
{% load i18n %}
{% load l10n %}
{% load wagtailadmin_tags %}
<table class="listing {% if full_width %}full-width{% endif %} {% block table_classname %}{% endblock %}">
{% if show_ordering_column or show_bulk_actions %}
<col width="10px"/>
{% endif %}
<col/>
{% if show_parent %}
<col/>
{% endif %}
<col width="12%"/>
<col width="12%"/>
<col width="12%"/>
<col width="10%"/>
<thead>
{% block pre_parent_page_headers %}
{% endblock %}
{% if parent_page %}
{% page_permissions parent_page as parent_page_perms %}
<tr class="index {% if not parent_page.live %} unpublished{% endif %}
{% block parent_page_row_classname %}{% endblock %}">
<td class="title"{% if show_ordering_column or show_bulk_actions %} colspan="2"{% endif %}>
{% block parent_page_title %}
{% endblock %}
</td>
<td class="updated" valign="bottom">{% if parent_page.latest_revision_created_at %}
<div class="human-readable-date"
title="{{ parent_page.latest_revision_created_at|date:"DATETIME_FORMAT" }}">
{% blocktrans with time_period=parent_page.latest_revision_created_at|timesince %}{{ time_period }}
ago{% endblocktrans %}</div>{% endif %}</td>
<td class="type" valign="bottom">
{% if not parent_page.is_root %}
{{ parent_page.content_type.model_class.get_verbose_name }}
{% endif %}
</td>
<td class="status" valign="bottom">
{% if not parent_page.is_root %}
{% include "wagtailadmin/shared/page_status_tag.html" with page=parent_page %}
{% endif %}
</td>
<td></td>
</tr>
{% endif %}
{% block post_parent_page_headers %}
{% endblock %}
</thead>
<tbody>
{% if pages %}
{% trans "Select page" as checkbox_aria_label %}
{% for page in pages %}
{% page_permissions page as page_perms %}
<tr {% if ordering == "ord" %}id="page_{{ page.id|unlocalize }}"
data-page-title="{{ page.get_admin_display_title }}"{% endif %}
class="{% if not page.live %}unpublished{% endif %} {% block page_row_classname %}{% endblock %}">
{% if show_ordering_column %}
<td class="ord">{% if orderable and ordering == "ord" %}
<div class="handle icon icon-grip text-replace">{% trans 'Drag' %}</div>{% endif %}</td>
{% elif show_bulk_actions %}
{% include "wagtailadmin/bulk_actions/listing_checkbox_cell.html" with obj_type="page" obj=page aria_labelledby_prefix="page_" aria_labelledby=page.pk|unlocalize aria_labelledby_suffix="_title" %}
{% endif %}
<td id="page_{{ page.pk|unlocalize }}_title" class="title" valign="top" data-listing-page-title>
{% block page_title %}
{% endblock %}
{% if page.content_type.model == 'chapter' %}
<div style="margin-top:10px; padding-left: 30px">
<ul>
{% for c in page.get_children %}
{% if not c.specific.user_created and not c.specific.contentblocksnapshot %}
<li>
{% if page_perms.can_edit %}
<a href="{% url 'wagtailadmin_pages:edit' c.id %}"
title="{% trans 'Edit this page' %}"> {{ c.get_admin_display_title }}
</a>
{% else %}
{{ c.get_admin_display_title }}
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}
</td>
{% if show_parent %}
<td class="parent" valign="top">
{% block page_parent_page_title %}
{% with page.get_parent as parent %}
{% if parent %}
<a href="{% url 'wagtailadmin_explore' parent.id %}">{{ parent.specific_deferred.get_admin_display_title }}</a>
{% endif %}
{% endwith %}
{% endblock %}
</td>
{% endif %}
<td class="updated" valign="top">{% if page.latest_revision_created_at %}
<div class="human-readable-date"
title="{{ page.latest_revision_created_at|date:"DATETIME_FORMAT" }}">
{% blocktrans with time_period=page.latest_revision_created_at|timesince %}{{ time_period }}
ago{% endblocktrans %}</div>{% endif %}</td>
<td class="type" valign="top">{{ page.content_type.model_class.get_verbose_name }}</td>
<td class="status" valign="top">
{% include "wagtailadmin/shared/page_status_tag.html" with page=page %}
</td>
{% block page_navigation %}
{% endblock %}
</tr>
{% endfor %}
{% else %}
{% block no_results %}{% endblock %}
{% endif %}
</tbody>
</table>

View File

@ -0,0 +1,60 @@
{% load i18n wagtailadmin_tags %}
{# The title field for a page in the page listing, when in 'explore' mode #}
<div class="title-wrapper">
{% if page.is_site_root %}
{% if perms.wagtailcore.add_site or perms.wagtailcore.change_site or perms.wagtailcore.delete_site %}
<a href="{% url 'wagtailsites:index' %}" title="{% trans 'Sites menu' %}">{% icon name="site" classname="initial"
%}</a>
{% endif %}
{% endif %}
{% if page_perms.can_edit %}
<a href="{% url 'wagtailadmin_pages:edit' page.id %}" title="{% trans 'Edit this page' %}">
{% if not page.is_site_root and not page.is_leaf %}{% icon name="folder" classname="initial" %}{% endif %}
{{ page.get_admin_display_title }}
</a>
{% else %}
{% if not page.is_site_root and not page.is_leaf %}{% icon name="folder" classname="initial" %}{% endif %}
{{ page.get_admin_display_title }}
{% endif %}
{% if show_locale_labels %}
{% status page.locale.get_display_name classname="w-status--label" %}
{% endif %}
{% include "wagtailadmin/pages/listing/_privacy_indicator.html" with page=page %}
{% include "wagtailadmin/pages/listing/_locked_indicator.html" with page=page %}
</div>
<ul class="actions">
{% page_listing_buttons page request.user next_url=actions_next_url %}
</ul>
<!--Here starts the customization part. -->
<!--Commit: 3c5c9422353964aa25cdd04b296859f71c4c1a34-->
{% if page.content_type.model == 'chapter' %}
<div style="margin-top:10px; padding-left: 30px">
<ul>
{% for c in page.get_children %}
{% if not c.specific.user_created and not c.specific.contentblocksnapshot %}
<li>
{% if page_perms.can_edit %}
<a href="{% url 'wagtailadmin_pages:edit' c.id %}"
title="{% trans 'Edit this page' %}"> {{ c.get_admin_display_title }}
</a>
{% else %}
{{ c.get_admin_display_title }}
{% endif %}
</li>
{% endif %}
{% endfor %}
</ul>
</div>
{% endif %}

View File

@ -2,6 +2,7 @@ from django.contrib import admin
from wagtail.admin.panels import CommentPanel from wagtail.admin.panels import CommentPanel
from wagtail.admin.panels import FieldPanel, ObjectList from wagtail.admin.panels import FieldPanel, ObjectList
from wagtail.models import Page from wagtail.models import Page
from wagtail.admin.widgets.slug import SlugInput
class StrictHierarchyPage(Page): class StrictHierarchyPage(Page):
@ -41,4 +42,4 @@ def wagtail_parent_filter(parent_cls, child_cls):
def get_default_settings(): def get_default_settings():
return ObjectList([FieldPanel("slug"), CommentPanel()], heading="Settings") return ObjectList([FieldPanel("slug", widget=SlugInput), CommentPanel()], heading="Settings")