resuming views for stat
This commit is contained in:
commit
d19daa04b1
56 changed files with 1326 additions and 243 deletions
40
.gitlab-ci.yml
Normal file
40
.gitlab-ci.yml
Normal 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
2
Vagrantfile
vendored
|
@ -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.
|
||||
|
|
72
bda/admin.py
72
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)
|
||||
|
|
50
bda/forms.py
50
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())
|
||||
|
|
43
bda/management/commands/manage_reventes.py
Normal file
43
bda/management/commands/manage_reventes.py
Normal 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")
|
|
@ -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()
|
||||
|
|
66
bda/migrations/0009_revente.py
Normal file
66
bda/migrations/0009_revente.py
Normal 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),
|
||||
),
|
||||
]
|
235
bda/models.py
235
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()
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
{% for show, members, losers in results %}
|
||||
<div class="attribresult">
|
||||
<h3 class="horizontal-title">{{ show.title }} - {{ show.date_no_seconds }} @ {{ show.location }}</h3>
|
||||
<h3 class="horizontal-title">{{ show.title }} - {{ show.date }} @ {{ show.location }}</h3>
|
||||
<p>
|
||||
<strong>{{ show.nrequests }} demandes pour {{ show.slots }} places</strong>
|
||||
{{ show.price }}€ par place{% if user.profile.is_buro and show.nrequests < show.slots %}, {{ show.deficit }}€ de déficit{% endif %}
|
||||
|
|
9
bda/templates/bda-interested.html
Normal file
9
bda/templates/bda-interested.html
Normal 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 %}
|
6
bda/templates/bda-no-revente.html
Normal file
6
bda/templates/bda-no-revente.html
Normal 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 %}
|
|
@ -1,6 +1,6 @@
|
|||
{% extends "base_title.html" %}
|
||||
|
||||
{% block realcontent %}
|
||||
<h2><strong>Nope</strong></h1>
|
||||
<h2><strong>Nope</strong></h2>
|
||||
<p>Avant de revendre des places, il faut aller les payer !</p>
|
||||
{% endblock %}
|
||||
|
|
|
@ -1,26 +1,73 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||
{% endblock %}
|
||||
{% load bootstrap %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
||||
<h2>Revente de place</h1>
|
||||
<form action="" method="post" id="resellform">
|
||||
<h2>Revente de place</h2>
|
||||
<h3>Places non revendues</h3>
|
||||
<form class="form-horizontal" action="" method="post">
|
||||
{% csrf_token %}
|
||||
{% if form.spectacle.errors %}<ul class="errorlist"><li>Sélectionnez un spetacle</li></ul>{% endif %}
|
||||
<p>
|
||||
<pre>
|
||||
Bonjour,<br />
|
||||
<br />
|
||||
Je souhaite revendre {{ form.count }} place(s) pour {{ form.spectacle }}.<br />
|
||||
Contactez-moi par email si vous êtes intéressé !<br />
|
||||
<br />
|
||||
{{ user.get_full_name }} ({{ user.email }})
|
||||
</pre>
|
||||
</p>
|
||||
<input class="btn btn-primary" type="submit" value="Envoyer" />
|
||||
<div class="form-group">
|
||||
<div class="multiple-checkbox">
|
||||
<ul>
|
||||
{% for box in resellform.attributions %}
|
||||
<li>
|
||||
{{box.tag}}
|
||||
{{box.choice_label}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<input type="submit" class="btn btn-primary" name="resell" value="Revendre les places sélectionnées">
|
||||
</div>
|
||||
</form>
|
||||
<br>
|
||||
{% if annulform.attributions or overdue %}
|
||||
<h3>Places en cours de revente</h3>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-group">
|
||||
<div class="multiple-checkbox">
|
||||
<ul>
|
||||
{% for box in annulform.attributions %}
|
||||
<li>
|
||||
{{box.tag}}
|
||||
{{box.choice_label}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% for attrib in overdue %}
|
||||
<li>
|
||||
<input type="checkbox" style="visibility:hidden">
|
||||
{{attrib.spectacle}}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% if annulform.attributions %}
|
||||
<input type="submit" class="btn btn-primary" name="annul" value="Annuler les reventes sélectionnées">
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endif %}
|
||||
<br>
|
||||
{% if sold %}
|
||||
<h3>Places revendues</h3>
|
||||
<table class="table">
|
||||
{% for attrib in sold %}
|
||||
<tr>
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<td>{{attrib.spectacle}}</td>
|
||||
<td>{{attrib.revente.soldTo.user.get_full_name}}</td>
|
||||
<td><button type="submit" class="btn btn-primary" name="transfer"
|
||||
value="{{attrib.revente.id}}">Transférer</button></td>
|
||||
<td><button type="submit" class="btn btn-primary" name="reinit"
|
||||
value="{{attrib.revente.id}}">Réinitialiser</button></td>
|
||||
</form>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
|
14
bda/templates/bda-shotgun.html
Normal file
14
bda/templates/bda-shotgun.html
Normal 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 %}
|
|
@ -1,12 +1,8 @@
|
|||
{% extends "base_title.html" %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link type="text/css" rel="stylesheet" href="{% static "css/bda.css" %}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block realcontent %}
|
||||
|
||||
<h1>Revente de place</h1>
|
||||
<p class="success">Votre offre de revente de {{ places }} pour {{ show.title }} le {{ show.date_no_seconds }} ({{ show.location }}) à {{ show.price }}€ a bien été envoyée.</p>
|
||||
<h2>Revente de place</h2>
|
||||
<p class="success">Un mail a bien été envoyé à {{seller.get_full_name}} ({{seller.email}}), pour racheter une place pour {{spectacle.title}} !</p>
|
||||
{% endblock %}
|
||||
|
|
6
bda/templates/bda-wrongtime.html
Normal file
6
bda/templates/bda-wrongtime.html
Normal 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 %}
|
6
bda/templates/bda/mails/buy-shotgun.txt
Normal file
6
bda/templates/bda/mails/buy-shotgun.txt
Normal 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 }})
|
|
@ -1,14 +1,14 @@
|
|||
Bonjour {{ member.get_full_name }},
|
||||
Bonjour {{ name }},
|
||||
|
||||
Nous te rappellons que tu as eu la chance d'obtenir {{ member.nb_attr|pluralize:"une place,deux places" }}
|
||||
pour {{ show.title }}, le {{ show.date_no_seconds }} au {{ show.location }}. N'oublie pas de t'y rendre !
|
||||
{% if member.nb_attr == 2 %}
|
||||
Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }}
|
||||
pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !
|
||||
{% if nb_attr == 2 %}
|
||||
Tu as obtenu deux places pour ce spectacle. Nous te rappelons que
|
||||
ces places sont strictement réservées aux personnes de moins de 28 ans.
|
||||
{% endif %}
|
||||
{% if show.listing %}Pour ce spectacle, tu as reçu des places sur
|
||||
listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation
|
||||
pour retirer {{ member.nb_attr|pluralize:"ta place,tes places" }}.
|
||||
pour retirer {{ nb_attr|pluralize:"ta place,tes places" }}.
|
||||
{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont
|
||||
été distribués au burô.
|
||||
{% endif %}
|
9
bda/templates/bda/mails/revente-loser.txt
Normal file
9
bda/templates/bda/mails/revente-loser.txt
Normal 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
|
13
bda/templates/bda/mails/revente-new.txt
Normal file
13
bda/templates/bda/mails/revente-new.txt
Normal file
|
@ -0,0 +1,13 @@
|
|||
Bonjour {{ vendeur.first_name }},
|
||||
|
||||
Tu t’es 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 s’est 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
|
7
bda/templates/bda/mails/revente-seller.txt
Normal file
7
bda/templates/bda/mails/revente-seller.txt
Normal 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
|
7
bda/templates/bda/mails/revente-winner.txt
Normal file
7
bda/templates/bda/mails/revente-winner.txt
Normal 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
|
12
bda/templates/bda/mails/revente.txt
Normal file
12
bda/templates/bda/mails/revente.txt
Normal 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
|
11
bda/templates/bda/mails/shotgun.txt
Normal file
11
bda/templates/bda/mails/shotgun.txt
Normal 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
|
|
@ -18,7 +18,7 @@
|
|||
{% for spectacle in spectacles %}
|
||||
<tr>
|
||||
<td>{{ spectacle.title }}</td>
|
||||
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date_no_seconds }}</td>
|
||||
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
|
||||
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
|
||||
<td data-sort-value="{{ spectacle.slots }}">{{ spectacle.slots }} places</td>
|
||||
<td data-sort-value="{{ spectacle.total }}">{{ spectacle.total }} demandes</td>
|
||||
|
|
39
bda/templates/liste-reventes.html
Normal file
39
bda/templates/liste-reventes.html
Normal 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 %}
|
|
@ -11,7 +11,7 @@
|
|||
<tr>
|
||||
<td>{{place.spectacle.title}}</td>
|
||||
<td>{{place.spectacle.location}}</td>
|
||||
<td>{{place.spectacle.date_no_seconds}}</td>
|
||||
<td>{{place.spectacle.date}}</td>
|
||||
<td>{% if place.double %}deux places{%else%}une place{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
|
20
bda/templates/revente-confirm.html
Normal file
20
bda/templates/revente-confirm.html
Normal 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%}
|
|
@ -19,7 +19,7 @@
|
|||
{% for spectacle in object_list %}
|
||||
<tr class="clickable-row" data-href="{% url 'bda-spectacle' tirage_id spectacle.id %}">
|
||||
<td><a href="{% url 'bda-spectacle' tirage_id spectacle.id %}">{{ spectacle.title }} <span style="font-size:small;" class="glyphicon glyphicon-link" aria-hidden="true"></span></a></td>
|
||||
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date_no_seconds }}</td>
|
||||
<td data-sort-value="{{ spectacle.timestamp }}">{{ spectacle.date }}</td>
|
||||
<td data-sort-value="{{ spectacle.location }}">{{ spectacle.location }}</td>
|
||||
<td data-sort-value="{{ spectacle.price |stringformat:".3f" }}">
|
||||
{{ spectacle.price |floatformat }}€
|
||||
|
|
12
bda/urls.py
12
bda/urls.py
|
@ -32,6 +32,18 @@ urlpatterns = [
|
|||
url(r'^spectacles/unpaid/(?P<tirage_id>\d+)$',
|
||||
views.unpaid,
|
||||
name="bda-unpaid"),
|
||||
url(r'^liste-revente/(?P<tirage_id>\d+)$',
|
||||
views.list_revente,
|
||||
name="bda-liste-revente"),
|
||||
url(r'^buy-revente/(?P<spectacle_id>\d+)$',
|
||||
views.buy_revente,
|
||||
name="bda-buy-revente"),
|
||||
url(r'^revente-interested/(?P<revente_id>\d+)$',
|
||||
views.revente_interested,
|
||||
name='bda-revente-interested'),
|
||||
url(r'^revente-immediat/(?P<tirage_id>\d+)$',
|
||||
views.revente_shotgun,
|
||||
name="bda-shotgun"),
|
||||
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel),
|
||||
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
|
||||
name='bda-descriptions'),
|
||||
|
|
271
bda/views.py
271
bda/views.py
|
@ -4,27 +4,34 @@ from __future__ import division
|
|||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import random
|
||||
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db import models
|
||||
from django.db.models import Count
|
||||
from django.core import serializers
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Count, Q
|
||||
from django.core import serializers, mail
|
||||
from django.forms.models import inlineformset_factory
|
||||
from django.http import HttpResponseBadRequest
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.conf import settings
|
||||
import hashlib
|
||||
|
||||
from django.core.mail import send_mail
|
||||
from django.template import loader
|
||||
from django.utils import timezone
|
||||
from django.views.generic.list import ListView
|
||||
|
||||
import time
|
||||
from datetime import timedelta
|
||||
|
||||
from gestioncof.decorators import cof_required, buro_required
|
||||
from bda.models import Spectacle, Participant, ChoixSpectacle, Attribution,\
|
||||
Tirage, render_template
|
||||
Tirage, SpectacleRevente
|
||||
from bda.algorithm import Algorithm
|
||||
|
||||
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm
|
||||
from bda.forms import BaseBdaFormSet, TokenForm, ResellForm, AnnulForm,\
|
||||
InscriptionReventeForm
|
||||
|
||||
|
||||
@cof_required
|
||||
|
@ -231,6 +238,11 @@ def do_tirage(request, tirage_id):
|
|||
Attribution(spectacle=show, participant=member)
|
||||
for show, members, _ in results
|
||||
for member, _, _, _ in members])
|
||||
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
|
||||
for (show, _, losers) in results:
|
||||
for (loser, _, _, _) in losers:
|
||||
loser.choicesrevente.add(show)
|
||||
loser.save()
|
||||
return render(request, "bda-attrib-extra.html", data)
|
||||
else:
|
||||
return render(request, "bda-attrib.html", data)
|
||||
|
@ -251,25 +263,6 @@ def tirage(request, tirage_id):
|
|||
return render(request, "bda-token.html", {"form": form})
|
||||
|
||||
|
||||
def do_resell(request, form):
|
||||
spectacle = form.cleaned_data["spectacle"]
|
||||
count = form.cleaned_data["count"]
|
||||
places = "2 places" if count == "2" else "une place"
|
||||
mail = """Bonjour,
|
||||
|
||||
Je souhaite revendre %s pour %s le %s (%s) à %.02f€.
|
||||
Contactez moi par email si vous êtes intéressé·e·s !
|
||||
|
||||
%s (%s)""" % (places, spectacle.title, spectacle.date_no_seconds(),
|
||||
spectacle.location, spectacle.price,
|
||||
request.user.get_full_name(), request.user.email)
|
||||
send_mail("%s" % spectacle, mail,
|
||||
request.user.email, ["bda-revente@lists.ens.fr"],
|
||||
fail_silently=False)
|
||||
return render(request, "bda-success.html",
|
||||
{"show": spectacle, "places": places})
|
||||
|
||||
|
||||
@login_required
|
||||
def revente(request, tirage_id):
|
||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||
|
@ -277,14 +270,226 @@ def revente(request, tirage_id):
|
|||
user=request.user, tirage=tirage)
|
||||
if not participant.paid:
|
||||
return render(request, "bda-notpaid.html", {})
|
||||
if request.POST:
|
||||
form = ResellForm(participant, request.POST)
|
||||
if form.is_valid():
|
||||
return do_resell(request, form)
|
||||
if request.method == 'POST':
|
||||
if 'resell' in request.POST:
|
||||
resellform = ResellForm(participant, request.POST, prefix='resell')
|
||||
annulform = AnnulForm(participant, prefix='annul')
|
||||
if resellform.is_valid():
|
||||
mails = []
|
||||
attributions = resellform.cleaned_data["attributions"]
|
||||
with transaction.atomic():
|
||||
for attribution in attributions:
|
||||
revente, created = SpectacleRevente.objects.get_or_create(
|
||||
attribution=attribution,
|
||||
defaults={'seller': participant})
|
||||
if not created:
|
||||
revente.seller = participant
|
||||
revente.date = timezone.now()
|
||||
mail_subject = "BdA-Revente : {:s}".format(attribution.spectacle.title)
|
||||
mail_body = loader.render_to_string('bda/mails/revente-new.txt', {
|
||||
'vendeur': participant.user,
|
||||
'spectacle': attribution.spectacle,
|
||||
'revente': revente,
|
||||
})
|
||||
mails.append(mail.EmailMessage(
|
||||
mail_subject, mail_body,
|
||||
from_email=settings.MAIL_DATA['revente']['FROM'],
|
||||
to=[participant.user.email],
|
||||
reply_to=[settings.MAIL_DATA['revente']['REPLYTO']],
|
||||
))
|
||||
revente.save()
|
||||
mail.get_connection().send_messages(mails)
|
||||
|
||||
elif 'annul' in request.POST:
|
||||
annulform = AnnulForm(participant, request.POST, prefix='annul')
|
||||
resellform = ResellForm(participant, prefix='resell')
|
||||
if annulform.is_valid():
|
||||
attributions = annulform.cleaned_data["attributions"]
|
||||
for attribution in attributions:
|
||||
attribution.revente.delete()
|
||||
|
||||
elif 'transfer' in request.POST:
|
||||
resellform = ResellForm(participant, prefix='resell')
|
||||
annulform = AnnulForm(participant, prefix='annul')
|
||||
|
||||
revente_id = request.POST['transfer'][0]
|
||||
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
|
||||
id=revente_id)
|
||||
if rev.exists():
|
||||
revente = rev.get()
|
||||
attrib = revente.attribution
|
||||
attrib.participant = revente.soldTo
|
||||
attrib.save()
|
||||
|
||||
elif 'reinit' in request.POST:
|
||||
resellform = ResellForm(participant, prefix='resell')
|
||||
annulform = AnnulForm(participant, prefix='annul')
|
||||
revente_id = request.POST['reinit'][0]
|
||||
rev = SpectacleRevente.objects.filter(soldTo__isnull=False,
|
||||
id=revente_id)
|
||||
if rev.exists():
|
||||
revente = rev.get()
|
||||
if revente.attribution.spectacle.date > timezone.now():
|
||||
revente.date = timezone.now() - timedelta(hours=1)
|
||||
revente.soldTo = None
|
||||
revente.notif_sent = False
|
||||
revente.tirage_done = False
|
||||
if revente.answered_mail:
|
||||
revente.answered_mail.clear()
|
||||
revente.save()
|
||||
|
||||
else:
|
||||
resellform = ResellForm(participant, prefix='resell')
|
||||
annulform = AnnulForm(participant, prefix='annul')
|
||||
else:
|
||||
form = ResellForm(participant)
|
||||
resellform = ResellForm(participant, prefix='resell')
|
||||
annulform = AnnulForm(participant, prefix='annul')
|
||||
|
||||
overdue = participant.attribution_set.filter(
|
||||
spectacle__date__gte=timezone.now(),
|
||||
revente__isnull=False,
|
||||
revente__seller=participant,
|
||||
revente__date__lte=timezone.now()-timedelta(hours=1)).filter(
|
||||
Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant))
|
||||
sold = participant.attribution_set.filter(
|
||||
spectacle__date__gte=timezone.now(),
|
||||
revente__isnull=False,
|
||||
revente__soldTo__isnull=False).exclude(
|
||||
revente__soldTo=participant)
|
||||
|
||||
return render(request, "bda-revente.html",
|
||||
{"form": form, 'tirage': tirage})
|
||||
{'tirage': tirage, 'overdue': overdue, "sold": sold,
|
||||
"annulform": annulform, "resellform": resellform})
|
||||
|
||||
|
||||
@login_required
|
||||
def revente_interested(request, revente_id):
|
||||
revente = get_object_or_404(SpectacleRevente, id=revente_id)
|
||||
participant, created = Participant.objects.get_or_create(
|
||||
user=request.user, tirage=revente.attribution.spectacle.tirage)
|
||||
if timezone.now() < revente.date + timedelta(hours=1) or revente.shotgun:
|
||||
return render(request, "bda-wrongtime.html", {})
|
||||
|
||||
revente.answered_mail.add(participant)
|
||||
return render(request, "bda-interested.html",
|
||||
{"spectacle": revente.attribution.spectacle,
|
||||
"date": revente.expiration_time})
|
||||
|
||||
|
||||
@login_required
|
||||
def list_revente(request, tirage_id):
|
||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||
participant, created = Participant.objects.get_or_create(
|
||||
user=request.user, tirage=tirage)
|
||||
spectacles = tirage.spectacle_set.filter(
|
||||
date__gte=timezone.now())
|
||||
shotgun = []
|
||||
deja_revente = False
|
||||
success = False
|
||||
for spectacle in spectacles:
|
||||
revente_objects = SpectacleRevente.objects.filter(
|
||||
attribution__spectacle=spectacle,
|
||||
soldTo__isnull=True)
|
||||
revente_count = 0
|
||||
for revente in revente_objects:
|
||||
if revente.shotgun:
|
||||
revente_count += 1
|
||||
if revente_count:
|
||||
spectacle.revente_count = revente_count
|
||||
shotgun.append(spectacle)
|
||||
|
||||
if request.method == 'POST':
|
||||
form = InscriptionReventeForm(tirage, request.POST)
|
||||
if form.is_valid():
|
||||
choices = form.cleaned_data['spectacles']
|
||||
participant.choicesrevente = choices
|
||||
participant.save()
|
||||
for spectacle in choices:
|
||||
qset = SpectacleRevente.objects.filter(
|
||||
attribution__spectacle=spectacle)
|
||||
if qset.exists():
|
||||
# On l'inscrit à l'un des tirages au sort
|
||||
for revente in qset.all():
|
||||
if revente.shotgun and not revente.soldTo:
|
||||
deja_revente = True
|
||||
else:
|
||||
revente.answered_mail.add(participant)
|
||||
revente.save()
|
||||
break
|
||||
success = True
|
||||
else:
|
||||
form = InscriptionReventeForm(
|
||||
tirage,
|
||||
initial={'spectacles': participant.choicesrevente.all()})
|
||||
|
||||
return render(request, "liste-reventes.html",
|
||||
{"form": form, 'shotgun': shotgun,
|
||||
"deja_revente": deja_revente, "success": success})
|
||||
|
||||
|
||||
@login_required
|
||||
def buy_revente(request, spectacle_id):
|
||||
spectacle = get_object_or_404(Spectacle, id=spectacle_id)
|
||||
tirage = spectacle.tirage
|
||||
participant, created = Participant.objects.get_or_create(
|
||||
user=request.user, tirage=tirage)
|
||||
reventes = SpectacleRevente.objects.filter(
|
||||
attribution__spectacle=spectacle,
|
||||
soldTo__isnull=True)
|
||||
if reventes.filter(seller=participant).exists():
|
||||
revente = reventes.filter(seller=participant)[0]
|
||||
revente.delete()
|
||||
return HttpResponseRedirect(reverse("bda-shotgun",
|
||||
args=[tirage.id]))
|
||||
reventes_shotgun = []
|
||||
for revente in reventes.all():
|
||||
if revente.shotgun:
|
||||
reventes_shotgun.append(revente)
|
||||
|
||||
if not reventes_shotgun:
|
||||
return render(request, "bda-no-revente.html", {})
|
||||
|
||||
if request.POST:
|
||||
revente = random.choice(reventes_shotgun)
|
||||
revente.soldTo = participant
|
||||
revente.save()
|
||||
mail = loader.render_to_string('bda/mails/buy-shotgun.txt', {
|
||||
'spectacle': spectacle,
|
||||
'acheteur': request.user,
|
||||
'vendeur': revente.seller.user,
|
||||
})
|
||||
send_mail("BdA-Revente : %s" % spectacle.title, mail,
|
||||
request.user.email,
|
||||
[revente.seller.user.email],
|
||||
fail_silently=False)
|
||||
return render(request, "bda-success.html",
|
||||
{"seller": revente.attribution.participant.user,
|
||||
"spectacle": spectacle})
|
||||
|
||||
return render(request, "revente-confirm.html",
|
||||
{"spectacle": spectacle,
|
||||
"user": request.user})
|
||||
|
||||
|
||||
@login_required
|
||||
def revente_shotgun(request, tirage_id):
|
||||
tirage = get_object_or_404(Tirage, id=tirage_id)
|
||||
spectacles = tirage.spectacle_set.filter(
|
||||
date__gte=timezone.now())
|
||||
shotgun = []
|
||||
for spectacle in spectacles:
|
||||
revente_objects = SpectacleRevente.objects.filter(
|
||||
attribution__spectacle=spectacle,
|
||||
soldTo__isnull=True)
|
||||
revente_count = 0
|
||||
for revente in revente_objects:
|
||||
if revente.shotgun:
|
||||
revente_count += 1
|
||||
if revente_count:
|
||||
shotgun.append(spectacle)
|
||||
|
||||
return render(request, "bda-shotgun.html",
|
||||
{"shotgun": shotgun})
|
||||
|
||||
|
||||
@buro_required
|
||||
|
@ -345,11 +550,11 @@ def send_rappel(request, spectacle_id):
|
|||
# Mails d'exemples
|
||||
fake_member = request.user
|
||||
fake_member.nb_attr = 1
|
||||
exemple_mail_1place = render_template('mail-rappel.txt', {
|
||||
exemple_mail_1place = loader.render_to_string('bda/mails/rappel.txt', {
|
||||
'member': fake_member,
|
||||
'show': show})
|
||||
fake_member.nb_attr = 2
|
||||
exemple_mail_2places = render_template('mail-rappel.txt', {
|
||||
exemple_mail_2places = loader.render_to_string('bda/mails/rappel.txt', {
|
||||
'member': fake_member,
|
||||
'show': show})
|
||||
# Contexte
|
||||
|
|
0
cof/locale/__init__.py
Normal file
0
cof/locale/__init__.py
Normal file
0
cof/locale/fr/__init__.py
Normal file
0
cof/locale/fr/__init__.py
Normal file
9
cof/locale/fr/formats.py
Normal file
9
cof/locale/fr/formats.py
Normal 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'
|
|
@ -29,9 +29,6 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
|
|||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['127.0.0.1']
|
||||
|
||||
|
||||
# Application definition
|
||||
INSTALLED_APPS = (
|
||||
'gestioncof',
|
||||
|
@ -102,6 +99,7 @@ DATABASES = {
|
|||
'NAME': os.environ['DBNAME'],
|
||||
'USER': os.environ['DBUSER'],
|
||||
'PASSWORD': os.environ['DBPASSWD'],
|
||||
'HOST': os.environ.get('DBHOST', 'localhost'),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +125,7 @@ STATIC_URL = '/static/'
|
|||
STATIC_ROOT = '/var/www/static/'
|
||||
|
||||
STATICFILES_DIRS = (
|
||||
os.path.join(BASE_DIR, 'static/'),
|
||||
os.path.join(BASE_DIR, 'static/'),
|
||||
)
|
||||
|
||||
# Media upload (through ImageField, SiteField)
|
||||
|
@ -139,26 +137,28 @@ MEDIA_URL = '/media/'
|
|||
# Various additional settings
|
||||
SITE_ID = 1
|
||||
|
||||
# URL prefix for admin static files -- CSS, JavaScript and images.
|
||||
# Make sure to use a trailing slash.
|
||||
# Examples: "http://foo.com/static/admin/", "/static/admin/".
|
||||
ADMIN_MEDIA_PREFIX = '/static/grappelli/'
|
||||
GRAPPELLI_ADMIN_HEADLINE = "GestioCOF"
|
||||
GRAPPELLI_ADMIN_TITLE = "<a href=\"/\">GestioCOF</a>"
|
||||
|
||||
PETITS_COURS_FROM = "Le COF <cof@ens.fr>"
|
||||
PETITS_COURS_BCC = "archivescof@gmail.com"
|
||||
PETITS_COURS_REPLYTO = "cof@ens.fr"
|
||||
MAIL_DATA = {
|
||||
'petits_cours': {
|
||||
'FROM': "Le COF <cof@ens.fr>",
|
||||
'BCC': "archivescof@gmail.com",
|
||||
'REPLYTO': "cof@ens.fr"},
|
||||
'rappels': {
|
||||
'FROM': 'Le BdA <bda@ens.fr>',
|
||||
'REPLYTO': 'Le BdA <bda@ens.fr>'},
|
||||
'revente': {
|
||||
'FROM': 'BdA-Revente <bda-revente@ens.fr>',
|
||||
'REPLYTO': 'BdA-Revente <bda-revente@ens.fr>'},
|
||||
}
|
||||
|
||||
RAPPEL_FROM = 'Le BdA <bda@ens.fr>'
|
||||
RAPPEL_REPLY_TO = RAPPEL_FROM
|
||||
|
||||
LOGIN_URL = "/gestion/login"
|
||||
LOGIN_REDIRECT_URL = "/gestion/"
|
||||
LOGIN_URL = "cof-login"
|
||||
LOGIN_REDIRECT_URL = "home"
|
||||
|
||||
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
|
||||
CAS_IGNORE_REFERER = True
|
||||
CAS_REDIRECT_URL = '/gestion/'
|
||||
CAS_REDIRECT_URL = '/'
|
||||
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
|
||||
AUTHENTICATION_BACKENDS = (
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
|
@ -178,7 +178,7 @@ CHANNEL_LAYERS = {
|
|||
"default": {
|
||||
"BACKEND": "asgi_redis.RedisChannelLayer",
|
||||
"CONFIG": {
|
||||
"hosts": [("localhost", 6379)],
|
||||
"hosts": [(os.environ.get("REDIS_HOST", "localhost"), 6379)],
|
||||
},
|
||||
"ROUTING": "cof.routing.channel_routing",
|
||||
}
|
||||
|
@ -201,3 +201,5 @@ def show_toolbar(request):
|
|||
DEBUG_TOOLBAR_CONFIG = {
|
||||
'SHOW_TOOLBAR_CALLBACK': show_toolbar,
|
||||
}
|
||||
|
||||
FORMAT_MODULE_PATH = 'cof.locale'
|
||||
|
|
37
cof/urls.py
37
cof/urls.py
|
@ -1,30 +1,33 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Fichier principal de configuration des urls du projet GestioCOF
|
||||
"""
|
||||
|
||||
from __future__ import division
|
||||
from __future__ import print_function
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import autocomplete_light
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url, patterns
|
||||
from django.conf.urls import include, url
|
||||
from django.conf.urls.static import static
|
||||
from django.contrib import admin
|
||||
from django.views.generic.base import TemplateView
|
||||
|
||||
import autocomplete_light
|
||||
|
||||
from django.contrib.auth import views as django_views
|
||||
from django_cas_ng import views as django_cas_views
|
||||
|
||||
from gestioncof import views as gestioncof_views, csv_views
|
||||
from gestioncof.urls import export_patterns, petitcours_patterns, \
|
||||
surveys_patterns, events_patterns, calendar_patterns, \
|
||||
clubs_patterns
|
||||
|
||||
from gestioncof.autocomplete import autocomplete
|
||||
|
||||
autocomplete_light.autodiscover()
|
||||
admin.autodiscover()
|
||||
|
||||
my_urlpatterns = [
|
||||
urlpatterns = [
|
||||
# Page d'accueil
|
||||
url(r'^$', gestioncof_views.home, name='home'),
|
||||
# Le BdA
|
||||
|
@ -48,7 +51,7 @@ my_urlpatterns = [
|
|||
url(r'^cas/logout$', django_cas_views.logout),
|
||||
url(r'^outsider/login$', gestioncof_views.login_ext),
|
||||
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
|
||||
url(r'^login$', gestioncof_views.login),
|
||||
url(r'^login$', gestioncof_views.login, name="cof-login"),
|
||||
url(r'^logout$', gestioncof_views.logout),
|
||||
# Infos persos
|
||||
url(r'^profile$', gestioncof_views.profile),
|
||||
|
@ -81,20 +84,16 @@ my_urlpatterns = [
|
|||
url(r'^utile_bda/bda_diff$', gestioncof_views.liste_bdadiff),
|
||||
url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof),
|
||||
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
|
||||
url(r'^k-fet/', include('kfet.urls'))
|
||||
] + \
|
||||
(static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
||||
if settings.DEBUG
|
||||
else [])
|
||||
# Si on est en production, MEDIA_ROOT est servi par Apache.
|
||||
# Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^gestion/', include(my_urlpatterns))
|
||||
url(r'^k-fet/', include('kfet.urls')),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
import debug_toolbar
|
||||
urlpatterns += patterns('',
|
||||
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)
|
||||
|
|
|
@ -144,9 +144,9 @@ User.profile_phone = ProfileInfo("phone", "Téléphone")
|
|||
User.profile_occupation = ProfileInfo("occupation", "Occupation")
|
||||
User.profile_departement = ProfileInfo("departement", "Departement")
|
||||
User.profile_mailing_cof = ProfileInfo("mailing_cof", "ML COF", True)
|
||||
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BDA", True)
|
||||
User.profile_mailing_bda = ProfileInfo("mailing_bda", "ML BdA", True)
|
||||
User.profile_mailing_bda_revente = ProfileInfo("mailing_bda_revente",
|
||||
"ML BDA-R", True)
|
||||
"ML BdA-R", True)
|
||||
|
||||
|
||||
class UserProfileAdmin(UserAdmin):
|
||||
|
|
|
@ -14,7 +14,7 @@ from django.contrib.auth.models import User
|
|||
from django.views.generic import ListView
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.template import loader, Context
|
||||
from django.template import loader
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.db.models import Min
|
||||
|
@ -30,13 +30,7 @@ from captcha.fields import ReCaptchaField
|
|||
|
||||
from datetime import datetime
|
||||
import base64
|
||||
import simplejson
|
||||
|
||||
|
||||
def render_template(template_path, data):
|
||||
tmpl = loader.get_template(template_path)
|
||||
context = Context(data)
|
||||
return tmpl.render(context)
|
||||
import json
|
||||
|
||||
|
||||
class DemandeListView(ListView):
|
||||
|
@ -137,14 +131,14 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
|
|||
proposed_for = proposed_for.items()
|
||||
attribdata = list(attribdata.items())
|
||||
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
||||
mainmail = render_template("petits-cours-mail-demandeur.txt",
|
||||
{"proposals": proposals,
|
||||
"unsatisfied": unsatisfied,
|
||||
"extra":
|
||||
'<textarea name="extra" '
|
||||
'style="width:99%; height: 90px;">'
|
||||
'</textarea>'
|
||||
})
|
||||
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
|
||||
"proposals": proposals,
|
||||
"unsatisfied": unsatisfied,
|
||||
"extra":
|
||||
'<textarea name="extra" '
|
||||
'style="width:99%; height: 90px;">'
|
||||
'</textarea>'
|
||||
})
|
||||
return render(request, "traitement_demande_petit_cours.html",
|
||||
{"demande": demande,
|
||||
"unsatisfied": unsatisfied,
|
||||
|
@ -153,7 +147,7 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
|
|||
"proposed_mails": proposed_mails,
|
||||
"mainmail": mainmail,
|
||||
"attribdata":
|
||||
base64.b64encode(simplejson.dumps(attribdata)
|
||||
base64.b64encode(json.dumps(attribdata)
|
||||
.encode('utf_8')),
|
||||
"redo": redo,
|
||||
"errors": errors,
|
||||
|
@ -163,8 +157,10 @@ def _finalize_traitement(request, demande, proposals, proposed_for,
|
|||
def _generate_eleve_email(demande, proposed_for):
|
||||
proposed_mails = []
|
||||
for user, matieres in proposed_for:
|
||||
msg = render_template("petits-cours-mail-eleve.txt",
|
||||
{"demande": demande, "matieres": matieres})
|
||||
msg = loader.render_to_string("petits-cours-mail-eleve.txt", {
|
||||
"demande": demande,
|
||||
"matieres": matieres
|
||||
})
|
||||
proposed_mails.append((user, msg))
|
||||
return proposed_mails
|
||||
|
||||
|
@ -262,7 +258,7 @@ def _traitement_post(request, demande):
|
|||
extra = request.POST["extra"].strip()
|
||||
redo = "redo" in request.POST
|
||||
attribdata = request.POST["attribdata"]
|
||||
attribdata = dict(simplejson.loads(base64.b64decode(attribdata)))
|
||||
attribdata = dict(json.loads(base64.b64decode(attribdata)))
|
||||
for matiere in demande.matieres.all():
|
||||
if matiere.id not in attribdata:
|
||||
unsatisfied.append(matiere)
|
||||
|
@ -278,13 +274,14 @@ def _traitement_post(request, demande):
|
|||
proposals_list = proposals.items()
|
||||
proposed_for = proposed_for.items()
|
||||
proposed_mails = _generate_eleve_email(demande, proposed_for)
|
||||
mainmail = render_template("petits-cours-mail-demandeur.txt",
|
||||
{"proposals": proposals_list,
|
||||
"unsatisfied": unsatisfied,
|
||||
"extra": extra})
|
||||
frommail = settings.PETITS_COURS_FROM
|
||||
bccaddress = settings.PETITS_COURS_BCC
|
||||
replyto = settings.PETITS_COURS_REPLYTO
|
||||
mainmail = loader.render_to_string("petits-cours-mail-demandeur.txt", {
|
||||
"proposals": proposals_list,
|
||||
"unsatisfied": unsatisfied,
|
||||
"extra": extra,
|
||||
})
|
||||
frommail = settings.MAIL_DATA['petits_cours']['FROM']
|
||||
bccaddress = settings.MAIL_DATA['petits_cours']['BCC']
|
||||
replyto = settings.MAIL_DATA['petits_cours']['REPLYTO']
|
||||
mails_to_send = []
|
||||
for (user, msg) in proposed_mails:
|
||||
msg = EmailMessage("Petits cours ENS par le COF", msg,
|
||||
|
|
|
@ -29,6 +29,12 @@ class COFCASBackend(CASBackend):
|
|||
request.session['attributes'] = attributes
|
||||
if not username:
|
||||
return None
|
||||
|
||||
# Le CAS de l'ENS accepte les logins avec des espaces au début
|
||||
# et à la fin, ainsi qu’avec une casse variable. On normalise pour
|
||||
# éviter les doublons.
|
||||
username = username.strip().lower()
|
||||
|
||||
profiles = CofProfile.objects.filter(login_clipper=username)
|
||||
if len(profiles) > 0:
|
||||
profile = profiles.order_by('-is_cof')[0]
|
||||
|
|
|
@ -43,6 +43,8 @@
|
|||
{% else %}
|
||||
<li><a href="{% url "bda-places-attribuees" tirage.id %}">Mes places</a></li>
|
||||
<li><a href="{% url "bda-revente" tirage.id %}">Revendre une place</a></li>
|
||||
<li><a href="{% url "bda-liste-revente" tirage.id %}">S'inscrire à BdA-Revente</a></li>
|
||||
<li><a href="{% url "bda-shotgun" tirage.id %}">Places disponibles immédiatement</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endfor %}
|
||||
|
@ -82,7 +84,7 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<h3 class="block-title">Gestion tirages BDA<span class="pull-right glyphicon glyphicon-list"></span></h3>
|
||||
<h3 class="block-title">Gestion tirages BdA<span class="pull-right glyphicon glyphicon-list"></span></h3>
|
||||
<div class="hm-block">
|
||||
{% if active_tirages %}
|
||||
{% for tirage in active_tirages %}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -549,7 +550,6 @@ def export_members(request):
|
|||
return response
|
||||
|
||||
|
||||
@buro_required
|
||||
def csv_export_mega(filename, qs):
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename=' + filename
|
||||
|
@ -571,12 +571,12 @@ def csv_export_mega(filename, qs):
|
|||
|
||||
@buro_required
|
||||
def export_mega_remarksonly(request):
|
||||
filename = 'remarques_mega_2015.csv'
|
||||
filename = 'remarques_mega_2016.csv'
|
||||
response = HttpResponse(content_type='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename=' + filename
|
||||
writer = unicodecsv.writer(response)
|
||||
|
||||
event = Event.objects.get(title="Mega 15")
|
||||
event = Event.objects.get(title="Mega 2016")
|
||||
commentfield = event.commentfields.get(name="Commentaires")
|
||||
for val in commentfield.values.all():
|
||||
reg = val.registration
|
||||
|
@ -599,42 +599,40 @@ def export_mega_bytype(request, type):
|
|||
if type not in types:
|
||||
raise Http404
|
||||
|
||||
event = Event.objects.get(title="Mega 15")
|
||||
event = Event.objects.get(title="Mega 2016")
|
||||
type_option = event.options.get(name="Type")
|
||||
participant_type = type_option.choices.get(value=types[type]).id
|
||||
qs = EventRegistration.objects.filter(event=event).filter(
|
||||
options__id__exact=participant_type)
|
||||
return csv_export_mega(type + '_mega_2015.csv', qs)
|
||||
return csv_export_mega(type + '_mega_2016.csv', qs)
|
||||
|
||||
|
||||
@buro_required
|
||||
def export_mega_orgas(request):
|
||||
event = Event.objects.get(title="Mega 15")
|
||||
type_option = event.options.get(name="Type")
|
||||
participant_type_a = type_option.choices.get(value="Conscrit étudiant").id
|
||||
participant_type_b = type_option.choices.get(value="Conscrit élève").id
|
||||
event = Event.objects.get(title="Mega 2016")
|
||||
type_option = event.options.get(name="Conscrit ou orga ?")
|
||||
participant_type = type_option.choices.get(value="Vieux").id
|
||||
qs = EventRegistration.objects.filter(event=event).exclude(
|
||||
options__id__in=(participant_type_a, participant_type_b))
|
||||
return csv_export_mega('orgas_mega_15.csv', qs)
|
||||
options__id=participant_type)
|
||||
return csv_export_mega('orgas_mega_2016.csv', qs)
|
||||
|
||||
|
||||
@buro_required
|
||||
def export_mega_participants(request):
|
||||
event = Event.objects.get(title="Mega 15")
|
||||
type_option = event.options.get(name="Type")
|
||||
participant_type_a = type_option.choices.get(value="Conscrit étudiant").id
|
||||
participant_type_b = type_option.choices.get(value="Conscrit élève").id
|
||||
event = Event.objects.get(title="Mega 2016")
|
||||
type_option = event.options.get(name="Conscrit ou orga ?")
|
||||
participant_type = type_option.choices.get(value="Conscrit").id
|
||||
qs = EventRegistration.objects.filter(event=event).filter(
|
||||
options__id__in=(participant_type_a, participant_type_b))
|
||||
return csv_export_mega('participants_mega_15.csv', qs)
|
||||
options__id=participant_type)
|
||||
return csv_export_mega('participants_mega_2016.csv', qs)
|
||||
|
||||
|
||||
@buro_required
|
||||
def export_mega(request):
|
||||
event = Event.objects.filter(title="Mega 15")
|
||||
event = Event.objects.filter(title="Mega 2016")
|
||||
qs = EventRegistration.objects.filter(event=event) \
|
||||
.order_by("user__username")
|
||||
return csv_export_mega('all_mega_2015.csv', qs)
|
||||
return csv_export_mega('all_mega_2016.csv', qs)
|
||||
|
||||
|
||||
@buro_required
|
||||
|
@ -710,12 +708,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 +726,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"
|
||||
|
|
|
@ -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/$"),
|
||||
]
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
{% extends 'kfet/base.html' %}
|
||||
{% load staticfiles %}
|
||||
|
||||
{% block title %}Informations sur l'article {{ article }}{% endblock %}
|
||||
{% block content-header-title %}Article - {{ article.name }}{% endblock %}
|
||||
|
@ -76,10 +77,69 @@
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /row-->
|
||||
<h2>Statistiques</h2>
|
||||
<div class="row">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Ventes de {{ article.name }}</h3>
|
||||
<canvas id="myChart1" width="200" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<div class="panel-md-margin">
|
||||
<h3>Répartition des câlins</h3>
|
||||
<canvas id="myChart2" width="200" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /row -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<script src="{% static 'kfet/js/Chart.bundle.js' %}"></script>
|
||||
<script>
|
||||
jQuery(document).ready(function() {
|
||||
var ctx1 = $("#myChart1");
|
||||
var myChart = new Chart(ctx1, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
|
||||
datasets: [{
|
||||
label: '# of Votes',
|
||||
data: [12, 19, 3, 5, 2, 3],
|
||||
backgroundColor: [
|
||||
'rgba(255, 99, 132, 0.2)',
|
||||
'rgba(54, 162, 235, 0.2)',
|
||||
'rgba(255, 206, 86, 0.2)',
|
||||
'rgba(75, 192, 192, 0.2)',
|
||||
'rgba(153, 102, 255, 0.2)',
|
||||
'rgba(255, 159, 64, 0.2)'
|
||||
],
|
||||
borderColor: [
|
||||
'rgba(255,99,132,1)',
|
||||
'rgba(54, 162, 235, 1)',
|
||||
'rgba(255, 206, 86, 1)',
|
||||
'rgba(75, 192, 192, 1)',
|
||||
'rgba(153, 102, 255, 1)',
|
||||
'rgba(255, 159, 64, 1)'
|
||||
],
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
scales: {
|
||||
yAxes: [{
|
||||
ticks: {
|
||||
beginAtZero:true
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
|
|
@ -74,7 +74,7 @@ jQuery(document).ready(function() {
|
|||
options: {
|
||||
responsive: true,
|
||||
tooltips: {
|
||||
mode: 'nearest',
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
hover: {
|
64
kfet/templates/kfet/object_stat_resume.html
Normal file
64
kfet/templates/kfet/object_stat_resume.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
<!doctype html>
|
||||
{% load staticfiles %}
|
||||
{% load dictionary_extras %}
|
||||
|
||||
<!-- TODO: SUPPRIMER-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
{# CSS #}
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
|
||||
<link href='https://fonts.googleapis.com/css?family=Roboto:400,700|Oswald:400,700|Roboto+Mono:400,700' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/jquery-confirm.css' %}">
|
||||
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/index.css' %}">
|
||||
|
||||
{# JS #}
|
||||
<script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<!-- END TODO -->
|
||||
<body>
|
||||
<div class="btn-group btn-group-justified" role="group" aria-label="select-period">
|
||||
{% for k,stat in stats.items %}
|
||||
<div class="btn-group" role="group">
|
||||
<button id="{{ stat | get_item:'btn' }}" type="button" class="btn btn-primary">{{ stat | get_item:'label' }}</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div><!-- /boutons -->
|
||||
<div id="{{ content_id}}">
|
||||
</div>
|
||||
<script>
|
||||
jQuery(document).ready(function() {
|
||||
// VARIABLES
|
||||
// défaut
|
||||
content_id = $("#{{content_id}}");
|
||||
{% for k,stat in stats.items %}
|
||||
{% if k == default_stat %}
|
||||
default_url_{{id_prefix}} = "{{ stat | get_item:'url' }}";
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
// INIT
|
||||
get_thing(default_url_{{id_prefix}}, content_id, "Ouppss ?");
|
||||
{% for k,stat in stats.items %}
|
||||
$("#{{stat|get_item:'btn'}}").on('click', function() {
|
||||
get_thing("{{stat|get_item:'url'}}", content_id, "Ouuups ?")
|
||||
});
|
||||
{% endfor %}
|
||||
// FONCTIONS
|
||||
// Permet de raffraichir un champ, étant donné :
|
||||
// thing_url : l'url contenant le contenu
|
||||
// thing_div : le div où le mettre
|
||||
// empty_... : le truc à dire si on a un contenu vide
|
||||
function get_thing(thing_url, thing_div, empty_thing_message) {
|
||||
$.get(thing_url, function(data) {
|
||||
if(jQuery.trim(data).length==0) {
|
||||
thing_div.html(empty_thing_message);
|
||||
} else {
|
||||
thing_div.html(data);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
5
kfet/templatetags/dictionary_extras.py
Normal file
5
kfet/templatetags/dictionary_extras.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from django.template.defaulttags import register
|
||||
|
||||
@register.filter
|
||||
def get_item(dictionary, key):
|
||||
return dictionary.get(key)
|
15
kfet/urls.py
15
kfet/urls.py
|
@ -122,12 +122,15 @@ urlpatterns = [
|
|||
name = 'kfet.article.update'),
|
||||
|
||||
# Article - Statistics
|
||||
url('^articles/(?P<pk>\d+)/stat/week$',
|
||||
views.ArticleStatWeek.as_view(),
|
||||
name = 'kfet.article.stats.week'),
|
||||
url('^articles/(?P<pk>\d+)/stat/day$',
|
||||
views.ArticleStatDay.as_view(),
|
||||
name = 'kfet.article.stats.day'),
|
||||
url('^articles/(?P<pk>\d+)/stat/last/$',
|
||||
views.ArticleStatLastAll.as_view(),
|
||||
name = 'kfet.article.stat.last'),
|
||||
url('^articles/(?P<pk>\d+)/stat/last/week/$',
|
||||
views.ArticleStatLastWeek.as_view(),
|
||||
name = 'kfet.article.stat.last.week'),
|
||||
url('^articles/(?P<pk>\d+)/stat/last/day/$',
|
||||
views.ArticleStatLastDay.as_view(),
|
||||
name = 'kfet.article.stat.last.day'),
|
||||
|
||||
# -----
|
||||
# K-Psul urls
|
||||
|
|
|
@ -1948,11 +1948,13 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView):
|
|||
return super(SupplierUpdate, self).form_valid(form)
|
||||
|
||||
|
||||
# -----
|
||||
# ==========
|
||||
# Statistics
|
||||
# -----
|
||||
# ==========
|
||||
|
||||
# ---------------
|
||||
# Vues génériques
|
||||
# ---------------
|
||||
# source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/
|
||||
class JSONResponseMixin(object):
|
||||
"""
|
||||
|
@ -1991,12 +1993,72 @@ class HybridDetailView(JSONResponseMixin,
|
|||
return super(HybridDetailView, self).render_to_response(context)
|
||||
|
||||
|
||||
# Article Statistiques
|
||||
# Un résume des toutes les vues de stat d'un objet
|
||||
# NE REND PAS DE JSON
|
||||
class ObjectResumeStat(DetailView):
|
||||
template_name = 'kfet/object_stat_resume.html'
|
||||
context_object_name = 'lul'
|
||||
id_prefix = 'id_a_definir'
|
||||
# nombre de vues à résumer
|
||||
nb_stat = 2
|
||||
# Le combienième est celui par defaut ?
|
||||
# (entre 0 et nb_stat-1)
|
||||
nb_default = 0
|
||||
stat_labels = ['stat_1', 'stat_2']
|
||||
stat_urls = ['url_1', 'url_2']
|
||||
|
||||
|
||||
class ArticleStat(HybridDetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
# On hérite
|
||||
# Pas besoin, c'est essentiellement inutile
|
||||
# context = super(ObjectResumeStat, self).get_context_data(**kwargs)
|
||||
object_id = self.object.id
|
||||
context = {}
|
||||
stats = {}
|
||||
for i in range(self.nb_stat):
|
||||
stats[i] = {
|
||||
'label': self.stat_labels[i],
|
||||
'btn': "btn_%s_%d_%d" % (self.id_prefix,
|
||||
object_id,
|
||||
i),
|
||||
'url': reverse_lazy(self.stat_urls[i],
|
||||
args=[object_id]),
|
||||
}
|
||||
prefix = "%s_%d" % (self.id_prefix, object_id)
|
||||
context['id_prefix'] = prefix
|
||||
context['content_id'] = "content_%s" % prefix
|
||||
context['stats'] = stats
|
||||
context['default_stat'] = self.nb_default
|
||||
context['object_id'] = object_id
|
||||
return context
|
||||
|
||||
|
||||
# ------------------------
|
||||
# Article Satistiques Last
|
||||
# ------------------------
|
||||
ID_PREFIX_ART_LAST = "last_art"
|
||||
ID_PREFIX_ART_LAST_DAYS = "last_days_art"
|
||||
ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art"
|
||||
|
||||
|
||||
# Un résumé de toutes les vues ArticleStatLast
|
||||
# NE REND PAS DE JSON
|
||||
class ArticleStatLastAll(ObjectResumeStat):
|
||||
model = Article
|
||||
template_name = 'kfet/article_stat.html'
|
||||
context_object_name = 'article'
|
||||
id_prefix = ID_PREFIX_ART_LAST
|
||||
nb_stat = 2
|
||||
nb_default = 1
|
||||
stat_labels = ["Dernières semaines", "Derniers jours"]
|
||||
stat_urls = ['kfet.article.stat.last.week',
|
||||
'kfet.article.stat.last.day']
|
||||
|
||||
|
||||
# Rend un graph des ventes sur une plage de temps à préciser.
|
||||
# Le graphique distingue les ventes sur LIQ et sur les autres trigrammes
|
||||
class ArticleStatLast(HybridDetailView):
|
||||
model = Article
|
||||
template_name = 'kfet/article_stat_last.html'
|
||||
context_object_name = 'article'
|
||||
end_date = timezone.now()
|
||||
id_prefix = "lol"
|
||||
|
@ -2006,7 +2068,7 @@ class ArticleStat(HybridDetailView):
|
|||
if self.request.GET.get('format') == 'json':
|
||||
return self.render_to_json_response(context)
|
||||
else:
|
||||
return super(ArticleStat, self).render_to_response(context)
|
||||
return super(ArticleStatLast, self).render_to_response(context)
|
||||
|
||||
# doit rendre un dictionnaire des dates
|
||||
# la première date correspond au début
|
||||
|
@ -2064,14 +2126,16 @@ class ArticleStat(HybridDetailView):
|
|||
context['nb_accounts'] = nb_accounts
|
||||
context['nb_liq'] = nb_liq
|
||||
# ID unique
|
||||
context['chart_id'] = "%s_%s" % (self.id_prefix,
|
||||
self.object.name)
|
||||
context['chart_id'] = "%s_%d" % (self.id_prefix,
|
||||
self.object.id)
|
||||
return context
|
||||
|
||||
|
||||
class ArticleStatDay(ArticleStat):
|
||||
# Rend les ventes des 7 derniers jours
|
||||
# Aujourd'hui non compris
|
||||
class ArticleStatLastDay(ArticleStatLast):
|
||||
end_date = this_morning()
|
||||
id_prefix = "last_week"
|
||||
id_prefix = ID_PREFIX_ART_LAST_DAYS
|
||||
|
||||
def get_dates(self, **kwargs):
|
||||
return lastdays(7)
|
||||
|
@ -2081,9 +2145,11 @@ class ArticleStatDay(ArticleStat):
|
|||
return daynames(days)
|
||||
|
||||
|
||||
class ArticleStatWeek(ArticleStat):
|
||||
# Rend les ventes de 7 dernières semaines
|
||||
# La semaine en cours n'est pas comprise
|
||||
class ArticleStatLastWeek(ArticleStatLast):
|
||||
end_date = this_monday_morning()
|
||||
id_prefix = "last_weeks"
|
||||
id_prefix = ID_PREFIX_ART_LAST_WEEKS
|
||||
|
||||
def get_dates(self, **kwargs):
|
||||
return lastweeks(7)
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -8,8 +8,9 @@ DBNAME="cof_gestion"
|
|||
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
||||
|
||||
# Installation de paquets utiles
|
||||
apt-get update && apt-get install -y mercurial python-pip python-dev \
|
||||
apt-get update && apt-get install -y python3-pip python3-dev python3-venv \
|
||||
libmysqlclient-dev libjpeg-dev git redis-server
|
||||
pip install -U pip
|
||||
|
||||
# Configuration et installation de mysql. Le mot de passe root est le même que
|
||||
# le mot de passe pour l'utilisateur local - pour rappel, ceci est une instance
|
||||
|
@ -23,16 +24,16 @@ mysql -uroot -p$DBPASSWD -e "CREATE DATABASE $DBNAME; GRANT ALL PRIVILEGES ON $D
|
|||
|
||||
# Installation et configuration d'Apache
|
||||
apt-get install -y apache2
|
||||
a2enmod proxy proxy_http
|
||||
a2enmod proxy proxy_http proxy_wstunnel headers
|
||||
cp /vagrant/provisioning/apache.conf /etc/apache2/sites-available/gestiocof.conf
|
||||
a2ensite gestiocof
|
||||
a2dissite 000-default
|
||||
service apache2 restart
|
||||
mkdir /var/www/static
|
||||
chown -R vagrant:www-data /var/www/static
|
||||
chown -R ubuntu:www-data /var/www/static
|
||||
|
||||
# Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh`
|
||||
cat > ~vagrant/.bash_profile <<EOF
|
||||
cat >> ~ubuntu/.bashrc <<EOF
|
||||
# On utilise la version de développement de GestioCOF
|
||||
export DJANGO_SETTINGS_MODULE='cof.settings_dev'
|
||||
|
||||
|
@ -44,25 +45,29 @@ export DBPASSWD="$DBPASSWD"
|
|||
# Permet d'utiliser les utilitaires pythons locaux
|
||||
export PATH="\$PATH:\$HOME/.local/bin"
|
||||
|
||||
# Charge le virtualenv
|
||||
source ~/venv/bin/activate
|
||||
|
||||
# On va dans /vagrant où se trouve le code de gestioCOF
|
||||
cd /vagrant
|
||||
EOF
|
||||
chown vagrant: ~vagrant/.bash_profile
|
||||
|
||||
# On va dans /vagrant où se trouve gestioCOF
|
||||
cd /vagrant
|
||||
|
||||
# Installation des dépendances python
|
||||
sudo -H -u vagrant pip install --user -r requirements.txt -r requirements-devel.txt
|
||||
# Installation du virtualenv, on utilise désormais python3
|
||||
sudo -H -u ubuntu python3 -m venv ~ubuntu/venv
|
||||
sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -U pip
|
||||
sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r requirements.txt -r requirements-devel.txt
|
||||
|
||||
# Préparation de Django
|
||||
sudo -H -u vagrant DJANGO_SETTINGS_MODULE='cof.settings_dev' DBUSER=$DBUSER DBNAME=$DBNAME DBPASSWD=$DBPASSWD sh provisioning/prepare_django.sh
|
||||
sudo -H -u ubuntu DJANGO_SETTINGS_MODULE='cof.settings_dev' DBUSER=$DBUSER DBNAME=$DBNAME DBPASSWD=$DBPASSWD bash provisioning/prepare_django.sh
|
||||
|
||||
# Installation du cron pour les mails de rappels
|
||||
sudo -H -u vagrant crontab provisioning/cron.dev
|
||||
sudo -H -u ubuntu crontab provisioning/cron.dev
|
||||
|
||||
# On installe Daphne et on demande à supervisor de le lancer
|
||||
pip install daphne
|
||||
sudo -H -u ubuntu ~ubuntu/venv/bin/pip install daphne
|
||||
apt-get install -y supervisor
|
||||
cp /vagrant/provisioning/supervisor.conf /etc/supervisor/conf.d/gestiocof.conf
|
||||
sed "s/{DBUSER}/$DBUSER/" -i /etc/supervisor/conf.d/gestiocof.conf
|
||||
|
|
|
@ -7,3 +7,4 @@ DBNAME="cof_gestion"
|
|||
DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
|
||||
|
||||
19 */12 * * * date >> /vagrant/rappels.log ; python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1
|
||||
*/5 * * * * python /vagrant/manage.py manage_revente >> /vagrant/reventes.log 2>&1
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
-r requirements.txt
|
||||
django-debug-toolbar
|
||||
ipython
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue