grimboite/articles/dev/2013-07-26-regroup_by.md

5.1 KiB

Title Date Slug Tags
(re)group by, ou comment grouper facilement des données dans un template Django 2013-07-26 django-regroup-by django, groupby, dev

Un truc hyper sympa en Django, c'est de pouvoir construire un dictionnaire à la volée dans un template. A partir du modèle de l'application, on peut facilement regrouper un ensemble d'éléments sur base d'un champ particulier lors de l'affichage d'un template.

Pour l'exemple ci-dessous, je définis plusieurs modules à exécuter et une ou plusieurs planifications pour chacun d'entre eux. Ces planifications peuvent être programmées une fois par mois (au début, au milieu ou à la fin du mois), à un moment de la semaine ou quotidiennement. Pour l'affichage, une manière simple de faire serait la suivante:

{% for mod in modules %}
	<h2>{{ mod }}</h2>
	<p class="description info">{{ mod.description }}</p>

	<h2>Planifications</h2>
	<ul class="ulbox">
		{% for plan in mod.planification_set.all  %}
			<li>Planifié à {{ p.time }} ({{ p.frequency }})</li>
		{% endfor %}
	</ul>
{% endfor %}

On aurait tous les modules qui s'afficheraient les uns après les autres (ça tombe bien, c'est ce qu'on veut), mais les planifications ne seraient pas regroupées. On aura vite le cas suivant:

Module 1
	Planifié à 18h (quotidiennement)
	Planifié à 18h (lundi)
	Planifié à 10h (au début du mois)
	Planifié à 10h (quotidiennement)

Pour l'affichage, j'aimerais en fait que toutes les planifications soient regroupées par fréquence, puis par moment d'exécution, pour avoir la visualisation suivante:

Module 1
	Quotidiennement
		Planifié à 10h
		Planifié à 17h
	Lundi
		Planifié à 18h
	Au début du mois
		Planifié à 10h

C'est là que la méthode regroup de Django peut nous aider.

Supposons que nous ayons le modèle suivant :

class Module(models.Model):
	label = models.CharField(max_length=255, editable=False, db_index=True)
	description = models.CharField(max_length=255, blank=True, editable=False)

	def __unicode__(self):
		return unicode(self.label)

class Planification(models.Model):
	FREQUENCY = (
			('1x par jour', ((0, 'Quotidiennement'),)),
			('1x par semaine', (
				(1, 'Lundi'),
				(2, 'Mardi'),
				(3, 'Mercredi'),
				(4, 'Jeudi'),
				(5, 'Vendredi'),
				(6, 'Samedi'),
				(7, 'Dimanche')
			)),
			('1x par mois', (
				(-1, 'Au début du mois'),
				(-15, 'Au milieu du mois'),
				(-30, 'A la fin du mois')
			)),)

	MONTH_LENGTH=[31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

	module = models.ForeignKey('Module')
	time = models.TimeField(default='18:00:00')
	frequency = models.IntegerField(choices=FREQUENCY, verbose_name="Fréquence de planification")

	class Meta:
		""" Le petit paramètre qui permet de trier directement nos planifications par fréquence, puis par moment d'exécution """
		ordering = ['frequency', 'time',]

	def __unicode__(self):
		return u'[%s] %s, %s' % (self.module, self.get_frequency_display(), self.time)

	def __self__(self):
		return '[%s] %s, %s' % (self.module, self.get_frequency_display(), self.time)

	def __repr__(self):
		return '[%s] %s, %s' % (self.module, self.get_frequency_display(), self.time)

Ce n'est pas trop compliqué, il y a juste une ForeignKey planquée quelque part entre la classe Module et la classe de Planification et qui permet de faire le lien entre ces deux classes. Dans le template:

  1. on va d'abord boucler sur tous les modules présents,
  2. puis on va appeler le groupement sur les planifications qui y sont associées.
  3. On va ensuite boucler non seulement sur les différentes clés (la méthode regroup construit un dictionnaire et pas une simple liste), puis sur chacune des valeurs proposées pour cette clé:
{% for mod in modules %}
	<h2>{{ mod }}</h2>
	<p class="description info">{{ mod.description }}</p>

	{% regroup mod.planification_set.all by get_frequency_display as plans_by_freq %}
	{% if plans_by_freq %}
		<h2>Planifications</h2>
		<ul class="ulbox">
			{% for plan in plans_by_freq  %}
				<li><b>{{ plan.grouper }}</b>
				<ul>
					{% for p in plan.list %}
						<li>{{ p.time }}</li>
					{% endfor %}
				</ul>
				</li>
			{% endfor %}
		</ul>
	{% endif %}
{% endfor %}

Explications dans l'ordre:

  • La méthode regroup <bidule> by <chose> permet de construire le dictionnaire sur base du champ souhaité
  • Le {% if plans_by_freq %} n'est pas obligatoire, il est juste là pour ne pas afficher le titre s'il n'y a aucune planification associée
  • {% for plan in plans_by_freq %} boucle sur toutes les clés/valeurs
  • {{ plan.grouper }} permet d'afficher la valeur de la clé de tri
  • {% for p in plan.list %} permet de boucler sur toutes les valeurs associées à la clé courante.

Django va alors trier ce dictionnaire avec les paramètres par défaut définis au niveau de la classe. C'est pour cette raison que j'avais ajouté un tri dans la classe Planification:

class Meta:
	ordering = ['frequency', 'time']

Petite astuce en plus: si on veut utiliser l'affichage d'un champ CHOICE plutôt que la valeur de l'option, il suffit d'utiliser la méthode get_*field*_display dans le template. Bref, c'est magique.