diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 16d51f3ac8..6686c12899 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,7 @@ Changelog Unreleased ========== +* feat: Added code for burger menu in actions column of changelist view. 1.0.7 (2022-04-01) ================== diff --git a/djangocms_navigation/admin.py b/djangocms_navigation/admin.py index 7241c03679..13ae481faf 100644 --- a/djangocms_navigation/admin.py +++ b/djangocms_navigation/admin.py @@ -82,8 +82,13 @@ class MenuContentAdmin(admin.ModelAdmin): list_filter = (LanguageFilter, ) class Media: - js = ("admin/js/jquery.init.js", "djangocms_versioning/js/actions.js",) - css = {"all": ("djangocms_versioning/css/actions.css", "djangocms_version_locking/css/version-locking.css",)} + js = ("admin/js/jquery.init.js", "djangocms_versioning/js/actions.js") + css = { + "all": ( + "djangocms_versioning/css/actions.css", + "djangocms_version_locking/css/version-locking.css", + ) + } def get_version(self, obj): """ @@ -325,11 +330,11 @@ class MenuItemAdmin(TreeAdmin): actions = None change_form_template = "admin/djangocms_navigation/menuitem/change_form.html" change_list_template = "admin/djangocms_navigation/menuitem/change_list.html" - list_display = ["__str__", "get_object_url", "soft_root", 'hide_node'] sortable_by = ["pk"] list_per_page = TREE_MAX_RESULT_PER_PAGE_COUNT class Media: + js = ("admin/js/jquery.init.js", "djangocms_versioning/js/actions.js") css = { "all": ( "djangocms_versioning/css/actions.css", diff --git a/djangocms_navigation/templates/admin/djangocms_navigation/menucontent/change_list.html b/djangocms_navigation/templates/admin/djangocms_navigation/menucontent/change_list.html new file mode 100644 index 0000000000..1c993ffdcd --- /dev/null +++ b/djangocms_navigation/templates/admin/djangocms_navigation/menucontent/change_list.html @@ -0,0 +1,11 @@ +{% extends "admin/change_list.html" %} +{% load static %} + +{% block extrahead %} + {{ block.super }} + + {# INFO: versioning_static_url_prefix variable is used in versioning actions.js #} + +{% endblock extrahead %} diff --git a/djangocms_navigation/templates/admin/djangocms_navigation/menuitem/change_list.html b/djangocms_navigation/templates/admin/djangocms_navigation/menuitem/change_list.html index df56d43e05..abb33f8345 100644 --- a/djangocms_navigation/templates/admin/djangocms_navigation/menuitem/change_list.html +++ b/djangocms_navigation/templates/admin/djangocms_navigation/menuitem/change_list.html @@ -2,6 +2,14 @@ {% extends "admin/tree_change_list.html" %} {% load static admin_list admin_urls navigation_admin_tree i18n djangocms_versioning %} +{% block extrahead %} + {{ block.super }} + {# INFO: versioning_static_url_prefix variable is used to inject static_url into actions.js #} + +{% endblock extrahead %} + {% block extrastyle %} {{ block.super }} diff --git a/djangocms_navigation/templatetags/navigation_admin_tree.py b/djangocms_navigation/templatetags/navigation_admin_tree.py index 35c70fdc44..274213fe2a 100644 --- a/djangocms_navigation/templatetags/navigation_admin_tree.py +++ b/djangocms_navigation/templatetags/navigation_admin_tree.py @@ -1,10 +1,21 @@ # -*- coding: utf-8 -*- -from django import template + +import datetime + from django.contrib.admin.templatetags.admin_list import ( result_headers, result_hidden_fields, ) +from django.contrib.admin.utils import ( + display_for_field, + display_for_value, + lookup_field, +) +from django.core.exceptions import ObjectDoesNotExist +from django.db import models +from django.template import Library from django.templatetags.static import static +from django.utils.encoding import force_str from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ @@ -13,7 +24,7 @@ from treebeard.templatetags.admin_tree import check_empty_dict, results -register = template.Library() +register = Library() """ This module is simply for overwriting some of treebeard's admin_tree templatetag functions @@ -21,9 +32,62 @@ CAVEAT: Treebeard encapulates markup in some of it's template tags, so we are needing to keep the same approach in order to overwrite markup, ie. with get_space and get_collapse below. + +INFO: get_result_and_row_class is an almost exact copy, with the exception of a field_name +function to allow proper str representation for callables included as part of list_display +on changelist views. """ +def get_field_name(field_name): + if callable(field_name): + return field_name.__name__ + return field_name + + +def get_result_and_row_class(cl, field_name, result): + empty_value_display = cl.model_admin.get_empty_value_display() + row_classes = ['field-%s' % get_field_name(field_name)] + try: + f, attr, value = lookup_field(field_name, result, cl.model_admin) + except ObjectDoesNotExist: + result_repr = empty_value_display + else: + empty_value_display = getattr(attr, 'empty_value_display', empty_value_display) + if f is None: + if field_name == 'action_checkbox': + row_classes = ['action-checkbox'] + allow_tags = getattr(attr, 'allow_tags', False) + boolean = getattr(attr, 'boolean', False) + result_repr = display_for_value(value, empty_value_display, boolean) + # Strip HTML tags in the resulting text, except if the + # function has an "allow_tags" attribute set to True. + # WARNING: this will be deprecated in Django 2.0 + if allow_tags: + result_repr = mark_safe(result_repr) + if isinstance(value, (datetime.date, datetime.time)): + row_classes.append('nowrap') + else: + if isinstance(getattr(f, 'remote_field'), models.ManyToOneRel): + field_val = getattr(result, f.name) + if field_val is None: + result_repr = empty_value_display + else: + result_repr = field_val + else: + result_repr = display_for_field(value, f, empty_value_display) + if isinstance(f, (models.DateField, models.TimeField, + models.ForeignKey)): + row_classes.append('nowrap') + if force_str(result_repr) == '': + result_repr = mark_safe(' ') + row_class = mark_safe(' class="%s"' % ' '.join(row_classes)) + return result_repr, row_class + + +admin_tree.get_result_and_row_class = get_result_and_row_class + + def get_spacer(first, result): if first: spacer = f' ' diff --git a/tests/test_admin.py b/tests/test_admin.py index af2747c961..ca29df9d1f 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -1,6 +1,7 @@ import html import importlib import json +import re import sys from unittest.mock import patch @@ -241,6 +242,20 @@ def test_list_display_without_versioning(self): menu_content_admin.get_list_display(request), ["title", "get_menuitem_link", "get_preview_link"] ) + def test_menucontent_changelist_verify_burger_menu_available(self): + """ + The actions column should display a burger menu for any actions in addition + to edit or preview. Dependent on djangocms_versioning, verify media is + present. + """ + factories.MenuContentWithVersionFactory() + list_url = reverse("admin:djangocms_navigation_menucontent_changelist") + + response = self.client.get(list_url) + soup = BeautifulSoup(str(response.content), features="lxml") + + self.assertTrue(soup.find("script", src=re.compile("djangocms_versioning/js/actions.js"))) + class MenuItemModelAdminTestCase(TestCase): def setUp(self): @@ -974,6 +989,23 @@ def test_menuitem_changelist_check_for_expand_all(self): self.assertEqual(_('Toggle expand/collapse all'), link['title']) self.assertIn('+', link.string) + def test_menuitem_changelist_verify_burger_menu_available(self): + """ + The actions burger menu should be available for MenuItem. Dependent on + djangocms_versioning, verify css class is present and js is loaded. + """ + menu_content = factories.MenuContentWithVersionFactory() + + list_url = reverse( + "admin:djangocms_navigation_menuitem_list", args=(menu_content.id,) + ) + response = self.client.get(list_url) + + soup = BeautifulSoup(str(response.content), features="lxml") + + self.assertTrue(soup.find_all("a", class_="cms-versioning-action-btn")) + self.assertTrue(soup.find("script", src=re.compile("djangocms_versioning/js/actions.js"))) + @override_settings( CMS_PERMISSION=True,