forked from DGNum/gestioCOF
Merge branch 'kerl/event_options_and_extra_fields' into 'master'
Les événements du nouveau module `events` récupèrent les même fonctionnalités que les événements de `gestioncof` See merge request klub-dev-ens/gestioCOF!398
This commit is contained in:
commit
cc72f47f00
5 changed files with 482 additions and 18 deletions
23
CHANGELOG.md
23
CHANGELOG.md
|
@ -5,10 +5,25 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
|
||||||
|
|
||||||
## Le FUTUR ! (pas prêt pour la prod)
|
## Le FUTUR ! (pas prêt pour la prod)
|
||||||
|
|
||||||
- Nouveau module de gestion des événements
|
### Nouveau module de gestion des événements
|
||||||
- Nouveau module BDS
|
|
||||||
- Nouveau module clubs
|
- Désormais complet niveau modèles
|
||||||
- Module d'autocomplétion indépendant des apps
|
- Export des participants implémenté
|
||||||
|
|
||||||
|
#### TODO
|
||||||
|
|
||||||
|
- Vue de création d'événements ergonomique
|
||||||
|
- Vue d'inscription à un événement **ou** intégration propre dans la vue
|
||||||
|
"inscription d'un nouveau membre"
|
||||||
|
|
||||||
|
### Nouveau module BDS
|
||||||
|
|
||||||
|
Uniquement un modèle BDSProfile pour le moment…
|
||||||
|
|
||||||
|
### Nouveau module de gestion des clubs
|
||||||
|
|
||||||
|
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
|
||||||
|
adhérents ni des cotisations.
|
||||||
|
|
||||||
## Upcoming
|
## Upcoming
|
||||||
|
|
||||||
|
|
199
events/migrations/0003_options_and_extra_fields.py
Normal file
199
events/migrations/0003_options_and_extra_fields.py
Normal file
|
@ -0,0 +1,199 @@
|
||||||
|
# Generated by Django 2.2.8 on 2019-12-22 14:54
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("events", "0002_event_subscribers"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ExtraField",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(
|
||||||
|
max_length=200, verbose_name="champ d'événement supplémentaire"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"field_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("shorttext", "texte court (une ligne)"),
|
||||||
|
("longtext", "texte long (plusieurs lignes)"),
|
||||||
|
],
|
||||||
|
max_length=9,
|
||||||
|
verbose_name="type de champ",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Option",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"name",
|
||||||
|
models.CharField(max_length=200, verbose_name="option d'événement"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"multi_choices",
|
||||||
|
models.BooleanField(default=False, verbose_name="choix multiples"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "option d'événement",
|
||||||
|
"verbose_name_plural": "options d'événement",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="OptionChoice",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("choice", models.CharField(max_length=200, verbose_name="choix")),
|
||||||
|
(
|
||||||
|
"option",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="choices",
|
||||||
|
to="events.Option",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "choix d'option d'événement",
|
||||||
|
"verbose_name_plural": "choix d'option d'événement",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Registration",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "inscription à un événement",
|
||||||
|
"verbose_name_plural": "inscriptions à un événement",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.RemoveField(model_name="event", name="subscribers"),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="event",
|
||||||
|
name="subscribers",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
through="events.Registration",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
verbose_name="inscrit⋅e⋅s",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="registration",
|
||||||
|
name="event",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to="events.Event"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="registration",
|
||||||
|
name="options_choices",
|
||||||
|
field=models.ManyToManyField(to="events.OptionChoice"),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="registration",
|
||||||
|
name="user",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="option",
|
||||||
|
name="event",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="options",
|
||||||
|
to="events.Event",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="ExtraFieldContent",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.AutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("content", models.TextField(verbose_name="contenu du champ")),
|
||||||
|
(
|
||||||
|
"field",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="events.ExtraField",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"registration",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="extra_info",
|
||||||
|
to="events.Registration",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"verbose_name": "contenu d'un champ événement supplémentaire",
|
||||||
|
"verbose_name_plural": "contenus d'un champ événement supplémentaire",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="extrafield",
|
||||||
|
name="event",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="extra_fields",
|
||||||
|
to="events.Event",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
148
events/models.py
148
events/models.py
|
@ -1,4 +1,35 @@
|
||||||
|
"""
|
||||||
|
Event framework for GestioCOF and GestioBDS.
|
||||||
|
|
||||||
|
The events implemented in this module provide two types of customisations to event
|
||||||
|
creators (the COF and BDS staff): options and extra (text) fields.
|
||||||
|
|
||||||
|
Options
|
||||||
|
-------
|
||||||
|
|
||||||
|
An option is an extra field in the registration form with a predefined list of available
|
||||||
|
choices. Any number of options can be added to an event.
|
||||||
|
|
||||||
|
For instance, a typical use-case is events where meals are served to participants
|
||||||
|
with different possible menus, say: vegeterian / vegan / without pork / etc. This
|
||||||
|
example can be implemented with an `Option(name="menu")` and an `OptionChoice` for each
|
||||||
|
available menu.
|
||||||
|
|
||||||
|
In this example, the choice was exclusive: participants can only chose one menu. For
|
||||||
|
situations, where multiple choices can be made at the same time, use the `multi_choices`
|
||||||
|
flag.
|
||||||
|
|
||||||
|
Extra fields
|
||||||
|
------------
|
||||||
|
|
||||||
|
Extra fields can also be added to the registration form that can receive arbitrary text.
|
||||||
|
Typically, this can be a "remark" field (prefer the LONGTEXT option in this case) or
|
||||||
|
small form entries such as "phone number" or "emergency contact" (prefer the SHORTTEXT
|
||||||
|
option in this case).
|
||||||
|
"""
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
@ -16,7 +47,9 @@ class Event(models.Model):
|
||||||
)
|
)
|
||||||
registration_open = models.BooleanField(_("inscriptions ouvertes"), default=True)
|
registration_open = models.BooleanField(_("inscriptions ouvertes"), default=True)
|
||||||
old = models.BooleanField(_("archiver (événement fini)"), default=False)
|
old = models.BooleanField(_("archiver (événement fini)"), default=False)
|
||||||
subscribers = models.ManyToManyField(User, verbose_name=_("inscrit⋅e⋅s"))
|
subscribers = models.ManyToManyField(
|
||||||
|
User, through="Registration", verbose_name=_("inscrit⋅e⋅s")
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = _("événement")
|
verbose_name = _("événement")
|
||||||
|
@ -26,8 +59,113 @@ class Event(models.Model):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
|
||||||
# TODO: gérer les options (EventOption & EventOptionChoice de gestioncof)
|
class Option(models.Model):
|
||||||
# par exemple: "option végé au Mega (oui / non)"
|
"""Extra form fields with a limited set of available choices.
|
||||||
|
|
||||||
# TODO: gérer les champs commentaires (EventCommentField & EventCommentChoice)
|
The available choices are given by `OptionChoice`s (see below). A typical use-case
|
||||||
# par exemple: "champ "allergies / régime particulier" au Mega
|
is events where the participants have the choice between different menus (e.g.
|
||||||
|
vegan / vegetarian / without-pork / etc).
|
||||||
|
"""
|
||||||
|
|
||||||
|
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="options")
|
||||||
|
name = models.CharField(_("option d'événement"), max_length=200)
|
||||||
|
multi_choices = models.BooleanField(_("choix multiples"), default=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("option d'événement")
|
||||||
|
verbose_name_plural = _("options d'événement")
|
||||||
|
unique_together = [["event", "name"]]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
class OptionChoice(models.Model):
|
||||||
|
"""A possible choice for an event option."""
|
||||||
|
|
||||||
|
option = models.ForeignKey(Option, on_delete=models.CASCADE, related_name="choices")
|
||||||
|
choice = models.CharField(_("choix"), max_length=200)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("choix d'option d'événement")
|
||||||
|
verbose_name_plural = _("choix d'option d'événement")
|
||||||
|
unique_together = [["option", "choice"]]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.choice
|
||||||
|
|
||||||
|
|
||||||
|
class ExtraField(models.Model):
|
||||||
|
"""Extra event field receiving arbitrary text.
|
||||||
|
|
||||||
|
Extra text field that can be added by event creators to the event registration form.
|
||||||
|
Typical examples are "remarks" fields (of type LONGTEXT) or more specific fields
|
||||||
|
such as "emergency contact" (of type SHORTTEXT probably?).
|
||||||
|
"""
|
||||||
|
|
||||||
|
LONGTEXT = "longtext"
|
||||||
|
SHORTTEXT = "shorttext"
|
||||||
|
|
||||||
|
FIELD_TYPE = [
|
||||||
|
(SHORTTEXT, _("texte court (une ligne)")),
|
||||||
|
(LONGTEXT, _("texte long (plusieurs lignes)")),
|
||||||
|
]
|
||||||
|
|
||||||
|
event = models.ForeignKey(
|
||||||
|
Event, on_delete=models.CASCADE, related_name="extra_fields"
|
||||||
|
)
|
||||||
|
name = models.CharField(_("champ d'événement supplémentaire"), max_length=200)
|
||||||
|
field_type = models.CharField(_("type de champ"), max_length=9, choices=FIELD_TYPE)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
unique_together = [["event", "name"]]
|
||||||
|
|
||||||
|
|
||||||
|
class ExtraFieldContent(models.Model):
|
||||||
|
"""Value entered in an extra field."""
|
||||||
|
|
||||||
|
field = models.ForeignKey(ExtraField, on_delete=models.CASCADE)
|
||||||
|
registration = models.ForeignKey(
|
||||||
|
"Registration", on_delete=models.CASCADE, related_name="extra_info"
|
||||||
|
)
|
||||||
|
content = models.TextField(_("contenu du champ"))
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if self.registration.event != self.field.event:
|
||||||
|
raise ValidationError(
|
||||||
|
_("Inscription et champ texte incohérents pour ce commentaire")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("contenu d'un champ événement supplémentaire")
|
||||||
|
verbose_name_plural = _("contenus d'un champ événement supplémentaire")
|
||||||
|
unique_together = [["field", "registration"]]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
max_length = 50
|
||||||
|
if len(self.content) > max_length:
|
||||||
|
return self.content[: max_length - 1] + "…"
|
||||||
|
else:
|
||||||
|
return self.content
|
||||||
|
|
||||||
|
|
||||||
|
class Registration(models.Model):
|
||||||
|
"""A user registration to an event."""
|
||||||
|
|
||||||
|
event = models.ForeignKey(Event, on_delete=models.CASCADE)
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
options_choices = models.ManyToManyField(OptionChoice)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
if not all((ch.option.event == self.event for ch in self.options_choices)):
|
||||||
|
raise ValidationError(
|
||||||
|
_("Choix d'options incohérents avec l'événement pour cette inscription")
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = _("inscription à un événement")
|
||||||
|
verbose_name_plural = _("inscriptions à un événement")
|
||||||
|
unique_together = [["event", "user"]]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "inscription de {} à {}".format(self.user, self.event)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import csv
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
@ -5,7 +6,14 @@ from django.contrib.auth.models import Permission
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from events.models import Event
|
from events.models import (
|
||||||
|
Event,
|
||||||
|
ExtraField,
|
||||||
|
ExtraFieldContent,
|
||||||
|
Option,
|
||||||
|
OptionChoice,
|
||||||
|
Registration,
|
||||||
|
)
|
||||||
|
|
||||||
User = get_user_model()
|
User = get_user_model()
|
||||||
|
|
||||||
|
@ -23,7 +31,7 @@ def make_staff_user(name):
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
class CSVExportTest(TestCase):
|
class MessagePatch:
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
# Signals handlers on login/logout send messages.
|
# Signals handlers on login/logout send messages.
|
||||||
# Due to the way the Django' test Client performs login, this raise an
|
# Due to the way the Django' test Client performs login, this raise an
|
||||||
|
@ -32,11 +40,14 @@ class CSVExportTest(TestCase):
|
||||||
patcher_messages.start()
|
patcher_messages.start()
|
||||||
self.addCleanup(patcher_messages.stop)
|
self.addCleanup(patcher_messages.stop)
|
||||||
|
|
||||||
|
|
||||||
|
class CSVExportAccessTest(MessagePatch, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
self.staff = make_staff_user("staff")
|
self.staff = make_staff_user("staff")
|
||||||
self.u1 = make_user("toto")
|
self.u1 = make_user("toto")
|
||||||
self.u2 = make_user("titi")
|
|
||||||
self.event = Event.objects.create(title="test_event", location="somewhere")
|
self.event = Event.objects.create(title="test_event", location="somewhere")
|
||||||
self.event.subscribers.set([self.u1, self.u2])
|
|
||||||
self.url = reverse("events:csv-participants", args=[self.event.id])
|
self.url = reverse("events:csv-participants", args=[self.event.id])
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
|
@ -57,3 +68,74 @@ class CSVExportTest(TestCase):
|
||||||
client.force_login(self.u1)
|
client.force_login(self.u1)
|
||||||
r = client.get(self.url)
|
r = client.get(self.url)
|
||||||
self.assertEqual(r.status_code, 403)
|
self.assertEqual(r.status_code, 403)
|
||||||
|
|
||||||
|
|
||||||
|
class CSVExportContentTest(MessagePatch, TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
|
||||||
|
self.event = Event.objects.create(title="test_event", location="somewhere")
|
||||||
|
self.url = reverse("events:csv-participants", args=[self.event.id])
|
||||||
|
|
||||||
|
self.u1 = User.objects.create_user(
|
||||||
|
username="toto_foo", first_name="toto", last_name="foo", email="toto@a.b"
|
||||||
|
)
|
||||||
|
self.u2 = User.objects.create_user(
|
||||||
|
username="titi_bar", first_name="titi", last_name="bar", email="titi@a.b"
|
||||||
|
)
|
||||||
|
self.staff = make_staff_user("staff")
|
||||||
|
self.client = Client()
|
||||||
|
self.client.force_login(self.staff)
|
||||||
|
|
||||||
|
def test_simple_event(self):
|
||||||
|
self.event.subscribers.set([self.u1, self.u2])
|
||||||
|
|
||||||
|
participants = self.client.get(self.url).content.decode("utf-8")
|
||||||
|
participants = [
|
||||||
|
line for line in csv.reader(participants.split("\n")) if line != []
|
||||||
|
]
|
||||||
|
self.assertEqual(len(participants), 3)
|
||||||
|
self.assertEqual(participants[1], ["toto_foo", "toto@a.b", "toto", "foo"])
|
||||||
|
self.assertEqual(participants[2], ["titi_bar", "titi@a.b", "titi", "bar"])
|
||||||
|
|
||||||
|
def test_complex_event(self):
|
||||||
|
registration = Registration.objects.create(event=self.event, user=self.u1)
|
||||||
|
# Set up some options
|
||||||
|
option1 = Option.objects.create(
|
||||||
|
event=self.event, name="abc", multi_choices=False
|
||||||
|
)
|
||||||
|
option2 = Option.objects.create(
|
||||||
|
event=self.event, name="def", multi_choices=True
|
||||||
|
)
|
||||||
|
OptionChoice.objects.bulk_create(
|
||||||
|
[
|
||||||
|
OptionChoice(option=option1, choice="a"),
|
||||||
|
OptionChoice(option=option1, choice="b"),
|
||||||
|
OptionChoice(option=option1, choice="c"),
|
||||||
|
OptionChoice(option=option2, choice="d"),
|
||||||
|
OptionChoice(option=option2, choice="e"),
|
||||||
|
OptionChoice(option=option2, choice="f"),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
registration.options_choices.set(
|
||||||
|
OptionChoice.objects.filter(choice__in=["d", "f"])
|
||||||
|
)
|
||||||
|
registration.options_choices.add(OptionChoice.objects.get(choice="a"))
|
||||||
|
# And an extra field
|
||||||
|
field = ExtraField.objects.create(event=self.event, name="remarks")
|
||||||
|
ExtraFieldContent.objects.create(
|
||||||
|
field=field, registration=registration, content="hello"
|
||||||
|
)
|
||||||
|
|
||||||
|
participants = self.client.get(self.url).content.decode("utf-8")
|
||||||
|
participants = list(csv.reader(participants.split("\n")))
|
||||||
|
toto_registration = participants[1]
|
||||||
|
|
||||||
|
# This is not super nice, but it makes the test deterministic.
|
||||||
|
if toto_registration[5] == "f & d":
|
||||||
|
toto_registration[5] = "d & f"
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"],
|
||||||
|
toto_registration,
|
||||||
|
)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.http import HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from events.models import Event
|
from events.models import Event, Registration
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -13,13 +13,43 @@ from events.models import Event
|
||||||
def participants_csv(request, event_id):
|
def participants_csv(request, event_id):
|
||||||
event = get_object_or_404(Event, id=event_id)
|
event = get_object_or_404(Event, id=event_id)
|
||||||
|
|
||||||
|
# Create a CSV response
|
||||||
filename = "{}-participants.csv".format(slugify(event.title))
|
filename = "{}-participants.csv".format(slugify(event.title))
|
||||||
response = HttpResponse(content_type="text/csv")
|
response = HttpResponse(content_type="text/csv")
|
||||||
response["Content-Disposition"] = 'attachment; filename="{}"'.format(filename)
|
response["Content-Disposition"] = 'attachment; filename="{}"'.format(filename)
|
||||||
|
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
writer.writerow(["username", "email", "prénom", "nom de famille"])
|
|
||||||
for user in event.subscribers.all():
|
# The first line of the file is a header
|
||||||
writer.writerow([user.username, user.email, user.first_name, user.last_name])
|
header = ["username", "email", "prénom", "nom de famille"]
|
||||||
|
options_names = list(event.options.values_list("name", flat=True).order_by("id"))
|
||||||
|
header += options_names
|
||||||
|
extra_fields = list(
|
||||||
|
event.extra_fields.values_list("name", flat=True).order_by("id")
|
||||||
|
)
|
||||||
|
header += extra_fields
|
||||||
|
writer.writerow(header)
|
||||||
|
|
||||||
|
# Next, one line by registered user
|
||||||
|
registrations = Registration.objects.filter(event=event)
|
||||||
|
for registration in registrations:
|
||||||
|
user = registration.user
|
||||||
|
row = [user.username, user.email, user.first_name, user.last_name]
|
||||||
|
|
||||||
|
# Options
|
||||||
|
all_choices = registration.options_choices.values_list("choice", flat=True)
|
||||||
|
options_choices = [
|
||||||
|
" & ".join(all_choices.filter(option__id=id))
|
||||||
|
for id in event.options.values_list("id", flat=True).order_by("id")
|
||||||
|
]
|
||||||
|
row += options_choices
|
||||||
|
# Extra info
|
||||||
|
extra_info = list(
|
||||||
|
registration.extra_info.values_list("content", flat=True).order_by(
|
||||||
|
"field__id"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
row += extra_info
|
||||||
|
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
Loading…
Reference in a new issue