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 # For a complete reference, please see the online documentation at
# https://docs.vagrantup.com. # 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 # On associe le port 80 dans la machine virtuelle avec le port 8080 de notre
# ordinateur, et le port 8000 avec le port 8000. # 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 __future__ import unicode_literals
from django.core.mail import send_mail from django.core.mail import send_mail
from django.contrib import admin from django.contrib import admin
from django.db.models import Sum, Count from django.db.models import Sum, Count
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\ from django.template.defaultfilters import pluralize
Attribution, Tirage, Quote, CategorieSpectacle from django.utils import timezone
from django import forms from django import forms
from bda.models import Spectacle, Salle, Participant, ChoixSpectacle,\
Attribution, Tirage, Quote, CategorieSpectacle, SpectacleRevente
from datetime import timedelta from datetime import timedelta
@ -210,6 +211,70 @@ class SalleAdmin(admin.ModelAdmin):
search_fields = ('name', 'address') 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(CategorieSpectacle)
admin.site.register(Spectacle, SpectacleAdmin) admin.site.register(Spectacle, SpectacleAdmin)
admin.site.register(Salle, SalleAdmin) admin.site.register(Salle, SalleAdmin)
@ -217,3 +282,4 @@ admin.site.register(Participant, ParticipantAdmin)
admin.site.register(Attribution, AttributionAdmin) admin.site.register(Attribution, AttributionAdmin)
admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin) admin.site.register(ChoixSpectacle, ChoixSpectacleAdmin)
admin.site.register(Tirage, TirageAdmin) 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 print_function
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import timedelta
from django import forms from django import forms
from django.forms.models import BaseInlineFormSet 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): class BaseBdaFormSet(BaseInlineFormSet):
@ -35,17 +39,47 @@ class TokenForm(forms.Form):
token = forms.CharField(widget=forms.widgets.Textarea()) token = forms.CharField(widget=forms.widgets.Textarea())
class SpectacleModelChoiceField(forms.ModelChoiceField): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField):
def label_from_instance(self, obj): def label_from_instance(self, obj):
return "%s le %s (%s) à %.02f" % (obj.title, obj.date_no_seconds(), return "%s" % obj.spectacle
obj.location, obj.price)
class ResellForm(forms.Form): class ResellForm(forms.Form):
count = forms.ChoiceField(choices=(("1", "1"), ("2", "2"),)) attributions = AttributionModelMultipleChoiceField(
spectacle = SpectacleModelChoiceField(queryset=Spectacle.objects.none()) queryset=Attribution.objects.none(),
widget=forms.CheckboxSelectMultiple,
required=False)
def __init__(self, participant, *args, **kwargs): def __init__(self, participant, *args, **kwargs):
super(ResellForm, self).__init__(*args, **kwargs) super(ResellForm, self).__init__(*args, **kwargs)
self.fields['spectacle'].queryset = participant.attributions.all() \ self.fields['attributions'].queryset = participant.attribution_set\
.distinct() .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 -*- # -*- coding: utf-8 -*-
"""
Gestion en ligne de commande des mails de rappel.
"""
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import timedelta
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone from django.utils import timezone
from datetime import timedelta
from bda.models import Spectacle from bda.models import Spectacle
class Command(BaseCommand): class Command(BaseCommand):
help = 'Envoie les mails de rappel des spectacles dont la date ' \ help = 'Envoie les mails de rappel des spectacles dont la date ' \
'approche.\nNe renvoie pas les mails déjà envoyés.' 'approche.\nNe renvoie pas les mails déjà envoyés.'
leave_locale_alone = True
def handle(self, *args, **options): def handle(self, *args, **options):
now = timezone.now() 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 from __future__ import unicode_literals
import calendar import calendar
import random
from datetime import timedelta
from django.contrib.sites.models import Site
from django.db import models from django.db import models
from django.contrib.auth.models import User 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.core import mail
from django.conf import settings 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 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 @python_2_unicode_compatible
class Tirage(models.Model): class Tirage(models.Model):
title = models.CharField("Titre", max_length=300) title = models.CharField("Titre", max_length=300)
@ -31,12 +28,9 @@ class Tirage(models.Model):
enable_do_tirage = models.BooleanField("Le tirage peut être lancé", enable_do_tirage = models.BooleanField("Le tirage peut être lancé",
default=False) default=False)
def date_no_seconds(self):
return self.fermeture.astimezone(timezone.get_current_timezone()) \
.strftime('%d %b %Y %H:%M')
def __str__(self): 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 @python_2_unicode_compatible
@ -83,42 +77,46 @@ class Spectacle(models.Model):
verbose_name = "Spectacle" verbose_name = "Spectacle"
ordering = ("date", "title",) ordering = ("date", "title",)
def __repr__(self):
return "[%s]" % self
def timestamp(self): def timestamp(self):
return "%d" % calendar.timegm(self.date.utctimetuple()) 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): def __str__(self):
return "%s - %s, %s, %.02f" % (self.title, self.date_no_seconds(), return "%s - %s, %s, %.02f" % (
self.location, self.price) self.title,
formats.localize(timezone.template_localtime(self.date)),
self.location,
self.price
)
def send_rappel(self): 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 # On récupère la liste des participants
members = {} members = {}
for attr in Attribution.objects.filter(spectacle=self).all(): for attr in Attribution.objects.filter(spectacle=self).all():
member = attr.participant.user member = attr.participant.user
if member.id in members: if member.id in members:
members[member.id].nb_attr = 2 members[member.id][1] = 2
else: else:
member.nb_attr = 1 members[member.id] = [member.first_name, 1, member.email]
members[member.id] = member # Pour le BdA
members[0] = ['BdA', 1, 'bda@ens.fr']
members[-1] = ['BdA', 2, 'bda@ens.fr']
# On écrit un mail personnalisé à chaque participant # On écrit un mail personnalisé à chaque participant
mails_to_send = [] mails_to_send = []
mail_object = "%s - %s - %s" % (self.title, self.date_no_seconds(), mail_object = str(self)
self.location)
for member in members.values(): for member in members.values():
mail_body = render_template('mail-rappel.txt', { mail_body = loader.render_to_string('bda/mails/rappel.txt', {
'member': member, 'name': member[0],
'show': self}) 'nb_attr': member[1],
'show': self})
mail_tot = mail.EmailMessage( mail_tot = mail.EmailMessage(
mail_object, mail_body, mail_object, mail_body,
settings.RAPPEL_FROM, [member.email], settings.MAIL_DATA['rappels']['FROM'], [member[2]],
[], headers={'Reply-To': settings.RAPPEL_REPLY_TO}) [], headers={
'Reply-To': settings.MAIL_DATA['rappels']['REPLYTO']})
mails_to_send.append(mail_tot) mails_to_send.append(mail_tot)
# On envoie les mails # On envoie les mails
connection = mail.get_connection() connection = mail.get_connection()
@ -158,6 +156,9 @@ class Participant(models.Model):
max_length=6, choices=PAYMENT_TYPES, max_length=6, choices=PAYMENT_TYPES,
blank=True) blank=True)
tirage = models.ForeignKey(Tirage) tirage = models.ForeignKey(Tirage)
choicesrevente = models.ManyToManyField(Spectacle,
related_name="subscribed",
blank=True)
def __str__(self): def __str__(self):
return "%s - %s" % (self.user, self.tirage.title) return "%s - %s" % (self.user, self.tirage.title)
@ -205,4 +206,170 @@ class Attribution(models.Model):
given = models.BooleanField("Donnée", default=False) given = models.BooleanField("Donnée", default=False)
def __str__(self): 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 %} {% for show, members, losers in results %}
<div class="attribresult"> <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> <p>
<strong>{{ show.nrequests }} demandes pour {{ show.slots }} places</strong> <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 %} {{ 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" %} {% extends "base_title.html" %}
{% block realcontent %} {% block realcontent %}
<h2><strong>Nope</strong></h1> <h2><strong>Nope</strong></h2>
<p>Avant de revendre des places, il faut aller les payer !</p> <p>Avant de revendre des places, il faut aller les payer !</p>
{% endblock %} {% endblock %}

View file

@ -1,26 +1,73 @@
{% extends "base_title.html" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load bootstrap %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %} {% block realcontent %}
<h2>Revente de place</h1> <h2>Revente de place</h2>
<form action="" method="post" id="resellform"> <h3>Places non revendues</h3>
<form class="form-horizontal" action="" method="post">
{% csrf_token %} {% csrf_token %}
{% if form.spectacle.errors %}<ul class="errorlist"><li>Sélectionnez un spetacle</li></ul>{% endif %} <div class="form-group">
<p> <div class="multiple-checkbox">
<pre> <ul>
Bonjour,<br /> {% for box in resellform.attributions %}
<br /> <li>
Je souhaite revendre {{ form.count }} place(s) pour {{ form.spectacle }}.<br /> {{box.tag}}
Contactez-moi par email si vous êtes intéressé !<br /> {{box.choice_label}}
<br /> </li>
{{ user.get_full_name }} ({{ user.email }}) {% endfor %}
</pre> </ul>
</p> </div>
<input class="btn btn-primary" type="submit" value="Envoyer" /> </div>
<div class="form-actions">
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
</div>
</form> </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 %} {% 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" %} {% extends "base_title.html" %}
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %}
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
{% endblock %}
{% block realcontent %} {% block realcontent %}
<h1>Revente de place</h1> <h2>Revente de place</h2>
<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> <p class="success">Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !</p>
{% endblock %} {% 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" }} Nous te rappellons que tu as eu la chance d'obtenir {{ 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 ! pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
{% if member.nb_attr == 2 %} {% if nb_attr == 2 %}
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que 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. ces places sont strictement réservées aux personnes de moins de 28 ans.
{% endif %} {% endif %}
{% if show.listing %}Pour ce spectacle, tu as reçu des places sur {% 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 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 {% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
été distribués au burô. été distribués au burô.
{% endif %} {% 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 %} {% for spectacle in spectacles %}
<tr> <tr>
<td>{{ spectacle.title }}</td> <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.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.slots }}">{{ spectacle.slots }} places</td> <td data-sort-value="{{ spectacle.slots }}">{{ spectacle.slots }} places</td>
<td data-sort-value="{{ spectacle.total }}">{{ spectacle.total }} demandes</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> <tr>
<td>{{place.spectacle.title}}</td> <td>{{place.spectacle.title}}</td>
<td>{{place.spectacle.location}}</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> <td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
</tr> </tr>
{% endfor %} {% 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 %} {% for spectacle in object_list %}
<tr class="clickable-row" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}"> <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><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.location }}">{{ spectacle.location }}</td>
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}"> <td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">
{{ spectacle.price |floatformat }}€ {{ spectacle.price |floatformat }}€

View file

@ -32,6 +32,18 @@ urlpatterns = [
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$', url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
views.unpaid, views.unpaid,
name="bda-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'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles, url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'), name='bda-descriptions'),

View file

@ -4,27 +4,34 @@ from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import random
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.db import models from django.db import models, transaction
from django.db.models import Count from django.db.models import Count, Q
from django.core import serializers from django.core import serializers, mail
from django.forms.models import inlineformset_factory 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 import hashlib
from django.core.mail import send_mail from django.core.mail import send_mail
from django.template import loader
from django.utils import timezone from django.utils import timezone
from django.views.generic.list import ListView from django.views.generic.list import ListView
import time import time
from datetime import timedelta
from gestioncof.decorators import cof_required, buro_required from gestioncof.decorators import cof_required, buro_required
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\ from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
Tirage, render_template Tirage, SpectacleRevente
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
InscriptionReventeForm
@cof_required @cof_required
@ -231,6 +238,11 @@ def do_tirage(request, tirage_id):
Attribution(spectacle=show, participant=member) Attribution(spectacle=show, participant=member)
for show, members, _ in results for show, members, _ in results
for member, _, _, _ in members]) 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) return render(request, "bda-attrib-extra.html", data)
else: else:
return render(request, "bda-attrib.html", data) return render(request, "bda-attrib.html", data)
@ -251,25 +263,6 @@ def tirage(request, tirage_id):
return render(request, "bda-token.html", {"form": form}) 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 @login_required
def revente(request, tirage_id): def revente(request, tirage_id):
tirage = get_object_or_404(Tirage, id=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) user=request.user, tirage=tirage)
if not participant.paid: if not participant.paid:
return render(request, "bda-notpaid.html", {}) return render(request, "bda-notpaid.html", {})
if request.POST: if request.method == 'POST':
form = ResellForm(participant, request.POST) if 'resell' in request.POST:
if form.is_valid(): resellform = ResellForm(participant, request.POST, prefix='resell')
return do_resell(request, form) 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: 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", 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 @buro_required
@ -345,11 +550,11 @@ def send_rappel(request, spectacle_id):
# Mails d'exemples # Mails d'exemples
fake_member = request.user fake_member = request.user
fake_member.nb_attr = 1 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, 'member': fake_member,
'show': show}) 'show': show})
fake_member.nb_attr = 2 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, 'member': fake_member,
'show': show}) 'show': show})
# Contexte # 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! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['127.0.0.1']
# Application definition # Application definition
INSTALLED_APPS = ( INSTALLED_APPS = (
'gestioncof', 'gestioncof',
@ -56,6 +53,7 @@ INSTALLED_APPS = (
) )
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'debug_toolbar.middleware.DebugToolbarMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
@ -101,6 +99,7 @@ DATABASES = {
'NAME': os.environ['DBNAME'], 'NAME': os.environ['DBNAME'],
'USER': os.environ['DBUSER'], 'USER': os.environ['DBUSER'],
'PASSWORD': os.environ['DBPASSWD'], 'PASSWORD': os.environ['DBPASSWD'],
'HOST': os.environ.get('DBHOST', 'localhost'),
} }
} }
@ -126,7 +125,7 @@ STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/static/' STATIC_ROOT = '/var/www/static/'
STATICFILES_DIRS = ( STATICFILES_DIRS = (
os.path.join(BASE_DIR, 'static/'), os.path.join(BASE_DIR, 'static/'),
) )
# Media upload (through ImageField, SiteField) # Media upload (through ImageField, SiteField)
@ -138,26 +137,28 @@ MEDIA_URL = '/media/'
# Various additional settings # Various additional settings
SITE_ID = 1 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_HEADLINE = "GestioCOF"
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>" GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>"
PETITS_COURS_FROM = "Le COF <cof@ens.fr>" MAIL_DATA = {
PETITS_COURS_BCC = "archivescof@gmail.com" 'petits_cours': {
PETITS_COURS_REPLYTO = "cof@ens.fr" '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>' LOGIN_URL = "cof-login"
RAPPEL_REPLY_TO = RAPPEL_FROM LOGIN_REDIRECT_URL = "home"
LOGIN_URL = "/gestion/login"
LOGIN_REDIRECT_URL = "/gestion/"
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
CAS_IGNORE_REFERER = True CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = '/gestion/' CAS_REDIRECT_URL = '/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
@ -177,7 +178,7 @@ CHANNEL_LAYERS = {
"default": { "default": {
"BACKEND": "asgi_redis.RedisChannelLayer", "BACKEND": "asgi_redis.RedisChannelLayer",
"CONFIG": { "CONFIG": {
"hosts": [("localhost", 6379)], "hosts": [(os.environ.get("REDIS_HOST", "localhost"), 6379)],
}, },
"ROUTING": "cof.routing.channel_routing", "ROUTING": "cof.routing.channel_routing",
} }
@ -200,3 +201,5 @@ def show_toolbar(request):
DEBUG_TOOLBAR_CONFIG = { DEBUG_TOOLBAR_CONFIG = {
'SHOW_TOOLBAR_CALLBACK': show_toolbar, 'SHOW_TOOLBAR_CALLBACK': show_toolbar,
} }
FORMAT_MODULE_PATH = 'cof.locale'

View file

@ -1,30 +1,33 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
"""
Fichier principal de configuration des urls du projet GestioCOF
"""
from __future__ import division from __future__ import division
from __future__ import print_function from __future__ import print_function
from __future__ import unicode_literals from __future__ import unicode_literals
import autocomplete_light
from django.conf import settings from django.conf import settings
from django.conf.urls import include, url from django.conf.urls import include, url
from django.conf.urls.static import static from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
import autocomplete_light
from django.contrib.auth import views as django_views from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views from django_cas_ng import views as django_cas_views
from gestioncof import views as gestioncof_views, csv_views from gestioncof import views as gestioncof_views, csv_views
from gestioncof.urls import export_patterns, petitcours_patterns, \ from gestioncof.urls import export_patterns, petitcours_patterns, \
surveys_patterns, events_patterns, calendar_patterns, \ surveys_patterns, events_patterns, calendar_patterns, \
clubs_patterns clubs_patterns
from gestioncof.autocomplete import autocomplete from gestioncof.autocomplete import autocomplete
autocomplete_light.autodiscover() autocomplete_light.autodiscover()
admin.autodiscover() admin.autodiscover()
my_urlpatterns = [ urlpatterns = [
# Page d'accueil # Page d'accueil
url(r'^$', gestioncof_views.home, name='home'), url(r'^$', gestioncof_views.home, name='home'),
# Le BdA # Le BdA
@ -48,7 +51,7 @@ my_urlpatterns = [
url(r'^cas/logout$', django_cas_views.logout), url(r'^cas/logout$', django_cas_views.logout),
url(r'^outsider/login$', gestioncof_views.login_ext), url(r'^outsider/login$', gestioncof_views.login_ext),
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}), 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), url(r'^logout$', gestioncof_views.logout),
# Infos persos # Infos persos
url(r'^profile$', gestioncof_views.profile), 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_bda/bda_diff$', gestioncof_views.liste_bdadiff),
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof), url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^k-fet/', include('kfet.urls')) 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))
] ]
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_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement") User.profile_departement = ProfileInfo("departement", "Departement")
User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True) 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", User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente",
"ML BDA-R", True) "ML BdA-R", True)
class UserProfileAdmin(UserAdmin): class UserProfileAdmin(UserAdmin):

View file

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

View file

@ -29,6 +29,12 @@ class COFCASBackend(CASBackend):
request.session['attributes'] = attributes request.session['attributes'] = attributes
if not username: if not username:
return None 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) profiles = CofProfile.objects.filter(login_clipper=username)
if len(profiles) > 0: if len(profiles) > 0:
profile = profiles.order_by('-is_cof')[0] profile = profiles.order_by('-is_cof')[0]

View file

@ -43,6 +43,8 @@
{% else %} {% else %}
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li> <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-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 %} {% endif %}
</ul> </ul>
{% endfor %} {% endfor %}
@ -82,7 +84,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
</div> </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"> <div class="hm-block">
{% if active_tirages %} {% if active_tirages %}
{% for tirage in 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.decorators import login_required
from django.contrib.auth.views import login as django_login_view from django.contrib.auth.views import login as django_login_view
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.utils import timezone from django.utils import timezone
import django.utils.six as six import django.utils.six as six
@ -710,12 +711,15 @@ def calendar_ics(request, token):
tirage__active=True) tirage__active=True)
shows = shows.distinct() shows = shows.distinct()
vcal = Calendar() vcal = Calendar()
site = Site.objects.get_current()
for show in shows: for show in shows:
vevent = Vevent() vevent = Vevent()
vevent.add('dtstart', show.date) vevent.add('dtstart', show.date)
vevent.add('dtend', show.date + timedelta(seconds=7200)) vevent.add('dtend', show.date + timedelta(seconds=7200))
vevent.add('summary', show.title) vevent.add('summary', show.title)
vevent.add('location', show.location.name) 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) vcal.add_component(vevent)
if subscription.subscribe_to_events: if subscription.subscribe_to_events:
for event in Event.objects.filter(old=False).all(): 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('summary', event.title)
vevent.add('location', event.location) vevent.add('location', event.location)
vevent.add('description', event.description) vevent.add('description', event.description)
vevent.add('uid', 'event-{:d}@{:s}'.format(
event.pk, site.domain))
vcal.add_component(vevent) vcal.add_component(vevent)
response = HttpResponse(content=vcal.to_ical()) response = HttpResponse(content=vcal.to_ical())
response['Content-Type'] = "text/calendar" response['Content-Type'] = "text/calendar"

View file

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

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 }}"> <tr class="transfer_form" id="{{ form.prefix }}">
<td class="from_acc_data"></td> <td class="from_acc_data"></td>
<td class="from_acc"> <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 }} {{ form.from_acc }}
</td> </td>
<td class="amount">{{ form.amount }}</td> <td class="amount">{{ form.amount }}</td>
<td class="to_acc"> <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 }} {{ form.to_acc }}
</td> </td>
<td class="to_acc_data"></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 $next = $form.next('.transfer_form').find('.from_acc input');
} }
var $input_id = $input.next('input'); var $input_id = $input.next('input');
getAccountData(trigramme, function(data) { if (isValidTrigramme(trigramme)) {
$input_id.val(data.id); getAccountData(trigramme, function(data) {
$data.text(data.name); $input_id.val(data.id);
$next.focus(); $data.text(data.name);
}); $next.focus();
});
} else {
$input_id.val('');
$data.text('');
}
} }
$('.input_from_acc, .input_to_acc').on('input', function() { $('.input_from_acc, .input_to_acc').on('input', function() {
var tri = $(this).val().toUpperCase(); var tri = $(this).val().toUpperCase();
if (isValidTrigramme(tri)) { updateAccountData(tri, $(this));
updateAccountData(tri, $(this));
}
}); });
$('#transfers_form').on('submit', function(e) { $('#transfers_form').on('submit', function(e) {

View file

@ -6,6 +6,16 @@
ProxyRequests Off ProxyRequests Off
ProxyPass /static/ ! ProxyPass /static/ !
ProxyPass /media/ ! 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 /ws/ ws://127.0.0.1:8001/ws/
ProxyPass / http://127.0.0.1:8001/ ProxyPass / http://127.0.0.1:8001/
ProxyPassReverse / http://127.0.0.1:8001/ ProxyPassReverse / http://127.0.0.1:8001/

View file

@ -8,8 +8,9 @@ DBNAME="cof_gestion"
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
# Installation de paquets utiles # 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 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 # 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 # 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 # Installation et configuration d'Apache
apt-get install -y apache2 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 cp /vagrant/provisioning/apache.conf /etc/apache2/sites-available/gestiocof.conf
a2ensite gestiocof a2ensite gestiocof
a2dissite 000-default a2dissite 000-default
service apache2 restart service apache2 restart
mkdir /var/www/static 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` # 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 # On utilise la version de développement de GestioCOF
export DJANGO_SETTINGS_MODULE='cof.settings_dev' export DJANGO_SETTINGS_MODULE='cof.settings_dev'
@ -44,25 +45,29 @@ export DBPASSWD="$DBPASSWD"
# Permet d'utiliser les utilitaires pythons locaux # Permet d'utiliser les utilitaires pythons locaux
export PATH="\$PATH:\$HOME/.local/bin" export PATH="\$PATH:\$HOME/.local/bin"
# Charge le virtualenv
source ~/venv/bin/activate
# On va dans /vagrant où se trouve le code de gestioCOF # On va dans /vagrant où se trouve le code de gestioCOF
cd /vagrant cd /vagrant
EOF EOF
chown vagrant: ~vagrant/.bash_profile
# On va dans /vagrant où se trouve gestioCOF # On va dans /vagrant où se trouve gestioCOF
cd /vagrant cd /vagrant
# Installation des dépendances python # Installation du virtualenv, on utilise désormais python3
sudo -H -u vagrant pip install --user -r requirements.txt -r requirements-devel.txt 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 # 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 # 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 # 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 apt-get install -y supervisor
cp /vagrant/provisioning/supervisor.conf /etc/supervisor/conf.d/gestiocof.conf cp /vagrant/provisioning/supervisor.conf /etc/supervisor/conf.d/gestiocof.conf
sed "s/{DBUSER}/$DBUSER/" -i /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" DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
19 */12 * * * date >> /vagrant/rappels.log ; python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1 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. - Garde les logs peut être une bonne idée.
Exemple : voir le fichier `provisioning/cron.dev`. 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 # Doit être lancé par bootstrap.sh
source ~/venv/bin/activate
python manage.py migrate python manage.py migrate
python manage.py loaddata users root bda gestion sites python manage.py loaddata users root bda gestion sites
python manage.py collectstatic --noinput python manage.py collectstatic --noinput

View file

@ -1,7 +1,7 @@
[program:worker] [program:worker]
command=/usr/bin/python /vagrant/manage.py runworker command=/home/ubuntu/venv/bin/python /vagrant/manage.py runworker
directory=/vagrant/ directory=/vagrant/
user=vagrant user=ubuntu
environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev" environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev"
autostart=true autostart=true
autorestart=true autorestart=true
@ -10,11 +10,11 @@ stopasgroup=true
redirect_stderr=true redirect_stderr=true
[program:interface] [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" environment=DBUSER={DBUSER},DBNAME={DBNAME},DBPASSWD={DBPASSWD},DJANGO_SETTINGS_MODULE="cof.settings_dev"
directory=/vagrant/ directory=/vagrant/
redirect_stderr=true redirect_stderr=true
autostart=true autostart=true
autorestart=true autorestart=true
stopasgroup=true stopasgroup=true
user=vagrant user=ubuntu

View file

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

View file

@ -1,13 +1,12 @@
configparser==3.5.0 configparser==3.5.0
Django==1.8 Django==1.8.*
django-autocomplete-light==2.3.3 django-autocomplete-light==2.3.3
django-autoslug==1.9.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-grappelli==2.8.1
django-recaptcha==1.0.5 django-recaptcha==1.0.5
mysqlclient==1.3.7 mysqlclient==1.3.7
Pillow==3.3.0 Pillow==3.3.0
simplejson==3.8.2
six==1.10.0 six==1.10.0
unicodecsv==0.14.1 unicodecsv==0.14.1
icalendar==3.10 icalendar==3.10
@ -15,7 +14,7 @@ django-bootstrap-form==3.2.1
asgiref==0.14.0 asgiref==0.14.0
daphne==0.14.3 daphne==0.14.3
asgi-redis==0.14.0 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 statistics==1.0.3.5
future==0.15.2 future==0.15.2
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1