Un bon menu de navigation est crucial. Dans cet article, on va créer un menu flexible qui utilise la structure de page de Wagtail, est modifiable et répond à la connexion et à la déconnexion de l'utilisateur.
La création d'un menu de navigation de base est facile avec des outils tels que Bootstrap. Cependant, lorsque votre site devient un peu plus compliqué, votre menu de navigation en fait de même. Dans ce didacticiel, on va créer un menu de navigation avec les exigences suivantes:
Comme on le verra, cela nécessitera quelques compromis sur l'endroit où mettre quelle fonctionnalité.
Tout d'abord, il faut dire qu'un package génial et facile à créer des menus dans Wagtail est wagtailmenus. Malheureusement, il ne semble pas y avoir de moyen de faire répondre le menu aux utilisateurs connectés. Alors peut-être que c'est un de ces moments où on ne peut pas utiliser la pléthore de packages et d'options que Django et Wagtail nous offrent et on se met à écrire du code Python soi-même. Heureusement, il existe un exemple sur lequel on peut s'appuyer.
Notre système de navigation comprendra quatre éléments: un modèle de menu, un modèle d'élément de menu, une balise de modèle qui nous permettra d'insérer le menu dans nos modèles et un modèle qui affiche le menu. Le menu est sous-classé à partir de ClusterableModel
. Il s'agit d'un modèle du package django-modelcluster qui est utilisé par Wagtail (et installé avec Wagtail), a.o. pour permettre aux modèles 'parent' et 'enfant' (comme un menu et ses éléments de menu) d'être traités comme une seule unité. Le modèle menu
n'a que deux champs, un title
et un 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
Comme dans l'exemple, on a utilisé le package django-extensions, qui a un modèle AutoSlugField
qui remplit automatiquement le champ slug
avec une valeur dérivée du titre. Dans Wagtail admin, le champ slug
se remplit automatiquement lors de la saisie du titre; il convient de noter que cela ne fonctionne que lorsque le champ titre est appelé title
et le champ slug appelé slug
. Installez le package avec:
pip3 install django-extensions
Ajoutez-le à requirements.txt
et ajoutez django_extensions
à INSTALLED_APPS
.
Le champ menu_items
est le related_name
de la relation avec le modèle MenuItem
qu'on va créer maintenant. De cette façon, on peut créer les éléments de menu lors de la modification de notre menu. Notre modèle d'élément de menu doit comporter les éléments suivants:
Le modèle découle assez logiquement de tout cela. On ne l'enregistrera pas en tant que bloc (snippet) (par opposition à Menu
), car on pourra ajouter et modifier des éléments de menu via le bloc Menu
dans 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
L'utilisation de la classe Orderable
nous permet de donner un ordre aux éléments de menu, ce qu'on veut évidemment. Comme mentionné ci-dessus, on a le nom associé menu_items
dont on a besoin dans notre modèle Menu
. Le modèle ParentalKey
est similaire à une ForeignKey
et est également importé du package django-modelcluster
. Le champ link_page
pointe vers une TranslatablePage
dans notre arbre Wagtail. Le champ name_of_submenu
est le nom du sous-menu, s'il y en a un. Le champ icon
nous permet de télécharger une image. Enfin, le champ show_when
utilise l'option choices
pour répertorier un certain nombre d'options quand afficher l'élément de menu.
Tous les champs de Menu
et MenuItem
peuvent être transmis via une balise de modèle à un modèle html. Cependant, on doit encore faire trois choses:
link_url
/ link_page
dans la bonne langue (dans le cas d'un site multilingue)link_page
a des enfants, et en fonction de ce résultat, créer le sous-menuUne petite remarque sur le design. Les points 1 et 2 peuvent être considérés comme des aspects du modèle MenuItem
et doivent donc faire partie de la définition du modèle. Le point 3 est plus difficile: on peut (et va) avoir une situation où un menu a un élément qui pointe vers une page Wagtail avec des enfants qu'on voudra dans un sous-menu. La construction de ce sous-menu n'utilise pas le modèle Menu
. Il est possible de résoudre ce problème dans le modèle html: vérifiez s'il existe un sous-menu, sinon créez un bloc de code séparé pour les enfants de link_page
. Cependant,la logique doit être exclue du modèle html. Notre choix est donc de résoudre ce problème dans la balise de modèle. Veuillez donner votre avis dans un commentaire ci-dessous si vous pensez qu'il existe une meilleure solution. Commençons par traduire link_page
. Si vous n'utilisez qu'une seule langue, vous pouvez ignorer cette étape. On utilise le concept de pages canoniques de wagtailtrans: une page canonique d'une page donnée est cette page dans la langue par défaut. Vous pouvez voir la page canonique dans Wagtail admin pour chaque page, sous l'onglet Paramètres. Chaque page traduisible a des champs language
et canonical_page
; si le champ canonical_page
n'est pas défini, alors la page elle-même est la page canonique. On obtient donc la version traduite de la link_page
(s'il y en a une) comme suit:
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
Obtenir une URL traduite dérivée de link_url
si elle est définie, ou bien de link_page
, est alors relativement facile (cela suppose qu'on utilise des préfixes de langue pour nos URL):
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
On veut également traduire le titre du sous-menu dans un slug, qui est l'identifiant de menu qu'on utilise (car il est moins sujet aux erreurs qu'un titre):
@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
Enfin, on peut décider si un élément de menu doit être affiché: uniquement si le champ show_when
correspond au statut d'authentification de l'utilisateur.
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))
Passons maintenant à notre balise de modèle. Un élément de menu à afficher dans un modèle nécessite les éléments suivants:
S'il y a un slug identifiant un sous-menu, on pourra passer ce menu à notre modèle html. Mais si on doit construire notre sous-menu à partir des enfants d'une page, il n'y a pas d'instance Menu
. Au lieu de cela, on construit une liste d'éléments de menu qui couvrira les deux options. Pour cela, on utilise une simple balise. Créez un fichier templatetags/cms_tags.py
. La base est (on importe déjà certaines choses dont on aura besoin):
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
Donc, comme arguments, on prend:
logged_in
indiquant si notre utilisateur est connecté ou non.La sortie de get_menu
sera une liste d'éléments de menu, chacun avec les éléments suivants: un titre, une url, un slug d'un sous-menu possible, une page pour un sous-menu possible, et éventuellement une icône. Avec cela, on va construire notre menu dans le modèle html.
Tout d'abord le cas où un menu avec le slug donné existe. On récupère la langue courante via la fonction get_language
de Django. Avec les méthodes et les propriétés de nos modèles, la liste est alors facile à construire.
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
Si aucun menu personnalisé n'est défini, on construit la même liste en utilisant les enfants de la page donnée:
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
La seule vérification qu'on fait est de savoir si la page enfant a l'indicateur show_in_menus
défini sur True
. Cet indicateur est Wagtail standard et peut être défini dans l'onglet Promotion dans l'éditeur. S'il n'y a aucun enfant à afficher, la liste sera vide. Si la page n'est pas valide, on renvoie None
et on n'a pas de menu.
Cela complète notre balise. Créez un modèle main_menu.html
et utilisez la balise avec les lignes:
{% load cms_tags wagtailimages_tags %}
{% get_menu "main" None request.user.is_authenticated as navigation %}
On fait entrer wagtailimages_tags
également, car on utilisera la balise image
de Wagtail pour afficher les icônes. On appellera notre menu principal main
, c'est le nom qu'on devra également utiliser dans notre éditeur. Le deuxième argument None
correspond à l'argument page
de notre balise de modèle. Puisqu'on définira un menu principal, on n'en aura pas besoin, alors on peut donc utiliser None
. Enfin request.user.is_authenticated
nous indique si l'utilisateur est connecté ou non. Pour construire le menu, on utilisera le composant Bootstrap standard navbar
. La partie intéressante du modèle est la suivante:
{% 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 %}
Pour chaque élément de notre menu navigation
, on essaye d'abord de récupérer un sous-menu. S'il y en a un, on utilise une bascule déroulante, sinon une info-bulle (les deux Bootstrap standard). Le lien est simplement item.url
. S'il y a une icône, on l'utilise, sinon on utilise item.title
. L'ajout de certaines classes permet un style spécifique si on le veut. L'affichage du sous-menu est fondamentalement le même, sauf qu'on n'assume pas de troisième niveau dans notre menu. Notre fonction get_menu
permettrait parfaitement un troisième niveau ou autant de niveaux qu'on veut, mais ici on s'arrête à deux. Ensuite, le code pour afficher le sous-menu est simplement:
{% 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 %}
Le style peut être minimal. Si on a une icône dans notre menu, on peut l'agrandir légèrement lorsque l'utilisateur la survole:
/* slightly enlarge the image on hover */
a.menuitem .image-menu {
transition: transform 300ms;
}
a.menuitem:hover .image-menu {
transform: scale(1.1);
}
Le code complet de main_menu.html
et tous les autres fichiers se trouvent dans le référentiel. On a maintenant seulement besoin de mettre la ligne suivante dans notre modèle base.html
, au début de notre corps:
{% include "main_menu.html" %}
C'est tout. On peut migrer la base de données pour intégrer nos nouveaux modèles, puis aller dans notre éditeur, dans Blocs (Snippets), créer le menu main
, ajouter des icônes et voir à quoi il ressemble. Dans un didacticiel précédent, on a configuré l'authentification avec allauth, alors on peut créer un menu account
avec un sous-menu avec des liens vers /accounts/login
, /accounts/logout
, etc. De cette façon, on combine notre application d'authentification et notre application de gestion de contenu dans un menu de navigation. La vidéo ci-jointe en montre plus.
Il y a encore un défaut: nos éléments de menu auto-construits ne sont pas encore traduits. On va résoudre ce problème dans un autre tutoriel. Lisez la suite si vous souhaitez également ajouter un menu de pied de page avec une déclaration de cookie et une politique de confidentialité à votre site, ainsi qu'un favicon et un logo d'entreprise.
Commentez cet article (connectez-vous d'abord ou confirmez par nom et email ci-dessous)