Compare commits

..

1 commit

Author SHA1 Message Date
Martin Pépin e56123e5f7
Sitecof: always use absolute URLs
Remaining to do: change the 'sitecof' slug to 'news' in production.

By using absolute URLs we can prevent Daphne from adding the /gestion
prefix and serve the COF site under cof.ens.fr/news rather than
cof.ens.fr/gestion/sitecof
2020-08-30 15:20:37 +02:00
356 changed files with 3375 additions and 5620 deletions

1
.envrc
View file

@ -1 +0,0 @@
use nix

2
.gitignore vendored
View file

@ -5,7 +5,6 @@ cof/settings.py
settings.py settings.py
*~ *~
venv/ venv/
.venv/
.vagrant .vagrant
/src /src
media/ media/
@ -20,4 +19,3 @@ media/
# VSCode # VSCode
.vscode/ .vscode/
.direnv

View file

@ -17,15 +17,12 @@ variables:
# psql password authentication # psql password authentication
PGPASSWORD: $POSTGRES_PASSWORD PGPASSWORD: $POSTGRES_PASSWORD
# apps to check migrations for
MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared"
.test_template: .test_template:
before_script: before_script:
- mkdir -p vendor/{pip,apt} - mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py
# Remove the old test database if it has not been done yet # Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade -r requirements-prod.txt coverage tblib - pip install --upgrade -r requirements-prod.txt coverage tblib
@ -43,27 +40,19 @@ variables:
# Keep this disabled for now, as it may kill GitLab... # Keep this disabled for now, as it may kill GitLab...
# coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/'
kfettest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script:
- coverage run manage.py test kfet
coftest: coftest:
stage: test stage: test
extends: .test_template extends: .test_template
variables: variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" DJANGO_SETTINGS_MODULE: "cof.settings.cof_prod"
script: script:
- coverage run manage.py test gestioncof bda petitscours shared --parallel - coverage run manage.py test gestioncof bda kfet petitscours shared --parallel
bdstest: bdstest:
stage: test stage: test
extends: .test_template extends: .test_template
variables: variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod" DJANGO_SETTINGS_MODULE: "cof.settings.bds_prod"
script: script:
- coverage run manage.py test bds clubs events --parallel - coverage run manage.py test bds clubs events --parallel
@ -76,7 +65,7 @@ linters:
- black --check . - black --check .
- isort --check --diff . - isort --check --diff .
# Print errors only # Print errors only
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared
cache: cache:
key: linters key: linters
paths: paths:
@ -86,14 +75,14 @@ linters:
migration_checks: migration_checks:
stage: test stage: test
variables: variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.local" DJANGO_SETTINGS_MODULE: "cof.settings.local"
before_script: before_script:
- mkdir -p vendor/{pip,apt} - mkdir -p vendor/{pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev
- cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py - cp cof/settings/secret_example.py cof/settings/secret.py
- pip install --upgrade -r requirements-devel.txt - pip install --upgrade -r requirements-devel.txt
- python --version - python --version
script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS script: python manage.py makemigrations --dry-run --check
services: services:
# this should not be necessary… # this should not be necessary…
- postgres:11.7 - postgres:11.7

View file

@ -21,170 +21,12 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
adhérents ni des cotisations. adhérents ni des cotisations.
## TODO Prod ## Upcoming
- Lancer `python manage.py update_translation_fields` après la migration ### Site du COF
- Mettre à jour les units systemd `daphne.service` et `worker.service`
- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés
## Version ??? - ??/??/????
## Version 0.15.1 - 15/06/2023
### K-Fêt
- Rattrape les erreurs d'envoi de mail de négatif
- Utilise l'adresse chefs pour les envois de négatifs
## Version 0.15 - 22/05/2023
### K-Fêt
- Rajoute un formulaire de contact
- Rajoute un formulaire de demande de soirée
- Désactive les mails d'envoi de négatifs sur les comptes gelés
## Version 0.14 - 19/05/2023
- Répare les dépendances en spécifiant toutes les versions
### K-Fêt
- Répare la gestion des changement d'heure via moment.js
## Version 0.13 - 19/02/2023
### K-Fêt
- Rajoute la valeur des inventaires
- Résout les problèmes de négatif ne disparaissant pas
- Affiche son surnom s'il y en a un
- Bugfixes
## Version 0.12.1 - 03/10/2022
### K-Fêt
- Fixe un problème de rendu causé par l'agrandissement du menu
- Mise à jour vers Channels 3.x et Django 3.2
## Version 0.12 - 17/06/2022
### K-Fêt
- Ajoute une exception à la limite d'historique pour les comptes `LIQ` et `#13`
- Répare le problème des étiquettes LIQ/Comptes K-Fêt inversées dans les stats des articles K-Fêt
## Version 0.11 - 26/10/2021
### COF
- Répare un problème de rendu sur le wagtail du COF
### K-Fêt
- Ajoute de mails de rappels pour les comptes en négatif
- La recherche de comptes sur K-Psul remarche normalement
- Le pointeur de la souris change de forme quand on survole un item d'autocomplétion
- Modification du gel de compte:
- on ne peut plus geler/dégeler son compte soi-même (il faut la permission "Gérer les permissions K-Fêt")
- on ne peut rien compter sur un compte gelé (aucune override possible), et les K-Fêteux·ses dont le compte est gelé perdent tout accès à K-Psul
- les comptes actuellement gelés (sur l'ancien système) sont dégelés automatiquement
- Modification du fonctionnement des négatifs
- impossible d'avoir des négatifs inférieurs à `kfet_config.overdraft_amount`
- il n'y a plus de limite de temps sur les négatifs
- supression des autorisations de négatif
- il n'est plus possible de réinitialiser la durée d'un négatif en faisant puis en annulant une charge
- La gestion des erreurs passe du client au serveur, ce qui permet d'avoir des messages plus explicites
- La supression d'opérations anciennes est réparée
## Version 0.10 - 18/04/2021
### K-Fêt
- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à
la fois.
- L'interface indique plus clairement quand on fait une erreur en modifiant un
compte.
- On supprime la fonction "décalage de balance".
- L'accès à l'historique est maintenant limité à 7 jours pour raison de
confidentialité. Les chefs/trez peuvent disposer d'une permission
supplémentaire pour accéder à jusqu'à 30 jours en cas de problème de compta.
L'accès à son historique personnel n'est pas limité. Les durées sont
configurables dans `settings/cof_prod.py`.
### COF
- Le Captcha sur la page de demande de petits cours utilise maintenant hCaptcha
au lieu de ReCaptcha, pour mieux respecter la vie privée des utilisateur·ices
## Version 0.9 - 06/02/2020
### COF / BdA
- Le COF peut remettre à zéro la liste de ses adhérents en août (sans passer par
KDE).
- La page d'accueil affiche la date de fermeture des tirages BdA.
- On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes
ses places pour pouvoir revendre.
- On s'assure que l'email fourni lors d'une demande de petit cours est valide.
### BDS
- Le burô peut maintenant accorder ou révoquer le statut de membre du Burô
en modifiant le profil d'un membre du BDS.
- Le burô peut exporter la liste de ses membres avec email au format CSV depuis
la page d'accueil.
### K-Fêt
- On affiche les articles actuellement en vente en premier lors des inventaires
et des commandes.
- On peut supprimer un inventaire. Seuls les articles dont c'est le dernier
inventaire sont affectés.
## Version 0.8 - 03/12/2020
### COF
- La page "Mes places" dans la section BdA indique quelles places sont sur
listing.
- ergonomie de l'interface admin du BdA : moins d'options inutiles lors de
la sélection de participants.
- les tirages sont maintenant archivables pour éviter d'avoir encore d'autres
options inutiles.
- l'autocomplétion dans l'admin BdA est réparée.
- Les icones de la page de gestion des petits cours sont (à nouveau) réparées.
- On a supprimé la possibilité de modifier les mails automatiques depuis
l'interface admin car trop problématique. Faute de mieux, envoyer un mail à
KDE pour modifier ces mails.
- corrige un crash sporadique sur la page d'inscription au système de petits
cours
### K-Fêt
- (fix partiel) Empêche la K-Fêt de modifier des données COF (e.g. nom, prénom,
username) lors de la création d'un nouveau compte.
- Les statistiques de conso globales montrent deux courbes COF / non-COF au
lieu de LIQ / sur compte.
- Un bug empêchait de fermer manuellement la K-Fêt depuis un compte non
privilégié en tapant un mot de passe. C'est corrigé.
## Version 0.7.2 - 08/09/2020
- Nouvelle page 404
- Correction de bug en K-Fêt : le lien pour créer un nouveau compte exté apparaît
à nouveau dans l'autocomplétion
## Version 0.7.1 - 05/09/2020
Petits ajustements sur le site du COF :
- Possibilité d'ajouter des champs d'infos supplémentaires en plus de l'email et - Possibilité d'ajouter des champs d'infos supplémentaires en plus de l'email et
de la page web dans les annuaires (clubs et partenaires). de la page web dans les annuaires (clubs et partenaires).
- Corrige un bug d'affichage des adresses emails de clubs
## Version 0.7 - 29/08/2020 ## Version 0.7 - 29/08/2020

View file

@ -1,4 +1,4 @@
# GestioCOF / GestioBDS # GestioCOF
[![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) [![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
[![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) [![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master)
@ -38,11 +38,11 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier
pip install -U pip # parfois nécessaire la première fois pip install -U pip # parfois nécessaire la première fois
pip install -r requirements-devel.txt pip install -r requirements-devel.txt
Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique `cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier: pour profiter de façon transparente des mises à jour du fichier:
ln -s secret_example.py gestioasso/settings/secret.py ln -s secret_example.py cof/settings/secret.py
Nous avons un git hook de pre-commit pour formatter et vérifier que votre code Nous avons un git hook de pre-commit pour formatter et vérifier que votre code
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez

42
Vagrantfile vendored
View file

@ -1,19 +1,47 @@
# -*- mode: ruby -*- # -*- mode: ruby -*-
# vi: set ft=ruby : # vi: set ft=ruby :
# Configuration de base pour GestioCOF. # All Vagrant configuration is done below. The "2" in Vagrant.configure
# Voir https://docs.vagrantup.com pour plus d'informations. # configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.
Vagrant.configure(2) do |config| Vagrant.configure(2) do |config|
# On se base sur Debian 10 (Buster) pour avoir le même environnement qu'en # The most common configuration options are documented and commented below.
# production. # For a complete reference, please see the online documentation at
config.vm.box = "debian/contrib-buster64" # https://docs.vagrantup.com.
config.vm.box = "ubuntu/xenial64"
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre # On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
# ordinateur, et le port 8000 avec le port 8000. # ordinateur, et le port 8000 avec le port 8000.
config.vm.network :forwarded_port, guest: 80, host: 8080 config.vm.network :forwarded_port, guest: 80, host: 8080
config.vm.network :forwarded_port, guest: 8000, host: 8000 config.vm.network :forwarded_port, guest: 8000, host: 8000
# Le restes de la configuration (installation de paquets, etc) est géré un # Create a private network, which allows host-only access to the machine
# script shell. # using a specific IP.
# config.vm.network "private_network", ip: "192.168.33.10"
# Provider-specific configuration so you can fine-tune various
# backing providers for Vagrant. These expose provider-specific options.
# Example for VirtualBox:
#
# config.vm.provider "virtualbox" do |vb|
# # Display the VirtualBox GUI when booting the machine
# vb.gui = true
#
# # Customize the amount of memory on the VM:
# vb.memory = "1024"
# end
#
# View the documentation for the provider you are using for more
# information on available options.
# Enable provisioning with a shell script. Additional provisioners such as
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
# documentation for more information about their specific syntax and use.
# config.vm.provision "shell", inline: <<-SHELL
# sudo apt-get update
# sudo apt-get install -y apache2
# SHELL
config.vm.provision :shell, path: "provisioning/bootstrap.sh" config.vm.provision :shell, path: "provisioning/bootstrap.sh"
end end

View file

@ -1,11 +1,10 @@
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from dal.autocomplete import ModelSelect2 from dal.autocomplete import ModelSelect2
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.core.mail import send_mass_mail
from django.db.models import Count, Q, Sum from django.db.models import Count, Q, Sum
from django.template import loader
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
from django.utils import timezone from django.utils import timezone
@ -33,6 +32,20 @@ class ReadOnlyMixin(object):
return readonly_fields + self.readonly_fields_update return readonly_fields + self.readonly_fields_update
class ChoixSpectacleAdminForm(forms.ModelForm):
class Meta:
widgets = {
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
class ChoixSpectacleInline(admin.TabularInline):
model = ChoixSpectacle
form = ChoixSpectacleAdminForm
sortable_field_name = "priority"
class AttributionTabularAdminForm(forms.ModelForm): class AttributionTabularAdminForm(forms.ModelForm):
listing = None listing = None
@ -80,12 +93,9 @@ class WithoutListingAttributionInline(AttributionInline):
class ParticipantAdminForm(forms.ModelForm): class ParticipantAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
queryset = Spectacle.objects.select_related("location") self.fields["choicesrevente"].queryset = Spectacle.objects.select_related(
"location"
if self.instance.pk is not None: )
queryset = queryset.filter(tirage=self.instance.tirage)
self.fields["choicesrevente"].queryset = queryset
class ParticipantPaidFilter(admin.SimpleListFilter): class ParticipantPaidFilter(admin.SimpleListFilter):
@ -159,23 +169,19 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
form = ParticipantAdminForm form = ParticipantAdminForm
def send_attribs(self, request, queryset): def send_attribs(self, request, queryset):
emails = [] datatuple = []
for member in queryset.all(): for member in queryset.all():
subject = "Résultats du tirage au sort"
attribs = member.attributions.all() attribs = member.attributions.all()
context = {"member": member.user} context = {"member": member.user}
shortname = ""
template_name = ""
if len(attribs) == 0: if len(attribs) == 0:
template_name = "bda/mails/attributions-decus.txt" shortname = "bda-attributions-decus"
else: else:
template_name = "bda/mails/attributions.txt" shortname = "bda-attributions"
context["places"] = attribs context["places"] = attribs
print(context)
message = loader.render_to_string(template_name, context) datatuple.append((shortname, context, "bda@ens.fr", [member.user.email]))
emails.append((subject, message, "bda@ens.fr", [member.user.email])) send_mass_custom_mail(datatuple)
send_mass_mail(emails)
count = len(queryset.all()) count = len(queryset.all())
if count == 1: if count == 1:
message_bit = "1 membre a" message_bit = "1 membre a"
@ -191,6 +197,17 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
class AttributionAdminForm(forms.ModelForm): class AttributionAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "spectacle" in self.fields:
self.fields["spectacle"].queryset = Spectacle.objects.select_related(
"location"
)
if "participant" in self.fields:
self.fields["participant"].queryset = Participant.objects.select_related(
"user", "tirage"
)
def clean(self): def clean(self):
cleaned_data = super().clean() cleaned_data = super().clean()
participant = cleaned_data.get("participant") participant = cleaned_data.get("participant")
@ -203,14 +220,9 @@ class AttributionAdminForm(forms.ModelForm):
) )
return cleaned_data return cleaned_data
class Meta:
widgets = {
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
list_display = ("id", "spectacle", "participant", "given", "paid") list_display = ("id", "spectacle", "participant", "given", "paid")
search_fields = ( search_fields = (
"spectacle__title", "spectacle__title",
@ -223,7 +235,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
class ChoixSpectacleAdmin(admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin):
autocomplete_fields = ["participant", "spectacle"] form = ChoixSpectacleAdminForm
def tirage(self, obj): def tirage(self, obj):
return obj.participant.tirage return obj.participant.tirage
@ -267,14 +279,15 @@ class SalleAdmin(admin.ModelAdmin):
class SpectacleReventeAdminForm(forms.ModelForm): class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
qset = Participant.objects.select_related("user", "tirage") self.fields["confirmed_entry"].queryset = Participant.objects.select_related(
"user", "tirage"
if self.instance.pk is not None: )
qset = qset.filter(tirage=self.instance.seller.tirage) self.fields["seller"].queryset = Participant.objects.select_related(
"user", "tirage"
self.fields["confirmed_entry"].queryset = qset )
self.fields["seller"].queryset = qset self.fields["soldTo"].queryset = Participant.objects.select_related(
self.fields["soldTo"].queryset = qset "user", "tirage"
)
class SpectacleReventeAdmin(admin.ModelAdmin): class SpectacleReventeAdmin(admin.ModelAdmin):

View file

@ -2,6 +2,7 @@ import random
class Algorithm(object): class Algorithm(object):
shows = None shows = None
ranks = None ranks = None
origranks = None origranks = None

View file

@ -3,7 +3,7 @@ from django.forms.models import BaseInlineFormSet
from django.template import loader from django.template import loader
from django.utils import timezone from django.utils import timezone
from bda.models import SpectacleRevente from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet): class InscriptionInlineFormSet(BaseInlineFormSet):
@ -77,7 +77,7 @@ class ResellForm(forms.Form):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.fields["attributions"] = TemplateLabelField( self.fields["attributions"] = TemplateLabelField(
queryset=participant.attribution_set.filter( queryset=participant.attribution_set.filter(
spectacle__date__gte=timezone.now(), paid=True spectacle__date__gte=timezone.now()
) )
.exclude(revente__seller=participant) .exclude(revente__seller=participant)
.select_related("spectacle", "spectacle__location", "participant__user"), .select_related("spectacle", "spectacle__location", "participant__user"),

View file

@ -81,7 +81,7 @@ class Command(MyBaseCommand):
shows = random.sample( shows = random.sample(
list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2 list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2
) )
for rank, show in enumerate(shows): for (rank, show) in enumerate(shows):
choices.append( choices.append(
ChoixSpectacle( ChoixSpectacle(
participant=part, participant=part,

View file

@ -6,6 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [ operations = [

View file

@ -35,6 +35,7 @@ def fill_tirage_fields(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0001_initial")] dependencies = [("bda", "0001_initial")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0002_add_tirage")] dependencies = [("bda", "0002_add_tirage")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0003_update_tirage_and_spectacle")] dependencies = [("bda", "0003_update_tirage_and_spectacle")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0004_mails-rappel")] dependencies = [("bda", "0004_mails-rappel")]
operations = [ operations = [

View file

@ -18,6 +18,7 @@ def forwards_func(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0005_encoding")] dependencies = [("bda", "0005_encoding")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0006_add_tirage_switch")] dependencies = [("bda", "0006_add_tirage_switch")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0007_extends_spectacle")] dependencies = [("bda", "0007_extends_spectacle")]
operations = [ operations = [

View file

@ -6,6 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0008_py3")] dependencies = [("bda", "0008_py3")]
operations = [ operations = [

View file

@ -21,6 +21,7 @@ def forwards_func(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0009_revente")] dependencies = [("bda", "0009_revente")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0010_spectaclerevente_shotgun")] dependencies = [("bda", "0010_spectaclerevente_shotgun")]
operations = [ operations = [

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")] dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [ operations = [

View file

@ -13,6 +13,7 @@ def swap_double_choice(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0011_tirage_appear_catalogue")] dependencies = [("bda", "0011_tirage_appear_catalogue")]
operations = [ operations = [

View file

@ -6,6 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")] dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")]
operations = [] operations = []

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0013_merge_20180524_2123")] dependencies = [("bda", "0013_merge_20180524_2123")]
operations = [ operations = [

View file

@ -29,6 +29,7 @@ def set_participant_payment(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0014_attribution_paid_field")] dependencies = [("bda", "0014_attribution_paid_field")]
operations = [ operations = [

View file

@ -4,6 +4,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0015_move_bda_payment")] dependencies = [("bda", "0015_move_bda_payment")]
operations = [ operations = [

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bda", "0016_delete_participant_paid")] dependencies = [("bda", "0016_delete_participant_paid")]
operations = [ operations = [

View file

@ -1,37 +0,0 @@
# Generated by Django 2.2.12 on 2020-10-21 16:18
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bda", "0017_participant_accepte_charte"),
]
operations = [
migrations.AlterModelOptions(
name="participant",
options={"ordering": ("-tirage", "user__last_name", "user__first_name")},
),
migrations.AddField(
model_name="tirage",
name="archived",
field=models.BooleanField(default=False, verbose_name="Archivé"),
),
migrations.AlterField(
model_name="participant",
name="tirage",
field=models.ForeignKey(
limit_choices_to={"archived": False},
on_delete=django.db.models.deletion.CASCADE,
to="bda.Tirage",
),
),
migrations.AddConstraint(
model_name="participant",
constraint=models.UniqueConstraint(
fields=("tirage", "user"), name="unique_tirage"
),
),
]

View file

@ -1,23 +0,0 @@
# Generated by Django 3.2.13 on 2022-06-30 10:45
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bda", "0018_auto_20201021_1818"),
]
operations = [
migrations.AlterUniqueTogether(
name="choixspectacle",
unique_together=set(),
),
migrations.AddConstraint(
model_name="choixspectacle",
constraint=models.UniqueConstraint(
fields=("participant", "spectacle"), name="unique_participation"
),
),
]

View file

@ -2,14 +2,14 @@ import calendar
import random import random
from datetime import timedelta from datetime import timedelta
from custommail.models import CustomMail
from custommail.shortcuts import send_mass_custom_mail
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core import mail from django.core import mail
from django.core.mail import EmailMessage, send_mass_mail
from django.db import models from django.db import models
from django.db.models import Count, Exists from django.db.models import Count, Exists
from django.template import loader
from django.utils import formats, timezone from django.utils import formats, timezone
@ -31,7 +31,6 @@ class Tirage(models.Model):
"Tirage à afficher dans le catalogue", default=False "Tirage à afficher dans le catalogue", default=False
) )
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False) enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
archived = models.BooleanField("Archivé", default=False)
def __str__(self): def __str__(self):
return "%s - %s" % ( return "%s - %s" % (
@ -117,19 +116,16 @@ class Spectacle(models.Model):
bda_generic.nb_attr = 1 bda_generic.nb_attr = 1
members.append(bda_generic) members.append(bda_generic)
# On écrit un mail personnalisé à chaque participant # On écrit un mail personnalisé à chaque participant
mails = [ datatuple = [
( (
str(self), "bda-rappel",
loader.render_to_string( {"member": member, "nb_attr": member.nb_attr, "show": self},
"bda/mails/rappel.txt",
context={"member": member, "nb_attr": member.nb_attr, "show": self},
),
settings.MAIL_DATA["rappels"]["FROM"], settings.MAIL_DATA["rappels"]["FROM"],
[member.email], [member.email],
) )
for member in members for member in members
] ]
send_mass_mail(mails) send_mass_custom_mail(datatuple)
# On enregistre le fait que l'envoi a bien eu lieu # On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now() self.rappel_sent = timezone.now()
self.save() self.save()
@ -198,9 +194,7 @@ class Participant(models.Model):
attributions = models.ManyToManyField( attributions = models.ManyToManyField(
Spectacle, through="Attribution", related_name="attributed_to" Spectacle, through="Attribution", related_name="attributed_to"
) )
tirage = models.ForeignKey( tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
Tirage, on_delete=models.CASCADE, limit_choices_to={"archived": False}
)
accepte_charte = models.BooleanField("A accepté la charte BdA", default=False) accepte_charte = models.BooleanField("A accepté la charte BdA", default=False)
choicesrevente = models.ManyToManyField( choicesrevente = models.ManyToManyField(
Spectacle, related_name="subscribed", blank=True Spectacle, related_name="subscribed", blank=True
@ -211,12 +205,6 @@ class Participant(models.Model):
def __str__(self): def __str__(self):
return "%s - %s" % (self.user, self.tirage.title) return "%s - %s" % (self.user, self.tirage.title)
class Meta:
ordering = ("-tirage", "user__last_name", "user__first_name")
constraints = [
models.UniqueConstraint(fields=("tirage", "user"), name="unique_tirage"),
]
DOUBLE_CHOICES = ( DOUBLE_CHOICES = (
("1", "1 place"), ("1", "1 place"),
@ -253,11 +241,7 @@ class ChoixSpectacle(models.Model):
class Meta: class Meta:
ordering = ("priority",) ordering = ("priority",)
constraints = [ unique_together = (("participant", "spectacle"),)
models.UniqueConstraint(
fields=["participant", "spectacle"], name="unique_participation"
)
]
verbose_name = "voeu" verbose_name = "voeu"
verbose_name_plural = "voeux" verbose_name_plural = "voeux"
@ -364,24 +348,21 @@ class SpectacleRevente(models.Model):
BdA-Revente à tous les intéressés. BdA-Revente à tous les intéressés.
""" """
inscrits = self.attribution.spectacle.subscribed.select_related("user") inscrits = self.attribution.spectacle.subscribed.select_related("user")
mails = [ datatuple = [
( (
"BdA-Revente : {}".format(self.attribution.spectacle.title), "bda-revente",
loader.render_to_string( {
"bda/mails/revente-new.txt",
context={
"member": participant.user, "member": participant.user,
"show": self.attribution.spectacle, "show": self.attribution.spectacle,
"revente": self, "revente": self,
"site": Site.objects.get_current(), "site": Site.objects.get_current(),
}, },
),
settings.MAIL_DATA["revente"]["FROM"], settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email], [participant.user.email],
) )
for participant in inscrits for participant in inscrits
] ]
send_mass_mail(mails) send_mass_custom_mail(datatuple)
self.notif_sent = True self.notif_sent = True
self.notif_time = timezone.now() self.notif_time = timezone.now()
self.save() self.save()
@ -392,23 +373,20 @@ class SpectacleRevente(models.Model):
leur indiquer qu'il est désormais disponible au shotgun. leur indiquer qu'il est désormais disponible au shotgun.
""" """
inscrits = self.attribution.spectacle.subscribed.select_related("user") inscrits = self.attribution.spectacle.subscribed.select_related("user")
mails = [ datatuple = [
( (
"BdA-Revente : {}".format(self.attribution.spectacle.title), "bda-shotgun",
loader.render_to_string( {
"bda/mails/revente-shotgun.txt",
context={
"member": participant.user, "member": participant.user,
"show": self.attribution.spectacle, "show": self.attribution.spectacle,
"site": Site.objects.get_current(), "site": Site.objects.get_current(),
}, },
),
settings.MAIL_DATA["revente"]["FROM"], settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email], [participant.user.email],
) )
for participant in inscrits for participant in inscrits
] ]
send_mass_mail(mails) send_mass_custom_mail(datatuple)
self.notif_sent = True self.notif_sent = True
self.notif_time = timezone.now() self.notif_time = timezone.now()
# Flag inutile, sauf si l'horloge interne merde # Flag inutile, sauf si l'horloge interne merde
@ -440,30 +418,31 @@ class SpectacleRevente(models.Model):
"show": spectacle, "show": spectacle,
} }
subject = "BdA-Revente : {}".format(spectacle.title) c_mails_qs = CustomMail.objects.filter(
shortname__in=[
"bda-revente-winner",
"bda-revente-loser",
"bda-revente-seller",
]
)
c_mails = {cm.shortname: cm for cm in c_mails_qs}
mails.append( mails.append(
EmailMessage( c_mails["bda-revente-winner"].get_message(
subject=subject, context,
body=loader.render_to_string(
"bda/mails/revente-tirage-winner.txt",
context=context,
),
from_email=settings.MAIL_DATA["revente"]["FROM"], from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[winner.user.email], to=[winner.user.email],
) )
) )
mails.append( mails.append(
EmailMessage( c_mails["bda-revente-seller"].get_message(
subject=subject, context,
body=loader.render_to_string(
"bda/mails/revente-tirage-seller.txt",
context=context,
),
from_email=settings.MAIL_DATA["revente"]["FROM"], from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[seller.user.email], to=[seller.user.email],
reply_to=[winner.user.email], reply_to=[winner.user.email],
), )
) )
# Envoie un mail aux perdants # Envoie un mail aux perdants
@ -473,15 +452,11 @@ class SpectacleRevente(models.Model):
new_context["acheteur"] = inscrit.user new_context["acheteur"] = inscrit.user
mails.append( mails.append(
EmailMessage( c_mails["bda-revente-loser"].get_message(
subject=subject, new_context,
body=loader.render_to_string(
"bda/mails/revente-tirage-loser.txt",
context=new_context,
),
from_email=settings.MAIL_DATA["revente"]["FROM"], from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[inscrit.user.email], to=[inscrit.user.email],
), )
) )
mail_conn = mail.get_connection() mail_conn = mail.get_connection()

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}
<h2>État des inscriptions BdA</h2> <h2>État des inscriptions BdA</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script> <script type="text/javascript" src="{% static 'vendor/jquery/jquery-ui.min.js' %}" ></script>

View file

@ -26,6 +26,13 @@
<hr \> <hr \>
<p>
<em>Note :</em> le template de ce mail peut être modifié à
<a href="{% url 'admin:custommail_custommail_change' custommail.pk %}">cette adresse</a>
</p>
<hr \>
<h3>Forme des mails</h3> <h3>Forme des mails</h3>
<h4>Une seule place</h4> <h4>Une seule place</h4>

View file

@ -1,10 +0,0 @@
Cher-e {{ member.first_name }},
Tu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as
obtenu aucune place.
Nous proposons cependant de nombreuses offres hors-tirage tout au long de
l'année, et nous t'invitons à nous contacter si l'une d'entre elles
t'intéresse !
--
Le Bureau des Arts

View file

@ -1,31 +0,0 @@
Cher-e {{ member.first_name }},
Tu t'es inscrit-e pour le tirage au sort du BdA. Tu as été sélectionné-e
pour les spectacles suivants :
{% for place in places %}
- 1 place pour {{ place }}{% endfor %}
*Paiement*
L'intégralité de ces places de spectacles est à régler dès maintenant et AVANT
vendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi
entre 12h et 14h, et entre 18h et 20h). Des facilités de paiement sont bien
évidemment possibles : nous pouvons ne pas encaisser le chèque immédiatement,
ou bien découper votre paiement en deux fois. Pour ceux qui ne pourraient pas
venir payer au bureau, merci de nous contacter par mail.
*Mode de retrait des places*
Au moment du paiement, certaines places vous seront remises directement,
d'autres seront à récupérer au cours de l'année, d'autres encore seront
nominatives et à retirer le soir même dans les théâtres correspondants.
Pour chaque spectacle, vous recevrez un mail quelques jours avant la
représentation vous indiquant le mode de retrait.
Nous vous rappelons que l'obtention de places du BdA vous engage à
respecter les règles de fonctionnement :
https://bda.ens.fr/lequipe/charte-bda/
Un système de revente des places via les mails BdA-revente est disponible
directement sur votre compte GestioCOF.
En vous souhaitant de très beaux spectacles tout au long de l'année,
--
Le Bureau des Arts

View file

@ -1,23 +0,0 @@
Bonjour {{ member.first_name }},
Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }}
pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
{% if nb_attr == 2 %}
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
ces places sont strictement réservées aux personnes de moins de 28 ans.
{% endif %}
{% if show.listing %}Pour ce spectacle, tu as reçu {{ nb_attr|pluralize:"une place,des places" }} sur
listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
pour {{ nb_attr|pluralize:"la,les" }} retirer.
{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
été distribués au burô.
{% endif %}
Si tu ne peux plus assister à cette représentation, tu peux
revendre ta place via BdA-revente, accessible directement sur
GestioCOF (lien "revendre une place du premier tirage" sur la page
d'accueil https://www.cof.ens.fr/gestion/).
En te souhaitant un excellent spectacle,
--
Le Bureau des Arts

View file

@ -1,12 +0,0 @@
Bonjour {{ member.first_name }}
Une place pour le spectacle {{ show.title }} ({{ show.date }})
a été postée sur BdA-Revente.
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant
sur ce lien : https://{{ site }}{% url "bda-revente-confirm" revente.id %}.
Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à
un tirage au sort le {{ revente.date_tirage|date:"DATE_FORMAT" }}.
Chaleureusement,
Le BdA

View file

@ -1,13 +0,0 @@
Bonjour {{ vendeur.first_name }},
Tu tes bien inscrit·e pour revendre une place pour {{ show.title }}.
{% with revente.date_tirage as time %}
Le tirage au sort entre tout·e·s les racheteuse·eur·s potentiel·le·s aura lieu
le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}).
Si personne ne sest inscrit pour racheter la place, celle-ci apparaîtra parmi
les « Places disponibles immédiatement à la revente » sur GestioCOF.
{% endwith %}
Bonne revente !
Le Bureau des Arts

View file

@ -1,6 +0,0 @@
Bonjour {{ vendeur.first_name }} !
Je souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
Contacte-moi si tu es toujours intéressé·e !
{{ acheteur.get_full_name }} ({{ acheteur.email }})

View file

@ -1,11 +0,0 @@
Bonjour {{ member.first_name }}
Une place pour le spectacle {{ show.title }} ({{ show.date }})
a été postée sur BdA-Revente.
Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour
cette place : elle est disponible immédiatement à l'adresse
https://{{ site }}{% url "bda-revente-buy" show.id %}, à la disposition de tous.
Chaleureusement,
Le BdA

View file

@ -1,9 +0,0 @@
Bonjour {{ acheteur.first_name }},
Tu t'étais inscrit·e pour la revente de la place de {{ vendeur.get_full_name }}
pour {{ show.title }}.
Malheureusement, une autre personne a été tirée au sort pour racheter la place.
Tu pourras certainement retenter ta chance pour une autre revente !
À très bientôt,
Le Bureau des Arts

View file

@ -1,7 +0,0 @@
Bonjour {{ vendeur.first_name }},
La personne tirée au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.
Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail.
Chaleureusement,
Le BdA

View file

@ -1,7 +0,0 @@
Bonjour {{ acheteur.first_name }},
Tu as été tiré·e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€.
Tu peux contacter le/la vendeur·se à l'adresse {{ vendeur.email }}.
Chaleureusement,
Le BdA

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}
<h2>{{ spectacle }}</h2> <h2>{{ spectacle }}</h2>
@ -16,7 +16,7 @@
<tbody> <tbody>
{% for participant in participants %} {% for participant in participants %}
<tr> <tr>
<td data-sort-value="{{ participant.name}}">{{participant.name}}</td> <td data-sort-value="{{ participan.name}}">{{participant.name}}</td>
<td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td> <td data-sort-value="{{participant.nb_places}}">{{participant.nb_places}} place{{participant.nb_places|pluralize}}</td>
<td data-sort-value="{{participant.email}}">{{participant.email}}</td> <td data-sort-value="{{participant.email}}">{{participant.email}}</td>
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}> <td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>

View file

@ -10,7 +10,6 @@
<td>{{place.spectacle.location}}</td> <td>{{place.spectacle.location}}</td>
<td>{{place.spectacle.date}}</td> <td>{{place.spectacle.date}}</td>
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td> <td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
<td>{% if place.spectacle.listing %}sur listing{% else %}place physique{% endif %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{%block realcontent %} {%block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}
<h2>Inscription à une revente</h2> <h2>Inscription à une revente</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}

View file

@ -0,0 +1,6 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2><strong>Nope</strong></h2>
<p>Avant de revendre des places, il faut aller les payer !</p>
{% endblock %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles%}
{% block realcontent %} {% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2> <h2>Inscriptions pour BdA-Revente</h2>

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block realcontent %} {% block realcontent %}

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load static %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />

View file

@ -1,3 +1,7 @@
import os
from django.conf import settings
from django.core.management import call_command
from django.utils import timezone from django.utils import timezone
from shared.tests.mixins import ViewTestCaseMixin from shared.tests.mixins import ViewTestCaseMixin
@ -24,6 +28,12 @@ class BdATestHelpers:
if self.bda_testdata: if self.bda_testdata:
self.load_bda_testdata() self.load_bda_testdata()
def require_custommails(self):
data_file = os.path.join(
settings.BASE_DIR, "gestioncof", "management", "data", "custommail.json"
)
call_command("syncmails", data_file, verbosity=0)
def load_bda_testdata(self): def load_bda_testdata(self):
self.tirage = Tirage.objects.create( self.tirage = Tirage.objects.create(
title="Test tirage", title="Test tirage",

View file

@ -19,6 +19,8 @@ User = get_user_model()
class SpectacleReventeTests(TestCase): class SpectacleReventeTests(TestCase):
fixtures = ["gestioncof/management/data/custommail.json"]
def setUp(self): def setUp(self):
now = timezone.now() now = timezone.now()

View file

@ -29,7 +29,7 @@ class InscriptionViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/inscription/{}".format(self.tirage.id) return "/bda/inscription/{}".format(self.tirage.id)
def test_get_opened(self): def test_get_opened(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1) self.tirage.ouverture = timezone.now() - timedelta(days=1)
@ -149,7 +149,7 @@ class PlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/places/{}".format(self.tirage.id) return "/bda/places/{}".format(self.tirage.id)
class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@ -166,7 +166,7 @@ class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/etat-places/{}".format(self.tirage.id) return "/bda/etat-places/{}".format(self.tirage.id)
class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@ -185,7 +185,7 @@ class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/tirage/{}".format(self.tirage.id) return "/bda/tirage/{}".format(self.tirage.id)
def test_perform_tirage_disabled(self): def test_perform_tirage_disabled(self):
# Cannot be performed if disabled # Cannot be performed if disabled
@ -225,7 +225,7 @@ class SpectacleListViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/spectacles/{}".format(self.tirage.id) return "/bda/spectacles/{}".format(self.tirage.id)
class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@ -242,7 +242,7 @@ class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id) return "/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id)
class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@ -259,7 +259,7 @@ class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/spectacles/unpaid/{}".format(self.tirage.id) return "/bda/spectacles/unpaid/{}".format(self.tirage.id)
class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@ -276,9 +276,10 @@ class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
@property @property
def url_expected(self): def url_expected(self):
return "/gestion/bda/mails-rappel/{}".format(self.show1.id) return "/bda/mails-rappel/{}".format(self.show1.id)
def test_post(self): def test_post(self):
self.require_custommails()
resp = self.client.post(self.url) resp = self.client.post(self.url)
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
# TODO: check that emails are sent # TODO: check that emails are sent
@ -291,7 +292,7 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
bda_testdata = True bda_testdata = True
def test_api_list(self): def test_api_list(self):
url_list = "/gestion/bda/catalogue/list" url_list = "/bda/catalogue/list"
resp = self.client.get(url_list) resp = self.client.get(url_list)
self.assertJSONEqual( self.assertJSONEqual(
resp.content.decode("utf-8"), resp.content.decode("utf-8"),
@ -299,7 +300,7 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
) )
def test_api_details(self): def test_api_details(self):
url_details = "/gestion/bda/catalogue/details?id={}".format(self.tirage.id) url_details = "/bda/catalogue/details?id={}".format(self.tirage.id)
resp = self.client.get(url_details) resp = self.client.get(url_details)
self.assertJSONEqual( self.assertJSONEqual(
resp.content.decode("utf-8"), resp.content.decode("utf-8"),
@ -310,9 +311,7 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
) )
def test_api_descriptions(self): def test_api_descriptions(self):
url_descriptions = "/gestion/bda/catalogue/descriptions?id={}".format( url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id)
self.tirage.id
)
resp = self.client.get(url_descriptions) resp = self.client.get(url_descriptions)
raw = resp.content.decode("utf-8") raw = resp.content.decode("utf-8")
try: try:
@ -356,9 +355,7 @@ class TestReventeManageTest(TestCase):
def test_can_get(self): def test_can_get(self):
client = Client() client = Client()
client.force_login( client.force_login(self.user)
self.user, backend="django.contrib.auth.backends.ModelBackend"
)
r = client.get(self.url) r = client.get(self.url)
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)

View file

@ -1,80 +1,74 @@
from django.urls import re_path from django.conf.urls import url
from bda import views from bda import views
from bda.views import SpectacleListView from bda.views import SpectacleListView
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
urlpatterns = [ urlpatterns = [
re_path( url(
r"^inscription/(?P<tirage_id>\d+)$", r"^inscription/(?P<tirage_id>\d+)$",
views.inscription, views.inscription,
name="bda-tirage-inscription", name="bda-tirage-inscription",
), ),
re_path(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"), url(r"^places/(?P<tirage_id>\d+)$", views.places, name="bda-places-attribuees"),
re_path( url(r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places"),
r"^etat-places/(?P<tirage_id>\d+)$", views.etat_places, name="bda-etat-places" url(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
), url(
re_path(r"^tirage/(?P<tirage_id>\d+)$", views.tirage, name="bda-tirage"),
re_path(
r"^spectacles/(?P<tirage_id>\d+)$", r"^spectacles/(?P<tirage_id>\d+)$",
buro_required(SpectacleListView.as_view()), buro_required(SpectacleListView.as_view()),
name="bda-liste-spectacles", name="bda-liste-spectacles",
), ),
re_path( url(
r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$", r"^spectacles/(?P<tirage_id>\d+)/(?P<spectacle_id>\d+)$",
views.spectacle, views.spectacle,
name="bda-spectacle", name="bda-spectacle",
), ),
re_path( url(
r"^spectacles/unpaid/(?P<tirage_id>\d+)$", r"^spectacles/unpaid/(?P<tirage_id>\d+)$",
views.UnpaidParticipants.as_view(), views.UnpaidParticipants.as_view(),
name="bda-unpaid", name="bda-unpaid",
), ),
re_path( url(
r"^spectacles/autocomplete$", r"^spectacles/autocomplete$",
views.spectacle_autocomplete, views.spectacle_autocomplete,
name="bda-spectacle-autocomplete", name="bda-spectacle-autocomplete",
), ),
re_path( url(
r"^participants/autocomplete$", r"^participants/autocomplete$",
views.participant_autocomplete, views.participant_autocomplete,
name="bda-participant-autocomplete", name="bda-participant-autocomplete",
), ),
# Urls BdA-Revente # Urls BdA-Revente
re_path( url(
r"^revente/(?P<tirage_id>\d+)/manage$", r"^revente/(?P<tirage_id>\d+)/manage$",
views.revente_manage, views.revente_manage,
name="bda-revente-manage", name="bda-revente-manage",
), ),
re_path( url(
r"^revente/(?P<tirage_id>\d+)/subscribe$", r"^revente/(?P<tirage_id>\d+)/subscribe$",
views.revente_subscribe, views.revente_subscribe,
name="bda-revente-subscribe", name="bda-revente-subscribe",
), ),
re_path( url(
r"^revente/(?P<tirage_id>\d+)/tirages$", r"^revente/(?P<tirage_id>\d+)/tirages$",
views.revente_tirages, views.revente_tirages,
name="bda-revente-tirages", name="bda-revente-tirages",
), ),
re_path( url(
r"^revente/(?P<spectacle_id>\d+)/buy$", r"^revente/(?P<spectacle_id>\d+)/buy$",
views.revente_buy, views.revente_buy,
name="bda-revente-buy", name="bda-revente-buy",
), ),
re_path( url(
r"^revente/(?P<revente_id>\d+)/confirm$", r"^revente/(?P<revente_id>\d+)/confirm$",
views.revente_confirm, views.revente_confirm,
name="bda-revente-confirm", name="bda-revente-confirm",
), ),
re_path( url(
r"^revente/(?P<tirage_id>\d+)/shotgun$", r"^revente/(?P<tirage_id>\d+)/shotgun$",
views.revente_shotgun, views.revente_shotgun,
name="bda-revente-shotgun", name="bda-revente-shotgun",
), ),
re_path( url(r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels"),
r"^mails-rappel/(?P<spectacle_id>\d+)$", views.send_rappel, name="bda-rappels" url(r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"),
),
re_path(
r"^catalogue/(?P<request_type>[a-z]+)$", views.catalogue, name="bda-catalogue"
),
] ]

View file

@ -4,17 +4,17 @@ import random
import time import time
from collections import defaultdict from collections import defaultdict
from custommail.models import CustomMail
from custommail.shortcuts import send_custom_mail, send_mass_custom_mail
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.core import serializers from django.core import serializers
from django.core.exceptions import NON_FIELD_ERRORS from django.core.exceptions import NON_FIELD_ERRORS
from django.core.mail import send_mail, send_mass_mail
from django.db import transaction from django.db import transaction
from django.db.models import Count, Prefetch from django.db.models import Count, Prefetch
from django.forms.models import inlineformset_factory from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.template import loader
from django.template.defaultfilters import pluralize from django.template.defaultfilters import pluralize
from django.urls import reverse from django.urls import reverse
from django.utils import formats, timezone from django.utils import formats, timezone
@ -274,13 +274,13 @@ def do_tirage(tirage_elt, token):
results = Algorithm(data["shows"], data["members"], choices)(token) results = Algorithm(data["shows"], data["members"], choices)(token)
# On compte les places attribuées et les déçus # On compte les places attribuées et les déçus
for _, members, losers in results: for (_, members, losers) in results:
data["total_slots"] += len(members) data["total_slots"] += len(members)
data["total_losers"] += len(losers) data["total_losers"] += len(losers)
# On calcule le déficit et les bénéfices pour le BdA # On calcule le déficit et les bénéfices pour le BdA
# FIXME: le traitement de l'opéra est sale # FIXME: le traitement de l'opéra est sale
for show, members, _ in results: for (show, members, _) in results:
deficit = (show.slots - len(members)) * show.price deficit = (show.slots - len(members)) * show.price
data["total_sold"] += show.slots * show.price data["total_sold"] += show.slots * show.price
if deficit >= 0: if deficit >= 0:
@ -293,8 +293,8 @@ def do_tirage(tirage_elt, token):
# so assign a single object for each Participant id # so assign a single object for each Participant id
members_uniq = {} members_uniq = {}
members2 = {} members2 = {}
for show, members, _ in results: for (show, members, _) in results:
for member, _, _, _ in members: for (member, _, _, _) in members:
if member.id not in members_uniq: if member.id not in members_uniq:
members_uniq[member.id] = member members_uniq[member.id] = member
members2[member] = [] members2[member] = []
@ -385,6 +385,12 @@ def revente_manage(request, tirage_id):
user=request.user, tirage=tirage user=request.user, tirage=tirage
) )
# If the participant has just been created, the `paid` field is not
# automatically added by our custom ObjectManager. Skip the check in this
# scenario.
if not created and not participant.paid:
return render(request, "bda/revente/notpaid.html", {})
resellform = ResellForm(participant, prefix="resell") resellform = ResellForm(participant, prefix="resell")
annulform = AnnulForm(participant, prefix="annul") annulform = AnnulForm(participant, prefix="annul")
soldform = SoldForm(participant, prefix="sold") soldform = SoldForm(participant, prefix="sold")
@ -394,7 +400,7 @@ def revente_manage(request, tirage_id):
if "resell" in request.POST: if "resell" in request.POST:
resellform = ResellForm(participant, request.POST, prefix="resell") resellform = ResellForm(participant, request.POST, prefix="resell")
if resellform.is_valid(): if resellform.is_valid():
mails = [] datatuple = []
attributions = resellform.cleaned_data["attributions"] attributions = resellform.cleaned_data["attributions"]
with transaction.atomic(): with transaction.atomic():
for attribution in attributions: for attribution in attributions:
@ -409,17 +415,16 @@ def revente_manage(request, tirage_id):
"show": attribution.spectacle, "show": attribution.spectacle,
"revente": revente, "revente": revente,
} }
mails.append( datatuple.append(
( (
"BdA-Revente : {}".format(attribution.spectacle), "bda-revente-new",
loader.render_to_string( context,
"bda/mails/revente-seller.txt", context=context
),
settings.MAIL_DATA["revente"]["FROM"], settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email], [participant.user.email],
) )
) )
send_mass_mail(mails) revente.save()
send_mass_custom_mail(datatuple)
# On annule une revente # On annule une revente
elif "annul" in request.POST: elif "annul" in request.POST:
annulform = AnnulForm(participant, request.POST, prefix="annul") annulform = AnnulForm(participant, request.POST, prefix="annul")
@ -638,16 +643,12 @@ def revente_buy(request, spectacle_id):
"acheteur": request.user, "acheteur": request.user,
"vendeur": revente.seller.user, "vendeur": revente.seller.user,
} }
send_custom_mail(
send_mail( "bda-buy-shotgun",
"BdA-Revente : {}".format(spectacle.title), "bda@ens.fr",
loader.render_to_string(
"bda/mails/revente-shotgun-seller.txt", context=context
),
request.user.email,
[revente.seller.user.email], [revente.seller.user.email],
context=context,
) )
return render( return render(
request, request,
"bda/revente/mail-success.html", "bda/revente/mail-success.html",
@ -700,7 +701,7 @@ def spectacle(request, tirage_id, spectacle_id):
"username": participant.user.username, "username": participant.user.username,
"email": participant.user.email, "email": participant.user.email,
"given": int(attrib.given), "given": int(attrib.given),
"paid": attrib.paid, "paid": True,
"nb_places": 1, "nb_places": 1,
} }
if participant.id in participants: if participant.id in participants:
@ -750,21 +751,19 @@ class UnpaidParticipants(BuroRequiredMixin, ListView):
def send_rappel(request, spectacle_id): def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id) show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples # Mails d'exemples
subject = show.title custommail = CustomMail.objects.get(shortname="bda-rappel")
body_mail_1place = loader.render_to_string( exemple_mail_1place = custommail.render(
"bda/mails/rappel.txt", {"member": request.user, "show": show, "nb_attr": 1}
context={"member": request.user, "show": show, "nb_attr": 1},
) )
body_mail_2places = loader.render_to_string( exemple_mail_2places = custommail.render(
"bda/mails/rappel.txt", {"member": request.user, "show": show, "nb_attr": 2}
context={"member": request.user, "show": show, "nb_attr": 2},
) )
# Contexte # Contexte
ctxt = { ctxt = {
"show": show, "show": show,
"exemple_mail_1place": (subject, body_mail_1place), "exemple_mail_1place": exemple_mail_1place,
"exemple_mail_2places": (subject, body_mail_2places), "exemple_mail_2places": exemple_mail_2places,
"custommail": custommail,
} }
# Envoi confirmé # Envoi confirmé
if request.method == "POST": if request.method == "POST":

View file

@ -0,0 +1 @@
default_app_config = "bds.apps.BdsConfig"

View file

@ -1,4 +1,5 @@
from django.apps import AppConfig, apps as global_apps from django import apps as global_apps
from django.apps import AppConfig
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import post_migrate from django.db.models.signals import post_migrate

View file

@ -1,7 +1,6 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.forms import UserCreationForm
from django.utils.translation import gettext_lazy as _
from bds.models import BDSProfile from bds.models import BDSProfile
@ -9,8 +8,6 @@ User = get_user_model()
class UserForm(forms.ModelForm): class UserForm(forms.ModelForm):
is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False)
class Meta: class Meta:
model = User model = User
fields = ["email", "first_name", "last_name"] fields = ["email", "first_name", "last_name"]
@ -36,6 +33,4 @@ class ProfileForm(forms.ModelForm):
class Meta: class Meta:
model = BDSProfile model = BDSProfile
exclude = ["user"] exclude = ["user"]
widgets = { widgets = {"birthdate": forms.DateInput(attrs={"type": "date"})}
"birthdate": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d")
}

View file

@ -8,6 +8,7 @@ import bds.models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]

View file

@ -9,6 +9,7 @@ def create_bds_buro_group(apps, schema_editor):
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [("bds", "0001_initial")] dependencies = [("bds", "0001_initial")]
operations = [ operations = [

View file

@ -4,6 +4,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0002_bds_group"), ("bds", "0002_bds_group"),
] ]

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0003_staff_permission"), ("bds", "0003_staff_permission"),
] ]

View file

@ -4,6 +4,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0004_is_member_cotiz_type"), ("bds", "0004_is_member_cotiz_type"),
] ]

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
("bds", "0005_remove_bdsprofile_certificate_file"), ("bds", "0005_remove_bdsprofile_certificate_file"),
] ]

View file

@ -3,7 +3,6 @@ from os.path import splitext
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import models from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from shared.utils import choices_length from shared.utils import choices_length
@ -94,16 +93,6 @@ class BDSProfile(models.Model):
), ),
) )
@classmethod
def expired_members(cls):
now = timezone.now()
qs = cls.objects.filter(is_member=True)
if now.month > 1 and now.month < 7:
return qs.filter(cotisation_period="SE1")
elif now.month < 2 or now.month > 8:
return qs.none()
return qs
class Meta: class Meta:
verbose_name = _("Profil BDS") verbose_name = _("Profil BDS")
verbose_name_plural = _("Profils BDS") verbose_name_plural = _("Profils BDS")

View file

@ -1,4 +1,4 @@
{% load static %} {% load staticfiles %}
{% load bulma_utils %} {% load bulma_utils %}
<!DOCTYPE html> <!DOCTYPE html>

View file

@ -1,22 +0,0 @@
{% extends "bds/base.html" %}
{% block content %}
<h1 class="title">Liste des adhésions expirées</h1>
{% if object_list %}
<div class="content">
<ul>
{% for p in object_list %}
<li>{{ p.user.first_name }} {{ p.user.last_name }} ({{ p.user.username }}), {{ p.get_cotisation_period_display }}</li>
{% endfor %}
</ul>
</div>
<div class="buttons is-centered">
<a class="button is-danger" href="{% url 'bds:members.reset' %}">Réinitialiser les adhésions expirées</a>
</div>
{% endif %}
{% endblock %}

View file

@ -34,13 +34,6 @@
<br> <br>
<br> <br>
<a class=button href="{% url 'bds:export.members' %}">Télécharger la liste des membres (CSV)</a>
<a class=button href="{% url 'bds:members.expired' %}">Liste des adhésions expirées ({{ nb_expired }})</a>
<br>
<br>
Le site est encore en développement. Le site est encore en développement.
<br> <br>
Suivez notre avancement sur Suivez notre avancement sur

View file

@ -22,18 +22,6 @@ def login_url(next=None):
return "{}?next={}".format(login_url, next) return "{}?next={}".format(login_url, next)
class TestHomeView(TestCase):
@mock.patch("gestioncof.signals.messages")
def test_get(self, mock_messages):
user = User.objects.create_user(username="random_user")
give_bds_buro_permissions(user)
self.client.force_login(
user, backend="django.contrib.auth.backends.ModelBackend"
)
resp = self.client.get(reverse("bds:home"))
self.assertEqual(resp.status_code, 200)
class TestRegistrationView(TestCase): class TestRegistrationView(TestCase):
@mock.patch("gestioncof.signals.messages") @mock.patch("gestioncof.signals.messages")
def test_get_autocomplete(self, mock_messages): def test_get_autocomplete(self, mock_messages):
@ -46,14 +34,14 @@ class TestRegistrationView(TestCase):
self.assertRedirects(resp, login_url(next=url)) self.assertRedirects(resp, login_url(next=url))
# Logged-in but unprivileged GET # Logged-in but unprivileged GET
client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") client.force_login(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 403) self.assertEquals(resp.status_code, 403)
# Burô user GET # Burô user GET
give_bds_buro_permissions(user) give_bds_buro_permissions(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEquals(resp.status_code, 200)
@mock.patch("gestioncof.signals.messages") @mock.patch("gestioncof.signals.messages")
def test_get(self, mock_messages): def test_get(self, mock_messages):
@ -66,11 +54,11 @@ class TestRegistrationView(TestCase):
self.assertRedirects(resp, login_url(next=url)) self.assertRedirects(resp, login_url(next=url))
# Logged-in but unprivileged GET # Logged-in but unprivileged GET
client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") client.force_login(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 403) self.assertEquals(resp.status_code, 403)
# Burô user GET # Burô user GET
give_bds_buro_permissions(user) give_bds_buro_permissions(user)
resp = client.get(url) resp = client.get(url)
self.assertEqual(resp.status_code, 200) self.assertEquals(resp.status_code, 200)

View file

@ -14,11 +14,4 @@ urlpatterns = [
name="user.create.fromclipper", name="user.create.fromclipper",
), ),
path("user/delete/<int:pk>", views.UserDeleteView.as_view(), name="user.delete"), path("user/delete/<int:pk>", views.UserDeleteView.as_view(), name="user.delete"),
path("members", views.export_members, name="export.members"),
path(
"members/expired",
views.ResetMembershipListView.as_view(),
name="members.expired",
),
path("members/reset", views.ResetMembershipView.as_view(), name="members.reset"),
] ]

View file

@ -1,14 +1,9 @@
import csv
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import Permission
from django.http import HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import DeleteView, ListView, RedirectView, TemplateView from django.views.generic import DeleteView, TemplateView
from bds.autocomplete import bds_search from bds.autocomplete import bds_search
from bds.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm from bds.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm
@ -30,7 +25,6 @@ class Home(StaffRequiredMixin, TemplateView):
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context["member_count"] = BDSProfile.objects.filter(is_member=True).count() context["member_count"] = BDSProfile.objects.filter(is_member=True).count()
context["nb_expired"] = BDSProfile.expired_members().count()
return context return context
@ -42,9 +36,6 @@ class UserUpdateView(StaffRequiredMixin, MultipleFormView):
"profile": ProfileForm, "profile": ProfileForm,
} }
def get_user_initial(self):
return {"is_buro": self.get_user_instance().has_perm("bds.is_team")}
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
self.user = get_object_or_404(User, pk=self.kwargs["pk"]) self.user = get_object_or_404(User, pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)
@ -61,11 +52,6 @@ class UserUpdateView(StaffRequiredMixin, MultipleFormView):
def form_valid(self, forms): def form_valid(self, forms):
user = forms["user"].save() user = forms["user"].save()
profile = forms["profile"].save(commit=False) profile = forms["profile"].save(commit=False)
perm = Permission.objects.get(content_type__app_label="bds", codename="is_team")
if forms["user"].cleaned_data["is_buro"]:
user.user_permissions.add(perm)
else:
user.user_permissions.remove(perm)
profile.user = user profile.user = user
profile.save() profile.save()
messages.success(self.request, _("Profil mis à jour avec succès !")) messages.success(self.request, _("Profil mis à jour avec succès !"))
@ -142,43 +128,3 @@ class UserDeleteView(StaffRequiredMixin, DeleteView):
messages.success(request, self.success_message) messages.success(request, self.success_message)
return super().delete(request, *args, **kwargs) return super().delete(request, *args, **kwargs)
class ResetMembershipListView(StaffRequiredMixin, ListView):
model = BDSProfile
template_name = "bds/expired_members.html"
def get_queryset(self):
return BDSProfile.expired_members()
class ResetMembershipView(StaffRequiredMixin, RedirectView):
url = reverse_lazy("bds:members.expired")
def get(self, request, *args, **kwargs):
qs = BDSProfile.expired_members()
nb = qs.count()
qs.update(cotisation_period="NO", is_member=False, mails_bds=False)
messages.success(request, f"{nb} adhésions réinitialisées")
return super().get(request, *args, **kwargs)
@permission_required("bds.is_team")
def export_members(request):
response = HttpResponse(content_type="text/csv")
response["Content-Disposition"] = "attachment; filename=membres_bds.csv"
writer = csv.writer(response)
for profile in BDSProfile.objects.filter(is_member=True).all():
user = profile.user
bits = [
user.username,
user.get_full_name(),
user.email,
]
writer.writerow([str(bit) for bit in bits])
return response

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]

8
cof/asgi.py Normal file
View file

@ -0,0 +1,8 @@
import os
from channels.asgi import get_channel_layer
if "DJANGO_SETTINGS_MODULE" not in os.environ:
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
channel_layer = get_channel_layer()

3
cof/routing.py Normal file
View file

@ -0,0 +1,3 @@
from channels.routing import include
routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")]

View file

@ -1,9 +1,7 @@
""" """
Settings de production de GestioBDS. Django development settings for the cof project.
The settings that are not listed here are imported from .common
Surcharge les settings définis dans common.py
""" """
from .common import * # NOQA from .common import * # NOQA
from .common import INSTALLED_APPS from .common import INSTALLED_APPS

View file

@ -1,13 +1,8 @@
""" """
Settings de production de GestioCOF. Django development settings for the cof project.
The settings that are not listed here are imported from .common
Surcharge les settings définis dans common.py
""" """
import os import os
from datetime import timedelta
from django.utils import timezone
from .common import * # NOQA from .common import * # NOQA
from .common import ( from .common import (
@ -28,8 +23,8 @@ REDIS_DB = import_secret("REDIS_DB")
REDIS_HOST = import_secret("REDIS_HOST") REDIS_HOST = import_secret("REDIS_HOST")
REDIS_PORT = import_secret("REDIS_PORT") REDIS_PORT = import_secret("REDIS_PORT")
HCAPTCHA_SITEKEY = import_secret("HCAPTCHA_SITEKEY") RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
HCAPTCHA_SECRET = import_secret("HCAPTCHA_SECRET") RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
# --- # ---
@ -52,10 +47,11 @@ INSTALLED_APPS = (
+ [ + [
"bda", "bda",
"petitscours", "petitscours",
"hcaptcha", "captcha",
"kfet", "kfet",
"kfet.open", "kfet.open",
"channels", "channels",
"custommail",
"djconfig", "djconfig",
"wagtail.contrib.forms", "wagtail.contrib.forms",
"wagtail.contrib.redirects", "wagtail.contrib.redirects",
@ -85,6 +81,7 @@ MIDDLEWARE = (
+ MIDDLEWARE + MIDDLEWARE
+ [ + [
"djconfig.middleware.DjConfigMiddleware", "djconfig.middleware.DjConfigMiddleware",
"wagtail.core.middleware.SiteMiddleware",
"wagtail.contrib.redirects.middleware.RedirectMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware",
] ]
) )
@ -108,23 +105,15 @@ MEDIA_URL = "/gestion/media/"
CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr")
ASGI_APPLICATION = "gestioasso.routing.application"
# --- # ---
# Auth-related stuff # Auth-related stuff
# --- # ---
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS += [
[
# Must be in first
"kfet.auth.backends.BlockFrozenAccountBackend"
]
+ AUTHENTICATION_BACKENDS
+ [
"gestioncof.shared.COFCASBackend", "gestioncof.shared.COFCASBackend",
"kfet.auth.backends.GenericBackend", "kfet.auth.backends.GenericBackend",
] ]
)
LOGIN_URL = "cof-login" LOGIN_URL = "cof-login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
@ -148,7 +137,7 @@ CACHES = {
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "shared.channels.ChannelLayer", "BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": { "CONFIG": {
"hosts": [ "hosts": [
( (
@ -161,9 +150,11 @@ CHANNEL_LAYERS = {
) )
] ]
}, },
"ROUTING": "cof.routing.routing",
} }
} }
# --- # ---
# reCAPTCHA settings # reCAPTCHA settings
# https://github.com/praekelt/django-recaptcha # https://github.com/praekelt/django-recaptcha
@ -205,29 +196,8 @@ MAIL_DATA = {
"REPLYTO": "cof@ens.fr", "REPLYTO": "cof@ens.fr",
}, },
"rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"}, "rappels": {"FROM": "Le BdA <bda@ens.fr>", "REPLYTO": "Le BdA <bda@ens.fr>"},
"kfet": {
"FROM": "La K-Fêt <chefs-k-fet@ens.fr>",
"REPLYTO": "La K-Fêt <chefs-k-fet@ens.fr>",
},
"revente": { "revente": {
"FROM": "BdA-Revente <bda-revente@ens.fr>", "FROM": "BdA-Revente <bda-revente@ens.fr>",
"REPLYTO": "BdA-Revente <bda-revente@ens.fr>", "REPLYTO": "BdA-Revente <bda-revente@ens.fr>",
}, },
} }
# ---
# kfet history limits
# ---
# L'historique n'est accesible que d'aujourd'hui
# à aujourd'hui - KFET_HISTORY_DATE_LIMIT
KFET_HISTORY_DATE_LIMIT = timedelta(days=7)
# Limite plus longue pour les chefs/trez
# (qui ont la permission kfet.access_old_history)
KFET_HISTORY_LONG_DATE_LIMIT = timedelta(days=30)
# These accounts don't represent actual people and can be freely accessed
# Identification based on trigrammes
KFET_HISTORY_NO_DATE_LIMIT_TRIGRAMMES = ["LIQ", "#13"]
KFET_HISTORY_NO_DATE_LIMIT = timezone.datetime(1794, 10, 30) # AKA the distant past

View file

@ -1,5 +1,8 @@
""" """
Settings par défaut et settings communs à GestioCOF et GestioBDS. Django common settings for cof project.
Everything which is supposed to be identical between the production server and
the local development server should be here.
""" """
import os import os
@ -62,7 +65,7 @@ INSTALLED_APPS = [
"django.contrib.messages", "django.contrib.messages",
"django.contrib.admin", "django.contrib.admin",
"django.contrib.admindocs", "django.contrib.admindocs",
"gestioasso.apps.IgnoreSrcStaticFilesConfig", "cof.apps.IgnoreSrcStaticFilesConfig",
"django_cas_ng", "django_cas_ng",
"bootstrapform", "bootstrapform",
"widget_tweaks", "widget_tweaks",
@ -79,7 +82,7 @@ MIDDLEWARE = [
"django.middleware.locale.LocaleMiddleware", "django.middleware.locale.LocaleMiddleware",
] ]
ROOT_URLCONF = "gestioasso.urls" ROOT_URLCONF = "cof.urls"
TEMPLATES = [ TEMPLATES = [
{ {
@ -101,7 +104,7 @@ TEMPLATES = [
DATABASES = { DATABASES = {
"default": { "default": {
"ENGINE": "django.db.backends.postgresql", "ENGINE": "django.db.backends.postgresql_psycopg2",
"NAME": DBNAME, "NAME": DBNAME,
"USER": DBUSER, "USER": DBUSER,
"PASSWORD": DBPASSWD, "PASSWORD": DBPASSWD,
@ -111,7 +114,6 @@ DATABASES = {
SITE_ID = 1 SITE_ID = 1
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
# --- # ---
# Internationalization # Internationalization
@ -124,7 +126,7 @@ USE_I18N = True
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
LANGUAGES = (("fr", "Français"), ("en", "English")) LANGUAGES = (("fr", "Français"), ("en", "English"))
FORMAT_MODULE_PATH = "gestioasso.locale" FORMAT_MODULE_PATH = "cof.locale"
# --- # ---

View file

@ -1,9 +1,4 @@
""" """Django local development settings."""
Settings utilisés lors d'un développement en local (dans un virtualenv).
Active toutes les applications (de GestioCOF et de GestioBDS).
Surcharge les settings définis dans common.py
"""
import os import os
from . import bds_prod from . import bds_prod
@ -47,7 +42,8 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"
# Use the default in memory asgi backend for local development # Use the default in memory asgi backend for local development
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "channels.layers.InMemoryChannelLayer", "BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "cof.routing.routing",
} }
} }

View file

@ -1,7 +1,3 @@
"""
Secrets à re-définir en production.
"""
SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah" SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah"
ADMINS = None ADMINS = None
SERVER_EMAIL = "root@vagrant" SERVER_EMAIL = "root@vagrant"
@ -16,8 +12,8 @@ REDIS_PORT = 6379
REDIS_DB = 0 REDIS_DB = 0
REDIS_HOST = "127.0.0.1" REDIS_HOST = "127.0.0.1"
HCAPTCHA_SITEKEY = "10000000-ffff-ffff-ffff-000000000001" RECAPTCHA_PUBLIC_KEY = "DUMMY"
HCAPTCHA_SECRET = "0x0000000000000000000000000000000000000000" RECAPTCHA_PRIVATE_KEY = "DUMMY"
EMAIL_HOST = None EMAIL_HOST = None

166
cof/urls.py Normal file
View file

@ -0,0 +1,166 @@
"""
Fichier principal de configuration des urls du projet GestioCOF
"""
from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.conf.urls.static import static
from django.contrib import admin
from django.contrib.auth import views as django_auth_views
from django.urls import include, path
from django.views.generic.base import TemplateView
from django_cas_ng import views as django_cas_views
admin.autodiscover()
urlpatterns = [
path("admin/doc/", include("django.contrib.admindocs.urls")),
path("admin/", admin.site.urls),
]
if "gestioncof" in settings.INSTALLED_APPS:
from django_js_reverse.views import urls_js
from wagtail.admin import urls as wagtailadmin_urls
from wagtail.documents import urls as wagtaildocs_urls
from gestioncof import csv_views, views as gestioncof_views
from gestioncof.urls import (
calendar_patterns,
clubs_patterns,
events_patterns,
export_patterns,
surveys_patterns,
)
# Also includes BdA, K-Fêt, etc.
urlpatterns += [
path("admin/logout/", gestioncof_views.logout),
path(
"admin/<slug:app_label>/<slug:model_name>/csv/",
csv_views.admin_list_export,
{"fields": ["username"]},
),
# Page d'accueil
path("", gestioncof_views.HomeView.as_view(), name="home"),
# Le BdA
path("bda/", include("bda.urls")),
# Les exports
path("export/", include(export_patterns)),
# Les petits cours
path("petitcours/", include("petitscours.urls")),
# Les sondages
path("survey/", include(surveys_patterns)),
# Evenements
path("event/", include(events_patterns)),
# Calendrier
path("calendar/", include(calendar_patterns)),
# Clubs
path("clubs/", include(clubs_patterns)),
# Authentification
path(
"cof/denied",
TemplateView.as_view(template_name="cof-denied.html"),
name="cof-denied",
),
path("cas/login", django_cas_views.LoginView.as_view(), name="cas_login_view"),
path("cas/logout", django_cas_views.LogoutView.as_view()),
path(
"outsider/login",
gestioncof_views.LoginExtView.as_view(),
name="ext_login_view",
),
path(
"outsider/logout",
django_auth_views.LogoutView.as_view(),
{"next_page": "home"},
),
path("login", gestioncof_views.login, name="cof-login"),
path("logout", gestioncof_views.logout, name="cof-logout"),
# Infos persos
path("profile", gestioncof_views.profile, name="profile"),
path(
"outsider/password-change",
django_auth_views.PasswordChangeView.as_view(),
name="password_change",
),
path(
"outsider/password-change-done",
django_auth_views.PasswordChangeDoneView.as_view(),
name="password_change_done",
),
# Inscription d'un nouveau membre
path("registration", gestioncof_views.registration, name="registration"),
path(
"registration/clipper/<slug:login_clipper>/<fullname>",
gestioncof_views.registration_form2,
name="clipper-registration",
),
path(
"registration/user/<username>",
gestioncof_views.registration_form2,
name="user-registration",
),
path(
"registration/empty",
gestioncof_views.registration_form2,
name="empty-registration",
),
# Autocompletion
path(
"autocomplete/registration",
gestioncof_views.RegistrationAutocompleteView.as_view(),
name="cof.registration.autocomplete",
),
path(
"user/autocomplete",
gestioncof_views.UserAutocompleteView.as_view(),
name="cof-user-autocomplete",
),
# Liens utiles du COF et du BdA
path("utile_cof", gestioncof_views.utile_cof, name="utile_cof"),
path("utile_bda", gestioncof_views.utile_bda, name="utile_bda"),
path("utile_bda/bda_diff", gestioncof_views.liste_bdadiff, name="ml_diffbda"),
path("utile_cof/diff_cof", gestioncof_views.liste_diffcof, name="ml_diffcof"),
path(
"utile_bda/bda_revente",
gestioncof_views.liste_bdarevente,
name="ml_bda_revente",
),
path("k-fet/", include("kfet.urls")),
path("cms/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_urls)),
# djconfig
path("config", gestioncof_views.ConfigUpdate.as_view(), name="config.edit"),
# js-reverse
path("jsreverse/", urls_js, name="js_reverse"),
]
if "bds" in settings.INSTALLED_APPS:
urlpatterns.append(path("bds/", include("bds.urls")))
if "events" in settings.INSTALLED_APPS:
# The new event application is still in development
# → for now it is namespaced below events_v2
# → rename this when the old events system is out
urlpatterns += [path("event_v2/", include("events.urls"))]
if "authens" in settings.INSTALLED_APPS:
urlpatterns.append(path("authens/", include("authens.urls")))
if "debug_toolbar" in settings.INSTALLED_APPS:
import debug_toolbar
urlpatterns += [path("__debug__/", include(debug_toolbar.urls))]
if settings.DEBUG:
# Si on est en production, MEDIA_ROOT est servi par Apache.
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
# Wagtail for uncatched
if "wagtail.core" in settings.INSTALLED_APPS:
from wagtail.core import urls as wagtail_urls
urlpatterns += i18n_patterns(
path("", include(wagtail_urls)), prefix_default_language=False
)

View file

@ -2,5 +2,5 @@ import os
from django.core.wsgi import get_wsgi_application from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.bds_prod") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings.bds_prod")
application = get_wsgi_application() application = get_wsgi_application()

View file

@ -4,6 +4,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
initial = True initial = True
dependencies = [] dependencies = []

View file

@ -5,6 +5,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("events", "0001_event"), ("events", "0001_event"),

View file

@ -6,6 +6,7 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("events", "0002_event_subscribers"), ("events", "0002_event_subscribers"),

View file

@ -5,6 +5,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL), migrations.swappable_dependency(settings.AUTH_USER_MODEL),
("events", "0003_options_and_extra_fields"), ("events", "0003_options_and_extra_fields"),

View file

@ -1,63 +0,0 @@
# Generated by Django 3.2.13 on 2022-06-30 10:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("events", "0004_unique_constraints"),
]
operations = [
migrations.AlterUniqueTogether(
name="extrafield",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="extrafieldcontent",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="option",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="optionchoice",
unique_together=set(),
),
migrations.AlterUniqueTogether(
name="registration",
unique_together=set(),
),
migrations.AddConstraint(
model_name="extrafield",
constraint=models.UniqueConstraint(
fields=("event", "name"), name="unique_extra_field"
),
),
migrations.AddConstraint(
model_name="extrafieldcontent",
constraint=models.UniqueConstraint(
fields=("field", "registration"), name="unique_extra_field_content"
),
),
migrations.AddConstraint(
model_name="option",
constraint=models.UniqueConstraint(
fields=("event", "name"), name="unique_event_option"
),
),
migrations.AddConstraint(
model_name="optionchoice",
constraint=models.UniqueConstraint(
fields=("option", "choice"), name="unique_option_choice"
),
),
migrations.AddConstraint(
model_name="registration",
constraint=models.UniqueConstraint(
fields=("event", "user"), name="unique_registration"
),
),
]

Some files were not shown because too many files have changed in this diff Show more