Compare commits

..

4 commits

Author SHA1 Message Date
Aurélien Delobelle
a4be431c4f core.ci -- Add dependencies for LDAP stuff 2018-10-21 17:14:10 +02:00
Aurélien Delobelle
030a02375c Fix & clean login/logout urls
- Use the right slug for kfet root page.
- Redirection url of login and generic login now contains the query
string of the current request.
2018-10-21 17:09:12 +02:00
Aurélien Delobelle
e56200a569 kfet -- LoginGenericView directly disconnects users.
Since allauth is installed, users are not automatically logged out of CAS
when logging out GestioCOF.
This change simplifies the view and avoid being stuck because of
the redirect to the logout page, which happened via a GET request and so
prompting to confirm.
2018-10-21 17:09:12 +02:00
Aurélien Delobelle
05eeb6a25c core -- Install django-allauth-ens
Refer to allauth doc for an accurate features list:
  http://django-allauth.readthedocs.io/en/latest/

Users can now change their password, ask for a password reset, or set
one if they don't have one.

In particular, it allows users whose account has been created via a
clipper authentication to configure a password before losing their
clipper. Even if they have already lost it, they are able to get one
using the "Reset password" functionality.

Allauth multiple emails management is deactivated. Requests to the
related url redirect to the home page.

All the login and logout views are replaced by the allauth' ones. It
also concerns the Django and Wagtail admin sites.

Note that users are no longer logged out of the clipper CAS server when
they authenticated via this server. Instead a message suggests the user
to disconnect.

Clipper connections and `login_clipper`
---------------------------------------

- Non-empty `login_clipper` are now unique among `CofProfile` instances.
- They are created once for users with a non-empty 'login_clipper' (with
the data migration 0014_create_clipper_connections).
- The `login_clipper` of CofProfile instances are sync with their
clipper connections:
    * `CofProfile.sync_clipper_connections` method updates the
connections based on `login_clipper`.
    * Signals receivers `sync_clipper…` update `login_clipper` based on
connections creations/updates/deletions.

Misc
----

- Add NullCharField (model field) which allows to use `unique=True` on
CharField (even with empty strings).
- Parts of kfet mixins for TestCase are now in shared.tests.testcase,
  as they are used elsewhere than in the kfet app.
2018-10-21 17:09:12 +02:00
724 changed files with 50391 additions and 75077 deletions

1
.envrc
View file

@ -1 +0,0 @@
use nix

6
.gitignore vendored
View file

@ -5,19 +5,13 @@ cof/settings.py
settings.py
*~
venv/
.venv/
.vagrant
/src
media/
*.log
.sass-cache/
*.sqlite3
.coverage
# PyCharm
.idea
.cache
# VSCode
.vscode/
.direnv

View file

@ -1,7 +1,8 @@
image: "python:3.7"
image: "python:3.5"
variables:
# GestioCOF settings
DJANGO_SETTINGS_MODULE: "cof.settings.prod"
DBHOST: "postgres"
REDIS_HOST: "redis"
REDIS_PASSWD: "dummy"
@ -17,23 +18,23 @@ variables:
# psql password authentication
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:
stage: test
before_script:
- 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
- sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py
- sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client build-essential python3-dev libldap2-dev libsasl2-dev
- 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 = "";' cof/settings/secret.py
# 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"
- pip install --upgrade -r requirements-prod.txt coverage tblib
- pip install --upgrade -r requirements.txt coverage
- python --version
script:
- coverage run manage.py test
after_script:
- coverage report
services:
- postgres:11.7
- postgres:9.6
- redis:latest
cache:
key: test
@ -43,61 +44,18 @@ variables:
# Keep this disabled for now, as it may kill GitLab...
# 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:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod"
script:
- coverage run manage.py test gestioncof bda petitscours shared --parallel
bdstest:
stage: test
extends: .test_template
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod"
script:
- coverage run manage.py test bds clubs events --parallel
linters:
image: python:3.6
stage: test
before_script:
- mkdir -p vendor/pip
- pip install --upgrade black isort flake8
script:
- black --check .
- isort --check --diff .
- isort --recursive --check-only --diff bda cof gestioncof kfet provisioning shared utils
# Print errors only
- flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared
- flake8 --exit-zero bda cof gestioncof kfet provisioning shared utils
cache:
key: linters
paths:
- vendor/
# Check whether there are some missing migrations.
migration_checks:
stage: test
variables:
DJANGO_SETTINGS_MODULE: "gestioasso.settings.local"
before_script:
- 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
- cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py
- pip install --upgrade -r requirements-devel.txt
- python --version
script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS
services:
# this should not be necessary…
- postgres:11.7
cache:
key: migration_checks
paths:
- vendor/

View file

@ -48,7 +48,7 @@ if type isort &>/dev/null; then
ISORT_OUTPUT="/tmp/gc-isort-output.log"
touch $ISORT_OUTPUT
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check &>$ISORT_OUTPUT; then
if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check-only &>$ISORT_OUTPUT; then
echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT
printf "Reformatted.\n"
formatter_updated=1

View file

@ -1,303 +0,0 @@
# Changelog
Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre
2018).
## Le FUTUR ! (pas prêt pour la prod)
### Nouveau module de gestion des événements
- Désormais complet niveau modèles
- Export des participants implémenté
#### TODO
- Vue de création d'événements ergonomique
- Vue d'inscription à un événement **ou** intégration propre dans la vue
"inscription d'un nouveau membre"
### Nouveau module de gestion des clubs
Uniquement un modèle simple de clubs avec des respos. Aucune gestion des
adhérents ni des cotisations.
## TODO Prod
- Lancer `python manage.py update_translation_fields` après la migration
- 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
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
### GestioBDS
- Ajout d'un bouton pour supprimer un compte
- Le nombre d'adhérent⋅es est affiché sur la page d'accueil
- le groupe BDS a les bonnes permissions
### Site du COF
- Captcha fonctionnel pour les mailing-listes
### K-Fêt
- L'autocomplétion pour la création de compte K-Fêt se lance à 3 caractères seulement,
donc est plus rapide.
## Version 0.6 - 27/07/2020
Arrivée du BDS !
GestioCOF et GestioBDS ont du code en commun mais tournent de façon séparée, les
deux bases de données sont distinctes.
## Version 0.5 - 11/07/2020
### Problèmes corrigés
- La recherche d'utilisateurices (COF + K-Fêt) fonctionne de nouveau
- Bug d'affichage quand on a beaucoup de clubs dans le cadre "Accès rapide" sur
la page des clubs (nouveau site du COF)
- Version mobile plus ergonimique sur le nouveau site du COF
- Cliquer sur "visualiser" sur les pages de clubs dans wagtail ne provoque plus
d'erreurs 500 (nouveau site du COF)
- L'historique des ventes des articles K-Fêt fonctionne à nouveau
- Les montants en K-Fêt sont à nouveau affichés en UKF (et non en €).
- Les boutons "afficher/cacher" des mails et noms des participant⋅e⋅s à un
spectacle BdA fonctionnent à nouveau.
- on ne peut plus compter de consos sur ☠☠☠, ni éditer les comptes spéciaux
(LIQ, GNR, ☠☠☠, #13).
### Nouvelles fonctionnalités
- On n'affiche que 4 articles sur la pages "nouveautés" (nouveau site du COF)
- Plus de traductions sur le nouveau site du COF
- Les transferts apparaissent maintenant dans l'historique K-Fêt et l'historique
personnel.
- les statistiques K-Fêt remontent à plus d'un an (et le code est simplifié)
## Version 0.4.1 - 17/01/2020
- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés
aux urls (\#, /...)
## Version 0.4 - 15/01/2020
- Corrige un bug d'affichage d'images sur l'interface des petits cours
- La page des transferts permet de créer un nombre illimité de transferts en
une fois.
- Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs
- Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil
- Certaines opérations sont à nouveau accessibles depuis la session partagée
K-Fêt.
- Le bouton "déconnexion" déconnecte vraiment du CAS pour les comptes clipper
- Corrige un crash sur la page des reventes pour les nouveaux participants.
- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux
## Version 0.3.3 - 30/11/2019
- Corrige un problème de redirection lors de la déconnexion (CAS seulement)
- Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF
- Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ
- Corrige une privilege escalation liée aux sessions partagées en K-Fêt
https://git.eleves.ens.fr/klub-dev-ens/gestioCOF/issues/240
## Version 0.3.2 - 04/11/2019
- Bugfix: modifier un compte K-Fêt ne supprime plus nom/prénom
## Version 0.3.1 - 19/10/2019
- Bugfix: l'historique des utilisateurices s'affiche à nouveau
## Version 0.3 - 16/10/2019
- Comptes extés: lien pour changer son mot de passe sur la page d'accueil
- Les utilisateurices non-COF peuvent éditer leur profil
- Un peu de pub pour KDEns sur la page d'accueil
- Fix erreur 500 sur /bda/revente/<tirage_id>/manage
- Si on essaie d'accéder au compte que qqn d'autre on a une 404 (et plus une 403)
- On ne peut plus modifier des comptes COF depuis l'interface K-Fêt
- Le champ de paiement BdA se fait au niveau des attributions
- Affiche un message d'erreur plutôt que de crasher si échec de l'envoi du mail
de bienvenue aux nouveaux membres
- On peut supprimer des comptes et des articles K-Fêt
- Passage à Django2
- Dev : on peut désactiver la barre de debug avec une variable shell
- Remplace les CSS de Google par des polices de proximité
- Passage du site du COF et de la K-Fêt en Wagtail 2.3 et Wagtail-modeltranslation 0.9
- Ajoute un lien vers l'administration générale depuis les petits cours
- Abandon de l'ancien catalogue BdA (déjà plus utilisé depuis longtemps)
- Force l'unicité des logins clipper
- Nouveau site du COF en wagtail
- Meilleurs affichage des longues listes de spectacles à cocher dans BdA-Revente
- Bugfix : les pages de la revente ne sont plus accessibles qu'aux membres du
COF
## Version 0.2 - 07/11/2018
- Corrections de bugs d'interface dans l'inscription aux tirages BdA
- On peut annuler une revente à tout moment
- Pleiiiiin de tests
## Version 0.1 - 09/09/2018
Début de la numérotation des versions, début du changelog

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)
[![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 -r requirements-devel.txt
Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers
`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`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:
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
vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez
@ -186,18 +186,6 @@ Pour lancer les tests :
python manage.py test
```
### Astuces
- En développement on utilise la django debug toolbar parce que c'est utile pour
débuguer les templates ou les requêtes SQL mais des fois c'est pénible parce
ça fait ramer GestioCOF (surtout dans wagtail).
Vous pouvez la désactiver temporairement en définissant la variable
d'environnement `DJANGO_NO_DDT` dans votre shell : par exemple dans
bash/zsh/…:
```
$ export DJANGO_NO_DDT=1
```
## Documentation utilisateur

1
TODO_PROD.md Normal file
View file

@ -0,0 +1 @@
- Changer les urls dans les mails "bda-revente" et "bda-shotgun"

42
Vagrantfile vendored
View file

@ -1,19 +1,47 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Configuration de base pour GestioCOF.
# Voir https://docs.vagrantup.com pour plus d'informations.
# All Vagrant configuration is done below. The "2" in Vagrant.configure
# 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|
# On se base sur Debian 10 (Buster) pour avoir le même environnement qu'en
# production.
config.vm.box = "debian/contrib-buster64"
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
config.vm.box = "ubuntu/xenial64"
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
# ordinateur, et le port 8000 avec le port 8000.
config.vm.network :forwarded_port, guest: 80, host: 8080
config.vm.network :forwarded_port, guest: 8000, host: 8000
# Le restes de la configuration (installation de paquets, etc) est géré un
# script shell.
# Create a private network, which allows host-only access to the machine
# 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"
end

View file

@ -1,11 +1,10 @@
from datetime import timedelta
from custommail.shortcuts import send_mass_custom_mail
from dal.autocomplete import ModelSelect2
from django import forms
from django.contrib import admin
from django.core.mail import send_mass_mail
from django.db.models import Count, Q, Sum
from django.template import loader
from django.db.models import Count, Sum
from django.template.defaultfilters import pluralize
from django.utils import timezone
@ -33,6 +32,20 @@ class ReadOnlyMixin(object):
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):
listing = None
@ -68,51 +81,27 @@ class WithListingAttributionInline(AttributionInline):
exclude = ("given",)
form = WithListingAttributionTabularAdminForm
listing = True
verbose_name_plural = "Attributions sur listing"
class WithoutListingAttributionInline(AttributionInline):
form = WithoutListingAttributionTabularAdminForm
listing = False
verbose_name_plural = "Attributions hors listing"
class ParticipantAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
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):
"""
Permet de filtrer les participants sur s'ils ont payé leurs places ou pas
"""
title = "A payé"
parameter_name = "paid"
def lookups(self, request, model_admin):
return ((True, "Oui"), (False, "Non"))
def queryset(self, request, queryset):
return queryset.filter(paid=self.value())
self.fields["choicesrevente"].queryset = Spectacle.objects.select_related(
"location"
)
class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
inlines = [WithListingAttributionInline, WithoutListingAttributionInline]
def get_queryset(self, request):
return self.model.objects.annotate_paid().annotate(
nb_places=Count("attributions"),
remain=Sum(
"attribution__spectacle__price", filter=Q(attribution__paid=False)
),
total=Sum("attributions__price"),
return Participant.objects.annotate(
nb_places=Count("attributions"), total=Sum("attributions__price")
)
def nb_places(self, obj):
@ -121,13 +110,6 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
nb_places.admin_order_field = "nb_places"
nb_places.short_description = "Nombre de places"
def paid(self, obj):
return obj.paid
paid.short_description = "A payé"
paid.boolean = True
paid.admin_order_field = "paid"
def total(self, obj):
tot = obj.total
if tot:
@ -136,46 +118,31 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
return "0 €"
total.admin_order_field = "total"
total.short_description = "Total des places"
def remain(self, obj):
rem = obj.remain
if rem:
return "%.02f" % rem
else:
return "0 €"
remain.admin_order_field = "remain"
remain.short_description = "Reste à payer"
list_display = ("user", "nb_places", "total", "paid", "remain", "tirage")
list_filter = (ParticipantPaidFilter, "tirage")
total.short_description = "Total à payer"
list_display = ("user", "nb_places", "total", "paid", "paymenttype", "tirage")
list_filter = ("paid", "tirage")
search_fields = ("user__username", "user__first_name", "user__last_name")
actions = ["send_attribs"]
actions_on_bottom = True
list_per_page = 400
readonly_fields = ("total", "paid")
readonly_fields = ("total",)
readonly_fields_update = ("user", "tirage")
form = ParticipantAdminForm
def send_attribs(self, request, queryset):
emails = []
datatuple = []
for member in queryset.all():
subject = "Résultats du tirage au sort"
attribs = member.attributions.all()
context = {"member": member.user}
template_name = ""
shortname = ""
if len(attribs) == 0:
template_name = "bda/mails/attributions-decus.txt"
shortname = "bda-attributions-decus"
else:
template_name = "bda/mails/attributions.txt"
shortname = "bda-attributions"
context["places"] = attribs
message = loader.render_to_string(template_name, context)
emails.append((subject, message, "bda@ens.fr", [member.user.email]))
send_mass_mail(emails)
print(context)
datatuple.append((shortname, context, "bda@ens.fr", [member.user.email]))
send_mass_custom_mail(datatuple)
count = len(queryset.all())
if count == 1:
message_bit = "1 membre a"
@ -191,6 +158,17 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin):
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):
cleaned_data = super().clean()
participant = cleaned_data.get("participant")
@ -203,14 +181,13 @@ class AttributionAdminForm(forms.ModelForm):
)
return cleaned_data
class Meta:
widgets = {
"participant": ModelSelect2(url="bda-participant-autocomplete"),
"spectacle": ModelSelect2(url="bda-spectacle-autocomplete"),
}
class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
def paid(self, obj):
return obj.participant.paid
paid.short_description = "A payé"
paid.boolean = True
list_display = ("id", "spectacle", "participant", "given", "paid")
search_fields = (
"spectacle__title",
@ -223,7 +200,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin):
class ChoixSpectacleAdmin(admin.ModelAdmin):
autocomplete_fields = ["participant", "spectacle"]
form = ChoixSpectacleAdminForm
def tirage(self, obj):
return obj.participant.tirage
@ -267,14 +244,15 @@ class SalleAdmin(admin.ModelAdmin):
class SpectacleReventeAdminForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
qset = Participant.objects.select_related("user", "tirage")
if self.instance.pk is not None:
qset = qset.filter(tirage=self.instance.seller.tirage)
self.fields["confirmed_entry"].queryset = qset
self.fields["seller"].queryset = qset
self.fields["soldTo"].queryset = qset
self.fields["confirmed_entry"].queryset = Participant.objects.select_related(
"user", "tirage"
)
self.fields["seller"].queryset = Participant.objects.select_related(
"user", "tirage"
)
self.fields["soldTo"].queryset = Participant.objects.select_related(
"user", "tirage"
)
class SpectacleReventeAdmin(admin.ModelAdmin):

View file

@ -2,6 +2,7 @@ import random
class Algorithm(object):
shows = None
ranks = None
origranks = None
@ -9,10 +10,10 @@ class Algorithm(object):
def __init__(self, shows, members, choices):
"""Initialisation :
- on aggrège toutes les demandes pour chaque spectacle dans
show.requests
- on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings"""
- on aggrège toutes les demandes pour chaque spectacle dans
show.requests
- on crée des tables de demandes pour chaque personne, afin de
pouvoir modifier les rankings"""
self.max_group = 2 * max(choice.priority for choice in choices)
self.shows = []
showdict = {}

View file

@ -1,9 +1,8 @@
from django import forms
from django.forms.models import BaseInlineFormSet
from django.template import loader
from django.utils import timezone
from bda.models import SpectacleRevente
from bda.models import Attribution, Spectacle, SpectacleRevente
class InscriptionInlineFormSet(BaseInlineFormSet):
@ -39,146 +38,142 @@ class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea())
class TemplateLabelField(forms.ModelMultipleChoiceField):
"""
Extends ModelMultipleChoiceField to offer two more customization options :
- `label_from_instance` can be used with a template file
- the widget rendering template can be specified with `option_template_name`
"""
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return str(obj.spectacle)
def __init__(
self,
label_template_name=None,
context_object_name="obj",
option_template_name=None,
*args,
**kwargs
):
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, *args, own=True, **kwargs):
super().__init__(*args, **kwargs)
self.label_template_name = label_template_name
self.context_object_name = context_object_name
if option_template_name is not None:
self.widget.option_template_name = option_template_name
self.own = own
def label_from_instance(self, obj):
if self.label_template_name is None:
return super().label_from_instance(obj)
label = "{show}{suffix}"
suffix = ""
if self.own:
# C'est notre propre revente : informations sur le statut
if obj.soldTo is not None:
suffix = " -- Vendue à {firstname} {lastname}".format(
firstname=obj.soldTo.user.first_name,
lastname=obj.soldTo.user.last_name,
)
elif obj.shotgun:
suffix = " -- Tirage infructueux"
elif obj.notif_sent:
suffix = " -- Inscriptions au tirage en cours"
else:
return loader.render_to_string(
self.label_template_name, context={self.context_object_name: obj}
# Ce n'est pas à nous : on ne voit jamais l'acheteur
suffix = " -- Vendue par {firstname} {lastname}".format(
firstname=obj.seller.user.first_name, lastname=obj.seller.user.last_name
)
# Formulaires pour revente_manage
return label.format(show=str(obj.attribution.spectacle), suffix=suffix)
class ResellForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["attributions"] = TemplateLabelField(
queryset=participant.attribution_set.filter(
spectacle__date__gte=timezone.now(), paid=True
)
self.fields["attributions"].queryset = (
participant.attribution_set.filter(spectacle__date__gte=timezone.now())
.exclude(revente__seller=participant)
.select_related("spectacle", "spectacle__location", "participant__user"),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/attribution_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="attribution",
.select_related("spectacle", "spectacle__location", "participant__user")
)
class AnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=True,
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
label="",
queryset=participant.original_shows.filter(
self.fields["reventes"].queryset = (
participant.original_shows.filter(
attribution__spectacle__date__gte=timezone.now(), soldTo__isnull=True
)
.select_related(
"attribution__spectacle", "attribution__spectacle__location"
)
.order_by("-date"),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_self_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
.order_by("-date")
)
class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, tirage, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["spectacles"].queryset = tirage.spectacle_set.select_related(
"location"
).filter(date__gte=timezone.now())
class ReventeTirageAnnulForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
label="",
queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = participant.entered.filter(
soldTo__isnull=True
).select_related("attribution__spectacle", "seller__user")
class ReventeTirageForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=False,
label="",
queryset=SpectacleRevente.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"].queryset = (
SpectacleRevente.objects.filter(
notif_sent=True, shotgun=False, tirage_done=False
)
.exclude(confirmed_entry=participant)
.select_related("attribution__spectacle")
)
class SoldForm(forms.Form):
reventes = ReventeModelMultipleChoiceField(
own=True,
label="",
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
)
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=participant.original_shows.filter(soldTo__isnull=False)
self.fields["reventes"].queryset = (
participant.original_shows.filter(soldTo__isnull=False)
.exclude(soldTo=participant)
.select_related(
"attribution__spectacle", "attribution__spectacle__location"
),
widget=forms.CheckboxSelectMultiple,
label_template_name="bda/forms/revente_sold_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
)
# Formulaire pour revente_subscribe
class InscriptionReventeForm(forms.Form):
def __init__(self, tirage, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["spectacles"] = TemplateLabelField(
queryset=tirage.spectacle_set.select_related("location").filter(
date__gte=timezone.now()
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/spectacle_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="spectacle",
)
# Formulaires pour revente_tirages
class ReventeTirageAnnulForm(forms.Form):
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=participant.entered.filter(soldTo__isnull=True).select_related(
"attribution__spectacle", "seller__user"
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_other_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
)
class ReventeTirageForm(forms.Form):
def __init__(self, participant, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["reventes"] = TemplateLabelField(
queryset=(
SpectacleRevente.objects.filter(
notif_sent=True,
shotgun=False,
tirage_done=False,
attribution__spectacle__tirage=participant.tirage,
)
.exclude(confirmed_entry=participant)
.select_related("attribution__spectacle")
),
widget=forms.CheckboxSelectMultiple,
required=False,
label_template_name="bda/forms/revente_other_label_table.html",
option_template_name="bda/forms/checkbox_table.html",
context_object_name="revente",
)
)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,15 +12,15 @@ def forwards_func(apps, schema_editor):
for revente in SpectacleRevente.objects.all():
is_expired = timezone.now() > revente.date_tirage()
is_direct = (
revente.attribution.spectacle.date >= revente.date
and timezone.now() > revente.date + timedelta(minutes=15)
is_direct = revente.attribution.spectacle.date >= revente.date and timezone.now() > revente.date + timedelta(
minutes=15
)
revente.shotgun = is_expired or is_direct
revente.save()
class Migration(migrations.Migration):
dependencies = [("bda", "0009_revente")]
operations = [

View file

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

View file

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

View file

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

View file

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

View file

@ -1,30 +0,0 @@
# Generated by Django 2.2 on 2019-06-03 19:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0013_merge_20180524_2123")]
operations = [
migrations.AddField(
model_name="attribution",
name="paid",
field=models.BooleanField(default=False, verbose_name="Payée"),
),
migrations.AddField(
model_name="attribution",
name="paymenttype",
field=models.CharField(
blank=True,
choices=[
("cash", "Cash"),
("cb", "CB"),
("cheque", "Chèque"),
("autre", "Autre"),
],
max_length=6,
verbose_name="Moyen de paiement",
),
),
]

View file

@ -1,36 +0,0 @@
# Generated by Django 2.2 on 2019-06-03 19:30
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db import migrations
def set_attr_payment(apps, schema_editor):
Attribution = apps.get_model("bda", "Attribution")
for attr in Attribution.objects.all():
attr.paid = attr.participant.paid
attr.paymenttype = attr.participant.paymenttype
attr.save()
def set_participant_payment(apps, schema_editor):
Participant = apps.get_model("bda", "Participant")
for part in Participant.objects.all():
attr_set = part.attribution_set
part.paid = attr_set.exists() and not attr_set.filter(paid=False).exists()
try:
# S'il n'y a qu'un seul type de paiement, on le set
part.paymenttype = (
attr_set.values_list("paymenttype", flat=True).distinct().get()
)
# Sinon, whatever
except (ObjectDoesNotExist, MultipleObjectsReturned):
pass
part.save()
class Migration(migrations.Migration):
dependencies = [("bda", "0014_attribution_paid_field")]
operations = [
migrations.RunPython(set_attr_payment, set_participant_payment, atomic=True)
]

View file

@ -1,12 +0,0 @@
# Generated by Django 2.2 on 2019-06-03 19:30
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [("bda", "0015_move_bda_payment")]
operations = [
migrations.RemoveField(model_name="participant", name="paid"),
migrations.RemoveField(model_name="participant", name="paymenttype"),
]

View file

@ -1,17 +0,0 @@
# Generated by Django 2.2 on 2019-09-18 16:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [("bda", "0016_delete_participant_paid")]
operations = [
migrations.AddField(
model_name="participant",
name="accepte_charte",
field=models.BooleanField(
default=False, verbose_name="A accepté la charte BdA"
),
)
]

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
from datetime import timedelta
from custommail.models import CustomMail
from custommail.shortcuts import send_mass_custom_mail
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.core import mail
from django.core.mail import EmailMessage, send_mass_mail
from django.db import models
from django.db.models import Count, Exists
from django.template import loader
from django.db.models import Count
from django.utils import formats, timezone
@ -31,7 +31,6 @@ class Tirage(models.Model):
"Tirage à afficher dans le catalogue", default=False
)
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False)
archived = models.BooleanField("Archivé", default=False)
def __str__(self):
return "%s - %s" % (
@ -117,19 +116,16 @@ class Spectacle(models.Model):
bda_generic.nb_attr = 1
members.append(bda_generic)
# On écrit un mail personnalisé à chaque participant
mails = [
datatuple = [
(
str(self),
loader.render_to_string(
"bda/mails/rappel.txt",
context={"member": member, "nb_attr": member.nb_attr, "show": self},
),
"bda-rappel",
{"member": member, "nb_attr": member.nb_attr, "show": self},
settings.MAIL_DATA["rappels"]["FROM"],
[member.email],
)
for member in members
]
send_mass_mail(mails)
send_mass_custom_mail(datatuple)
# On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now()
self.save()
@ -155,41 +151,6 @@ PAYMENT_TYPES = (
)
class Attribution(models.Model):
participant = models.ForeignKey("Participant", on_delete=models.CASCADE)
spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="attribues"
)
given = models.BooleanField("Donnée", default=False)
paid = models.BooleanField("Payée", default=False)
paymenttype = models.CharField(
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
)
def __str__(self):
return "%s -- %s, %s" % (
self.participant.user,
self.spectacle.title,
self.spectacle.date,
)
class ParticipantPaidQueryset(models.QuerySet):
"""
Un manager qui annote le queryset avec un champ `paid`,
indiquant si un participant a payé toutes ses attributions.
"""
def annotate_paid(self):
# OuterRef permet de se référer à un champ d'un modèle non encore fixé
# Voir:
# https://docs.djangoproject.com/en/2.2/ref/models/expressions/#django.db.models.OuterRef
unpaid = Attribution.objects.filter(
participant=models.OuterRef("pk"), paid=False
)
return self.annotate(paid=~Exists(unpaid))
class Participant(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
choices = models.ManyToManyField(
@ -198,25 +159,18 @@ class Participant(models.Model):
attributions = models.ManyToManyField(
Spectacle, through="Attribution", related_name="attributed_to"
)
tirage = models.ForeignKey(
Tirage, on_delete=models.CASCADE, limit_choices_to={"archived": False}
paid = models.BooleanField("A payé", default=False)
paymenttype = models.CharField(
"Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True
)
accepte_charte = models.BooleanField("A accepté la charte BdA", default=False)
tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE)
choicesrevente = models.ManyToManyField(
Spectacle, related_name="subscribed", blank=True
)
objects = ParticipantPaidQueryset.as_manager()
def __str__(self):
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 = (
("1", "1 place"),
@ -253,15 +207,26 @@ class ChoixSpectacle(models.Model):
class Meta:
ordering = ("priority",)
constraints = [
models.UniqueConstraint(
fields=["participant", "spectacle"], name="unique_participation"
)
]
unique_together = (("participant", "spectacle"),)
verbose_name = "voeu"
verbose_name_plural = "voeux"
class Attribution(models.Model):
participant = models.ForeignKey(Participant, on_delete=models.CASCADE)
spectacle = models.ForeignKey(
Spectacle, on_delete=models.CASCADE, related_name="attribues"
)
given = models.BooleanField("Donnée", default=False)
def __str__(self):
return "%s -- %s, %s" % (
self.participant.user,
self.spectacle.title,
self.spectacle.date,
)
class SpectacleRevente(models.Model):
attribution = models.OneToOneField(
Attribution, on_delete=models.CASCADE, related_name="revente"
@ -364,24 +329,21 @@ class SpectacleRevente(models.Model):
BdA-Revente à tous les intéressés.
"""
inscrits = self.attribution.spectacle.subscribed.select_related("user")
mails = [
datatuple = [
(
"BdA-Revente : {}".format(self.attribution.spectacle.title),
loader.render_to_string(
"bda/mails/revente-new.txt",
context={
"member": participant.user,
"show": self.attribution.spectacle,
"revente": self,
"site": Site.objects.get_current(),
},
),
"bda-revente",
{
"member": participant.user,
"show": self.attribution.spectacle,
"revente": self,
"site": Site.objects.get_current(),
},
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
for participant in inscrits
]
send_mass_mail(mails)
send_mass_custom_mail(datatuple)
self.notif_sent = True
self.notif_time = timezone.now()
self.save()
@ -392,23 +354,20 @@ class SpectacleRevente(models.Model):
leur indiquer qu'il est désormais disponible au shotgun.
"""
inscrits = self.attribution.spectacle.subscribed.select_related("user")
mails = [
datatuple = [
(
"BdA-Revente : {}".format(self.attribution.spectacle.title),
loader.render_to_string(
"bda/mails/revente-shotgun.txt",
context={
"member": participant.user,
"show": self.attribution.spectacle,
"site": Site.objects.get_current(),
},
),
"bda-shotgun",
{
"member": participant.user,
"show": self.attribution.spectacle,
"site": Site.objects.get_current(),
},
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
for participant in inscrits
]
send_mass_mail(mails)
send_mass_custom_mail(datatuple)
self.notif_sent = True
self.notif_time = timezone.now()
# Flag inutile, sauf si l'horloge interne merde
@ -440,30 +399,31 @@ class SpectacleRevente(models.Model):
"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(
EmailMessage(
subject=subject,
body=loader.render_to_string(
"bda/mails/revente-tirage-winner.txt",
context=context,
),
c_mails["bda-revente-winner"].get_message(
context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[winner.user.email],
)
)
mails.append(
EmailMessage(
subject=subject,
body=loader.render_to_string(
"bda/mails/revente-tirage-seller.txt",
context=context,
),
c_mails["bda-revente-seller"].get_message(
context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[seller.user.email],
reply_to=[winner.user.email],
),
)
)
# Envoie un mail aux perdants
@ -473,15 +433,11 @@ class SpectacleRevente(models.Model):
new_context["acheteur"] = inscrit.user
mails.append(
EmailMessage(
subject=subject,
body=loader.render_to_string(
"bda/mails/revente-tirage-loser.txt",
context=new_context,
),
c_mails["bda-revente-loser"].get_message(
new_context,
from_email=settings.MAIL_DATA["revente"]["FROM"],
to=[inscrit.user.email],
),
)
)
mail_conn = mail.get_connection()

View file

@ -1,125 +0,0 @@
form#tokenform {
text-align: center;
font-size: 2em;
}
label {
margin-right: 10px;
vertical-align: top;
}
form#tokenform textarea {
font-size: 2em;
width: 350px;
height: 200px;
font-family: 'Droif Serif', serif;
}
/* wft ?
input {
width: 400px;
font-size: 2em;
}*/
ul.losers {
display: inline;
margin: 0;
padding: 0;
}
ul.losers li {
display: inline;
}
span.details {
font-size: 0.7em;
}
td {
border: 0px solid black;
padding: 2px;
}
.attribresult {
margin: 10px 0px;
}
.spectacle-passe {
opacity:0.5;
}
/** JQuery-Confirm box **/
.jconfirm .jconfirm-bg {
background-color: rgb(0,0,0,0.6) !important;
}
.jconfirm .jconfirm-box {
padding:0;
border-radius:0 !important;
font-family:Roboto;
}
.jconfirm .jconfirm-box .content-pane {
border-bottom:1px solid #ddd;
margin: 0px !important;
}
.jconfirm .jconfirm-box .content {
padding: 5px;
}
.jconfirm .jconfirm-box .content-pane {
border-bottom:1px solid #ddd;
margin: 0px !important;
}
.jconfirm .jconfirm-box .content {
padding: 10px;
}
.jconfirm .jconfirm-box .content a,
.jconfirm .jconfirm-box .content a:hover {
color: #D81138;
font-weight: bold;
}
.jconfirm .jconfirm-box .buttons {
margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */
padding:0;
height:40px;
}
.jconfirm .jconfirm-box .buttons button {
min-width:40px;
height:100%;
margin:0;
margin:0 !important;
border-radius: 0 !important;
}
.jconfirm .jconfirm-box .buttons button:first-child:focus,
.jconfirm .jconfirm-box .buttons button:first-child:hover {
color:#FFF !important;
background:forestgreen !important;
}
.jconfirm .jconfirm-box .buttons button:nth-child(2):focus,
.jconfirm .jconfirm-box .buttons button:nth-child(2):hover {
color:#FFF !important;
background:#D93A32 !important;
}
.jconfirm .jconfirm-box div.title-c .title {
display: block;
padding:0 15px;
height:40px;
line-height:40px;
font-family:Dosis;
font-size:20px;
font-weight:bold;
color:#FFF;
background-color:rgb(222, 130, 107);
}

48
bda/static/css/bda.css Normal file
View file

@ -0,0 +1,48 @@
form#tokenform {
text-align: center;
font-size: 2em;
}
label {
margin-right: 10px;
vertical-align: top;
}
form#tokenform textarea {
font-size: 2em;
width: 350px;
height: 200px;
font-family: 'Droif Serif', serif;
}
/* wft ?
input {
width: 400px;
font-size: 2em;
}*/
ul.losers {
display: inline;
margin: 0;
padding: 0;
}
ul.losers li {
display: inline;
}
span.details {
font-size: 0.7em;
}
td {
border: 0px solid black;
padding: 2px;
}
.attribresult {
margin: 10px 0px;
}
.spectacle-passe {
opacity:0.5;
}

Binary file not shown.

View file

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

View file

@ -1,5 +1,5 @@
{% extends "base_title.html" %}
{% load static %}
{% load staticfiles %}
{% block realcontent %}
<h2>État des inscriptions BdA</h2>
@ -42,6 +42,9 @@
Total&nbsp;: {{ total }} place{{ total|pluralize }} demandée{{ total|pluralize }}
sur {{ proposed }} place{{ proposed|pluralize }} proposée{{ proposed|pluralize }}
</span>
<script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
</script>
<script type="text/javascript">
$(function(){
$("table.etat-bda").stupidtable();

View file

@ -1 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=attribution.spectacle %}

View file

@ -1,4 +0,0 @@
<tr>
<td><input type="{{ widget.type }}" name="{{ widget.name }}" {% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}></td>
{{ widget.label }}
</tr>

View file

@ -1 +0,0 @@
<td data-sort-value="{{ revente.date_tirage | date:"U" }}">{{ revente.date_tirage }}</td>

View file

@ -1,3 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% with user=revente.seller.user %} <td>{{user.first_name}} {{user.last_name}}</td> {% endwith%}
{% include 'bda/forms/date_tirage.html' %}

View file

@ -1,2 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% include 'bda/forms/date_tirage.html' %}

View file

@ -1,4 +0,0 @@
{% include 'bda/forms/spectacle_label_table.html' with spectacle=revente.attribution.spectacle %}
{% with user=revente.soldTo.user %}
<td><a href="mailto:{{ user.email }}">{{user.first_name}} {{user.last_name}}</a></td>
{% endwith %}

View file

@ -1,4 +0,0 @@
<td>{{ spectacle.title }}</td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">{{ spectacle.price |floatformat }}€</td>

View file

@ -1,13 +1,11 @@
{% extends "base_title.html" %}
{% load static %}
{% load staticfiles %}
{% 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-confirm.js" %}"></script>
<script type="text/javascript" src="{% static 'gestioncof/vendor/jquery.ui.touch-punch.min.js' %}" ></script>
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-confirm.css' %}">
<link type="text/css" rel="stylesheet" href="{% static 'vendor/jquery/jquery-ui.min.css' %}" />
<link type="text/css" rel="stylesheet" href="{% static 'bda/css/bda.css' %}" />
<script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %}
@ -54,11 +52,6 @@ var django = {
} else {
deleteInput.attr("checked", true);
}
} else {
// Reset the default values
var selects = $(form).find("select");
$(selects[0]).val("");
$(selects[1]).val("1");
}
// callback
});
@ -120,50 +113,11 @@ var django = {
});
</script>
<input type="hidden" name="dbstate" value="{{ dbstate }}" />
<input type="submit" class="btn btn-primary" id="bda-inscr" value="Enregistrer" />
<input type="submit" class="btn btn-primary" value="Enregistrer" />
</div>
<p class="footnotes">
<sup>1</sup>: cette liste de v&oelig;ux est ordonnée (du plus important au moins important), pour ajuster la priorité vous pouvez déplacer chaque v&oelig;u.<br />
</p>
</div>
</form>
{% if not charte %}
<script>
(function ($) {
var charte_ok = false ;
function link_charte() {
$.confirm({
title: 'Charte du BdA',
columnClass: 'col-md-6 col-md-offset-3',
content: `
<div>
En vous inscrivant à ce tirage du Bureau des Arts, vous vous engagez à \
respecter la charte du BdA:</br> \
<a target="_blank" href='https://bda.ens.fr/lequipe/charte-bda/'>https://bda.ens.fr/lequipe/charte-bda/</a>
</div>`,
backgroundDismiss: true,
opacity: 1,
animation:'top',
closeAnimation:'bottom',
keyboardEnabled: true,
confirmButton: '<span class="glyphicon glyphicon-ok"></span>',
cancelButton: '<span class="glyphicon glyphicon-remove"></span>',
confirm: function() {
charte_ok = true ;
$("#bda_form").submit();
},
});
}
$(document).ready(function($) {
$("#bda_form").submit(function(e) {
if (!charte_ok) {
e.preventDefault();
link_charte();
}
})
})
})(django.jQuery);
</script>
{% endif %}
{% endblock %}

View file

@ -26,6 +26,13 @@
<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>
<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" %}
{% load static %}
{% load staticfiles %}
{% block realcontent %}
<h2>{{ spectacle }}</h2>
@ -16,7 +16,7 @@
<tbody>
{% for participant in participants %}
<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.email}}">{{participant.email}}</td>
<td data-sort-value="{{ participant.paid}}" class={%if participant.paid %}"greenratio"{%else%}"redratio"{%endif%}>
@ -38,7 +38,7 @@
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
<div>
<div>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participant⋅e⋅s</button>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
<pre id="export-mails" style="display:none">{% spaceless %}
{% for participant in participants %}{{ participant.email }}, {% endfor %}
{% endspaceless %}</pre>
@ -56,7 +56,9 @@
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
</div>
<script type="text/javascript">
<script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script>
function toggle(id) {
var pre = document.getElementById(id) ;
pre.style.display = pre.style.display == "none" ? "block" : "none" ;

View file

@ -10,7 +10,6 @@
<td>{{place.spectacle.location}}</td>
<td>{{place.spectacle.date}}</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>
{% endfor %}
</table>

View file

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

View file

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

View file

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

View file

@ -1,45 +1,35 @@
{% extends "base_title.html" %}
{% load static %}
{% load bootstrap %}
{% block realcontent %}
<h2>Gestion des places que je revends</h2>
{% with resell_attributions=resellform.attributions annul_reventes=annulform.reventes sold_reventes=soldform.reventes %}
{% if resell_exists %}
{% if resell_attributions %}
<br />
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les places que vous souhaitez revendre, et validez. Vous aurez
ensuite 1h pour changer d'avis avant que la revente soit confirmée et
que les notifications soient envoyées aux intéressé·e·s.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Date</th>
<th data-sort="int">Prix</th>
</tr>
</thead>
<tbody>
{% for checkbox in resellform.attributions %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les places que vous souhaitez revendre, et validez. Vous aurez
ensuite 1h pour changer d'avis avant que la revente soit confirmée et
que les notifications soient envoyées aux intéressé·e·s.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ resellform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
<hr />
{% endif %}
{% if annul_exists %}
{% if annul_reventes %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
<div class="bg-info text-info center-block">
@ -47,75 +37,44 @@
Vous pouvez annuler les reventes qui n'ont pas encore trouvé preneur·se.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class='form-group'>
<div class='multiple-checkbox'>
<ul>
{% for revente in annul_reventes %}
<li>{{ revente.tag }} {{ revente.choice_label }}</li>
{% endfor %}
</ul>
</div>
</div>
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
</form>
<hr />
{% endif %}
{% if sold_exists %}
{% if sold_reventes %}
<h3>Places revendues</h3>
<form action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Pour chaque revente, vous devez soit l'annuler soit la confirmer pour
transférer la place la place à la personne tirée au sort.
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Pour chaque revente, vous devez soit l'annuler soit la confirmer pour
transférer la place la place à la personne tirée au sort.
L'annulation sert par exemple à pouvoir remettre la place en jeu si
vous ne parvenez pas à entrer en contact avec la personne tirée au
sort.
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue à</th>
</tr>
</thead>
<tbody>
{% for checkbox in soldform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
L'annulation sert par exemple à pouvoir remettre la place en jeu si
vous ne parvenez pas à entrer en contact avec la personne tirée au
sort.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ soldform|bootstrap }}
</div>
<button type="submit" class="btn btn-primary" name="transfer">Transférer</button>
<button type="submit" class="btn btn-primary" name="reinit">Réinitialiser</button>
</form>
{% endif %}
{% if not resell_exists and not annul_exists and not sold_exists %}
{% if not resell_attributions and not annul_reventes and not sold_reventes %}
<p>Plus de reventes possibles !</p>
{% endif %}
<script language="JavaScript">
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endwith %}
{% endblock %}

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

@ -2,26 +2,11 @@
{% block realcontent %}
<h2>Places disponibles immédiatement</h2>
{% if spectacles %}
<table class="table table-striped stupidtable" id="bda-shotgun">
<thead>
<tr>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th></th>
</tr>
</thead>
<tbody>
{% for spectacle in spectacles %}
<tr>
{% include "bda/forms/spectacle_label_table.html" with spectacle=spectacle %}
<td class="button"><a role="button" class="btn btn-primary" href="{% url 'bda-revente-buy' spectacle.id %}">Racheter</a>
</tr>
{% endfor %}
</tbody>
</table>
{% if shotgun %}
<ul class="list-unstyled">
{% for spectacle in shotgun %}
<li><a href="{% url "bda-revente-buy" spectacle.id %}">{{spectacle}}</a></li>
{% endfor %}
{% else %}
<p> Pas de places disponibles immédiatement, désolé !</p>
{% endif %}

View file

@ -1,12 +1,12 @@
{% extends "base_title.html" %}
{% load static %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
<form action="" class="form-horizontal" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Cochez les spectacles pour lesquels vous souhaitez recevoir une
Cochez les spectacles pour lesquels vous souhaitez recevoir un
notification quand une place est disponible en revente. <br />
Lorsque vous validez vos choix, si un tirage au sort est en cours pour
un des spectacles que vous avez sélectionné, vous serez automatiquement
@ -21,21 +21,14 @@
<button type="button"
class="btn btn-primary"
onClick="select(false)">Tout désélectionner</button>
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
</tr>
</thead>
<tbody>
{% for checkbox in form.spectacles %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="multiple-checkbox">
<ul>
{% for checkbox in form.spectacles %}
<li>{{ checkbox }}</li>
{% endfor %}
</ul>
</div>
</div>
<input type="submit"
class="btn btn-primary"
@ -43,22 +36,11 @@
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length; i < n; i++) {
checkboxes[i].checked = check;
}
}
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length; i < n; i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %}

View file

@ -1,70 +1,45 @@
{% extends "base_title.html" %}
{% load static %}
{% load bootstrap %}
{% block realcontent %}
<h2>Tirages au sort de reventes</h2>
{% if annul_exists %}
{% if annulform.reventes %}
<h3>Les reventes auxquelles vous êtes inscrit·e</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Voici la liste des reventes auxquelles vous êtes inscrit·e ; si vous ne souhaitez plus participer au tirage au sort vous pouvez vous en désister.
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez vous désinscrire des reventes suivantes tant que le tirage n'a
pas eu lieu.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
{{ annulform|bootstrap }}
</div>
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue par</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in annulform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
name="annul"
value="Se désister des tirages sélectionnés">
value="Se désinscrire des tirages sélectionnés">
</div>
</form>
{% endif %}
<hr />
{% endif %}
{% if sub_exists %}
{% if subform.reventes %}
<h3>Tirages en cours</h3>
<form class="form-horizontal" action="" method="post">
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Vous pouvez vous inscrire aux tirages en cours suivants.
Vous pouvez vous inscrire aux tirage en cours suivants.
</div>
<div class="bootstrap-form-reduce">
{% csrf_token %}
<table class="table table-striped stupidtable">
<thead>
<tr>
<th></th>
<th data-sort="string">Titre</th>
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="int">Prix</th>
<th>Vendue par</th>
<th data-sort="int">Tirage le</th>
</tr>
</thead>
<tbody>
{% for checkbox in subform.reventes %}{{ checkbox }}{% endfor %}
</tbody>
</table>
{{ subform|bootstrap }}
</div>
<div class="form-actions">
<input type="submit"
class="btn btn-primary"
@ -74,26 +49,4 @@
</form>
{% endif %}
{% if not annul_exists and not sub_exists %}
<div class="bg-info text-info center-block">
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
Aucune revente n'est active pour le moment !
</div>
{% endif %}
<script language="JavaScript">
$(function(){
$("table.stupidtable").stupidtable();
});
$("tr").click(function() {
$(this).find("input[type=checkbox]").click()
});
$("input[type=checkbox]").click(function(e) {
e.stopPropagation();
});
</script>
{% endblock %}

View file

@ -0,0 +1,113 @@
{% load staticfiles %}
<!doctype html>
<html>
<head>
<base target="_parent"/>
<style>
@font-face {
font-family: josefinsans;
src: url({% static "fonts/josefinsans.ttf" %});
}
*::-moz-selection {
background: #B0B0B0;
}
*::selection {
background: #B0B0B0;
}
.descTable{
width: 100%;
margin: 0 auto 1em;
border-bottom: 2px solid;
border-collapse: collapse;
border-spacing: 0;
font-size: 14px;
line-height: 2;
max-width: 100%;
background-color: transparent;
font-family: 'josefinsans', 'Arial';
font-weight: 700;
color: #5a5a5a;
}
img{
max-width: 100%;
}
</style>
<meta charset="utf8" />
</head>
<script src="https://code.jquery.com/jquery-3.1.0.min.js"></script>
<body>
{% for show in shows %}
<table class="descTable">
<thead>
<tr>
<th colspan="2"><p style="text-align:center;font-size:22px;">{{ show.title }}</p></th>
</tr>
</thead>
<tbody>
<tr>
<td><p style="text-align: left;">{{ show.location }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.category }}</p></td>
</tr>
<tr>
<td><p style="text-align: left;">{{ show.date|date:"l j F Y - H\hi" }}</p></td><td class="column-2"><p style="text-align: right;">{{ show.slots }} place{{ show.slots|pluralize}} {% if show.slots_description != "" %}({{ show.slots_description }}){% endif %} - {{ show.price }} euro{{ show.price|pluralize}}</p></td>
</tr>
{% if show.vips %}
<tr>
<td colspan="2"><p style="text-align: justify;">{{ show.vips }}</p></td>
</tr>
{% endif %}
<tr>
<td colspan="2">
<p style="text-align: justify;">{{ show.description }}</p>
{% for quote in show.quote_set.all %}
<p style="text-align:center; font-style: italic;">«{{ quote.text }}»{% if quote.author %} - {{ quote.author }}{% endif %}</p>
{% endfor %}
</td>
</tr>
{% if show.image %}
<tr>
<td colspan="2"><p style="text-align:center;"><a href="{{ show.ext_link }}"><img class="imgDesc" style="display: inline;" src="{{ MEDIA_URL }}{{ show.image }}" alt="{{ show.title }}"></a></p></td>
</tr>
{% endif %}
</tbody>
</table>
{% endfor %}
<script>
// Correction de la taille des images
/*$(document).ready(function() {
$(".descTable").each(function() {
$(this).width($("body").width());
});
$(".imgDesc").on("load", function() {
// Dimensions
origHeight = 500; // Hauteur souhaitée
w = $(this).width();
h = $(this).height();
r = w/h; // Ratio de l'image
maxWidth = $("body").width();
if (r * origHeight > maxWidth)
{
$(this).width(maxWidth);
$(this).height(maxWidth/r);
}
else
{
$(this).width(r * origHeight);
$(this).height(origHeight);
}
});
});*/
</script>
</body>
</html>

View file

@ -1,8 +1,8 @@
{% extends "base_title.html" %}
{% load static %}
{% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "bda/css/bda.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %}
@ -17,11 +17,11 @@
<th data-sort="int">Date</th>
<th data-sort="string">Lieu</th>
<th data-sort="float">Prix</th>
</tr>
</tr>
</thead>
<tbody>
{% for spectacle in object_list %}
<tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
<tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
<td data-sort-value="{{ spectacle.timestamp }}"">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
@ -32,7 +32,9 @@
{% endfor %}
</tbody>
</table>
<script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}">
</script>
<script type="text/javascript">
$(function(){
$("table.etat-bda").stupidtable();
@ -49,5 +51,6 @@
<h3> Exports </h3>
<ul>
<li><a href="{% url 'bda-unpaid' tirage_id %}">Mailing list impayés</a>
<li><a href="{% url 'bda-descriptions' tirage_id %}">Lien vers les descriptions des spectacles, à utiliser dans une page wordpress</a>
</ul>
{% endblock %}

View file

@ -1,65 +0,0 @@
from django.utils import timezone
from shared.tests.mixins import ViewTestCaseMixin
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
from .utils import create_user
class BdAViewTestCaseMixin(ViewTestCaseMixin):
def get_users_base(self):
return {
"bda_other": create_user(username="bda_other"),
"bda_member": create_user(username="bda_member", is_cof=True),
"bda_staff": create_user(username="bda_staff", is_cof=True, is_buro=True),
}
class BdATestHelpers:
bda_testdata = False
def setUp(self):
super().setUp()
if self.bda_testdata:
self.load_bda_testdata()
def load_bda_testdata(self):
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
self.show1 = Spectacle.objects.create(
title="foo",
date=timezone.now(),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.show2 = Spectacle.objects.create(
title="bar",
date=timezone.now(),
location=self.location,
price=1,
slots=142,
tirage=self.tirage,
listing=False,
category=self.category,
)
self.show3 = Spectacle.objects.create(
title="baz",
date=timezone.now(),
location=self.location,
price=2,
slots=242,
tirage=self.tirage,
listing=False,
category=self.category,
)

View file

@ -19,6 +19,8 @@ User = get_user_model()
class SpectacleReventeTests(TestCase):
fixtures = ["gestioncof/management/data/custommail.json"]
def setUp(self):
now = timezone.now()
@ -67,7 +69,7 @@ class SpectacleReventeTests(TestCase):
revente = self.rev
wanted_by = [self.p1, self.p2, self.p3]
revente.confirmed_entry.set(wanted_by)
revente.confirmed_entry = wanted_by
with mock.patch("bda.models.random.choice") as mc:
# Set winner to self.p1.

View file

@ -1,306 +1,216 @@
import json
import os
from datetime import timedelta
from unittest import mock
from urllib.parse import urlencode
from django.contrib.auth import get_user_model
from django.conf import settings
from django.contrib.auth.models import User
from django.core.management import call_command
from django.test import Client, TestCase
from django.urls import reverse
from django.utils import formats, timezone
from django.utils import timezone
from ..models import Participant, Tirage
from .mixins import BdATestHelpers, BdAViewTestCaseMixin
User = get_user_model()
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
class InscriptionViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-tirage-inscription"
def create_user(username, is_cof=False, is_buro=False):
user = User.objects.create_user(username=username, password=username)
user.profile.is_cof = is_cof
user.profile.is_buro = is_buro
user.profile.save()
return user
http_methods = ["GET", "POST"]
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
def user_is_cof(user):
return (user is not None) and user.profile.is_cof
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
def user_is_staff(user):
return (user is not None) and user.profile.is_buro
@property
def url_expected(self):
return "/gestion/bda/inscription/{}".format(self.tirage.id)
def test_get_opened(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
class BdATestHelpers:
def setUp(self):
# Some user with different access privileges
staff = create_user(username="bda_staff", is_cof=True, is_buro=True)
staff_c = Client()
staff_c.force_login(staff)
resp = self.client.get(self.url)
member = create_user(username="bda_member", is_cof=True)
member_c = Client()
member_c.force_login(member)
self.assertEqual(resp.status_code, 200)
self.assertFalse(resp.context["messages"])
other = create_user(username="bda_other")
other_c = Client()
other_c.force_login(other)
def test_get_closed_future(self):
self.tirage.ouverture = timezone.now() + timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=2)
self.tirage.save()
self.client_matrix = [
(staff, staff_c),
(member, member_c),
(other, other_c),
(None, Client()),
]
resp = self.client.get(self.url)
def require_custommails(self):
data_file = os.path.join(
settings.BASE_DIR, "gestioncof", "management", "data", "custommail.json"
)
call_command("syncmails", data_file, verbosity=0)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Le tirage n'est pas encore ouvert : ouverture le {}".format(
formats.localize(timezone.template_localtime(self.tirage.ouverture))
),
[str(msg) for msg in resp.context["messages"]],
def check_restricted_access(
self, url, validate_user=user_is_cof, redirect_url=None
):
def craft_redirect_url(user):
if redirect_url:
return redirect_url
login_url = reverse("account_login")
if url:
login_url += "?{}".format(urlencode({"next": url}, safe="/"))
return login_url
for (user, client) in self.client_matrix:
resp = client.get(url, follow=True)
if validate_user(user):
self.assertEqual(200, resp.status_code)
else:
self.assertRedirects(resp, craft_redirect_url(user))
class TestBdAViews(BdATestHelpers, TestCase):
def setUp(self):
# Set up the helpers
super().setUp()
# Some BdA stuff
self.tirage = Tirage.objects.create(
title="Test tirage",
appear_catalogue=True,
ouverture=timezone.now(),
fermeture=timezone.now(),
)
self.category = CategorieSpectacle.objects.create(name="Category")
self.location = Salle.objects.create(name="here")
Spectacle.objects.bulk_create(
[
Spectacle(
title="foo",
date=timezone.now(),
location=self.location,
price=0,
slots=42,
tirage=self.tirage,
listing=False,
category=self.category,
),
Spectacle(
title="bar",
date=timezone.now(),
location=self.location,
price=1,
slots=142,
tirage=self.tirage,
listing=False,
category=self.category,
),
Spectacle(
title="baz",
date=timezone.now(),
location=self.location,
price=2,
slots=242,
tirage=self.tirage,
listing=False,
category=self.category,
),
]
)
def test_get_closed_past(self):
self.tirage.ouverture = timezone.now() - timedelta(days=2)
self.tirage.fermeture = timezone.now() - timedelta(days=1)
self.tirage.save()
def test_bda_inscriptions(self):
# TODO: test the form
url = "/bda/inscription/{}".format(self.tirage.id)
self.check_restricted_access(url)
resp = self.client.get(self.url)
def test_bda_places(self):
url = "/bda/places/{}".format(self.tirage.id)
self.check_restricted_access(url)
self.assertEqual(resp.status_code, 200)
self.assertIn(
" C'est fini : tirage au sort dans la journée !",
[str(msg) for msg in resp.context["messages"]],
)
def test_etat_places(self):
url = "/bda/etat-places/{}".format(self.tirage.id)
self.check_restricted_access(url)
def get_base_post_data(self):
return {
"choixspectacle_set-TOTAL_FORMS": "3",
"choixspectacle_set-INITIAL_FORMS": "0",
"choixspectacle_set-MIN_NUM_FORMS": "0",
"choixspectacle_set-MAX_NUM_FORMS": "1000",
}
def test_perform_tirage(self):
# Only staff member can perform a tirage
url = "/bda/tirage/{}".format(self.tirage.id)
self.check_restricted_access(url, validate_user=user_is_staff)
base_post_data = property(get_base_post_data)
def test_post(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
data = dict(
self.base_post_data,
**{
"choixspectacle_set-TOTAL_FORMS": "2",
"choixspectacle_set-0-id": "",
"choixspectacle_set-0-participant": "",
"choixspectacle_set-0-spectacle": str(self.show1.pk),
"choixspectacle_set-0-double_choice": "1",
"choixspectacle_set-0-priority": "2",
"choixspectacle_set-1-id": "",
"choixspectacle_set-1-participant": "",
"choixspectacle_set-1-spectacle": str(self.show2.pk),
"choixspectacle_set-1-double_choice": "autoquit",
"choixspectacle_set-1-priority": "1",
}
)
resp = self.client.post(self.url, data)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Votre inscription a été mise à jour avec succès !",
[str(msg) for msg in resp.context["messages"]],
)
participant = Participant.objects.get(
user=self.users["bda_member"], tirage=self.tirage
)
self.assertSetEqual(
set(
participant.choixspectacle_set.values_list(
"priority", "spectacle_id", "double_choice"
)
),
{(1, self.show2.pk, "autoquit"), (2, self.show1.pk, "1")},
)
def test_post_state_changed(self):
self.tirage.ouverture = timezone.now() - timedelta(days=1)
self.tirage.fermeture = timezone.now() + timedelta(days=1)
self.tirage.save()
data = {"dbstate": "different"}
resp = self.client.post(self.url, data)
self.assertEqual(resp.status_code, 200)
self.assertIn(
"Impossible d'enregistrer vos modifications : vous avez apporté d'autres "
"modifications entre temps.",
[str(msg) for msg in resp.context["messages"]],
)
class PlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-places-attribuees"
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/places/{}".format(self.tirage.id)
class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-etat-places"
auth_user = "bda_member"
auth_forbidden = [None, "bda_other"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/etat-places/{}".format(self.tirage.id)
class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-tirage"
http_methods = ["GET", "POST"]
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/tirage/{}".format(self.tirage.id)
def test_perform_tirage_disabled(self):
_, staff_c = self.client_matrix[0]
# Cannot be performed if disabled
self.tirage.enable_do_tirage = False
self.tirage.save()
resp = self.client.get(self.url)
resp = staff_c.get(url)
self.assertTemplateUsed(resp, "tirage-failed.html")
def test_perform_tirage_opened_registrations(self):
# Cannot be performed if registrations are still open
self.tirage.enable_do_tirage = True
self.tirage.fermeture = timezone.now() + timedelta(seconds=3600)
self.tirage.save()
resp = self.client.get(self.url)
resp = staff_c.get(url)
self.assertTemplateUsed(resp, "tirage-failed.html")
def test_perform_tirage(self):
# Otherwise, perform the tirage
self.tirage.enable_do_tirage = True
self.tirage.fermeture = timezone.now()
self.tirage.save()
resp = self.client.get(self.url)
resp = staff_c.get(url)
self.assertTemplateNotUsed(resp, "tirage-failed.html")
def test_spectacles_list(self):
url = "/bda/spectacles/{}".format(self.tirage.id)
self.check_restricted_access(url, validate_user=user_is_staff)
class SpectacleListViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-liste-spectacles"
def test_spectacle_detail(self):
show = self.tirage.spectacle_set.first()
url = "/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
self.check_restricted_access(url, validate_user=user_is_staff)
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
def test_tirage_unpaid(self):
url = "/bda/spectacles/unpaid/{}".format(self.tirage.id)
self.check_restricted_access(url, validate_user=user_is_staff)
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/spectacles/{}".format(self.tirage.id)
class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-spectacle"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id, "spectacle_id": self.show1.id}
@property
def url_expected(self):
return "/gestion/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id)
class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-unpaid"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"tirage_id": self.tirage.id}
@property
def url_expected(self):
return "/gestion/bda/spectacles/unpaid/{}".format(self.tirage.id)
class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
url_name = "bda-rappels"
auth_user = "bda_staff"
auth_forbidden = [None, "bda_other", "bda_member"]
bda_testdata = True
@property
def url_kwargs(self):
return {"spectacle_id": self.show1.id}
@property
def url_expected(self):
return "/gestion/bda/mails-rappel/{}".format(self.show1.id)
def test_post(self):
resp = self.client.post(self.url)
def test_send_reminders(self):
self.require_custommails()
# Just get the page
show = self.tirage.spectacle_set.first()
url = "/bda/mails-rappel/{}".format(show.id)
self.check_restricted_access(url, validate_user=user_is_staff)
# Actually send the reminder emails
_, staff_c = self.client_matrix[0]
resp = staff_c.post(url)
self.assertEqual(200, resp.status_code)
# TODO: check that emails are sent
def test_catalogue_api(self):
url_list = "/bda/catalogue/list"
url_details = "/bda/catalogue/details?id={}".format(self.tirage.id)
url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id)
class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
auth_user = None
auth_forbidden = []
# Anyone can get
def anyone_can_get(url):
self.check_restricted_access(url, validate_user=lambda user: True)
bda_testdata = True
anyone_can_get(url_list)
anyone_can_get(url_details)
anyone_can_get(url_descriptions)
def test_api_list(self):
url_list = "/gestion/bda/catalogue/list"
resp = self.client.get(url_list)
# The resulting JSON contains the information
_, client = self.client_matrix[0]
# List
resp = client.get(url_list)
self.assertJSONEqual(
resp.content.decode("utf-8"),
[{"id": self.tirage.id, "title": self.tirage.title}],
)
def test_api_details(self):
url_details = "/gestion/bda/catalogue/details?id={}".format(self.tirage.id)
resp = self.client.get(url_details)
# Details
resp = client.get(url_details)
self.assertJSONEqual(
resp.content.decode("utf-8"),
{
@ -309,11 +219,8 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
},
)
def test_api_descriptions(self):
url_descriptions = "/gestion/bda/catalogue/descriptions?id={}".format(
self.tirage.id
)
resp = self.client.get(url_descriptions)
# Descriptions
resp = client.get(url_descriptions)
raw = resp.content.decode("utf-8")
try:
results = json.loads(raw)
@ -326,43 +233,6 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase):
)
# ----- BdA Revente --------------------------------------- #
def make_participant(name: str, tirage: Tirage) -> User:
user = User.objects.create_user(username=name, password=name)
user.profile.is_cof = True
user.profile.save()
Participant.objects.create(user=user, tirage=tirage)
return user
class TestReventeManageTest(TestCase):
def setUp(self):
self.tirage = Tirage.objects.create(
title="tirage1",
ouverture=timezone.now(),
fermeture=timezone.now() + timedelta(days=90),
)
self.user = make_participant("toto", self.tirage)
self.url = reverse("bda-revente-manage", args=[self.tirage.id])
# Signals handlers on login/logout send messages.
# Due to the way the Django' test Client performs login, this raise an
# error. As workaround, we mock the Django' messages module.
patcher_messages = mock.patch("gestioncof.signals.messages")
patcher_messages.start()
self.addCleanup(patcher_messages.stop)
def test_can_get(self):
client = Client()
client.force_login(
self.user, backend="django.contrib.auth.backends.ModelBackend"
)
r = client.get(self.url)
self.assertEqual(r.status_code, 200)
class TestBdaRevente:
pass
# TODO

View file

@ -1,36 +0,0 @@
from datetime import timedelta
from django.contrib.auth.models import User
from django.utils import timezone
from ..models import CategorieSpectacle, Salle, Spectacle, Tirage
def create_user(username, is_cof=False, is_buro=False):
user = User.objects.create_user(username=username, password=username)
user.profile.is_cof = is_cof
user.profile.is_buro = is_buro
user.profile.save()
return user
def user_is_cof(user):
return (user is not None) and user.profile.is_cof
def user_is_staff(user):
return (user is not None) and user.profile.is_buro
def create_spectacle(**kwargs):
defaults = {
"title": "Title",
"category": CategorieSpectacle.objects.first(),
"date": (timezone.now() + timedelta(days=7)).date(),
"location": Salle.objects.first(),
"price": 10.0,
"slots": 20,
"tirage": Tirage.objects.first(),
"listing": False,
}
return Spectacle.objects.create(**dict(defaults, **kwargs))

View file

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

View file

@ -4,19 +4,19 @@ import random
import time
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.contrib import messages
from django.contrib.auth.decorators import login_required
from django.core import serializers
from django.core.exceptions import NON_FIELD_ERRORS
from django.core.mail import send_mail, send_mass_mail
from django.core.urlresolvers import reverse
from django.db import transaction
from django.db.models import Count, Prefetch
from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
from django.shortcuts import get_object_or_404, render
from django.template import loader
from django.template.defaultfilters import pluralize
from django.urls import reverse
from django.utils import formats, timezone
from django.views.generic.list import ListView
@ -41,8 +41,8 @@ from bda.models import (
SpectacleRevente,
Tirage,
)
from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required
from shared.views import Select2QuerySetView
from gestioncof.decorators import buro_required, cof_required
from utils.views.autocomplete import Select2QuerySetView
@cof_required
@ -188,40 +188,22 @@ def inscription(request, tirage_id):
ChoixSpectacle,
fields=("spectacle", "double_choice", "priority"),
formset=InscriptionInlineFormSet,
error_messages={
NON_FIELD_ERRORS: {
"unique_together": "Vous avez déjà demandé ce voeu plus haut !"
}
},
)
success = False
stateerror = False
if request.method == "POST":
# use *this* queryset
dbstate = _hash_queryset(participant.choixspectacle_set.all())
if "dbstate" in request.POST and dbstate != request.POST["dbstate"]:
stateerror = True
formset = BdaFormSet(instance=participant)
messages.error(
request,
"Impossible d'enregistrer vos modifications "
": vous avez apporté d'autres modifications "
"entre temps.",
)
else:
formset = BdaFormSet(request.POST, instance=participant)
if formset.is_valid():
formset.save()
success = True
formset = BdaFormSet(instance=participant)
participant.accepte_charte = True
participant.save()
messages.success(
request, "Votre inscription a été mise à jour avec succès !"
)
else:
messages.error(
request,
"Une erreur s'est produite lors de l'enregistrement de vos vœux. "
"Avez-vous demandé plusieurs fois le même spectacle ?",
)
else:
formset = BdaFormSet(instance=participant)
# use *this* queryset
@ -232,6 +214,18 @@ def inscription(request, tirage_id):
total_price += choice.spectacle.price
if choice.double:
total_price += choice.spectacle.price
# Messages
if success:
messages.success(
request, "Votre inscription a été mise à jour avec " "succès !"
)
if stateerror:
messages.error(
request,
"Impossible d'enregistrer vos modifications "
": vous avez apporté d'autres modifications "
"entre temps.",
)
return render(
request,
"bda/inscription-tirage.html",
@ -240,7 +234,6 @@ def inscription(request, tirage_id):
"total_price": total_price,
"dbstate": dbstate,
"tirage": tirage,
"charte": participant.accepte_charte,
},
)
@ -274,13 +267,13 @@ def do_tirage(tirage_elt, token):
results = Algorithm(data["shows"], data["members"], choices)(token)
# 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_losers"] += len(losers)
# On calcule le déficit et les bénéfices pour le BdA
# 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
data["total_sold"] += show.slots * show.price
if deficit >= 0:
@ -293,8 +286,8 @@ def do_tirage(tirage_elt, token):
# so assign a single object for each Participant id
members_uniq = {}
members2 = {}
for show, members, _ in results:
for member, _, _, _ in members:
for (show, members, _) in results:
for (member, _, _, _) in members:
if member.id not in members_uniq:
members_uniq[member.id] = member
members2[member] = []
@ -370,7 +363,7 @@ def tirage(request, tirage_id):
return render(request, "bda-token.html", {"form": form})
@cof_required
@login_required
def revente_manage(request, tirage_id):
"""
Gestion de ses propres reventes :
@ -381,10 +374,13 @@ def revente_manage(request, tirage_id):
- Annulation d'une revente après que le tirage a eu lieu
"""
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.annotate_paid().get_or_create(
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage
)
if not participant.paid:
return render(request, "bda/revente/notpaid.html", {})
resellform = ResellForm(participant, prefix="resell")
annulform = AnnulForm(participant, prefix="annul")
soldform = SoldForm(participant, prefix="sold")
@ -394,7 +390,7 @@ def revente_manage(request, tirage_id):
if "resell" in request.POST:
resellform = ResellForm(participant, request.POST, prefix="resell")
if resellform.is_valid():
mails = []
datatuple = []
attributions = resellform.cleaned_data["attributions"]
with transaction.atomic():
for attribution in attributions:
@ -409,17 +405,16 @@ def revente_manage(request, tirage_id):
"show": attribution.spectacle,
"revente": revente,
}
mails.append(
datatuple.append(
(
"BdA-Revente : {}".format(attribution.spectacle),
loader.render_to_string(
"bda/mails/revente-seller.txt", context=context
),
"bda-revente-new",
context,
settings.MAIL_DATA["revente"]["FROM"],
[participant.user.email],
)
)
send_mass_mail(mails)
revente.save()
send_mass_custom_mail(datatuple)
# On annule une revente
elif "annul" in request.POST:
annulform = AnnulForm(participant, request.POST, prefix="annul")
@ -450,10 +445,6 @@ def revente_manage(request, tirage_id):
new_date = timezone.now() - SpectacleRevente.remorse_time
revente.reset(new_date=new_date)
sold_exists = soldform.fields["reventes"].queryset.exists()
annul_exists = annulform.fields["reventes"].queryset.exists()
resell_exists = resellform.fields["attributions"].queryset.exists()
return render(
request,
"bda/revente/manage.html",
@ -462,14 +453,11 @@ def revente_manage(request, tirage_id):
"soldform": soldform,
"annulform": annulform,
"resellform": resellform,
"sold_exists": sold_exists,
"annul_exists": annul_exists,
"resell_exists": resell_exists,
},
)
@cof_required
@login_required
def revente_tirages(request, tirage_id):
"""
Affiche à un participant la liste de toutes les reventes en cours (pour un
@ -512,22 +500,14 @@ def revente_tirages(request, tirage_id):
),
)
annul_exists = annulform.fields["reventes"].queryset.exists()
sub_exists = subform.fields["reventes"].queryset.exists()
return render(
request,
"bda/revente/tirages.html",
{
"annulform": annulform,
"subform": subform,
"annul_exists": annul_exists,
"sub_exists": sub_exists,
},
{"annulform": annulform, "subform": subform},
)
@cof_required
@login_required
def revente_confirm(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, _ = Participant.objects.get_or_create(
@ -544,7 +524,7 @@ def revente_confirm(request, revente_id):
)
@cof_required
@login_required
def revente_subscribe(request, tirage_id):
"""
Permet à un participant de sélectionner ses préférences pour les reventes.
@ -561,7 +541,7 @@ def revente_subscribe(request, tirage_id):
form = InscriptionReventeForm(tirage, request.POST)
if form.is_valid():
choices = form.cleaned_data["spectacles"]
participant.choicesrevente.set(choices)
participant.choicesrevente = choices
participant.save()
for spectacle in choices:
qset = SpectacleRevente.objects.filter(attribution__spectacle=spectacle)
@ -589,18 +569,18 @@ def revente_subscribe(request, tirage_id):
)
# Messages
if success:
messages.success(request, "Votre inscription a bien été prise en compte")
messages.success(request, "Ton inscription a bien été prise en compte")
if deja_revente:
messages.info(
request,
"Des reventes existent déjà pour certains de "
"ces spectacles, vérifiez les places "
"ces spectacles, vérifie les places "
"disponibles sans tirage !",
)
if inscrit_revente:
shows = map("<li>{!s}</li>".format, inscrit_revente)
msg = (
"Vous avez été inscrit·e à des reventes en cours pour les spectacles "
"Tu as été inscrit à des reventes en cours pour les spectacles "
"<ul>{:s}</ul>".format("\n".join(shows))
)
messages.info(request, msg, extra_tags="safe")
@ -608,7 +588,7 @@ def revente_subscribe(request, tirage_id):
return render(request, "bda/revente/subscribe.html", {"form": form})
@cof_required
@login_required
def revente_buy(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage
@ -638,16 +618,12 @@ def revente_buy(request, spectacle_id):
"acheteur": request.user,
"vendeur": revente.seller.user,
}
send_mail(
"BdA-Revente : {}".format(spectacle.title),
loader.render_to_string(
"bda/mails/revente-shotgun-seller.txt", context=context
),
request.user.email,
send_custom_mail(
"bda-buy-shotgun",
"bda@ens.fr",
[revente.seller.user.email],
context=context,
)
return render(
request,
"bda/revente/mail-success.html",
@ -661,7 +637,7 @@ def revente_buy(request, spectacle_id):
)
@cof_required
@login_required
def revente_shotgun(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles = (
@ -681,7 +657,7 @@ def revente_shotgun(request, tirage_id):
)
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
return render(request, "bda/revente/shotgun.html", {"spectacles": shotgun})
return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun})
@buro_required
@ -700,13 +676,12 @@ def spectacle(request, tirage_id, spectacle_id):
"username": participant.user.username,
"email": participant.user.email,
"given": int(attrib.given),
"paid": attrib.paid,
"paid": participant.paid,
"nb_places": 1,
}
if participant.id in participants:
participants[participant.id]["nb_places"] += 1
participants[participant.id]["given"] += attrib.given
participants[participant.id]["paid"] &= attrib.paid
else:
participants[participant.id] = participant_info
@ -734,37 +709,34 @@ class SpectacleListView(ListView):
return context
class UnpaidParticipants(BuroRequiredMixin, ListView):
context_object_name = "unpaid"
template_name = "bda-unpaid.html"
def get_queryset(self):
return (
Participant.objects.annotate_paid()
.filter(tirage__id=self.kwargs["tirage_id"], paid=False)
.select_related("user")
)
@buro_required
def unpaid(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
unpaid = (
tirage.participant_set.annotate(nb_attributions=Count("attribution"))
.filter(paid=False, nb_attributions__gt=0)
.select_related("user")
)
return render(request, "bda-unpaid.html", {"unpaid": unpaid})
@buro_required
def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples
subject = show.title
body_mail_1place = loader.render_to_string(
"bda/mails/rappel.txt",
context={"member": request.user, "show": show, "nb_attr": 1},
custommail = CustomMail.objects.get(shortname="bda-rappel")
exemple_mail_1place = custommail.render(
{"member": request.user, "show": show, "nb_attr": 1}
)
body_mail_2places = loader.render_to_string(
"bda/mails/rappel.txt",
context={"member": request.user, "show": show, "nb_attr": 2},
exemple_mail_2places = custommail.render(
{"member": request.user, "show": show, "nb_attr": 2}
)
# Contexte
ctxt = {
"show": show,
"exemple_mail_1place": (subject, body_mail_1place),
"exemple_mail_2places": (subject, body_mail_2places),
"exemple_mail_1place": exemple_mail_1place,
"exemple_mail_2places": exemple_mail_2places,
"custommail": custommail,
}
# Envoi confirmé
if request.method == "POST":
@ -785,6 +757,25 @@ def send_rappel(request, spectacle_id):
return render(request, "bda/mails-rappel.html", ctxt)
def descriptions_spectacles(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
shows_qs = tirage.spectacle_set.select_related("location").prefetch_related(
"quote_set"
)
category_name = request.GET.get("category", "")
location_id = request.GET.get("location", "")
if category_name:
shows_qs = shows_qs.filter(category__name=category_name)
if location_id:
try:
shows_qs = shows_qs.filter(location__id=int(location_id))
except ValueError:
return HttpResponseBadRequest(
"La variable GET 'location' doit contenir un entier"
)
return render(request, "descriptions.html", {"shows": shows_qs})
def catalogue(request, request_type):
"""
Vue destinée à communiquer avec un client AJAX, fournissant soit :

View file

@ -1,5 +0,0 @@
from django.contrib import admin
from bds.models import BDSProfile
admin.site.register(BDSProfile)

View file

@ -1,28 +0,0 @@
from django.apps import AppConfig, apps as global_apps
from django.db.models import Q
from django.db.models.signals import post_migrate
def bds_group_perms(app_config, apps=global_apps, **kwargs):
try:
Permission = apps.get_model("auth", "Permission")
Group = apps.get_model("auth", "Group")
group = Group.objects.get(name="Burô du BDS")
perms = Permission.objects.filter(
Q(content_type__app_label="bds")
| Q(content_type__app_label="auth") & Q(content_type__model="user")
)
group.permissions.set(perms)
group.save()
except (LookupError, Group.DoesNotExist):
return
class BdsConfig(AppConfig):
name = "bds"
verbose_name = "Gestion des adhérent·e·s du BDS"
def ready(self):
post_migrate.connect(bds_group_perms, sender=self)

View file

@ -1,63 +0,0 @@
from urllib.parse import urlencode
from django.contrib.auth import get_user_model
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from shared import autocomplete
User = get_user_model()
class BDSMemberSearch(autocomplete.ModelSearch):
model = User
search_fields = ["username", "first_name", "last_name"]
verbose_name = _("Membres du BDS")
def get_queryset_filter(self, *args, **kwargs):
qset_filter = super().get_queryset_filter(*args, **kwargs)
qset_filter &= Q(bds__is_member=True)
return qset_filter
def result_uuid(self, user):
return user.username
def result_link(self, user):
return reverse("bds:user.update", args=(user.pk,))
class BDSOthersSearch(autocomplete.ModelSearch):
model = User
search_fields = ["username", "first_name", "last_name"]
verbose_name = _("Non-membres du BDS")
def get_queryset_filter(self, *args, **kwargs):
qset_filter = super().get_queryset_filter(*args, **kwargs)
qset_filter &= Q(bds__isnull=True) | Q(bds__is_member=False)
return qset_filter
def result_uuid(self, user):
return user.username
def result_link(self, user):
return reverse("bds:user.update", args=(user.pk,))
class BDSLDAPSearch(autocomplete.LDAPSearch):
def result_link(self, clipper):
url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,))
get = {"fullname": clipper.fullname, "mail": clipper.mail}
return "{}?{}".format(url, urlencode(get))
class BDSSearch(autocomplete.Compose):
search_units = [
("members", BDSMemberSearch()),
("others", BDSOthersSearch()),
("clippers", BDSLDAPSearch()),
]
bds_search = BDSSearch()

View file

@ -1,41 +0,0 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from django.utils.translation import gettext_lazy as _
from bds.models import BDSProfile
User = get_user_model()
class UserForm(forms.ModelForm):
is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False)
class Meta:
model = User
fields = ["email", "first_name", "last_name"]
class UserFromClipperForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["username"].disabled = True
class Meta:
model = User
fields = ["username", "email", "first_name", "last_name"]
class UserFromScratchForm(UserCreationForm):
class Meta:
model = User
fields = ["username", "email", "first_name", "last_name"]
class ProfileForm(forms.ModelForm):
class Meta:
model = BDSProfile
exclude = ["user"]
widgets = {
"birthdate": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d")
}

View file

@ -1,141 +0,0 @@
# Generated by Django 2.2 on 2019-07-17 12:48
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import bds.models
class Migration(migrations.Migration):
initial = True
dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
operations = [
migrations.CreateModel(
name="BDSProfile",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"phone",
models.CharField(
blank=True, max_length=20, verbose_name="téléphone"
),
),
(
"occupation",
models.CharField(
choices=[
("EXT", "Extérieur"),
("1A", "1A"),
("2A", "2A"),
("3A", "3A"),
("4A", "4A"),
("MAG", "Magistérien"),
("ARC", "Archicube"),
("DOC", "Doctorant"),
("CST", "CST"),
("PER", "Personnel ENS"),
],
default="1A",
max_length=3,
verbose_name="occupation",
),
),
(
"departement",
models.CharField(
blank=True, max_length=50, verbose_name="département"
),
),
(
"birthdate",
models.DateField(
blank=True, null=True, verbose_name="date de naissance"
),
),
(
"mails_bds",
models.BooleanField(
default=False, verbose_name="recevoir les mails du BDS"
),
),
(
"is_buro",
models.BooleanField(
default=False, verbose_name="membre du Burô du BDS"
),
),
(
"has_certificate",
models.BooleanField(
default=False, verbose_name="certificat médical"
),
),
(
"certificate_file",
models.FileField(
blank=True,
upload_to=bds.models.BDSProfile.get_certificate_filename,
verbose_name="fichier de certificat médical",
),
),
(
"ASPSL_number",
models.CharField(
blank=True,
max_length=50,
null=True,
verbose_name="numéro AS PSL",
),
),
(
"FFSU_number",
models.CharField(
blank=True, max_length=50, null=True, verbose_name="numéro FFSU"
),
),
(
"cotisation_period",
models.CharField(
choices=[
("ANN", "Année"),
("SE1", "Premier semestre"),
("SE2", "Deuxième semestre"),
("NO", "Aucune"),
],
default="NO",
max_length=3,
verbose_name="inscription",
),
),
(
"registration_date",
models.DateField(
auto_now_add=True, verbose_name="date d'inscription"
),
),
(
"user",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="bds",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Profil BDS",
"verbose_name_plural": "Profils BDS",
},
)
]

View file

@ -1,16 +0,0 @@
# Generated by Django 2.2 on 2019-07-17 14:56
from django.db import migrations
def create_bds_buro_group(apps, schema_editor):
Group = apps.get_model("auth", "Group")
Group.objects.get_or_create(name="Burô du BDS")
class Migration(migrations.Migration):
dependencies = [("bds", "0001_initial")]
operations = [
migrations.RunPython(create_bds_buro_group, migrations.RunPython.noop)
]

View file

@ -1,24 +0,0 @@
# Generated by Django 2.2.8 on 2019-12-20 22:48
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bds", "0002_bds_group"),
]
operations = [
migrations.AlterModelOptions(
name="bdsprofile",
options={
"permissions": (("is_team", "est membre du burô"),),
"verbose_name": "Profil BDS",
"verbose_name_plural": "Profils BDS",
},
),
migrations.RemoveField(
model_name="bdsprofile",
name="is_buro",
),
]

View file

@ -1,33 +0,0 @@
# Generated by Django 2.2.8 on 2019-12-22 10:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bds", "0003_staff_permission"),
]
operations = [
migrations.AddField(
model_name="bdsprofile",
name="cotisation_type",
field=models.CharField(
choices=[
("ETU", "Étudiant"),
("NOR", "Normalien"),
("EXT", "Extérieur"),
("ARC", "Archicube"),
],
default="Normalien",
max_length=9,
verbose_name="type de cotisation",
),
preserve_default=False,
),
migrations.AddField(
model_name="bdsprofile",
name="is_member",
field=models.BooleanField(default=False, verbose_name="adhérent⋅e du BDS"),
),
]

View file

@ -1,16 +0,0 @@
# Generated by Django 2.2.14 on 2020-07-27 20:14
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("bds", "0004_is_member_cotiz_type"),
]
operations = [
migrations.RemoveField(
model_name="bdsprofile",
name="certificate_file",
),
]

View file

@ -1,22 +0,0 @@
# Generated by Django 2.2.12 on 2020-08-28 12:14
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("bds", "0005_remove_bdsprofile_certificate_file"),
]
operations = [
migrations.AddField(
model_name="bdsprofile",
name="comments",
field=models.TextField(
blank=True,
help_text="Attention : l'utilisateur·ice dispose d'un droit d'accès"
" aux données le/la concernant, dont le contenu de ce champ !",
verbose_name="commentaires",
),
),
]

View file

@ -1,122 +0,0 @@
from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
class StaffRequiredMixin(PermissionRequiredMixin):
permission_required = "bds.is_team"
class MultipleFormMixin(ContextMixin):
"""Mixin pour gérer plusieurs formulaires dans la même vue.
Le fonctionnement est relativement identique à celui de
FormMixin, dont la documentation est disponible ici :
https://docs.djangoproject.com/en/3.0/ref/class-based-views/mixins-editing/
Les principales différences sont :
- au lieu de form_class, il faut donner comme attribut un dict de la forme
{<form_name>: <form_class>}, avec tous les formulaires à instancier. On
peut aussi redéfinir `get_form_classes`
- les données initiales se récupèrent pour chaque form via l'attribut
`<form_name>_initial` ou la fonction `get_<form_name>_initial`. De même,
si certaines forms sont des `ModelForm`s, on peut définir la fonction
`get_<form_name>_instance`.
- chaque form a un préfixe rajouté, par défaut <form_name>, mais qui peut
être customisé via `prefixes` ou `get_prefixes`.
"""
form_classes = {}
prefixes = {}
initial = {}
success_url = None
def get_form_classes(self):
return self.form_classes
def get_initial(self, form_name):
initial_attr = "%s_initial" % form_name
initial_method = "get_%s_initial" % form_name
initial_method = getattr(self, initial_method, None)
if hasattr(self, initial_attr):
return getattr(self, initial_attr)
elif callable(initial_method):
return initial_method()
else:
return self.initial.copy()
def get_prefix(self, form_name):
return self.prefixes.get(form_name, form_name)
def get_instance(self, form_name):
# Au cas où certaines des forms soient des ModelForms
instance_method = "get_%s_instance" % form_name
instance_method = getattr(self, instance_method, None)
if callable(instance_method):
return instance_method()
else:
return None
def get_form_kwargs(self, form_name):
kwargs = {
"initial": self.get_initial(form_name),
"prefix": self.get_prefix(form_name),
"instance": self.get_instance(form_name),
}
if self.request.method in ("POST", "PUT"):
kwargs.update({"data": self.request.POST, "files": self.request.FILES})
return kwargs
def get_forms(self):
form_classes = self.get_form_classes()
return {
form_name: form_class(**self.get_form_kwargs(form_name))
for form_name, form_class in form_classes.items()
}
def get_success_url(self):
if not self.success_url:
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
return str(self.success_url)
def form_valid(self, forms):
# on garde le nom form_valid pour l'interface avec SuccessMessageMixin
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, forms):
"""If the form is invalid, render the invalid form."""
return self.render_to_response(self.get_context_data(forms=forms))
class ProcessMultipleFormView(View):
"""Équivalent de `ProcessFormView` pour plusieurs forms.
Note : il faut que *tous* les formulaires soient valides pour
qu'ils soient sauvegardés !
"""
def get(self, request, *args, **kwargs):
forms = self.get_forms()
return self.render_to_response(self.get_context_data(forms=forms))
def post(self, request, *args, **kwargs):
forms = self.get_forms()
if all(form.is_valid() for form in forms.values()):
return self.form_valid(forms)
else:
return self.form_invalid(forms)
class BaseMultipleFormView(MultipleFormMixin, ProcessMultipleFormView):
pass
class MultipleFormView(TemplateResponseMixin, BaseMultipleFormView):
pass

View file

@ -1,113 +0,0 @@
from datetime import date
from os.path import splitext
from django.contrib.auth import get_user_model
from django.db import models
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from shared.utils import choices_length
User = get_user_model()
class BDSProfile(models.Model):
OCCUPATION_CHOICES = (
("EXT", "Extérieur"),
("1A", "1A"),
("2A", "2A"),
("3A", "3A"),
("4A", "4A"),
("MAG", "Magistérien"),
("ARC", "Archicube"),
("DOC", "Doctorant"),
("CST", "CST"),
("PER", "Personnel ENS"),
)
TYPE_COTIZ_CHOICES = (
("ETU", "Étudiant"),
("NOR", "Normalien"),
("EXT", "Extérieur"),
("ARC", "Archicube"),
)
COTIZ_DURATION_CHOICES = (
("ANN", "Année"),
("SE1", "Premier semestre"),
("SE2", "Deuxième semestre"),
("NO", "Aucune"),
)
def get_certificate_filename(instance, filename):
_, ext = splitext(filename) # récupère l'extension du fichier
year = str(date.now().year)
return "certifs/{username}-{year}.{ext}".format(
username=instance.user.username, year=year, ext=ext
)
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="bds")
phone = models.CharField(_("téléphone"), max_length=20, blank=True)
occupation = models.CharField(
_("occupation"),
default="1A",
choices=OCCUPATION_CHOICES,
max_length=choices_length(OCCUPATION_CHOICES),
)
departement = models.CharField(_("département"), max_length=50, blank=True)
birthdate = models.DateField(
auto_now_add=False,
auto_now=False,
verbose_name=_("date de naissance"),
blank=True,
null=True,
)
is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False)
mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False)
has_certificate = models.BooleanField(_("certificat médical"), default=False)
ASPSL_number = models.CharField(
_("numéro AS PSL"), max_length=50, blank=True, null=True
)
FFSU_number = models.CharField(
_("numéro FFSU"), max_length=50, blank=True, null=True
)
cotisation_period = models.CharField(
_("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3
)
registration_date = models.DateField(
auto_now_add=True, verbose_name=_("date d'inscription")
)
cotisation_type = models.CharField(
_("type de cotisation"), choices=TYPE_COTIZ_CHOICES, max_length=9
)
comments = models.TextField(
_("commentaires"),
blank=True,
help_text=_(
"Attention : l'utilisateur·ice dispose d'un droit d'accès aux données "
"le/la concernant, dont le contenu de ce champ !"
),
)
@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:
verbose_name = _("Profil BDS")
verbose_name_plural = _("Profils BDS")
permissions = (("is_team", _("est membre du burô")),)
def __str__(self):
return self.user.username

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,15 +0,0 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN" "http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg" width="300px" height="246px" viewBox="0 0 3000 2460" preserveAspectRatio="xMidYMid meet">
<g id="layer101" fill="#ffffff" stroke="none">
<path d="M0 1230 l0 -1230 1500 0 1500 0 0 1230 0 1230 -1500 0 -1500 0 0 -1230z"/>
</g>
<g id="layer102" fill="#3e2263" stroke="none">
<path d="M0 1230 l0 -1230 1500 0 1500 0 0 1230 0 1230 -1500 0 -1500 0 0 -1230z m1716 765 c103 -17 204 -46 284 -83 l55 -25 -40 -8 c-80 -16 -169 -10 -245 16 -123 42 -232 59 -385 59 -126 0 -278 -18 -341 -40 -13 -5 -16 -4 -9 4 14 15 166 60 254 76 102 19 315 19 427 1z m-703 -217 c15 -6 27 -13 27 -17 0 -4 -37 -25 -82 -47 -86 -43 -182 -120 -193 -155 -8 -26 32 -113 78 -167 44 -52 46 -62 10 -62 -39 1 -106 33 -165 80 -40 31 -54 38 -64 29 -21 -18 -17 -50 16 -116 57 -113 151 -199 259 -235 49 -16 51 -18 16 -13 -123 19 -260 151 -322 310 -15 39 -14 -81 1 -147 25 -107 55 -176 112 -251 49 -65 52 -73 38 -88 -21 -23 -104 -62 -216 -100 -51 -18 -97 -35 -102 -40 -4 -4 32 -10 80 -14 106 -9 256 13 317 48 l39 22 56 -33 c32 -18 114 -61 184 -95 80 -39 126 -67 123 -74 -6 -19 12 -16 19 3 9 24 28 5 22 -22 -7 -26 8 -34 18 -9 11 29 28 16 21 -18 -6 -30 -5 -31 10 -13 8 11 15 24 15 29 0 4 5 5 10 2 6 -3 8 -18 4 -33 -5 -26 -5 -26 10 -8 15 19 18 19 118 -12 113 -35 284 -72 329 -72 24 0 29 4 29 25 0 34 -36 125 -63 158 l-22 28 -59 -30 c-74 -39 -165 -43 -233 -10 -24 11 -43 25 -43 30 0 4 28 6 63 2 49 -4 71 -2 98 12 46 23 79 55 79 75 0 16 -48 66 -135 139 l-40 33 43 -23 c55 -30 166 -121 188 -154 9 -13 13 -29 9 -36 -15 -23 -100 -69 -129 -69 -17 0 -38 -5 -46 -10 -12 -8 -10 -10 10 -10 49 1 104 19 149 51 l45 31 24 -28 c33 -40 72 -128 79 -180 10 -79 -25 -96 -233 -114 -84 -7 -148 -7 -218 0 -106 12 -269 51 -317 76 -28 14 -31 13 -62 -10 -56 -43 -167 -89 -225 -94 l-55 -5 6 29 c2 16 14 55 26 86 21 57 27 112 15 142 -3 9 -34 38 -68 64 l-62 49 -104 7 c-166 12 -192 34 -83 70 34 12 65 24 68 28 4 4 -7 27 -24 52 -118 171 -124 402 -16 571 18 29 50 88 70 131 42 93 99 154 168 184 82 35 212 44 280 18z m540 -39 c60 -16 70 -41 75 -196 l5 -134 -39 3 -39 3 -5 114 c-4 93 -8 116 -22 125 -31 19 -41 -7 -47 -126 l-6 -113 -32 -3 c-41 -4 -44 6 -34 151 9 143 25 172 101 186 3 0 22 -4 43 -10z m322 -34 c0 -30 0 -30 -64 -33 l-64 -3 7 -130 7 -129 -35 0 -36 0 0 165 0 166 93 -3 92 -3 0 -30z m115 -55 l5 -84 18 27 c21 33 39 34 60 5 15 -22 16 -18 16 60 l1 82 35 0 35 0 -2 -162 -3 -163 -40 0 c-38 0 -41 2 -53 43 -16 49 -24 48 -42 -7 -12 -38 -16 -41 -50 -41 -34 0 -38 3 -44 31 -8 43 -8 284 1 293 4 4 19 6 33 4 24 -3 25 -6 30 -88z m370 -42 c193 -243 228 -505 100 -759 -61 -120 -195 -260 -328 -343 -66 -41 -60 -26 13 33 36 29 92 85 125 126 182 230 209 500 75 753 -42 79 -45 89 -45 163 0 44 4 79 9 79 5 0 28 -24 51 -52z m-1111 -294 c76 -30 94 -102 41 -156 -27 -27 -29 -32 -17 -47 34 -40 25 -100 -19 -120 -30 -13 -143 -15 -164 -1 -11 7 -15 42 -18 164 l-4 156 23 9 c37 15 115 13 158 -5z m571 -9 c84 -43 61 -135 -43 -173 -46 -18 -52 -23 -52 -48 0 -39 30 -42 79 -9 l37 25 -3 -41 c-4 -49 -32 -76 -90 -85 -32 -6 -41 -2 -68 24 -24 25 -30 39 -30 74 0 57 21 85 87 114 40 18 54 29 51 42 -5 29 -71 28 -111 -2 -19 -14 -36 -26 -39 -26 -11 0 -16 55 -7 72 24 45 131 63 189 33z m-303 -12 c56 -26 75 -65 71 -150 -3 -67 -6 -75 -38 -108 -43 -44 -94 -61 -146 -48 l-39 9 -3 144 c-1 79 0 150 2 157 8 19 110 17 153 -4z"/>
<path d="M1503 633 c4 -3 10 -3 14 0 3 4 0 7 -7 7 -7 0 -10 -3 -7 -7z"/>
<path d="M1532 448 c3 -7 15 -14 29 -16 23 -2 23 -2 5 13 -24 18 -39 20 -34 3z"/>
<path d="M1140 1231 c0 -45 10 -61 36 -61 26 0 64 32 64 53 0 23 -23 37 -62 37 -35 0 -38 -2 -38 -29z"/>
<path d="M1140 1070 c0 -35 17 -45 56 -36 20 5 25 12 22 34 -3 23 -8 27 -40 30 -36 3 -38 2 -38 -28z"/>
<path d="M1430 1151 c0 -72 3 -91 14 -91 28 0 47 13 61 41 31 60 10 121 -47 135 l-28 6 0 -91z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View file

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns="http://www.w3.org/2000/svg"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 2560 2560"
height="256"
width="256"
version="1.0">
<g
transform="translate(-220,50)"
stroke="none"
fill="#ffffff"
id="layer101">
<path
id="path2"
d="M 0,1230 V 0 H 1500 3000 V 1230 2460 H 1500 0 Z" />
</g>
<g
transform="translate(-220,50)"
stroke="none"
fill="#3e2263"
id="layer102">
<path
id="path5"
d="M 220,1230 V -50 H 1500 2780 V 1230 2510 H 1500 220 Z m 1496,765 c 103,-17 204,-46 284,-83 l 55,-25 -40,-8 c -80,-16 -169,-10 -245,16 -123,42 -232,59 -385,59 -126,0 -278,-18 -341,-40 -13,-5 -16,-4 -9,4 14,15 166,60 254,76 102,19 315,19 427,1 z m -703,-217 c 15,-6 27,-13 27,-17 0,-4 -37,-25 -82,-47 -86,-43 -182,-120 -193,-155 -8,-26 32,-113 78,-167 44,-52 46,-62 10,-62 -39,1 -106,33 -165,80 -40,31 -54,38 -64,29 -21,-18 -17,-50 16,-116 57,-113 151,-199 259,-235 49,-16 51,-18 16,-13 -123,19 -260,151 -322,310 -15,39 -14,-81 1,-147 25,-107 55,-176 112,-251 49,-65 52,-73 38,-88 -21,-23 -104,-62 -216,-100 -51,-18 -97,-35 -102,-40 -4,-4 32,-10 80,-14 106,-9 256,13 317,48 l 39,22 56,-33 c 32,-18 114,-61 184,-95 80,-39 126,-67 123,-74 -6,-19 12,-16 19,3 9,24 28,5 22,-22 -7,-26 8,-34 18,-9 11,29 28,16 21,-18 -6,-30 -5,-31 10,-13 8,11 15,24 15,29 0,4 5,5 10,2 6,-3 8,-18 4,-33 -5,-26 -5,-26 10,-8 15,19 18,19 118,-12 113,-35 284,-72 329,-72 24,0 29,4 29,25 0,34 -36,125 -63,158 l -22,28 -59,-30 c -74,-39 -165,-43 -233,-10 -24,11 -43,25 -43,30 0,4 28,6 63,2 49,-4 71,-2 98,12 46,23 79,55 79,75 0,16 -48,66 -135,139 l -40,33 43,-23 c 55,-30 166,-121 188,-154 9,-13 13,-29 9,-36 -15,-23 -100,-69 -129,-69 -17,0 -38,-5 -46,-10 -12,-8 -10,-10 10,-10 49,1 104,19 149,51 l 45,31 24,-28 c 33,-40 72,-128 79,-180 10,-79 -25,-96 -233,-114 -84,-7 -148,-7 -218,0 -106,12 -269,51 -317,76 -28,14 -31,13 -62,-10 -56,-43 -167,-89 -225,-94 l -55,-5 6,29 c 2,16 14,55 26,86 21,57 27,112 15,142 -3,9 -34,38 -68,64 l -62,49 -104,7 c -166,12 -192,34 -83,70 34,12 65,24 68,28 4,4 -7,27 -24,52 -118,171 -124,402 -16,571 18,29 50,88 70,131 42,93 99,154 168,184 82,35 212,44 280,18 z m 540,-39 c 60,-16 70,-41 75,-196 l 5,-134 -39,3 -39,3 -5,114 c -4,93 -8,116 -22,125 -31,19 -41,-7 -47,-126 l -6,-113 -32,-3 c -41,-4 -44,6 -34,151 9,143 25,172 101,186 3,0 22,-4 43,-10 z m 322,-34 c 0,-30 0,-30 -64,-33 l -64,-3 7,-130 7,-129 h -35 -36 v 165 166 l 93,-3 92,-3 z m 115,-55 5,-84 18,27 c 21,33 39,34 60,5 15,-22 16,-18 16,60 l 1,82 h 35 35 l -2,-162 -3,-163 h -40 c -38,0 -41,2 -53,43 -16,49 -24,48 -42,-7 -12,-38 -16,-41 -50,-41 -34,0 -38,3 -44,31 -8,43 -8,284 1,293 4,4 19,6 33,4 24,-3 25,-6 30,-88 z m 370,-42 c 193,-243 228,-505 100,-759 -61,-120 -195,-260 -328,-343 -66,-41 -60,-26 13,33 36,29 92,85 125,126 182,230 209,500 75,753 -42,79 -45,89 -45,163 0,44 4,79 9,79 5,0 28,-24 51,-52 z M 1249,1314 c 76,-30 94,-102 41,-156 -27,-27 -29,-32 -17,-47 34,-40 25,-100 -19,-120 -30,-13 -143,-15 -164,-1 -11,7 -15,42 -18,164 l -4,156 23,9 c 37,15 115,13 158,-5 z m 571,-9 c 84,-43 61,-135 -43,-173 -46,-18 -52,-23 -52,-48 0,-39 30,-42 79,-9 l 37,25 -3,-41 c -4,-49 -32,-76 -90,-85 -32,-6 -41,-2 -68,24 -24,25 -30,39 -30,74 0,57 21,85 87,114 40,18 54,29 51,42 -5,29 -71,28 -111,-2 -19,-14 -36,-26 -39,-26 -11,0 -16,55 -7,72 24,45 131,63 189,33 z m -303,-12 c 56,-26 75,-65 71,-150 -3,-67 -6,-75 -38,-108 -43,-44 -94,-61 -146,-48 l -39,9 -3,144 c -1,79 0,150 2,157 8,19 110,17 153,-4 z" />
<path
id="path7"
d="m 1503,633 c 4,-3 10,-3 14,0 3,4 0,7 -7,7 -7,0 -10,-3 -7,-7 z" />
<path
id="path9"
d="m 1532,448 c 3,-7 15,-14 29,-16 23,-2 23,-2 5,13 -24,18 -39,20 -34,3 z" />
<path
id="path11"
d="m 1140,1231 c 0,-45 10,-61 36,-61 26,0 64,32 64,53 0,23 -23,37 -62,37 -35,0 -38,-2 -38,-29 z" />
<path
id="path13"
d="m 1140,1070 c 0,-35 17,-45 56,-36 20,5 25,12 22,34 -3,23 -8,27 -40,30 -36,3 -38,2 -38,-28 z" />
<path
id="path15"
d="m 1430,1151 c 0,-72 3,-91 14,-91 28,0 47,13 61,41 31,60 10,121 -47,135 l -28,6 z" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4 KiB

View file

@ -1,80 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 15.0.2, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:ns1="http://sozi.baierouge.fr"
xmlns:cc="http://web.resource.org/cc/"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:dc="http://purl.org/dc/elements/1.1/"
id="Layer_1"
enable-background="new 0 0 40 40"
xml:space="preserve"
viewBox="0 0 40 40"
version="1.1"
y="0px"
x="0px"
>
<polygon
points="36.351 32.435 25.5 36.271 25.5 3.94 36.351 6.242"
fill="#ffffff"
/>
<g
fill="#ffffff"
>
<path
d="m13.627 29.253l8.123-8.941c0.241-0.241 0.379-0.58 0.379-0.934s-0.138-0.693-0.379-0.934l-8.123-8.943c-0.346-0.348-0.853-0.44-1.286-0.238-0.436 0.203-0.717 0.662-0.717 1.171v3.193h-7.745c-0.658 0-1.191 0.572-1.191 1.277v8.943c0 0.705 0.533 1.276 1.191 1.276h7.745v3.194c0 0.509 0.281 0.969 0.716 1.172 0.434 0.204 0.94 0.112 1.287-0.236z"
/>
<path
d="m13.627 29.253l8.123-8.941c0.241-0.241 0.379-0.58 0.379-0.934s-0.138-0.693-0.379-0.934l-8.123-8.943c-0.346-0.348-0.853-0.44-1.286-0.238-0.436 0.203-0.717 0.662-0.717 1.171v3.193h-7.745c-0.658 0-1.191 0.572-1.191 1.277v8.943c0 0.705 0.533 1.276 1.191 1.276h7.745v3.194c0 0.509 0.281 0.969 0.716 1.172 0.434 0.204 0.94 0.112 1.287-0.236z"
/>
</g
>
<path
stroke-linejoin="round"
d="m24.166 6.224h-1.288c-1.78 0-3.223 1.442-3.223 3.223v3.981"
stroke="#ffffff"
stroke-linecap="round"
stroke-miterlimit="10"
fill="none"
/>
<path
stroke-linejoin="round"
d="m19.655 25.495v3.733c0 1.78 1.442 3.223 3.223 3.223h1.288"
stroke="#ffffff"
stroke-linecap="round"
stroke-miterlimit="10"
fill="none"
/>
<metadata
><rdf:RDF
><cc:Work
><dc:format
>image/svg+xml</dc:format
><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
/><cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
/><dc:publisher
><cc:Agent
rdf:about="http://openclipart.org/"
><dc:title
>Openclipart</dc:title
></cc:Agent
></dc:publisher
></cc:Work
><cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/"
><cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/><cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/><cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/></cc:License
></rdf:RDF
></metadata
></svg
>

Before

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -1,7 +0,0 @@
$(function () {
// Close notifications when delete button is pressed
$(".notification .delete").on("click", function () {
$(this).parent().remove();
});
});

View file

@ -1,152 +0,0 @@
// Compilation command :
// sass -I shared/static/src/ --watch bds/static/src/sass/bds.scss bds/static/bds/css/bds.css --style compressed
$text: black;
@import "bulma/bulma.sass";
$primary_color: #3e2263;
$background_color: #ddcecc;
html, body {
background: $background_color;
font-size: 18px;
}
a {
text-decoration: none;
color: #a82305;
}
/* header */
#search-bar {
background-color: $primary_color;
padding: 0 1em;
margin-bottom: 0;
#logout-mobile {
display: none;
}
@include mobile {
display: flex;
flex-wrap: wrap;
#logout-mobile {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
#logout {
display: none;
}
#search-input {
flex: 0 1 100%;
}
}
// Workaround : `justify-content : <left/right>` pas encore supporté
// Voir https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content
& :first-child, & :last-child {
justify-content: space-between;
}
& :last-child {
flex-direction: row-reverse;
}
input {
border-radius: 0;
margin: 10px 0;
}
}
/* Autocomplétion du BDS */
.highlight {
text-decoration: underline;
font-weight: bold;
}
.yourlabs-autocomplete {
ul {
list-style: none;
padding: 0;
margin: 0;
li {
height: 2em;
line-height: 2em;
padding: 0;
a {
color: inherit;
}
}
li.hilight {
background: #e8554e;
}
}
}
.autocomplete-item {
display: block;
width: 480px;
height: 100%;
padding: 2px 10px;
margin: 0;
}
.autocomplete-header {
background: #b497e1;
}
.autocomplete-value, .autocomplete-new, .autocomplete-more {
background: white;
}
/* --- Forms --- */
$button_color: lighten($primary_color, 10);
input[type="submit"] {
background-color: $button_color;
color: findColorInvert($button_color);
&:hover {
background-color: $primary_color;
color: findColorInvert($primary_color);
}
}
.button.is-primary {
background-color: $button_color;
color: findColorInvert($button_color);
&:hover {
background-color: $primary_color;
color: findColorInvert($primary_color);
}
}
/* --- Message styling --- */
.notification {
padding: 0.5em 0;
font-size: 1.2em;
text-align: center;
}
/* --- Modals --- */
.modal-card-head {
background-color: $primary_color;
.modal-card-title {
color: findColorInvert($primary_color);
}
}

View file

@ -1,50 +0,0 @@
{% load static %}
{% load bulma_utils %}
<!DOCTYPE html>
<html>
<head>
<title>{{ site.name }}</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
{# CSS #}
<link rel="stylesheet" href="{% static "bds/css/bds.css" %}">
<link type="text/css" rel="stylesheet" href="{% static 'vendor/font-awesome/css/font-awesome.min.css' %}">
{# Javascript #}
<script src="{% static 'vendor/jquery/jquery-3.3.1.min.js' %}"></script>
<script src="{% static "vendor/jquery/jquery.autocomplete-light.min.js" %}"></script>
<script src="{% static 'bds/js/bds.js' %}"></script>
{% block extra_head %}{% endblock extra_head %}
</head>
<body>
{% include "bds/nav.html" %}
{% block layout %}
<div class="columns">
<div class="column is-two-thirds is-offset-2">
<section class="section">
{% if messages %}
{% for message in messages %}
<div class="notification is-{{ message.level_tag|bulma_message_tag }}">
{% if 'safe' in message.tags %}
{{ message|safe }}
{% else %}
{{ message }}
{% endif %}
<button class="delete"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock content %}
</section>
</div>
</div>
{% endblock layout %}
</body>
</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

@ -1,16 +0,0 @@
<div class="control">
{% if field.auto_id %}
<label class="checkbox {% if field.field.required %}{{ form.required_css_class }}{% endif %}">
{{ field }} {{ field.label }}
</label>
{% endif %}
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>

View file

@ -1,33 +0,0 @@
{% load bulma_utils %}
<div class="field">
{% if field|is_checkbox %}
{% include "bds/forms/checkbox.html" with field=field %}
{% elif field|is_radio %}
{% include "bds/forms/radio.html" with field=field %}
{% elif field|is_input %}
{% include "bds/forms/input.html" with field=field %}
{% elif field|is_textarea %}
{% include "bds/forms/textarea.html" with field=field %}
{% elif field|is_select %}
{% include "bds/forms/select.html" with field=field %}
{% elif field|is_file %}
{% include "bds/forms/file.html" with field=field %}
{% else %}
{% include "bds/forms/other.html" with field=field %}
{% endif %}
</div>

View file

@ -1,31 +0,0 @@
{% load bulma_utils %}
{% load i18n %}
<label class="label {% if field.field.required %}{{ form.required_css_class }}{% endif %}">
{{ field.label }}
</label>
<div class="control">
<label class="file-label">
{{ field|bulmafy:'file-input' }}
<span class="file-cta">
<span class="file-icon">
<i class="fa fa-upload"></i>
</span>
<span class="file-label">
{% trans "Choisissez un fichier..." %}
</span>
</span>
</label>
{% for error in field.errors %}
<span class="help is-danger {{ form.error_css_class }}">{{ error }}</span>
{% endfor %}
{% if field.help_text %}
<p class="help">
{{ field.help_text|safe }}
</p>
{% endif %}
</div>

View file

@ -1,22 +0,0 @@
{% if errors %}
{% if form.non_field_errors %}
<div class="message is-danger">
<div class="message-header">
<button class="delete" aria-label="delete"></button>
</div>
<div class="message-body">
{% for non_field_error in form.non_field_errors %}
{{ non_field_error }}
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
{% for field in form.hidden_fields %}
{{ field }}
{% endfor %}
{% for field in form.visible_fields %}
{% include 'bds/forms/field.html' %}
{% endfor %}

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