WIP: Aureplop/kpsul js refactor #501
120 changed files with 10546 additions and 737 deletions
137
README.md
137
README.md
|
@ -1,17 +1,75 @@
|
|||
# GestioCOF
|
||||
|
||||
![build_status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/build.svg)
|
||||
|
||||
## Installation
|
||||
|
||||
Il est possible d'installer GestioCOF sur votre machine de deux façons différentes :
|
||||
|
||||
- L'[installation manuelle](#installation-manuelle) (**recommandée** sous linux et OSX), plus légère
|
||||
- L'[installation via vagrant](#vagrant) qui fonctionne aussi sous windows mais un peu plus lourde
|
||||
|
||||
### Installation manuelle
|
||||
|
||||
Il est fortement conseillé d'utiliser un environnement virtuel pour Python.
|
||||
|
||||
Il vous faudra installer pip, les librairies de développement de python ainsi
|
||||
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
|
||||
Debian et dérivées (Ubuntu, ...) :
|
||||
|
||||
sudo apt-get install python3-pip python3-dev python3-venv sqlite3
|
||||
|
||||
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
|
||||
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
|
||||
(le dossier où se trouve ce README), et créez-le maintenant :
|
||||
|
||||
python3 -m venv venv
|
||||
|
||||
Pour l'activer, il faut taper
|
||||
|
||||
. venv/bin/activate
|
||||
|
||||
depuis le même dossier.
|
||||
|
||||
Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
||||
`requirements-devel.txt` :
|
||||
|
||||
pip install -U pip # parfois nécessaire la première fois
|
||||
pip install -r requirements-devel.txt
|
||||
|
||||
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 cof/settings/secret.py
|
||||
|
||||
|
||||
#### Fin d'installation
|
||||
|
||||
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
|
||||
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
|
||||
données bidons bien pratiques pour développer avec la commande suivante :
|
||||
|
||||
bash provisioning/prepare_django.sh
|
||||
|
||||
Voir le paragraphe ["outils pour développer"](#outils-pour-d-velopper) plus bas
|
||||
pour plus de détails.
|
||||
|
||||
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
|
||||
|
||||
python manage.py runserver
|
||||
|
||||
|
||||
### Vagrant
|
||||
|
||||
La façon recommandée d'installer GestioCOF sur votre machine est d'utiliser
|
||||
Une autre façon d'installer GestioCOF sur votre machine est d'utiliser
|
||||
[Vagrant](https://www.vagrantup.com/). Vagrant permet de créer une machine
|
||||
virtuelle minimale sur laquelle tournera GestioCOF; ainsi on s'assure que tout
|
||||
le monde à la même configuration de développement (même sous Windows !), et
|
||||
l'installation se fait en une commande.
|
||||
|
||||
Pour utiliser Vagrant, il faut le
|
||||
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
|
||||
[télécharger](https://www.vagrantup.com/downloads.html) et l'installer.
|
||||
|
||||
Si vous êtes sous Linux, votre distribution propose probablement des paquets
|
||||
Vagrant dans le gestionnaire de paquets (la version sera moins récente, ce qui
|
||||
|
@ -81,55 +139,6 @@ Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
|
|||
code change, il faut relancer le worker avec `sudo systemctl restart
|
||||
worker.service` pour visualiser la dernière version du code.
|
||||
|
||||
|
||||
### Installation manuelle
|
||||
|
||||
Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant,
|
||||
il est fortement conseillé d'utiliser un environnement virtuel pour Python.
|
||||
|
||||
Il vous faudra installer pip, les librairies de développement de python ainsi
|
||||
que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
|
||||
Debian et dérivées (Ubuntu, ...) :
|
||||
|
||||
sudo apt-get install python3-pip python3-dev sqlite3
|
||||
|
||||
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
|
||||
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
|
||||
(le dossier où se trouve ce README), et créez-le maintenant :
|
||||
|
||||
python3 -m venv venv
|
||||
|
||||
Pour l'activer, il faut faire
|
||||
|
||||
. venv/bin/activate
|
||||
|
||||
dans le même dossier.
|
||||
|
||||
Vous pouvez maintenant installer les dépendances Python depuis le fichier
|
||||
`requirements-devel.txt` :
|
||||
|
||||
pip install -U pip
|
||||
pip install -r requirements-devel.txt
|
||||
|
||||
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 cof/settings/secret.py
|
||||
|
||||
|
||||
#### Fin d'installation
|
||||
|
||||
Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
|
||||
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
|
||||
données bidons bien pratiques pour développer avec la commande suivante :
|
||||
|
||||
bash provisioning/prepare_django.sh
|
||||
|
||||
Vous êtes prêts à développer ! Lancer GestioCOF en faisant
|
||||
|
||||
python manage.py runserver
|
||||
|
||||
### Mise à jour
|
||||
|
||||
Pour mettre à jour les paquets Python, utiliser la commande suivante :
|
||||
|
@ -141,6 +150,32 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire :
|
|||
python manage.py migrate
|
||||
|
||||
|
||||
## Outils pour développer
|
||||
|
||||
### Base de donnée
|
||||
|
||||
Quelle que soit la méthode d'installation choisie, la base de donnée locale est
|
||||
peuplée avec des données artificielles pour faciliter le développement.
|
||||
|
||||
- Un compte `root` (mot de passe `root`) avec tous les accès est créé. Connectez
|
||||
vous sur ce compte pour accéder à tout GestioCOF.
|
||||
- Des comptes utilisateurs COF et non-COF sont créés ainsi que quelques
|
||||
spectacles BdA et deux tirages au sort pour jouer avec les fonctionnalités du BdA.
|
||||
- À chaque compte est associé un trigramme K-Fêt
|
||||
- Un certain nombre d'articles K-Fêt sont renseignés.
|
||||
|
||||
### Tests unitaires
|
||||
|
||||
On écrit désormais des tests unitaires qui sont lancés automatiquement sur gitlab
|
||||
à chaque push. Il est conseillé de lancer les tests sur sa machine avant de proposer un patch pour s'assurer qu'on ne casse pas une fonctionnalité existante.
|
||||
|
||||
Pour lancer les tests :
|
||||
|
||||
```
|
||||
python manage.py test
|
||||
```
|
||||
|
||||
|
||||
## Documentation utilisateur
|
||||
|
||||
Une brève documentation utilisateur est accessible sur le
|
||||
|
|
1
TODO_PROD.md
Normal file
1
TODO_PROD.md
Normal file
|
@ -0,0 +1 @@
|
|||
- Changer les urls dans les mails "bda-revente" et "bda-shotgun"
|
14
bda/admin.py
14
bda/admin.py
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import timedelta
|
||||
from custommail.shortcuts import send_mass_custom_mail
|
||||
|
||||
|
@ -166,7 +164,7 @@ class AttributionAdminForm(forms.ModelForm):
|
|||
)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super(AttributionAdminForm, self).clean()
|
||||
cleaned_data = super().clean()
|
||||
participant = cleaned_data.get("participant")
|
||||
spectacle = cleaned_data.get("spectacle")
|
||||
if participant and spectacle:
|
||||
|
@ -236,7 +234,7 @@ class SpectacleReventeAdminForm(forms.ModelForm):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['answered_mail'].queryset = (
|
||||
self.fields['confirmed_entry'].queryset = (
|
||||
Participant.objects
|
||||
.select_related('user', 'tirage')
|
||||
)
|
||||
|
@ -299,13 +297,7 @@ class SpectacleReventeAdmin(admin.ModelAdmin):
|
|||
count = queryset.count()
|
||||
for revente in queryset.filter(
|
||||
attribution__spectacle__date__gte=timezone.now()):
|
||||
revente.date = timezone.now() - timedelta(hours=1)
|
||||
revente.soldTo = None
|
||||
revente.notif_sent = False
|
||||
revente.tirage_done = False
|
||||
if revente.answered_mail:
|
||||
revente.answered_mail.clear()
|
||||
revente.save()
|
||||
revente.reset(new_date=timezone.now() - timedelta(hours=1))
|
||||
self.message_user(
|
||||
request,
|
||||
"%d attribution%s %s été réinitialisée%s avec succès." % (
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db.models import Max
|
||||
|
||||
import random
|
||||
|
|
113
bda/forms.py
113
bda/forms.py
|
@ -1,10 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django import forms
|
||||
from django.forms.models import BaseInlineFormSet
|
||||
from django.utils import timezone
|
||||
|
||||
from bda.models import Attribution, Spectacle
|
||||
from bda.models import Attribution, Spectacle, SpectacleRevente
|
||||
|
||||
|
||||
class InscriptionInlineFormSet(BaseInlineFormSet):
|
||||
|
@ -43,7 +41,33 @@ class TokenForm(forms.Form):
|
|||
|
||||
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
def label_from_instance(self, obj):
|
||||
return "%s" % str(obj.spectacle)
|
||||
return str(obj.spectacle)
|
||||
|
||||
|
||||
class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField):
|
||||
def __init__(self, *args, own=True, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.own = own
|
||||
|
||||
def label_from_instance(self, obj):
|
||||
label = "{show}{suffix}"
|
||||
suffix = ""
|
||||
if self.own:
|
||||
# C'est notre propre revente : pas besoin de spécifier le vendeur
|
||||
if obj.soldTo is not None:
|
||||
suffix = " -- Vendue à {firstname} {lastname}".format(
|
||||
firstname=obj.soldTo.user.first_name,
|
||||
lastname=obj.soldTo.user.last_name,
|
||||
)
|
||||
else:
|
||||
# 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,
|
||||
)
|
||||
|
||||
return label.format(show=str(obj.attribution.spectacle),
|
||||
suffix=suffix)
|
||||
|
||||
|
||||
class ResellForm(forms.Form):
|
||||
|
@ -54,7 +78,7 @@ class ResellForm(forms.Form):
|
|||
required=False)
|
||||
|
||||
def __init__(self, participant, *args, **kwargs):
|
||||
super(ResellForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['attributions'].queryset = (
|
||||
participant.attribution_set
|
||||
.filter(spectacle__date__gte=timezone.now())
|
||||
|
@ -65,22 +89,22 @@ class ResellForm(forms.Form):
|
|||
|
||||
|
||||
class AnnulForm(forms.Form):
|
||||
attributions = AttributionModelMultipleChoiceField(
|
||||
reventes = ReventeModelMultipleChoiceField(
|
||||
own=True,
|
||||
label='',
|
||||
queryset=Attribution.objects.none(),
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
required=False)
|
||||
|
||||
def __init__(self, participant, *args, **kwargs):
|
||||
super(AnnulForm, self).__init__(*args, **kwargs)
|
||||
self.fields['attributions'].queryset = (
|
||||
participant.attribution_set
|
||||
.filter(spectacle__date__gte=timezone.now(),
|
||||
revente__isnull=False,
|
||||
revente__notif_sent=False,
|
||||
revente__soldTo__isnull=True)
|
||||
.select_related('spectacle', 'spectacle__location',
|
||||
'participant__user')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['reventes'].queryset = (
|
||||
participant.original_shows
|
||||
.filter(attribution__spectacle__date__gte=timezone.now(),
|
||||
notif_sent=False,
|
||||
soldTo__isnull=True)
|
||||
.select_related('attribution__spectacle',
|
||||
'attribution__spectacle__location')
|
||||
)
|
||||
|
||||
|
||||
|
@ -91,7 +115,7 @@ class InscriptionReventeForm(forms.Form):
|
|||
required=False)
|
||||
|
||||
def __init__(self, tirage, *args, **kwargs):
|
||||
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['spectacles'].queryset = (
|
||||
tirage.spectacle_set
|
||||
.select_related('location')
|
||||
|
@ -99,19 +123,58 @@ class InscriptionReventeForm(forms.Form):
|
|||
)
|
||||
|
||||
|
||||
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):
|
||||
attributions = AttributionModelMultipleChoiceField(
|
||||
reventes = ReventeModelMultipleChoiceField(
|
||||
own=True,
|
||||
label='',
|
||||
queryset=Attribution.objects.none(),
|
||||
widget=forms.CheckboxSelectMultiple)
|
||||
|
||||
def __init__(self, participant, *args, **kwargs):
|
||||
super(SoldForm, self).__init__(*args, **kwargs)
|
||||
self.fields['attributions'].queryset = (
|
||||
participant.attribution_set
|
||||
.filter(revente__isnull=False,
|
||||
revente__soldTo__isnull=False)
|
||||
.exclude(revente__soldTo=participant)
|
||||
.select_related('spectacle', 'spectacle__location',
|
||||
'participant__user')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['reventes'].queryset = (
|
||||
participant.original_shows
|
||||
.filter(soldTo__isnull=False)
|
||||
.exclude(soldTo=participant)
|
||||
.select_related('attribution__spectacle',
|
||||
'attribution__spectacle__location')
|
||||
)
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Gestion en ligne de commande des reventes.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils import timezone
|
||||
from bda.models import SpectacleRevente
|
||||
|
@ -21,23 +16,36 @@ class Command(BaseCommand):
|
|||
now = timezone.now()
|
||||
reventes = SpectacleRevente.objects.all()
|
||||
for revente in reventes:
|
||||
# Check si < 24h
|
||||
if (revente.attribution.spectacle.date <=
|
||||
revente.date + timedelta(days=1)) and \
|
||||
now >= revente.date + timedelta(minutes=15) and \
|
||||
not revente.notif_sent:
|
||||
self.stdout.write(str(now))
|
||||
revente.mail_shotgun()
|
||||
self.stdout.write("Mail de disponibilité immédiate envoyé")
|
||||
# Check si délai de retrait dépassé
|
||||
elif (now >= revente.date + timedelta(hours=1) and
|
||||
not revente.notif_sent):
|
||||
# Le spectacle est bientôt et on a pas encore envoyé de mail :
|
||||
# on met la place au shotgun et on prévient.
|
||||
if revente.is_urgent and not revente.notif_sent:
|
||||
if revente.can_notif:
|
||||
self.stdout.write(str(now))
|
||||
revente.mail_shotgun()
|
||||
self.stdout.write(
|
||||
"Mails de disponibilité immédiate envoyés "
|
||||
"pour la revente [%s]" % revente
|
||||
)
|
||||
|
||||
# Le spectacle est dans plus longtemps : on prévient
|
||||
elif (revente.can_notif and not revente.notif_sent):
|
||||
self.stdout.write(str(now))
|
||||
revente.send_notif()
|
||||
self.stdout.write("Mail d'inscription à une revente envoyé")
|
||||
# Check si tirage à faire
|
||||
elif (now >= revente.date_tirage and
|
||||
not revente.tirage_done):
|
||||
self.stdout.write(
|
||||
"Mails d'inscription à la revente [%s] envoyés"
|
||||
% revente
|
||||
)
|
||||
|
||||
# On fait le tirage
|
||||
elif (now >= revente.date_tirage and not revente.tirage_done):
|
||||
self.stdout.write(str(now))
|
||||
revente.tirage()
|
||||
self.stdout.write("Tirage effectué, mails envoyés")
|
||||
winner = revente.tirage()
|
||||
self.stdout.write(
|
||||
"Tirage effectué pour la revente [%s]"
|
||||
% revente
|
||||
)
|
||||
|
||||
if winner:
|
||||
self.stdout.write("Gagnant : %s" % winner.user)
|
||||
else:
|
||||
self.stdout.write("Pas de gagnant ; place au shotgun")
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Gestion en ligne de commande des mails de rappel.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import timedelta
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
|
29
bda/migrations/0012_notif_time.py
Normal file
29
bda/migrations/0012_notif_time.py
Normal file
|
@ -0,0 +1,29 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bda', '0011_tirage_appear_catalogue'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='spectaclerevente',
|
||||
old_name='answered_mail',
|
||||
new_name='confirmed_entry',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='spectaclerevente',
|
||||
name='confirmed_entry',
|
||||
field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='spectaclerevente',
|
||||
name='notif_time',
|
||||
field=models.DateTimeField(blank=True, verbose_name="Moment d'envoi de la notification", null=True),
|
||||
),
|
||||
]
|
53
bda/migrations/0012_swap_double_choice.py
Normal file
53
bda/migrations/0012_swap_double_choice.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def swap_double_choice(apps, schema_editor):
|
||||
choices = apps.get_model("bda", "ChoixSpectacle").objects
|
||||
|
||||
choices.filter(double_choice="double").update(double_choice="tmp")
|
||||
choices.filter(double_choice="autoquit").update(double_choice="double")
|
||||
choices.filter(double_choice="tmp").update(double_choice="autoquit")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bda', '0011_tirage_appear_catalogue'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Temporarily allow an extra "tmp" value for the `double_choice` field
|
||||
migrations.AlterField(
|
||||
model_name='choixspectacle',
|
||||
name='double_choice',
|
||||
field=models.CharField(
|
||||
verbose_name='Nombre de places',
|
||||
max_length=10,
|
||||
default='1',
|
||||
choices=[
|
||||
('tmp', 'tmp'),
|
||||
('1', '1 place'),
|
||||
('double', '2 places si possible, 1 sinon'),
|
||||
('autoquit', '2 places sinon rien')
|
||||
]
|
||||
),
|
||||
),
|
||||
migrations.RunPython(swap_double_choice, migrations.RunPython.noop),
|
||||
migrations.AlterField(
|
||||
model_name='choixspectacle',
|
||||
name='double_choice',
|
||||
field=models.CharField(
|
||||
verbose_name='Nombre de places',
|
||||
max_length=10,
|
||||
default='1',
|
||||
choices=[
|
||||
('1', '1 place'),
|
||||
('double', '2 places si possible, 1 sinon'),
|
||||
('autoquit', '2 places sinon rien')
|
||||
]
|
||||
),
|
||||
),
|
||||
]
|
16
bda/migrations/0013_merge_20180524_2123.py
Normal file
16
bda/migrations/0013_merge_20180524_2123.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.13 on 2018-05-24 19:23
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bda', '0012_notif_time'),
|
||||
('bda', '0012_swap_double_choice'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
171
bda/models.py
171
bda/models.py
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import calendar
|
||||
import random
|
||||
from datetime import timedelta
|
||||
|
@ -174,10 +172,11 @@ class Participant(models.Model):
|
|||
def __str__(self):
|
||||
return "%s - %s" % (self.user, self.tirage.title)
|
||||
|
||||
|
||||
DOUBLE_CHOICES = (
|
||||
("1", "1 place"),
|
||||
("autoquit", "2 places si possible, 1 sinon"),
|
||||
("double", "2 places sinon rien"),
|
||||
("double", "2 places si possible, 1 sinon"),
|
||||
("autoquit", "2 places sinon rien"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -232,9 +231,9 @@ class SpectacleRevente(models.Model):
|
|||
)
|
||||
date = models.DateTimeField("Date de mise en vente",
|
||||
default=timezone.now)
|
||||
answered_mail = models.ManyToManyField(Participant,
|
||||
related_name="wanted",
|
||||
blank=True)
|
||||
confirmed_entry = models.ManyToManyField(Participant,
|
||||
related_name="entered",
|
||||
blank=True)
|
||||
seller = models.ForeignKey(
|
||||
Participant, on_delete=models.CASCADE,
|
||||
verbose_name="Vendeur",
|
||||
|
@ -248,21 +247,61 @@ class SpectacleRevente(models.Model):
|
|||
|
||||
notif_sent = models.BooleanField("Notification envoyée",
|
||||
default=False)
|
||||
|
||||
notif_time = models.DateTimeField("Moment d'envoi de la notification",
|
||||
blank=True, null=True)
|
||||
|
||||
tirage_done = models.BooleanField("Tirage effectué",
|
||||
default=False)
|
||||
|
||||
shotgun = models.BooleanField("Disponible immédiatement",
|
||||
default=False)
|
||||
####
|
||||
# Some class attributes
|
||||
###
|
||||
# TODO : settings ?
|
||||
|
||||
# Temps minimum entre le tirage et le spectacle
|
||||
min_margin = timedelta(days=5)
|
||||
|
||||
# Temps entre la création d'une revente et l'envoi du mail
|
||||
remorse_time = timedelta(hours=1)
|
||||
|
||||
# Temps min/max d'attente avant le tirage
|
||||
max_wait_time = timedelta(days=3)
|
||||
min_wait_time = timedelta(days=1)
|
||||
|
||||
@property
|
||||
def real_notif_time(self):
|
||||
if self.notif_time:
|
||||
return self.notif_time
|
||||
else:
|
||||
return self.date + self.remorse_time
|
||||
|
||||
@property
|
||||
def date_tirage(self):
|
||||
"""Renvoie la date du tirage au sort de la revente."""
|
||||
# L'acheteur doit être connu au plus 12h avant le spectacle
|
||||
|
||||
remaining_time = (self.attribution.spectacle.date
|
||||
- self.date - timedelta(hours=13))
|
||||
# Au minimum, on attend 2 jours avant le tirage
|
||||
delay = min(remaining_time, timedelta(days=2))
|
||||
# Le vendeur a aussi 1h pour changer d'avis
|
||||
return self.date + delay + timedelta(hours=1)
|
||||
- self.real_notif_time - self.min_margin)
|
||||
|
||||
delay = min(remaining_time, self.max_wait_time)
|
||||
|
||||
return self.real_notif_time + delay
|
||||
|
||||
@property
|
||||
def is_urgent(self):
|
||||
"""
|
||||
Renvoie True iff la revente doit être mise au shotgun directement.
|
||||
Plus précisément, on doit avoir min_margin + min_wait_time de marge.
|
||||
"""
|
||||
spectacle_date = self.attribution.spectacle.date
|
||||
return (spectacle_date <= timezone.now() + self.min_margin
|
||||
+ self.min_wait_time)
|
||||
|
||||
@property
|
||||
def can_notif(self):
|
||||
return (timezone.now() >= self.date + self.remorse_time)
|
||||
|
||||
def __str__(self):
|
||||
return "%s -- %s" % (self.seller,
|
||||
|
@ -271,6 +310,18 @@ class SpectacleRevente(models.Model):
|
|||
class Meta:
|
||||
verbose_name = "Revente"
|
||||
|
||||
def reset(self, new_date=timezone.now()):
|
||||
"""Réinitialise la revente pour permettre une remise sur le marché"""
|
||||
self.seller = self.attribution.participant
|
||||
self.date = new_date
|
||||
self.confirmed_entry.clear()
|
||||
self.soldTo = None
|
||||
self.notif_sent = False
|
||||
self.notif_time = None
|
||||
self.tirage_done = False
|
||||
self.shotgun = False
|
||||
self.save()
|
||||
|
||||
def send_notif(self):
|
||||
"""
|
||||
Envoie une notification pour indiquer la mise en vente d'une place sur
|
||||
|
@ -291,6 +342,7 @@ class SpectacleRevente(models.Model):
|
|||
]
|
||||
send_mass_custom_mail(datatuple)
|
||||
self.notif_sent = True
|
||||
self.notif_time = timezone.now()
|
||||
self.save()
|
||||
|
||||
def mail_shotgun(self):
|
||||
|
@ -312,76 +364,79 @@ class SpectacleRevente(models.Model):
|
|||
]
|
||||
send_mass_custom_mail(datatuple)
|
||||
self.notif_sent = True
|
||||
self.notif_time = timezone.now()
|
||||
# Flag inutile, sauf si l'horloge interne merde
|
||||
self.tirage_done = True
|
||||
self.shotgun = True
|
||||
self.save()
|
||||
|
||||
def tirage(self):
|
||||
def tirage(self, send_mails=True):
|
||||
"""
|
||||
Lance le tirage au sort associé à la revente. Un gagnant est choisi
|
||||
parmis les personnes intéressées par le spectacle. Les personnes sont
|
||||
ensuites prévenues par mail du résultat du tirage.
|
||||
"""
|
||||
inscrits = list(self.answered_mail.all())
|
||||
inscrits = list(self.confirmed_entry.all())
|
||||
spectacle = self.attribution.spectacle
|
||||
seller = self.seller
|
||||
winner = None
|
||||
|
||||
if inscrits:
|
||||
# Envoie un mail au gagnant et au vendeur
|
||||
winner = random.choice(inscrits)
|
||||
self.soldTo = winner
|
||||
if send_mails:
|
||||
mails = []
|
||||
|
||||
mails = []
|
||||
context = {
|
||||
'acheteur': winner.user,
|
||||
'vendeur': seller.user,
|
||||
'show': spectacle,
|
||||
}
|
||||
|
||||
context = {
|
||||
'acheteur': winner.user,
|
||||
'vendeur': seller.user,
|
||||
'show': spectacle,
|
||||
}
|
||||
c_mails_qs = CustomMail.objects.filter(shortname__in=[
|
||||
'bda-revente-winner', 'bda-revente-loser',
|
||||
'bda-revente-seller',
|
||||
])
|
||||
|
||||
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}
|
||||
|
||||
c_mails = {cm.shortname: cm for cm in c_mails_qs}
|
||||
|
||||
mails.append(
|
||||
c_mails['bda-revente-winner'].get_message(
|
||||
context,
|
||||
from_email=settings.MAIL_DATA['revente']['FROM'],
|
||||
to=[winner.user.email],
|
||||
)
|
||||
)
|
||||
|
||||
mails.append(
|
||||
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
|
||||
for inscrit in inscrits:
|
||||
if inscrit != winner:
|
||||
new_context = dict(context)
|
||||
new_context['acheteur'] = inscrit.user
|
||||
|
||||
mails.append(
|
||||
c_mails['bda-revente-loser'].get_message(
|
||||
new_context,
|
||||
from_email=settings.MAIL_DATA['revente']['FROM'],
|
||||
to=[inscrit.user.email],
|
||||
)
|
||||
mails.append(
|
||||
c_mails['bda-revente-winner'].get_message(
|
||||
context,
|
||||
from_email=settings.MAIL_DATA['revente']['FROM'],
|
||||
to=[winner.user.email],
|
||||
)
|
||||
)
|
||||
|
||||
mail_conn = mail.get_connection()
|
||||
mail_conn.send_messages(mails)
|
||||
mails.append(
|
||||
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
|
||||
for inscrit in inscrits:
|
||||
if inscrit != winner:
|
||||
new_context = dict(context)
|
||||
new_context['acheteur'] = inscrit.user
|
||||
|
||||
mails.append(
|
||||
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()
|
||||
mail_conn.send_messages(mails)
|
||||
# Si personne ne veut de la place, elle part au shotgun
|
||||
else:
|
||||
self.shotgun = True
|
||||
self.tirage_done = True
|
||||
self.save()
|
||||
return winner
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
</tr></thead>
|
||||
<tbody class="bda_formset_content">
|
||||
{% endif %}
|
||||
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
||||
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||
<td class="bda-field-{{ field.name }}">
|
||||
|
|
|
@ -27,6 +27,14 @@ var django = {
|
|||
var newFor = $(this).attr('for').replace('-' + (total-1) + '-','-' + total + '-');
|
||||
$(this).attr('for', newFor);
|
||||
});
|
||||
// Cloning <select> element doesn't properly propagate the default
|
||||
// selected <option>, so we set it manually.
|
||||
newElement.find('select').each(function (index, select) {
|
||||
var defaultValue = $(select).find('option[selected]').val();
|
||||
if (typeof defaultValue !== 'undefined') {
|
||||
$(select).val(defaultValue);
|
||||
}
|
||||
});
|
||||
total++;
|
||||
$('#id_' + type + '-TOTAL_FORMS').val(total);
|
||||
$(selector).after(newElement);
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block realcontent %}
|
||||
<h2>Inscriptions pour BdA-Revente</h2>
|
||||
<form action="" class="form-horizontal" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<h3>Spectacles</h3>
|
||||
<br/>
|
||||
<button type="button" class="btn btn-primary" onClick="select(true)">Tout sélectionner</button>
|
||||
<button type="button" class="btn btn-primary" onClick="select(false)">Tout désélectionner</button>
|
||||
|
||||
<div class="multiple-checkbox">
|
||||
<ul>
|
||||
{% for checkbox in form.spectacles %}
|
||||
<li>{{checkbox}}</li>
|
||||
{%endfor%}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-primary" value="S'inscrire pour les places sélectionnées">
|
||||
</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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -16,7 +16,7 @@
|
|||
<h4 class="bda-prix">Total à payer : {{ total|floatformat }}€</h4>
|
||||
<br/>
|
||||
<p>Ne manque pas un spectacle avec le
|
||||
<a href="{% url "gestioncof.views.calendar" %}">calendrier
|
||||
<a href="{% url "calendar" %}">calendrier
|
||||
automatique !</a></p>
|
||||
{% else %}
|
||||
<h3>Vous n'avez aucune place :(</h3>
|
||||
|
|
90
bda/templates/bda/revente/manage.html
Normal file
90
bda/templates/bda/revente/manage.html
Normal file
|
@ -0,0 +1,90 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% 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 resellform.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>
|
||||
<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_reventes or overdue %}
|
||||
<h3>Places en cours de revente</h3>
|
||||
<form action="" method="post">
|
||||
{% if annul_reventes %}
|
||||
<div class="bg-info text-info center-block">
|
||||
<span class="glyphicon glyphicon-info-sign" aria-hidden="true"></span>
|
||||
Vous pouvez annuler les places mises en vente il y a moins d'une heure.
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
<div class='form-group'>
|
||||
<div class='multiple-checkbox'>
|
||||
<ul>
|
||||
{% for revente in annul_reventes %}
|
||||
<li>{{ revente.tag }} {{ revente.choice_label }}</li>
|
||||
{% endfor %}
|
||||
{% for attrib in overdue %}
|
||||
<li>
|
||||
<input type="checkbox" style="visibility:hidden">
|
||||
{{ attrib.spectacle }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% if annul_reventes %}
|
||||
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||
{% endif %}
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
{% 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.
|
||||
|
||||
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_attributions and not annul_attributions and not overdue and not sold_reventes %}
|
||||
<p>Plus de reventes possibles !</p>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
{% endblock %}
|
|
@ -5,7 +5,7 @@
|
|||
{% if shotgun %}
|
||||
<ul class="list-unstyled">
|
||||
{% for spectacle in shotgun %}
|
||||
<li><a href="{% url "bda-buy-revente" spectacle.id %}">{{spectacle}}</a></li>
|
||||
<li><a href="{% url "bda-revente-buy" spectacle.id %}">{{spectacle}}</a></li>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p> Pas de places disponibles immédiatement, désolé !</p>
|
46
bda/templates/bda/revente/subscribe.html
Normal file
46
bda/templates/bda/revente/subscribe.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% 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 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
|
||||
inscrit à ce tirage.
|
||||
</div>
|
||||
<br />
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
onClick="select(true)">Tout sélectionner</button>
|
||||
<button type="button"
|
||||
class="btn btn-primary"
|
||||
onClick="select(false)">Tout désélectionner</button>
|
||||
|
||||
<div class="multiple-checkbox">
|
||||
<ul>
|
||||
{% for checkbox in form.spectacles %}
|
||||
<li>{{ checkbox }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<input type="submit"
|
||||
class="btn btn-primary"
|
||||
value="S'inscrire pour les places sélectionnées">
|
||||
</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;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
52
bda/templates/bda/revente/tirages.html
Normal file
52
bda/templates/bda/revente/tirages.html
Normal file
|
@ -0,0 +1,52 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
||||
<h2>Tirages au sort de reventes</h2>
|
||||
|
||||
{% 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>
|
||||
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>
|
||||
<div class="form-actions">
|
||||
<input type="submit"
|
||||
class="btn btn-primary"
|
||||
name="annul"
|
||||
value="Se désinscrire des tirages sélectionnés">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
{% 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 tirage en cours suivants.
|
||||
</div>
|
||||
<div class="bootstrap-form-reduce">
|
||||
{% csrf_token %}
|
||||
{{ subform|bootstrap }}
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<input type="submit"
|
||||
class="btn btn-primary"
|
||||
name="subscribe"
|
||||
value="S'inscrire aux tirages sélectionnés">
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
|
@ -6,7 +6,7 @@
|
|||
<p>Le tirage au sort de cette revente a déjà été effectué !</p>
|
||||
|
||||
<p>Si personne n'était intéressé, elle est maintenant disponible
|
||||
<a href="{% url "bda-buy-revente" revente.attribution.spectacle.id %}">ici</a>.</p>
|
||||
<a href="{% url "bda-revente-buy" revente.attribution.spectacle.id %}">ici</a>.</p>
|
||||
{% else %}
|
||||
<p> Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !</p>
|
||||
{% endif %}
|
|
@ -1,56 +0,0 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
||||
<h2>Revente de place</h2>
|
||||
{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %}
|
||||
|
||||
{% if resellform.attributions %}
|
||||
<h3>Places non revendues</h3>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{{resellform|bootstrap}}
|
||||
<div class="form-actions">
|
||||
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if annul_attributions or overdue %}
|
||||
<h3>Places en cours de revente</h3>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<div class='form-group'>
|
||||
<div class='multiple-checkbox'>
|
||||
<ul>
|
||||
{% for attrib in annul_attributions %}
|
||||
<li>{{attrib.tag}} {{attrib.choice_label}}</li>
|
||||
{% endfor %}
|
||||
{% for attrib in overdue %}
|
||||
<li>
|
||||
<input type="checkbox" style="visibility:hidden">
|
||||
{{attrib.spectacle}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if annul_attributions %}
|
||||
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if sold_attributions %}
|
||||
<h3>Places revendues</h3>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
{{soldform|bootstrap}}
|
||||
<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_attributions and not annul_attributions and not overdue and not sold_attributions %}
|
||||
<p>Plus de reventes possibles !</p>
|
||||
{% endif %}
|
||||
|
||||
{% endwith %}
|
||||
{% endblock %}
|
|
@ -67,7 +67,7 @@ class SpectacleReventeTests(TestCase):
|
|||
revente = self.rev
|
||||
|
||||
wanted_by = [self.p1, self.p2, self.p3]
|
||||
revente.answered_mail = wanted_by
|
||||
revente.confirmed_entry = wanted_by
|
||||
|
||||
with mock.patch('bda.models.random.choice') as mc:
|
||||
# Set winner to self.p1.
|
||||
|
|
69
bda/tests/test_revente.py
Normal file
69
bda/tests/test_revente.py
Normal file
|
@ -0,0 +1,69 @@
|
|||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, Client
|
||||
from django.utils import timezone
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from bda.models import (Tirage, Spectacle, Salle, CategorieSpectacle,
|
||||
SpectacleRevente, Attribution, Participant)
|
||||
|
||||
|
||||
class TestModels(TestCase):
|
||||
def setUp(self):
|
||||
self.tirage = Tirage.objects.create(
|
||||
title="Tirage test",
|
||||
appear_catalogue=True,
|
||||
ouverture=timezone.now(),
|
||||
fermeture=timezone.now()
|
||||
)
|
||||
self.category = CategorieSpectacle.objects.create(name="Category")
|
||||
self.location = Salle.objects.create(name="here")
|
||||
self.spectacle_soon = Spectacle.objects.create(
|
||||
title="foo", date=timezone.now()+timedelta(days=1),
|
||||
location=self.location, price=0, slots=42,
|
||||
tirage=self.tirage, listing=False, category=self.category
|
||||
)
|
||||
self.spectacle_later = Spectacle.objects.create(
|
||||
title="bar", date=timezone.now()+timedelta(days=30),
|
||||
location=self.location, price=0, slots=42,
|
||||
tirage=self.tirage, listing=False, category=self.category
|
||||
)
|
||||
|
||||
user_buyer = User.objects.create_user(
|
||||
username="bda_buyer", password="testbuyer"
|
||||
)
|
||||
user_seller = User.objects.create_user(
|
||||
username="bda_seller", password="testseller"
|
||||
)
|
||||
self.buyer = Participant.objects.create(
|
||||
user=user_buyer, tirage=self.tirage
|
||||
)
|
||||
self.seller = Participant.objects.create(
|
||||
user=user_seller, tirage=self.tirage
|
||||
)
|
||||
|
||||
self.attr_soon = Attribution.objects.create(
|
||||
participant=self.seller, spectacle=self.spectacle_soon
|
||||
)
|
||||
self.attr_later = Attribution.objects.create(
|
||||
participant=self.seller, spectacle=self.spectacle_later
|
||||
)
|
||||
self.revente_soon = SpectacleRevente.objects.create(
|
||||
seller=self.seller,
|
||||
attribution=self.attr_soon
|
||||
)
|
||||
self.revente_later = SpectacleRevente.objects.create(
|
||||
seller=self.seller,
|
||||
attribution=self.attr_later
|
||||
)
|
||||
|
||||
def test_urgent(self):
|
||||
self.assertTrue(self.revente_soon.is_urgent)
|
||||
self.assertFalse(self.revente_later.is_urgent)
|
||||
|
||||
def test_tirage(self):
|
||||
self.revente_soon.confirmed_entry.add(self.buyer)
|
||||
|
||||
self.assertEqual(self.revente_soon.tirage(send_mails=False),
|
||||
self.buyer)
|
||||
self.assertIsNone(self.revente_later.tirage(send_mails=False))
|
41
bda/urls.py
41
bda/urls.py
|
@ -1,9 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf.urls import url
|
||||
from gestioncof.decorators import buro_required
|
||||
from bda.views import SpectacleListView
|
||||
|
@ -16,9 +10,6 @@ urlpatterns = [
|
|||
url(r'^places/(?P<tirage_id>\d+)$',
|
||||
views.places,
|
||||
name="bda-places-attribuees"),
|
||||
url(r'^revente/(?P<tirage_id>\d+)$',
|
||||
views.revente,
|
||||
name='bda-revente'),
|
||||
url(r'^etat-places/(?P<tirage_id>\d+)$',
|
||||
views.etat_places,
|
||||
name='bda-etat-places'),
|
||||
|
@ -38,18 +29,28 @@ urlpatterns = [
|
|||
url(r'^participants/autocomplete$',
|
||||
views.participant_autocomplete,
|
||||
name="bda-participant-autocomplete"),
|
||||
url(r'^liste-revente/(?P<tirage_id>\d+)$',
|
||||
views.list_revente,
|
||||
name="bda-liste-revente"),
|
||||
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
|
||||
views.buy_revente,
|
||||
name="bda-buy-revente"),
|
||||
url(r'^revente-interested/(?P<revente_id>\d+)$',
|
||||
views.revente_interested,
|
||||
name='bda-revente-interested'),
|
||||
url(r'^revente-immediat/(?P<tirage_id>\d+)$',
|
||||
|
||||
# Urls BdA-Revente
|
||||
|
||||
url(r'^revente/(?P<tirage_id>\d+)/manage$',
|
||||
views.revente_manage,
|
||||
name='bda-revente-manage'),
|
||||
url(r'^revente/(?P<tirage_id>\d+)/subscribe$',
|
||||
views.revente_subscribe,
|
||||
name="bda-revente-subscribe"),
|
||||
url(r'^revente/(?P<tirage_id>\d+)/tirages$',
|
||||
views.revente_tirages,
|
||||
name="bda-revente-tirages"),
|
||||
url(r'^revente/(?P<spectacle_id>\d+)/buy$',
|
||||
views.revente_buy,
|
||||
name="bda-revente-buy"),
|
||||
url(r'^revente/(?P<revente_id>\d+)/confirm$',
|
||||
views.revente_confirm,
|
||||
name='bda-revente-confirm'),
|
||||
url(r'^revente/(?P<tirage_id>\d+)/shotgun$',
|
||||
views.revente_shotgun,
|
||||
name="bda-shotgun"),
|
||||
name="bda-revente-shotgun"),
|
||||
|
||||
url(r'^mails-rappel/(?P<spectacle_id>\d+)$',
|
||||
views.send_rappel,
|
||||
name="bda-rappels"
|
||||
|
|
145
bda/views.py
145
bda/views.py
|
@ -1,11 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from collections import defaultdict
|
||||
import random
|
||||
import hashlib
|
||||
import time
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
|
||||
from custommail.models import CustomMail
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
@ -14,6 +11,7 @@ from django.contrib import messages
|
|||
from django.db import transaction
|
||||
from django.core import serializers
|
||||
from django.db.models import Count, Q, Prefetch
|
||||
from django.template.defaultfilters import pluralize
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.http import (
|
||||
HttpResponseBadRequest, HttpResponseRedirect, JsonResponse
|
||||
|
@ -30,7 +28,7 @@ from bda.models import (
|
|||
from bda.algorithm import Algorithm
|
||||
from bda.forms import (
|
||||
TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm,
|
||||
InscriptionInlineFormSet,
|
||||
InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm
|
||||
)
|
||||
|
||||
from utils.views.autocomplete import Select2QuerySetView
|
||||
|
@ -351,13 +349,21 @@ def tirage(request, tirage_id):
|
|||
|
||||
|
||||
@login_required
|
||||
def revente(request, tirage_id):
|
||||
def revente_manage(request, tirage_id):
|
||||
"""
|
||||
Gestion de ses propres reventes :
|
||||
- Création d'une revente
|
||||
- Annulation d'une revente
|
||||
- Confirmation d'une revente = transfert de la place à la personne qui
|
||||
rachète
|
||||
- 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.get_or_create(
|
||||
user=request.user, tirage=tirage)
|
||||
|
||||
if not participant.paid:
|
||||
return render(request, "bda-notpaid.html", {})
|
||||
return render(request, "bda/revente/notpaid.html", {})
|
||||
|
||||
resellform = ResellForm(participant, prefix='resell')
|
||||
annulform = AnnulForm(participant, prefix='annul')
|
||||
|
@ -377,12 +383,8 @@ def revente(request, tirage_id):
|
|||
attribution=attribution,
|
||||
defaults={'seller': participant})
|
||||
if not created:
|
||||
revente.seller = participant
|
||||
revente.date = timezone.now()
|
||||
revente.soldTo = None
|
||||
revente.notif_sent = False
|
||||
revente.tirage_done = False
|
||||
revente.shotgun = False
|
||||
revente.reset()
|
||||
|
||||
context = {
|
||||
'vendeur': participant.user,
|
||||
'show': attribution.spectacle,
|
||||
|
@ -399,18 +401,18 @@ def revente(request, tirage_id):
|
|||
elif 'annul' in request.POST:
|
||||
annulform = AnnulForm(participant, request.POST, prefix='annul')
|
||||
if annulform.is_valid():
|
||||
attributions = annulform.cleaned_data["attributions"]
|
||||
for attribution in attributions:
|
||||
attribution.revente.delete()
|
||||
reventes = annulform.cleaned_data["reventes"]
|
||||
for revente in reventes:
|
||||
revente.delete()
|
||||
# On confirme une vente en transférant la place à la personne qui a
|
||||
# gagné le tirage
|
||||
elif 'transfer' in request.POST:
|
||||
soldform = SoldForm(participant, request.POST, prefix='sold')
|
||||
if soldform.is_valid():
|
||||
attributions = soldform.cleaned_data['attributions']
|
||||
for attribution in attributions:
|
||||
attribution.participant = attribution.revente.soldTo
|
||||
attribution.save()
|
||||
reventes = soldform.cleaned_data['reventes']
|
||||
for revente in reventes:
|
||||
revente.attribution.participant = revente.soldTo
|
||||
revente.attribution.save()
|
||||
|
||||
# On annule la revente après le tirage au sort (par exemple si
|
||||
# la personne qui a gagné le tirage ne se manifeste pas). La place est
|
||||
|
@ -418,18 +420,13 @@ def revente(request, tirage_id):
|
|||
elif 'reinit' in request.POST:
|
||||
soldform = SoldForm(participant, request.POST, prefix='sold')
|
||||
if soldform.is_valid():
|
||||
attributions = soldform.cleaned_data['attributions']
|
||||
for attribution in attributions:
|
||||
if attribution.spectacle.date > timezone.now():
|
||||
revente = attribution.revente
|
||||
revente.date = timezone.now() - timedelta(minutes=65)
|
||||
revente.soldTo = None
|
||||
revente.notif_sent = False
|
||||
revente.tirage_done = False
|
||||
revente.shotgun = False
|
||||
if revente.answered_mail:
|
||||
revente.answered_mail.clear()
|
||||
revente.save()
|
||||
reventes = soldform.cleaned_data['reventes']
|
||||
for revente in reventes:
|
||||
if revente.attribution.spectacle.date > timezone.now():
|
||||
# On antidate pour envoyer le mail plus vite
|
||||
new_date = (timezone.now()
|
||||
- SpectacleRevente.remorse_time)
|
||||
revente.reset(new_date=new_date)
|
||||
|
||||
overdue = participant.attribution_set.filter(
|
||||
spectacle__date__gte=timezone.now(),
|
||||
|
@ -439,28 +436,80 @@ def revente(request, tirage_id):
|
|||
.filter(
|
||||
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
|
||||
|
||||
return render(request, "bda/reventes.html",
|
||||
return render(request, "bda/revente/manage.html",
|
||||
{'tirage': tirage, 'overdue': overdue, "soldform": soldform,
|
||||
"annulform": annulform, "resellform": resellform})
|
||||
|
||||
|
||||
@login_required
|
||||
def revente_interested(request, revente_id):
|
||||
def revente_tirages(request, tirage_id):
|
||||
"""
|
||||
Affiche à un participant la liste de toutes les reventes en cours (pour un
|
||||
tirage donné) et lui permet de s'inscrire et se désinscrire à ces reventes.
|
||||
"""
|
||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||
participant, _ = Participant.objects.get_or_create(
|
||||
user=request.user, tirage=tirage)
|
||||
subform = ReventeTirageForm(participant, prefix="subscribe")
|
||||
annulform = ReventeTirageAnnulForm(participant, prefix="annul")
|
||||
|
||||
if request.method == 'POST':
|
||||
if "subscribe" in request.POST:
|
||||
subform = ReventeTirageForm(participant, request.POST,
|
||||
prefix="subscribe")
|
||||
if subform.is_valid():
|
||||
reventes = subform.cleaned_data['reventes']
|
||||
count = reventes.count()
|
||||
for revente in reventes:
|
||||
revente.confirmed_entry.add(participant)
|
||||
if count > 0:
|
||||
messages.success(
|
||||
request,
|
||||
"Tu as bien été inscrit à {} revente{}"
|
||||
.format(count, pluralize(count))
|
||||
)
|
||||
elif "annul" in request.POST:
|
||||
annulform = ReventeTirageAnnulForm(participant, request.POST,
|
||||
prefix="annul")
|
||||
if annulform.is_valid():
|
||||
reventes = annulform.cleaned_data['reventes']
|
||||
count = reventes.count()
|
||||
for revente in reventes:
|
||||
revente.confirmed_entry.remove(participant)
|
||||
if count > 0:
|
||||
messages.success(
|
||||
request,
|
||||
"Tu as bien été désinscrit de {} revente{}"
|
||||
.format(count, pluralize(count))
|
||||
)
|
||||
|
||||
return render(request, "bda/revente/tirages.html",
|
||||
{"annulform": annulform, "subform": subform})
|
||||
|
||||
|
||||
@login_required
|
||||
def revente_confirm(request, revente_id):
|
||||
revente = get_object_or_404(SpectacleRevente, id=revente_id)
|
||||
participant, _ = Participant.objects.get_or_create(
|
||||
user=request.user, tirage=revente.attribution.spectacle.tirage)
|
||||
if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun:
|
||||
return render(request, "bda-wrongtime.html",
|
||||
if not revente.notif_sent or revente.shotgun:
|
||||
return render(request, "bda/revente/wrongtime.html",
|
||||
{"revente": revente})
|
||||
|
||||
revente.answered_mail.add(participant)
|
||||
return render(request, "bda-interested.html",
|
||||
revente.confirmed_entry.add(participant)
|
||||
return render(request, "bda/revente/confirmed.html",
|
||||
{"spectacle": revente.attribution.spectacle,
|
||||
"date": revente.date_tirage})
|
||||
|
||||
|
||||
@login_required
|
||||
def list_revente(request, tirage_id):
|
||||
def revente_subscribe(request, tirage_id):
|
||||
"""
|
||||
Permet à un participant de sélectionner ses préférences pour les reventes.
|
||||
Il recevra des notifications pour les spectacles qui l'intéressent et il
|
||||
est automatiquement inscrit aux reventes en cours au moment où il ajoute un
|
||||
spectacle à la liste des spectacles qui l'intéressent.
|
||||
"""
|
||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||
participant, _ = Participant.objects.get_or_create(
|
||||
user=request.user, tirage=tirage)
|
||||
|
@ -486,12 +535,12 @@ def list_revente(request, tirage_id):
|
|||
# la revente ayant le moins d'inscrits
|
||||
min_resell = (
|
||||
qset.filter(shotgun=False)
|
||||
.annotate(nb_subscribers=Count('answered_mail'))
|
||||
.annotate(nb_subscribers=Count('confirmed_entry'))
|
||||
.order_by('nb_subscribers')
|
||||
.first()
|
||||
)
|
||||
if min_resell is not None:
|
||||
min_resell.answered_mail.add(participant)
|
||||
min_resell.confirmed_entry.add(participant)
|
||||
inscrit_revente.append(spectacle)
|
||||
success = True
|
||||
else:
|
||||
|
@ -514,11 +563,11 @@ def list_revente(request, tirage_id):
|
|||
)
|
||||
messages.info(request, msg, extra_tags="safe")
|
||||
|
||||
return render(request, "bda/liste-reventes.html", {"form": form})
|
||||
return render(request, "bda/revente/subscribe.html", {"form": form})
|
||||
|
||||
|
||||
@login_required
|
||||
def buy_revente(request, spectacle_id):
|
||||
def revente_buy(request, spectacle_id):
|
||||
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
|
||||
tirage = spectacle.tirage
|
||||
participant, _ = Participant.objects.get_or_create(
|
||||
|
@ -532,13 +581,13 @@ def buy_revente(request, spectacle_id):
|
|||
own_reventes = reventes.filter(seller=participant)
|
||||
if len(own_reventes) > 0:
|
||||
own_reventes[0].delete()
|
||||
return HttpResponseRedirect(reverse("bda-shotgun",
|
||||
return HttpResponseRedirect(reverse("bda-revente-shotgun",
|
||||
args=[tirage.id]))
|
||||
|
||||
reventes_shotgun = reventes.filter(shotgun=True)
|
||||
|
||||
if not reventes_shotgun:
|
||||
return render(request, "bda-no-revente.html", {})
|
||||
return render(request, "bda/revente/none.html", {})
|
||||
|
||||
if request.POST:
|
||||
revente = random.choice(reventes_shotgun)
|
||||
|
@ -555,11 +604,11 @@ def buy_revente(request, spectacle_id):
|
|||
[revente.seller.user.email],
|
||||
context=context,
|
||||
)
|
||||
return render(request, "bda-success.html",
|
||||
return render(request, "bda/revente/mail-success.html",
|
||||
{"seller": revente.attribution.participant.user,
|
||||
"spectacle": spectacle})
|
||||
|
||||
return render(request, "revente-confirm.html",
|
||||
return render(request, "bda/revente/confirm-shotgun.html",
|
||||
{"spectacle": spectacle,
|
||||
"user": request.user})
|
||||
|
||||
|
@ -583,7 +632,7 @@ def revente_shotgun(request, tirage_id):
|
|||
)
|
||||
shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0]
|
||||
|
||||
return render(request, "bda-shotgun.html",
|
||||
return render(request, "bda/revente/shotgun.html",
|
||||
{"shotgun": shotgun})
|
||||
|
||||
|
||||
|
@ -630,7 +679,7 @@ class SpectacleListView(ListView):
|
|||
return categories
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(SpectacleListView, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['tirage_id'] = self.tirage.id
|
||||
context['tirage_name'] = self.tirage.title
|
||||
return context
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Formats français.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
DATETIME_FORMAT = r'l j F Y \à H:i'
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Django common settings for cof project.
|
||||
|
||||
|
@ -7,6 +6,7 @@ the local development server should be here.
|
|||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
from . import secret
|
||||
|
@ -42,9 +42,6 @@ REDIS_DB = import_secret("REDIS_DB")
|
|||
REDIS_HOST = import_secret("REDIS_HOST")
|
||||
REDIS_PORT = import_secret("REDIS_PORT")
|
||||
|
||||
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
|
||||
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
|
||||
|
||||
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
|
||||
LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL")
|
||||
|
||||
|
@ -53,9 +50,13 @@ BASE_DIR = os.path.dirname(
|
|||
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
)
|
||||
|
||||
TESTING = sys.argv[1] == 'test'
|
||||
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = [
|
||||
'shared',
|
||||
|
||||
'gestioncof',
|
||||
|
||||
# Must be before 'django.contrib.admin'.
|
||||
|
@ -100,9 +101,11 @@ INSTALLED_APPS = [
|
|||
'taggit',
|
||||
'kfet.auth',
|
||||
'kfet.cms',
|
||||
'corsheaders',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
|
@ -202,8 +205,23 @@ AUTHENTICATION_BACKENDS = (
|
|||
'kfet.auth.backends.GenericBackend',
|
||||
)
|
||||
|
||||
|
||||
# reCAPTCHA settings
|
||||
# https://github.com/praekelt/django-recaptcha
|
||||
#
|
||||
# Default settings authorize reCAPTCHA usage for local developement.
|
||||
# Public and private keys are appended in the 'prod' module settings.
|
||||
|
||||
NOCAPTCHA = True
|
||||
RECAPTCHA_USE_SSL = True
|
||||
|
||||
CORS_ORIGIN_WHITELIST = (
|
||||
'bda.ens.fr',
|
||||
'www.bda.ens.fr'
|
||||
'cof.ens.fr',
|
||||
'www.cof.ens.fr',
|
||||
)
|
||||
|
||||
# Cache settings
|
||||
|
||||
CACHES = {
|
||||
|
|
|
@ -4,13 +4,18 @@ The settings that are not listed here are imported from .common
|
|||
"""
|
||||
|
||||
from .common import * # NOQA
|
||||
from .common import INSTALLED_APPS, MIDDLEWARE
|
||||
from .common import INSTALLED_APPS, MIDDLEWARE, TESTING
|
||||
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
DEBUG = True
|
||||
|
||||
if TESTING:
|
||||
PASSWORD_HASHERS = [
|
||||
'django.contrib.auth.hashers.MD5PasswordHasher',
|
||||
]
|
||||
|
||||
|
||||
# ---
|
||||
# Apache static/media config
|
||||
|
@ -36,12 +41,13 @@ def show_toolbar(request):
|
|||
"""
|
||||
return DEBUG
|
||||
|
||||
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
|
||||
if not TESTING:
|
||||
INSTALLED_APPS += ["debug_toolbar", "debug_panel"]
|
||||
|
||||
MIDDLEWARE = [
|
||||
"debug_panel.middleware.DebugPanelMiddleware"
|
||||
] + MIDDLEWARE
|
||||
MIDDLEWARE = [
|
||||
"debug_panel.middleware.DebugPanelMiddleware"
|
||||
] + MIDDLEWARE
|
||||
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
||||
}
|
||||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ The settings that are not listed here are imported from .common
|
|||
import os
|
||||
|
||||
from .common import * # NOQA
|
||||
from .common import BASE_DIR
|
||||
from .common import BASE_DIR, import_secret
|
||||
|
||||
|
||||
DEBUG = False
|
||||
|
@ -28,3 +28,7 @@ STATIC_ROOT = os.path.join(
|
|||
STATIC_URL = "/gestion/static/"
|
||||
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
|
||||
MEDIA_URL = "/gestion/media/"
|
||||
|
||||
|
||||
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
|
||||
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
|
||||
|
|
17
cof/urls.py
17
cof/urls.py
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Fichier principal de configuration des urls du projet GestioCOF
|
||||
"""
|
||||
|
@ -72,7 +70,8 @@ urlpatterns = [
|
|||
url(r'^registration/empty$', gestioncof_views.registration_form2,
|
||||
name="empty-registration"),
|
||||
# Autocompletion
|
||||
url(r'^autocomplete/registration$', autocomplete),
|
||||
url(r'^autocomplete/registration$', autocomplete,
|
||||
name="cof.registration.autocomplete"),
|
||||
url(r'^user/autocomplete$', gestioncof_views.user_autocomplete,
|
||||
name='cof-user-autocomplete'),
|
||||
# Interface admin
|
||||
|
@ -87,15 +86,19 @@ urlpatterns = [
|
|||
name='utile_cof'),
|
||||
url(r'^utile_bda$', gestioncof_views.utile_bda,
|
||||
name='utile_bda'),
|
||||
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
|
||||
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
|
||||
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
|
||||
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff,
|
||||
name="ml_diffbda"),
|
||||
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof,
|
||||
name='ml_diffcof'),
|
||||
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente,
|
||||
name="ml_bda_revente"),
|
||||
url(r'^k-fet/', include('kfet.urls')),
|
||||
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
|
||||
url(r'^cms/', include(wagtailadmin_urls)),
|
||||
url(r'^documents/', include(wagtaildocs_urls)),
|
||||
# djconfig
|
||||
url(r"^config", gestioncof_views.ConfigUpdate.as_view()),
|
||||
url(r"^config", gestioncof_views.ConfigUpdate.as_view(),
|
||||
name='config.edit'),
|
||||
]
|
||||
|
||||
if 'debug_toolbar' in settings.INSTALLED_APPS:
|
||||
|
|
|
@ -181,7 +181,7 @@ class UserProfileAdmin(UserAdmin):
|
|||
def get_fieldsets(self, request, user=None):
|
||||
if not request.user.is_superuser:
|
||||
return self.staff_fieldsets
|
||||
return super(UserProfileAdmin, self).get_fieldsets(request, user)
|
||||
return super().get_fieldsets(request, user)
|
||||
|
||||
def save_model(self, request, user, form, change):
|
||||
cof_group, created = Group.objects.get_or_create(name='COF')
|
||||
|
@ -267,7 +267,7 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin):
|
|||
|
||||
class ClubAdminForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
cleaned_data = super(ClubAdminForm, self).clean()
|
||||
cleaned_data = super().clean()
|
||||
respos = cleaned_data.get('respos')
|
||||
members = cleaned_data.get('membres')
|
||||
for respo in respos.all():
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ldap3 import Connection
|
||||
|
||||
from django import shortcuts
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import csv
|
||||
from django.http import HttpResponse, HttpResponseForbidden
|
||||
from django.template.defaultfilters import slugify
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ class EventForm(forms.Form):
|
|||
event = kwargs.pop("event")
|
||||
self.event = event
|
||||
current_choices = kwargs.pop("current_choices", None)
|
||||
super(EventForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
choices = {}
|
||||
if current_choices:
|
||||
for choice in current_choices.all():
|
||||
|
@ -60,7 +60,7 @@ class SurveyForm(forms.Form):
|
|||
def __init__(self, *args, **kwargs):
|
||||
survey = kwargs.pop("survey")
|
||||
current_answers = kwargs.pop("current_answers", None)
|
||||
super(SurveyForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
answers = {}
|
||||
if current_answers:
|
||||
for answer in current_answers.all():
|
||||
|
@ -100,7 +100,7 @@ class SurveyForm(forms.Form):
|
|||
class SurveyStatusFilterForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
survey = kwargs.pop("survey")
|
||||
super(SurveyStatusFilterForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
for question in survey.questions.all():
|
||||
for answer in question.answers.all():
|
||||
name = "question_%d_answer_%d" % (question.id, answer.id)
|
||||
|
@ -129,7 +129,7 @@ class SurveyStatusFilterForm(forms.Form):
|
|||
class EventStatusFilterForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
event = kwargs.pop("event")
|
||||
super(EventStatusFilterForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
for option in event.options.all():
|
||||
for choice in option.choices.all():
|
||||
name = "option_%d_choice_%d" % (option.id, choice.id)
|
||||
|
@ -175,12 +175,12 @@ class UserProfileForm(forms.ModelForm):
|
|||
last_name = forms.CharField(label=_('Nom'), max_length=30)
|
||||
|
||||
def __init__(self, *args, **kw):
|
||||
super(UserProfileForm, self).__init__(*args, **kw)
|
||||
super().__init__(*args, **kw)
|
||||
self.fields['first_name'].initial = self.instance.user.first_name
|
||||
self.fields['last_name'].initial = self.instance.user.last_name
|
||||
|
||||
def save(self, *args, **kw):
|
||||
super(UserProfileForm, self).save(*args, **kw)
|
||||
super().save(*args, **kw)
|
||||
self.instance.user.first_name = self.cleaned_data.get('first_name')
|
||||
self.instance.user.last_name = self.cleaned_data.get('last_name')
|
||||
self.instance.user.save()
|
||||
|
@ -193,7 +193,7 @@ class UserProfileForm(forms.ModelForm):
|
|||
|
||||
class RegistrationUserForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kw):
|
||||
super(RegistrationUserForm, self).__init__(*args, **kw)
|
||||
super().__init__(*args, **kw)
|
||||
self.fields['username'].help_text = ""
|
||||
|
||||
class Meta:
|
||||
|
@ -219,8 +219,7 @@ class RegistrationPassUserForm(RegistrationUserForm):
|
|||
return pass2
|
||||
|
||||
def save(self, commit=True, *args, **kwargs):
|
||||
user = super(RegistrationPassUserForm, self).save(commit, *args,
|
||||
**kwargs)
|
||||
user = super().save(commit, *args, **kwargs)
|
||||
user.set_password(self.cleaned_data['password2'])
|
||||
if commit:
|
||||
user.save()
|
||||
|
@ -229,7 +228,7 @@ class RegistrationPassUserForm(RegistrationUserForm):
|
|||
|
||||
class RegistrationProfileForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kw):
|
||||
super(RegistrationProfileForm, self).__init__(*args, **kw)
|
||||
super().__init__(*args, **kw)
|
||||
self.fields['mailing_cof'].initial = True
|
||||
self.fields['mailing_bda'].initial = True
|
||||
self.fields['mailing_bda_revente'].initial = True
|
||||
|
@ -274,7 +273,7 @@ class AdminEventForm(forms.Form):
|
|||
kwargs["initial"] = {"status": "wait"}
|
||||
else:
|
||||
kwargs["initial"] = {"status": "no"}
|
||||
super(AdminEventForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
choices = {}
|
||||
for choice in current_choices:
|
||||
if choice.event_option.id not in choices:
|
||||
|
@ -337,24 +336,27 @@ class BaseEventRegistrationFormset(BaseFormSet):
|
|||
self.events = kwargs.pop('events')
|
||||
self.current_registrations = kwargs.pop('current_registrations', None)
|
||||
self.extra = len(self.events)
|
||||
super(BaseEventRegistrationFormset, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _construct_form(self, index, **kwargs):
|
||||
kwargs['event'] = self.events[index]
|
||||
if self.current_registrations is not None:
|
||||
kwargs['current_registration'] = self.current_registrations[index]
|
||||
return super(BaseEventRegistrationFormset, self)._construct_form(
|
||||
index, **kwargs)
|
||||
return super()._construct_form(index, **kwargs)
|
||||
|
||||
|
||||
EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
|
||||
|
||||
|
||||
class CalendarForm(forms.ModelForm):
|
||||
subscribe_to_events = forms.BooleanField(
|
||||
initial=True,
|
||||
label="Événements du COF")
|
||||
label="Événements du COF",
|
||||
required=False)
|
||||
subscribe_to_my_shows = forms.BooleanField(
|
||||
initial=True,
|
||||
label="Les spectacles pour lesquels j'ai obtenu une place")
|
||||
label="Les spectacles pour lesquels j'ai obtenu une place",
|
||||
required=False)
|
||||
other_shows = forms.ModelMultipleChoiceField(
|
||||
label="Spectacles supplémentaires",
|
||||
queryset=Spectacle.objects.filter(tirage__active=True),
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Import des mails de GestioCOF dans la base de donnée
|
||||
"""
|
||||
|
@ -63,8 +62,9 @@ class Command(BaseCommand):
|
|||
except CustomMail.DoesNotExist:
|
||||
mail = CustomMail.objects.create(**fields)
|
||||
status['synced'] += 1
|
||||
self.stdout.write(
|
||||
'SYNCED {:s}'.format(fields['shortname']))
|
||||
if options['verbosity']:
|
||||
self.stdout.write(
|
||||
'SYNCED {:s}'.format(fields['shortname']))
|
||||
assoc['mails'][obj['pk']] = mail
|
||||
|
||||
# Variables
|
||||
|
@ -79,8 +79,9 @@ class Command(BaseCommand):
|
|||
except Variable.DoesNotExist:
|
||||
Variable.objects.create(**fields)
|
||||
|
||||
# C'est agréable d'avoir le résultat affiché
|
||||
self.stdout.write(
|
||||
'{synced:d} mails synchronized {unchanged:d} unchanged'
|
||||
.format(**status)
|
||||
)
|
||||
if options['verbosity']:
|
||||
# C'est agréable d'avoir le résultat affiché
|
||||
self.stdout.write(
|
||||
'{synced:d} mails synchronized {unchanged:d} unchanged'
|
||||
.format(**status)
|
||||
)
|
||||
|
|
|
@ -159,23 +159,23 @@
|
|||
},
|
||||
{
|
||||
"model": "custommail.custommail",
|
||||
"pk": 3,
|
||||
"fields": {
|
||||
"shortname": "bda-revente",
|
||||
"subject": "{{ show }}",
|
||||
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA",
|
||||
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente."
|
||||
},
|
||||
"pk": 3
|
||||
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour leur signaler qu'une place vient d'\u00eatre mise en vente.",
|
||||
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "custommail.custommail",
|
||||
"pk": 4,
|
||||
"fields": {
|
||||
"shortname": "bda-shotgun",
|
||||
"subject": "{{ show }}",
|
||||
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA",
|
||||
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es."
|
||||
},
|
||||
"pk": 4
|
||||
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
|
||||
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "custommail.custommail",
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from captcha.fields import ReCaptchaField
|
||||
|
||||
from django import forms
|
||||
|
@ -12,7 +10,7 @@ from gestioncof.petits_cours_models import PetitCoursDemande, PetitCoursAbility
|
|||
|
||||
class BaseMatieresFormSet(BaseInlineFormSet):
|
||||
def clean(self):
|
||||
super(BaseMatieresFormSet, self).clean()
|
||||
super().clean()
|
||||
if any(self.errors):
|
||||
# Don't bother validating the formset unless each form is
|
||||
# valid on its own
|
||||
|
@ -36,7 +34,7 @@ class DemandeForm(ModelForm):
|
|||
captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DemandeForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['matieres'].help_text = ''
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from functools import reduce
|
||||
|
||||
from django.db import models
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import json
|
||||
from custommail.shortcuts import render_custom_mail
|
||||
|
||||
|
@ -44,7 +42,7 @@ class DemandeDetailView(DetailView):
|
|||
context_object_name = "demande"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(DemandeDetailView, self).get_context_data(**kwargs)
|
||||
context = super().get_context_data(**kwargs)
|
||||
obj = self.object
|
||||
context['attributions'] = obj.petitcoursattribution_set.all()
|
||||
return context
|
||||
|
|
|
@ -1140,3 +1140,14 @@ p.help-block {
|
|||
margin: 5px auto;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
div.bg-info {
|
||||
border-radius: 3px;
|
||||
padding: 0.3em 1em;
|
||||
margin-left: 1em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.bootstrap-form-reduce > .form-group {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% if success %}
|
||||
<p class="success">Votre demande a été enregistrée avec succès !</p>
|
||||
{% else %}
|
||||
<form id="demandecours" method="post" action="{% url "gestioncof.petits_cours_views.demande_raw" %}">
|
||||
<form id="demandecours" method="post" action="{% url "petits-cours-demande-raw" %}">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form | bootstrap }}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% if success %}
|
||||
<p class="success">Votre demande a été enregistrée avec succès !</p>
|
||||
{% else %}
|
||||
<form id="demandecours" method="post" action="{% url "gestioncof.petits_cours_views.demande" %}">
|
||||
<form id="demandecours" method="post" action="{% url "petits-cours-demande" %}">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ form.as_table }}
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{% endif %}
|
||||
{% include "tristate_js.html" %}
|
||||
<h3>Filtres</h3>
|
||||
<form method="post" action="{% url 'gestioncof.views.event_status' event.id %}">
|
||||
<form method="post" action="{% url 'event.details.status' event.id %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" />
|
||||
|
|
|
@ -12,7 +12,7 @@ souscrire aux événements du COF et/ou aux spectacles BdA.
|
|||
|
||||
{% if token %}
|
||||
<p>Votre calendrier (compatible avec toutes les applications d'agenda) se trouve à
|
||||
<a href="{% url 'gestioncof.views.calendar_ics' token %}">cette adresse</a>.</p>
|
||||
<a href="{% url 'calendar.ics' token %}">cette adresse</a>.</p>
|
||||
|
||||
<ul>
|
||||
<li>Pour l'ajouter à Thunderbird (lightning), il faut copier ce lien et aller
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
{% block realcontent %}
|
||||
<h2>Modifier mon profil</h2>
|
||||
<form id="profile form-horizontal" method="post" action="{% url 'gestioncof.views.profile' %}">
|
||||
<form id="profile form-horizontal" method="post" action="{% url 'profile' %}">
|
||||
<div class="row" style="margin: 0 15%;">
|
||||
{% csrf_token %}
|
||||
<fieldset"center-block">
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{% else %}
|
||||
<h3>Inscription d'un nouveau compte (extérieur ?)</h3>
|
||||
{% endif %}
|
||||
<form role="form" id="profile" method="post" action="{% url 'gestioncof.views.registration' %}">
|
||||
<form role="form" id="profile" method="post" action="{% url 'registration' %}">
|
||||
{% csrf_token %}
|
||||
<table>
|
||||
{{ user_form | bootstrap }}
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
{% if survey.details %}
|
||||
<p>{{ survey.details }}</p>
|
||||
{% endif %}
|
||||
<form class="form-horizontal" method="post" action="{% url 'gestioncof.views.survey' survey.id %}">
|
||||
<form class="form-horizontal" method="post" action="{% url 'survey.details' survey.id %}">
|
||||
{% csrf_token %}
|
||||
{{ form | bootstrap}}
|
||||
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
<h2>Liens utiles du COF</h2>
|
||||
<h3>COF</h3>
|
||||
<ul>
|
||||
<li><a href="{% url 'gestioncof.views.export_members' %}">Export des membres du COF</a></li>
|
||||
<li><a href="{% url 'gestioncof.views.liste_diffcof' %}">Diffusion COF</a></li>
|
||||
<li><a href="{% url 'cof.membres_export' %}">Export des membres du COF</a></li>
|
||||
<li><a href="{% url 'ml_diffcof' %}">Diffusion COF</a></li>
|
||||
</ul>
|
||||
|
||||
<h3>Mega</h3>
|
||||
<ul>
|
||||
<li><a href="{% url 'gestioncof.views.export_mega_participants' %}">Export des non-orgas uniquement</a></li>
|
||||
<li><a href="{% url 'gestioncof.views.export_mega_orgas' %}">Export des orgas uniquement</a></li>
|
||||
<li><a href="{% url 'gestioncof.views.export_mega' %}">Export de tout le monde</a></li>
|
||||
<li><a href="{% url 'cof.mega_export_participants' %}">Export des non-orgas uniquement</a></li>
|
||||
<li><a href="{% url 'cof.mega_export_orgas' %}">Export des orgas uniquement</a></li>
|
||||
<li><a href="{% url 'cof.mega_export' %}">Export de tout le monde</a></li>
|
||||
</ul>
|
||||
|
||||
<p>Note : pour ouvrir les fichiers .csv avec Excel, il faut
|
||||
|
|
|
@ -43,9 +43,10 @@
|
|||
<li><a href="{% url "bda-etat-places" tirage.id %}">État des demandes</a></li>
|
||||
{% else %}
|
||||
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li>
|
||||
<li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li>
|
||||
<li><a href="{% url "bda-liste-revente" tirage.id %}">S'inscrire à BdA-Revente</a></li>
|
||||
<li><a href="{% url "bda-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
|
||||
<li><a href="{% url "bda-revente-manage" tirage.id %}">Gérer les places que je revends</a></li>
|
||||
<li><a href="{% url "bda-revente-tirages" tirage.id %}">Voir les reventes en cours</a></li>
|
||||
<li><a href="{% url "bda-revente-subscribe" tirage.id %}">Indiquer les spectacles qui m'intéressent</a></li>
|
||||
<li><a href="{% url "bda-revente-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
</tr></thead>
|
||||
<tbody class="bda_formset_content">
|
||||
{% endif %}
|
||||
<tr class="{% cycle row1,row2 %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
||||
<tr class="{% cycle 'row1' 'row2' %} dynamic-form {% if form.instance.pk %}has_original{% endif %}">
|
||||
{% for field in form.visible_fields %}
|
||||
{% if field.name != "DELETE" and field.name != "priority" %}
|
||||
<td class="bda-field-{{ field.name }}">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
{% block page_size %}col-sm-8{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
|
||||
<script src="{% static "vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js" %}" type="text/javascript"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
@ -18,7 +18,7 @@
|
|||
// On attend que la page soit prête pour executer le code
|
||||
$(document).ready(function() {
|
||||
$('input#search_autocomplete').yourlabsAutocomplete({
|
||||
url: '{% url 'gestioncof.autocomplete.autocomplete' %}',
|
||||
url: '{% url 'cof.registration.autocomplete' %}',
|
||||
minimumCharacters: 3,
|
||||
id: 'search_autocomplete',
|
||||
choiceSelector: 'li:has(a)',
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
{% endif %}
|
||||
<h3>Filtres</h3>
|
||||
{% include "tristate_js.html" %}
|
||||
<form method="post" action="{% url 'gestioncof.views.survey_status' survey.id %}">
|
||||
<form method="post" action="{% url 'survey.details.status' survey.id %}">
|
||||
{% csrf_token %}
|
||||
{{ form.as_p }}
|
||||
<input style="margin-top:10px;" type="submit" class="btn btn-primary" value="Filtrer" />
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<h2>Liens utiles du BdA</h2>
|
||||
<h3>Listes mail</h3>
|
||||
<ul>
|
||||
<li><a href="{% url 'gestioncof.views.liste_bdadiff' %}">BdA diffusion</a></li>
|
||||
<li><a href="{% url 'gestioncof.views.liste_bdarevente' %}">BdA revente</a></li>
|
||||
<li><a href="{% url 'ml_diffbda' %}">BdA diffusion</a></li>
|
||||
<li><a href="{% url 'ml_bda_revente' %}">BdA revente</a></li>
|
||||
</ul>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django import template
|
||||
from django.utils.safestring import mark_safe
|
||||
|
||||
|
|
0
gestioncof/tests/__init__.py
Normal file
0
gestioncof/tests/__init__.py
Normal file
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This file demonstrates writing tests using the unittest module. These will pass
|
||||
when you run "manage.py test".
|
||||
|
@ -6,10 +5,6 @@ when you run "manage.py test".
|
|||
Replace this with more appropriate tests for your application.
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from gestioncof.models import CofProfile, User
|
874
gestioncof/tests/test_views.py
Normal file
874
gestioncof/tests/test_views.py
Normal file
|
@ -0,0 +1,874 @@
|
|||
import csv
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.api import get_messages
|
||||
from django.contrib.messages.storage.base import Message
|
||||
from django.test import Client, TestCase
|
||||
from django.urls import reverse
|
||||
|
||||
from bda.models import Salle, Tirage
|
||||
from gestioncof.models import (
|
||||
CalendarSubscription, Club, Event, Survey, SurveyAnswer
|
||||
)
|
||||
from gestioncof.tests.testcases import ViewTestCaseMixin
|
||||
|
||||
from .utils import create_member, create_root, create_user
|
||||
|
||||
|
||||
class HomeViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'home'
|
||||
url_expected = '/'
|
||||
|
||||
auth_user = 'user'
|
||||
auth_forbidden = [None]
|
||||
|
||||
def test(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class ProfileViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'profile'
|
||||
url_expected = '/profile'
|
||||
|
||||
http_methods = ['GET', 'POST']
|
||||
|
||||
auth_user = 'member'
|
||||
auth_forbidden = [None, 'user']
|
||||
|
||||
def test_get(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_post(self):
|
||||
u = self.users['member']
|
||||
|
||||
r = self.client.post(self.url, {
|
||||
'first_name': 'First',
|
||||
'last_name': 'Last',
|
||||
'phone': '',
|
||||
# 'mailing_cof': '1',
|
||||
# 'mailing_bda': '1',
|
||||
# 'mailing_bda_revente': '1',
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
expected_message = Message(messages.SUCCESS, (
|
||||
"Votre profil a été mis à jour avec succès !"
|
||||
))
|
||||
self.assertIn(expected_message, get_messages(r.wsgi_request))
|
||||
u.refresh_from_db()
|
||||
self.assertEqual(u.first_name, 'First')
|
||||
self.assertEqual(u.last_name, 'Last')
|
||||
self.assertFalse(u.profile.mailing_cof)
|
||||
self.assertFalse(u.profile.mailing_bda)
|
||||
self.assertFalse(u.profile.mailing_bda_revente)
|
||||
|
||||
|
||||
class UtilsViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'utile_cof'
|
||||
url_expected = '/utile_cof'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def test(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class MailingListDiffCof(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'ml_diffcof'
|
||||
url_expected = '/utile_cof/diff_cof'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.u1 = create_member('u1', attrs={'mailing_cof': True})
|
||||
self.u2 = create_member('u2', attrs={'mailing_cof': False})
|
||||
self.u3 = create_user('u3', attrs={'mailing_cof': True})
|
||||
|
||||
def test(self):
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.context['personnes'].get(), self.u1.profile)
|
||||
|
||||
|
||||
class ConfigUpdateViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'config.edit'
|
||||
url_expected = '/config'
|
||||
|
||||
http_methods = ['GET', 'POST']
|
||||
|
||||
auth_user = 'root'
|
||||
auth_forbidden = [None, 'user', 'member', 'staff']
|
||||
|
||||
def get_users_extra(self):
|
||||
return {
|
||||
'root': create_root('root'),
|
||||
}
|
||||
|
||||
def test_get(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_post(self):
|
||||
r = self.client.post(self.url, {
|
||||
'gestion_banner': 'Announcement !',
|
||||
})
|
||||
|
||||
self.assertRedirects(r, reverse('home'))
|
||||
|
||||
|
||||
class UserAutocompleteViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'cof-user-autocomplete'
|
||||
url_expected = '/user/autocomplete'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def test(self):
|
||||
r = self.client.get(self.url, {'q': 'user'})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class ExportMembersViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'cof.membres_export'
|
||||
url_expected = '/export/members'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def test(self):
|
||||
u1, u2 = self.users['member'], self.users['staff']
|
||||
u1.first_name = 'first'
|
||||
u1.last_name = 'last'
|
||||
u1.email = 'user@mail.net'
|
||||
u1.save()
|
||||
u1.profile.phone = '0123456789'
|
||||
u1.profile.departement = 'Dept'
|
||||
u1.profile.save()
|
||||
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
data = list(csv.reader(r.content.decode('utf-8').split('\n')[:-1]))
|
||||
expected = [
|
||||
[
|
||||
str(u1.pk), 'member', 'first', 'last', 'user@mail.net',
|
||||
'0123456789', '1A', 'Dept', 'normalien',
|
||||
],
|
||||
[str(u2.pk), 'staff', '', '', '', '', '1A', '', 'normalien'],
|
||||
]
|
||||
# Sort before checking equality, the order of the output of csv.reader
|
||||
# does not seem deterministic
|
||||
expected.sort(key=lambda row: int(row[0]))
|
||||
data.sort(key=lambda row: int(row[0]))
|
||||
self.assertListEqual(data, expected)
|
||||
|
||||
|
||||
class MegaHelpers:
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
u1 = create_user('u1')
|
||||
u1.first_name = 'first'
|
||||
u1.last_name = 'last'
|
||||
u1.email = 'user@mail.net'
|
||||
u1.save()
|
||||
u1.profile.phone = '0123456789'
|
||||
u1.profile.departement = 'Dept'
|
||||
u1.profile.comments = 'profile.comments'
|
||||
u1.profile.save()
|
||||
|
||||
u2 = create_user('u2')
|
||||
u2.profile.save()
|
||||
|
||||
m = Event.objects.create(title='MEGA 2017')
|
||||
|
||||
cf1 = m.commentfields.create(name='Commentaire')
|
||||
cf2 = m.commentfields.create(
|
||||
name='Comment Field 2', fieldtype='char',
|
||||
)
|
||||
|
||||
option_type = m.options.create(name='Conscrit/Orga ?')
|
||||
choice_orga = option_type.choices.create(value='Orga')
|
||||
choice_conscrit = option_type.choices.create(value='Conscrit')
|
||||
|
||||
mr1 = m.eventregistration_set.create(user=u1)
|
||||
mr1.options.add(choice_orga)
|
||||
mr1.comments.create(commentfield=cf1, content='Comment 1')
|
||||
mr1.comments.create(commentfield=cf2, content='Comment 2')
|
||||
|
||||
mr2 = m.eventregistration_set.create(user=u2)
|
||||
mr2.options.add(choice_conscrit)
|
||||
|
||||
self.u1 = u1
|
||||
self.u2 = u2
|
||||
self.m = m
|
||||
self.choice_orga = choice_orga
|
||||
self.choice_conscrit = choice_conscrit
|
||||
|
||||
|
||||
class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||
url_name = 'cof.mega_export'
|
||||
url_expected = '/export/mega'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def test(self):
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(self.load_from_csv_response(r), [
|
||||
[
|
||||
'u1', 'first', 'last', 'user@mail.net', '0123456789',
|
||||
str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2',
|
||||
],
|
||||
['u2', '', '', '', '', str(self.u2.pk), '', ''],
|
||||
])
|
||||
|
||||
|
||||
class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||
url_name = 'cof.mega_export_orgas'
|
||||
url_expected = '/export/mega/orgas'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def test(self):
|
||||
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(self.load_from_csv_response(r), [
|
||||
[
|
||||
'u1', 'first', 'last', 'user@mail.net', '0123456789',
|
||||
str(self.u1.pk), 'profile.comments', 'Comment 1---Comment 2',
|
||||
],
|
||||
])
|
||||
|
||||
|
||||
class ExportMegaParticipantsViewTests(
|
||||
MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||
url_name = 'cof.mega_export_participants'
|
||||
url_expected = '/export/mega/participants'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def test(self):
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(self.load_from_csv_response(r), [
|
||||
['u2', '', '', '', '', str(self.u2.pk), '', ''],
|
||||
])
|
||||
|
||||
|
||||
class ExportMegaRemarksViewTests(
|
||||
MegaHelpers, ViewTestCaseMixin, TestCase):
|
||||
url_name = 'cof.mega_export_remarks'
|
||||
url_expected = '/export/mega/avecremarques'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
def test(self):
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertListEqual(self.load_from_csv_response(r), [
|
||||
[
|
||||
'u1', 'first', 'last', 'user@mail.net', '0123456789',
|
||||
str(self.u1.pk), 'profile.comments', 'Comment 1',
|
||||
],
|
||||
])
|
||||
|
||||
|
||||
class ClubListViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'liste-clubs'
|
||||
url_expected = '/clubs/liste'
|
||||
|
||||
auth_user = 'member'
|
||||
auth_forbidden = [None, 'user']
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.c1 = Club.objects.create(name='Club1')
|
||||
self.c2 = Club.objects.create(name='Club2')
|
||||
|
||||
m = self.users['member']
|
||||
self.c1.membres.add(m)
|
||||
self.c1.respos.add(m)
|
||||
|
||||
def test_as_member(self):
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.context['owned_clubs'].get(), self.c1)
|
||||
self.assertEqual(r.context['other_clubs'].get(), self.c2)
|
||||
|
||||
def test_as_staff(self):
|
||||
u = self.users['staff']
|
||||
c = Client()
|
||||
c.force_login(u)
|
||||
|
||||
r = c.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertQuerysetEqual(
|
||||
r.context['owned_clubs'], map(repr, [self.c1, self.c2]),
|
||||
ordered=False,
|
||||
)
|
||||
|
||||
|
||||
class ClubMembersViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'membres-club'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
@property
|
||||
def url_kwargs(self):
|
||||
return {'name': self.c.name}
|
||||
|
||||
@property
|
||||
def url_expected(self):
|
||||
return '/clubs/membres/{}'.format(self.c.name)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.u1 = create_user('u1')
|
||||
self.u2 = create_user('u2')
|
||||
|
||||
self.c = Club.objects.create(name='Club')
|
||||
self.c.membres.add(self.u1, self.u2)
|
||||
self.c.respos.add(self.u1)
|
||||
|
||||
def test_as_staff(self):
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertEqual(r.context['members_no_respo'].get(), self.u2)
|
||||
|
||||
def test_as_respo(self):
|
||||
u = self.users['user']
|
||||
self.c.respos.add(u)
|
||||
|
||||
c = Client()
|
||||
c.force_login(u)
|
||||
r = c.get(self.url)
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class ClubChangeRespoViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'change-respo'
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
@property
|
||||
def url_kwargs(self):
|
||||
return {'club_name': self.c.name, 'user_id': self.users['user'].pk}
|
||||
|
||||
@property
|
||||
def url_expected(self):
|
||||
return '/clubs/change_respo/{}/{}'.format(
|
||||
self.c.name, self.users['user'].pk,
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.c = Club.objects.create(name='Club')
|
||||
|
||||
def test(self):
|
||||
u = self.users['user']
|
||||
expected_redirect = reverse('membres-club', kwargs={
|
||||
'name': self.c.name,
|
||||
})
|
||||
self.c.membres.add(u)
|
||||
|
||||
r = self.client.get(self.url)
|
||||
self.assertRedirects(r, expected_redirect)
|
||||
self.assertIn(u, self.c.respos.all())
|
||||
|
||||
self.client.get(self.url)
|
||||
self.assertNotIn(u, self.c.respos.all())
|
||||
|
||||
|
||||
class CalendarViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'calendar'
|
||||
url_expected = '/calendar/subscription'
|
||||
|
||||
auth_user = 'member'
|
||||
auth_forbidden = [None, 'user']
|
||||
|
||||
post_expected_message = Message(
|
||||
messages.SUCCESS, "Calendrier mis à jour avec succès.")
|
||||
|
||||
def test_get(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_post_new(self):
|
||||
r = self.client.post(self.url, {
|
||||
'subscribe_to_events': True,
|
||||
'subscribe_to_my_shows': True,
|
||||
'other_shows': [],
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
|
||||
cs = self.users['member'].calendarsubscription
|
||||
self.assertTrue(cs.subscribe_to_events)
|
||||
self.assertTrue(cs.subscribe_to_my_shows)
|
||||
|
||||
def test_post_edit(self):
|
||||
u = self.users['member']
|
||||
token = uuid.uuid4()
|
||||
cs = CalendarSubscription.objects.create(token=token, user=u)
|
||||
|
||||
r = self.client.post(self.url, {
|
||||
'other_shows': [],
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
|
||||
cs.refresh_from_db()
|
||||
self.assertEqual(cs.token, token)
|
||||
self.assertFalse(cs.subscribe_to_events)
|
||||
self.assertFalse(cs.subscribe_to_my_shows)
|
||||
|
||||
def test_post_other_shows(self):
|
||||
t = Tirage.objects.create(
|
||||
ouverture=self.now,
|
||||
fermeture=self.now,
|
||||
active=True,
|
||||
)
|
||||
l = Salle.objects.create()
|
||||
s = t.spectacle_set.create(
|
||||
date=self.now, price=3.5, slots=20, location=l, listing=True)
|
||||
|
||||
r = self.client.post(self.url, {'other_shows': [str(s.pk)]})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class CalendarICSViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'calendar.ics'
|
||||
|
||||
auth_user = None
|
||||
auth_forbidden = []
|
||||
|
||||
@property
|
||||
def url_kwargs(self):
|
||||
return {'token': self.token}
|
||||
|
||||
@property
|
||||
def url_expected(self):
|
||||
return '/calendar/{}/calendar.ics'.format(self.token)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.token = uuid.uuid4()
|
||||
|
||||
self.t = Tirage.objects.create(
|
||||
ouverture=self.now,
|
||||
fermeture=self.now,
|
||||
active=True,
|
||||
)
|
||||
l = Salle.objects.create(name='Location')
|
||||
self.s1 = self.t.spectacle_set.create(
|
||||
price=1, slots=10, location=l, listing=True,
|
||||
title='Spectacle 1', date=self.now + timedelta(days=1),
|
||||
)
|
||||
self.s2 = self.t.spectacle_set.create(
|
||||
price=2, slots=20, location=l, listing=True,
|
||||
title='Spectacle 2', date=self.now + timedelta(days=2),
|
||||
)
|
||||
self.s3 = self.t.spectacle_set.create(
|
||||
price=3, slots=30, location=l, listing=True,
|
||||
title='Spectacle 3', date=self.now + timedelta(days=3),
|
||||
)
|
||||
|
||||
def test(self):
|
||||
u = self.users['user']
|
||||
p = u.participant_set.create(tirage=self.t)
|
||||
p.attribution_set.create(spectacle=self.s1)
|
||||
|
||||
self.cs = CalendarSubscription.objects.create(
|
||||
user=u, token=self.token,
|
||||
subscribe_to_my_shows=True, subscribe_to_events=True,
|
||||
)
|
||||
self.cs.other_shows.add(self.s2)
|
||||
|
||||
r = self.client.get(self.url)
|
||||
|
||||
def get_dt_from_ical(v):
|
||||
return v.dt
|
||||
|
||||
self.assertCalEqual(r.content.decode('utf-8'), [
|
||||
{
|
||||
'summary': 'Spectacle 1',
|
||||
'dtstart': (get_dt_from_ical, (
|
||||
(self.now + timedelta(days=1)).replace(microsecond=0)
|
||||
)),
|
||||
'dtend': (get_dt_from_ical, (
|
||||
(self.now + timedelta(days=1, hours=2)).replace(
|
||||
microsecond=0)
|
||||
)),
|
||||
'location': 'Location',
|
||||
'uid': 'show-{}-{}@example.com'.format(self.s1.pk, self.t.pk),
|
||||
},
|
||||
{
|
||||
'summary': 'Spectacle 2',
|
||||
'dtstart': (get_dt_from_ical, (
|
||||
(self.now + timedelta(days=2)).replace(microsecond=0)
|
||||
)),
|
||||
'dtend': (get_dt_from_ical, (
|
||||
(self.now + timedelta(days=2, hours=2)).replace(
|
||||
microsecond=0)
|
||||
)),
|
||||
'location': 'Location',
|
||||
'uid': 'show-{}-{}@example.com'.format(self.s2.pk, self.t.pk),
|
||||
},
|
||||
])
|
||||
|
||||
|
||||
class EventViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'event.details'
|
||||
http_methods = ['GET', 'POST']
|
||||
|
||||
auth_user = 'user'
|
||||
auth_forbidden = [None]
|
||||
|
||||
post_expected_message = Message(messages.SUCCESS, (
|
||||
"Votre inscription a bien été enregistrée ! Vous pouvez cependant la "
|
||||
"modifier jusqu'à la fin des inscriptions."
|
||||
))
|
||||
|
||||
@property
|
||||
def url_kwargs(self):
|
||||
return {'event_id': self.e.pk}
|
||||
|
||||
@property
|
||||
def url_expected(self):
|
||||
return '/event/{}'.format(self.e.pk)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.e = Event.objects.create()
|
||||
|
||||
self.ecf1 = self.e.commentfields.create(name='Comment Field 1')
|
||||
self.ecf2 = self.e.commentfields.create(
|
||||
name='Comment Field 2', fieldtype='char',
|
||||
)
|
||||
|
||||
self.o1 = self.e.options.create(name='Option 1')
|
||||
self.o2 = self.e.options.create(name='Option 2', multi_choices=True)
|
||||
|
||||
self.oc1 = self.o1.choices.create(value='O1 - Choice 1')
|
||||
self.oc2 = self.o1.choices.create(value='O1 - Choice 2')
|
||||
self.oc3 = self.o2.choices.create(value='O2 - Choice 1')
|
||||
self.oc4 = self.o2.choices.create(value='O2 - Choice 2')
|
||||
|
||||
def test_get(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_post_new(self):
|
||||
r = self.client.post(self.url, {
|
||||
'option_{}'.format(self.o1.pk): [str(self.oc1.pk)],
|
||||
'option_{}'.format(self.o2.pk): [
|
||||
str(self.oc3.pk), str(self.oc4.pk),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
|
||||
|
||||
er = self.e.eventregistration_set.get(user=self.users['user'])
|
||||
self.assertQuerysetEqual(
|
||||
er.options.all(), map(repr, [self.oc1, self.oc3, self.oc4]),
|
||||
ordered=False,
|
||||
)
|
||||
# TODO: Make the view care about comments.
|
||||
# self.assertQuerysetEqual(
|
||||
# er.comments.all(), map(repr, []),
|
||||
# ordered=False,
|
||||
# )
|
||||
|
||||
def test_post_edit(self):
|
||||
er = self.e.eventregistration_set.create(user=self.users['user'])
|
||||
er.options.add(self.oc1, self.oc3, self.oc4)
|
||||
er.comments.create(
|
||||
commentfield=self.ecf1, content='Comment 1',
|
||||
)
|
||||
|
||||
r = self.client.post(self.url, {
|
||||
'option_{}'.format(self.o1.pk): [],
|
||||
'option_{}'.format(self.o2.pk): [str(self.oc3.pk)],
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
|
||||
|
||||
er.refresh_from_db()
|
||||
self.assertQuerysetEqual(
|
||||
er.options.all(), map(repr, [self.oc3]),
|
||||
ordered=False,
|
||||
)
|
||||
# TODO: Make the view care about comments.
|
||||
# self.assertQuerysetEqual(
|
||||
# er.comments.all(), map(repr, []),
|
||||
# ordered=False,
|
||||
# )
|
||||
|
||||
|
||||
class EventStatusViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'event.details.status'
|
||||
|
||||
http_methods = ['GET', 'POST']
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
@property
|
||||
def url_kwargs(self):
|
||||
return {'event_id': self.e.pk}
|
||||
|
||||
@property
|
||||
def url_expected(self):
|
||||
return '/event/{}/status'.format(self.e.pk)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.e = Event.objects.create()
|
||||
|
||||
self.cf1 = self.e.commentfields.create(name='Comment Field 1')
|
||||
self.cf2 = self.e.commentfields.create(
|
||||
name='Comment Field 2', fieldtype='char',
|
||||
)
|
||||
|
||||
self.o1 = self.e.options.create(name='Option 1')
|
||||
self.o2 = self.e.options.create(name='Option 2', multi_choices=True)
|
||||
|
||||
self.oc1 = self.o1.choices.create(value='O1 - Choice 1')
|
||||
self.oc2 = self.o1.choices.create(value='O1 - Choice 2')
|
||||
self.oc3 = self.o2.choices.create(value='O2 - Choice 1')
|
||||
self.oc4 = self.o2.choices.create(value='O2 - Choice 2')
|
||||
|
||||
self.er1 = self.e.eventregistration_set.create(user=self.users['user'])
|
||||
self.er1.options.add(self.oc1)
|
||||
self.er2 = self.e.eventregistration_set.create(
|
||||
user=self.users['member'],
|
||||
)
|
||||
|
||||
def _get_oc_filter_name(self, oc):
|
||||
return 'option_{}_choice_{}'.format(oc.event_option.pk, oc.pk)
|
||||
|
||||
def _test_filters(self, filters, expected):
|
||||
r = self.client.post(self.url, {
|
||||
self._get_oc_filter_name(oc): v for oc, v in filters
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertQuerysetEqual(
|
||||
r.context['user_choices'], map(repr, expected),
|
||||
ordered=False,
|
||||
)
|
||||
|
||||
def test_filter_none(self):
|
||||
self._test_filters([(self.oc1, 'none')], [self.er1, self.er2])
|
||||
|
||||
def test_filter_yes(self):
|
||||
self._test_filters([(self.oc1, 'yes')], [self.er1])
|
||||
|
||||
def test_filter_no(self):
|
||||
self._test_filters([(self.oc1, 'no')], [self.er2])
|
||||
|
||||
|
||||
class SurveyViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'survey.details'
|
||||
http_methods = ['GET', 'POST']
|
||||
|
||||
auth_user = 'user'
|
||||
auth_forbidden = [None]
|
||||
|
||||
post_expected_message = Message(messages.SUCCESS, (
|
||||
"Votre réponse a bien été enregistrée ! Vous pouvez cependant la "
|
||||
"modifier jusqu'à la fin du sondage."
|
||||
))
|
||||
|
||||
@property
|
||||
def url_kwargs(self):
|
||||
return {'survey_id': self.s.pk}
|
||||
|
||||
@property
|
||||
def url_expected(self):
|
||||
return '/survey/{}'.format(self.s.pk)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.s = Survey.objects.create(title='Title')
|
||||
|
||||
self.q1 = self.s.questions.create(question='Question 1 ?')
|
||||
self.q2 = self.s.questions.create(
|
||||
question='Question 2 ?',
|
||||
multi_answers=True,
|
||||
)
|
||||
|
||||
self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1')
|
||||
self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2')
|
||||
self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1')
|
||||
self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2')
|
||||
|
||||
def test_get(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def test_post_new(self):
|
||||
r = self.client.post(self.url, {
|
||||
'question_{}'.format(self.q1.pk): [str(self.qa1.pk)],
|
||||
'question_{}'.format(self.q2.pk): [
|
||||
str(self.qa3.pk), str(self.qa4.pk),
|
||||
],
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
|
||||
|
||||
a = self.s.surveyanswer_set.get(user=self.users['user'])
|
||||
self.assertQuerysetEqual(
|
||||
a.answers.all(), map(repr, [self.qa1, self.qa3, self.qa4]),
|
||||
ordered=False,
|
||||
)
|
||||
|
||||
def test_post_edit(self):
|
||||
a = self.s.surveyanswer_set.create(user=self.users['user'])
|
||||
a.answers.add(self.qa1, self.qa1, self.qa4)
|
||||
|
||||
r = self.client.post(self.url, {
|
||||
'question_{}'.format(self.q1.pk): [],
|
||||
'question_{}'.format(self.q2.pk): [str(self.qa3.pk)],
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertIn(self.post_expected_message, get_messages(r.wsgi_request))
|
||||
|
||||
a.refresh_from_db()
|
||||
self.assertQuerysetEqual(
|
||||
a.answers.all(), map(repr, [self.qa3]),
|
||||
ordered=False,
|
||||
)
|
||||
|
||||
def test_post_delete(self):
|
||||
a = self.s.surveyanswer_set.create(user=self.users['user'])
|
||||
a.answers.add(self.qa1, self.qa4)
|
||||
|
||||
r = self.client.post(self.url, {'delete': '1'})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
expected_message = Message(
|
||||
messages.SUCCESS, "Votre réponse a bien été supprimée")
|
||||
self.assertIn(expected_message, get_messages(r.wsgi_request))
|
||||
|
||||
with self.assertRaises(SurveyAnswer.DoesNotExist):
|
||||
a.refresh_from_db()
|
||||
|
||||
def test_forbidden_closed(self):
|
||||
self.s.survey_open = False
|
||||
self.s.save()
|
||||
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertNotEqual(r.status_code, 200)
|
||||
|
||||
def test_forbidden_old(self):
|
||||
self.s.old = True
|
||||
self.s.save()
|
||||
|
||||
r = self.client.get(self.url)
|
||||
|
||||
self.assertNotEqual(r.status_code, 200)
|
||||
|
||||
|
||||
class SurveyStatusViewTests(ViewTestCaseMixin, TestCase):
|
||||
url_name = 'survey.details.status'
|
||||
|
||||
http_methods = ['GET', 'POST']
|
||||
|
||||
auth_user = 'staff'
|
||||
auth_forbidden = [None, 'user', 'member']
|
||||
|
||||
@property
|
||||
def url_kwargs(self):
|
||||
return {'survey_id': self.s.pk}
|
||||
|
||||
@property
|
||||
def url_expected(self):
|
||||
return '/survey/{}/status'.format(self.s.pk)
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
|
||||
self.s = Survey.objects.create(title='Title')
|
||||
|
||||
self.q1 = self.s.questions.create(question='Question 1 ?')
|
||||
self.q2 = self.s.questions.create(
|
||||
question='Question 2 ?',
|
||||
multi_answers=True,
|
||||
)
|
||||
|
||||
self.qa1 = self.q1.answers.create(answer='Q1 - Answer 1')
|
||||
self.qa2 = self.q1.answers.create(answer='Q1 - Answer 2')
|
||||
self.qa3 = self.q2.answers.create(answer='Q2 - Answer 1')
|
||||
self.qa4 = self.q2.answers.create(answer='Q2 - Answer 2')
|
||||
|
||||
self.a1 = self.s.surveyanswer_set.create(user=self.users['user'])
|
||||
self.a1.answers.add(self.qa1)
|
||||
self.a2 = self.s.surveyanswer_set.create(user=self.users['member'])
|
||||
|
||||
def test_get(self):
|
||||
r = self.client.get(self.url)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
|
||||
def _get_qa_filter_name(self, qa):
|
||||
return 'question_{}_answer_{}'.format(qa.survey_question.pk, qa.pk)
|
||||
|
||||
def _test_filters(self, filters, expected):
|
||||
r = self.client.post(self.url, {
|
||||
self._get_qa_filter_name(qa): v for qa, v in filters
|
||||
})
|
||||
|
||||
self.assertEqual(r.status_code, 200)
|
||||
self.assertQuerysetEqual(
|
||||
r.context['user_answers'], map(repr, expected),
|
||||
ordered=False,
|
||||
)
|
||||
|
||||
def test_filter_none(self):
|
||||
self._test_filters([(self.qa1, 'none')], [self.a1, self.a2])
|
||||
|
||||
def test_filter_yes(self):
|
||||
self._test_filters([(self.qa1, 'yes')], [self.a1])
|
||||
|
||||
def test_filter_no(self):
|
||||
self._test_filters([(self.qa1, 'no')], [self.a2])
|
24
gestioncof/tests/testcases.py
Normal file
24
gestioncof/tests/testcases.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin
|
||||
|
||||
from .utils import create_user, create_member, create_staff
|
||||
|
||||
|
||||
class ViewTestCaseMixin(BaseViewTestCaseMixin):
|
||||
"""
|
||||
TestCase extension to ease testing of cof views.
|
||||
|
||||
Most information can be found in the base parent class doc.
|
||||
This class performs some changes to users management, detailed below.
|
||||
|
||||
During setup, three users are created:
|
||||
- 'user': a basic user without any permission,
|
||||
- 'member': (profile.is_cof is True),
|
||||
- 'staff': (profile.is_cof is True) && (profile.is_buro is True).
|
||||
"""
|
||||
|
||||
def get_users_base(self):
|
||||
return {
|
||||
'user': create_user('user'),
|
||||
'member': create_member('member'),
|
||||
'staff': create_staff('staff'),
|
||||
}
|
61
gestioncof/tests/utils.py
Normal file
61
gestioncof/tests/utils.py
Normal file
|
@ -0,0 +1,61 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
def _create_user(username, is_cof=False, is_staff=False, attrs=None):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
|
||||
password = attrs.pop('password', username)
|
||||
|
||||
user_keys = [
|
||||
'first_name', 'last_name', 'email', 'is_staff', 'is_superuser',
|
||||
]
|
||||
user_attrs = {k: v for k, v in attrs.items() if k in user_keys}
|
||||
|
||||
profile_keys = [
|
||||
'is_cof', 'login_clipper', 'phone', 'occupation', 'departement',
|
||||
'type_cotiz', 'mailing_cof', 'mailing_bda', 'mailing_bda_revente',
|
||||
'comments', 'is_buro', 'petit_cours_accept',
|
||||
'petit_cours_remarques',
|
||||
]
|
||||
profile_attrs = {k: v for k, v in attrs.items() if k in profile_keys}
|
||||
|
||||
if is_cof:
|
||||
profile_attrs['is_cof'] = True
|
||||
|
||||
if is_staff:
|
||||
# At the moment, admin is accessible by COF staff.
|
||||
user_attrs['is_staff'] = True
|
||||
profile_attrs['is_buro'] = True
|
||||
|
||||
user = User(username=username, **user_attrs)
|
||||
user.set_password(password)
|
||||
user.save()
|
||||
|
||||
for k, v in profile_attrs.items():
|
||||
setattr(user.profile, k, v)
|
||||
user.profile.save()
|
||||
|
||||
return user
|
||||
|
||||
|
||||
def create_user(username, attrs=None):
|
||||
return _create_user(username, attrs=attrs)
|
||||
|
||||
|
||||
def create_member(username, attrs=None):
|
||||
return _create_user(username, is_cof=True, attrs=attrs)
|
||||
|
||||
|
||||
def create_staff(username, attrs=None):
|
||||
return _create_user(username, is_cof=True, is_staff=True, attrs=attrs)
|
||||
|
||||
|
||||
def create_root(username, attrs=None):
|
||||
if attrs is None:
|
||||
attrs = {}
|
||||
attrs.setdefault('is_staff', True)
|
||||
attrs.setdefault('is_superuser', True)
|
||||
return _create_user(username, attrs=attrs)
|
|
@ -1,17 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.conf.urls import url
|
||||
from gestioncof.petits_cours_views import DemandeListView, DemandeDetailView
|
||||
from gestioncof import views, petits_cours_views
|
||||
from gestioncof.decorators import buro_required
|
||||
|
||||
export_patterns = [
|
||||
url(r'^members$', views.export_members),
|
||||
url(r'^mega/avecremarques$', views.export_mega_remarksonly),
|
||||
url(r'^mega/participants$', views.export_mega_participants),
|
||||
url(r'^mega/orgas$', views.export_mega_orgas),
|
||||
url(r'^members$', views.export_members,
|
||||
name='cof.membres_export'),
|
||||
url(r'^mega/avecremarques$', views.export_mega_remarksonly,
|
||||
name='cof.mega_export_remarks'),
|
||||
url(r'^mega/participants$', views.export_mega_participants,
|
||||
name='cof.mega_export_participants'),
|
||||
url(r'^mega/orgas$', views.export_mega_orgas,
|
||||
name='cof.mega_export_orgas'),
|
||||
# url(r'^mega/(?P<type>.+)$', views.export_mega_bytype),
|
||||
url(r'^mega$', views.export_mega),
|
||||
url(r'^mega$', views.export_mega,
|
||||
name='cof.mega_export'),
|
||||
]
|
||||
|
||||
petitcours_patterns = [
|
||||
|
@ -52,7 +55,8 @@ events_patterns = [
|
|||
calendar_patterns = [
|
||||
url(r'^subscription$', views.calendar,
|
||||
name='calendar'),
|
||||
url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', views.calendar_ics)
|
||||
url(r'^(?P<token>[a-z0-9-]+)/calendar.ics$', views.calendar_ics,
|
||||
name='calendar.ics'),
|
||||
]
|
||||
|
||||
clubs_patterns = [
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden
|
|||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.views import (
|
||||
login as django_login_view, logout as django_logout_view,
|
||||
redirect_to_login,
|
||||
)
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.sites.models import Site
|
||||
|
@ -338,7 +339,7 @@ def profile(request):
|
|||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request,
|
||||
"Votre profil a été mis à jour avec succès !")
|
||||
"Votre profil a été mis à jour avec succès !")
|
||||
else:
|
||||
form = UserProfileForm(instance=request.user.profile)
|
||||
return render(request, "gestioncof/profile.html", {"form": form})
|
||||
|
@ -566,7 +567,7 @@ def liste_clubs(request):
|
|||
if request.user.profile.is_buro:
|
||||
data = {'owned_clubs': clubs.all()}
|
||||
else:
|
||||
data = {'owned_clubs': request.user.clubs_geres,
|
||||
data = {'owned_clubs': request.user.clubs_geres.all(),
|
||||
'other_clubs': clubs.exclude(respos=request.user)}
|
||||
return render(request, 'liste_clubs.html', data)
|
||||
|
||||
|
@ -782,7 +783,7 @@ class ConfigUpdate(FormView):
|
|||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user is None or not request.user.is_superuser:
|
||||
raise Http404
|
||||
return redirect_to_login(request.get_full_path())
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def form_valid(self, form):
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.forms.widgets import Widget
|
||||
from django.forms.utils import flatatt
|
||||
from django.utils.safestring import mark_safe
|
||||
|
@ -11,7 +5,7 @@ from django.utils.safestring import mark_safe
|
|||
|
||||
class TriStateCheckbox(Widget):
|
||||
def __init__(self, attrs=None, choices=()):
|
||||
super(TriStateCheckbox, self).__init__(attrs)
|
||||
super().__init__(attrs)
|
||||
# choices can be any iterable, but we may need to render this widget
|
||||
# multiple times. Thus, collapse it into a list so it can be consumed
|
||||
# more than once.
|
||||
|
@ -20,6 +14,7 @@ class TriStateCheckbox(Widget):
|
|||
def render(self, name, value, attrs=None, choices=()):
|
||||
if value is None:
|
||||
value = 'none'
|
||||
final_attrs = self.build_attrs(attrs, value=value)
|
||||
attrs['value'] = value
|
||||
final_attrs = self.build_attrs(self.attrs, attrs)
|
||||
output = ["<span class=\"tristate\"%s></span>" % flatatt(final_attrs)]
|
||||
return mark_safe('\n'.join(output))
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (absolute_import, division,
|
||||
print_function, unicode_literals)
|
||||
from builtins import *
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class KFetConfig(AppConfig):
|
||||
name = 'kfet'
|
||||
verbose_name = "Application K-Fêt"
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth import get_user_model
|
||||
from kfet.models import Account, GenericTeamToken
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from django.contrib.auth import get_user_model
|
||||
|
||||
from .backends import AccountBackend
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
from django.core import signing
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ldap3 import Connection
|
||||
from django.shortcuts import render
|
||||
from django.http import Http404
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from kfet.config import kfet_config
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
|
@ -44,7 +42,7 @@ class AccountForm(forms.ModelForm):
|
|||
|
||||
# Surcharge pour passer data à Account.save()
|
||||
def save(self, data = {}, *args, **kwargs):
|
||||
obj = super(AccountForm, self).save(commit = False, *args, **kwargs)
|
||||
obj = super().save(commit = False, *args, **kwargs)
|
||||
obj.save(data = data)
|
||||
return obj
|
||||
|
||||
|
@ -77,14 +75,19 @@ class AccountRestrictForm(AccountForm):
|
|||
class Meta(AccountForm.Meta):
|
||||
fields = ['is_frozen']
|
||||
|
||||
|
||||
class AccountPwdForm(forms.Form):
|
||||
pwd1 = forms.CharField(
|
||||
label="Mot de passe K-Fêt",
|
||||
help_text="Le mot de passe doit contenir au moins huit caractères",
|
||||
widget=forms.PasswordInput)
|
||||
label="Mot de passe K-Fêt",
|
||||
required=False,
|
||||
help_text="Le mot de passe doit contenir au moins huit caractères",
|
||||
widget=forms.PasswordInput,
|
||||
)
|
||||
pwd2 = forms.CharField(
|
||||
label="Confirmer le mot de passe",
|
||||
widget=forms.PasswordInput)
|
||||
label="Confirmer le mot de passe",
|
||||
required=False,
|
||||
widget=forms.PasswordInput,
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
pwd1 = self.cleaned_data.get('pwd1', '')
|
||||
|
@ -93,7 +96,8 @@ class AccountPwdForm(forms.Form):
|
|||
raise ValidationError("Mot de passe trop court")
|
||||
if pwd1 != pwd2:
|
||||
raise ValidationError("Les mots de passes sont différents")
|
||||
super(AccountPwdForm, self).clean()
|
||||
super().clean()
|
||||
|
||||
|
||||
class CofForm(forms.ModelForm):
|
||||
def clean_is_cof(self):
|
||||
|
@ -197,7 +201,7 @@ class CheckoutStatementCreateForm(forms.ModelForm):
|
|||
or self.cleaned_data['balance_200'] is None
|
||||
or self.cleaned_data['balance_500'] is None):
|
||||
raise ValidationError("Y'a un problème. Si tu comptes la caisse, mets au moins des 0 stp (et t'as pas idée de comment c'est long de vérifier que t'as mis des valeurs de partout...)")
|
||||
super(CheckoutStatementCreateForm, self).clean()
|
||||
super().clean()
|
||||
|
||||
class CheckoutStatementUpdateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
@ -238,7 +242,7 @@ class ArticleForm(forms.ModelForm):
|
|||
required = False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ArticleForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.instance.pk:
|
||||
self.initial['suppliers'] = self.instance.suppliers.values_list('pk', flat=True)
|
||||
|
||||
|
@ -252,7 +256,7 @@ class ArticleForm(forms.ModelForm):
|
|||
category, _ = ArticleCategory.objects.get_or_create(name=category_new)
|
||||
self.cleaned_data['category'] = category
|
||||
|
||||
super(ArticleForm, self).clean()
|
||||
super().clean()
|
||||
|
||||
class Meta:
|
||||
model = Article
|
||||
|
@ -296,17 +300,17 @@ class KPsulAccountForm(forms.ModelForm):
|
|||
|
||||
class KPsulCheckoutForm(forms.Form):
|
||||
checkout = forms.ModelChoiceField(
|
||||
queryset=(
|
||||
Checkout.objects
|
||||
.filter(
|
||||
is_protected=False,
|
||||
valid_from__lte=timezone.now(),
|
||||
valid_to__gte=timezone.now(),
|
||||
)
|
||||
),
|
||||
queryset=None,
|
||||
widget=forms.Select(attrs={'id': 'id_checkout_select'}),
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Create the queryset on form instanciation to use the current time.
|
||||
self.fields['checkout'].queryset = (
|
||||
Checkout.objects.is_valid().filter(is_protected=False))
|
||||
|
||||
|
||||
class KPsulOperationForm(forms.ModelForm):
|
||||
article = forms.ModelChoiceField(
|
||||
|
@ -323,7 +327,7 @@ class KPsulOperationForm(forms.ModelForm):
|
|||
}
|
||||
|
||||
def clean(self):
|
||||
super(KPsulOperationForm, self).clean()
|
||||
super().clean()
|
||||
type_ope = self.cleaned_data.get('type')
|
||||
amount = self.cleaned_data.get('amount')
|
||||
article = self.cleaned_data.get('article')
|
||||
|
@ -366,7 +370,7 @@ class AddcostForm(forms.Form):
|
|||
raise ValidationError('Compte invalide')
|
||||
else:
|
||||
self.cleaned_data['amount'] = 0
|
||||
super(AddcostForm, self).clean()
|
||||
super().clean()
|
||||
|
||||
|
||||
# -----
|
||||
|
@ -464,7 +468,7 @@ class InventoryArticleForm(forms.Form):
|
|||
stock_new = forms.IntegerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(InventoryArticleForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'initial' in kwargs:
|
||||
self.name = kwargs['initial']['name']
|
||||
self.stock_old = kwargs['initial']['stock_old']
|
||||
|
@ -486,7 +490,7 @@ class OrderArticleForm(forms.Form):
|
|||
quantity_ordered = forms.IntegerField(required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OrderArticleForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'initial' in kwargs:
|
||||
self.name = kwargs['initial']['name']
|
||||
self.stock = kwargs['initial']['stock']
|
||||
|
@ -516,7 +520,7 @@ class OrderArticleToInventoryForm(forms.Form):
|
|||
quantity_received = forms.IntegerField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(OrderArticleToInventoryForm, self).__init__(*args, **kwargs)
|
||||
super().__init__(*args, **kwargs)
|
||||
if 'initial' in kwargs:
|
||||
self.name = kwargs['initial']['name']
|
||||
self.category = kwargs['initial']['category']
|
||||
|
|
20
kfet/migrations/0063_promo.py
Normal file
20
kfet/migrations/0063_promo.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.12 on 2018-04-05 21:47
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('kfet', '0062_delete_globalpermissions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='account',
|
||||
name='promo',
|
||||
field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017), (2018, 2018)], default=2017, null=True),
|
||||
),
|
||||
]
|
|
@ -1,4 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from functools import reduce
|
||||
|
||||
from django.db import models
|
||||
from django.core.validators import RegexValidator
|
||||
|
@ -7,7 +7,6 @@ from gestioncof.models import CofProfile
|
|||
from django.urls import reverse
|
||||
from django.utils.six.moves import reduce
|
||||
from django.utils import timezone
|
||||
from django.utils.encoding import python_2_unicode_compatible
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from django.db import transaction
|
||||
from django.db.models import F
|
||||
|
@ -256,7 +255,7 @@ class Account(models.Model):
|
|||
cof.save()
|
||||
if data:
|
||||
self.cofprofile = cof
|
||||
super(Account, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def change_pwd(self, clear_password):
|
||||
from .auth.utils import hash_password
|
||||
|
@ -341,6 +340,13 @@ class AccountNegative(models.Model):
|
|||
return self.start + kfet_config.overdraft_duration
|
||||
|
||||
|
||||
class CheckoutQuerySet(models.QuerySet):
|
||||
|
||||
def is_valid(self):
|
||||
now = timezone.now()
|
||||
return self.filter(valid_from__lte=now, valid_to__gte=now)
|
||||
|
||||
|
||||
class Checkout(models.Model):
|
||||
created_by = models.ForeignKey(
|
||||
Account, on_delete = models.PROTECT,
|
||||
|
@ -353,6 +359,8 @@ class Checkout(models.Model):
|
|||
default = 0)
|
||||
is_protected = models.BooleanField(default = False)
|
||||
|
||||
objects = CheckoutQuerySet.as_manager()
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('kfet.checkout.read', kwargs={'pk': self.pk})
|
||||
|
||||
|
@ -362,6 +370,22 @@ class Checkout(models.Model):
|
|||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
created = self.pk is None
|
||||
|
||||
ret = super().save(*args, **kwargs)
|
||||
|
||||
if created:
|
||||
self.statements.create(
|
||||
amount_taken=0,
|
||||
balance_old=self.balance,
|
||||
balance_new=self.balance,
|
||||
by=self.created_by,
|
||||
)
|
||||
|
||||
return ret
|
||||
|
||||
|
||||
class CheckoutTransfer(models.Model):
|
||||
from_checkout = models.ForeignKey(
|
||||
Checkout, on_delete = models.PROTECT,
|
||||
|
@ -372,7 +396,7 @@ class CheckoutTransfer(models.Model):
|
|||
amount = models.DecimalField(
|
||||
max_digits = 6, decimal_places = 2)
|
||||
|
||||
@python_2_unicode_compatible
|
||||
|
||||
class CheckoutStatement(models.Model):
|
||||
by = models.ForeignKey(
|
||||
Account, on_delete = models.PROTECT,
|
||||
|
@ -424,7 +448,7 @@ class CheckoutStatement(models.Model):
|
|||
self.balance_new + self.amount_taken - self.balance_old)
|
||||
with transaction.atomic():
|
||||
Checkout.objects.filter(pk=checkout_id).update(balance=self.balance_new)
|
||||
super(CheckoutStatement, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
else:
|
||||
self.amount_error = (
|
||||
self.balance_new + self.amount_taken - self.balance_old)
|
||||
|
@ -438,10 +462,9 @@ class CheckoutStatement(models.Model):
|
|||
and last_statement.balance_new != self.balance_new):
|
||||
Checkout.objects.filter(pk=self.checkout_id).update(
|
||||
balance=F('balance') - last_statement.balance_new + self.balance_new)
|
||||
super(CheckoutStatement, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class ArticleCategory(models.Model):
|
||||
name = models.CharField("nom", max_length=45)
|
||||
has_addcost = models.BooleanField("majorée", default=True,
|
||||
|
@ -454,7 +477,6 @@ class ArticleCategory(models.Model):
|
|||
return self.name
|
||||
|
||||
|
||||
@python_2_unicode_compatible
|
||||
class Article(models.Model):
|
||||
name = models.CharField("nom", max_length = 45)
|
||||
is_sold = models.BooleanField("en vente", default = True)
|
||||
|
@ -541,7 +563,7 @@ class InventoryArticle(models.Model):
|
|||
# d'erreur
|
||||
if not self.inventory.order:
|
||||
self.stock_error = self.stock_new - self.stock_old
|
||||
super(InventoryArticle, self).save(*args, **kwargs)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Supplier(models.Model):
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from channels.routing import include, route_class
|
||||
|
||||
from . import consumers
|
||||
|
|
|
@ -75,6 +75,10 @@ ul {
|
|||
padding:8px !important;
|
||||
}
|
||||
|
||||
.table thead .sm-padding {
|
||||
padding:3px !important;
|
||||
}
|
||||
|
||||
.table tr.section {
|
||||
background: #c63b52 !important;
|
||||
color:#fff;
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
/* Libs customizations */
|
||||
@import url("libs/jconfirm-kfet.css");
|
||||
@import url("libs/jquery-tablesorter-kfet.css");
|
||||
@import url("libs/multiple-select-kfet.css");
|
||||
|
||||
/* Base */
|
||||
|
@ -54,6 +55,11 @@
|
|||
color: #C81022;
|
||||
}
|
||||
|
||||
.table thead .glyphicon {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Pages tableaux seuls
|
||||
|
@ -82,6 +88,11 @@
|
|||
border-radius: 0;
|
||||
}
|
||||
|
||||
.table td.small-width {
|
||||
/* Header still extends the width of the column, but it will be minimal. */
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
padding: 15px 0;
|
||||
background: #d86c7e;
|
||||
|
|
0
kfet/static/kfet/css/libs/jquery-tablesorter-kfet.css
Normal file
0
kfet/static/kfet/css/libs/jquery-tablesorter-kfet.css
Normal file
|
@ -464,3 +464,77 @@ function submit_url(el) {
|
|||
let url = $(el).data('url');
|
||||
create_form(url).appendTo($('body')).submit();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* jquery-tablesorter
|
||||
* https://mottie.github.io/tablesorter/docs/
|
||||
*
|
||||
* Known bugs (v2.29.0):
|
||||
* - Sort order icons in sticky headers are not updated.
|
||||
* Status: Fixed in next release.
|
||||
*
|
||||
* TODO:
|
||||
* - Handle i18n.
|
||||
*/
|
||||
|
||||
|
||||
function registerBoolParser(id, true_str, false_str) {
|
||||
$.tablesorter.addParser({
|
||||
id: id,
|
||||
format: function(s) {
|
||||
return s.toLowerCase()
|
||||
.replace(true_str, 1)
|
||||
.replace(false_str, 0);
|
||||
},
|
||||
type: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Parsers for the text representations of boolean.
|
||||
registerBoolParser('yesno', 'oui', 'non');
|
||||
|
||||
registerBoolParser('article__is_sold', 'en vente', 'non vendu');
|
||||
registerBoolParser('article__hidden', 'caché', 'affiché');
|
||||
|
||||
|
||||
// https://mottie.github.io/tablesorter/docs/index.html#variable-defaults
|
||||
$.extend(true, $.tablesorter.defaults, {
|
||||
headerTemplate: '{content} {icon}',
|
||||
|
||||
cssIconAsc : 'glyphicon glyphicon-chevron-up',
|
||||
cssIconDesc : 'glyphicon glyphicon-chevron-down',
|
||||
cssIconNone : 'glyphicon glyphicon-resize-vertical',
|
||||
|
||||
// Only four-digits format year is handled by the builtin parser
|
||||
// 'shortDate'.
|
||||
dateFormat: 'ddmmyyyy',
|
||||
|
||||
// Accented characters are replaced with their non-accented one.
|
||||
sortLocaleCompare: true,
|
||||
// French format: 1 234,56
|
||||
usNumberFormat: false,
|
||||
|
||||
widgets: ['stickyHeaders'],
|
||||
widgetOptions: {
|
||||
stickyHeaders_offset: '.navbar',
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// https://mottie.github.io/tablesorter/docs/index.html#variable-language
|
||||
$.extend($.tablesorter.language, {
|
||||
sortAsc : 'Trié par ordre croissant, ',
|
||||
sortDesc : 'Trié par ordre décroissant, ',
|
||||
sortNone : 'Non trié, ',
|
||||
sortDisabled : 'tri désactivé et/ou non-modifiable',
|
||||
nextAsc : 'cliquer pour trier par ordre croissant',
|
||||
nextDesc : 'cliquer pour trier par ordre décroissant',
|
||||
nextNone : 'cliquer pour retirer le tri'
|
||||
});
|
||||
|
||||
|
||||
$( function() {
|
||||
$('.sortable').tablesorter();
|
||||
});
|
||||
|
|
6014
kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js
vendored
Normal file
6014
kfet/static/kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from datetime import date, datetime, time, timedelta
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
|
|
|
@ -37,13 +37,16 @@
|
|||
|
||||
<section>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(trigramme,asc)] #}
|
||||
data-sortlist="[[0,0]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center">Tri.</td>
|
||||
<td>Nom</td>
|
||||
<td class="text-right">Balance</td>
|
||||
<td class="text-center">COF</td>
|
||||
<td class="text-center" data-sorter="yesno">COF</td>
|
||||
<td>Dpt</td>
|
||||
<td class="text-center">Promo</td>
|
||||
</tr>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block header-title %}Création d'un compte{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
|
||||
<script src="{% static "vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js" %}" type="text/javascript"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% block header-title %}Création d'un compte{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
|
||||
<script src="{% static "vendor/jquery.autocomplete-light/3.5.0/dist/jquery.autocomplete-light.min.js" %}" type="text/javascript"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block main-class %}content-form{% endblock %}
|
||||
|
|
|
@ -35,16 +35,19 @@
|
|||
{% block main %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(trigramme,asc)] #}
|
||||
data-sortlist="[[0,0]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="text-center">Tri.</td>
|
||||
<td>Nom</td>
|
||||
<td class="text-right">Balance</td>
|
||||
<td class="text-right">Réelle</td>
|
||||
<td>Début</td>
|
||||
<td data-sorter="shortDate">Début</td>
|
||||
<td>Découvert autorisé</td>
|
||||
<td>Jusqu'au</td>
|
||||
<td data-sorter="shortDate">Jusqu'au</td>
|
||||
<td>Balance offset</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -63,9 +66,13 @@
|
|||
{{ neg.account.real_balance|floatformat:2 }}€
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ neg.start|date:'d/m/Y H:i:s'}}</td>
|
||||
<td title="{{ neg.start }}">
|
||||
{{ neg.start|date:'d/m/Y H:i'}}
|
||||
</td>
|
||||
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td>
|
||||
<td>{{ neg.authz_overdrafy_until|default_if_none:'' }}</td>
|
||||
<td title="{{ neg.authz_overdraft_until }}">
|
||||
{{ neg.authz_overdraft_until|date:'d/m/Y H:i' }}
|
||||
</td>
|
||||
<td>{{ neg.balance_offset|default_if_none:'' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -7,8 +7,11 @@
|
|||
|
||||
<aside>
|
||||
<div class="heading">
|
||||
{{ articles|length }}
|
||||
<span class="sub">article{{ articles|length|pluralize }}</span>
|
||||
{{ nb_articles }}
|
||||
<span class="sub">article{{ nb_articles|pluralize }}</span>
|
||||
</div>
|
||||
<div class="heading">
|
||||
<span class="sub">dont {{ articles|length }} en vente</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
|
@ -25,39 +28,99 @@
|
|||
{% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<h2>Article{{ articles|length|pluralize}} en vente</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(is_sold,desc), (name,asc)] #}
|
||||
data-sortlist="[[3,1], [0,0]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Nom</td>
|
||||
<td class="text-right">Prix</td>
|
||||
<td class="text-right">Stock</td>
|
||||
<td class="text-right">En vente</td>
|
||||
<td class="text-right">Affiché</td>
|
||||
<td class="text-right">Dernier inventaire</td>
|
||||
<td class="text-right" data-sorter="article__is_sold">En vente</td>
|
||||
<td class="text-right" data-sorter="article__hidden">Affiché</td>
|
||||
<td class="text-right" data-sorter="shortDate">Dernier inventaire</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for article in articles %}
|
||||
{% ifchanged article.category %}
|
||||
<tr class="section">
|
||||
<td colspan="6">{{ article.category.name }}</td>
|
||||
</tr>
|
||||
{% endifchanged %}
|
||||
{% regroup articles by category as category_list %}
|
||||
|
||||
{% for category in category_list %}
|
||||
<tbody class="tablesorter-no-sort">
|
||||
<tr class="section">
|
||||
<td colspan="6">{{ category.grouper }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
{% for article in category.list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'kfet.article.read' article.pk %}">
|
||||
{{ article.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">{{ article.price }}€</td>
|
||||
<td class="text-right">{{ article.stock }}</td>
|
||||
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
|
||||
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
|
||||
{% with last_inventory=article.inventory.0 %}
|
||||
<td class="text-right" title="{{ last_inventory.at }}">
|
||||
{{ last_inventory.at|date:'d/m/Y H:i' }}
|
||||
</td>
|
||||
{% endwith %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Article{{ not_sold_articles|length|pluralize }} non vendu{{ nots_sold_article|length|pluralize }}</h2>
|
||||
<div class="table-responsive">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(is_sold,desc), (name,asc)] #}
|
||||
data-sortlist="[[3,1], [0,0]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'kfet.article.read' article.pk %}">
|
||||
{{ article.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">{{ article.price }}€</td>
|
||||
<td class="text-right">{{ article.stock }}</td>
|
||||
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
|
||||
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
|
||||
<td class="text-right">{{ article.inventory.0.at }}</td>
|
||||
<td>Nom</td>
|
||||
<td class="text-right">Prix</td>
|
||||
<td class="text-right">Stock</td>
|
||||
<td class="text-right" data-sorter="article__is_sold">En vente</td>
|
||||
<td class="text-right" data-sorter="article__hidden">Affiché</td>
|
||||
<td class="text-right" data-sorter="shortDate">Dernier inventaire</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</thead>
|
||||
{% regroup not_sold_articles by category as not_sold_category_list %}
|
||||
|
||||
{% for category in not_sold_category_list %}
|
||||
<tbody class="tablesorter-no-sort">
|
||||
<tr class="section">
|
||||
<td colspan="6">{{ category.grouper }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody>
|
||||
{% for article in category.list %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'kfet.article.read' article.pk %}">
|
||||
{{ article.name }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-right">{{ article.price }}€</td>
|
||||
<td class="text-right">{{ article.stock }}</td>
|
||||
<td class="text-right">{{ article.is_sold | yesno:"En vente,Non vendu"}}</td>
|
||||
<td class="text-right">{{ article.hidden | yesno:"Caché,Affiché" }}</td>
|
||||
{% with last_inventory=article.inventory.0 %}
|
||||
<td class="text-right" title="{{ last_inventory.at }}">
|
||||
{{ last_inventory.at|date:'d/m/Y H:i' }}
|
||||
</td>
|
||||
{% endwith %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(inventory.at,desc)] #}
|
||||
data-sortlist="[[0,1]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td data-sorter="shortDate">Date</td>
|
||||
<td>Stock</td>
|
||||
<td>Erreur</td>
|
||||
</tr>
|
||||
|
@ -9,9 +12,9 @@
|
|||
<tbody>
|
||||
{% for inventoryart in inventoryarts %}
|
||||
<tr>
|
||||
<td>
|
||||
<td title="{{ inventoryart.inventory.at }}">
|
||||
<a href="{% url "kfet.inventory.read" inventoryart.inventory.pk %}">
|
||||
{{ inventoryart.inventory.at }}
|
||||
{{ inventoryart.inventory.at|date:'d/m/Y H:i' }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ inventoryart.stock_new }}</td>
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(at,desc)] #}
|
||||
data-sortlist="[[0,1]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td data-sorter="shortDate">Date</td>
|
||||
<td>Fournisseur</td>
|
||||
<td>HT</td>
|
||||
<td>TVA</td>
|
||||
|
@ -11,7 +14,9 @@
|
|||
<tbody>
|
||||
{% for supplierart in supplierarts %}
|
||||
<tr>
|
||||
<td>{{ supplierart.at }}</td>
|
||||
<td title="{{ supplierart.at }}">
|
||||
{{ supplierart.at|date:'d/m/Y' }}
|
||||
</td>
|
||||
<td>{{ supplierart.supplier.name }}</td>
|
||||
<td>{{ supplierart.price_HT|default_if_none:"" }}</td>
|
||||
<td>{{ supplierart.TVA|default_if_none:"" }}</td>
|
||||
|
|
|
@ -24,6 +24,7 @@
|
|||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/vendor/jquery-tablesorter/jquery.tablesorter.combined.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
|
||||
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% load i18n static %}
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
{% slugurl "kfet" as kfet_home_url %}
|
||||
{% slugurl "k-fet" as kfet_home_url %}
|
||||
|
||||
<nav class="navbar navbar-fixed-top">
|
||||
<div class="container-fluid">
|
||||
|
|
|
@ -17,12 +17,15 @@
|
|||
{% block main %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(name,asc)] #}
|
||||
data-sortlist="[[0,0]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Nom</td>
|
||||
<td class="text-right">Nombre d'articles</td>
|
||||
<td class="text-right">Peut être majorée</td>
|
||||
<td class="text-right" data-sorter="yesno">Peut être majorée</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
|
@ -24,13 +24,16 @@
|
|||
{% block main %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(valid_to,desc)] #}
|
||||
data-sortlist="[[3,1]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Nom</td>
|
||||
<td class="text-right">Balance</td>
|
||||
<td class="text-right">Déb. valid.</td>
|
||||
<td class="text-right">Fin valid.</td>
|
||||
<td class="text-right" data-parser="shortDate">Déb. valid.</td>
|
||||
<td class="text-right" data-parser="shortDate">Fin valid.</td>
|
||||
<td class="text-right">Protégée</td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -43,8 +46,12 @@
|
|||
</a>
|
||||
</td>
|
||||
<td class="text-right">{{ checkout.balance}}€</td>
|
||||
<td class="text-right">{{ checkout.valid_from }}</td>
|
||||
<td class="text-right">{{ checkout.valid_to }}</td>
|
||||
<td class="text-right" title="{{ checkout.valid_from }}">
|
||||
{{ checkout.valid_from|date:'d/m/Y H:i' }}
|
||||
</td>
|
||||
<td class="text-right" title="{{ checkout.valid_to }}">
|
||||
{{ checkout.valid_to|date:'d/m/Y H:i' }}
|
||||
</td>
|
||||
<td class="text-right">{{ checkout.is_protected|yesno }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
|
@ -14,10 +14,13 @@
|
|||
{% if not statements %}
|
||||
Pas de relevé
|
||||
{% else %}
|
||||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(at,desc)] #}
|
||||
data-sortlist="[[0,1]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date/heure</td>
|
||||
<td data-sorter="shortDate">Date/heure</td>
|
||||
<td>Montant pris</td>
|
||||
<td>Montant laissé</td>
|
||||
<td>Erreur</td>
|
||||
|
@ -25,9 +28,9 @@
|
|||
<tbody>
|
||||
{% for statement in statements %}
|
||||
<tr>
|
||||
<td>
|
||||
<td title="{{ statement.at }}">
|
||||
<a href="{% url 'kfet.checkoutstatement.update' checkout.pk statement.pk %}">
|
||||
{{ statement.at }}
|
||||
{{ statement.at|date:'d/m/Y H:i' }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ statement.amount_taken }}</td>
|
||||
|
|
|
@ -17,10 +17,13 @@
|
|||
{% block main %}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-condensed">
|
||||
<table
|
||||
class="table table-hover table-condensed sortable"
|
||||
{# Initial sort: [(at,desc)] #}
|
||||
data-sortlist="[[0,1]]">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td data-sorter="shortDate">Date</td>
|
||||
<td>Par</td>
|
||||
<td>Nb articles</td>
|
||||
</tr>
|
||||
|
@ -28,9 +31,9 @@
|
|||
<tbody>
|
||||
{% for inventory in inventories %}
|
||||
<tr>
|
||||
<td>
|
||||
<td title="{{ inventory.at }}">
|
||||
<a href="{% url 'kfet.inventory.read' inventory.pk %}">
|
||||
<span>{{ inventory.at }}</span>
|
||||
{{ inventory.at|date:'d/m/Y H:i' }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ inventory.by }}</td>
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue