Merge branch 'Kerl/use_django_custommail' of git.eleves.ens.fr:cof-geek/gestioCOF into Kerl/use_django_custommail

This commit is contained in:
Qwann 2017-02-11 15:34:04 +01:00
commit 5d35112c87
52 changed files with 1892 additions and 14582 deletions

View file

@ -84,29 +84,30 @@ visualiser la dernière version du code.
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il
est fortement conseillé d'utiliser un environnement virtuel pour Python. est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer mercurial, pip, les librairies de développement de Il vous faudra installer pip, les librairies de développement de python, un
python, un client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées
dérivées (Ubuntu, ...) : (Ubuntu, ...) :
sudo apt-get install mercurial python-pip python-dev libmysqlclient-dev sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server
redis-server
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant : (le dossier où se trouve ce README), et créez-le maintenant :
virtualenv env virtualenv env -p $(which python3)
Pour l'activer, il faut faire L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir
python3, si c'est la version de python par défaut sur votre système, ceci n'est
pas nécessaire. Pour l'activer, il faut faire
. env/bin/activate . env/bin/activate
dans le même dossier. dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis les fichiers Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements.txt` et `requirements-devel.txt` : `requirements-devel.txt` :
pip install -r requirements.txt -r requirements-devel.txt pip install -r requirements-devel.txt
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`. Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`.
@ -174,9 +175,10 @@ Charger les mails indispensables au bon fonctionnement de GestioCOF :
python manage.py syncmails python manage.py syncmails
Une base de donnée pré-remplie est disponible en lançant la commande : Une base de donnée pré-remplie est disponible en lançant les commandes :
python manage.py loaddata users root bda gestion sites accounts groups articles python manage.py loaddata gestion sites accounts groups articles
python manage.py loaddevdata
Vous êtes prêts à développer ! Lancer GestioCOF en faisant Vous êtes prêts à développer ! Lancer GestioCOF en faisant

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,107 @@
"""
Crée deux tirages de test et y inscrit les utilisateurs
"""
import os
import random
from django.utils import timezone
from django.contrib.auth.models import User
from gestioncof.management.base import MyBaseCommand
from bda.models import Tirage, Spectacle, Salle, Participant, ChoixSpectacle
from bda.views import do_tirage
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'data')
class Command(MyBaseCommand):
help = "Crée deux tirages de test et y inscrit les utilisateurs."
def handle(self, *args, **options):
# ---
# Tirages
# ---
Tirage.objects.all().delete()
Tirage.objects.bulk_create([
Tirage(
title="Tirage de test 1",
ouverture=timezone.now()-timezone.timedelta(days=7),
fermeture=timezone.now(),
active=True
),
Tirage(
title="Tirage de test 2",
ouverture=timezone.now(),
fermeture=timezone.now()+timezone.timedelta(days=60),
active=True
)
])
tirages = Tirage.objects.all()
# ---
# Salles
# ---
locations = self.from_json('locations.json', DATA_DIR, Salle)
# ---
# Spectacles
# ---
def show_callback(show):
"""
Assigne un tirage, une date et un lieu à un spectacle et décide si
les places sont sur listing.
"""
show.tirage = random.choice(tirages)
show.listing = bool(random.randint(0, 1))
show.date = (
show.tirage.fermeture
+ timezone.timedelta(days=random.randint(60, 90))
)
show.location = random.choice(locations)
return show
shows = self.from_json(
'shows.json', DATA_DIR, Spectacle, show_callback
)
# ---
# Inscriptions
# ---
self.stdout.write("Inscription des utilisateurs aux tirages")
ChoixSpectacle.objects.all().delete()
choices = []
for user in User.objects.filter(profile__is_cof=True):
for tirage in tirages:
part, _ = Participant.objects.get_or_create(
user=user,
tirage=tirage
)
shows = random.sample(
list(tirage.spectacle_set.all()),
tirage.spectacle_set.count() // 2
)
for (rank, show) in enumerate(shows):
choices.append(ChoixSpectacle(
participant=part,
spectacle=show,
priority=rank + 1,
double_choice=random.choice(
['1', 'double', 'autoquit']
)
))
ChoixSpectacle.objects.bulk_create(choices)
self.stdout.write("- {:d} inscriptions générées".format(len(choices)))
# ---
# On lance le premier tirage
# ---
self.stdout.write("Lancement du premier tirage")
do_tirage(tirages[0], "dummy_token")

View file

@ -0,0 +1,26 @@
[
{
"name": "Cour\u00f4",
"address": "45 rue d'Ulm, cour\u00f4"
},
{
"name": "K-F\u00eat",
"address": "45 rue d'Ulm, escalier C, niveau -1"
},
{
"name": "Th\u00e9\u00e2tre",
"address": "45 rue d'Ulm, escalier C, niveau -1"
},
{
"name": "Cours Pasteur",
"address": "45 rue d'Ulm, cours pasteur"
},
{
"name": "Salle des actes",
"address": "45 rue d'Ulm, escalier A, niveau 1"
},
{
"name": "Amphi Rataud",
"address": "45 rue d'Ulm, NIR, niveau PB"
}
]

View file

@ -0,0 +1,100 @@
[
{
"description": "Jazz / Funk",
"title": "Un super concert",
"price": 10.0,
"slots_description": "Debout",
"slots": 5
},
{
"description": "Homemade",
"title": "Une super pi\u00e8ce",
"price": 10.0,
"slots_description": "Assises",
"slots": 60
},
{
"description": "Plein air, soleil, bonne musique",
"title": "Concert pour la f\u00eate de la musique",
"price": 5.0,
"slots_description": "Debout, attention \u00e0 la fontaine",
"slots": 30
},
{
"description": "Sous le regard s\u00e9v\u00e8re de Louis Pasteur",
"title": "Op\u00e9ra sans d\u00e9cors",
"price": 5.0,
"slots_description": "Assis sur l'herbe",
"slots": 20
},
{
"description": "Buffet \u00e0 la fin",
"title": "Concert Trouv\u00e8re",
"price": 20.0,
"slots_description": "Assises",
"slots": 15
},
{
"description": "Vive les maths",
"title": "Dessin \u00e0 la craie sur tableau noir",
"price": 10.0,
"slots_description": "Assises, tablette pour prendre des notes",
"slots": 30
},
{
"description": "Une pi\u00e8ce \u00e0 un personnage",
"title": "D\u00e9cors, d\u00e9montage en musique",
"price": 0.0,
"slots_description": "Assises",
"slots": 20
},
{
"description": "Annulera, annulera pas\u00a0?",
"title": "La Nuit",
"price": 27.0,
"slots_description": "",
"slots": 1000
},
{
"description": "Le boum fait sa carte blanche",
"title": "Turbomix",
"price": 10.0,
"slots_description": "Debout les mains en l'air",
"slots": 20
},
{
"description": "Unique repr\u00e9sentation",
"title": "Carinettes et trombone",
"price": 15.0,
"slots_description": "Chaises ikea",
"slots": 10
},
{
"description": "Suivi d'une jam session",
"title": "Percussion sur rondins",
"price": 5.0,
"slots_description": "B\u00fbches",
"slots": 14
},
{
"description": "\u00c9preuve sportive et artistique",
"title": "Bassin aux ernests, nage libre",
"price": 5.0,
"slots_description": "Humides",
"slots": 10
},
{
"description": "Sonore",
"title": "Chant du barde",
"price": 13.0,
"slots_description": "Ne venez pas",
"slots": 20
},
{
"description": "Cocorico",
"title": "Chant du coq",
"price": 4.0,
"slots_description": "bancs",
"slots": 15
}
]

View file

@ -10,10 +10,8 @@ from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from django.utils import timezone, formats from django.utils import timezone, formats
from django.utils.encoding import python_2_unicode_compatible
@python_2_unicode_compatible
class Tirage(models.Model): class Tirage(models.Model):
title = models.CharField("Titre", max_length=300) title = models.CharField("Titre", max_length=300)
ouverture = models.DateTimeField("Date et heure d'ouverture du tirage") ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
@ -28,7 +26,6 @@ class Tirage(models.Model):
timezone.template_localtime(self.fermeture))) timezone.template_localtime(self.fermeture)))
@python_2_unicode_compatible
class Salle(models.Model): class Salle(models.Model):
name = models.CharField("Nom", max_length=300) name = models.CharField("Nom", max_length=300)
address = models.TextField("Adresse") address = models.TextField("Adresse")
@ -37,7 +34,6 @@ class Salle(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class CategorieSpectacle(models.Model): class CategorieSpectacle(models.Model):
name = models.CharField('Nom', max_length=100, unique=True) name = models.CharField('Nom', max_length=100, unique=True)
@ -114,6 +110,10 @@ class Spectacle(models.Model):
# On renvoie la liste des destinataires # On renvoie la liste des destinataires
return members.values() return members.values()
@property
def is_past(self):
return self.date < timezone.now()
class Quote(models.Model): class Quote(models.Model):
spectacle = models.ForeignKey(Spectacle) spectacle = models.ForeignKey(Spectacle)
@ -129,7 +129,6 @@ PAYMENT_TYPES = (
) )
@python_2_unicode_compatible
class Participant(models.Model): class Participant(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
choices = models.ManyToManyField(Spectacle, choices = models.ManyToManyField(Spectacle,
@ -157,7 +156,6 @@ DOUBLE_CHOICES = (
) )
@python_2_unicode_compatible
class ChoixSpectacle(models.Model): class ChoixSpectacle(models.Model):
participant = models.ForeignKey(Participant) participant = models.ForeignKey(Participant)
spectacle = models.ForeignKey(Spectacle, related_name="participants") spectacle = models.ForeignKey(Spectacle, related_name="participants")
@ -176,7 +174,7 @@ class ChoixSpectacle(models.Model):
def __str__(self): def __str__(self):
return "Vœux de %s pour %s" % ( return "Vœux de %s pour %s" % (
self.participant.user.get_full_name, self.participant.user.get_full_name(),
self.spectacle.title) self.spectacle.title)
class Meta: class Meta:
@ -186,7 +184,6 @@ class ChoixSpectacle(models.Model):
verbose_name_plural = "voeux" verbose_name_plural = "voeux"
@python_2_unicode_compatible
class Attribution(models.Model): class Attribution(models.Model):
participant = models.ForeignKey(Participant) participant = models.ForeignKey(Participant)
spectacle = models.ForeignKey(Spectacle, related_name="attribues") spectacle = models.ForeignKey(Spectacle, related_name="attribues")
@ -197,7 +194,6 @@ class Attribution(models.Model):
self.spectacle.date) self.spectacle.date)
@python_2_unicode_compatible
class SpectacleRevente(models.Model): class SpectacleRevente(models.Model):
attribution = models.OneToOneField(Attribution, attribution = models.OneToOneField(Attribution,
related_name="revente") related_name="revente")

View file

@ -43,3 +43,6 @@ td {
margin: 10px 0px; margin: 10px 0px;
} }
.spectacle-passe {
opacity:0.5;
}

View file

@ -2,11 +2,10 @@
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link href="{{ STATIC_URL }}grappelli/jquery/ui/css/custom-theme/jquery-ui-1.8.custom.css" rel="stylesheet" type="text/css" media="screen" title="no title" charset="utf-8" /> <script src="{% static 'js/jquery.min.js'%}" type="text/javascript"></script>
<script src="{{ STATIC_URL }}grappelli/jquery/jquery-1.6.2.min.js" type="text/javascript"></script> <script src="{% static 'js/jquery-ui.min.js' %}" type="text/javascript"></script>
<script src="{{ STATIC_URL }}grappelli/jquery/ui/js/jquery-ui-1.8.15.custom.min.js" type="text/javascript"></script> <script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link href="{{ STATIC_URL }}grappelli/css/tools.css" rel="stylesheet" type="text/css" /> <link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
<link href="{{ STATIC_URL }}grappelli/css/jquery-ui-grappelli-extensions.css" rel="stylesheet" type="text/css" />
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %} {% endblock %}
@ -102,7 +101,7 @@ var django = {
{% endif %} {% endif %}
<form class="form-horizontal" id="bda_form" method="post" action="{% url 'bda-tirage-inscription' tirage.id %}"> <form class="form-horizontal" id="bda_form" method="post" action="{% url 'bda-tirage-inscription' tirage.id %}">
{% csrf_token %} {% csrf_token %}
{% include "inscription-formset.html" %} {% include "bda/inscription-formset.html" %}
<div class="inscription-bottom"> <div class="inscription-bottom">
<span class="bda-prix">Prix total actuel : {{ total_price }}€</span> <span class="bda-prix">Prix total actuel : {{ total_price }}€</span>
<div class="pull-right"> <div class="pull-right">

View file

@ -1,6 +1,10 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %} {% block realcontent %}
<h2><strong>{{tirage_name}}</strong></h2> <h2><strong>{{tirage_name}}</strong></h2>
<h3>Liste des spectacles</h3> <h3>Liste des spectacles</h3>
@ -17,9 +21,9 @@
</thead> </thead>
<tbody> <tbody>
{% for spectacle in object_list %} {% for spectacle in object_list %}
<tr class="clickable-row" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}"> <tr class="clickable-row {% if spectacle.is_past %}spectacle-passe{% endif %}" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td> <td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td> <td data-sort-value="{{ spectacle.timestamp }}"">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td> <td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}"> <td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">
{{ spectacle.price |floatformat }}€ {{ spectacle.price |floatformat }}€

View file

@ -124,14 +124,14 @@ def inscription(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id) tirage = get_object_or_404(Tirage, id=tirage_id)
if timezone.now() < tirage.ouverture: if timezone.now() < tirage.ouverture:
error_desc = tirage.ouverture.strftime('Ouverture le %d %b %Y à %H:%M') error_desc = tirage.ouverture.strftime('Ouverture le %d %b %Y à %H:%M')
return render(request, 'resume_inscription.html', return render(request, 'bda/resume-inscription-tirage.html',
{"error_title": "Le tirage n'est pas encore ouvert !", {"error_title": "Le tirage n'est pas encore ouvert !",
"error_description": error_desc}) "error_description": error_desc})
if timezone.now() > tirage.fermeture: if timezone.now() > tirage.fermeture:
participant, created = Participant.objects.get_or_create( participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage) user=request.user, tirage=tirage)
choices = participant.choixspectacle_set.order_by("priority").all() choices = participant.choixspectacle_set.order_by("priority").all()
return render(request, "resume_inscription.html", return render(request, "bda/resume-inscription-tirage.html",
{"error_title": "C'est fini !", {"error_title": "C'est fini !",
"error_description": "error_description":
"Tirage au sort dans la journée !", "Tirage au sort dans la journée !",
@ -170,7 +170,7 @@ def inscription(request, tirage_id):
total_price += choice.spectacle.price total_price += choice.spectacle.price
if choice.double: if choice.double:
total_price += choice.spectacle.price total_price += choice.spectacle.price
return render(request, "inscription-bda.html", return render(request, "bda/inscription-tirage.html",
{"formset": formset, {"formset": formset,
"success": success, "success": success,
"total_price": total_price, "total_price": total_price,
@ -179,79 +179,96 @@ def inscription(request, tirage_id):
"stateerror": stateerror}) "stateerror": stateerror})
def do_tirage(request, tirage_id): def do_tirage(tirage_elt, token):
tirage_elt = get_object_or_404(Tirage, id=tirage_id) """
form = TokenForm(request.POST) Fonction auxiliaire à la vue ``tirage`` qui lance effectivement le tirage
if not form.is_valid(): après qu'on a vérifié que c'est légitime et que le token donné en argument
return tirage(request, tirage_id) est correct.
Rend les résultats
"""
# Initialisation du dictionnaire data qui va contenir les résultats
start = time.time() start = time.time()
data = {} data = {
shows = tirage_elt.spectacle_set.select_related().all() 'shows': tirage_elt.spectacle_set.select_related().all(),
members = tirage_elt.participant_set.all() 'token': token,
choices = ChoixSpectacle.objects.filter(spectacle__tirage=tirage_elt) \ 'members': tirage_elt.participant_set.all(),
.order_by('participant', 'priority').select_related().all() 'total_slots': 0,
algo = Algorithm(shows, members, choices) 'total_losers': 0,
results = algo(form.cleaned_data["token"]) 'total_sold': 0,
total_slots = 0 'total_deficit': 0,
total_losers = 0 'opera_deficit': 0,
}
# On lance le tirage
choices = (
ChoixSpectacle.objects
.filter(spectacle__tirage=tirage_elt)
.order_by('participant', 'priority')
.select_related().all()
)
results = Algorithm(data['shows'], data['members'], choices)(token)
# On compte les places attribuées et les déçus
for (_, members, losers) in results: for (_, members, losers) in results:
total_slots += len(members) data['total_slots'] += len(members)
total_losers += len(losers) data['total_losers'] += len(losers)
data["total_slots"] = total_slots
data["total_losers"] = total_losers # On calcule le déficit et les bénéfices pour le BdA
data["shows"] = shows # FIXME: le traitement de l'opéra est sale
data["token"] = form.cleaned_data["token"]
data["members"] = members
data["results"] = results
total_sold = 0
total_deficit = 0
opera_deficit = 0
for (show, members, _) in results: for (show, members, _) in results:
deficit = (show.slots - len(members)) * show.price deficit = (show.slots - len(members)) * show.price
total_sold += show.slots * show.price data['total_sold'] += show.slots * show.price
if deficit >= 0: if deficit >= 0:
if "Opéra" in show.location.name: if "Opéra" in show.location.name:
opera_deficit += deficit data['opera_deficit'] += deficit
total_deficit += deficit data['total_deficit'] += deficit
data["total_sold"] = total_sold - total_deficit data["total_sold"] -= data['total_deficit']
data["total_deficit"] = total_deficit
data["opera_deficit"] = opera_deficit # Participant objects are not shared accross spectacle results,
# so assign a single object for each Participant id
members_uniq = {}
members2 = {}
for (show, members, _) in results:
for (member, _, _, _) in members:
if member.id not in members_uniq:
members_uniq[member.id] = member
members2[member] = []
member.total = 0
member = members_uniq[member.id]
members2[member].append(show)
member.total += show.price
members2 = members2.items()
data["members2"] = sorted(members2, key=lambda m: m[0].user.last_name)
# ---
# À partir d'ici, le tirage devient effectif
# ---
# On suppression les vieilles attributions, on sauvegarde le token et on
# désactive le tirage
Attribution.objects.filter(spectacle__tirage=tirage_elt).delete()
tirage_elt.tokens += '{:s}\n"""{:s}"""\n'.format(
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
token)
tirage_elt.enable_do_tirage = False
tirage_elt.save()
# On enregistre les nouvelles attributions
Attribution.objects.bulk_create([
Attribution(spectacle=show, participant=member)
for show, members, _ in results
for member, _, _, _ in members
])
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
for (show, _, losers) in results:
for (loser, _, _, _) in losers:
loser.choicesrevente.add(show)
loser.save()
data["duration"] = time.time() - start data["duration"] = time.time() - start
if request.user.is_authenticated(): data["results"] = results
members2 = {} return data
# Participant objects are not shared accross spectacle results,
# so assign a single object for each Participant id
members_uniq = {}
for (show, members, _) in results:
for (member, _, _, _) in members:
if member.id not in members_uniq:
members_uniq[member.id] = member
members2[member] = []
member.total = 0
member = members_uniq[member.id]
members2[member].append(show)
member.total += show.price
members2 = members2.items()
data["members2"] = sorted(members2, key=lambda m: m[0].user.last_name)
# À partir d'ici, le tirage devient effectif
Attribution.objects.filter(spectacle__tirage=tirage_elt).delete()
tirage_elt.tokens += "%s\n\"\"\"%s\"\"\"\n" % (
timezone.now().strftime("%y-%m-%d %H:%M:%S"),
form.cleaned_data['token'])
tirage_elt.enable_do_tirage = False
tirage_elt.save()
Attribution.objects.bulk_create([
Attribution(spectacle=show, participant=member)
for show, members, _ in results
for member, _, _, _ in members])
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
for (show, _, losers) in results:
for (loser, _, _, _) in losers:
loser.choicesrevente.add(show)
loser.save()
return render(request, "bda-attrib-extra.html", data)
else:
return render(request, "bda-attrib.html", data)
@buro_required @buro_required
@ -263,7 +280,8 @@ def tirage(request, tirage_id):
if request.POST: if request.POST:
form = TokenForm(request.POST) form = TokenForm(request.POST)
if form.is_valid(): if form.is_valid():
return do_tirage(request, tirage_id) results = do_tirage(tirage_elt, form.cleaned_data['token'])
return render(request, "bda-attrib-extra.html", results)
else: else:
form = TokenForm() form = TokenForm()
return render(request, "bda-token.html", {"form": form}) return render(request, "bda-token.html", {"form": form})

View file

@ -9,10 +9,6 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/ https://docs.djangoproject.com/en/1.8/ref/settings/
""" """
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os import os
@ -163,6 +159,8 @@ AUTHENTICATION_BACKENDS = (
'kfet.backends.GenericTeamBackend', 'kfet.backends.GenericTeamBackend',
) )
# LDAP_SERVER_URL = 'ldaps://ldap.spi.ens.fr:636'
# EMAIL_HOST="nef.ens.fr" # EMAIL_HOST="nef.ens.fr"
RECAPTCHA_PUBLIC_KEY = "DUMMY" RECAPTCHA_PUBLIC_KEY = "DUMMY"

View file

@ -4,10 +4,6 @@
Fichier principal de configuration des urls du projet GestioCOF Fichier principal de configuration des urls du projet GestioCOF
""" """
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light import autocomplete_light
from django.conf import settings from django.conf import settings
@ -61,7 +57,8 @@ urlpatterns = [
name='password_change_done'), name='password_change_done'),
# Inscription d'un nouveau membre # Inscription d'un nouveau membre
url(r'^registration$', gestioncof_views.registration), url(r'^registration$', gestioncof_views.registration),
url(r'^registration/clipper/(?P<login_clipper>[\w-]+)$', url(r'^registration/clipper/(?P<login_clipper>[\w-]+)/'
r'(?P<fullname>.*)$',
gestioncof_views.registration_form2, name="clipper-registration"), gestioncof_views.registration_form2, name="clipper-registration"),
url(r'^registration/user/(?P<username>.+)$', url(r'^registration/user/(?P<username>.+)$',
gestioncof_views.registration_form2, name="user-registration"), gestioncof_views.registration_form2, name="user-registration"),

View file

@ -1,18 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from ldap3 import Connection
from __future__ import print_function
from __future__ import unicode_literals
from django import shortcuts from django import shortcuts
from django.http import Http404 from django.http import Http404
from django.db.models import Q from django.db.models import Q
from django.contrib.auth.models import User from django.contrib.auth.models import User
from gestioncof.models import CofProfile, Clipper from django.conf import settings
from gestioncof.models import CofProfile
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@buro_required @buro_required
def autocomplete(request): def autocomplete(request):
if "q" not in request.GET: if "q" not in request.GET:
@ -25,37 +30,51 @@ def autocomplete(request):
queries = {} queries = {}
bits = q.split() bits = q.split()
queries['members'] = CofProfile.objects.filter(Q(is_cof=True)) # Fetching data from User and CofProfile tables
queries['users'] = User.objects.filter(Q(profile__is_cof=False)) queries['members'] = CofProfile.objects.filter(is_cof=True)
queries['clippers'] = Clipper.objects queries['users'] = User.objects.filter(profile__is_cof=False)
for bit in bits: for bit in bits:
queries['members'] = queries['members'].filter( queries['members'] = queries['members'].filter(
Q(user__first_name__icontains=bit) Q(user__first_name__icontains=bit)
| Q(user__last_name__icontains=bit) | Q(user__last_name__icontains=bit)
| Q(user__username__icontains=bit) | Q(user__username__icontains=bit)
| Q(login_clipper__icontains=bit)) | Q(login_clipper__icontains=bit))
queries['users'] = queries['users'].filter( queries['users'] = queries['users'].filter(
Q(first_name__icontains=bit) Q(first_name__icontains=bit)
| Q(last_name__icontains=bit) | Q(last_name__icontains=bit)
| Q(username__icontains=bit)) | Q(username__icontains=bit))
queries['clippers'] = queries['clippers'].filter(
Q(fullname__icontains=bit)
| Q(username__icontains=bit))
queries['members'] = queries['members'].distinct() queries['members'] = queries['members'].distinct()
queries['users'] = queries['users'].distinct() queries['users'] = queries['users'].distinct()
usernames = list(queries['members'].values_list('login_clipper',
flat='True')) \
+ list(queries['users'].values_list('profile__login_clipper',
flat='True'))
queries['clippers'] = queries['clippers'] \
.exclude(username__in=usernames).distinct()
# add clippers
# Clearing redundancies
usernames = (
set(queries['members'].values_list('login_clipper', flat='True'))
| set(queries['users'].values_list('profile__login_clipper',
flat='True'))
)
# Fetching data from the SPI
if hasattr(settings, 'LDAP_SERVER_URL'):
# Fetching
ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(**{"bit": bit})
for bit in bits]
))
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
]
# Resulting data
data.update(queries) data.update(queries)
data['options'] = sum(len(query) for query in queries)
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
return shortcuts.render(request, "autocomplete_user.html", data) return shortcuts.render(request, "autocomplete_user.html", data)

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import autocomplete_light import autocomplete_light
from django.contrib.auth.models import User from django.contrib.auth.models import User
autocomplete_light.register( autocomplete_light.register(
User, search_fields=('username', 'first_name', 'last_name'), User, search_fields=('username', 'first_name', 'last_name'),
autocomplete_js_attributes={'placeholder': 'membre...'}) attrs={'placeholder': 'membre...'}
)

View file

@ -152,156 +152,6 @@
"model": "gestioncof.petitcourssubject", "model": "gestioncof.petitcourssubject",
"pk": 4 "pk": 4
}, },
{
"fields": {
"matiere": 1,
"niveau": "college",
"user": 11,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 1
},
{
"fields": {
"matiere": 1,
"niveau": "lycee",
"user": 11,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 2
},
{
"fields": {
"matiere": 1,
"niveau": "prepa1styear",
"user": 11,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 3
},
{
"fields": {
"matiere": 1,
"niveau": "prepa2ndyear",
"user": 11,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 4
},
{
"fields": {
"matiere": 1,
"niveau": "licence3",
"user": 11,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 5
},
{
"fields": {
"matiere": 1,
"niveau": "prepa1styear",
"user": 43,
"agrege": true
},
"model": "gestioncof.petitcoursability",
"pk": 6
},
{
"fields": {
"matiere": 1,
"niveau": "prepa2ndyear",
"user": 43,
"agrege": true
},
"model": "gestioncof.petitcoursability",
"pk": 7
},
{
"fields": {
"matiere": 1,
"niveau": "licence3",
"user": 43,
"agrege": true
},
"model": "gestioncof.petitcoursability",
"pk": 8
},
{
"fields": {
"matiere": 2,
"niveau": "college",
"user": 43,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 9
},
{
"fields": {
"matiere": 2,
"niveau": "lycee",
"user": 43,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 10
},
{
"fields": {
"matiere": 3,
"niveau": "lycee",
"user": 48,
"agrege": true
},
"model": "gestioncof.petitcoursability",
"pk": 11
},
{
"fields": {
"matiere": 3,
"niveau": "prepa1styear",
"user": 48,
"agrege": true
},
"model": "gestioncof.petitcoursability",
"pk": 12
},
{
"fields": {
"matiere": 3,
"niveau": "prepa2ndyear",
"user": 48,
"agrege": true
},
"model": "gestioncof.petitcoursability",
"pk": 13
},
{
"fields": {
"matiere": 3,
"niveau": "licence3",
"user": 48,
"agrege": true
},
"model": "gestioncof.petitcoursability",
"pk": 14
},
{
"fields": {
"matiere": 4,
"niveau": "college",
"user": 10,
"agrege": false
},
"model": "gestioncof.petitcoursability",
"pk": 15
},
{ {
"fields": { "fields": {
"traitee": false, "traitee": false,

View file

@ -1,41 +0,0 @@
[
{
"fields": {
"username": "root",
"first_name": "super",
"last_name": "user",
"is_active": true,
"is_superuser": true,
"is_staff": true,
"last_login": null,
"groups": [],
"user_permissions": [],
"password": "pbkdf2_sha256$12000$yRpkPuayQ8De$h6bDe+Q4kMikzwEbLNw2I9/V/1/v3F3yLIjEZIFSHrY=",
"email": "root@localhost",
"date_joined": "2016-06-15T17:50:57Z"
},
"model": "auth.user",
"pk": 62
},
{
"fields": {
"departement": "",
"type_cotiz": "normalien",
"petits_cours_remarques": "",
"is_buro": true,
"is_cof": true,
"mailing_cof": true,
"comments": "Super utilisateur",
"login_clipper": "",
"phone": "",
"num": 62,
"mailing_bda_revente": true,
"user": 62,
"petits_cours_accept": false,
"mailing_bda": true,
"occupation": "1A"
},
"model": "gestioncof.cofprofile",
"pk": 62
}
]

File diff suppressed because it is too large Load diff

View file

@ -378,12 +378,12 @@ EventFormset = formset_factory(AdminEventForm, BaseEventRegistrationFormset)
class CalendarForm(forms.ModelForm): class CalendarForm(forms.ModelForm):
subscribe_to_events = forms.BooleanField( subscribe_to_events = forms.BooleanField(
initial=True, initial=True,
label="Événements du COF.") label="Événements du COF")
subscribe_to_my_shows = forms.BooleanField( subscribe_to_my_shows = forms.BooleanField(
initial=True, initial=True,
label="Les spectacles pour lesquels j'ai obtenu une place.") label="Les spectacles pour lesquels j'ai obtenu une place")
other_shows = forms.ModelMultipleChoiceField( other_shows = forms.ModelMultipleChoiceField(
label="Spectacles supplémentaires.", label="Spectacles supplémentaires",
queryset=Spectacle.objects.filter(tirage__active=True), queryset=Spectacle.objects.filter(tirage__active=True),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False) required=False)

View file

@ -0,0 +1,41 @@
"""
Un mixin à utiliser avec BaseCommand pour charger des objets depuis un json
"""
import os
import json
from django.core.management.base import BaseCommand
class MyBaseCommand(BaseCommand):
"""
Ajoute une méthode ``from_json`` qui charge des objets à partir d'un json.
"""
def from_json(self, filename, data_dir, klass,
callback=lambda obj: obj):
"""
Charge les objets contenus dans le fichier json référencé par
``filename`` dans la base de donnée. La fonction callback est appelées
sur chaque objet avant enregistrement.
"""
self.stdout.write("Chargement de {:s}".format(filename))
with open(os.path.join(data_dir, filename), 'r') as file:
descriptions = json.load(file)
objects = []
nb_new = 0
for description in descriptions:
qset = klass.objects.filter(**description)
try:
objects.append(qset.get())
except klass.DoesNotExist:
obj = klass(**description)
obj = callback(obj)
obj.save()
objects.append(obj)
nb_new += 1
self.stdout.write("- {:d} objets créés".format(nb_new))
self.stdout.write("- {:d} objets gardés en l'état"
.format(len(objects)-nb_new))
return objects

View file

@ -0,0 +1,109 @@
"""
Charge des données de test dans la BDD
- Utilisateurs
- Sondage
- Événement
- Petits cours
"""
import os
import random
from django.contrib.auth.models import User
from django.core.management import call_command
from gestioncof.management.base import MyBaseCommand
from gestioncof.petits_cours_models import (
PetitCoursAbility, PetitCoursSubject, LEVELS_CHOICES,
PetitCoursAttributionCounter
)
# Où sont stockés les fichiers json
DATA_DIR = os.path.join(os.path.dirname(os.path.dirname(__file__)),
'data')
class Command(MyBaseCommand):
help = "Charge des données de test dans la BDD"
def add_arguments(self, parser):
"""
Permet de ne pas créer l'utilisateur "root".
"""
parser.add_argument(
'--no-root',
action='store_true',
dest='no-root',
default=False,
help='Ne crée pas l\'utilisateur "root"'
)
def handle(self, *args, **options):
# ---
# Utilisateurs
# ---
# Gaulois
gaulois = self.from_json('gaulois.json', DATA_DIR, User)
for user in gaulois:
user.profile.is_cof = True
user.profile.save()
# Romains
self.from_json('romains.json', DATA_DIR, User)
# Root
no_root = options.get('no-root', False)
if not no_root:
self.stdout.write("Création de l'utilisateur root")
root, _ = User.objects.get_or_create(
username='root',
first_name='super',
last_name='user',
email='root@localhost')
root.set_password('root')
root.is_staff = True
root.is_superuser = True
root.profile.is_cof = True
root.profile.is_buro = True
root.profile.save()
root.save()
# ---
# Petits cours
# ---
self.stdout.write("Inscriptions au système des petits cours")
levels = [id for (id, verbose) in LEVELS_CHOICES]
subjects = list(PetitCoursSubject.objects.all())
nb_of_teachers = 0
for user in gaulois:
if random.randint(0, 1):
nb_of_teachers += 1
# L'utilisateur reçoit les demandes de petits cours
user.profile.petits_cours_accept = True
user.save()
# L'utilisateur est compétent dans une matière
subject = random.choice(subjects)
if not PetitCoursAbility.objects.filter(
user=user,
matiere=subject).exists():
PetitCoursAbility.objects.create(
user=user,
matiere=subject,
niveau=random.choice(levels),
agrege=bool(random.randint(0, 1))
)
# On initialise son compteur d'attributions
PetitCoursAttributionCounter.objects.get_or_create(
user=user,
matiere=subject
)
self.stdout.write("- {:d} inscriptions".format(nb_of_teachers))
# ---
# Le BdA
# ---
call_command('loadbdadevdata')

View file

@ -0,0 +1,368 @@
[
{
"username": "Abraracourcix",
"email": "Abraracourcix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Abraracourcix"
},
{
"username": "Acidenitrix",
"email": "Acidenitrix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Acidenitrix"
},
{
"username": "Agecanonix",
"email": "Agecanonix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Agecanonix"
},
{
"username": "Alambix",
"email": "Alambix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Alambix"
},
{
"username": "Amerix",
"email": "Amerix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Amerix"
},
{
"username": "Amnesix",
"email": "Amnesix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Amnesix"
},
{
"username": "Aniline",
"email": "Aniline.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Aniline"
},
{
"username": "Aplusbegalix",
"email": "Aplusbegalix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Aplusbegalix"
},
{
"username": "Archeopterix",
"email": "Archeopterix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Archeopterix"
},
{
"username": "Assurancetourix",
"email": "Assurancetourix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Assurancetourix"
},
{
"username": "Asterix",
"email": "Asterix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Asterix"
},
{
"username": "Astronomix",
"email": "Astronomix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Astronomix"
},
{
"username": "Avoranfix",
"email": "Avoranfix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Avoranfix"
},
{
"username": "Barometrix",
"email": "Barometrix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Barometrix"
},
{
"username": "Beaufix",
"email": "Beaufix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Beaufix"
},
{
"username": "Berlix",
"email": "Berlix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Berlix"
},
{
"username": "Bonemine",
"email": "Bonemine.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Bonemine"
},
{
"username": "Boufiltre",
"email": "Boufiltre.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Boufiltre"
},
{
"username": "Catedralgotix",
"email": "Catedralgotix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Catedralgotix"
},
{
"username": "CesarLabeldecadix",
"email": "CesarLabeldecadix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "CesarLabeldecadix"
},
{
"username": "Cetautomatix",
"email": "Cetautomatix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Cetautomatix"
},
{
"username": "Cetyounix",
"email": "Cetyounix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Cetyounix"
},
{
"username": "Changeledix",
"email": "Changeledix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Changeledix"
},
{
"username": "Chanteclairix",
"email": "Chanteclairix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Chanteclairix"
},
{
"username": "Cicatrix",
"email": "Cicatrix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Cicatrix"
},
{
"username": "Comix",
"email": "Comix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Comix"
},
{
"username": "Diagnostix",
"email": "Diagnostix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Diagnostix"
},
{
"username": "Doublepolemix",
"email": "Doublepolemix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Doublepolemix"
},
{
"username": "Eponine",
"email": "Eponine.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Eponine"
},
{
"username": "Falbala",
"email": "Falbala.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Falbala"
},
{
"username": "Fanzine",
"email": "Fanzine.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Fanzine"
},
{
"username": "Gelatine",
"email": "Gelatine.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Gelatine"
},
{
"username": "Goudurix",
"email": "Goudurix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Goudurix"
},
{
"username": "Homeopatix",
"email": "Homeopatix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Homeopatix"
},
{
"username": "Idefix",
"email": "Idefix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Idefix"
},
{
"username": "Ielosubmarine",
"email": "Ielosubmarine.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Ielosubmarine"
},
{
"username": "Keskonrix",
"email": "Keskonrix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Keskonrix"
},
{
"username": "Lentix",
"email": "Lentix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Lentix"
},
{
"username": "Maestria",
"email": "Maestria.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Maestria"
},
{
"username": "MaitrePanix",
"email": "MaitrePanix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "MaitrePanix"
},
{
"username": "MmeAgecanonix",
"email": "MmeAgecanonix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "MmeAgecanonix"
},
{
"username": "Moralelastix",
"email": "Moralelastix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Moralelastix"
},
{
"username": "Obelix",
"email": "Obelix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Obelix"
},
{
"username": "Obelodalix",
"email": "Obelodalix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Obelodalix"
},
{
"username": "Odalix",
"email": "Odalix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Odalix"
},
{
"username": "Ordralfabetix",
"email": "Ordralfabetix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Ordralfabetix"
},
{
"username": "Orthopedix",
"email": "Orthopedix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Orthopedix"
},
{
"username": "Panoramix",
"email": "Panoramix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Panoramix"
},
{
"username": "Plaintcontrix",
"email": "Plaintcontrix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Plaintcontrix"
},
{
"username": "Praline",
"email": "Praline.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Praline"
},
{
"username": "Prefix",
"email": "Prefix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Prefix"
},
{
"username": "Prolix",
"email": "Prolix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Prolix"
},
{
"username": "Pronostix",
"email": "Pronostix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Pronostix"
},
{
"username": "Quatredeusix",
"email": "Quatredeusix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Quatredeusix"
},
{
"username": "Saingesix",
"email": "Saingesix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Saingesix"
},
{
"username": "Segregationnix",
"email": "Segregationnix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Segregationnix"
},
{
"username": "Septantesix",
"email": "Septantesix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Septantesix"
},
{
"username": "Tournedix",
"email": "Tournedix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Tournedix"
},
{
"username": "Tragicomix",
"email": "Tragicomix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Tragicomix"
},
{
"username": "Coriza",
"email": "Coriza.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Coriza"
},
{
"username": "Zerozerosix",
"email": "Zerozerosix.gaulois@ens.fr",
"last_name": "Gaulois",
"first_name": "Zerozerosix"
}
]

View file

@ -0,0 +1,614 @@
[
{
"username": "Abel",
"email": "Abel.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Abel"
},
{
"username": "Abelardus",
"email": "Abelardus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Abelardus"
},
{
"username": "Abrahamus",
"email": "Abrahamus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Abrahamus"
},
{
"username": "Acacius",
"email": "Acacius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Acacius"
},
{
"username": "Accius",
"email": "Accius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Accius"
},
{
"username": "Achaicus",
"email": "Achaicus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Achaicus"
},
{
"username": "Achill",
"email": "Achill.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Achill"
},
{
"username": "Achilles",
"email": "Achilles.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Achilles"
},
{
"username": "Achilleus",
"email": "Achilleus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Achilleus"
},
{
"username": "Acrisius",
"email": "Acrisius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Acrisius"
},
{
"username": "Actaeon",
"email": "Actaeon.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Actaeon"
},
{
"username": "Acteon",
"email": "Acteon.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Acteon"
},
{
"username": "Adalricus",
"email": "Adalricus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adalricus"
},
{
"username": "Adelfonsus",
"email": "Adelfonsus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adelfonsus"
},
{
"username": "Adelphus",
"email": "Adelphus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adelphus"
},
{
"username": "Adeodatus",
"email": "Adeodatus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adeodatus"
},
{
"username": "Adolfus",
"email": "Adolfus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adolfus"
},
{
"username": "Adolphus",
"email": "Adolphus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adolphus"
},
{
"username": "Adrastus",
"email": "Adrastus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adrastus"
},
{
"username": "Adrianus",
"email": "Adrianus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Adrianus"
},
{
"username": "\u00c6gidius",
"email": "\u00c6gidius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6gidius"
},
{
"username": "\u00c6lia",
"email": "\u00c6lia.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6lia"
},
{
"username": "\u00c6lianus",
"email": "\u00c6lianus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6lianus"
},
{
"username": "\u00c6milianus",
"email": "\u00c6milianus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6milianus"
},
{
"username": "\u00c6milius",
"email": "\u00c6milius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6milius"
},
{
"username": "Aeneas",
"email": "Aeneas.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Aeneas"
},
{
"username": "\u00c6olus",
"email": "\u00c6olus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6olus"
},
{
"username": "\u00c6schylus",
"email": "\u00c6schylus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6schylus"
},
{
"username": "\u00c6son",
"email": "\u00c6son.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6son"
},
{
"username": "\u00c6sop",
"email": "\u00c6sop.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6sop"
},
{
"username": "\u00c6ther",
"email": "\u00c6ther.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6ther"
},
{
"username": "\u00c6tius",
"email": "\u00c6tius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "\u00c6tius"
},
{
"username": "Agapetus",
"email": "Agapetus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Agapetus"
},
{
"username": "Agapitus",
"email": "Agapitus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Agapitus"
},
{
"username": "Agapius",
"email": "Agapius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Agapius"
},
{
"username": "Agathangelus",
"email": "Agathangelus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Agathangelus"
},
{
"username": "Aigidius",
"email": "Aigidius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Aigidius"
},
{
"username": "Aiolus",
"email": "Aiolus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Aiolus"
},
{
"username": "Ajax",
"email": "Ajax.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Ajax"
},
{
"username": "Alair",
"email": "Alair.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alair"
},
{
"username": "Alaricus",
"email": "Alaricus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alaricus"
},
{
"username": "Albanus",
"email": "Albanus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Albanus"
},
{
"username": "Alberic",
"email": "Alberic.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alberic"
},
{
"username": "Albericus",
"email": "Albericus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Albericus"
},
{
"username": "Albertus",
"email": "Albertus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Albertus"
},
{
"username": "Albinus",
"email": "Albinus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Albinus"
},
{
"username": "Albus",
"email": "Albus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Albus"
},
{
"username": "Alcaeus",
"email": "Alcaeus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alcaeus"
},
{
"username": "Alcander",
"email": "Alcander.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alcander"
},
{
"username": "Alcimus",
"email": "Alcimus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alcimus"
},
{
"username": "Alcinder",
"email": "Alcinder.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alcinder"
},
{
"username": "Alerio",
"email": "Alerio.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alerio"
},
{
"username": "Alexandrus",
"email": "Alexandrus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alexandrus"
},
{
"username": "Alexis",
"email": "Alexis.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alexis"
},
{
"username": "Alexius",
"email": "Alexius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alexius"
},
{
"username": "Alexus",
"email": "Alexus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alexus"
},
{
"username": "Alfonsus",
"email": "Alfonsus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alfonsus"
},
{
"username": "Alfredus",
"email": "Alfredus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alfredus"
},
{
"username": "Almericus",
"email": "Almericus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Almericus"
},
{
"username": "Aloisius",
"email": "Aloisius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Aloisius"
},
{
"username": "Aloysius",
"email": "Aloysius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Aloysius"
},
{
"username": "Alphaeus",
"email": "Alphaeus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alphaeus"
},
{
"username": "Alpheaus",
"email": "Alpheaus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alpheaus"
},
{
"username": "Alpheus",
"email": "Alpheus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alpheus"
},
{
"username": "Alphoeus",
"email": "Alphoeus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alphoeus"
},
{
"username": "Alphonsus",
"email": "Alphonsus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alphonsus"
},
{
"username": "Alphonzus",
"email": "Alphonzus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alphonzus"
},
{
"username": "Alvinius",
"email": "Alvinius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alvinius"
},
{
"username": "Alvredus",
"email": "Alvredus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Alvredus"
},
{
"username": "Amadeus",
"email": "Amadeus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amadeus"
},
{
"username": "Amaliricus",
"email": "Amaliricus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amaliricus"
},
{
"username": "Amandus",
"email": "Amandus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amandus"
},
{
"username": "Amantius",
"email": "Amantius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amantius"
},
{
"username": "Amarandus",
"email": "Amarandus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amarandus"
},
{
"username": "Amaranthus",
"email": "Amaranthus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amaranthus"
},
{
"username": "Amatus",
"email": "Amatus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amatus"
},
{
"username": "Ambrosianus",
"email": "Ambrosianus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Ambrosianus"
},
{
"username": "Ambrosius",
"email": "Ambrosius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Ambrosius"
},
{
"username": "Amedeus",
"email": "Amedeus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amedeus"
},
{
"username": "Americus",
"email": "Americus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Americus"
},
{
"username": "Amlethus",
"email": "Amlethus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amlethus"
},
{
"username": "Amletus",
"email": "Amletus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amletus"
},
{
"username": "Amor",
"email": "Amor.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amor"
},
{
"username": "Ampelius",
"email": "Ampelius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Ampelius"
},
{
"username": "Amphion",
"email": "Amphion.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Amphion"
},
{
"username": "Anacletus",
"email": "Anacletus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Anacletus"
},
{
"username": "Anastasius",
"email": "Anastasius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Anastasius"
},
{
"username": "Anastatius",
"email": "Anastatius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Anastatius"
},
{
"username": "Anastius",
"email": "Anastius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Anastius"
},
{
"username": "Anatolius",
"email": "Anatolius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Anatolius"
},
{
"username": "Androcles",
"email": "Androcles.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Androcles"
},
{
"username": "Andronicus",
"email": "Andronicus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Andronicus"
},
{
"username": "Anencletus",
"email": "Anencletus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Anencletus"
},
{
"username": "Angelicus",
"email": "Angelicus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Angelicus"
},
{
"username": "Angelus",
"email": "Angelus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Angelus"
},
{
"username": "Anicetus",
"email": "Anicetus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Anicetus"
},
{
"username": "Antigonus",
"email": "Antigonus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Antigonus"
},
{
"username": "Antipater",
"email": "Antipater.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Antipater"
},
{
"username": "Antoninus",
"email": "Antoninus.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Antoninus"
},
{
"username": "Antonius",
"email": "Antonius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Antonius"
},
{
"username": "Aphrodisius",
"email": "Aphrodisius.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Aphrodisius"
},
{
"username": "Apollinaris",
"email": "Apollinaris.Romain@ens.fr",
"last_name": "Romain",
"first_name": "Apollinaris"
}
]

View file

@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0008_py3'),
]
operations = [
migrations.DeleteModel(
name='Clipper',
),
]

View file

@ -6,7 +6,7 @@ from django.db import migrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
('gestioncof', '0008_py3'), ('gestioncof', '0009_delete_clipper'),
] ]
operations = [ operations = [

View file

@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import User from django.contrib.auth.models import User
@ -248,15 +244,6 @@ class SurveyAnswer(models.Model):
self.survey.title) self.survey.title)
@python_2_unicode_compatible
class Clipper(models.Model):
username = models.CharField("Identifiant", max_length=20)
fullname = models.CharField("Nom complet", max_length=200)
def __str__(self):
return "Clipper %s" % self.username
@python_2_unicode_compatible @python_2_unicode_compatible
class CalendarSubscription(models.Model): class CalendarSubscription(models.Model):
token = models.UUIDField() token = models.UUIDField()

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
from captcha.fields import ReCaptchaField
from django import forms
from django.forms import ModelForm
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.auth.models import User
from gestioncof.petits_cours_models import PetitCoursDemande, PetitCoursAbility
class BaseMatieresFormSet(BaseInlineFormSet):
def clean(self):
super(BaseMatieresFormSet, self).clean()
if any(self.errors):
# Don't bother validating the formset unless each form is
# valid on its own
return
matieres = []
for i in range(0, self.total_form_count()):
form = self.forms[i]
if not form.cleaned_data:
continue
matiere = form.cleaned_data['matiere']
niveau = form.cleaned_data['niveau']
delete = form.cleaned_data['DELETE']
if not delete and (matiere, niveau) in matieres:
raise forms.ValidationError(
"Vous ne pouvez pas vous inscrire deux fois pour la "
"même matiere avec le même niveau.")
matieres.append((matiere, niveau))
class DemandeForm(ModelForm):
captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})
def __init__(self, *args, **kwargs):
super(DemandeForm, self).__init__(*args, **kwargs)
self.fields['matieres'].help_text = ''
class Meta:
model = PetitCoursDemande
fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu',
'matieres', 'agrege_requis', 'niveau', 'remarques')
widgets = {'matieres': forms.CheckboxSelectMultiple}
MatieresFormSet = inlineformset_factory(
User,
PetitCoursAbility,
fields=("matiere", "niveau", "agrege"),
formset=BaseMatieresFormSet
)

View file

@ -1,14 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division from functools import reduce
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models from django.db import models
from django.db.models import Min
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
from django.utils.six.moves import reduce
def choices_length(choices): def choices_length(choices):
@ -24,7 +21,6 @@ LEVELS_CHOICES = (
) )
@python_2_unicode_compatible
class PetitCoursSubject(models.Model): class PetitCoursSubject(models.Model):
name = models.CharField(_("Matière"), max_length=30) name = models.CharField(_("Matière"), max_length=30)
users = models.ManyToManyField(User, related_name="petits_cours_matieres", users = models.ManyToManyField(User, related_name="petits_cours_matieres",
@ -38,7 +34,6 @@ class PetitCoursSubject(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class PetitCoursAbility(models.Model): class PetitCoursAbility(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière")) matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
@ -52,11 +47,11 @@ class PetitCoursAbility(models.Model):
verbose_name_plural = "Compétences des petits cours" verbose_name_plural = "Compétences des petits cours"
def __str__(self): def __str__(self):
return "%s - %s - %s" % (self.user.username, return "{:s} - {!s} - {:s}".format(
self.matiere, self.niveau) self.user.username, self.matiere, self.niveau
)
@python_2_unicode_compatible
class PetitCoursDemande(models.Model): class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200) name = models.CharField(_("Nom/prénom"), max_length=200)
email = models.CharField(_("Adresse email"), max_length=300) email = models.CharField(_("Adresse email"), max_length=300)
@ -70,7 +65,7 @@ class PetitCoursDemande(models.Model):
freq = models.CharField( freq = models.CharField(
_("Fréquence"), _("Fréquence"),
help_text=_("Indiquez ici la fréquence envisagée " help_text=_("Indiquez ici la fréquence envisagée "
+ "(hebdomadaire, 2 fois par semaine, ...)"), "(hebdomadaire, 2 fois par semaine, ...)"),
max_length=300, blank=True) max_length=300, blank=True)
lieu = models.CharField( lieu = models.CharField(
_("Lieu (si préférence)"), _("Lieu (si préférence)"),
@ -94,16 +89,42 @@ class PetitCoursDemande(models.Model):
blank=True, null=True) blank=True, null=True)
created = models.DateTimeField(_("Date de création"), auto_now_add=True) created = models.DateTimeField(_("Date de création"), auto_now_add=True)
def get_candidates(self, redo=False):
"""
Donne la liste des profs disponibles pour chaque matière de la demande.
- On ne donne que les agrégés si c'est demandé
- Si ``redo`` vaut ``True``, cela signifie qu'on retraite la demande et
il ne faut pas proposer à nouveau des noms qui ont déjà été proposés
"""
for matiere in self.matieres.all():
candidates = PetitCoursAbility.objects.filter(
matiere=matiere,
niveau=self.niveau,
user__profile__is_cof=True,
user__profile__petits_cours_accept=True
)
if self.agrege_requis:
candidates = candidates.filter(agrege=True)
if redo:
attrs = self.petitcoursattribution_set.filter(matiere=matiere)
already_proposed = [
attr.user
for attr in attrs
]
candidates = candidates.exclude(user__in=already_proposed)
candidates = candidates.order_by('?').select_related().all()
yield (matiere, candidates)
class Meta: class Meta:
verbose_name = "Demande de petits cours" verbose_name = "Demande de petits cours"
verbose_name_plural = "Demandes de petits cours" verbose_name_plural = "Demandes de petits cours"
def __str__(self): def __str__(self):
return "Demande %d du %s" % (self.id, return "Demande {:d} du {:s}".format(
self.created.strftime("%d %b %Y")) self.id, self.created.strftime("%d %b %Y")
)
@python_2_unicode_compatible
class PetitCoursAttribution(models.Model): class PetitCoursAttribution(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande")) demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande"))
@ -118,20 +139,40 @@ class PetitCoursAttribution(models.Model):
verbose_name_plural = "Attributions de petits cours" verbose_name_plural = "Attributions de petits cours"
def __str__(self): def __str__(self):
return "Attribution de la demande %d à %s pour %s" \ return "Attribution de la demande {:d} à {:s} pour {!s}".format(
% (self.demande.id, self.user.username, self.matiere) self.demande.id, self.user.username, self.matiere
)
@python_2_unicode_compatible
class PetitCoursAttributionCounter(models.Model): class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere")) matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere"))
count = models.IntegerField("Nombre d'envois", default=0) count = models.IntegerField("Nombre d'envois", default=0)
@classmethod
def get_uptodate(cls, user, matiere):
"""
Donne le compteur de l'utilisateur pour cette matière. Si le compteur
n'existe pas encore, il est initialisé avec le minimum des valeurs des
compteurs de tout le monde.
"""
counter, created = cls.objects.get_or_create(
user=user, matiere=matiere)
if created:
mincount = (
cls.objects.filter(matiere=matiere).exclude(user=user)
.aggregate(Min('count'))
['count__min']
)
counter.count = mincount
counter.save()
return counter
class Meta: class Meta:
verbose_name = "Compteur d'attribution de petits cours" verbose_name = "Compteur d'attribution de petits cours"
verbose_name_plural = "Compteurs d'attributions de petits cours" verbose_name_plural = "Compteurs d'attributions de petits cours"
def __str__(self): def __str__(self):
return "%d demandes envoyées à %s pour %s" \ return "{:d} demandes envoyées à {:s} pour {!s}".format(
% (self.count, self.user.username, self.matiere) self.count, self.user.username, self.matiere
)

View file

@ -1,28 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import base64
import json import json
from datetime import datetime from datetime import datetime
from captcha.fields import ReCaptchaField
from custommail.shortcuts import render_custom_mail from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail from django.core import mail
from django.forms import ModelForm
from django import forms
from django.forms.models import inlineformset_factory, BaseInlineFormSet
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.views.generic import ListView from django.views.generic import ListView, DetailView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db.models import Min
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import PetitCoursDemande, \ from gestioncof.petits_cours_models import (
PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursAbility, \ PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursSubject PetitCoursAbility, PetitCoursSubject
)
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables from gestioncof.shared import lock_table, unlock_tables
@ -35,47 +30,17 @@ class DemandeListView(ListView):
def get_queryset(self): def get_queryset(self):
return PetitCoursDemande.objects.order_by('traitee', '-id').all() return PetitCoursDemande.objects.order_by('traitee', '-id').all()
@method_decorator(buro_required)
def dispatch(self, *args, **kwargs):
return super(DemandeListView, self).dispatch(*args, **kwargs)
class DemandeDetailView(DetailView):
model = PetitCoursDemande
template_name = "gestioncof/details_demande_petit_cours.html"
context_object_name = "demande"
@buro_required def get_context_data(self, **kwargs):
def details(request, demande_id): context = super(DemandeDetailView, self).get_context_data(**kwargs)
demande = get_object_or_404(PetitCoursDemande, id=demande_id) obj = self.object
attributions = PetitCoursAttribution.objects.filter(demande=demande).all() context['attributions'] = obj.petitcoursattribution_set.all()
return render(request, "details_demande_petit_cours.html", return context
{"demande": demande,
"attributions": attributions})
def _get_attrib_counter(user, matiere):
counter, created = PetitCoursAttributionCounter \
.objects.get_or_create(user=user, matiere=matiere)
if created:
mincount = PetitCoursAttributionCounter.objects \
.filter(matiere=matiere).exclude(user=user).all() \
.aggregate(Min('count'))
counter.count = mincount['count__min']
counter.save()
return counter
def _get_demande_candidates(demande, redo=False):
for matiere in demande.matieres.all():
candidates = PetitCoursAbility.objects.filter(matiere=matiere,
niveau=demande.niveau)
candidates = candidates.filter(user__profile__is_cof=True,
user__profile__petits_cours_accept=True)
if demande.agrege_requis:
candidates = candidates.filter(agrege=True)
if redo:
attributions = PetitCoursAttribution.objects \
.filter(demande=demande, matiere=matiere).all()
for attrib in attributions:
candidates = candidates.exclude(user=attrib.user)
candidates = candidates.order_by('?').select_related().all()
yield (matiere, candidates)
@buro_required @buro_required
@ -89,12 +54,15 @@ def traitement(request, demande_id, redo=False):
proposed_for = {} proposed_for = {}
unsatisfied = [] unsatisfied = []
attribdata = {} attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
tuples = [] tuples = []
for candidate in candidates: for candidate in candidates:
user = candidate.user user = candidate.user
tuples.append((candidate, _get_attrib_counter(user, matiere))) tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count) tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples) candidates, _ = zip(*tuples)
candidates = candidates[0:min(3, len(candidates))] candidates = candidates[0:min(3, len(candidates))]
@ -166,7 +134,7 @@ def _traitement_other_preparing(request, demande):
proposed_for = {} proposed_for = {}
attribdata = {} attribdata = {}
errors = [] errors = []
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
candidates = dict([(candidate.user.id, candidate.user) candidates = dict([(candidate.user.id, candidate.user)
for candidate in candidates]) for candidate in candidates])
@ -174,17 +142,19 @@ def _traitement_other_preparing(request, demande):
proposals[matiere] = [] proposals[matiere] = []
for choice_id in range(min(3, len(candidates))): for choice_id in range(min(3, len(candidates))):
choice = int( choice = int(
request.POST["proposal-%d-%d" % (matiere.id, choice_id)]) request.POST["proposal-{:d}-{:d}"
.format(matiere.id, choice_id)]
)
if choice == -1: if choice == -1:
continue continue
if choice not in candidates: if choice not in candidates:
errors.append("Choix invalide pour la proposition %d" errors.append("Choix invalide pour la proposition {:d}"
"en %s" % (choice_id + 1, matiere)) "en {!s}".format(choice_id + 1, matiere))
continue continue
user = candidates[choice] user = candidates[choice]
if user in proposals[matiere]: if user in proposals[matiere]:
errors.append("La proposition %d en %s est un doublon" errors.append("La proposition {:d} en {!s} est un doublon"
% (choice_id + 1, matiere)) .format(choice_id + 1, matiere))
continue continue
proposals[matiere].append(user) proposals[matiere].append(user)
attribdata[matiere.id].append(user.id) attribdata[matiere.id].append(user.id)
@ -193,12 +163,13 @@ def _traitement_other_preparing(request, demande):
else: else:
proposed_for[user].append(matiere) proposed_for[user].append(matiere)
if not proposals[matiere]: if not proposals[matiere]:
errors.append("Aucune proposition pour %s" % (matiere,)) errors.append("Aucune proposition pour {!s}".format(matiere))
elif len(proposals[matiere]) < 3: elif len(proposals[matiere]) < 3:
errors.append("Seulement %d proposition%s pour %s" errors.append("Seulement {:d} proposition{:s} pour {!s}"
% (len(proposals[matiere]), .format(
"s" if len(proposals[matiere]) > 1 else "", len(proposals[matiere]),
matiere)) "s" if len(proposals[matiere]) > 1 else "",
matiere))
else: else:
unsatisfied.append(matiere) unsatisfied.append(matiere)
return _finalize_traitement(request, demande, proposals, proposed_for, return _finalize_traitement(request, demande, proposals, proposed_for,
@ -215,12 +186,15 @@ def _traitement_other(request, demande, redo):
proposed_for = {} proposed_for = {}
unsatisfied = [] unsatisfied = []
attribdata = {} attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo): for matiere, candidates in demande.get_candidates(redo):
if candidates: if candidates:
tuples = [] tuples = []
for candidate in candidates: for candidate in candidates:
user = candidate.user user = candidate.user
tuples.append((candidate, _get_attrib_counter(user, matiere))) tuples.append((
candidate,
PetitCoursAttributionCounter.get_uptodate(user, matiere)
))
tuples = sorted(tuples, key=lambda c: c[1].count) tuples = sorted(tuples, key=lambda c: c[1].count)
candidates, _ = zip(*tuples) candidates, _ = zip(*tuples)
attribdata[matiere.id] = [] attribdata[matiere.id] = []
@ -309,37 +283,11 @@ def _traitement_post(request, demande):
}) })
class BaseMatieresFormSet(BaseInlineFormSet):
def clean(self):
super(BaseMatieresFormSet, self).clean()
if any(self.errors):
# Don't bother validating the formset unless each form is
# valid on its own
return
matieres = []
for i in range(0, self.total_form_count()):
form = self.forms[i]
if not form.cleaned_data:
continue
matiere = form.cleaned_data['matiere']
niveau = form.cleaned_data['niveau']
delete = form.cleaned_data['DELETE']
if not delete and (matiere, niveau) in matieres:
raise forms.ValidationError(
"Vous ne pouvez pas vous inscrire deux fois pour la "
"même matiere avec le même niveau.")
matieres.append((matiere, niveau))
@login_required @login_required
def inscription(request): def inscription(request):
profile, created = CofProfile.objects.get_or_create(user=request.user) profile, created = CofProfile.objects.get_or_create(user=request.user)
if not profile.is_cof: if not profile.is_cof:
return redirect("cof-denied") return redirect("cof-denied")
MatieresFormSet = inlineformset_factory(User, PetitCoursAbility,
fields=("matiere", "niveau",
"agrege",),
formset=BaseMatieresFormSet)
success = False success = False
if request.method == "POST": if request.method == "POST":
formset = MatieresFormSet(request.POST, instance=request.user) formset = MatieresFormSet(request.POST, instance=request.user)
@ -350,10 +298,14 @@ def inscription(request):
profile.save() profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User, lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject) PetitCoursSubject)
abilities = PetitCoursAbility.objects \ abilities = (
.filter(user=request.user).all() PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities: for ability in abilities:
_get_attrib_counter(ability.user, ability.matiere) PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
unlock_tables() unlock_tables()
success = True success = True
formset = MatieresFormSet(instance=request.user) formset = MatieresFormSet(instance=request.user)
@ -365,20 +317,6 @@ def inscription(request):
"remarques": profile.petits_cours_remarques}) "remarques": profile.petits_cours_remarques})
class DemandeForm(ModelForm):
captcha = ReCaptchaField(attrs={'theme': 'clean', 'lang': 'fr'})
def __init__(self, *args, **kwargs):
super(DemandeForm, self).__init__(*args, **kwargs)
self.fields['matieres'].help_text = ''
class Meta:
model = PetitCoursDemande
fields = ('name', 'email', 'phone', 'quand', 'freq', 'lieu',
'matieres', 'agrege_requis', 'niveau', 'remarques')
widgets = {'matieres': forms.CheckboxSelectMultiple}
@csrf_exempt @csrf_exempt
def demande(request): def demande(request):
success = False success = False

View file

@ -1088,3 +1088,8 @@ tr.awesome{
color: white; color: white;
padding: 20px; padding: 20px;
} }
.petitcours-raw {
padding:20px;
background:#fff;
}

File diff suppressed because one or more lines are too long

13
gestioncof/static/js/jquery-ui.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,11 @@
/*!
* jQuery UI Touch Punch 0.2.3
*
* Copyright 20112014, Dave Furfero
* Dual licensed under the MIT or GPL Version 2 licenses.
*
* Depends:
* jquery.ui.widget.js
* jquery.ui.mouse.js
*/
!function(a){function f(a,b){if(!(a.originalEvent.touches.length>1)){a.preventDefault();var c=a.originalEvent.changedTouches[0],d=document.createEvent("MouseEvents");d.initMouseEvent(b,!0,!0,window,1,c.screenX,c.screenY,c.clientX,c.clientY,!1,!1,!1,!1,0,null),a.target.dispatchEvent(d)}}if(a.support.touch="ontouchend"in document,a.support.touch){var e,b=a.ui.mouse.prototype,c=b._mouseInit,d=b._mouseDestroy;b._touchStart=function(a){var b=this;!e&&b._mouseCapture(a.originalEvent.changedTouches[0])&&(e=!0,b._touchMoved=!1,f(a,"mouseover"),f(a,"mousemove"),f(a,"mousedown"))},b._touchMove=function(a){e&&(this._touchMoved=!0,f(a,"mousemove"))},b._touchEnd=function(a){e&&(f(a,"mouseup"),f(a,"mouseout"),this._touchMoved||f(a,"click"),e=!1)},b._mouseInit=function(){var b=this;b.element.bind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),c.call(b)},b._mouseDestroy=function(){var b=this;b.element.unbind({touchstart:a.proxy(b,"_touchStart"),touchmove:a.proxy(b,"_touchMove"),touchend:a.proxy(b,"_touchEnd")}),d.call(b)}}}(jQuery);

View file

@ -15,7 +15,7 @@
{% if clippers %} {% if clippers %}
<li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li> <li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li>
{% for clipper in clippers %}{% if forloop.counter < 5 %} {% for clipper in clippers %}{% if forloop.counter < 5 %}
<li class="autocomplete-value"><a href="{% url 'clipper-registration' clipper.username %}">{{ clipper|highlight_clipper:q }}</a></li> <li class="autocomplete-value"><a href="{% url 'clipper-registration' clipper.clipper clipper.fullname %}">{{ clipper|highlight_clipper:q }}</a></li>
{% elif forloop.counter == 5 %}<li class="autocomplete-more">...</a>{% endif %}{% endfor %} {% elif forloop.counter == 5 %}<li class="autocomplete-more">...</a>{% endif %}{% endfor %}
{% endif %} {% endif %}

View file

@ -1,11 +1,19 @@
{% extends "base.html" %}
{% load bootstrap %}
{% block content %}
<div class="petitcours-raw">
{% if success %} {% if success %}
<p class="success">Votre demande a été enregistrée avec succès !</p> <p class="success">Votre demande a été enregistrée avec succès !</p>
{% else %} {% else %}
<form id="demandecours" method="post" action="{% url "gestioncof.petits_cours_views.demande_raw" %}"> <form id="demandecours" method="post" action="{% url "gestioncof.petits_cours_views.demande_raw" %}">
{% csrf_token %} {% csrf_token %}
<table> <table>
{{ form.as_table }} {{ form | bootstrap }}
</table> </table>
<input type="submit" class="btn-submit" value="Enregistrer" /> <input type="submit" class="btn-submit" value="Enregistrer" />
</form> </form>
{% endif %} {% endif %}
</div>
{% endblock %}

View file

@ -1,4 +1,5 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %} {% block realcontent %}
@ -36,8 +37,21 @@ souscrire aux événements du COF et/ou aux spectacles BdA.
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
{{ form.as_p }} {{ form | bootstrap }}
<input type="submit" value="Enregistrer" class="btn btn-primary" /> <p>
<button type="button" class="btn btn-default" onClick="select(true)">Tout sélectionner</button>
<button type="button" class="btn btn-default" onClick="select(false)">Tout désélectionner</button>
</p>
<input type="submit" value="Enregistrer" class="btn btn-primary center-block" />
</form> </form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("other_shows");
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %} {% endblock %}

View file

@ -17,4 +17,8 @@
<li><a href="{% url 'gestioncof.views.export_mega_orgas' %}">Export des orgas uniquement</a></li> <li><a href="{% url 'gestioncof.views.export_mega_orgas' %}">Export des orgas uniquement</a></li>
<li><a href="{% url 'gestioncof.views.export_mega' %}">Export de tout le monde</a></li> <li><a href="{% url 'gestioncof.views.export_mega' %}">Export de tout le monde</a></li>
</ul> </ul>
<p>Note&nbsp;: pour ouvrir les fichiers .csv avec Excel, il faut
passer par <tt>Fichier &gt; Importer</tt> et sélectionner la
virgule comme séparateur.</p>
{% endblock %} {% endblock %}

View file

@ -2,11 +2,11 @@
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link href="{% static "grappelli/jquery/ui/css/custom-theme/jquery-ui-1.8.custom.css" %}" rel="stylesheet" type="text/css" media="screen" title="no title" charset="utf-8" /> <script src="{% static "js/jquery.min.js" %}" type="text/javascript"></script>
<script src="{% static "grappelli/jquery/jquery-1.6.2.min.js" %}" type="text/javascript"></script> <script src="{% static "js/jquery-ui.min.js" %}" type="text/javascript"></script>
<script src="{% static "grappelli/jquery/ui/js/jquery-ui-1.8.15.custom.min.js" %}" type="text/javascript"></script> <script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link href="{% static "grappelli/css/tools.css" %}" rel="stylesheet" type="text/css" /> <link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
<link href="{% static "grappelli/css/jquery-ui-grappelli-extensions.css" %}" rel="stylesheet" type="text/css" />
{% endblock %} {% endblock %}
{% block realcontent %} {% block realcontent %}

View file

@ -18,7 +18,7 @@
$(document).ready(function() { $(document).ready(function() {
$('input#search_autocomplete').yourlabsAutocomplete({ $('input#search_autocomplete').yourlabsAutocomplete({
url: '{% url 'gestioncof.autocomplete.autocomplete' %}', url: '{% url 'gestioncof.autocomplete.autocomplete' %}',
minimumCharacters: 1, minimumCharacters: 3,
id: 'search_autocomplete', id: 'search_autocomplete',
choiceSelector: 'li:has(a)', choiceSelector: 'li:has(a)',
placeholder: "Chercher un utilisateur par nom, prénom ou identifiant clipper", placeholder: "Chercher un utilisateur par nom, prénom ou identifiant clipper",

View file

@ -43,7 +43,7 @@ def highlight_user(user, q):
@register.filter @register.filter
def highlight_clipper(clipper, q): def highlight_clipper(clipper, q):
if clipper.fullname: if clipper.fullname:
text = "%s (<tt>%s</tt>)" % (clipper.fullname, clipper.username) text = "%s (<tt>%s</tt>)" % (clipper.fullname, clipper.clipper)
else: else:
text = clipper.username text = clipper.clipper
return highlight_text(text, q) return highlight_text(text, q)

View file

@ -1,12 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.conf.urls import url from django.conf.urls import url
from gestioncof.petits_cours_views import DemandeListView from gestioncof.petits_cours_views import DemandeListView, DemandeDetailView
from gestioncof import views, petits_cours_views from gestioncof import views, petits_cours_views
from gestioncof.decorators import buro_required
export_patterns = [ export_patterns = [
url(r'^members$', views.export_members), url(r'^members$', views.export_members),
@ -24,10 +21,11 @@ petitcours_patterns = [
name='petits-cours-demande'), name='petits-cours-demande'),
url(r'^demande-raw$', petits_cours_views.demande_raw, url(r'^demande-raw$', petits_cours_views.demande_raw,
name='petits-cours-demande-raw'), name='petits-cours-demande-raw'),
url(r'^demandes$', DemandeListView.as_view(), url(r'^demandes$',
buro_required(DemandeListView.as_view()),
name='petits-cours-demandes-list'), name='petits-cours-demandes-list'),
url(r'^demandes/(?P<demande_id>\d+)$', url(r'^demandes/(?P<pk>\d+)$',
petits_cours_views.details, buro_required(DemandeDetailView.as_view()),
name='petits-cours-demande-details'), name='petits-cours-demande-details'),
url(r'^demandes/(?P<demande_id>\d+)/traitement$', url(r'^demandes/(?P<demande_id>\d+)/traitement$',
petits_cours_views.traitement, petits_cours_views.traitement,

View file

@ -1,9 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import unicodecsv import unicodecsv
import uuid import uuid
from datetime import timedelta from datetime import timedelta
@ -25,7 +21,7 @@ from gestioncof.models import Event, EventRegistration, EventOption, \
EventOptionChoice EventOptionChoice
from gestioncof.models import EventCommentField, EventCommentValue, \ from gestioncof.models import EventCommentField, EventCommentValue, \
CalendarSubscription CalendarSubscription
from gestioncof.models import CofProfile, Clipper, Club from gestioncof.models import CofProfile, Club
from gestioncof.decorators import buro_required, cof_required from gestioncof.decorators import buro_required, cof_required
from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \
SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \
@ -321,11 +317,11 @@ def registration_set_ro_fields(user_form, profile_form):
@buro_required @buro_required
def registration_form2(request, login_clipper=None, username=None): def registration_form2(request, login_clipper=None, username=None,
fullname=None):
events = Event.objects.filter(old=False).all() events = Event.objects.filter(old=False).all()
member = None member = None
if login_clipper: if login_clipper:
clipper = get_object_or_404(Clipper, username=login_clipper)
try: # check if the given user is already registered try: # check if the given user is already registered
member = User.objects.get(username=login_clipper) member = User.objects.get(username=login_clipper)
username = member.username username = member.username
@ -336,8 +332,8 @@ def registration_form2(request, login_clipper=None, username=None):
user_form = RegistrationUserForm(initial={ user_form = RegistrationUserForm(initial={
'username': login_clipper, 'username': login_clipper,
'email': "%s@clipper.ens.fr" % login_clipper}) 'email': "%s@clipper.ens.fr" % login_clipper})
if clipper.fullname: if fullname:
bits = clipper.fullname.split(" ") bits = fullname.split(" ")
user_form.fields['first_name'].initial = bits[0] user_form.fields['first_name'].initial = bits[0]
if len(bits) > 1: if len(bits) > 1:
user_form.fields['last_name'].initial = " ".join(bits[1:]) user_form.fields['last_name'].initial = " ".join(bits[1:])
@ -412,12 +408,12 @@ def registration(request):
try: try:
member = User.objects.get(username=username) member = User.objects.get(username=username)
user_form = RegistrationUserForm(request_dict, instance=member) user_form = RegistrationUserForm(request_dict, instance=member)
except User.DoesNotExist: if member.profile.login_clipper:
try: login_clipper = member.profile.login_clipper
clipper = Clipper.objects.get(username=username) else:
login_clipper = clipper.username
except Clipper.DoesNotExist:
user_form.force_long_username() user_form.force_long_username()
except User.DoesNotExist:
user_form.force_long_username()
else: else:
user_form.force_long_username() user_form.force_long_username()
@ -644,7 +640,7 @@ def export_mega(request):
@buro_required @buro_required
def utile_cof(request): def utile_cof(request):
return render(request, "utile_cof.html", {}) return render(request, "gestioncof/utile_cof.html", {})
@buro_required @buro_required
@ -693,15 +689,15 @@ def calendar(request):
subscription.token = uuid.uuid4() subscription.token = uuid.uuid4()
subscription.save() subscription.save()
form.save_m2m() form.save_m2m()
return render(request, "calendar_subscription.html", return render(request, "gestioncof/calendar_subscription.html",
{'form': form, {'form': form,
'success': True, 'success': True,
'token': str(subscription.token)}) 'token': str(subscription.token)})
else: else:
return render(request, "calendar_subscription.html", return render(request, "gestioncof/calendar_subscription.html",
{'form': form, 'error': "Formulaire incorrect"}) {'form': form, 'error': "Formulaire incorrect"})
else: else:
return render(request, "calendar_subscription.html", return render(request, "gestioncof/calendar_subscription.html",
{'form': CalendarForm(instance=instance), {'form': CalendarForm(instance=instance),
'token': instance.token if instance else None}) 'token': instance.token if instance else None})

View file

@ -1,16 +1,23 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, import ldap3
print_function, unicode_literals)
from builtins import *
from django.shortcuts import render from django.shortcuts import render
from django.http import Http404 from django.http import Http404
from django.db.models import Q from django.db.models import Q
from gestioncof.models import User, Clipper from django.conf import settings
from gestioncof.models import User
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.models import Account from kfet.models import Account
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@teamkfet_required @teamkfet_required
def account_create(request): def account_create(request):
if "q" not in request.GET: if "q" not in request.GET:
@ -25,58 +32,67 @@ def account_create(request):
queries = {} queries = {}
search_words = q.split() search_words = q.split()
# Fetching data from User, CofProfile and Account tables
queries['kfet'] = Account.objects queries['kfet'] = Account.objects
queries['users_cof'] = User.objects.filter(Q(profile__is_cof = True)) queries['users_cof'] = User.objects.filter(profile__is_cof = True)
queries['users_notcof'] = User.objects.filter(Q(profile__is_cof = False)) queries['users_notcof'] = User.objects.filter(profile__is_cof = False)
queries['clippers'] = Clipper.objects
for word in search_words: for word in search_words:
queries['kfet'] = queries['kfet'].filter( queries['kfet'] = queries['kfet'].filter(
Q(cofprofile__user__username__icontains = word) Q(cofprofile__user__username__icontains = word)
| Q(cofprofile__user__first_name__icontains = word) | Q(cofprofile__user__first_name__icontains = word)
| Q(cofprofile__user__last_name__icontains = word) | Q(cofprofile__user__last_name__icontains = word)
) )
queries['users_cof'] = queries['users_cof'].filter( queries['users_cof'] = queries['users_cof'].filter(
Q(username__icontains = word) Q(username__icontains = word)
| Q(first_name__icontains = word) | Q(first_name__icontains = word)
| Q(last_name__icontains = word) | Q(last_name__icontains = word)
) )
queries['users_notcof'] = queries['users_notcof'].filter( queries['users_notcof'] = queries['users_notcof'].filter(
Q(username__icontains = word) Q(username__icontains = word)
| Q(first_name__icontains = word) | Q(first_name__icontains = word)
| Q(last_name__icontains = word) | Q(last_name__icontains = word)
) )
queries['clippers'] = queries['clippers'].filter(
Q(username__icontains = word)
| Q(fullname__icontains = word)
)
# Clearing redundancies
queries['kfet'] = queries['kfet'].distinct() queries['kfet'] = queries['kfet'].distinct()
usernames = set(
usernames = list( \
queries['kfet'].values_list('cofprofile__user__username', flat=True)) queries['kfet'].values_list('cofprofile__user__username', flat=True))
queries['kfet'] = [
(account, account.cofprofile.user)
for account in queries['kfet']
]
queries['kfet'] = [ (account, account.cofprofile.user) \ queries['users_cof'] = \
for account in queries['kfet'] ]
queries['users_cof'] = \
queries['users_cof'].exclude(username__in=usernames).distinct() queries['users_cof'].exclude(username__in=usernames).distinct()
queries['users_notcof'] = \ queries['users_notcof'] = \
queries['users_notcof'].exclude(username__in=usernames).distinct() queries['users_notcof'].exclude(username__in=usernames).distinct()
usernames |= set(
usernames += list( \
queries['users_cof'].values_list('username', flat=True)) queries['users_cof'].values_list('username', flat=True))
usernames += list( \ usernames |= set(
queries['users_notcof'].values_list('username', flat=True)) queries['users_notcof'].values_list('username', flat=True))
queries['clippers'] = \ # Fetching data from the SPI
queries['clippers'].exclude(username__in=usernames).distinct() if hasattr(settings, 'LDAP_SERVER_URL'):
# Fetching
ldap_query = '(|{:s})'.format(''.join(
['(cn=*{bit:s}*)(uid=*{bit:s}*)'.format(**{"bit": bit}) for bit in bits]
))
with Connection(settings.LDAP_SERVER_URL) as conn:
conn.search(
'dc=spi,dc=ens,dc=fr', ldap_query,
attributes=['uid', 'cn']
)
queries['clippers'] = conn.entries
# Clearing redundancies
queries['clippers'] = [
Clipper(clipper.uid, clipper.cn)
for clipper in queries['clippers']
if str(clipper.uid) not in usernames
]
# Resulting data
data.update(queries) data.update(queries)
data['options'] = sum([len(query) for query in queries])
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
return render(request, "kfet/account_create_autocomplete.html", data) return render(request, "kfet/account_create_autocomplete.html", data)

View file

@ -36,7 +36,7 @@
<li class="user_category"><span class="text">Utilisateurs clipper</span></li> <li class="user_category"><span class="text">Utilisateurs clipper</span></li>
{% for clipper in clippers %} {% for clipper in clippers %}
<li> <li>
<a href="{% url "kfet.account.create.fromclipper" clipper.username %}"> <a href="{% url "kfet.account.create.fromclipper" clipper.clipper clipper.fullname%}">
{{ clipper|highlight_clipper:q }} {{ clipper|highlight_clipper:q }}
</a> </a>
</li> </li>

View file

@ -35,7 +35,8 @@ urlpatterns = [
name = 'kfet.account.create_special'), name = 'kfet.account.create_special'),
url(r'^accounts/new/user/(?P<username>.+)$', views.account_create_ajax, url(r'^accounts/new/user/(?P<username>.+)$', views.account_create_ajax,
name = 'kfet.account.create.fromuser'), name = 'kfet.account.create.fromuser'),
url(r'^accounts/new/clipper/(?P<login_clipper>.+)$', views.account_create_ajax, url(r'^accounts/new/clipper/(?P<login_clipper>[\w-]+)/(?P<fullname>.*)$',
views.account_create_ajax,
name = 'kfet.account.create.fromclipper'), name = 'kfet.account.create.fromclipper'),
url(r'^accounts/new/empty$', views.account_create_ajax, url(r'^accounts/new/empty$', views.account_create_ajax,
name = 'kfet.account.create.empty'), name = 'kfet.account.create.empty'),

View file

@ -22,7 +22,7 @@ from django.db.models import F, Sum, Prefetch, Count, Func
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from gestioncof.models import CofProfile, Clipper from gestioncof.models import CofProfile
from kfet.decorators import teamkfet_required from kfet.decorators import teamkfet_required
from kfet.models import (Account, Checkout, Article, Settings, AccountNegative, from kfet.models import (Account, Checkout, Article, Settings, AccountNegative,
CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory,
@ -222,19 +222,20 @@ def account_form_set_readonly_fields(user_form, cof_form):
cof_form.fields['login_clipper'].widget.attrs['readonly'] = True cof_form.fields['login_clipper'].widget.attrs['readonly'] = True
cof_form.fields['is_cof'].widget.attrs['disabled'] = True cof_form.fields['is_cof'].widget.attrs['disabled'] = True
def get_account_create_forms(request=None, username=None, login_clipper=None): def get_account_create_forms(request=None, username=None, login_clipper=None,
fullname=None):
user = None user = None
clipper = None clipper = False
if login_clipper and (login_clipper == username or not username): if login_clipper and (login_clipper == username or not username):
# à partir d'un clipper # à partir d'un clipper
# le user associé à ce clipper ne devrait pas encore exister # le user associé à ce clipper ne devrait pas encore exister
clipper = get_object_or_404(Clipper, username = login_clipper) clipper = True
try: try:
# Vérification que clipper ne soit pas déjà dans User # Vérification que clipper ne soit pas déjà dans User
user = User.objects.get(username=login_clipper) user = User.objects.get(username=login_clipper)
# Ici, on nous a menti, le user existe déjà # Ici, on nous a menti, le user existe déjà
username = user.username username = user.username
clipper = None clipper = False
except User.DoesNotExist: except User.DoesNotExist:
# Clipper (sans user déjà existant) # Clipper (sans user déjà existant)
@ -242,9 +243,9 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
user_initial = { user_initial = {
'username' : login_clipper, 'username' : login_clipper,
'email' : "%s@clipper.ens.fr" % login_clipper} 'email' : "%s@clipper.ens.fr" % login_clipper}
if clipper.fullname: if fullname:
# Prefill du nom et prénom # Prefill du nom et prénom
names = clipper.fullname.split() names = fullname.split()
# Le premier, c'est le prénom # Le premier, c'est le prénom
user_initial['first_name'] = names[0] user_initial['first_name'] = names[0]
if len(names) > 1: if len(names) > 1:
@ -308,8 +309,11 @@ def get_account_create_forms(request=None, username=None, login_clipper=None):
@login_required @login_required
@teamkfet_required @teamkfet_required
def account_create_ajax(request, username=None, login_clipper=None): def account_create_ajax(request, username=None, login_clipper=None,
forms = get_account_create_forms(request=None, username=username, login_clipper=login_clipper) fullname=None):
forms = get_account_create_forms(
request=None, username=username, login_clipper=login_clipper,
fullname=fullname)
return render(request, "kfet/account_create_form.html", { return render(request, "kfet/account_create_form.html", {
'account_form' : forms['account_form'], 'account_form' : forms['account_form'],
'cof_form' : forms['cof_form'], 'cof_form' : forms['cof_form'],

View file

@ -4,5 +4,6 @@
source ~/venv/bin/activate source ~/venv/bin/activate
python manage.py migrate python manage.py migrate
python manage.py syncmails python manage.py syncmails
python manage.py loaddata users root bda gestion sites python manage.py loaddata gestion sites
python manage.py loaddevdata
python manage.py collectstatic --noinput python manage.py collectstatic --noinput

View file

@ -17,5 +17,6 @@ asgi-redis==0.14.0
statistics==1.0.3.5 statistics==1.0.3.5
future==0.15.2 future==0.15.2
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
git+https://github.com/Aureplop/channels.git#egg=channel
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3
git+https://github.com/Aureplop/channels.git#egg=channels

View file

@ -1,34 +0,0 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings")
from gestioncof.models import Clipper
current = {}
print("[ FETCHING ]")
for clipper in Clipper.objects.all():
current[clipper.username] = clipper
print("[ SYNCING ]")
for line in sys.stdin:
bits = line.split(":")
username = bits[0]
fullname = bits[4]
if username in current:
clipper = current[username]
if clipper.fullname != fullname:
clipper.fullname = fullname
clipper.save()
print("Updated", username)
else:
clipper = Clipper(username=username, fullname=fullname)
clipper.save()
print("Created", username)
print("[ DONE ]")

View file

@ -1,2 +0,0 @@
#!/bin/sh
ssh cof@sas.eleves.ens.fr ypcat passwd | python sync_clipper.py