diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..f420a9f5 --- /dev/null +++ b/.gitlab-ci.yml @@ -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 diff --git a/Vagrantfile b/Vagrantfile index c84c3dd9..e12a45ed 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -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. diff --git a/bda/admin.py b/bda/admin.py index b23d79e0..a9b3c51f 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -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) diff --git a/bda/forms.py b/bda/forms.py index 9f5e3890..fe20e565 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -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()) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py new file mode 100644 index 00000000..4b90bc57 --- /dev/null +++ b/bda/management/commands/manage_reventes.py @@ -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") diff --git a/bda/management/commands/sendrappels.py b/bda/management/commands/sendrappels.py index 1e8da240..88cf9d5c 100644 --- a/bda/management/commands/sendrappels.py +++ b/bda/management/commands/sendrappels.py @@ -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() diff --git a/bda/migrations/0009_revente.py b/bda/migrations/0009_revente.py new file mode 100644 index 00000000..1cca4e86 --- /dev/null +++ b/bda/migrations/0009_revente.py @@ -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), + ), + ] diff --git a/bda/models.py b/bda/models.py index 270921be..85c7fa4d 100644 --- a/bda/models.py +++ b/bda/models.py @@ -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() diff --git a/bda/templates/bda-attrib.html b/bda/templates/bda-attrib.html index 5c705a79..5c22d2b3 100644 --- a/bda/templates/bda-attrib.html +++ b/bda/templates/bda-attrib.html @@ -22,7 +22,7 @@ {% for show, members, losers in results %}
-

{{ show.title }} - {{ show.date_no_seconds }} @ {{ show.location }}

+

{{ show.title }} - {{ show.date }} @ {{ show.location }}

{{ show.nrequests }} demandes pour {{ show.slots }} places {{ show.price }}€ par place{% if user.profile.is_buro and show.nrequests < show.slots %}, {{ show.deficit }}€ de déficit{% endif %} diff --git a/bda/templates/bda-interested.html b/bda/templates/bda-interested.html new file mode 100644 index 00000000..acfb1d1e --- /dev/null +++ b/bda/templates/bda-interested.html @@ -0,0 +1,9 @@ +{% extends "base_title.html" %} +{% load staticfiles %} + +{% block realcontent %} +

Inscription à une revente

+

Votre inscription pour a bien été enregistrée !

+

Le tirage au sort pour cette revente ({{spectacle}}) sera effectué le {{date}}. + +{% endblock %} diff --git a/bda/templates/bda-no-revente.html b/bda/templates/bda-no-revente.html new file mode 100644 index 00000000..eabb3dc7 --- /dev/null +++ b/bda/templates/bda-no-revente.html @@ -0,0 +1,6 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

BdA-Revente

+

Il n'y a plus de places en revente pour ce spectacle, désolé !

+{% endblock %} diff --git a/bda/templates/bda-notpaid.html b/bda/templates/bda-notpaid.html index b25c2ba3..0dd4e4df 100644 --- a/bda/templates/bda-notpaid.html +++ b/bda/templates/bda-notpaid.html @@ -1,6 +1,6 @@ {% extends "base_title.html" %} {% block realcontent %} -

Nope

+

Nope

Avant de revendre des places, il faut aller les payer !

{% endblock %} diff --git a/bda/templates/bda-revente.html b/bda/templates/bda-revente.html index 10a93d43..569a3f3a 100644 --- a/bda/templates/bda-revente.html +++ b/bda/templates/bda-revente.html @@ -1,26 +1,73 @@ {% extends "base_title.html" %} -{% load staticfiles %} - -{% block extra_head %} - -{% endblock %} +{% load bootstrap %} {% block realcontent %} -

Revente de place

-
+

Revente de place

+

Places non revendues

+ {% csrf_token %} -{% if form.spectacle.errors %}{% endif %} -

-

-Bonjour,
-
-Je souhaite revendre {{ form.count }} place(s) pour {{ form.spectacle }}.
-Contactez-moi par email si vous êtes intéressé !
-
-{{ user.get_full_name }} ({{ user.email }}) -
-

- +
+
+
    + {% for box in resellform.attributions %} +
  • + {{box.tag}} + {{box.choice_label}} +
  • + {% endfor %} +
+
+
+
+ +
+
+{% if annulform.attributions or overdue %} +

Places en cours de revente

+
+ {% csrf_token %} +
+
+
    + {% for box in annulform.attributions %} +
  • + {{box.tag}} + {{box.choice_label}} +
  • + {% endfor %} + {% for attrib in overdue %} +
  • + + {{attrib.spectacle}} +
  • + {% endfor %} +
+
+
+ {% if annulform.attributions %} + + {% endif %} +
+{% endif %} +
+{% if sold %} +

Places revendues

+ + {% for attrib in sold %} + + + {% csrf_token %} + + + + + + + {% endfor %} +
{{attrib.spectacle}}{{attrib.revente.soldTo.user.get_full_name}}
+{% endif %} {% endblock %} diff --git a/bda/templates/bda-shotgun.html b/bda/templates/bda-shotgun.html new file mode 100644 index 00000000..e10fae00 --- /dev/null +++ b/bda/templates/bda-shotgun.html @@ -0,0 +1,14 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

Places disponibles immédiatement

+ {% if shotgun %} + {% endfor %} @@ -82,7 +84,7 @@ {% endfor %}
-

Gestion tirages BDA

+

Gestion tirages BdA

{% if active_tirages %} {% for tirage in active_tirages %} diff --git a/gestioncof/views.py b/gestioncof/views.py index 80f528f7..4f1e5009 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -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" diff --git a/kfet/routing.py b/kfet/routing.py index 9c816c92..63e0c963 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -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/$"), ] diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 0eceebd2..babd4175 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -924,7 +924,7 @@ $(document).ready(function() { } }); }, - onClose: function() { this._lastFocused = articleSelect; } + onClose: function() { this._lastFocused = (articleSelect.val() ? articleNb : articleSelect) ; } }); } diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index 294db1e5..83a09762 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -34,12 +34,12 @@ - + {{ form.from_acc }} {{ form.amount }} - + {{ form.to_acc }} @@ -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) { diff --git a/provisioning/apache.conf b/provisioning/apache.conf index 001c6ec9..cc815592 100644 --- a/provisioning/apache.conf +++ b/provisioning/apache.conf @@ -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/ diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index f072e6fc..89e8790d 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -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 <> ~ubuntu/.bashrc <> /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 diff --git a/provisioning/cron.md b/provisioning/cron.md index 8b3f608e..840a8716 100644 --- a/provisioning/cron.md +++ b/provisioning/cron.md @@ -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 diff --git a/provisioning/prepare_django.sh b/provisioning/prepare_django.sh index 8988e834..c8c80d05 100644 --- a/provisioning/prepare_django.sh +++ b/provisioning/prepare_django.sh @@ -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 diff --git a/provisioning/supervisor.conf b/provisioning/supervisor.conf index 4c46b952..c806a13d 100644 --- a/provisioning/supervisor.conf +++ b/provisioning/supervisor.conf @@ -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 diff --git a/requirements-devel.txt b/requirements-devel.txt index dc8dbc45..425dfc36 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,2 +1,3 @@ +-r requirements.txt django-debug-toolbar ipython diff --git a/requirements.txt b/requirements.txt index 2d1109eb..f7f6deca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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