diff --git a/bda/__init__.py b/bda/__init__.py
index e69de29b..8b137891 100644
--- a/bda/__init__.py
+++ b/bda/__init__.py
@@ -0,0 +1 @@
+
diff --git a/bda/models.py b/bda/models.py
index 0228b4c0..41462d70 100644
--- a/bda/models.py
+++ b/bda/models.py
@@ -7,11 +7,20 @@ from custommail.shortcuts import send_mass_custom_mail
from django.contrib.sites.models import Site
from django.db import models
+from django.db.models import Count
from django.contrib.auth.models import User
from django.conf import settings
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):
title = models.CharField("Titre", max_length=300)
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
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][1] = 2
- else:
- members[member.id] = [member, 1]
- # FIXME : faire quelque chose de ça, un utilisateur bda_generic ?
- # # Pour le BdA
- # members[0] = ['BdA', 1, 'bda@ens.fr']
- # members[-1] = ['BdA', 2, 'bda@ens.fr']
+ # On récupère la liste des participants + le BdA
+ members = list(
+ User.objects
+ .filter(participant__attributions=self)
+ .annotate(nb_attr=Count("id")).order_by()
+ )
+ bda_generic = get_generic_user()
+ bda_generic.nb_attr = 1
+ members.append(bda_generic)
# On écrit un mail personnalisé à chaque participant
datatuple = [(
'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'],
- [member[0].email])
- for member in members.values()
+ [member.email])
+ for member in members
]
send_mass_custom_mail(datatuple)
# On enregistre le fait que l'envoi a bien eu lieu
self.rappel_sent = timezone.now()
self.save()
# On renvoie la liste des destinataires
- return members.values()
+ return members
@property
def is_past(self):
diff --git a/bda/templates/bda/mails-rappel.html b/bda/templates/bda/mails-rappel.html
index 73625d1c..c10503b0 100644
--- a/bda/templates/bda/mails-rappel.html
+++ b/bda/templates/bda/mails-rappel.html
@@ -3,41 +3,46 @@
{% block realcontent %}
Mails de rappels
{% if sent %}
- Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes
-
- {% for member in members %}
- {{ member.get_full_name }} ({{ member.email }})
- {% endfor %}
-
+ Les mails de rappel pour le spectacle {{ show.title }} ont bien été envoyés aux personnes suivantes
+
+ {% for member in members %}
+ {{ member.get_full_name }} ({{ member.email }})
+ {% endfor %}
+
{% else %}
- Voulez vous envoyer les mails de rappel pour le spectacle
- {{ show.title }} ?
- {% if show.rappel_sent %}
- Attention, les mails ont déjà été envoyés le
- {{ show.rappel_sent }}
- {% endif %}
+ Voulez vous envoyer les mails de rappel pour le spectacle {{ show.title }} ?
{% endif %}
- {% if not sent %}
-
- {% endif %}
+
+
+
+
+
+ Note : le template de ce mail peut être modifié à
+ cette adresse
+
+
+
-
-
Forme des mails
Une seule place
- {% for part in exemple_mail_1place %}
- {{ part }}
- {% endfor %}
+ {% for part in exemple_mail_1place %}
+ {{ part }}
+ {% endfor %}
Deux places
{% for part in exemple_mail_2places %}
- {{ part }}
+ {{ part }}
{% endfor %}
+
{% endblock %}
diff --git a/bda/templates/bda-participants.html b/bda/templates/bda/participants.html
similarity index 77%
rename from bda/templates/bda-participants.html
rename to bda/templates/bda/participants.html
index 289d1761..85af4a2e 100644
--- a/bda/templates/bda-participants.html
+++ b/bda/templates/bda/participants.html
@@ -36,17 +36,26 @@
-
- Afficher/Cacher mails participants
-
-{%for participant in participants %}{{participant.email}}, {%endfor%}
-
-
- Afficher/Cacher liste noms
-
+
+
+
Afficher/Cacher mails participants
+
{% spaceless %}
+ {% for participant in participants %}{{ participant.email }}, {% endfor %}
+ {% endspaceless %}
+
+
+
+
Afficher/Cacher liste noms
+
{% spaceless %}
{% for participant in participants %}{{participant.name}} : {{participant.nb_places}} places
{% endfor %}
-
+ {% endspaceless %}
+
+
+
+
+
+
diff --git a/kfet/open/tests.py b/kfet/open/tests.py
new file mode 100644
index 00000000..1d6d5529
--- /dev/null
+++ b/kfet/open/tests.py
@@ -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'])
diff --git a/kfet/open/urls.py b/kfet/open/urls.py
new file mode 100644
index 00000000..bd227b96
--- /dev/null
+++ b/kfet/open/urls.py
@@ -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'),
+]
diff --git a/kfet/open/views.py b/kfet/open/views.py
new file mode 100644
index 00000000..4f1efa5f
--- /dev/null
+++ b/kfet/open/views.py
@@ -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()
diff --git a/kfet/routing.py b/kfet/routing.py
index 5ea343cb..54de69ae 100644
--- a/kfet/routing.py
+++ b/kfet/routing.py
@@ -1,12 +1,11 @@
# -*- coding: utf-8 -*-
-from __future__ import (absolute_import, division,
- print_function, unicode_literals)
-from builtins import *
+from channels.routing import include, route_class
-from channels.routing import route, route_class
-from kfet import consumers
+from . 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'),
]
diff --git a/kfet/signals.py b/kfet/signals.py
index 374e0dca..c677ac9c 100644
--- a/kfet/signals.py
+++ b/kfet/signals.py
@@ -11,6 +11,7 @@ from django.utils.safestring import mark_safe
def messages_on_login(sender, request, user, **kwargs):
if (not user.username == 'kfet_genericteam' and
user.has_perm('kfet.is_team') and
+ hasattr(request, 'GET') and
'k-fet' in request.GET.get('next', '')):
messages.info(request, mark_safe(
'
'
diff --git a/kfet/static/kfet/css/base/misc.css b/kfet/static/kfet/css/base/misc.css
index 9b09edae..680fb1ee 100644
--- a/kfet/static/kfet/css/base/misc.css
+++ b/kfet/static/kfet/css/base/misc.css
@@ -105,3 +105,14 @@ ul {
.toggle:hover .base {
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); }
+}
diff --git a/kfet/static/kfet/css/nav.css b/kfet/static/kfet/css/nav.css
new file mode 100644
index 00000000..bec05ccf
--- /dev/null
+++ b/kfet/static/kfet/css/nav.css
@@ -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;
+ }
+}
diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js
index c977d534..b34f2005 100644
--- a/kfet/static/kfet/js/kfet.js
+++ b/kfet/static/kfet/js/kfet.js
@@ -24,19 +24,24 @@ $(document).ready(function() {
class KfetWebsocket {
static get defaults() {
- return {"relative_url": "", "default_msg": {}, "handlers": []};
+ return {
+ relative_url: '',
+ default_msg: {},
+ handlers: [],
+ base_path: '/ws/k-fet/'
+ };
}
constructor(data) {
$.extend(this, this.constructor.defaults, data);
+ if (window.location.pathname.startsWith('/gestion/'))
+ this.base_path = '/gestion' + this.base_path;
}
get url() {
- var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws';
- var location_host = window.location.host;
- var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host;
-
- return websocket_protocol+"://" + location_url + this.relative_url ;
+ var protocol = window.location.protocol == 'https:' ? 'wss' : 'ws';
+ var host = window.location.host;
+ return protocol + "://" + host + this.base_path + this.relative_url;
}
add_handler(handler) {
@@ -60,7 +65,7 @@ class KfetWebsocket {
}
var OperationWebSocket = new KfetWebsocket({
- 'relative_url': '/ws/k-fet/k-psul/',
+ 'relative_url': 'k-psul/',
'default_msg': {'opegroups':[],'opes':[],'checkouts':[],'articles':[]},
});
diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html
index 15238b64..eb3122d6 100644
--- a/kfet/templates/kfet/account_read.html
+++ b/kfet/templates/kfet/account_read.html
@@ -4,10 +4,6 @@
{% load l10n %}
{% block extra_head %}
-
-
-
-
{% if account.user == request.user %}
diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html
index ecd69c3d..8f90f459 100644
--- a/kfet/templates/kfet/base.html
+++ b/kfet/templates/kfet/base.html
@@ -14,15 +14,24 @@
{# CSS #}
+
{# JS #}
+
+
+
+
+
+
+ {% include "kfetopen/init.html" %}
+
{% block extra_head %}{% endblock %}
{# Vieux IE pas comprendre HTML5 et Media Queries #}
diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html
index d5cf75a0..ba582bcf 100644
--- a/kfet/templates/kfet/base_nav.html
+++ b/kfet/templates/kfet/base_nav.html
@@ -9,6 +9,25 @@
+
+
+
+ {% if perms.kfet.is_team %}
+
+
+
+ {% endif %}
+
+
+
{% for item in menu_items %}
{% if item.text == "Accueil" %}
diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html
index a849f5c4..c13115dd 100644
--- a/kfet/templates/kfet/history.html
+++ b/kfet/templates/kfet/history.html
@@ -2,10 +2,7 @@
{% load l10n staticfiles widget_tweaks %}
{% block extra_head %}
-
-
-
{{ filter_form.media }}
diff --git a/kfet/templates/kfet/home.html b/kfet/templates/kfet/home.html
new file mode 100644
index 00000000..e5175dc3
--- /dev/null
+++ b/kfet/templates/kfet/home.html
@@ -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 %}
+
+{% endblock %}
+
+{% block main-size %}col-sm-10 col-sm-offset-1{% endblock %}
+
+{% block main-content %}
+
+
+
Carte
+
+
+
+ {% if pressions %}
+
Pressions du moment
+
+ {% for article in pressions %}
+
+
+ {{ article.name }}
+ {{ article.price | ukf:False}} UKF
+
+ {% endfor %}
+
+ {% endif %}
+
+ {% for article in articles %}
+ {% ifchanged article.category %}
+ {% if not forloop.first %}
+
+
+ {% endif %}
+
+
{{ article.category.name }}
+
+ {% endifchanged %}
+
+
+ {{ article.name }}
+ {{ article.price | ukf:False}} UKF
+
+ {% if forloop.last %}
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+{% endblock %}
+
diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html
index f131a021..c3084e71 100644
--- a/kfet/templates/kfet/inventory_create.html
+++ b/kfet/templates/kfet/inventory_create.html
@@ -1,10 +1,6 @@
{% extends "kfet/base_col_1.html" %}
{% load static widget_tweaks %}
-{% block extra_head %}
-
-
-{% endblock %}
{% block title %}Nouvel inventaire{% endblock %}
{% block header-title %}Création d'un inventaire{% endblock %}
diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html
index 606c1da6..a6a01d84 100644
--- a/kfet/templates/kfet/kpsul.html
+++ b/kfet/templates/kfet/kpsul.html
@@ -2,15 +2,8 @@
{% load staticfiles %}
{% block extra_head %}
-
-
-
-
-
-
-
{% endblock %}
diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html
index 5bef84cf..f6778b3f 100644
--- a/kfet/templates/kfet/transfers.html
+++ b/kfet/templates/kfet/transfers.html
@@ -1,14 +1,6 @@
{% extends 'kfet/base_col_2.html' %}
{% load staticfiles %}
-{% block extra_head %}
-
-
-
-
-
-{% endblock %}
-
{% block title %}Transferts{% endblock %}
{% block header-title %}Transferts{% endblock %}
diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html
index c675c3e0..b44bfd2d 100644
--- a/kfet/templates/kfet/transfers_create.html
+++ b/kfet/templates/kfet/transfers_create.html
@@ -3,7 +3,6 @@
{% block extra_head %}
-
{% endblock %}
{% block title %}Nouveaux transferts{% endblock %}
diff --git a/kfet/urls.py b/kfet/urls.py
index 49e43de8..c3499b18 100644
--- a/kfet/urls.py
+++ b/kfet/urls.py
@@ -1,6 +1,6 @@
# -*- 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 kfet import autocomplete, views
@@ -197,6 +197,7 @@ urlpatterns = [
(views.SettingsUpdate.as_view()),
name='kfet.settings.update'),
+
# -----
# Transfers urls
# -----
@@ -241,3 +242,8 @@ urlpatterns = [
url(r'^orders/(?P\d+)/to_inventory$', views.order_to_inventory,
name='kfet.order.to_inventory'),
]
+
+urlpatterns += [
+ # K-Fêt Open urls
+ url('^open/', include('kfet.open.urls')),
+]
diff --git a/kfet/utils.py b/kfet/utils.py
index 64b8b179..6b709eb5 100644
--- a/kfet/utils.py
+++ b/kfet/utils.py
@@ -1,4 +1,11 @@
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
@@ -8,3 +15,101 @@ def to_ukf(balance, is_cof=False):
subvention = kfet_config.subvention_cof
grant = (1 + subvention / 100) if is_cof else 1
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)
diff --git a/requirements.txt b/requirements.txt
index 93b5ebdf..1da8c361 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,6 +6,7 @@ django-cas-ng==3.5.7
django-djconfig==0.5.3
django-grappelli==2.8.1
django-recaptcha==1.0.5
+django-redis-cache==1.7.1
mysqlclient==1.3.7
Pillow==3.3.0
six==1.10.0
@@ -13,14 +14,14 @@ unicodecsv==0.14.1
icalendar==3.10
django-bootstrap-form==3.2.1
asgiref==1.1.1
-daphne==1.2.0
+daphne==1.3.0
asgi-redis==1.3.0
statistics==1.0.3.5
future==0.15.2
django-widget-tweaks==1.4.1
git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail
ldap3
-channels==1.1.3
+channels==1.1.5
python-dateutil
wagtail==1.10.*
wagtailmenus==2.2.*