Merge branch 'master' into aureplop/cof-tests_registration

This commit is contained in:
Aurélien Delobelle 2018-09-30 12:57:35 +02:00
commit 10f4bd02d5
98 changed files with 7440 additions and 717 deletions

View file

@ -1,3 +1,5 @@
image: "python:3.5"
services:
- postgres:latest
- redis:latest
@ -34,6 +36,7 @@ before_script:
# Remove the old test database if it has not been done yet
- psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB"
- pip install --upgrade --cache-dir vendor/pip -t vendor/python -r requirements.txt
- python --version
test:
stage: test

135
README.md
View file

@ -4,16 +4,72 @@
## 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
@ -83,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 :
@ -143,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
View file

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

View file

@ -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." % (

View file

@ -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

View file

@ -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')
)

View file

@ -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")

View file

@ -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

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

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

View 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 = [
]

View file

@ -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

View file

@ -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 %}

View 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 %}

View file

@ -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>

View 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 %}

View 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 %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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
View 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))

View file

@ -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"

View file

@ -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 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

View file

@ -1,9 +1,5 @@
# -*- encoding: utf-8 -*-
"""
Formats français.
"""
from __future__ import unicode_literals
DATETIME_FORMAT = r'l j F Y \à H:i'

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Django common settings for cof project.
@ -43,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")
@ -104,9 +100,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',
@ -206,8 +204,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 = {

View file

@ -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")

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
"""
Fichier principal de configuration des urls du projet GestioCOF
"""
@ -71,7 +69,7 @@ urlpatterns = [
name="empty-registration"),
# Autocompletion
url(r'^autocomplete/registration$', autocomplete,
name='cof.registration.autocomplete'),
name="cof.registration.autocomplete"),
url(r'^user/autocomplete$', gestioncof_views.user_autocomplete,
name='cof-user-autocomplete'),
# Interface admin
@ -86,10 +84,12 @@ 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_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),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente,
name="ml_bda_revente"),
url(r'^k-fet/', include('kfet.urls')),
url(r'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),

View file

@ -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():

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from ldap3 import Connection
from django import shortcuts

View file

@ -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

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.decorators import user_passes_test

View file

@ -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)
@ -170,30 +170,27 @@ class EventStatusFilterForm(forms.Form):
yield ("has_paid", None, value)
class UserProfileForm(forms.ModelForm):
first_name = forms.CharField(label=_('Prénom'), max_length=30)
last_name = forms.CharField(label=_('Nom'), max_length=30)
class UserForm(forms.ModelForm):
class Meta:
model = User
fields = ["first_name", "last_name", "email"]
def __init__(self, *args, **kw):
super(UserProfileForm, self).__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)
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()
class ProfileForm(forms.ModelForm):
class Meta:
model = CofProfile
fields = ["first_name", "last_name", "phone", "mailing_cof",
"mailing_bda", "mailing_bda_revente"]
fields = [
"phone",
"mailing_cof",
"mailing_bda",
"mailing_bda_revente",
"mailing_unernestaparis"
]
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 +216,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,10 +225,11 @@ 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
self.fields['mailing_unernestaparis'].initial = True
self.fields.keyOrder = [
'login_clipper',
@ -244,14 +241,17 @@ class RegistrationProfileForm(forms.ModelForm):
'mailing_cof',
'mailing_bda',
'mailing_bda_revente',
"mailing_unernestaparis",
'comments'
]
]
class Meta:
model = CofProfile
fields = ("login_clipper", "phone", "occupation",
"departement", "is_cof", "type_cotiz", "mailing_cof",
"mailing_bda", "mailing_bda_revente", "comments")
"mailing_bda", "mailing_bda_revente",
"mailing_unernestaparis", "comments")
STATUS_CHOICES = (('no', 'Non'),
('wait', 'Oui mais attente paiement'),
@ -274,7 +274,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,14 +337,15 @@ 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)

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
Import des mails de GestioCOF dans la base de donnée
"""

View file

@ -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",

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-02 21:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0013_pei'),
]
operations = [
migrations.AddField(
model_name='cofprofile',
name='mailing_unernestaparis',
field=models.BooleanField(default=False, verbose_name='Recevoir les mails unErnestAParis'),
),
]

View file

@ -72,6 +72,7 @@ class CofProfile(models.Model):
TYPE_COTIZ_CHOICES))
mailing_cof = models.BooleanField("Recevoir les mails COF", default=False)
mailing_bda = models.BooleanField("Recevoir les mails BdA", default=False)
mailing_unernestaparis = models.BooleanField("Recevoir les mails unErnestAParis", default=False)
mailing_bda_revente = models.BooleanField(
"Recevoir les mails de revente de places BdA", default=False)
comments = models.TextField(

View file

@ -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:

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from functools import reduce
from django.db import models

View file

@ -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

View file

@ -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;
}

View file

@ -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 }}

View file

@ -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 }}

View file

@ -4,26 +4,23 @@
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>Modifier mon profil</h2>
<form id="profile form-horizontal" method="post" action="{% url 'profile' %}">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
<fieldset"center-block">
{% for field in form %}
{{ field | bootstrap }}
{% endfor %}
</fieldset>
</div>
{% if user.profile.comments %}
<div class="row" style="margin: 0 15%;">
<h4>Commentaires</h4>
<p>
{{ user.profile.comments }}
</p>
</div>
{% endif %}
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right" value="Enregistrer" />
</div>
</form>
<h2>Modifier mon profil</h2>
<form id="profile form-horizontal" method="post" action="">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
{{ user_form | bootstrap }}
{{ profile_form | bootstrap }}
</div>
{% if user.profile.comments %}
<div class="row" style="margin: 0 15%;">
<h4>Commentaires</h4>
<p>{{ user.profile.comments }}</p>
</div>
{% endif %}
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right" value="Enregistrer" />
</div>
</form>
{% endblock %}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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

View 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

View file

@ -371,9 +371,9 @@ class ProfileViewTests(ViewTestCaseMixin, TestCase):
u = self.users['member']
r = self.client.post(self.url, {
'first_name': 'First',
'last_name': 'Last',
'phone': '',
'u-first_name': 'First',
'u-last_name': 'Last',
'p-phone': '',
# 'mailing_cof': '1',
# 'mailing_bda': '1',
# 'mailing_bda_revente': '1',
@ -516,14 +516,14 @@ class MegaHelpers:
u2 = create_user('u2')
u2.profile.save()
m = Event.objects.create(title='MEGA 2017')
m = Event.objects.create(title='MEGA 2018')
cf1 = m.commentfields.create(name='Commentaire')
cf1 = m.commentfields.create(name='Commentaires')
cf2 = m.commentfields.create(
name='Comment Field 2', fieldtype='char',
)
option_type = m.options.create(name='Conscrit/Orga ?')
option_type = m.options.create(name='Orga ? Conscrit ?')
choice_orga = option_type.choices.create(value='Orga')
choice_conscrit = option_type.choices.create(value='Conscrit')

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.conf.urls import url
from gestioncof.petits_cours_views import DemandeListView, DemandeDetailView
from gestioncof import views, petits_cours_views

View file

@ -32,7 +32,8 @@ from gestioncof.models import EventCommentField, EventCommentValue, \
from gestioncof.models import CofProfile, Club
from gestioncof.decorators import buro_required, cof_required
from gestioncof.forms import (
UserProfileForm, EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm,
UserForm, ProfileForm,
EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm,
RegistrationUserForm, RegistrationProfileForm, EventForm, CalendarForm,
EventFormset, RegistrationPassUserForm, ClubsForm, GestioncofConfigForm
)
@ -334,15 +335,20 @@ def survey_status(request, survey_id):
@cof_required
def profile(request):
user = request.user
data = request.POST if request.method == "POST" else None
user_form = UserForm(data=data, instance=user, prefix="u")
profile_form = ProfileForm(data=data, instance=user.profile, prefix="p")
if request.method == "POST":
form = UserProfileForm(request.POST, instance=request.user.profile)
if form.is_valid():
form.save()
messages.success(request,
"Votre profil a été mis à jour avec succès !")
else:
form = UserProfileForm(instance=request.user.profile)
return render(request, "gestioncof/profile.html", {"form": form})
if user_form.is_valid() and profile_form.is_valid():
user_form.save()
profile_form.save()
messages.success(
request,
_("Votre profil a été mis à jour avec succès !")
)
context = {"user_form": user_form, "profile_form": profile_form}
return render(request, "gestioncof/profile.html", context)
def registration_set_ro_fields(user_form, profile_form):
@ -588,6 +594,19 @@ def export_members(request):
return response
# ----------------------------------------
# Début des exports Mega machins hardcodés
# ----------------------------------------
MEGA_YEAR = 2018
MEGA_EVENT_NAME = "MEGA 2018"
MEGA_COMMENTFIELD_NAME = "Commentaires"
MEGA_CONSCRITORGAFIELD_NAME = "Orga ? Conscrit ?"
MEGA_CONSCRIT = "Conscrit"
MEGA_ORGA = "Orga"
def csv_export_mega(filename, qs):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=' + filename
@ -609,13 +628,13 @@ def csv_export_mega(filename, qs):
@buro_required
def export_mega_remarksonly(request):
filename = 'remarques_mega_2017.csv'
filename = 'remarques_mega_{}.csv'.format(MEGA_YEAR)
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=' + filename
writer = unicodecsv.writer(response)
event = Event.objects.get(title="MEGA 2017")
commentfield = event.commentfields.get(name="Commentaire")
event = Event.objects.get(title=MEGA_EVENT_NAME)
commentfield = event.commentfields.get(name=MEGA_COMMENTFIELD_NAME)
for val in commentfield.values.all():
reg = val.registration
user = reg.user
@ -647,32 +666,36 @@ def export_mega_remarksonly(request):
@buro_required
def export_mega_orgas(request):
event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Orga").id
event = Event.objects.get(title=MEGA_EVENT_NAME)
type_option = event.options.get(name=MEGA_CONSCRITORGAFIELD_NAME)
participant_type = type_option.choices.get(value=MEGA_ORGA).id
qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type
)
return csv_export_mega('orgas_mega_2017.csv', qs)
return csv_export_mega('orgas_mega_{}.csv'.format(MEGA_YEAR), qs)
@buro_required
def export_mega_participants(request):
event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Conscrit").id
event = Event.objects.get(title=MEGA_EVENT_NAME)
type_option = event.options.get(name=MEGA_CONSCRITORGAFIELD_NAME)
participant_type = type_option.choices.get(value=MEGA_CONSCRIT).id
qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type
)
return csv_export_mega('participants_mega_2017.csv', qs)
return csv_export_mega('conscrits_mega_{}.csv'.format(MEGA_YEAR), qs)
@buro_required
def export_mega(request):
event = Event.objects.filter(title="MEGA 2017")
event = Event.objects.filter(title=MEGA_EVENT_NAME)
qs = EventRegistration.objects.filter(event=event) \
.order_by("user__username")
return csv_export_mega('all_mega_2017.csv', qs)
return csv_export_mega('all_mega_{}.csv'.format(MEGA_YEAR), qs)
# ------------------------------
# Fin des exports Mega hardcodés
# ------------------------------
@buro_required

View file

@ -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.

View file

@ -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"

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model
from kfet.models import Account, GenericTeamToken

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from django.contrib.auth import get_user_model
from .backends import AccountBackend

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
from unittest import mock
from django.core import signing

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from ldap3 import Connection
from django.shortcuts import render
from django.http import Http404

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.core.exceptions import ValidationError
from django.db import models

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from kfet.config import kfet_config

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.decorators import user_passes_test

View file

@ -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
@ -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']

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.15 on 2018-09-02 21:13
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0063_promo'),
]
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=2018, null=True),
),
]

View file

@ -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
@ -397,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,
@ -449,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)
@ -463,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,
@ -479,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)
@ -566,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):

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from channels.routing import include, route_class
from . import consumers

View file

@ -75,6 +75,10 @@ ul {
padding:8px !important;
}
.table thead .sm-padding {
padding:3px !important;
}
.table tr.section {
background: #c63b52 !important;
color:#fff;

View file

@ -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;

View file

@ -235,3 +235,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();
});

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from datetime import date, datetime, time, timedelta
from dateutil.relativedelta import relativedelta

View file

@ -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>

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -27,7 +27,10 @@
{% block main %}
<div class="table-responsive">
<table class="table table-condensed">
<table
class="table table-condensed table-hover table-striped sortable"
{# Initial sort: [(article.name,asc)] #}
data-sortlist="[[0,0]]">
<thead>
<tr>
<td>Article</td>
@ -36,25 +39,28 @@
<td>Erreur</td>
</tr>
</thead>
<tbody>
{% for inventoryart in inventoryarts %}
{% ifchanged inventoryart.article.category %}
<tr class="section">
<td colspan="4">{{ inventoryart.article.category.name }}</td>
</tr>
{% endifchanged %}
<tr>
<td>
<a href="{% url "kfet.article.read" inventoryart.article.id %}">
{{ inventoryart.article.name }}
</a>
</td>
<td>{{ inventoryart.stock_old }}</td>
<td>{{ inventoryart.stock_new }}</td>
<td>{{ inventoryart.stock_error }}</td>
{% regroup inventoryarts by article.category as category_list %}
{% for category in category_list %}
<tbody class="tablesorter-no-sort">
<tr class="section">
<td colspan="4">{{ category.grouper.name }}</td>
</tr>
{% endfor %}
</tbody>
</tbody>
<tbody>
{% for inventoryart in category.list %}
<tr>
<td>
<a href="{% url "kfet.article.read" inventoryart.article.id %}">
{{ inventoryart.article.name }}
</a>
</td>
<td>{{ inventoryart.stock_old }}</td>
<td>{{ inventoryart.stock_new }}</td>
<td>{{ inventoryart.stock_error }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>

View file

@ -3,7 +3,7 @@
{% block extra_head %}
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/kpsul_grid.css' %}">
<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>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %}

View file

@ -55,11 +55,14 @@
<section>
<h2>Liste des commandes</h2>
<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="[[1,1]]">
<thead>
<tr>
<td></td>
<td>Date</td>
<td data-sorter="false"></td>
<td data-parser="shortDate">Date</td>
<td>Fournisseur</td>
<td>Inventaire</td>
</tr>
@ -74,9 +77,9 @@
</a>
{% endif %}
</td>
<td>
<td tile="{{ order.at }}">
<a href="{% url 'kfet.order.read' order.pk %}">
{{ order.at }}
{{ order.at|date:'d/m/Y H:i' }}
</a>
</td>
<td>{{ order.supplier }}</td>

View file

@ -11,60 +11,79 @@
<form action="" method="post">
{% csrf_token %}
<div class="table-responsive">
<table class="table table-hover table-condensed table-condensed-input text-center table-striped">
<table
class="table table-hover table-condensed table-condensed-input text-center table-striped sortable"
{# Initial sort: [(name,asc)] #}
data-sortlist="[[0,0]]">
<thead>
<tr>
<td rowspan="2">Article</td>
<td colspan="{{ scale|length }}">Ventes
<span class='glyphicon glyphicon-question-sign' title="Ventes des 5 dernières semaines" data-placement="bottom"></span>
</td>
<td rowspan="2">V. moy.<br>
<span class='glyphicon glyphicon-question-sign' title="Moyenne des ventes" data-placement="bottom"></span>
</td>
<td rowspan="2">E.T.<br>
<span class='glyphicon glyphicon-question-sign' title="Écart-type des ventes" data-placement="bottom"></span>
</td>
<td rowspan="2">Prév.<br>
<span class='glyphicon glyphicon-question-sign' title="Prévision de ventes" data-placement="bottom"></span>
</td>
<td colspan="{{ scale|length }}">
Ventes
<i class='glyphicon glyphicon-question-sign' title="Ventes des 5 dernières semaines" data-placement="bottom"></i>
</td>
<td rowspan="2">
V. moy.
<br>
<i class='glyphicon glyphicon-question-sign' title="Moyenne des ventes" data-placement="bottom"></i>
</td>
<td rowspan="2" data-sorter="false">
E.T.
<br>
<i class='glyphicon glyphicon-question-sign' title="Écart-type des ventes" data-placement="bottom"></i>
</td>
<td rowspan="2">
Prév.
<br>
<i class='glyphicon glyphicon-question-sign' title="Prévision de ventes" data-placement="bottom"></i>
</td>
<td rowspan="2">Stock</td>
<td rowspan="2">Box<br>
<span class='glyphicon glyphicon-question-sign' title="Capacité d'une boite" data-placement="bottom"></span>
</td>
<td rowspan="2">Rec.<br>
<span class='glyphicon glyphicon-question-sign' title="Quantité conseillée" data-placement="bottom"></span>
</td>
<td rowspan="2">Commande</td>
<td rowspan="2" data-sorter="false">
Box
<br>
<i class='glyphicon glyphicon-question-sign' title="Capacité d'une boite" data-placement="bottom"></i>
</td>
<td rowspan="2">
Rec.
<br>
<i class='glyphicon glyphicon-question-sign' title="Quantité conseillée" data-placement="bottom"></i>
</td>
<td rowspan="2" data-sorter="false" class="small-width">
Commande
</td>
</tr>
<tr>
{% for label in scale.get_labels %}
<td>{{ label }}</td>
<td class="sm-padding">{{ label }}</td>
{% endfor %}
</tr>
</thead>
<tbody>
{% for form in formset %}
{% ifchanged form.category %}
<tr class='section text-left'>
<td colspan="{{ scale|length|add:'8' }}">{{ form.category_name }}</td>
</tr>
{% endifchanged %}
<tr>
{{ form.article }}
<td class="text-left">{{ form.name }}</td>
{% for v_chunk in form.v_all %}
<td>{{ v_chunk }}</td>
{% endfor %}
<td>{{ form.v_moy }}</td>
<td>{{ form.v_et }}</td>
<td>{{ form.v_prev }}</td>
<td>{{ form.stock }}</td>
<td>{{ form.box_capacity|default:"" }}</td>
<td>{{ form.c_rec }}</td>
<td class="nopadding">{{ form.quantity_ordered | add_class:"form-control" }}</td>
{% regroup formset by category_name as category_list %}
{% for category in category_list %}
<tbody class="tablesorter-no-sort">
<tr class='section text-left'>
<td colspan="{{ scale|length|add:'8' }}">{{ category.grouper }}</td>
</tr>
{% endfor %}
</tbody>
</tbody>
<tbody>
{% for form in category.list %}
<tr>
{{ form.article }}
<td class="text-left">{{ form.name }}</td>
{% for v_chunk in form.v_all %}
<td>{{ v_chunk }}</td>
{% endfor %}
<td>{{ form.v_moy }}</td>
<td>{{ form.v_et }}</td>
<td>{{ form.v_prev }}</td>
<td>{{ form.stock }}</td>
<td>{{ form.box_capacity|default:"" }}</td>
<td>{{ form.c_rec }}</td>
<td class="nopadding">{{ form.quantity_ordered|add_class:"form-control" }}</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
{{ formset.management_form }}

View file

@ -42,7 +42,10 @@
<section>
<h2>Détails</h2>
<div class="table-responsive">
<table class="table table-condensed">
<table
class="table table-condensed table-hover table-striped sortable"
{# Initial sort: [(article.name,asc)] #}
data-sortlist="[[0,0]]">
<thead>
<tr>
<td>Article</td>
@ -51,32 +54,35 @@
<td>Reçu</td>
</tr>
</thead>
<tbody>
{% for orderart in orderarts %}
{% ifchanged orderart.article.category %}
<tr class="section">
<td colspan="4">{{ orderart.article.category.name }}</td>
</tr>
{% endifchanged %}
<tr>
<td>
<a href="{% url "kfet.article.read" orderart.article.id %}">
{{ orderart.article.name }}
</a>
</td>
<td>{{ orderart.quantity_ordered }}</td>
<td>
{% if orderart.article.box_capacity %}
{# c'est une division ! #}
{% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %}
{% endif %}
</td>
<td>
{{ orderart.quantity_received|default_if_none:'' }}
</td>
{% regroup orderarts by article.category as category_list %}
{% for category in category_list %}
<tbody class="tablesorter-no-sort">
<tr class="section">
<td colspan="4">{{ category.grouper.name }}</td>
</tr>
{% endfor %}
</tbody>
</tbody>
<tbody>
{% for orderart in category.list %}
<tr>
<td>
<a href="{% url "kfet.article.read" orderart.article.id %}">
{{ orderart.article.name }}
</a>
</td>
<td>{{ orderart.quantity_ordered }}</td>
<td>
{% if orderart.article.box_capacity %}
{# c'est une division ! #}
{% widthratio orderart.quantity_ordered orderart.article.box_capacity 1 %}
{% endif %}
</td>
<td>
{{ orderart.quantity_received|default_if_none:'' }}
</td>
</tr>
{% endfor %}
</tbody>
{% endfor %}
</table>
</div>
</section>

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import re
from django import template

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from decimal import Decimal
from django.test import TestCase

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from unittest.mock import patch
from django.test import TestCase, Client

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
from django.conf.urls import include, url
from django.contrib.auth.decorators import permission_required

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import ast
from urllib.parse import urlencode
@ -495,7 +493,7 @@ class AccountNegativeList(ListView):
context_object_name = 'negatives'
def get_context_data(self, **kwargs):
context = super(AccountNegativeList, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
real_balances = (neg.account.real_balance for neg in self.object_list)
context['negatives_sum'] = sum(real_balances)
return context
@ -530,7 +528,7 @@ class CheckoutCreate(SuccessMessageMixin, CreateView):
form.instance.created_by = self.request.user.profile.account_kfet
form.save()
return super(CheckoutCreate, self).form_valid(form)
return super().form_valid(form)
# Checkout - Read
@ -540,7 +538,7 @@ class CheckoutRead(DetailView):
context_object_name = 'checkout'
def get_context_data(self, **kwargs):
context = super(CheckoutRead, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['statements'] = context['checkout'].statements.order_by('-at')
return context
@ -559,7 +557,7 @@ class CheckoutUpdate(SuccessMessageMixin, UpdateView):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
return super(CheckoutUpdate, self).form_valid(form)
return super().form_valid(form)
# -----
# Checkout Statement views
@ -611,7 +609,7 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
at = self.object.at)
def get_context_data(self, **kwargs):
context = super(CheckoutStatementCreate, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
context['checkout'] = checkout
return context
@ -627,7 +625,7 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView):
form.instance.balance_new = getAmountBalance(form.cleaned_data)
form.instance.checkout_id = self.kwargs['pk_checkout']
form.instance.by = self.request.user.profile.account_kfet
return super(CheckoutStatementCreate, self).form_valid(form)
return super().form_valid(form)
class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
model = CheckoutStatement
@ -639,7 +637,7 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
return reverse_lazy('kfet.checkout.read', kwargs={'pk':self.kwargs['pk_checkout']})
def get_context_data(self, **kwargs):
context = super(CheckoutStatementUpdate, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
checkout = Checkout.objects.get(pk=self.kwargs['pk_checkout'])
context['checkout'] = checkout
return context
@ -651,7 +649,7 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView):
return self.form_invalid(form)
# Updating
form.instance.amount_taken = getAmountTaken(form.instance)
return super(CheckoutStatementUpdate, self).form_valid(form)
return super().form_valid(form)
# -----
# Category views
@ -683,7 +681,7 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView):
return self.form_invalid(form)
# Updating
return super(CategoryUpdate, self).form_valid(form)
return super().form_valid(form)
# -----
# Article views
@ -706,6 +704,14 @@ class ArticleList(ListView):
)
template_name = 'kfet/article.html'
context_object_name = 'articles'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
articles = context[self.context_object_name]
context['nb_articles'] = len(articles)
context[self.context_object_name] = articles.filter(is_sold=True)
context['not_sold_articles'] = articles.filter(is_sold=False)
return context
# Article - Create
@ -750,7 +756,7 @@ class ArticleCreate(SuccessMessageMixin, CreateView):
)
# Creating
return super(ArticleCreate, self).form_valid(form)
return super().form_valid(form)
# Article - Read
@ -760,7 +766,7 @@ class ArticleRead(DetailView):
context_object_name = 'article'
def get_context_data(self, **kwargs):
context = super(ArticleRead, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
inventoryarts = (InventoryArticle.objects
.filter(article=self.object)
.select_related('inventory')
@ -812,7 +818,7 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView):
article=article, supplier=supplier)
# Updating
return super(ArticleUpdate, self).form_valid(form)
return super().form_valid(form)
# -----
@ -1703,7 +1709,7 @@ class InventoryRead(DetailView):
context_object_name = 'inventory'
def get_context_data(self, **kwargs):
context = super(InventoryRead, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
inventoryarticles = (InventoryArticle.objects
.select_related('article', 'article__category')
.filter(inventory = self.object)
@ -1721,7 +1727,7 @@ class OrderList(ListView):
context_object_name = 'orders'
def get_context_data(self, **kwargs):
context = super(OrderList, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
context['suppliers'] = Supplier.objects.order_by('name')
return context
@ -1835,7 +1841,7 @@ def order_create(request, pk):
else:
formset = cls_formset(initial=initial)
scale.label_fmt = "S -{rev_i}"
scale.label_fmt = "S-{rev_i}"
return render(request, 'kfet/order_create.html', {
'supplier': supplier,
@ -1850,7 +1856,7 @@ class OrderRead(DetailView):
context_object_name = 'order'
def get_context_data(self, **kwargs):
context = super(OrderRead, self).get_context_data(**kwargs)
context = super().get_context_data(**kwargs)
orderarticles = (OrderArticle.objects
.select_related('article', 'article__category')
.filter(order=self.object)
@ -2004,7 +2010,7 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
form.add_error(None, 'Permission refusée')
return self.form_invalid(form)
# Updating
return super(SupplierUpdate, self).form_valid(form)
return super().form_valid(form)
# ==========
@ -2268,7 +2274,7 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView):
@method_decorator(login_required)
def dispatch(self, *args, **kwargs):
return super(AccountStatBalance, self).dispatch(*args, **kwargs)
return super().dispatch(*args, **kwargs)
# ------------------------

View file

@ -4,19 +4,17 @@ django-autocomplete-light==3.1.3
django-autoslug==1.9.3
django-cas-ng==3.5.7
django-djconfig==0.5.3
django-recaptcha==1.2.1
django-recaptcha==1.4.0
django-redis-cache==1.7.1
icalendar
psycopg2
Pillow
six
unicodecsv
django-bootstrap-form==3.3
asgiref==1.1.1
daphne==1.3.0
asgi-redis==1.3.0
statistics==1.0.3.5
future==0.15.2
django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3
@ -24,6 +22,7 @@ channels==1.1.5
python-dateutil
wagtail==1.10.*
wagtailmenus==2.2.*
django-cors-headers==2.2.0
# Production tools
wheel