Merge branch 'master' into Aufinal/dev_data_kfet

This commit is contained in:
Martin Pépin 2017-02-11 21:01:37 +01:00
commit 313b5cf61d
63 changed files with 1347 additions and 1161 deletions

View file

@ -8,7 +8,7 @@ from django import forms
from django.contrib import admin
from django.utils.translation import ugettext_lazy as _
from gestioncof.models import SurveyQuestionAnswer, SurveyQuestion, \
CofProfile, EventOption, EventOptionChoice, Event, Club, CustomMail, \
CofProfile, EventOption, EventOptionChoice, Event, Club, \
Survey, EventCommentField, EventRegistration
from gestioncof.petits_cours_models import PetitCoursDemande, \
PetitCoursSubject, PetitCoursAbility, PetitCoursAttribution, \
@ -267,10 +267,6 @@ class PetitCoursDemandeAdmin(admin.ModelAdmin):
search_fields = ('name', 'email', 'phone', 'lieu', 'remarques')
class CustomMailAdmin(admin.ModelAdmin):
search_fields = ('shortname', 'title')
class ClubAdminForm(forms.ModelForm):
def clean(self):
cleaned_data = super(ClubAdminForm, self).clean()
@ -297,7 +293,6 @@ admin.site.unregister(User)
admin.site.register(User, UserProfileAdmin)
admin.site.register(CofProfile)
admin.site.register(Club, ClubAdmin)
admin.site.register(CustomMail)
admin.site.register(PetitCoursSubject)
admin.site.register(PetitCoursAbility, PetitCoursAbilityAdmin)
admin.site.register(PetitCoursAttribution, PetitCoursAttributionAdmin)

View file

@ -1,18 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from ldap3 import Connection
from django import shortcuts
from django.http import Http404
from django.db.models import Q
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
class Clipper(object):
def __init__(self, clipper, fullname):
self.clipper = clipper
self.fullname = fullname
@buro_required
def autocomplete(request):
if "q" not in request.GET:
@ -25,37 +30,51 @@ def autocomplete(request):
queries = {}
bits = q.split()
queries['members'] = CofProfile.objects.filter(Q(is_cof=True))
queries['users'] = User.objects.filter(Q(profile__is_cof=False))
queries['clippers'] = Clipper.objects
# Fetching data from User and CofProfile tables
queries['members'] = CofProfile.objects.filter(is_cof=True)
queries['users'] = User.objects.filter(profile__is_cof=False)
for bit in bits:
queries['members'] = queries['members'].filter(
Q(user__first_name__icontains=bit)
| Q(user__last_name__icontains=bit)
| Q(user__username__icontains=bit)
| Q(login_clipper__icontains=bit))
Q(user__first_name__icontains=bit)
| Q(user__last_name__icontains=bit)
| Q(user__username__icontains=bit)
| Q(login_clipper__icontains=bit))
queries['users'] = queries['users'].filter(
Q(first_name__icontains=bit)
| Q(last_name__icontains=bit)
| Q(username__icontains=bit))
queries['clippers'] = queries['clippers'].filter(
Q(fullname__icontains=bit)
| Q(username__icontains=bit))
Q(first_name__icontains=bit)
| Q(last_name__icontains=bit)
| Q(username__icontains=bit))
queries['members'] = queries['members'].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)
options = 0
for query in queries.values():
options += len(query)
data['options'] = options
data['options'] = sum(len(query) for query in queries)
return shortcuts.render(request, "autocomplete_user.html", data)

View file

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

View file

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

View file

View file

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
"""
Import des mails de GestioCOF dans la base de donnée
"""
import json
import os
from custommail.models import Type, CustomMail, Variable
from django.core.management.base import BaseCommand
from django.contrib.contenttypes.models import ContentType
class Command(BaseCommand):
help = ("Va chercher les données mails de GestioCOF stocké au format json "
"dans /gestioncof/management/data/custommails.json. Le format des "
"données est celui donné par la commande :"
" `python manage.py dumpdata custommail --natural-foreign` "
"La bonne façon de mettre à jour ce fichier est donc de le "
"charger à l'aide de syncmails, le faire les modifications à "
"l'aide de l'interface administration et/ou du shell puis de le "
"remplacer par le nouveau résultat de la commande précédente.")
def handle(self, *args, **options):
path = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
'data', 'custommail.json')
with open(path, 'r') as jsonfile:
mail_data = json.load(jsonfile)
# On se souvient à quel objet correspond quel pk du json
assoc = {'types': {}, 'mails': {}}
status = {'synced': 0, 'unchanged': 0}
for obj in mail_data:
fields = obj['fields']
# Pour les trois types d'objets :
# - On récupère les objets référencés par les clefs étrangères
# - On crée l'objet si nécessaire
# - On le stocke éventuellement dans les deux dictionnaires définis
# plus haut
# Variable types
if obj['model'] == 'custommail.variabletype':
fields['inner1'] = assoc['types'].get(fields['inner1'])
fields['inner2'] = assoc['types'].get(fields['inner2'])
if fields['kind'] == 'model':
fields['content_type'] = (
ContentType.objects
.get_by_natural_key(*fields['content_type'])
)
var_type, _ = Type.objects.get_or_create(**fields)
assoc['types'][obj['pk']] = var_type
# Custom mails
if obj['model'] == 'custommail.custommail':
mail = None
try:
mail = CustomMail.objects.get(
shortname=fields['shortname'])
status['unchanged'] += 1
except CustomMail.DoesNotExist:
mail = CustomMail.objects.create(**fields)
status['synced'] += 1
self.stdout.write(
'SYNCED {:s}'.format(fields['shortname']))
assoc['mails'][obj['pk']] = mail
# Variables
if obj['model'] == 'custommail.custommailvariable':
fields['custommail'] = assoc['mails'].get(fields['custommail'])
fields['type'] = assoc['types'].get(fields['type'])
try:
Variable.objects.get(
custommail=fields['custommail'],
name=fields['name']
)
except Variable.DoesNotExist:
Variable.objects.create(**fields)
# C'est agréable d'avoir le résultat affiché
self.stdout.write(
'{synced:d} mails synchronized {unchanged:d} unchanged'
.format(**status)
)

View file

@ -0,0 +1,587 @@
[
{
"model": "custommail.variabletype",
"pk": 1,
"fields": {
"content_type": [
"auth",
"user"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 2,
"fields": {
"content_type": null,
"inner1": null,
"kind": "int",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 3,
"fields": {
"content_type": [
"bda",
"spectacle"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 4,
"fields": {
"content_type": [
"bda",
"spectaclerevente"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 5,
"fields": {
"content_type": [
"sites",
"site"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 6,
"fields": {
"content_type": [
"gestioncof",
"petitcoursdemande"
],
"inner1": null,
"kind": "model",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 7,
"fields": {
"content_type": null,
"inner1": null,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 8,
"fields": {
"content_type": null,
"inner1": 1,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 9,
"fields": {
"content_type": null,
"inner1": null,
"kind": "pair",
"inner2": 8
}
},
{
"model": "custommail.variabletype",
"pk": 10,
"fields": {
"content_type": null,
"inner1": 9,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.variabletype",
"pk": 11,
"fields": {
"content_type": null,
"inner1": 3,
"kind": "list",
"inner2": null
}
},
{
"model": "custommail.custommail",
"pk": 1,
"fields": {
"shortname": "welcome",
"subject": "Bienvenue au COF",
"description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre",
"body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime."
}
},
{
"model": "custommail.custommail",
"pk": 2,
"fields": {
"shortname": "bda-rappel",
"subject": "{{ show }}",
"description": "Mail de rappel pour les spectacles BdA",
"body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 3,
"fields": {
"shortname": "bda-revente",
"subject": "{{ show }}",
"description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 4,
"fields": {
"shortname": "bda-shotgun",
"subject": "{{ show }}",
"description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.",
"body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 5,
"fields": {
"shortname": "bda-revente-winner",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente",
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 6,
"fields": {
"shortname": "bda-revente-loser",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Notification envoy\u00e9e aux perdants d'un tirage de revente.",
"body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 7,
"fields": {
"shortname": "bda-revente-seller",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e",
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA"
}
},
{
"model": "custommail.custommail",
"pk": 8,
"fields": {
"shortname": "bda-revente-new",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires.",
"body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 9,
"fields": {
"shortname": "bda-buy-shotgun",
"subject": "BdA-Revente : {{ show.title }}",
"description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun.",
"body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})"
}
},
{
"model": "custommail.custommail",
"pk": 10,
"fields": {
"shortname": "petit-cours-mail-eleve",
"subject": "Petits cours ENS par le COF",
"description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours",
"body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours"
}
},
{
"model": "custommail.custommail",
"pk": 11,
"fields": {
"shortname": "petits-cours-mail-demandeur",
"subject": "Cours particuliers ENS",
"description": "Mail envoy\u00e9 aux personnent qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})",
"body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS"
}
},
{
"model": "custommail.custommail",
"pk": 12,
"fields": {
"shortname": "bda-attributions",
"subject": "R\u00e9sultats du tirage au sort",
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places",
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommail",
"pk": 13,
"fields": {
"shortname": "bda-attributions-decus",
"subject": "R\u00e9sultats du tirage au sort",
"description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place",
"body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts"
}
},
{
"model": "custommail.custommailvariable",
"pk": 1,
"fields": {
"name": "member",
"description": "Utilisateur de GestioCOF",
"custommail": 1,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 2,
"fields": {
"name": "member",
"description": "Utilisateur ayant eu une place pour ce spectacle",
"custommail": 2,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 3,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 2,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 4,
"fields": {
"name": "nb_attr",
"description": "Nombre de places obtenues",
"custommail": 2,
"type": 2
}
},
{
"model": "custommail.custommailvariable",
"pk": 5,
"fields": {
"name": "revente",
"description": "Revente mentionn\u00e9e dans le mail",
"custommail": 3,
"type": 4
}
},
{
"model": "custommail.custommailvariable",
"pk": 6,
"fields": {
"name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place",
"custommail": 3,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 7,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 3,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 8,
"fields": {
"name": "site",
"description": "Site web (gestioCOF)",
"custommail": 3,
"type": 5
}
},
{
"model": "custommail.custommailvariable",
"pk": 9,
"fields": {
"name": "site",
"description": "Site web (gestioCOF)",
"custommail": 4,
"type": 5
}
},
{
"model": "custommail.custommailvariable",
"pk": 10,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 4,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 11,
"fields": {
"name": "member",
"description": "Personne int\u00e9ress\u00e9e par la place",
"custommail": 4,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 12,
"fields": {
"name": "acheteur",
"description": "Gagnant-e du tirage",
"custommail": 5,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 13,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 5,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 14,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 5,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 15,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 6,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 16,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 6,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 17,
"fields": {
"name": "acheteur",
"description": "Personne inscrite au tirage qui n'a pas eu la place",
"custommail": 6,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 18,
"fields": {
"name": "acheteur",
"description": "Gagnant-e du tirage",
"custommail": 7,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 19,
"fields": {
"name": "vendeur",
"description": "Personne qui vend une place",
"custommail": 7,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 20,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 7,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 21,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 8,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 22,
"fields": {
"name": "vendeur",
"description": "Personne qui vend la place",
"custommail": 8,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 23,
"fields": {
"name": "revente",
"description": "Revente mentionn\u00e9e dans le mail",
"custommail": 8,
"type": 4
}
},
{
"model": "custommail.custommailvariable",
"pk": 24,
"fields": {
"name": "vendeur",
"description": "Personne qui vend la place",
"custommail": 9,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 25,
"fields": {
"name": "show",
"description": "Spectacle",
"custommail": 9,
"type": 3
}
},
{
"model": "custommail.custommailvariable",
"pk": 26,
"fields": {
"name": "acheteur",
"description": "Personne qui prend la place au shotgun",
"custommail": 9,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 27,
"fields": {
"name": "demande",
"description": "Demande de petit cours",
"custommail": 10,
"type": 6
}
},
{
"model": "custommail.custommailvariable",
"pk": 28,
"fields": {
"name": "matieres",
"description": "Liste des mati\u00e8res concern\u00e9es par la demande",
"custommail": 10,
"type": 7
}
},
{
"model": "custommail.custommailvariable",
"pk": 29,
"fields": {
"name": "proposals",
"description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re",
"custommail": 11,
"type": 10
}
},
{
"model": "custommail.custommailvariable",
"pk": 30,
"fields": {
"name": "unsatisfied",
"description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer",
"custommail": 11,
"type": 7
}
},
{
"model": "custommail.custommailvariable",
"pk": 31,
"fields": {
"name": "places",
"description": "Places de spectacle du participant",
"custommail": 12,
"type": 11
}
},
{
"model": "custommail.custommailvariable",
"pk": 32,
"fields": {
"name": "member",
"description": "Participant du tirage au sort",
"custommail": 12,
"type": 1
}
},
{
"model": "custommail.custommailvariable",
"pk": 33,
"fields": {
"name": "member",
"description": "Participant du tirage au sort",
"custommail": 13,
"type": 1
}
}
]

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

@ -0,0 +1,16 @@
# -*- coding: utf-8 -*-
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0009_delete_clipper'),
]
operations = [
migrations.DeleteModel(
name='CustomMail',
),
]

View file

@ -1,9 +1,5 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.db import models
from django.dispatch import receiver
from django.contrib.auth.models import User
@ -102,22 +98,6 @@ class Club(models.Model):
return self.name
@python_2_unicode_compatible
class CustomMail(models.Model):
shortname = models.SlugField(max_length=50, blank=False)
title = models.CharField("Titre", max_length=200, blank=False)
content = models.TextField("Contenu", blank=False)
comments = models.TextField("Informations contextuelles sur le mail",
blank=True)
class Meta:
verbose_name = "Mail personnalisable"
verbose_name_plural = "Mails personnalisables"
def __str__(self):
return "%s: %s" % (self.shortname, self.title)
@python_2_unicode_compatible
class Event(models.Model):
title = models.CharField("Titre", max_length=200)
@ -264,15 +244,6 @@ class SurveyAnswer(models.Model):
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
class CalendarSubscription(models.Model):
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 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from functools import reduce
from django.db import models
from django.db.models import Min
from django.contrib.auth.models import User
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):
@ -24,7 +21,6 @@ LEVELS_CHOICES = (
)
@python_2_unicode_compatible
class PetitCoursSubject(models.Model):
name = models.CharField(_("Matière"), max_length=30)
users = models.ManyToManyField(User, related_name="petits_cours_matieres",
@ -38,7 +34,6 @@ class PetitCoursSubject(models.Model):
return self.name
@python_2_unicode_compatible
class PetitCoursAbility(models.Model):
user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matière"))
@ -52,11 +47,11 @@ class PetitCoursAbility(models.Model):
verbose_name_plural = "Compétences des petits cours"
def __str__(self):
return "%s - %s - %s" % (self.user.username,
self.matiere, self.niveau)
return "{:s} - {!s} - {:s}".format(
self.user.username, self.matiere, self.niveau
)
@python_2_unicode_compatible
class PetitCoursDemande(models.Model):
name = models.CharField(_("Nom/prénom"), max_length=200)
email = models.CharField(_("Adresse email"), max_length=300)
@ -70,7 +65,7 @@ class PetitCoursDemande(models.Model):
freq = models.CharField(
_("Fréquence"),
help_text=_("Indiquez ici la fréquence envisagée "
+ "(hebdomadaire, 2 fois par semaine, ...)"),
"(hebdomadaire, 2 fois par semaine, ...)"),
max_length=300, blank=True)
lieu = models.CharField(
_("Lieu (si préférence)"),
@ -94,16 +89,42 @@ class PetitCoursDemande(models.Model):
blank=True, null=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:
verbose_name = "Demande de petits cours"
verbose_name_plural = "Demandes de petits cours"
def __str__(self):
return "Demande %d du %s" % (self.id,
self.created.strftime("%d %b %Y"))
return "Demande {:d} du {:s}".format(
self.id, self.created.strftime("%d %b %Y")
)
@python_2_unicode_compatible
class PetitCoursAttribution(models.Model):
user = models.ForeignKey(User)
demande = models.ForeignKey(PetitCoursDemande, verbose_name=_("Demande"))
@ -118,20 +139,40 @@ class PetitCoursAttribution(models.Model):
verbose_name_plural = "Attributions de petits cours"
def __str__(self):
return "Attribution de la demande %d à %s pour %s" \
% (self.demande.id, self.user.username, self.matiere)
return "Attribution de la demande {:d} à {:s} pour {!s}".format(
self.demande.id, self.user.username, self.matiere
)
@python_2_unicode_compatible
class PetitCoursAttributionCounter(models.Model):
user = models.ForeignKey(User)
matiere = models.ForeignKey(PetitCoursSubject, verbose_name=_("Matiere"))
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:
verbose_name = "Compteur d'attribution de petits cours"
verbose_name_plural = "Compteurs d'attributions de petits cours"
def __str__(self):
return "%d demandes envoyées à %s pour %s" \
% (self.count, self.user.username, self.matiere)
return "{:d} demandes envoyées à {:s} pour {!s}".format(
self.count, self.user.username, self.matiere
)

View file

@ -1,37 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import json
from datetime import datetime
from custommail.shortcuts import render_custom_mail
from django.shortcuts import render, get_object_or_404, redirect
from django.core import mail
from django.core.mail import EmailMessage
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.views.generic import ListView
from django.utils.decorators import method_decorator
from django.views.generic import ListView, DetailView
from django.views.decorators.csrf import csrf_exempt
from django.template import loader
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.db.models import Min
from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import PetitCoursDemande, \
PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursAbility, \
PetitCoursSubject
from gestioncof.petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject
)
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables
from captcha.fields import ReCaptchaField
from datetime import datetime
import base64
import json
class DemandeListView(ListView):
model = PetitCoursDemande
@ -41,47 +30,17 @@ class DemandeListView(ListView):
def get_queryset(self):
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 details(request, demande_id):
demande = get_object_or_404(PetitCoursDemande, id=demande_id)
attributions = PetitCoursAttribution.objects.filter(demande=demande).all()
return render(request, "details_demande_petit_cours.html",
{"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)
def get_context_data(self, **kwargs):
context = super(DemandeDetailView, self).get_context_data(**kwargs)
obj = self.object
context['attributions'] = obj.petitcoursattribution_set.all()
return context
@buro_required
@ -95,12 +54,15 @@ def traitement(request, demande_id, redo=False):
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo):
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
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)
candidates, _ = zip(*tuples)
candidates = candidates[0:min(3, len(candidates))]
@ -131,7 +93,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
proposed_for = proposed_for.items()
attribdata = list(attribdata.items())
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals,
"unsatisfied": unsatisfied,
"extra":
@ -139,7 +101,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
'style="width:99%; height: 90px;">'
'</textarea>'
})
return render(request, "traitement_demande_petit_cours.html",
return render(request, "gestioncof/traitement_demande_petit_cours.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
@ -153,14 +115,16 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
def _generate_eleve_email(demande, proposed_for):
proposed_mails = []
for user, matieres in proposed_for:
msg = loader.render_to_string("petits-cours-mail-eleve.txt", {
"demande": demande,
"matieres": matieres
})
proposed_mails.append((user, msg))
return proposed_mails
return [
(
user,
render_custom_mail('petit-cours-mail-eleve', {
"demande": demande,
"matieres": matieres
})
)
for user, matieres in proposed_for
]
def _traitement_other_preparing(request, demande):
@ -170,7 +134,7 @@ def _traitement_other_preparing(request, demande):
proposed_for = {}
attribdata = {}
errors = []
for matiere, candidates in _get_demande_candidates(demande, redo):
for matiere, candidates in demande.get_candidates(redo):
if candidates:
candidates = dict([(candidate.user.id, candidate.user)
for candidate in candidates])
@ -178,17 +142,19 @@ def _traitement_other_preparing(request, demande):
proposals[matiere] = []
for choice_id in range(min(3, len(candidates))):
choice = int(
request.POST["proposal-%d-%d" % (matiere.id, choice_id)])
request.POST["proposal-{:d}-{:d}"
.format(matiere.id, choice_id)]
)
if choice == -1:
continue
if choice not in candidates:
errors.append("Choix invalide pour la proposition %d"
"en %s" % (choice_id + 1, matiere))
errors.append("Choix invalide pour la proposition {:d}"
"en {!s}".format(choice_id + 1, matiere))
continue
user = candidates[choice]
if user in proposals[matiere]:
errors.append("La proposition %d en %s est un doublon"
% (choice_id + 1, matiere))
errors.append("La proposition {:d} en {!s} est un doublon"
.format(choice_id + 1, matiere))
continue
proposals[matiere].append(user)
attribdata[matiere.id].append(user.id)
@ -197,12 +163,13 @@ def _traitement_other_preparing(request, demande):
else:
proposed_for[user].append(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:
errors.append("Seulement %d proposition%s pour %s"
% (len(proposals[matiere]),
"s" if len(proposals[matiere]) > 1 else "",
matiere))
errors.append("Seulement {:d} proposition{:s} pour {!s}"
.format(
len(proposals[matiere]),
"s" if len(proposals[matiere]) > 1 else "",
matiere))
else:
unsatisfied.append(matiere)
return _finalize_traitement(request, demande, proposals, proposed_for,
@ -219,12 +186,15 @@ def _traitement_other(request, demande, redo):
proposed_for = {}
unsatisfied = []
attribdata = {}
for matiere, candidates in _get_demande_candidates(demande, redo):
for matiere, candidates in demande.get_candidates(redo):
if candidates:
tuples = []
for candidate in candidates:
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)
candidates, _ = zip(*tuples)
attribdata[matiere.id] = []
@ -241,7 +211,8 @@ def _traitement_other(request, demande, redo):
unsatisfied.append(matiere)
proposals = proposals.items()
proposed_for = proposed_for.items()
return render(request, "traitement_demande_petit_cours_autre_niveau.html",
return render(request,
"gestiocof/traitement_demande_petit_cours_autre_niveau.html",
{"demande": demande,
"unsatisfied": unsatisfied,
"proposals": proposals,
@ -272,7 +243,7 @@ def _traitement_post(request, demande):
proposals_list = proposals.items()
proposed_for = proposed_for.items()
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
mainmail = render_custom_mail("petits-cours-mail-demandeur", {
"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra,
@ -282,14 +253,14 @@ def _traitement_post(request, demande):
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
mails_to_send = []
for (user, msg) in proposed_mails:
msg = EmailMessage("Petits cours ENS par le COF", msg,
frommail, [user.email],
[bccaddress], headers={'Reply-To': replyto})
msg = mail.EmailMessage("Petits cours ENS par le COF", msg,
frommail, [user.email],
[bccaddress], headers={'Reply-To': replyto})
mails_to_send.append(msg)
mails_to_send.append(EmailMessage("Cours particuliers ENS", mainmail,
frommail, [demande.email],
[bccaddress],
headers={'Reply-To': replyto}))
mails_to_send.append(mail.EmailMessage("Cours particuliers ENS", mainmail,
frommail, [demande.email],
[bccaddress],
headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=True)
connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User)
@ -307,43 +278,18 @@ def _traitement_post(request, demande):
demande.traitee_par = request.user
demande.processed = datetime.now()
demande.save()
return render(request, "traitement_demande_petit_cours_success.html",
return render(request,
"gestioncof/traitement_demande_petit_cours_success.html",
{"demande": demande,
"redo": redo,
})
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
def inscription(request):
profile, created = CofProfile.objects.get_or_create(user=request.user)
if not profile.is_cof:
return redirect("cof-denied")
MatieresFormSet = inlineformset_factory(User, PetitCoursAbility,
fields=("matiere", "niveau",
"agrege",),
formset=BaseMatieresFormSet)
success = False
if request.method == "POST":
formset = MatieresFormSet(request.POST, instance=request.user)
@ -354,10 +300,14 @@ def inscription(request):
profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User,
PetitCoursSubject)
abilities = PetitCoursAbility.objects \
.filter(user=request.user).all()
abilities = (
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
_get_attrib_counter(ability.user, ability.matiere)
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
unlock_tables()
success = True
formset = MatieresFormSet(instance=request.user)
@ -369,20 +319,6 @@ def inscription(request):
"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
def demande(request):
success = False

View file

@ -9,12 +9,9 @@ from django.conf import settings
from django_cas_ng.backends import CASBackend
from django_cas_ng.utils import get_cas_client
from django.contrib.auth import get_user_model
from django.contrib.auth.models import User as DjangoUser
from django.db import connection
from django.core.mail import send_mail
from django.template import Template, Context
from gestioncof.models import CofProfile, CustomMail
from gestioncof.models import CofProfile
User = get_user_model()
@ -73,9 +70,9 @@ class COFCASBackend(CASBackend):
def context_processor(request):
'''Append extra data to the context of the given request'''
data = {
"user": request.user,
"site": Site.objects.get_current(),
}
"user": request.user,
"site": Site.objects.get_current(),
}
return data
@ -99,18 +96,3 @@ def unlock_tables(*models):
return row
unlock_table = unlock_tables
def send_custom_mail(to, shortname, context=None, from_email="cof@ens.fr"):
if context is None:
context = {}
if isinstance(to, DjangoUser):
context["nom"] = to.get_full_name()
context["prenom"] = to.first_name
to = to.email
mail = CustomMail.objects.get(shortname=shortname)
template = Template(mail.content)
message = template.render(Context(context))
send_mail(mail.title, message,
from_email, [to],
fail_silently=True)

View file

@ -1088,3 +1088,8 @@ tr.awesome{
color: white;
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 %}
<li class="autocomplete-header">Utilisateurs <tt>clipper</tt></li>
{% 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 %}
{% endif %}

View file

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

View file

@ -1,4 +1,5 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
@ -36,8 +37,21 @@ souscrire aux événements du COF et/ou aux spectacles BdA.
<form action="" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Enregistrer" class="btn btn-primary" />
{{ form | bootstrap }}
<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>
<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 %}

View file

@ -30,10 +30,16 @@
<h4>Mails pour les membres proposés :</h4>
{% for proposeduser, mail in proposed_mails %}
<h5>Pour {{ proposeduser }}:</h5>
<pre>{{ mail }}</pre>
{% with object=mail.0 content=mail.1 %}
<pre>{{ object }}</pre>
<pre>{{ content }}</pre>
{% endwith %}
{% endfor %}
<h4>Mail pour l'auteur de la demande :</h4>
<pre style="margin-top: 15px;">{{ mainmail|safe }}</pre>
{% with object=mainmail.0 content=mainmail.1 %}
<pre style="margin-top: 15px;">{{ object }}</pre>
<pre style="margin-top: 15px;">{{ content|safe }}</pre>
{% endwith %}
<input type="hidden" name="attribdata" value="{{ attribdata }}" />
{% if redo %}<input type="hidden" name="redo" value="1" />{% endif %}
<input class="btn btn-primary pull-right" type="submit" value="Valider le {% if redo %}re{% endif %}traitement de la demande" />

View file

@ -2,11 +2,11 @@
{% load staticfiles %}
{% 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 "grappelli/jquery/jquery-1.6.2.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>
<link href="{% static "grappelli/css/tools.css" %}" rel="stylesheet" type="text/css" />
<link href="{% static "grappelli/css/jquery-ui-grappelli-extensions.css" %}" rel="stylesheet" type="text/css" />
<script src="{% static "js/jquery.min.js" %}" type="text/javascript"></script>
<script src="{% static "js/jquery-ui.min.js" %}" type="text/javascript"></script>
<script src="{% static "js/jquery.ui.touch-punch.min.js" %}" type="text/javascript"></script>
<link type="text/css" rel="stylesheet" href="{% static "css/jquery-ui.min.css" %}" />
{% endblock %}
{% block realcontent %}

View file

@ -1,17 +0,0 @@
Bonjour,
Je vous contacte au sujet de votre annonce passée sur le site du COF pour rentrer en contact avec un élève normalien pour des cours particuliers. Voici les coordonnées d'élèves qui sont motivés par de tels cours et correspondent aux critères que vous nous aviez transmis :
{% for matiere, proposed in proposals %}¤ {{ matiere }} :{% for user in proposed %}
¤ {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}
{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'élève disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.
{% endif %}Si pour une raison ou une autre ces numéros ne suffisaient pas, n'hésitez pas à répondre à cet e-mail et je vous en ferai parvenir d'autres sans problème.
{% if extra|length > 0 %}
{{ extra|safe }}
{% endif %}
Cordialement,
--
Le COF, BdE de l'ENS

View file

@ -1,28 +0,0 @@
Salut,
Le COF a reçu une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonnées, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les numéros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :
¤ Nom : {{ demande.name }}
¤ Période : {{ demande.quand }}
¤ Fréquence : {{ demande.freq }}
¤ Lieu (si préféré) : {{ demande.lieu }}
¤ Niveau : {{ demande.get_niveau_display }}
¤ Remarques diverses (désolé pour les balises HTML) : {{ demande.remarques }}
{% if matieres|length > 1 %}¤ Matières :
{% for matiere in matieres %} ¤ {{ matiere }}
{% endfor %}{% else %}¤ Matière : {% for matiere in matieres %}{{ matiere }}
{% endfor %}{% endif %}
Voilà, cette personne te contactera peut-être sous peu, tu pourras voir les détails directement avec elle (prix, modalités, ...). Pour indication, 30 Euro/h semble être la moyenne.
Si tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, ça serait cool que tu décoches la case "Recevoir des propositions de petits cours" sur GestioCOF. Ensuite dès que tu voudras réapparaître tu pourras recocher la case et tu seras à nouveau sur la liste.
À bientôt,
--
Le COF, pour les petits cours

View file

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

View file

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

View file

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

View file

@ -1,13 +1,10 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import unicodecsv
import uuid
from datetime import timedelta
from icalendar import Calendar, Event as Vevent
from custommail.shortcuts import send_custom_mail
from django.shortcuts import redirect, get_object_or_404, render
from django.http import Http404, HttpResponse, HttpResponseForbidden
@ -24,8 +21,7 @@ from gestioncof.models import Event, EventRegistration, EventOption, \
EventOptionChoice
from gestioncof.models import EventCommentField, EventCommentValue, \
CalendarSubscription
from gestioncof.shared import send_custom_mail
from gestioncof.models import CofProfile, Clipper, Club
from gestioncof.models import CofProfile, Club
from gestioncof.decorators import buro_required, cof_required
from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \
SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \
@ -321,11 +317,11 @@ def registration_set_ro_fields(user_form, profile_form):
@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()
member = None
if login_clipper:
clipper = get_object_or_404(Clipper, username=login_clipper)
try: # check if the given user is already registered
member = User.objects.get(username=login_clipper)
username = member.username
@ -336,8 +332,8 @@ def registration_form2(request, login_clipper=None, username=None):
user_form = RegistrationUserForm(initial={
'username': login_clipper,
'email': "%s@clipper.ens.fr" % login_clipper})
if clipper.fullname:
bits = clipper.fullname.split(" ")
if fullname:
bits = fullname.split(" ")
user_form.fields['first_name'].initial = bits[0]
if len(bits) > 1:
user_form.fields['last_name'].initial = " ".join(bits[1:])
@ -412,12 +408,12 @@ def registration(request):
try:
member = User.objects.get(username=username)
user_form = RegistrationUserForm(request_dict, instance=member)
except User.DoesNotExist:
try:
clipper = Clipper.objects.get(username=username)
login_clipper = clipper.username
except Clipper.DoesNotExist:
if member.profile.login_clipper:
login_clipper = member.profile.login_clipper
else:
user_form.force_long_username()
except User.DoesNotExist:
user_form.force_long_username()
else:
user_form.force_long_username()
@ -438,7 +434,10 @@ def registration(request):
# Enregistrement du profil
profile = profile_form.save()
if profile.is_cof and not was_cof:
send_custom_mail(member, "bienvenue")
send_custom_mail(
"welcome", "cof@ens.fr", [member.email],
context={'member': member},
)
# Enregistrement des inscriptions aux événements
for form in event_formset:
if 'status' not in form.cleaned_data:
@ -461,17 +460,20 @@ def registration(request):
current_registration.paid = \
(form.cleaned_data['status'] == 'paid')
current_registration.save()
if form.event.title == "Mega 15" and created_reg:
field = EventCommentField.objects.get(
event=form.event, name="Commentaires")
try:
comments = EventCommentValue.objects.get(
commentfield=field,
registration=current_registration).content
except EventCommentValue.DoesNotExist:
comments = field.default
send_custom_mail(member, "mega",
{"remarques": comments})
# if form.event.title == "Mega 15" and created_reg:
# field = EventCommentField.objects.get(
# event=form.event, name="Commentaires")
# try:
# comments = EventCommentValue.objects.get(
# commentfield=field,
# registration=current_registration).content
# except EventCommentValue.DoesNotExist:
# comments = field.default
# FIXME : il faut faire quelque chose de propre ici,
# par exemple écrire un mail générique pour
# l'inscription aux événements et/ou donner la
# possibilité d'associer un mail aux événements
# send_custom_mail(...)
# Enregistrement des inscriptions aux clubs
member.clubs.clear()
for club in clubs_form.cleaned_data['clubs']:
@ -686,15 +688,15 @@ def calendar(request):
subscription.token = uuid.uuid4()
subscription.save()
form.save_m2m()
return render(request, "calendar_subscription.html",
return render(request, "gestioncof/calendar_subscription.html",
{'form': form,
'success': True,
'token': str(subscription.token)})
else:
return render(request, "calendar_subscription.html",
return render(request, "gestioncof/calendar_subscription.html",
{'form': form, 'error': "Formulaire incorrect"})
else:
return render(request, "calendar_subscription.html",
return render(request, "gestioncof/calendar_subscription.html",
{'form': CalendarForm(instance=instance),
'token': instance.token if instance else None})