Merge branch 'master' into aureplop/kfet_cms

This commit is contained in:
Aurélien Delobelle 2017-06-23 02:53:05 +02:00
commit 455b730cc3
41 changed files with 1162 additions and 156 deletions

View file

@ -0,0 +1 @@

View file

@ -7,11 +7,20 @@ from custommail.shortcuts import send_mass_custom_mail
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.db import models from django.db import models
from django.db.models import Count
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.conf import settings from django.conf import settings
from django.utils import timezone, formats from django.utils import timezone, formats
def get_generic_user():
generic, _ = User.objects.get_or_create(
username="bda_generic",
defaults={"email": "bda@ens.fr", "first_name": "Bureau des arts"}
)
return generic
class Tirage(models.Model): class Tirage(models.Model):
title = models.CharField("Titre", max_length=300) title = models.CharField("Titre", max_length=300)
ouverture = models.DateTimeField("Date et heure d'ouverture du tirage") ouverture = models.DateTimeField("Date et heure d'ouverture du tirage")
@ -96,32 +105,29 @@ class Spectacle(models.Model):
Envoie un mail de rappel à toutes les personnes qui ont une place pour Envoie un mail de rappel à toutes les personnes qui ont une place pour
ce spectacle. ce spectacle.
""" """
# On récupère la liste des participants # On récupère la liste des participants + le BdA
members = {} members = list(
for attr in Attribution.objects.filter(spectacle=self).all(): User.objects
member = attr.participant.user .filter(participant__attributions=self)
if member.id in members: .annotate(nb_attr=Count("id")).order_by()
members[member.id][1] = 2 )
else: bda_generic = get_generic_user()
members[member.id] = [member, 1] bda_generic.nb_attr = 1
# FIXME : faire quelque chose de ça, un utilisateur bda_generic ? members.append(bda_generic)
# # Pour le BdA
# members[0] = ['BdA', 1, 'bda@ens.fr']
# members[-1] = ['BdA', 2, 'bda@ens.fr']
# On écrit un mail personnalisé à chaque participant # On écrit un mail personnalisé à chaque participant
datatuple = [( datatuple = [(
'bda-rappel', 'bda-rappel',
{'member': member[0], 'nb_attr': member[1], 'show': self}, {'member': member, "nb_attr": member.nb_attr, 'show': self},
settings.MAIL_DATA['rappels']['FROM'], settings.MAIL_DATA['rappels']['FROM'],
[member[0].email]) [member.email])
for member in members.values() for member in members
] ]
send_mass_custom_mail(datatuple) send_mass_custom_mail(datatuple)
# On enregistre le fait que l'envoi a bien eu lieu # On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now() self.rappel_sent = timezone.now()
self.save() self.save()
# On renvoie la liste des destinataires # On renvoie la liste des destinataires
return members.values() return members
@property @property
def is_past(self): def is_past(self):

View file

@ -3,41 +3,46 @@
{% block realcontent %} {% block realcontent %}
<h2>Mails de rappels</h2> <h2>Mails de rappels</h2>
{% if sent %} {% if sent %}
<h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3> <h3>Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes</h3>
<ul> <ul>
{% for member in members %} {% for member in members %}
<li>{{ member.get_full_name }} ({{ member.email }})</li> <li>{{ member.get_full_name }} ({{ member.email }})</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% else %} {% else %}
<h3>Voulez vous envoyer les mails de rappel pour le spectacle <h3>Voulez vous envoyer les mails de rappel pour le spectacle {{ show.title }}&nbsp;?</h3>
{{ show.title }}&nbsp;?</h3>
{% if show.rappel_sent %}
<p class="error">Attention, les mails ont déjà été envoyés le
{{ show.rappel_sent }}</p>
{% endif %}
{% endif %} {% endif %}
{% if not sent %} <div class="empty-form">
<form action="" method="post"> {% if not sent %}
{% csrf_token %} <form action="" method="post">
<div class="pull-right"> {% csrf_token %}
<input class="btn btn-primary" type="submit" value="Envoyer" /> <div class="pull-right">
</div> <input class="btn btn-primary" type="submit" value="Envoyer" />
</form> </div>
{% endif %} </form>
{% endif %}
</div>
<hr \>
<p>
<em>Note :</em> le template de ce mail peut être modifié à
<a href="{% url 'admin:custommail_custommail_change' custommail.pk %}">cette adresse</a>
</p>
<hr \>
<br/>
<hr/>
<h3>Forme des mails</h3> <h3>Forme des mails</h3>
<h4>Une seule place</h4> <h4>Une seule place</h4>
{% for part in exemple_mail_1place %} {% for part in exemple_mail_1place %}
<pre>{{ part }}</pre> <pre>{{ part }}</pre>
{% endfor %} {% endfor %}
<h4>Deux places</h4> <h4>Deux places</h4>
{% for part in exemple_mail_2places %} {% for part in exemple_mail_2places %}
<pre>{{ part }}</pre> <pre>{{ part }}</pre>
{% endfor %} {% endfor %}
{% endblock %} {% endblock %}

View file

@ -36,17 +36,26 @@
</tbody> </tbody>
</table> </table>
<h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3> <h3><a href="{% url "admin:bda_attribution_add" %}?spectacle={{spectacle.id}}"><span class="glyphicon glyphicon-plus-sign"></span> Ajouter une attribution</a></h3>
<br> <div>
<button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button> <div>
<pre id="export-mails" style="display:none"> <button class="btn btn-default" type="button" onclick="toggle('export-mails')">Afficher/Cacher mails participants</button>
{%for participant in participants %}{{participant.email}}, {%endfor%} <pre id="export-mails" style="display:none">{% spaceless %}
</pre> {% for participant in participants %}{{ participant.email }}, {% endfor %}
<br> {% endspaceless %}</pre>
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button> </div>
<pre id="export-salle" style="display:none">
<div>
<button class="btn btn-default" type="button" onclick="toggle('export-salle')">Afficher/Cacher liste noms</button>
<pre id="export-salle" style="display:none">{% spaceless %}
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places {% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
{% endfor %} {% endfor %}
</pre> {% endspaceless %}</pre>
</div>
<div>
<a href="{% url 'bda-rappels' spectacle.id %}">Page d'envoi manuel des mails de rappel</a>
</div>
<script type="text/javascript" <script type="text/javascript"
src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script> src="{% static "js/joequery-Stupid-Table-Plugin/stupidtable.js" %}"></script>
<script> <script>

View file

@ -1,5 +1,6 @@
import json import json
from django.contrib.auth.models import User
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils import timezone from django.utils import timezone
@ -34,11 +35,36 @@ class TestBdAViews(TestCase):
), ),
]) ])
self.bda_user = User.objects.create_user(
username="bda_user", password="bda4ever"
)
self.bda_user.profile.is_cof = True
self.bda_user.profile.is_buro = True
self.bda_user.profile.save()
def bda_participants(self):
"""The BdA participants views can be queried"""
client = Client()
show = self.tirage.spectacle_set.first()
client.login(self.bda_user.username, "bda4ever")
tirage_resp = client.get("/bda/spectacles/{}".format(self.tirage.id))
show_resp = client.get(
"/bda/spectacles/{}/{}".format(self.tirage.id, show.id)
)
reminder_url = "/bda/mails-rappel/{}".format(show.id)
reminder_get_resp = client.get(reminder_url)
reminder_post_resp = client.post(reminder_url)
self.assertEqual(200, tirage_resp.status_code)
self.assertEqual(200, show_resp.status_code)
self.assertEqual(200, reminder_get_resp.status_code)
self.assertEqual(200, reminder_post_resp.status_code)
def test_catalogue(self): def test_catalogue(self):
"""Test the catalogue JSON API""" """Test the catalogue JSON API"""
client = Client() client = Client()
# The `list` hooh # The `list` hook
resp = client.get("/bda/catalogue/list") resp = client.get("/bda/catalogue/list")
self.assertJSONEqual( self.assertJSONEqual(
resp.content.decode("utf-8"), resp.content.decode("utf-8"),

View file

@ -44,7 +44,10 @@ urlpatterns = [
url(r'^revente-immediat/(?P<tirage_id>\d+)$', url(r'^revente-immediat/(?P<tirage_id>\d+)$',
views.revente_shotgun, views.revente_shotgun,
name="bda-shotgun"), name="bda-shotgun"),
url(r'^mails-rappel/(?P<spectacle_id>\d+)$', views.send_rappel), url(r'^mails-rappel/(?P<spectacle_id>\d+)$',
views.send_rappel,
name="bda-rappels"
),
url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles, url(r'^descriptions/(?P<tirage_id>\d+)$', views.descriptions_spectacles,
name='bda-descriptions'), name='bda-descriptions'),
url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue, url(r'^catalogue/(?P<request_type>[a-z]+)$', views.catalogue,

View file

@ -1,15 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from collections import defaultdict from collections import defaultdict
from functools import partial
import random import random
import hashlib import hashlib
import time import time
import json import json
from datetime import timedelta from datetime import timedelta
from custommail.shortcuts import ( from custommail.shortcuts import send_mass_custom_mail, send_custom_mail
send_mass_custom_mail, send_custom_mail, render_custom_mail from custommail.models import CustomMail
)
from django.shortcuts import render, get_object_or_404 from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
@ -27,7 +25,7 @@ from django.views.generic.list import ListView
from gestioncof.decorators import cof_required, buro_required from gestioncof.decorators import cof_required, buro_required
from bda.models import ( from bda.models import (
Spectacle, Participant, ChoixSpectacle, Attribution, Tirage, Spectacle, Participant, ChoixSpectacle, Attribution, Tirage,
SpectacleRevente, Salle, Quote, CategorieSpectacle SpectacleRevente, Salle, CategorieSpectacle
) )
from bda.algorithm import Algorithm from bda.algorithm import Algorithm
from bda.forms import ( from bda.forms import (
@ -305,7 +303,8 @@ def do_tirage(tirage_elt, token):
# On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues # On inscrit à BdA-Revente ceux qui n'ont pas eu les places voulues
ChoixRevente = Participant.choicesrevente.through ChoixRevente = Participant.choicesrevente.through
# Suppression des reventes demandées/enregistrées (si le tirage est relancé) # Suppression des reventes demandées/enregistrées
# (si le tirage est relancé)
( (
ChoixRevente.objects ChoixRevente.objects
.filter(spectacle__tirage=tirage_elt) .filter(spectacle__tirage=tirage_elt)
@ -612,7 +611,7 @@ def spectacle(request, tirage_id, spectacle_id):
participants_info = sorted(participants.values(), participants_info = sorted(participants.values(),
key=lambda part: part['lastname']) key=lambda part: part['lastname'])
return render(request, "bda-participants.html", return render(request, "bda/participants.html",
{"spectacle": spectacle, "participants": participants_info}) {"spectacle": spectacle, "participants": participants_info})
@ -651,20 +650,24 @@ def unpaid(request, tirage_id):
def send_rappel(request, spectacle_id): def send_rappel(request, spectacle_id):
show = get_object_or_404(Spectacle, id=spectacle_id) show = get_object_or_404(Spectacle, id=spectacle_id)
# Mails d'exemples # Mails d'exemples
exemple_mail_1place = render_custom_mail('bda-rappel', { custommail = CustomMail.objects.get(shortname="bda-rappel")
exemple_mail_1place = custommail.render({
'member': request.user, 'member': request.user,
'show': show, 'show': show,
'nb_attr': 1 'nb_attr': 1
}) })
exemple_mail_2places = render_custom_mail('bda-rappel', { exemple_mail_2places = custommail.render({
'member': request.user, 'member': request.user,
'show': show, 'show': show,
'nb_attr': 2 'nb_attr': 2
}) })
# Contexte # Contexte
ctxt = {'show': show, ctxt = {
'exemple_mail_1place': exemple_mail_1place, 'show': show,
'exemple_mail_2places': exemple_mail_2places} 'exemple_mail_1place': exemple_mail_1place,
'exemple_mail_2places': exemple_mail_2places,
'custommail': custommail,
}
# Envoi confirmé # Envoi confirmé
if request.method == 'POST': if request.method == 'POST':
members = show.send_rappel() members = show.send_rappel()
@ -673,6 +676,14 @@ def send_rappel(request, spectacle_id):
# Demande de confirmation # Demande de confirmation
else: else:
ctxt['sent'] = False ctxt['sent'] = False
if show.rappel_sent:
messages.warning(
request,
"Attention, un mail de rappel pour ce spectale a déjà été "
"envoyé le {}".format(formats.localize(
timezone.template_localtime(show.rappel_sent)
))
)
return render(request, "bda/mails-rappel.html", ctxt) return render(request, "bda/mails-rappel.html", ctxt)

View file

@ -1,3 +1,6 @@
from kfet.routing import channel_routing as kfet_channel_routings from channels.routing import include
channel_routing = kfet_channel_routings
routing = [
include('kfet.routing.routing', path=r'^/ws/k-fet'),
]

View file

@ -19,11 +19,12 @@ except ImportError:
except KeyError: except KeyError:
raise RuntimeError("Secrets missing") raise RuntimeError("Secrets missing")
# Other secrets # Other secrets
try: try:
from .secret import ( from .secret import (
SECRET_KEY, RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY, ADMINS, SECRET_KEY, RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY, ADMINS,
REDIS_PASSWD, REDIS_DB, REDIS_HOST, REDIS_PORT REDIS_PASSWD, REDIS_DB, REDIS_HOST, REDIS_PORT, KFETOPEN_TOKEN,
) )
except ImportError: except ImportError:
raise RuntimeError("Secrets missing") raise RuntimeError("Secrets missing")
@ -52,6 +53,7 @@ INSTALLED_APPS = [
'django_cas_ng', 'django_cas_ng',
'bootstrapform', 'bootstrapform',
'kfet', 'kfet',
'kfet.open',
'channels', 'channels',
'widget_tweaks', 'widget_tweaks',
'custommail', 'custommail',
@ -176,6 +178,18 @@ AUTHENTICATION_BACKENDS = (
RECAPTCHA_USE_SSL = True RECAPTCHA_USE_SSL = True
# Cache settings
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': 'redis://:{passwd}@{host}:{port}/db'
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
port=REDIS_PORT, db=REDIS_DB),
}
}
# Channels settings # Channels settings
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
@ -188,7 +202,7 @@ CHANNEL_LAYERS = {
port=REDIS_PORT, db=REDIS_DB) port=REDIS_PORT, db=REDIS_DB)
)], )],
}, },
"ROUTING": "cof.routing.channel_routing", "ROUTING": "cof.routing.routing",
} }
} }

View file

@ -6,3 +6,5 @@ REDIS_PORT = 6379
REDIS_DB = 0 REDIS_DB = 0
REDIS_HOST = "127.0.0.1" REDIS_HOST = "127.0.0.1"
ADMINS = None ADMINS = None
KFETOPEN_TOKEN = "plop"

View file

@ -40,8 +40,9 @@ a {
background: transparent; background: transparent;
} }
div.empty-form {
padding-bottom: 2em;
}
a:hover { a:hover {
color: #444; color: #444;
@ -341,10 +342,12 @@ fieldset legend {
font-weight: 700; font-weight: 700;
font-size: large; font-size: large;
color:#DE826B; color:#DE826B;
padding-bottom: .5em;
} }
#main-content h4 { #main-content h4 {
color:#DE826B; color:#DE826B;
padding-bottom: .5em;
} }
#main-content h2, #main-content h2,

View file

@ -1,39 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.core.serializers.json import json, DjangoJSONEncoder from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
from channels.generic.websockets import JsonWebsocketConsumer
class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer):
"""Custom Json Websocket Consumer.
Encode to JSON with DjangoJSONEncoder.
"""
@classmethod
def encode_json(cls, content):
return json.dumps(content, cls=DjangoJSONEncoder)
class PermConsumerMixin(object):
"""Add support to check permissions on Consumers.
Attributes:
perms_connect (list): Required permissions to connect to this
consumer.
"""
http_user = True # Enable message.user
perms_connect = []
def connect(self, message, **kwargs):
"""Check permissions on connection."""
if message.user.has_perms(self.perms_connect):
super().connect(message, **kwargs)
else:
self.close()
class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer):

View file

@ -33,12 +33,7 @@ class DateTimeWidget(forms.DateTimeInput):
css = { css = {
'all': ('kfet/css/bootstrap-datetimepicker.min.css',) 'all': ('kfet/css/bootstrap-datetimepicker.min.css',)
} }
js = ( js = ('kfet/js/bootstrap-datetimepicker.min.js',)
'kfet/js/moment.js',
'kfet/js/moment-fr.js',
'kfet/js/moment-timezone-with-data-2010-2020.js',
'kfet/js/bootstrap-datetimepicker.min.js',
)
# ----- # -----

View file

@ -73,7 +73,8 @@ class Account(models.Model):
('change_account_password', ('change_account_password',
"Modifier le mot de passe d'une personne de l'équipe"), "Modifier le mot de passe d'une personne de l'équipe"),
('special_add_account', ('special_add_account',
"Créer un compte avec une balance initiale") "Créer un compte avec une balance initiale"),
('can_force_close', "Fermer manuellement la K-Fêt"),
) )
def __str__(self): def __str__(self):

1
kfet/open/__init__.py Normal file
View file

@ -0,0 +1 @@
from .open import OpenKfet, kfet_open # noqa

25
kfet/open/consumers.py Normal file
View file

@ -0,0 +1,25 @@
from ..decorators import kfet_is_team
from ..utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
from .open import kfet_open
class OpenKfetConsumer(PermConsumerMixin, DjangoJsonWebsocketConsumer):
"""Consumer for K-Fêt Open.
WS groups:
kfet.open.base: Only carries the values visible for all users.
kfet.open.team: Carries all values (raw status...).
"""
def connection_groups(self, user, **kwargs):
"""Select which group the user should be connected."""
if kfet_is_team(user):
return ['kfet.open.team']
return ['kfet.open.base']
def connect(self, message, *args, **kwargs):
"""Send current status on connect."""
super().connect(message, *args, **kwargs)
self.send(kfet_open.export(message.user))

109
kfet/open/open.py Normal file
View file

@ -0,0 +1,109 @@
from datetime import timedelta
from django.utils import timezone
from ..decorators import kfet_is_team
from ..utils import CachedMixin
class OpenKfet(CachedMixin, object):
"""Manage "open" status of a place.
Stores raw data (e.g. sent by raspberry), and user-set values
(as force_close).
Setting differents `cache_prefix` allows different places management.
Current state persists through cache.
"""
# status is unknown after this duration
time_unknown = timedelta(minutes=15)
# status
OPENED = 'opened'
CLOSED = 'closed'
UNKNOWN = 'unknown'
# admin status
FAKE_CLOSED = 'fake_closed'
# cached attributes config
cached = {
'_raw_open': False,
'_last_update': None,
'force_close': False,
}
cache_prefix = 'kfetopen'
@property
def raw_open(self):
"""Defined as property to update `last_update` on `raw_open` update."""
return self._raw_open
@raw_open.setter
def raw_open(self, value):
self._last_update = timezone.now()
self._raw_open = value
@property
def last_update(self):
"""Prevent `last_update` to be set."""
return self._last_update
@property
def is_open(self):
"""Take into account force_close."""
return False if self.force_close else self.raw_open
def status(self):
if (self.last_update is None or
timezone.now() - self.last_update >= self.time_unknown):
return self.UNKNOWN
return self.OPENED if self.is_open else self.CLOSED
def admin_status(self, status=None):
if status is None:
status = self.status()
if status == self.CLOSED and self.raw_open:
return self.FAKE_CLOSED
return status
def _export(self):
"""Export internal state.
Used by WS initialization and updates.
Returns:
(tuple): (base, team)
- team for team users.
- base for others.
"""
status = self.status()
base = {
'status': status,
}
restrict = {
'admin_status': self.admin_status(status),
'force_close': self.force_close,
}
return base, {**base, **restrict}
def export(self, user):
"""Export internal state for a given user.
Returns:
(dict): Internal state. Only variables visible for the user are
exported, according to its permissions.
"""
base, team = self._export()
return team if kfet_is_team(user) else base
def send_ws(self):
"""Send internal state to websocket channels."""
from .consumers import OpenKfetConsumer
base, team = self._export()
OpenKfetConsumer.group_send('kfet.open.base', base)
OpenKfetConsumer.group_send('kfet.open.team', team)
kfet_open = OpenKfet()

8
kfet/open/routing.py Normal file
View file

@ -0,0 +1,8 @@
from channels.routing import route_class
from . import consumers
routing = [
route_class(consumers.OpenKfetConsumer)
]

View file

@ -0,0 +1,50 @@
.kfetopen-st-opened .bullet { background: #73C252; }
.kfetopen-st-closed .bullet { background: #B42B26; }
.kfetopen-st-unknown .bullet { background: #D4BE4C; }
.kfetopen-st-fake_closed .bullet {
background: repeating-linear-gradient(
45deg,
#73C252, #73C252 5px, #B42B26 5px, #B42B26 10px
);
}
.kfetopen {
float: left;
}
.kfetopen .base {
height: 50px;
padding: 15px;
display: inline-flex;
align-items: center;
}
.kfetopen .details {
margin: 0;
padding: 10px !important;
min-width: 200px;
font-family: "Roboto Slab";
font-size: 16px;
color: black;
}
.kfetopen .bullet {
width: 10px;
height: 10px;
border-radius: 50%;
transition: background 0.15s;
}
.kfetopen .warning {
margin-left: 15px;
}
.kfetopen .status-text {
text-transform: uppercase;
}
.kfetopen .force-close-btn {
width: 100%;
margin-top: 5px;
}

View file

@ -0,0 +1,113 @@
var OpenWS = new KfetWebsocket({
relative_url: "open/"
});
var OpenKfet = function(force_close_url, admin) {
this.force_close_url = force_close_url;
this.admin = admin;
this.status = this.UNKNOWN;
this.dom = {
status_text: $('.kfetopen .status-text'),
force_close_btn: $('.kfetopen .force-close-btn'),
warning: $('.kfetopen .warning')
},
this.dom.force_close_btn.click( () => this.toggle_force_close() );
setInterval( () => this.refresh(), this.refresh_interval * 1000);
OpenWS.add_handler( data => this.refresh(data) );
};
OpenKfet.prototype = {
// Status is unknown after . minutes without update.
time_unknown: 15,
// Maximum interval (seconds) between two UI refresh.
refresh_interval: 20,
// Prefix for classes describing place status.
class_prefix: 'kfetopen-st-',
// Set status-classes on this dom element.
target: 'body',
// Status
OPENED: "opened",
CLOSED: "closed",
UNKNOWN: "unknown",
// Admin status
FAKE_CLOSED: "fake_closed",
// Display values
status_text: {
opened: "ouverte",
closed: "fermée",
unknown: "_____"
},
force_text: {
activate: "Fermer manuellement",
deactivate: "Réouvrir la K-Fêt"
},
get is_recent() {
return this.last_update && moment().diff(this.last_update, 'minute') <= this.time_unknown;
},
refresh: function(data) {
if (data) {
$.extend(this, data);
this.last_update = moment();
}
if (!this.is_recent)
this.status = this.UNKNOWN;
this.refresh_dom();
},
refresh_dom: function() {
let status = this.status;
this.clear_class();
this.add_class(status);
this.dom.status_text.html(this.status_text[status]);
// admin specific
if (this.admin) {
this.add_class(this.admin_status);
if (this.force_close) {
this.dom.warning.addClass('in');
this.dom.force_close_btn.html(this.force_text['deactivate']);
} else {
this.dom.warning.removeClass('in');
this.dom.force_close_btn.html(this.force_text['activate']);
}
}
},
toggle_force_close: function(password) {
$.post({
url: this.force_close_url,
data: {force_close: !this.force_close},
beforeSend: function ($xhr) {
$xhr.setRequestHeader("X-CSRFToken", csrftoken);
if (password !== undefined)
$xhr.setRequestHeader("KFetPassword", password);
}
})
.fail(function($xhr) {
switch ($xhr.status) {
case 403:
requestAuth({'errors': {}}, this.toggle_force_close);
break;
}
});
},
clear_class: function() {
let re = new RegExp('(^|\\s)' + this.class_prefix + '\\S+', 'g');
$(this.target).attr('class', (i, c) => c ? c.replace(re, '') : '');
},
add_class: function(status) {
$(this.target).addClass(this.class_prefix + status);
}
};

View file

@ -0,0 +1,13 @@
{% load static %}
<link rel="stylesheet" type="text/css" href="{% static "kfetopen/kfet-open.css" %}">
<script type="text/javascript" src="{% static "kfetopen/kfet-open.js" %}"></script>
<script type="text/javascript">
$( function() {
kfet_open = new OpenKfet(
"{% url "kfet.open.edit_force_close" %}",
{{ perms.kfet.is_team|yesno:"true,false" }}
);
});
</script>

322
kfet/open/tests.py Normal file
View file

@ -0,0 +1,322 @@
import json
from datetime import timedelta
from django.contrib.auth.models import AnonymousUser, Permission, User
from django.test import Client
from django.utils import timezone
from channels.channel import Group
from channels.test import ChannelTestCase, WSClient
from . import kfet_open, OpenKfet
from .consumers import OpenKfetConsumer
class OpenKfetTest(ChannelTestCase):
"""OpenKfet object unit-tests suite."""
def setUp(self):
self.kfet_open = OpenKfet()
def tearDown(self):
self.kfet_open.clear_cache()
def test_defaults(self):
"""Default values."""
self.assertFalse(self.kfet_open.raw_open)
self.assertIsNone(self.kfet_open.last_update)
self.assertFalse(self.kfet_open.force_close)
self.assertFalse(self.kfet_open.is_open)
def test_raw_open(self):
"""Get and set raw_open; last_update is renewed."""
for raw_open in [True, False]:
prev_update = self.kfet_open.last_update
self.kfet_open.raw_open = raw_open
self.assertEqual(raw_open, self.kfet_open.raw_open)
self.assertNotEqual(prev_update, self.kfet_open.last_update)
def test_force_close(self):
"""Get and set force_close."""
for force_close in [True, False]:
self.kfet_open.force_close = force_close
self.assertEqual(force_close, self.kfet_open.force_close)
def test_is_open(self):
"""If force_close is disabled, is_open is raw_open."""
self.kfet_open.force_close = False
for raw_open in [True, False]:
self.kfet_open.raw_open = raw_open
self.assertEqual(raw_open, self.kfet_open.is_open)
def test_is_open_force_close(self):
"""If force_close is enabled, is_open is False."""
self.kfet_open.force_close = True
for raw_open in [True, False]:
self.kfet_open.raw_open = raw_open
self.assertFalse(self.kfet_open.is_open)
def test_status(self):
# (raw_open, force_close, expected status, expected admin)
cases = [
(False, False, OpenKfet.CLOSED, OpenKfet.CLOSED),
(False, True, OpenKfet.CLOSED, OpenKfet.CLOSED),
(True, False, OpenKfet.OPENED, OpenKfet.OPENED),
(True, True, OpenKfet.CLOSED, OpenKfet.FAKE_CLOSED),
]
for raw_open, force_close, exp_stat, exp_adm_stat in cases:
self.kfet_open.raw_open = raw_open
self.kfet_open.force_close = force_close
self.assertEqual(exp_stat, self.kfet_open.status())
self.assertEqual(exp_adm_stat, self.kfet_open.admin_status())
def test_status_unknown(self):
self.kfet_open.raw_open = True
self.kfet_open._last_update = timezone.now() - timedelta(days=30)
self.assertEqual(OpenKfet.UNKNOWN, self.kfet_open.status())
def test_export_user(self):
"""Export is limited for an anonymous user."""
export = self.kfet_open.export(AnonymousUser())
self.assertSetEqual(
set(['status']),
set(export),
)
def test_export_team(self):
"""Export all values for a team member."""
user = User.objects.create_user('team', '', 'team')
user.user_permissions.add(Permission.objects.get(codename='is_team'))
export = self.kfet_open.export(user)
self.assertSetEqual(
set(['status', 'admin_status', 'force_close']),
set(export),
)
def test_send_ws(self):
Group('kfet.open.base').add('test.open.base')
Group('kfet.open.team').add('test.open.team')
self.kfet_open.send_ws()
recv_base = self.get_next_message('test.open.base', require=True)
base = json.loads(recv_base['text'])
self.assertSetEqual(
set(['status']),
set(base),
)
recv_admin = self.get_next_message('test.open.team', require=True)
admin = json.loads(recv_admin['text'])
self.assertSetEqual(
set(['status', 'admin_status', 'force_close']),
set(admin),
)
class OpenKfetViewsTest(ChannelTestCase):
"""OpenKfet views unit-tests suite."""
def setUp(self):
# get some permissions
perms = {
'kfet.is_team': Permission.objects.get(codename='is_team'),
'kfet.can_force_close': Permission.objects.get(codename='can_force_close'),
}
# authenticated user and its client
self.u = User.objects.create_user('user', '', 'user')
self.c = Client()
self.c.login(username='user', password='user')
# team user and its clients
self.t = User.objects.create_user('team', '', 'team')
self.t.user_permissions.add(perms['kfet.is_team'])
self.c_t = Client()
self.c_t.login(username='team', password='team')
# admin user and its client
self.a = User.objects.create_user('admin', '', 'admin')
self.a.user_permissions.add(
perms['kfet.is_team'], perms['kfet.can_force_close'],
)
self.c_a = Client()
self.c_a.login(username='admin', password='admin')
def tearDown(self):
kfet_open.clear_cache()
def test_door(self):
"""Edit raw_status."""
for sent, expected in [(1, True), (0, False)]:
resp = Client().post('/k-fet/open/raw_open', {
'raw_open': sent,
'token': 'plop',
})
self.assertEqual(200, resp.status_code)
self.assertEqual(expected, kfet_open.raw_open)
def test_force_close(self):
"""Edit force_close."""
for sent, expected in [(1, True), (0, False)]:
resp = self.c_a.post('/k-fet/open/force_close', {'force_close': sent})
self.assertEqual(200, resp.status_code)
self.assertEqual(expected, kfet_open.force_close)
def test_force_close_forbidden(self):
"""Can't edit force_close without kfet.can_force_close permission."""
clients = [Client(), self.c, self.c_t]
for client in clients:
resp = client.post('/k-fet/open/force_close', {'force_close': 0})
self.assertEqual(403, resp.status_code)
class OpenKfetConsumerTest(ChannelTestCase):
"""OpenKfet consumer unit-tests suite."""
def test_standard_user(self):
"""Lambda user is added to kfet.open.base group."""
# setup anonymous client
c = WSClient()
# connect
c.send_and_consume('websocket.connect', path='/ws/k-fet/open',
fail_on_none=True)
# initialization data is replied on connection
self.assertIsNotNone(c.receive())
# client belongs to the 'kfet.open' group...
OpenKfetConsumer.group_send('kfet.open.base', {'test': 'plop'})
self.assertEqual(c.receive(), {'test': 'plop'})
# ...but not to the 'kfet.open.admin' one
OpenKfetConsumer.group_send('kfet.open.team', {'test': 'plop'})
self.assertIsNone(c.receive())
def test_team_user(self):
"""Team user is added to kfet.open.team group."""
# setup team user and its client
t = User.objects.create_user('team', '', 'team')
t.user_permissions.add(
Permission.objects.get(codename='is_team')
)
c = WSClient()
c.force_login(t)
# connect
c.send_and_consume('websocket.connect', path='/ws/k-fet/open',
fail_on_none=True)
# initialization data is replied on connection
self.assertIsNotNone(c.receive())
# client belongs to the 'kfet.open.admin' group...
OpenKfetConsumer.group_send('kfet.open.team', {'test': 'plop'})
self.assertEqual(c.receive(), {'test': 'plop'})
# ... but not to the 'kfet.open' one
OpenKfetConsumer.group_send('kfet.open.base', {'test': 'plop'})
self.assertIsNone(c.receive())
class OpenKfetScenarioTest(ChannelTestCase):
"""OpenKfet functionnal tests suite."""
def setUp(self):
# anonymous client (for views)
self.c = Client()
# anonymous client (for websockets)
self.c_ws = WSClient()
# root user
self.r = User.objects.create_superuser('root', '', 'root')
# its client (for views)
self.r_c = Client()
self.r_c.login(username='root', password='root')
# its client (for websockets)
self.r_c_ws = WSClient()
self.r_c_ws.force_login(self.r)
def tearDown(self):
kfet_open.clear_cache()
def ws_connect(self, ws_client):
ws_client.send_and_consume(
'websocket.connect', path='/ws/k-fet/open',
fail_on_none=True,
)
return ws_client.receive(json=True)
def test_scenario_0(self):
"""Clients connect."""
# test for anonymous user
msg = self.ws_connect(self.c_ws)
self.assertSetEqual(
set(['status']),
set(msg),
)
# test for root user
msg = self.ws_connect(self.r_c_ws)
self.assertSetEqual(
set(['status', 'admin_status', 'force_close']),
set(msg),
)
def test_scenario_1(self):
"""Clients connect, door opens, enable force close."""
self.ws_connect(self.c_ws)
self.ws_connect(self.r_c_ws)
# door sent "I'm open!"
self.c.post('/k-fet/open/raw_open', {
'raw_open': True,
'token': 'plop',
})
# anonymous user agree
msg = self.c_ws.receive(json=True)
self.assertEqual(OpenKfet.OPENED, msg['status'])
# root user too
msg = self.r_c_ws.receive(json=True)
self.assertEqual(OpenKfet.OPENED, msg['status'])
self.assertEqual(OpenKfet.OPENED, msg['admin_status'])
# admin says "no it's closed"
self.r_c.post('/k-fet/open/force_close', {'force_close': True})
# so anonymous user see it's closed
msg = self.c_ws.receive(json=True)
self.assertEqual(OpenKfet.CLOSED, msg['status'])
# root user too
msg = self.r_c_ws.receive(json=True)
self.assertEqual(OpenKfet.CLOSED, msg['status'])
# but root knows things
self.assertEqual(OpenKfet.FAKE_CLOSED, msg['admin_status'])
self.assertTrue(msg['force_close'])
def test_scenario_2(self):
"""Starting falsely closed, clients connect, disable force close."""
kfet_open.raw_open = True
kfet_open.force_close = True
msg = self.ws_connect(self.c_ws)
self.assertEqual(OpenKfet.CLOSED, msg['status'])
msg = self.ws_connect(self.r_c_ws)
self.assertEqual(OpenKfet.CLOSED, msg['status'])
self.assertEqual(OpenKfet.FAKE_CLOSED, msg['admin_status'])
self.assertTrue(msg['force_close'])
self.r_c.post('/k-fet/open/force_close', {'force_close': False})
msg = self.c_ws.receive(json=True)
self.assertEqual(OpenKfet.OPENED, msg['status'])
msg = self.r_c_ws.receive(json=True)
self.assertEqual(OpenKfet.OPENED, msg['status'])
self.assertEqual(OpenKfet.OPENED, msg['admin_status'])
self.assertFalse(msg['force_close'])

11
kfet/open/urls.py Normal file
View file

@ -0,0 +1,11 @@
from django.conf.urls import url
from . import views
urlpatterns = [
url(r'^raw_open$', views.raw_open,
name='kfet.open.edit_raw_open'),
url(r'^force_close$', views.force_close,
name='kfet.open.edit_force_close'),
]

32
kfet/open/views.py Normal file
View file

@ -0,0 +1,32 @@
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import permission_required
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from .open import kfet_open
TRUE_STR = ['1', 'True', 'true']
@csrf_exempt
@require_POST
def raw_open(request):
token = request.POST.get('token')
if token != settings.KFETOPEN_TOKEN:
raise PermissionDenied
raw_open = request.POST.get('raw_open') in TRUE_STR
kfet_open.raw_open = raw_open
kfet_open.send_ws()
return HttpResponse()
@permission_required('kfet.can_force_close', raise_exception=True)
@require_POST
def force_close(request):
force_close = request.POST.get('force_close') in TRUE_STR
kfet_open.force_close = force_close
kfet_open.send_ws()
return HttpResponse()

View file

@ -1,12 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division, from channels.routing import include, route_class
print_function, unicode_literals)
from builtins import *
from channels.routing import route, route_class from . import consumers
from kfet import consumers
channel_routing = [
route_class(consumers.KPsul, path=r"^/ws/k-fet/k-psul/$"), routing = [
route_class(consumers.KPsul, path=r'^/k-psul/$'),
include('kfet.open.routing.routing', path=r'^/open'),
] ]

View file

@ -11,6 +11,7 @@ from django.utils.safestring import mark_safe
def messages_on_login(sender, request, user, **kwargs): def messages_on_login(sender, request, user, **kwargs):
if (not user.username == 'kfet_genericteam' and if (not user.username == 'kfet_genericteam' and
user.has_perm('kfet.is_team') and user.has_perm('kfet.is_team') and
hasattr(request, 'GET') and
'k-fet' in request.GET.get('next', '')): 'k-fet' in request.GET.get('next', '')):
messages.info(request, mark_safe( messages.info(request, mark_safe(
'<a href="{}" class="genericteam" target="_blank">' '<a href="{}" class="genericteam" target="_blank">'

View file

@ -105,3 +105,14 @@ ul {
.toggle:hover .base { .toggle:hover .base {
display: none; display: none;
} }
/* Spinning animation -------------- */
.glyphicon.spinning {
animation: spin 1s infinite linear;
}
@keyframes spin {
from { transform: scale(1) rotate(0deg); }
to { transform: scale(1) rotate(360deg); }
}

View file

@ -0,0 +1,88 @@
.navbar {
background: #000;
color: #DDD;
font-family: Oswald;
border: 0;
}
.navbar .navbar-brand {
padding: 3px 25px;
}
.navbar .navbar-brand img {
height: 44px;
}
.navbar .navbar-toggle .icon-bar {
background-color: #FFF;
}
.navbar-nav {
font-weight: bold;
font-size: 14px;
text-transform: uppercase;
margin: 0 -15px;
}
@media (min-width: 768px) {
.navbar-nav {
margin: 0px;
}
.navbar-right {
margin-right: -15px;
}
}
.navbar-nav a {
transition: background-color, box-shadow, color;
transition-duration: 0.15s;
}
.navbar-nav > li > a {
color: #FFF;
}
.navbar-nav > li:hover > a,
.navbar-nav > li > a:focus,
.nav .open > a:hover,
.nav .open > a:focus {
background-color: #C8102E;
color: #FFF;
box-shadow: inset 0 5px 5px -5px #000;
}
.navbar .dropdown .dropdown-menu {
padding: 0;
border: 0;
border-radius: 0;
background-color: #FFF;
}
.navbar .dropdown .dropdown-menu > li > a {
padding: 8px 20px;
color: #000;
}
.navbar .dropdown .dropdown-menu > li > a:hover,
.navbar .dropdown .dropdown-meny > li > a:focus {
color: #c8102e;
background-color: transparent;
}
.navbar .dropdown .dropdown-menu .divider {
margin: 0;
}
@media (min-width: 768px) {
.navbar .dropdown .dropdown-menu {
display: block;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s;
}
.navbar .dropdown:hover .dropdown-menu {
visibility: visible;
opacity: 1;
}
}

View file

@ -24,19 +24,24 @@ $(document).ready(function() {
class KfetWebsocket { class KfetWebsocket {
static get defaults() { static get defaults() {
return {"relative_url": "", "default_msg": {}, "handlers": []}; return {
relative_url: '',
default_msg: {},
handlers: [],
base_path: '/ws/k-fet/'
};
} }
constructor(data) { constructor(data) {
$.extend(this, this.constructor.defaults, data); $.extend(this, this.constructor.defaults, data);
if (window.location.pathname.startsWith('/gestion/'))
this.base_path = '/gestion' + this.base_path;
} }
get url() { get url() {
var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; var protocol = window.location.protocol == 'https:' ? 'wss' : 'ws';
var location_host = window.location.host; var host = window.location.host;
var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host; return protocol + "://" + host + this.base_path + this.relative_url;
return websocket_protocol+"://" + location_url + this.relative_url ;
} }
add_handler(handler) { add_handler(handler) {
@ -60,7 +65,7 @@ class KfetWebsocket {
} }
var OperationWebSocket = new KfetWebsocket({ var OperationWebSocket = new KfetWebsocket({
'relative_url': '/ws/k-fet/k-psul/', 'relative_url': 'k-psul/',
'default_msg': {'opegroups':[],'opes':[],'checkouts':[],'articles':[]}, 'default_msg': {'opegroups':[],'opes':[],'checkouts':[],'articles':[]},
}); });

View file

@ -4,10 +4,6 @@
{% load l10n %} {% load l10n %}
{% block extra_head %} {% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% if account.user == request.user %} {% if account.user == request.user %}
<script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/Chart.bundle.js' %}"></script>

View file

@ -14,15 +14,24 @@
{# CSS #} {# 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 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|Roboto+Mono:400,700|Roboto+Slab:400,700' rel='stylesheet' type='text/css'> <link href='https://fonts.googleapis.com/css?family=Roboto:400,700|Roboto+Mono:400,700|Roboto+Slab:400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/jquery-confirm.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' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/index.css' %}">
{# JS #} {# JS #}
<script type="text/javascript" src="{% static "kfet/js/js.cookie.js" %}"></script>
<script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
{% include "kfetopen/init.html" %}
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}
{# Vieux IE pas comprendre HTML5 et Media Queries #} {# Vieux IE pas comprendre HTML5 et Media Queries #}

View file

@ -9,6 +9,25 @@
</a> </a>
</div> </div>
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li class="kfetopen dropdown">
<ul class="base nav dropdown-toggle" data-toggle="dropdown">
<li class="bullet"></li>
{% if perms.kfet.is_team %}
<li class="warning fade">
<span class="glyphicon glyphicon-warning-sign"></span>
</li>
{% endif %}
</ul>
<div class="details dropdown-menu">
La K-Fêt est
<b><span class="status-text">
<span class="glyphicon glyphicon-refresh spinning"></span>
</span></b>.
{% if perms.kfet.is_team %}
<button class="btn btn-primary force-close-btn">&nbsp;</button>
{% endif %}
</div>
</li>
{% for item in menu_items %} {% for item in menu_items %}
{% if item.text == "Accueil" %} {% if item.text == "Accueil" %}
<li class="{{ item.active_class }} hidden-xs hidden-sm"> <li class="{{ item.active_class }} hidden-xs hidden-sm">

View file

@ -2,10 +2,7 @@
{% load l10n staticfiles widget_tweaks %} {% load l10n staticfiles widget_tweaks %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/multiple-select.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/multiple-select.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/multiple-select.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/multiple-select.js' %}"></script>
{{ filter_form.media }} {{ filter_form.media }}
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>

View file

@ -0,0 +1,59 @@
{% extends "kfet/base_col_1.html" %}
{% load staticfiles %}
{% load kfet_tags %}
{% block title %}Accueil{% endblock %}
{% block header %}{% endblock %}
{% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/home.css' %}">
{% endblock %}
{% block main-size %}col-sm-10 col-sm-offset-1{% endblock %}
{% block main-content %}
<div class="content-right-block">
<h2>Carte</h2>
<div class="column-row">
<div class="column-sm-1 column-md-2 column-lg-3">
<div class="unbreakable carte-inverted">
{% if pressions %}
<h3>Pressions du moment</h3>
<ul class="carte">
{% for article in pressions %}
<li class="carte-line">
<div class="filler"></div>
<span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
</li>
{% endfor %}
</ul>
{% endif %}
</div><!-- endblock unbreakable -->
{% for article in articles %}
{% ifchanged article.category %}
{% if not forloop.first %}
</ul>
</div><!-- endblock unbreakable -->
{% endif %}
<div class="unbreakable">
<h3>{{ article.category.name }}</h3>
<ul class="carte">
{% endifchanged %}
<li class="carte-line">
<div class="filler"></div>
<span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price | ukf:False}} UKF</span>
</li>
{% if forloop.last %}
</ul>
</div><!-- endblock unbreakable -->
{% endif %}
{% endfor %}
</div>
</div>
</div>
{% endblock %}

View file

@ -1,10 +1,6 @@
{% extends "kfet/base_col_1.html" %} {% extends "kfet/base_col_1.html" %}
{% load static widget_tweaks %} {% load static widget_tweaks %}
{% block extra_head %}
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
{% endblock %}
{% block title %}Nouvel inventaire{% endblock %} {% block title %}Nouvel inventaire{% endblock %}
{% block header-title %}Création d'un inventaire{% endblock %} {% block header-title %}Création d'un inventaire{% endblock %}

View file

@ -2,15 +2,8 @@
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/kpsul_grid.css' %}"> <link rel="stylesheet" style="text/css" href="{% static 'kfet/css/kpsul_grid.css' %}">
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% endblock %} {% endblock %}

View file

@ -1,14 +1,6 @@
{% extends 'kfet/base_col_2.html' %} {% extends 'kfet/base_col_2.html' %}
{% load staticfiles %} {% load staticfiles %}
{% block extra_head %}
<link rel="stylesheet" style="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.js' %}"></script>
{% endblock %}
{% block title %}Transferts{% endblock %} {% block title %}Transferts{% endblock %}
{% block header-title %}Transferts{% endblock %} {% block header-title %}Transferts{% endblock %}

View file

@ -3,7 +3,6 @@
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/transfers_form.css' %}"> <link rel="stylesheet" type="text/css" href="{% static 'kfet/css/transfers_form.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
{% endblock %} {% endblock %}
{% block title %}Nouveaux transferts{% endblock %} {% block title %}Nouveaux transferts{% endblock %}

View file

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django.conf.urls import url from django.conf.urls import include, url
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from kfet import autocomplete, views from kfet import autocomplete, views
@ -197,6 +197,7 @@ urlpatterns = [
(views.SettingsUpdate.as_view()), (views.SettingsUpdate.as_view()),
name='kfet.settings.update'), name='kfet.settings.update'),
# ----- # -----
# Transfers urls # Transfers urls
# ----- # -----
@ -241,3 +242,8 @@ urlpatterns = [
url(r'^orders/(?P<pk>\d+)/to_inventory$', views.order_to_inventory, url(r'^orders/(?P<pk>\d+)/to_inventory$', views.order_to_inventory,
name='kfet.order.to_inventory'), name='kfet.order.to_inventory'),
] ]
urlpatterns += [
# K-Fêt Open urls
url('^open/', include('kfet.open.urls')),
]

View file

@ -1,4 +1,11 @@
import math import math
import json
from django.core.cache import cache
from django.core.serializers.json import DjangoJSONEncoder
from channels.channel import Group
from channels.generic.websockets import JsonWebsocketConsumer
from .config import kfet_config from .config import kfet_config
@ -8,3 +15,101 @@ def to_ukf(balance, is_cof=False):
subvention = kfet_config.subvention_cof subvention = kfet_config.subvention_cof
grant = (1 + subvention / 100) if is_cof else 1 grant = (1 + subvention / 100) if is_cof else 1
return math.floor(balance * 10 * grant) return math.floor(balance * 10 * grant)
# Storage
class CachedMixin:
"""Object with cached properties.
Attributes:
cached (dict): Keys are cached properties. Associated value is the
returned default by getters in case the key is missing from cache.
cache_prefix (str): Used to prefix keys in cache.
"""
cached = {}
cache_prefix = ''
def __init__(self, cache_prefix=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if cache_prefix is not None:
self.cache_prefix = cache_prefix
def cachekey(self, attr):
return '{}__{}'.format(self.cache_prefix, attr)
def __getattr__(self, attr):
if attr in self.cached:
return cache.get(self.cachekey(attr), self.cached.get(attr))
elif hasattr(super(), '__getattr__'):
return super().__getattr__(attr)
else:
raise AttributeError("can't get attribute")
def __setattr__(self, attr, value):
if attr in self.cached:
cache.set(self.cachekey(attr), value)
elif hasattr(super(), '__setattr__'):
super().__setattr__(attr, value)
else:
raise AttributeError("can't set attribute")
def clear_cache(self):
cache.delete_many([
self.cachekey(attr) for attr in self.cached.keys()
])
# Consumers
class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer):
"""Custom Json Websocket Consumer.
Encode to JSON with DjangoJSONEncoder.
"""
@classmethod
def encode_json(cls, content):
return json.dumps(content, cls=DjangoJSONEncoder)
class PermConsumerMixin:
"""Add support to check permissions on consumers.
Attributes:
perms_connect (list): Required permissions to connect to this
consumer.
message.user is appended as argument to each connection_groups method call.
"""
http_user = True # Enable message.user
perms_connect = []
def connect(self, message, **kwargs):
"""Check permissions on connection."""
if message.user.has_perms(self.perms_connect):
super().connect(message, **kwargs)
else:
self.close()
def raw_connect(self, message, **kwargs):
# Same as original raw_connect method of JsonWebsocketConsumer
# We add user to connection_groups call.
groups = self.connection_groups(user=message.user, **kwargs)
for group in groups:
Group(group, channel_layer=message.channel_layer).add(message.reply_channel)
self.connect(message, **kwargs)
def raw_disconnect(self, message, **kwargs):
# Same as original raw_connect method of JsonWebsocketConsumer
# We add user to connection_groups call.
groups = self.connection_groups(user=message.user, **kwargs)
for group in groups:
Group(group, channel_layer=message.channel_layer).discard(message.reply_channel)
self.disconnect(message, **kwargs)
def connection_groups(self, user, **kwargs):
"""`message.user` is available as `user` arg. Original behavior."""
super().connection_groups(user, user, **kwargs)

View file

@ -6,6 +6,7 @@ django-cas-ng==3.5.7
django-djconfig==0.5.3 django-djconfig==0.5.3
django-grappelli==2.8.1 django-grappelli==2.8.1
django-recaptcha==1.0.5 django-recaptcha==1.0.5
django-redis-cache==1.7.1
mysqlclient==1.3.7 mysqlclient==1.3.7
Pillow==3.3.0 Pillow==3.3.0
six==1.10.0 six==1.10.0
@ -13,14 +14,14 @@ unicodecsv==0.14.1
icalendar==3.10 icalendar==3.10
django-bootstrap-form==3.2.1 django-bootstrap-form==3.2.1
asgiref==1.1.1 asgiref==1.1.1
daphne==1.2.0 daphne==1.3.0
asgi-redis==1.3.0 asgi-redis==1.3.0
statistics==1.0.3.5 statistics==1.0.3.5
future==0.15.2 future==0.15.2
django-widget-tweaks==1.4.1 django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3 ldap3
channels==1.1.3 channels==1.1.5
python-dateutil python-dateutil
wagtail==1.10.* wagtail==1.10.*
wagtailmenus==2.2.* wagtailmenus==2.2.*