Merge branch 'k-fet' of git.eleves.ens.fr:cof-geek/gestioCOF into Aufinal/transferts_historique

This commit is contained in:
Ludovic Stephan 2016-12-04 00:22:46 -02:00
commit 027bc2e94b
52 changed files with 1109 additions and 211 deletions

40
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,40 @@
services:
- mysql:latest
- redis:latest
variables:
# GestioCOF settings
DJANGO_SETTINGS_MODULE: "cof.settings_dev"
DBNAME: "cof_gestion"
DBUSER: "cof_gestion"
DBPASSWD: "cof_password"
DBHOST: "mysql"
REDIS_HOST: "redis"
# Cached packages
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
# mysql service configuration
MYSQL_DATABASE: "$DBNAME"
MYSQL_USER: "$DBUSER"
MYSQL_PASSWORD: "$DBPASSWD"
MYSQL_ROOT_PASSWORD: "root_password"
cache:
paths:
- vendor/python
- vendor/pip
- vendor/apt
before_script:
- mkdir -p vendor/{python,pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq mysql-client
- mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST"
-e "GRANT ALL ON test_$DBNAME.* TO '$DBUSER'@'%'"
- pip install --cache-dir vendor/pip -t vendor/python -r requirements-devel.txt
test:
stage: test
script:
- python manage.py test

2
Vagrantfile vendored
View file

@ -10,7 +10,7 @@ Vagrant.configure(2) do |config|
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
config.vm.box = "ubuntu/trusty64"
config.vm.box = "ubuntu/xenial64"
# On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
# ordinateur, et le port 8000 avec le port 8000.

View file

@ -5,12 +5,13 @@ from __future__ import print_function
from __future__ import unicode_literals
from django.core.mail import send_mail
from django.contrib import admin
from django.db.models import Sum, Count
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle
from django.template.defaultfilters import pluralize
from django.utils import timezone
from django import forms
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
from datetime import timedelta
@ -210,6 +211,70 @@ class SalleAdmin(admin.ModelAdmin):
search_fields = ('name', 'address')
class SpectacleReventeAdmin(admin.ModelAdmin):
"""
Administration des reventes de spectacles
"""
model = SpectacleRevente
def spectacle(self, obj):
"""
Raccourci vers le spectacle associé à la revente.
"""
return obj.attribution.spectacle
list_display = ("spectacle", "seller", "date", "soldTo")
raw_id_fields = ("attribution",)
readonly_fields = ("shotgun", "expiration_time")
search_fields = ['attribution__spectacle__title',
'seller__user__username',
'seller__user__first_name',
'seller__user__last_name']
actions = ['transfer', 'reinit']
actions_on_bottom = True
def transfer(self, request, queryset):
"""
Effectue le transfert des reventes pour lesquels on connaît l'acheteur.
"""
reventes = queryset.exclude(soldTo__isnull=True).all()
count = reventes.count()
for revente in reventes:
attrib = revente.attribution
attrib.participant = revente.soldTo
attrib.save()
self.message_user(
request,
"%d attribution%s %s été transférée%s avec succès." % (
count, pluralize(count),
pluralize(count, "a,ont"), pluralize(count))
)
transfer.short_description = "Transférer les reventes sélectionnées"
def reinit(self, request, queryset):
"""
Réinitialise les reventes.
"""
count = queryset.count()
for revente in queryset.filter(
attribution__spectacle__date__gte=timezone.now()):
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
self.message_user(
request,
"%d attribution%s %s été réinitialisée%s avec succès." % (
count, pluralize(count),
pluralize(count, "a,ont"), pluralize(count))
)
reinit.short_description = "Réinitialiser les reventes sélectionnées"
admin.site.register(CategorieSpectacle)
admin.site.register(Spectacle, SpectacleAdmin)
admin.site.register(Salle, SalleAdmin)
@ -217,3 +282,4 @@ admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Attribution, AttributionAdmin)
admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin)
admin.site.register(Tirage, TirageAdmin)
admin.site.register(SpectacleRevente, SpectacleReventeAdmin)

View file

@ -4,9 +4,13 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from datetime import timedelta
from django import forms
from django.forms.models import BaseInlineFormSet
from bda.models import Spectacle
from django.db.models import Q
from django.utils import timezone
from bda.models import Attribution, Spectacle
class BaseBdaFormSet(BaseInlineFormSet):
@ -35,17 +39,47 @@ class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea())
class SpectacleModelChoiceField(forms.ModelChoiceField):
class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj):
return "%s le %s (%s) à %.02f" % (obj.title, obj.date_no_seconds(),
obj.location, obj.price)
return "%s" % obj.spectacle
class ResellForm(forms.Form):
count = forms.ChoiceField(choices=(("1", "1"), ("2", "2"),))
spectacle = SpectacleModelChoiceField(queryset=Spectacle.objects.none())
attributions = AttributionModelMultipleChoiceField(
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs):
super(ResellForm, self).__init__(*args, **kwargs)
self.fields['spectacle'].queryset = participant.attributions.all() \
.distinct()
self.fields['attributions'].queryset = participant.attribution_set\
.filter(spectacle__date__gte=timezone.now())\
.exclude(revente__seller=participant)
class AnnulForm(forms.Form):
attributions = AttributionModelMultipleChoiceField(
queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs):
super(AnnulForm, self).__init__(*args, **kwargs)
self.fields['attributions'].queryset = participant.attribution_set\
.filter(spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__date__gt=timezone.now()-timedelta(hours=1))\
.filter(Q(revente__soldTo__isnull=True) |
Q(revente__soldTo=participant))
class InscriptionReventeForm(forms.Form):
spectacles = forms.ModelMultipleChoiceField(
queryset=Spectacle.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, tirage, *args, **kwargs):
super(InscriptionReventeForm, self).__init__(*args, **kwargs)
self.fields['spectacles'].queryset = tirage.spectacle_set.filter(
date__gte=timezone.now())

View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
"""
Gestion en ligne de commande des reventes.
"""
from __future__ import unicode_literals
from datetime import timedelta
from django.core.management import BaseCommand
from django.utils import timezone
from bda.models import SpectacleRevente
class Command(BaseCommand):
help = "Envoie les mails de notification et effectue " \
"les tirages au sort des reventes"
leave_locale_alone = True
def handle(self, *args, **options):
now = timezone.now()
reventes = SpectacleRevente.objects.all()
for revente in reventes:
# Check si < 24h
if (revente.attribution.spectacle.date <=
revente.date + timedelta(days=1)) and \
now >= revente.date + timedelta(minutes=15) and \
not revente.notif_sent:
self.stdout.write(str(now))
revente.mail_shotgun()
self.stdout.write("Mail de disponibilité immédiate envoyé")
# Check si délai de retrait dépassé
elif (now >= revente.date + timedelta(hours=1) and
not revente.notif_sent):
self.stdout.write(str(now))
revente.send_notif()
self.stdout.write("Mail d'inscription à une revente envoyé")
# Check si tirage à faire
elif (now >= revente.expiration_time and
not revente.tirage_done):
self.stdout.write(str(now))
revente.tirage()
self.stdout.write("Tirage effectué, mails envoyés")

View file

@ -1,16 +1,21 @@
# -*- coding: utf-8 -*-
"""
Gestion en ligne de commande des mails de rappel.
"""
from __future__ import unicode_literals
from datetime import timedelta
from django.core.management.base import BaseCommand
from django.utils import timezone
from datetime import timedelta
from bda.models import Spectacle
class Command(BaseCommand):
help = 'Envoie les mails de rappel des spectacles dont la date ' \
'approche.\nNe renvoie pas les mails déjà envoyés.'
leave_locale_alone = True
def handle(self, *args, **options):
now = timezone.now()

View file

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import django.utils.timezone
class Migration(migrations.Migration):
dependencies = [
('bda', '0008_py3'),
]
operations = [
migrations.CreateModel(
name='SpectacleRevente',
fields=[
('id', models.AutoField(serialize=False, primary_key=True,
auto_created=True, verbose_name='ID')),
('date', models.DateTimeField(
verbose_name='Date de mise en vente',
default=django.utils.timezone.now)),
('notif_sent', models.BooleanField(
verbose_name='Notification envoyée', default=False)),
('tirage_done', models.BooleanField(
verbose_name='Tirage effectué', default=False)),
],
options={
'verbose_name': 'Revente',
},
),
migrations.AddField(
model_name='participant',
name='choicesrevente',
field=models.ManyToManyField(to='bda.Spectacle',
related_name='subscribed',
blank=True),
),
migrations.AddField(
model_name='spectaclerevente',
name='answered_mail',
field=models.ManyToManyField(to='bda.Participant',
related_name='wanted',
blank=True),
),
migrations.AddField(
model_name='spectaclerevente',
name='attribution',
field=models.OneToOneField(to='bda.Attribution',
related_name='revente'),
),
migrations.AddField(
model_name='spectaclerevente',
name='seller',
field=models.ForeignKey(to='bda.Participant',
verbose_name='Vendeur',
related_name='original_shows'),
),
migrations.AddField(
model_name='spectaclerevente',
name='soldTo',
field=models.ForeignKey(to='bda.Participant',
verbose_name='Vendue à', null=True,
blank=True),
),
]

View file

@ -5,22 +5,19 @@ from __future__ import print_function
from __future__ import unicode_literals
import calendar
import random
from datetime import timedelta
from django.contrib.sites.models import Site
from django.db import models
from django.contrib.auth.models import User
from django.template import loader, Context
from django.template import loader
from django.core import mail
from django.conf import settings
from django.utils import timezone
from django.utils import timezone, formats
from django.utils.encoding import python_2_unicode_compatible
def render_template(template_name, data):
tmpl = loader.get_template(template_name)
ctxt = Context(data)
return tmpl.render(ctxt)
@python_2_unicode_compatible
class Tirage(models.Model):
title = models.CharField("Titre", max_length=300)
@ -31,12 +28,9 @@ class Tirage(models.Model):
enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
default=False)
def date_no_seconds(self):
return self.fermeture.astimezone(timezone.get_current_timezone()) \
.strftime('%d %b %Y %H:%M')
def __str__(self):
return "%s - %s" % (self.title, self.date_no_seconds())
return "%s - %s" % (self.title, formats.localize(
timezone.template_localtime(self.fermeture)))
@python_2_unicode_compatible
@ -83,42 +77,46 @@ class Spectacle(models.Model):
verbose_name = "Spectacle"
ordering = ("date", "title",)
def __repr__(self):
return "[%s]" % self
def timestamp(self):
return "%d" % calendar.timegm(self.date.utctimetuple())
def date_no_seconds(self):
return self.date.astimezone(timezone.get_current_timezone()) \
.strftime('%d %b %Y %H:%M')
def __str__(self):
return "%s - %s, %s, %.02f" % (self.title, self.date_no_seconds(),
self.location, self.price)
return "%s - %s, %s, %.02f" % (
self.title,
formats.localize(timezone.template_localtime(self.date)),
self.location,
self.price
)
def send_rappel(self):
"""
Envoie un mail de rappel à toutes les personnes qui ont une place pour
ce spectacle.
"""
# On récupère la liste des participants
members = {}
for attr in Attribution.objects.filter(spectacle=self).all():
member = attr.participant.user
if member.id in members:
members[member.id].nb_attr = 2
members[member.id][1] = 2
else:
member.nb_attr = 1
members[member.id] = member
members[member.id] = [member.first_name, 1, member.email]
# Pour le BdA
members[0] = ['BdA', 1, 'bda@ens.fr']
members[-1] = ['BdA', 2, 'bda@ens.fr']
# On écrit un mail personnalisé à chaque participant
mails_to_send = []
mail_object = "%s - %s - %s" % (self.title, self.date_no_seconds(),
self.location)
mail_object = str(self)
for member in members.values():
mail_body = render_template('mail-rappel.txt', {
'member': member,
'show': self})
mail_body = loader.render_to_string('bda/mails/rappel.txt', {
'name': member[0],
'nb_attr': member[1],
'show': self})
mail_tot = mail.EmailMessage(
mail_object, mail_body,
settings.RAPPEL_FROM, [member.email],
[], headers={'Reply-To': settings.RAPPEL_REPLY_TO})
mail_object, mail_body,
settings.MAIL_DATA['rappels']['FROM'], [member[2]],
[], headers={
'Reply-To': settings.MAIL_DATA['rappels']['REPLYTO']})
mails_to_send.append(mail_tot)
# On envoie les mails
connection = mail.get_connection()
@ -158,6 +156,9 @@ class Participant(models.Model):
max_length=6, choices=PAYMENT_TYPES,
blank=True)
tirage = models.ForeignKey(Tirage)
choicesrevente = models.ManyToManyField(Spectacle,
related_name="subscribed",
blank=True)
def __str__(self):
return "%s - %s" % (self.user, self.tirage.title)
@ -205,4 +206,170 @@ class Attribution(models.Model):
given = models.BooleanField("Donnée", default=False)
def __str__(self):
return "%s -- %s" % (self.participant, self.spectacle)
return "%s -- %s, %s" % (self.participant.user, self.spectacle.title,
self.spectacle.date)
@python_2_unicode_compatible
class SpectacleRevente(models.Model):
attribution = models.OneToOneField(Attribution,
related_name="revente")
date = models.DateTimeField("Date de mise en vente",
default=timezone.now)
answered_mail = models.ManyToManyField(Participant,
related_name="wanted",
blank=True)
seller = models.ForeignKey(Participant,
related_name="original_shows",
verbose_name="Vendeur")
soldTo = models.ForeignKey(Participant, blank=True, null=True,
verbose_name="Vendue à")
notif_sent = models.BooleanField("Notification envoyée",
default=False)
tirage_done = models.BooleanField("Tirage effectué",
default=False)
@property
def expiration_time(self):
# L'acheteur doit être connu au plus 12h avant le spectacle
remaining_time = (self.attribution.spectacle.date
- self.date - timedelta(hours=13))
# Au minimum, on attend 2 jours avant le tirage
delay = min(remaining_time, timedelta(days=2))
# On a aussi 1h pour changer d'avis
return self.date + delay + timedelta(hours=1)
def expiration_time_str(self):
return self.expiration_time \
.astimezone(timezone.get_current_timezone()) \
.strftime('%d/%m/%y à %H:%M')
@property
def shotgun(self):
# Soit on a dépassé le délai du tirage, soit il reste peu de
# temps avant le spectacle
# On se laisse 5min de marge pour cron
return (timezone.now() > self.expiration_time + timedelta(minutes=5) or
(self.attribution.spectacle.date <= timezone.now() +
timedelta(days=1))) and (timezone.now() >= self.date +
timedelta(minutes=15))
def __str__(self):
return "%s -- %s" % (self.seller,
self.attribution.spectacle.title)
class Meta:
verbose_name = "Revente"
def send_notif(self):
inscrits = self.attribution.spectacle.subscribed.select_related('user')
mails_to_send = []
mail_object = "%s" % (self.attribution.spectacle)
for participant in inscrits:
mail_body = loader.render_to_string('bda/mails/revente.txt', {
'user': participant.user,
'spectacle': self.attribution.spectacle,
'revente': self,
'domain': Site.objects.get_current().domain})
mail_tot = mail.EmailMessage(
mail_object, mail_body,
settings.MAIL_DATA['revente']['FROM'],
[participant.user.email],
[], headers={
'Reply-To': settings.MAIL_DATA['revente']['REPLYTO']})
mails_to_send.append(mail_tot)
connection = mail.get_connection()
connection.send_messages(mails_to_send)
self.notif_sent = True
self.save()
def mail_shotgun(self):
"""
Envoie un mail à toutes les personnes intéréssées par le spectacle pour
leur indiquer qu'il est désormais disponible au shotgun.
"""
inscrits = self.attribution.spectacle.subscribed.select_related('user')
mails_to_send = []
mail_object = "%s" % (self.attribution.spectacle)
for participant in inscrits:
mail_body = loader.render_to_string('bda/mails/shotgun.txt', {
'user': participant.user,
'spectacle': self.attribution.spectacle,
'domain': Site.objects.get_current(),
'mail': self.attribution.participant.user.email})
mail_tot = mail.EmailMessage(
mail_object, mail_body,
settings.MAIL_DATA['revente']['FROM'],
[participant.user.email],
[], headers={
'Reply-To': settings.MAIL_DATA['revente']['REPLYTO']})
mails_to_send.append(mail_tot)
connection = mail.get_connection()
connection.send_messages(mails_to_send)
self.notif_sent = True
self.save()
def tirage(self):
"""
Lance le tirage au sort associé à la revente. Un gagnant est choisi
parmis les personnes intéressées par le spectacle. Les personnes sont
ensuites prévenues par mail du résultat du tirage.
"""
inscrits = list(self.answered_mail.all())
spectacle = self.attribution.spectacle
seller = self.seller
if inscrits:
mails = []
mail_subject = "BdA-Revente : {:s}".format(spectacle.title)
# Envoie un mail au gagnant et au vendeur
winner = random.choice(inscrits)
self.soldTo = winner
context = {
'acheteur': winner.user,
'vendeur': seller.user,
'spectacle': spectacle,
}
mails.append(mail.EmailMessage(
mail_subject,
loader.render_to_string('bda/mails/revente-winner.txt',
context),
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[winner.user.email],
reply_to=[seller.user.email],
))
mails.append(mail.EmailMessage(
mail_subject,
loader.render_to_string('bda/mails/revente-seller.txt',
context),
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[seller.user.email],
reply_to=[winner.user.email],
))
# Envoie un mail aux perdants
for inscrit in inscrits:
if inscrit == winner:
continue
mail_body = loader.render_to_string(
'bda/mails/revente-loser.txt',
{'acheteur': inscrit.user,
'vendeur': seller.user,
'spectacle': spectacle}
)
mails.append(mail.EmailMessage(
mail_subject, mail_body,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[inscrit.user.email],
reply_to=[settings.MAIL_DATA['revente']['REPLYTO']],
))
mail.get_connection().send_messages(mails)
self.tirage_done = True
self.save()

View file

@ -22,7 +22,7 @@
{% for show, members, losers in results %}
<div class="attribresult">
<h3 class="horizontal-title">{{ show.title }} - {{ show.date_no_seconds }} @ {{ show.location }}</h3>
<h3 class="horizontal-title">{{ show.title }} - {{ show.date }} @ {{ show.location }}</h3>
<p>
<strong>{{ show.nrequests }} demandes pour {{ show.slots }} places</strong>
{{ show.price }}€ par place{% if user.profile.is_buro and show.nrequests < show.slots %}, {{ show.deficit }} de déficit{% endif %}

View file

@ -0,0 +1,9 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block realcontent %}
<h2>Inscription à une revente</h2>
<p class="success"> Votre inscription pour a bien été enregistrée !</p>
<p>Le tirage au sort pour cette revente ({{spectacle}}) sera effectué le {{date}}.
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>BdA-Revente</h2>
<p>Il n'y a plus de places en revente pour ce spectacle, désolé !</p>
{% endblock %}

View file

@ -1,6 +1,6 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2><strong>Nope</strong></h1>
<h2><strong>Nope</strong></h2>
<p>Avant de revendre des places, il faut aller les payer !</p>
{% endblock %}

View file

@ -1,26 +1,73 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% load bootstrap %}
{% block realcontent %}
<h2>Revente de place</h1>
<form action="" method="post" id="resellform">
<h2>Revente de place</h2>
<h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{% if form.spectacle.errors %}<ul class="errorlist"><li>Sélectionnez un spetacle</li></ul>{% endif %}
<p>
<pre>
Bonjour,<br />
<br />
Je souhaite revendre {{ form.count }} place(s) pour {{ form.spectacle }}.<br />
Contactez-moi par email si vous êtes intéressé !<br />
<br />
{{ user.get_full_name }} ({{ user.email }})
</pre>
</p>
<input class="btn btn-primary" type="submit" value="Envoyer" />
<div class="form-group">
<div class="multiple-checkbox">
<ul>
{% for box in resellform.attributions %}
<li>
{{box.tag}}
{{box.choice_label}}
</li>
{% endfor %}
</ul>
</div>
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form>
<br>
{% if annulform.attributions or overdue %}
<h3>Places en cours de revente</h3>
<form action="" method="post">
{% csrf_token %}
<div class="form-group">
<div class="multiple-checkbox">
<ul>
{% for box in annulform.attributions %}
<li>
{{box.tag}}
{{box.choice_label}}
</li>
{% endfor %}
{% for attrib in overdue %}
<li>
<input type="checkbox" style="visibility:hidden">
{{attrib.spectacle}}
</li>
{% endfor %}
</ul>
</div>
</div>
{% if annulform.attributions %}
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
{% endif %}
</form>
{% endif %}
<br>
{% if sold %}
<h3>Places revendues</h3>
<table class="table">
{% for attrib in sold %}
<tr>
<form action="" method="post">
{% csrf_token %}
<td>{{attrib.spectacle}}</td>
<td>{{attrib.revente.soldTo.user.get_full_name}}</td>
<td><button type="submit" class="btn btn-primary" name="transfer"
value="{{attrib.revente.id}}">Transférer</button></td>
<td><button type="submit" class="btn btn-primary" name="reinit"
value="{{attrib.revente.id}}">Réinitialiser</button></td>
</form>
</tr>
{% endfor %}
</table>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,14 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Places disponibles immédiatement</h2>
{% if shotgun %}
<ul class="list-unstyled">
{% for spectacle in shotgun %}
<li><a href="{% url "bda-buy-revente" spectacle.id %}">{{spectacle}}</a></li>
{% endfor %}
{% else %}
<p> Pas de places disponibles immédiatement, désolé !</p>
{% endif %}
{% endblock %}

View file

@ -1,12 +1,8 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %}
<h1>Revente de place</h1>
<p class="success">Votre offre de revente de {{ places }} pour {{ show.title }} le {{ show.date_no_seconds }} ({{ show.location }}) à {{ show.price }}€ a bien été envoyée.</p>
<h2>Revente de place</h2>
<p class="success">Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !</p>
{% endblock %}

View file

@ -0,0 +1,6 @@
{% extends "base_title.html" %}
{% block realcontent %}
<h2>Nope</h2>
<p>Cette revente n'est pas disponible actuellement, désolé !</p>
{% endblock %}

View file

@ -0,0 +1,6 @@
Bonjour {{ vendeur.first_name }} !
Je souhaiterais racheter ta place pour {{ spectacle.title }} le {{ spectacle.date }} ({{ spectacle.location }}) à {{ spectacle.price|floatformat:2 }}€.
Contacte-moi si tu es toujours intéressé·e !
{{ acheteur.get_full_name }} ({{ acheteur.email }})

View file

@ -1,14 +1,14 @@
Bonjour {{ member.get_full_name }},
Bonjour {{ name }},
Nous te rappellons que tu as eu la chance d'obtenir {{ member.nb_attr|pluralize:"une place,deux places" }}
pour {{ show.title }}, le {{ show.date_no_seconds }} au {{ show.location }}. N'oublie pas de t'y rendre !
{% if member.nb_attr == 2 %}
Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }}
pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
{% if nb_attr == 2 %}
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
ces places sont strictement réservées aux personnes de moins de 28 ans.
{% endif %}
{% if show.listing %}Pour ce spectacle, tu as reçu des places sur
listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
pour retirer {{ member.nb_attr|pluralize:"ta place,tes places" }}.
pour retirer {{ nb_attr|pluralize:"ta place,tes places" }}.
{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
été distribués au burô.
{% endif %}

View file

@ -0,0 +1,9 @@
Bonjour {{ acheteur.first_name }},
Tu t'étais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}
pour {{ spectacle.title }}.
Malheureusement, une autre personne a été tirée au sort pour racheter la place.
Tu pourras certainement retenter ta chance pour une autre revente !
À très bientôt,
Le Bureau des Arts

View file

@ -0,0 +1,13 @@
Bonjour {{ vendeur.first_name }},
Tu tes bien inscrit-e pour la revente de {{ spectacle.title }}.
{% with revente.expiration_time as time %}
Le tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu
le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}).
Si personne ne sest inscrit pour racheter la place, celle-ci apparaitra parmi
les « Places disponibles immédiatement à la revente » sur GestioCOF.
{% endwith %}
Bonne revente !
Le Bureau des Arts

View file

@ -0,0 +1,7 @@
Bonjour {{ vendeur.first_name }},
La personne tirée au sort pour racheter ta place pour {{ spectacle.title }} est {{ acheteur.get_full_name }}.
Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail.
Chaleureusement,
Le BdA

View file

@ -0,0 +1,7 @@
Bonjour {{ acheteur.first_name }},
Tu as été tiré-e au sort pour racheter une place pour {{ spectacle.title }} le {{ spectacle.date }} ({{ spectacle.location }}) à {{ spectacle.price|floatformat:2 }}€.
Tu peux contacter le/la vendeur-se à l'adresse {{ vendeur.email }}, ou en répondant à ce mail.
Chaleureusement,
Le BdA

View file

@ -0,0 +1,12 @@
Bonjour {{ user.first_name }}
Une place pour le spectacle {{ spectacle.title }} ({{ spectacle.date }})
a été postée sur BdA-Revente.
Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant
sur ce lien : http://{{ domain }}{% url "bda-revente-interested" revente.id %}.
Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à
un tirage au sort le {{ revente.expiration_time_str }}.
Chaleureusement,
Le BdA

View file

@ -0,0 +1,11 @@
Bonjour {{ user.first_name }}
Une place pour le spectacle {{ spectacle.title }} ({{ spectacle.date }})
a été postée sur BdA-Revente.
Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour
cette place : elle est disponible immédiatement à l'adresse
http://{{ domain }}{% url "bda-buy-revente" spectacle.id %}, à la disposition de tous.
Chaleureusement,
Le BdA

View file

@ -18,7 +18,7 @@
{% for spectacle in spectacles %}
<tr>
<td>{{ spectacle.title }}</td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date_no_seconds }}</td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.slots }}">{{ spectacle.slots }} places</td>
<td data-sort-value="{{ spectacle.total }}">{{ spectacle.total }} demandes</td>

View file

@ -0,0 +1,39 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% block realcontent %}
<h2>Inscriptions pour BdA-Revente</h2>
{% if success %}
<p class="success">Ton inscription a bien été prise en compte !</p>
{% endif %}
{% if deja_revente %}
<p class="success">Des reventes existent déjà pour certains de ces spectacles ; vérifie les places disponibles sans tirage !</p>
{% endif %}
<form action="" class="form-horizontal" method="post">
{% csrf_token %}
<div class="form-group">
<h3>Spectacles</h3>
<br/>
<button type="button" class="btn btn-primary" onClick="select(true)">Tout sélectionner</button>
<button type="button" class="btn btn-primary" onClick="select(false)">Tout désélectionner</button>
<div class="multiple-checkbox">
<ul>
{% for checkbox in form.spectacles %}
<li>{{checkbox}}</li>
{%endfor%}
</ul>
</div>
</div>
<input type="submit" class="btn btn-primary" value="S'inscrire pour les places sélectionnées">
</form>
<script language="JavaScript">
function select(check) {
checkboxes = document.getElementsByName("spectacles");
for(var i=0, n=checkboxes.length;i<n;i++) {
checkboxes[i].checked = check;
}
}
</script>
{% endblock %}

View file

@ -11,7 +11,7 @@
<tr>
<td>{{place.spectacle.title}}</td>
<td>{{place.spectacle.location}}</td>
<td>{{place.spectacle.date_no_seconds}}</td>
<td>{{place.spectacle.date}}</td>
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
</tr>
{% endfor %}

View file

@ -0,0 +1,20 @@
{% extends "base_title.html" %}
{% load staticfiles %}
{%block realcontent %}
<h2>Rachat d'une place</h2>
<form action="" method="post">
{% csrf_token %}
<pre>
Bonjour !
Je souhaiterais racheter ta place pour {{spectacle.title}} le {{spectacle.date}} ({{spectacle.location}}) à {{spectacle.price}}€.
Contacte-moi si tu es toujours intéressé-e !
{{user.get_full_name}} ({{user.email}})
</pre>
<input type="submit" class="btn btn-primary pull-right" value="Envoyer">
</form>
<p class="bda-prix">Note : ce mail sera envoyé à une personne au hasard revendant sa place.</p>
{%endblock%}

View file

@ -19,7 +19,7 @@
{% for spectacle in object_list %}
<tr class="clickable-row" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date_no_seconds }}</td>
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">
{{ spectacle.price |floatformat }}€

View file

@ -32,6 +32,18 @@ urlpatterns = [
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
views.unpaid,
name="bda-unpaid"),
url(r'^liste-revente/(?P<tirage_id>\d+)$',
views.list_revente,
name="bda-liste-revente"),
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
views.buy_revente,
name="bda-buy-revente"),
url(r'^revente-interested/(?P<revente_id>\d+)$',
views.revente_interested,
name='bda-revente-interested'),
url(r'^revente-immediat/(?P<tirage_id>\d+)$',
views.revente_shotgun,
name="bda-shotgun"),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'),

View file

@ -4,27 +4,34 @@ from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
import random
from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from django.db import models
from django.db.models import Count
from django.core import serializers
from django.db import models, transaction
from django.db.models import Count, Q
from django.core import serializers, mail
from django.forms.models import inlineformset_factory
from django.http import HttpResponseBadRequest
from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.conf import settings
import hashlib
from django.core.mail import send_mail
from django.template import loader
from django.utils import timezone
from django.views.generic.list import ListView
import time
from datetime import timedelta
from gestioncof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
Tirage, render_template
Tirage, SpectacleRevente
from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
InscriptionReventeForm
@cof_required
@ -231,6 +238,11 @@ def do_tirage(request, tirage_id):
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)
@ -251,25 +263,6 @@ def tirage(request, tirage_id):
return render(request, "bda-token.html", {"form": form})
def do_resell(request, form):
spectacle = form.cleaned_data["spectacle"]
count = form.cleaned_data["count"]
places = "2 places" if count == "2" else "une place"
mail = """Bonjour,
Je souhaite revendre %s pour %s le %s (%s) à %.02f.
Contactez moi par email si vous êtes intéressé·e·s !
%s (%s)""" % (places, spectacle.title, spectacle.date_no_seconds(),
spectacle.location, spectacle.price,
request.user.get_full_name(), request.user.email)
send_mail("%s" % spectacle, mail,
request.user.email, ["bda-revente@lists.ens.fr"],
fail_silently=False)
return render(request, "bda-success.html",
{"show": spectacle, "places": places})
@login_required
def revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
@ -277,14 +270,226 @@ def revente(request, tirage_id):
user=request.user, tirage=tirage)
if not participant.paid:
return render(request, "bda-notpaid.html", {})
if request.POST:
form = ResellForm(participant, request.POST)
if form.is_valid():
return do_resell(request, form)
if request.method == 'POST':
if 'resell' in request.POST:
resellform = ResellForm(participant, request.POST, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
if resellform.is_valid():
mails = []
attributions = resellform.cleaned_data["attributions"]
with transaction.atomic():
for attribution in attributions:
revente, created = SpectacleRevente.objects.get_or_create(
attribution=attribution,
defaults={'seller': participant})
if not created:
revente.seller = participant
revente.date = timezone.now()
mail_subject = "BdA-Revente : {:s}".format(attribution.spectacle.title)
mail_body = loader.render_to_string('bda/mails/revente-new.txt', {
'vendeur': participant.user,
'spectacle': attribution.spectacle,
'revente': revente,
})
mails.append(mail.EmailMessage(
mail_subject, mail_body,
from_email=settings.MAIL_DATA['revente']['FROM'],
to=[participant.user.email],
reply_to=[settings.MAIL_DATA['revente']['REPLYTO']],
))
revente.save()
mail.get_connection().send_messages(mails)
elif 'annul' in request.POST:
annulform = AnnulForm(participant, request.POST, prefix='annul')
resellform = ResellForm(participant, prefix='resell')
if annulform.is_valid():
attributions = annulform.cleaned_data["attributions"]
for attribution in attributions:
attribution.revente.delete()
elif 'transfer' in request.POST:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['transfer'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
attrib = revente.attribution
attrib.participant = revente.soldTo
attrib.save()
elif 'reinit' in request.POST:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
revente_id = request.POST['reinit'][0]
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
id=revente_id)
if rev.exists():
revente = rev.get()
if revente.attribution.spectacle.date > timezone.now():
revente.date = timezone.now() - timedelta(hours=1)
revente.soldTo = None
revente.notif_sent = False
revente.tirage_done = False
if revente.answered_mail:
revente.answered_mail.clear()
revente.save()
else:
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
else:
form = ResellForm(participant)
resellform = ResellForm(participant, prefix='resell')
annulform = AnnulForm(participant, prefix='annul')
overdue = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__seller=participant,
revente__date__lte=timezone.now()-timedelta(hours=1)).filter(
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
sold = participant.attribution_set.filter(
spectacle__date__gte=timezone.now(),
revente__isnull=False,
revente__soldTo__isnull=False).exclude(
revente__soldTo=participant)
return render(request, "bda-revente.html",
{"form": form, 'tirage': tirage})
{'tirage': tirage, 'overdue': overdue, "sold": sold,
"annulform": annulform, "resellform": resellform})
@login_required
def revente_interested(request, revente_id):
revente = get_object_or_404(SpectacleRevente, id=revente_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=revente.attribution.spectacle.tirage)
if timezone.now() < revente.date + timedelta(hours=1) or revente.shotgun:
return render(request, "bda-wrongtime.html", {})
revente.answered_mail.add(participant)
return render(request, "bda-interested.html",
{"spectacle": revente.attribution.spectacle,
"date": revente.expiration_time})
@login_required
def list_revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
spectacles = tirage.spectacle_set.filter(
date__gte=timezone.now())
shotgun = []
deja_revente = False
success = False
for spectacle in spectacles:
revente_objects = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
revente_count = 0
for revente in revente_objects:
if revente.shotgun:
revente_count += 1
if revente_count:
spectacle.revente_count = revente_count
shotgun.append(spectacle)
if request.method == 'POST':
form = InscriptionReventeForm(tirage, request.POST)
if form.is_valid():
choices = form.cleaned_data['spectacles']
participant.choicesrevente = choices
participant.save()
for spectacle in choices:
qset = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle)
if qset.exists():
# On l'inscrit à l'un des tirages au sort
for revente in qset.all():
if revente.shotgun and not revente.soldTo:
deja_revente = True
else:
revente.answered_mail.add(participant)
revente.save()
break
success = True
else:
form = InscriptionReventeForm(
tirage,
initial={'spectacles': participant.choicesrevente.all()})
return render(request, "liste-reventes.html",
{"form": form, 'shotgun': shotgun,
"deja_revente": deja_revente, "success": success})
@login_required
def buy_revente(request, spectacle_id):
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
tirage = spectacle.tirage
participant, created = Participant.objects.get_or_create(
user=request.user, tirage=tirage)
reventes = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
if reventes.filter(seller=participant).exists():
revente = reventes.filter(seller=participant)[0]
revente.delete()
return HttpResponseRedirect(reverse("bda-shotgun",
args=[tirage.id]))
reventes_shotgun = []
for revente in reventes.all():
if revente.shotgun:
reventes_shotgun.append(revente)
if not reventes_shotgun:
return render(request, "bda-no-revente.html", {})
if request.POST:
revente = random.choice(reventes_shotgun)
revente.soldTo = participant
revente.save()
mail = loader.render_to_string('bda/mails/buy-shotgun.txt', {
'spectacle': spectacle,
'acheteur': request.user,
'vendeur': revente.seller.user,
})
send_mail("BdA-Revente : %s" % spectacle.title, mail,
request.user.email,
[revente.seller.user.email],
fail_silently=False)
return render(request, "bda-success.html",
{"seller": revente.attribution.participant.user,
"spectacle": spectacle})
return render(request, "revente-confirm.html",
{"spectacle": spectacle,
"user": request.user})
@login_required
def revente_shotgun(request, tirage_id):
tirage = get_object_or_404(Tirage, id=tirage_id)
spectacles = tirage.spectacle_set.filter(
date__gte=timezone.now())
shotgun = []
for spectacle in spectacles:
revente_objects = SpectacleRevente.objects.filter(
attribution__spectacle=spectacle,
soldTo__isnull=True)
revente_count = 0
for revente in revente_objects:
if revente.shotgun:
revente_count += 1
if revente_count:
shotgun.append(spectacle)
return render(request, "bda-shotgun.html",
{"shotgun": shotgun})
@buro_required
@ -345,11 +550,11 @@ def send_rappel(request, spectacle_id):
# Mails d'exemples
fake_member = request.user
fake_member.nb_attr = 1
exemple_mail_1place = render_template('mail-rappel.txt', {
exemple_mail_1place = loader.render_to_string('bda/mails/rappel.txt', {
'member': fake_member,
'show': show})
fake_member.nb_attr = 2
exemple_mail_2places = render_template('mail-rappel.txt', {
exemple_mail_2places = loader.render_to_string('bda/mails/rappel.txt', {
'member': fake_member,
'show': show})
# Contexte

0
cof/locale/__init__.py Normal file
View file

View file

9
cof/locale/fr/formats.py Normal file
View file

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

View file

@ -29,9 +29,6 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1']
# Application definition
INSTALLED_APPS = (
'gestioncof',
@ -56,6 +53,7 @@ INSTALLED_APPS = (
)
MIDDLEWARE_CLASSES = (
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
@ -101,6 +99,7 @@ DATABASES = {
'NAME': os.environ['DBNAME'],
'USER': os.environ['DBUSER'],
'PASSWORD': os.environ['DBPASSWD'],
'HOST': os.environ.get('DBHOST', 'localhost'),
}
}
@ -126,7 +125,7 @@ STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/static/'
STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static/'),
os.path.join(BASE_DIR, 'static/'),
)
# Media upload (through ImageField, SiteField)
@ -138,26 +137,28 @@ MEDIA_URL = '/media/'
# Various additional settings
SITE_ID = 1
# URL prefix for admin static files -- CSS, JavaScript and images.
# Make sure to use a trailing slash.
# Examples: "http://foo.com/static/admin/", "/static/admin/".
ADMIN_MEDIA_PREFIX = '/static/grappelli/'
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>"
PETITS_COURS_FROM = "Le COF <cof@ens.fr>"
PETITS_COURS_BCC = "archivescof@gmail.com"
PETITS_COURS_REPLYTO = "cof@ens.fr"
MAIL_DATA = {
'petits_cours': {
'FROM': "Le COF <cof@ens.fr>",
'BCC': "archivescof@gmail.com",
'REPLYTO': "cof@ens.fr"},
'rappels': {
'FROM': 'Le BdA <bda@ens.fr>',
'REPLYTO': 'Le BdA <bda@ens.fr>'},
'revente': {
'FROM': 'BdA-Revente <bda-revente@ens.fr>',
'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'},
}
RAPPEL_FROM = 'Le BdA <bda@ens.fr>'
RAPPEL_REPLY_TO = RAPPEL_FROM
LOGIN_URL = "/gestion/login"
LOGIN_REDIRECT_URL = "/gestion/"
LOGIN_URL = "cof-login"
LOGIN_REDIRECT_URL = "home"
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = '/gestion/'
CAS_REDIRECT_URL = '/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
@ -177,7 +178,7 @@ CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": {
"hosts": [("localhost", 6379)],
"hosts": [(os.environ.get("REDIS_HOST", "localhost"), 6379)],
},
"ROUTING": "cof.routing.channel_routing",
}
@ -200,3 +201,5 @@ def show_toolbar(request):
DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
}
FORMAT_MODULE_PATH = 'cof.locale'

View file

@ -1,30 +1,33 @@
# -*- coding: utf-8 -*-
"""
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
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib import admin
from django.views.generic.base import TemplateView
import autocomplete_light
from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views
from gestioncof import views as gestioncof_views, csv_views
from gestioncof.urls import export_patterns, petitcours_patterns, \
surveys_patterns, events_patterns, calendar_patterns, \
clubs_patterns
from gestioncof.autocomplete import autocomplete
autocomplete_light.autodiscover()
admin.autodiscover()
my_urlpatterns = [
urlpatterns = [
# Page d'accueil
url(r'^$', gestioncof_views.home, name='home'),
# Le BdA
@ -48,7 +51,7 @@ my_urlpatterns = [
url(r'^cas/logout$', django_cas_views.logout),
url(r'^outsider/login$', gestioncof_views.login_ext),
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
url(r'^login$', gestioncof_views.login),
url(r'^login$', gestioncof_views.login, name="cof-login"),
url(r'^logout$', gestioncof_views.logout),
# Infos persos
url(r'^profile$', gestioncof_views.profile),
@ -81,14 +84,16 @@ my_urlpatterns = [
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^k-fet/', include('kfet.urls'))
] + \
(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
if settings.DEBUG
else [])
# Si on est en production, MEDIA_ROOT est servi par Apache.
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
urlpatterns = [
url(r'^gestion/', include(my_urlpatterns))
url(r'^k-fet/', include('kfet.urls')),
]
if settings.DEBUG:
import debug_toolbar
urlpatterns += [
url(r'^__debug__/', include(debug_toolbar.urls)),
]
# Si on est en production, MEDIA_ROOT est servi par Apache.
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT)

View file

@ -144,9 +144,9 @@ User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement")
User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True)
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BDA", True)
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BdA", True)
User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente",
"ML BDA-R", True)
"ML BdA-R", True)
class UserProfileAdmin(UserAdmin):

View file

@ -14,7 +14,7 @@ from django.contrib.auth.models import User
from django.views.generic import ListView
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.template import loader, Context
from django.template import loader
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.db.models import Min
@ -30,13 +30,7 @@ from captcha.fields import ReCaptchaField
from datetime import datetime
import base64
import simplejson
def render_template(template_path, data):
tmpl = loader.get_template(template_path)
context = Context(data)
return tmpl.render(context)
import json
class DemandeListView(ListView):
@ -137,14 +131,14 @@ 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 = render_template("petits-cours-mail-demandeur.txt",
{"proposals": proposals,
"unsatisfied": unsatisfied,
"extra":
'<textarea name="extra" '
'style="width:99%; height: 90px;">'
'</textarea>'
})
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
"proposals": proposals,
"unsatisfied": unsatisfied,
"extra":
'<textarea name="extra" '
'style="width:99%; height: 90px;">'
'</textarea>'
})
return render(request, "traitement_demande_petit_cours.html",
{"demande": demande,
"unsatisfied": unsatisfied,
@ -153,7 +147,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
"proposed_mails": proposed_mails,
"mainmail": mainmail,
"attribdata":
base64.b64encode(simplejson.dumps(attribdata)
base64.b64encode(json.dumps(attribdata)
.encode('utf_8')),
"redo": redo,
"errors": errors,
@ -163,8 +157,10 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
def _generate_eleve_email(demande, proposed_for):
proposed_mails = []
for user, matieres in proposed_for:
msg = render_template("petits-cours-mail-eleve.txt",
{"demande": demande, "matieres": matieres})
msg = loader.render_to_string("petits-cours-mail-eleve.txt", {
"demande": demande,
"matieres": matieres
})
proposed_mails.append((user, msg))
return proposed_mails
@ -262,7 +258,7 @@ def _traitement_post(request, demande):
extra = request.POST["extra"].strip()
redo = "redo" in request.POST
attribdata = request.POST["attribdata"]
attribdata = dict(simplejson.loads(base64.b64decode(attribdata)))
attribdata = dict(json.loads(base64.b64decode(attribdata)))
for matiere in demande.matieres.all():
if matiere.id not in attribdata:
unsatisfied.append(matiere)
@ -278,13 +274,14 @@ def _traitement_post(request, demande):
proposals_list = proposals.items()
proposed_for = proposed_for.items()
proposed_mails = _generate_eleve_email(demande, proposed_for)
mainmail = render_template("petits-cours-mail-demandeur.txt",
{"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra})
frommail = settings.PETITS_COURS_FROM
bccaddress = settings.PETITS_COURS_BCC
replyto = settings.PETITS_COURS_REPLYTO
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
"proposals": proposals_list,
"unsatisfied": unsatisfied,
"extra": extra,
})
frommail = settings.MAIL_DATA['petits_cours']['FROM']
bccaddress = settings.MAIL_DATA['petits_cours']['BCC']
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,

View file

@ -29,6 +29,12 @@ class COFCASBackend(CASBackend):
request.session['attributes'] = attributes
if not username:
return None
# Le CAS de l'ENS accepte les logins avec des espaces au début
# et à la fin, ainsi quavec une casse variable. On normalise pour
# éviter les doublons.
username = username.strip().lower()
profiles = CofProfile.objects.filter(login_clipper=username)
if len(profiles) > 0:
profile = profiles.order_by('-is_cof')[0]

View file

@ -43,6 +43,8 @@
{% else %}
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li>
<li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li>
<li><a href="{% url "bda-liste-revente" tirage.id %}">S'inscrire à BdA-Revente</a></li>
<li><a href="{% url "bda-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
{% endif %}
</ul>
{% endfor %}
@ -82,7 +84,7 @@
{% endfor %}
</ul>
</div>
<h3 class="block-title">Gestion tirages BDA<span class="pull-right glyphicon glyphicon-list"></span></h3>
<h3 class="block-title">Gestion tirages BdA<span class="pull-right glyphicon glyphicon-list"></span></h3>
<div class="hm-block">
{% if active_tirages %}
{% for tirage in active_tirages %}

View file

@ -14,6 +14,7 @@ from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.contrib.auth.decorators import login_required
from django.contrib.auth.views import login as django_login_view
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.utils import timezone
import django.utils.six as six
@ -710,12 +711,15 @@ def calendar_ics(request, token):
tirage__active=True)
shows = shows.distinct()
vcal = Calendar()
site = Site.objects.get_current()
for show in shows:
vevent = Vevent()
vevent.add('dtstart', show.date)
vevent.add('dtend', show.date + timedelta(seconds=7200))
vevent.add('summary', show.title)
vevent.add('location', show.location.name)
vevent.add('uid', 'show-{:d}-{:d}@{:s}'.format(
show.pk, show.tirage_id, site.domain))
vcal.add_component(vevent)
if subscription.subscribe_to_events:
for event in Event.objects.filter(old=False).all():
@ -725,6 +729,8 @@ def calendar_ics(request, token):
vevent.add('summary', event.title)
vevent.add('location', event.location)
vevent.add('description', event.description)
vevent.add('uid', 'event-{:d}@{:s}'.format(
event.pk, site.domain))
vcal.add_component(vevent)
response = HttpResponse(content=vcal.to_ical())
response['Content-Type'] = "text/calendar"

View file

@ -8,7 +8,5 @@ from channels.routing import route, route_class
from kfet import consumers
channel_routing = [
route_class(consumers.KPsul, path=r"^/gestion/ws/k-fet/k-psul/$"),
#route("websocket.connect", ws_kpsul_history_connect),
#route('websocket.receive', ws_message)
route_class(consumers.KPsul, path=r"^ws/k-fet/k-psul/$"),
]

View file

@ -924,7 +924,7 @@ $(document).ready(function() {
}
});
},
onClose: function() { this._lastFocused = articleSelect; }
onClose: function() { this._lastFocused = (articleSelect.val() ? articleNb : articleSelect) ; }
});
}

View file

@ -34,12 +34,12 @@
<tr class="transfer_form" id="{{ form.prefix }}">
<td class="from_acc_data"></td>
<td class="from_acc">
<input type="text" name="from_acc" class="input_from_acc" autocomplete="off" spellcheck="false">
<input type="text" name="from_acc" class="input_from_acc" maxlength="3" autocomplete="off" spellcheck="false">
{{ form.from_acc }}
</td>
<td class="amount">{{ form.amount }}</td>
<td class="to_acc">
<input type="text" name="to_acc" class="input_to_acc" autocomplete="off" spellcheck="false">
<input type="text" name="to_acc" class="input_to_acc" maxlength="3" autocomplete="off" spellcheck="false">
{{ form.to_acc }}
</td>
<td class="to_acc_data"></td>
@ -71,18 +71,21 @@ $(document).ready(function () {
var $next = $form.next('.transfer_form').find('.from_acc input');
}
var $input_id = $input.next('input');
getAccountData(trigramme, function(data) {
$input_id.val(data.id);
$data.text(data.name);
$next.focus();
});
if (isValidTrigramme(trigramme)) {
getAccountData(trigramme, function(data) {
$input_id.val(data.id);
$data.text(data.name);
$next.focus();
});
} else {
$input_id.val('');
$data.text('');
}
}
$('.input_from_acc, .input_to_acc').on('input', function() {
var tri = $(this).val().toUpperCase();
if (isValidTrigramme(tri)) {
updateAccountData(tri, $(this));
}
updateAccountData(tri, $(this));
});
$('#transfers_form').on('submit', function(e) {

View file

@ -6,6 +6,16 @@
ProxyRequests Off
ProxyPass /static/ !
ProxyPass /media/ !
# Pour utiliser un sous-dossier (typiquement /gestion/), il faut faire a la
# place des lignes suivantes:
#
# RequestHeader set Daphne-Root-Path /gestion
# ProxyPass /gestion/ws/ ws://127.0.0.1:8001/gestion/ws/
# ProxyPass /gestion http://127.0.0.1:8001/gestion
# ProxyPassReverse /gestion http://127.0.0.1:8001/gestion
#
# Penser egalement a changer les /static/ et /media/ dans la config apache
# ainsi que dans les settings django.
ProxyPass /ws/ ws://127.0.0.1:8001/ws/
ProxyPass / http://127.0.0.1:8001/
ProxyPassReverse / http://127.0.0.1:8001/

View file

@ -8,8 +8,9 @@ DBNAME="cof_gestion"
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
# Installation de paquets utiles
apt-get update && apt-get install -y mercurial python-pip python-dev \
apt-get update && apt-get install -y python3-pip python3-dev python3-venv \
libmysqlclient-dev libjpeg-dev git redis-server
pip install -U pip
# Configuration et installation de mysql. Le mot de passe root est le même que
# le mot de passe pour l'utilisateur local - pour rappel, ceci est une instance
@ -23,16 +24,16 @@ mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $D
# Installation et configuration d'Apache
apt-get install -y apache2
a2enmod proxy proxy_http
a2enmod proxy proxy_http proxy_wstunnel headers
cp /vagrant/provisioning/apache.conf /etc/apache2/sites-available/gestiocof.conf
a2ensite gestiocof
a2dissite 000-default
service apache2 restart
mkdir /var/www/static
chown -R vagrant:www-data /var/www/static
chown -R ubuntu:www-data /var/www/static
# Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh`
cat > ~vagrant/.bash_profile <<EOF
cat >> ~ubuntu/.bashrc <<EOF
# On utilise la version de développement de GestioCOF
export DJANGO_SETTINGS_MODULE='cof.settings_dev'
@ -44,25 +45,29 @@ export DBPASSWD="$DBPASSWD"
# Permet d'utiliser les utilitaires pythons locaux
export PATH="\$PATH:\$HOME/.local/bin"
# Charge le virtualenv
source ~/venv/bin/activate
# On va dans /vagrant où se trouve le code de gestioCOF
cd /vagrant
EOF
chown vagrant: ~vagrant/.bash_profile
# On va dans /vagrant où se trouve gestioCOF
cd /vagrant
# Installation des dépendances python
sudo -H -u vagrant pip install --user -r requirements.txt -r requirements-devel.txt
# Installation du virtualenv, on utilise désormais python3
sudo -H -u ubuntu python3 -m venv ~ubuntu/venv
sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -U pip
sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r requirements.txt -r requirements-devel.txt
# Préparation de Django
sudo -H -u vagrant DJANGO_SETTINGS_MODULE='cof.settings_dev' DBUSER=$DBUSER DBNAME=$DBNAME DBPASSWD=$DBPASSWD sh provisioning/prepare_django.sh
sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings_dev' DBUSER=$DBUSER DBNAME=$DBNAME DBPASSWD=$DBPASSWD bash provisioning/prepare_django.sh
# Installation du cron pour les mails de rappels
sudo -H -u vagrant crontab provisioning/cron.dev
sudo -H -u ubuntu crontab provisioning/cron.dev
# On installe Daphne et on demande à supervisor de le lancer
pip install daphne
sudo -H -u ubuntu ~ubuntu/venv/bin/pip install daphne
apt-get install -y supervisor
cp /vagrant/provisioning/supervisor.conf /etc/supervisor/conf.d/gestiocof.conf
sed "s/{DBUSER}/$DBUSER/" -i /etc/supervisor/conf.d/gestiocof.conf

View file

@ -7,3 +7,4 @@ DBNAME="cof_gestion"
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
19 */12 * * * date >> /vagrant/rappels.log ; python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1
*/5 * * * * python /vagrant/manage.py manage_revente >> /vagrant/reventes.log 2>&1

View file

@ -14,3 +14,14 @@ envoyés).
- Garde les logs peut être une bonne idée.
Exemple : voir le fichier `provisioning/cron.dev`.
## Gestion des mails de revente
Il faut effectuer très régulièrement la commande `manage_reventes` de GestioCOF,
qui gère toutes les actions associées à BdA-Revente : envoi des mails de notification,
tirages.
- Pour l'instant un délai de 5 min est hardcodé
- Garde des logs ; ils vont finir par être assez lourds si on a beaucoup de reventes.
Exemple : provisioning/cron.dev

View file

@ -1,5 +1,7 @@
#!/bin/bash
# Doit être lancé par bootstrap.sh
source ~/venv/bin/activate
python manage.py migrate
python manage.py loaddata users root bda gestion sites
python manage.py collectstatic --noinput

View file

@ -1,7 +1,7 @@
[program:worker]
command=/usr/bin/python /vagrant/manage.py runworker
command=/home/ubuntu/venv/bin/python /vagrant/manage.py runworker
directory=/vagrant/
user=vagrant
user=ubuntu
environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev"
autostart=true
autorestart=true
@ -10,11 +10,11 @@ stopasgroup=true
redirect_stderr=true
[program:interface]
command=/usr/local/bin/daphne -b 127.0.0.1 -p 8001 cof.asgi:channel_layer
command=/home/ubuntu/venv/bin/daphne -b 127.0.0.1 -p 8001 cof.asgi:channel_layer
environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev"
directory=/vagrant/
redirect_stderr=true
autostart=true
autorestart=true
stopasgroup=true
user=vagrant
user=ubuntu

View file

@ -1,2 +1,3 @@
-r requirements.txt
django-debug-toolbar
ipython

View file

@ -1,13 +1,12 @@
configparser==3.5.0
Django==1.8
Django==1.8.*
django-autocomplete-light==2.3.3
django-autoslug==1.9.3
git+https://github.com/xapantu/django-cas-ng.git#egg=django-cas-ng
django-cas-ng==3.5.5
django-grappelli==2.8.1
django-recaptcha==1.0.5
mysqlclient==1.3.7
Pillow==3.3.0
simplejson==3.8.2
six==1.10.0
unicodecsv==0.14.1
icalendar==3.10
@ -15,7 +14,7 @@ django-bootstrap-form==3.2.1
asgiref==0.14.0
daphne==0.14.3
asgi-redis==0.14.0
-e git+https://github.com/Aureplop/channels.git#egg=channel
git+https://github.com/Aureplop/channels.git#egg=channel
statistics==1.0.3.5
future==0.15.2
django-widget-tweaks==1.4.1