Events are configurable

This commit mostly reproduces the structure of gestioncof's events,
renames some stuff and adds a generic export view.
This commit is contained in:
Martin Pépin 2019-12-22 21:27:28 +01:00
parent 6e9dc03bc7
commit d5e9d09044
4 changed files with 407 additions and 14 deletions

View 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",
),
),
]

View file

@ -16,7 +16,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 +28,91 @@ 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)" """Event options to be selected by participants at registration.
# TODO: gérer les champs commentaires (EventCommentField & EventCommentChoice) The possible choices are instances of `OptionChoice` (see below). A typical example
# par exemple: "champ "allergies / régime particulier" au Mega is when the participants have the choice between different meal types (e.g. vegan /
vegetarian / no pork / with meat). In this case, the "meal type" is an `Option` and
the three alternatives are `OptionChoice`s.
"""
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")
def __str__(self):
return self.name
class OptionChoice(models.Model):
"""A possible choice for an event option (see 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")
def __str__(self):
return self.choice
class ExtraField(models.Model):
"""Extra event field, for event creators.
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 questions
such as "emergency contact".
"""
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 ExtraFieldContent(models.Model):
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"))
class Meta:
verbose_name = _("contenu d'un champ événement supplémentaire")
verbose_name_plural = _("contenus d'un champ événement supplémentaire")
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):
event = models.ForeignKey(Event, on_delete=models.CASCADE)
user = models.ForeignKey(User, on_delete=models.CASCADE)
options_choices = models.ManyToManyField(OptionChoice)
class Meta:
verbose_name = _("inscription à un événement")
verbose_name_plural = _("inscriptions à un événement")
def __str__(self):
return "inscription de {} à {}".format(self.user, self.event)

View file

@ -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,68 @@ 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")))
self.assertEqual(
["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"],
participants[1],
)

View file

@ -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,46 @@ 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
options_choices = list(
" & ".join(
registration.options_choices.filter(option__id=id).values_list(
"choice", flat=True
)
)
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