Een goed navigatiemenu is cruciaal. In dit artikel zullen we een flexibel menu maken dat de paginastructuur van Wagtail gebruikt, bewerkbaar is en reageert op het inloggen en uitloggen van de gebruiker.
Het bouwen van een navigatiemenu is eenvoudig met tools zoals Bootstrap. Wanneer de site echter een beetje ingewikkelder wordt, wordt het navigatiemenu dat ook. In deze tutorial gaan we een navigatiemenu bouwen met de volgende vereisten:
Zoals we zullen zien, vereist dit een aantal afwegingen over waar welke functionaliteit te plaatsen.
Allereerst moet gezegd worden dat er een geweldig en eenvoudig pakket is om menu's te bouwen in Wagtail: wagtailmenus. Helaas lijkt er geen manier te zijn om het menu te laten reageren op gebruikers die zijn ingelogd. Dus misschien is dit een van die momenten waarop we de overvloed aan pakketten en opties die Django en Wagtail ons bieden niet kunnen gebruiken en we zelf Python-code gaan schrijven. Gelukkig is er een voorbeeld waarop we kunnen voortbouwen.
Ons navigatiesysteem zal uit vier elementen bestaan: een menumodel, een menu-itemmodel, een templatetag waarmee we het menu in onze templates kunnen invoegen en een template die het menu weergeeft. Het menu is afgeleid van ClusterableModel
. Dit is een model uit het pakket django-modelcluster dat wordt gebruikt door Wagtail (en samen met Wagtail wordt geïnstalleerd), o.a. om 'ouder'- en' kind'-modellen (zoals een menu en de menu-items) als één geheel te behandelen. Het menumodel heeft slechts twee velden, een title
en een slug
.
from django.db import models
from django_extensions.db.fields import AutoSlugField
from modelcluster.models import ClusterableModel
from wagtail.admin.edit_handlers import FieldPanel, InlinePanel, MultiFieldPanel
from wagtail.snippets.models import register_snippet
@register_snippet
class Menu(ClusterableModel):
title = models.CharField(max_length=50)
slug = AutoSlugField(populate_from='title', editable=True, help_text="Unique identifier of menu. Will be populated automatically from title of menu. Change only if needed.")
panels = [
MultiFieldPanel([
FieldPanel('title'),
FieldPanel('slug'),
], heading=_("Menu")),
InlinePanel('menu_items', label=_("Menu Item"))
]
def __str__(self):
return self.title
Net als in het voorbeeld hebben we het pakket django-extensions gebruikt, dat een model AutoSlugField
heeft dat het veld slug
automatisch vult met een waarde die is afgeleid van de titel. In Wagtail admin vult het slug-veld zich automatisch tijdens het typen van de titel; opgemerkt moet worden dat dit alleen werkt als het titelveld title
heet en het slug-veld slug
wordt genoemd. Installeer het pakket met:
pip3 install django-extensions
Voeg het toe aan requirements.txt
en voeg django_extensions
toe aan INSTALLED_APPS
.
Het veld menu_items
is de related_name
van de relatie met het MenuItem
-model dat we nu gaan maken. Op deze manier kunnen we de menu-items maken bij het bewerken van ons menu. Ons menu-itemmodel moet de volgende elementen bevatten:
Het model volgt daar logisch uit. We zullen het niet registreren als een snippet (in tegenstelling tot Menu
), omdat we menu-items kunnen toevoegen en bewerken via de Menu
-snippet in admin.
from modelcluster.fields import ParentalKey
from wagtail.admin.edit_handlers import PageChooserPanel
from wagtail.core.models import Orderable
from wagtail.images.edit_handlers import ImageChooserPanel
class MenuItem(Orderable):
menu = ParentalKey('Menu', related_name='menu_items', help_text=_("Menu to which this item belongs"))
title = models.CharField(max_length=50, help_text=_("Title of menu item that will be displayed"))
link_url = models.CharField(max_length=500, blank=True, null=True, help_text=_("URL to link to, e.g. /accounts/signup (no language prefix, LEAVE BLANK if you want to link to a page instead of a URL)"))
link_page = models.ForeignKey(
TranslatablePage, blank=True, null=True, related_name='+', on_delete=models.CASCADE, help_text=_("Page to link to (LEAVE BLANK if you want to link to a URL instead)"),
)
title_of_submenu = models.CharField(
blank=True, null=True, max_length=50, help_text=_("Title of submenu (LEAVE BLANK if there is no custom submenu)")
)
icon = models.ForeignKey(
'wagtailimages.Image', blank=True, null=True, on_delete=models.SET_NULL, related_name='+',
)
show_when = models.CharField(
max_length=15,
choices=[('always', _("Always")), ('logged_in', _("When logged in")), ('not_logged_in', _("When not logged in"))],
default='always',
)
panels = [
FieldPanel('title'),
FieldPanel('link_url'),
PageChooserPanel('link_page'),
FieldPanel('title_of_submenu'),
ImageChooserPanel('icon'),
FieldPanel('show_when'),
]
def __str__(self):
return self.title
Door de klasse Orderable
te gebruiken, kunnen we een volgorde aan de menu-items geven, wat we uiteraard willen. Zoals hierboven vermeld, hebben we de gerelateerde naam menu_items
die we nodig hebben in ons menumodel. Het ParentalKey
-model is vergelijkbaar met een ForeignKey
en wordt ook geïmporteerd uit het pakket django-modelcluster
. Het veld link_page
verwijst naar een TranslatablePage
binnen onze Wagtail-boom. Het veld name_of_submenu
is de naam van het submenu, als dat er is. Via het veld icon
kunnen we een afbeelding uploaden. Ten slotte gebruikt het veld show_when
de parameter choices
om een aantal opties weer te geven wanneer het menu-item moet worden weergegeven.
Alle velden van Menu
en MenuItem
kunnen via een template tag aan een template worden doorgegeven. We moeten echter nog een aantal dingen doen:
link_url
/ link_page
vertalen naar de juiste taal (in het geval van een meertalige site)link_page
kinderen heeft en, afhankelijk van deze uitkomst, het submenu makenEen opmerking over design. Punten 1 en 2 kunnen worden beschouwd als aspecten van het MenuItem
-model, en moeten daarom deel uitmaken van de modeldefinitie. Punt 3 is moeilijker: we kunnen (en zullen) een situatie hebben waarin een menu een item heeft dat verwijst naar een Wagtail-pagina met kinderen die we in een submenu zouden willen. Het construeren van dit submenu maakt geen gebruik van het menumodel. Het is mogelijk om dit op te lossen in de template: controleer of er een submenu bestaat, maak anders een apart codeblok voor de kinderen van link_page
. Business logic moet echter buiten de template worden gehouden. Onze keuze is dus om dit op te lossen in de template tag. Geef je mening in een opmerking hieronder als je denkt dat er een betere oplossing is. Laten we beginnen met het vertalen van link_page
. Als je slechts één taal gebruikt, kun je deze stap overslaan. We gebruiken het concept van canonieke pagina's van wagtailtrans: een canonieke pagina van een bepaalde pagina is die pagina in de standaardtaal. Je kunt de canonieke pagina in Wagtail admin voor elke pagina bekijken, onder het tabblad Instellingen. Elke vertaalbare pagina heeft de velden language
en canonical_page
; als het veld canonical_page
niet is gedefinieerd, dan is de pagina zelf de canonical page. Dus we krijgen de vertaalde versie van de link_page
(als die er is) als volgt:
from django.conf import settings
from wagtailtrans.models import Language
def trans_page(self, language_code):
if self.link_page:
can_page = self.link_page.canonical_page if self.link_page.canonical_page else self.link_page
if language_code == settings.LANGUAGE_CODE: # requested language is the canonical language
return can_page
try:
language = Language.objects.get(code=language_code)
except Language.DoesNotExist: # no language found, return original page
return self.link_page
return TranslatablePage.objects.get(language=language, canonical_page=can_page)
return None
Een vertaalde url verkrijgen die is afgeleid van link_url
als die is gedefinieerd, of anders van link_page
, is dan relatief eenvoudig (dit veronderstelt dat we taalvoorvoegsels gebruiken voor onze urls):
def trans_url(self, language_code):
if self.link_url:
return '/' + language_code + self.link_url
elif self.link_page:
return self.trans_page(language_code).url
return None
We willen ook de titel van het submenu vertalen in een slug, wat de menu-identifier is die we gebruiken (omdat het minder foutgevoelig is dan een titel):
@property
def slug_of_submenu(self):
# becomes slug of submenu if there is one, otherwise None
if self.title_of_submenu:
return slugify(self.title_of_submenu)
return None
Tenslotte kunnen we beslissen of een menu-item getoond moet worden: alleen als het veld show_when
overeenkomt met de authenticatiestatus van de gebruiker.
def show(self, authenticated):
return ((self.show_when == 'always')
or (self.show_when == 'logged_in' and authenticated)
or (self.show_when == 'not_logged_in' and not authenticated))
Nu onze templatetag. Een menu-item dat in een template moet worden weergegeven, heeft de volgende elementen nodig:
Als er een slug is die een submenu identificeert, kunnen we dat menu doorgeven aan onze template. Maar als we ons submenu moeten samenstellen uit kinderen van een pagina, is er geen menu-instantie. Dus in plaats daarvan stellen we een lijst samen met menu-items die beide opties dekken. Hiervoor gebruiken we een simple tag. Maak een bestand templatetags/cms_tags.py
. De basis is (we importeren al enkele dingen die we nodig hebben):
from cms.models import Menu
from django import template
from django.utils import translation
register = template.Library()
@register.simple_tag()
def get_menu(slug, page, logged_in):
# returns a list of dicts with title, url, slug, page and icon of all items in the menu of the given slug or page
CODE FOLLOWS
Dus als argumenten nemen we:
logged_in
die aangeeft of onze gebruiker al dan niet is ingelogd.De uitvoer van get_menu
zal een lijst met menu-items zijn, elk met de volgende elementen: een titel, een url, een slug van een mogelijk submenu, een pagina voor een mogelijk submenu en mogelijk een pictogram. Hiermee bouwen we ons menu op in de template.
Eerst het geval wanneer er een menu met de gegeven slug bestaat. We halen de huidige taal op via Django's functie get_language
. Met de methoden en eigenschappen in onze modellen is de lijst dan eenvoudig op te bouwen.
try:
# see if there is a custom menu defined for the slug of the item
candidates = Menu.objects.get(slug=slug).menu_items.all()
language_code = translation.get_language()
# create a list of all items that should be shown in the menu depending on logged_in
menu_items = []
for candidate in candidates:
if candidate.show(logged_in):
menu_items.append({'title': candidate.title, 'url': candidate.trans_url(language_code),
'slug': candidate.slug_of_submenu, 'page': candidate.trans_page(language_code), 'icon': candidate.icon})
return menu_items
except Menu.DoesNotExist:
pass
Als er geen aangepast menu is gedefinieerd, stellen we dezelfde lijst samen met de onderliggende items van de gegeven pagina:
try:
# if there is no custom menu, then there should be a valid page argument; see if it has children
candidates = page.get_children()
# if so, create a list of all items that have show_in_menus == True
menu_items = []
for candidate in candidates:
if candidate.show_in_menus:
menu_items.append({'title': candidate.title, 'url': candidate.url,
'slug': None, 'page': candidate, 'icon': None})
return menu_items
except AttributeError:
# neither custom menu nor valid page argument; return None
return None
De enige controle die we doen is of op de onderliggende pagina de vlag show_in_menus
is ingesteld op True
. Deze vlag is standaard Wagtail en kan worden ingesteld in het tabblad Promotie in de editor. Als er geen kinderen worden getoond, is de lijst leeg. Als de pagina ongeldig is, retourneren we None
en hebben we geen menu.
Dat maakt onze tag compleet. Maak een template main_menu.html
en gebruik de tag met de regels:
{% load cms_tags wagtailimages_tags %}
{% get_menu "main" None request.user.is_authenticated as navigation %}
We laden ook al wagtailimages_tags
, omdat we Wagtail's image
-tag zullen gebruiken om pictogrammen weer te geven. We noemen ons hoofdmenu main
, dat is de naam die we ook in onze editor moeten gebruiken. Het tweede argument None
komt overeen met het pagina-argument van onze templatetag. Aangezien we een menu main
zullen definiëren, hebben we dit niet nodig, dus we kunnen None
gebruiken. Tot slot vertelt request.user.is_authenticated
ons of de gebruiker is ingelogd of niet. Om het menu samen te stellen gebruiken we de standaard Bootstrap navbar-component. Het interessante deel van de template is het volgende:
{% for item in navigation %}
{% get_menu item.slug item.page request.user.is_authenticated as submenu %}
<li class="{% if submenu %}dropdown {% endif %}p-2">
<div class="dropdown show">
<a href="{{ item.url }}"
{% if submenu %} class="menuitem dropdown-toggle {% if item.icon %}menuicon{% endif %}" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
{% else %} data-toggle="tooltip" title="{{ item.title }}" class="menuitem"
{% endif %}
>
{% if item.icon %}
{% image item.icon fill-30x30 class="image-menu" %}
{% else %}
{{ item.title }}
{% endif %}
</a>
{% if submenu %}
DISPLAY THE SUBMENU
{% endif %}
</div>
</li>
{% endfor %}
Voor elk item in onze huidige menu navigation
proberen we eerst een submenu op te halen. Als er een is, gebruiken we een dropdown toggle, anders een tooltip (beide standaard Bootstrap). De link is gewoon item.url.
Als er een pictogram is, gebruiken we het, anders gebruiken we item.title
. Het toevoegen van enkele klassen maakt specifieke styling mogelijk als we dat willen. Het submenu weergeven is in principe hetzelfde, behalve dat we in ons menu geen derde niveau veronderstellen. Onze functie get_menu
zou prima een derde niveau of zoveel niveaus toestaan als we willen, maar we houden het hier bij twee. Dan is de code voor het weergeven van het submenu eenvoudigweg:
{% for subitem in submenu %}
<a href="{{ subitem.url }}" class="dropdown-item menuitem p-2 {% if subitem.icon %}menuicon{% endif %}">
{% if subitem.icon %}
{% image subitem.icon fill-30x30 class="image-menu" %}
{% else %}
{{ subitem.title }}
{% endif %}
</a>
{% endfor %}
De styling kan minimaal zijn. Als we een pictogram in ons menu hebben, kunnen we het iets vergroten wanneer de gebruiker de muis erboven beweegt:
/* slightly enlarge the image on hover */
a.menuitem .image-menu {
transition: transform 300ms;
}
a.menuitem:hover .image-menu {
transform: scale(1.1);
}
De volledige code van main_menu.html
en alle andere bestanden zijn te vinden in de repository. We hoeven nu alleen de volgende regel in onze base.html
-template te plaatsen, aan het begin van de body:
{% include "main_menu.html" %}
Dat is het. We kunnen de database migreren om onze nieuwe modellen op te nemen, dan naar onze editor gaan, naar Snippets, het menu main
maken, iconen toevoegen en zien hoe het eruit ziet. In een eerdere tutorial hebben we authenticatie met allauth opgezet, zodat we een menu account
kunnen definiëren met een submenu met links naar /accounts/login
, /accounts/logout
etc. Op deze manier combineren we onze authenticatie-app en onze content management app in één navigatiemenu. De bijbehorende video laat hier meer van zien.
Er is nog één minpunt: onze zelfgemaakte menu-items zijn nog niet vertaald. We lossen dit op in een andere tutorial.. Lees verder als je ook een footer-menu met een cookieverklaring en privacybeleid aan de site wilt toevoegen, samen met een favicon en een bedrijfslogo.
Reageer op dit artikel (log eerst in of bevestig hieronder met naam en email)