diff --git a/cof/routing.py b/cof/routing.py
index 7949ee3a..c604adf4 100644
--- a/cof/routing.py
+++ b/cof/routing.py
@@ -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'),
+]
diff --git a/cof/settings/common.py b/cof/settings/common.py
index 372d7d81..5ed17865 100644
--- a/cof/settings/common.py
+++ b/cof/settings/common.py
@@ -24,7 +24,7 @@ except KeyError:
try:
from .secret import (
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:
raise RuntimeError("Secrets missing")
@@ -53,6 +53,7 @@ INSTALLED_APPS = [
'django_cas_ng',
'bootstrapform',
'kfet',
+ 'kfet.open',
'channels',
'widget_tweaks',
'custommail',
@@ -179,7 +180,7 @@ CHANNEL_LAYERS = {
port=REDIS_PORT, db=REDIS_DB)
)],
},
- "ROUTING": "cof.routing.channel_routing",
+ "ROUTING": "cof.routing.routing",
}
}
diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py
index eeb5271c..a1d35b68 100644
--- a/cof/settings/secret_example.py
+++ b/cof/settings/secret_example.py
@@ -6,3 +6,5 @@ REDIS_PORT = 6379
REDIS_DB = 0
REDIS_HOST = "127.0.0.1"
ADMINS = None
+
+KFETOPEN_TOKEN = "plop"
diff --git a/kfet/consumers.py b/kfet/consumers.py
index ee096368..0f447d2d 100644
--- a/kfet/consumers.py
+++ b/kfet/consumers.py
@@ -1,39 +1,6 @@
# -*- coding: utf-8 -*-
-from django.core.serializers.json import json, DjangoJSONEncoder
-
-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()
+from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer):
diff --git a/kfet/forms.py b/kfet/forms.py
index 95e97d56..984932f2 100644
--- a/kfet/forms.py
+++ b/kfet/forms.py
@@ -25,18 +25,17 @@ from gestioncof.models import CofProfile
# -----
class DateTimeWidget(forms.DateTimeInput):
- def __init__(self, attrs = None):
+ def __init__(self, attrs=None):
super(DateTimeWidget, self).__init__(attrs)
self.attrs['format'] = '%Y-%m-%d %H:%M'
+
class Media:
css = {
- 'all': ('kfet/css/bootstrap-datetimepicker.min.css',)
- }
- js = (
- 'kfet/js/moment.js',
- 'kfet/js/moment-fr.js',
- 'kfet/js/bootstrap-datetimepicker.min.js',
- )
+ 'all': ('kfet/css/bootstrap-datetimepicker.min.css',)
+ }
+ js = ('kfet/js/bootstrap-datetimepicker.min.js',)
+
+
# -----
# Account forms
# -----
diff --git a/kfet/models.py b/kfet/models.py
index d218dd2d..d6b1cfbc 100644
--- a/kfet/models.py
+++ b/kfet/models.py
@@ -72,7 +72,8 @@ class Account(models.Model):
('change_account_password',
"Modifier le mot de passe d'une personne de l'équipe"),
('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):
diff --git a/kfet/open/__init__.py b/kfet/open/__init__.py
new file mode 100644
index 00000000..fb88af65
--- /dev/null
+++ b/kfet/open/__init__.py
@@ -0,0 +1 @@
+from .open import OpenKfet, kfet_open # noqa
diff --git a/kfet/open/consumers.py b/kfet/open/consumers.py
new file mode 100644
index 00000000..b28a4664
--- /dev/null
+++ b/kfet/open/consumers.py
@@ -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))
diff --git a/kfet/open/open.py b/kfet/open/open.py
new file mode 100644
index 00000000..7fd90c21
--- /dev/null
+++ b/kfet/open/open.py
@@ -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()
diff --git a/kfet/open/routing.py b/kfet/open/routing.py
new file mode 100644
index 00000000..681bfab2
--- /dev/null
+++ b/kfet/open/routing.py
@@ -0,0 +1,8 @@
+from channels.routing import route_class
+
+from . import consumers
+
+
+routing = [
+ route_class(consumers.OpenKfetConsumer)
+]
diff --git a/kfet/open/static/kfetopen/kfet-open.css b/kfet/open/static/kfetopen/kfet-open.css
new file mode 100644
index 00000000..6698c50b
--- /dev/null
+++ b/kfet/open/static/kfetopen/kfet-open.css
@@ -0,0 +1,48 @@
+.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;
+ 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;
+}
diff --git a/kfet/open/static/kfetopen/kfet-open.js b/kfet/open/static/kfetopen/kfet-open.js
new file mode 100644
index 00000000..b86cc5bc
--- /dev/null
+++ b/kfet/open/static/kfetopen/kfet-open.js
@@ -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);
+ }
+};
diff --git a/kfet/open/templates/kfetopen/init.html b/kfet/open/templates/kfetopen/init.html
new file mode 100644
index 00000000..3834b32a
--- /dev/null
+++ b/kfet/open/templates/kfetopen/init.html
@@ -0,0 +1,13 @@
+{% load static %}
+
+
+
+
+
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 e81f264a..12a1cd5f 100644
--- a/kfet/signals.py
+++ b/kfet/signals.py
@@ -10,6 +10,7 @@ from django.dispatch import receiver
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,
diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css
index 15b425e2..5aa74e6e 100644
--- a/kfet/static/kfet/css/index.css
+++ b/kfet/static/kfet/css/index.css
@@ -30,6 +30,15 @@ textarea {
border-radius:0 !important;
}
+.glyphicon.spinning {
+ animation: spin 1s infinite linear;
+}
+
+@keyframes spin {
+ from { transform: scale(1) rotate(0deg); }
+ to { transform: scale(1) rotate(360deg); }
+}
+
.table {
margin-bottom:0;
border-bottom:1px solid #ddd;
diff --git a/kfet/static/kfet/css/nav.css b/kfet/static/kfet/css/nav.css
index a4dabed2..bec05ccf 100644
--- a/kfet/static/kfet/css/nav.css
+++ b/kfet/static/kfet/css/nav.css
@@ -51,37 +51,37 @@
box-shadow: inset 0 5px 5px -5px #000;
}
-.navbar-nav .dropdown .dropdown-menu {
+.navbar .dropdown .dropdown-menu {
padding: 0;
border: 0;
border-radius: 0;
background-color: #FFF;
}
-.navbar-nav .dropdown .dropdown-menu > li > a {
+.navbar .dropdown .dropdown-menu > li > a {
padding: 8px 20px;
color: #000;
}
-.navbar-nav .dropdown .dropdown-menu > li > a:hover,
-.navbar-nav .dropdown .dropdown-meny > li > a:focus {
+.navbar .dropdown .dropdown-menu > li > a:hover,
+.navbar .dropdown .dropdown-meny > li > a:focus {
color: #c8102e;
background-color: transparent;
}
-.navbar-nav .dropdown .dropdown-menu .divider {
+.navbar .dropdown .dropdown-menu .divider {
margin: 0;
}
@media (min-width: 768px) {
- .navbar-nav .dropdown .dropdown-menu {
+ .navbar .dropdown .dropdown-menu {
display: block;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s;
}
- .nav .dropdown:hover .dropdown-menu {
+ .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 72ae675a..b07bb0b1 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 ;
+ get 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 282e035f..fbdfd61f 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 da37abae..a4df77cd 100644
--- a/kfet/templates/kfet/base.html
+++ b/kfet/templates/kfet/base.html
@@ -10,15 +10,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 273cdc0b..d3f748bf 100644
--- a/kfet/templates/kfet/base_nav.html
+++ b/kfet/templates/kfet/base_nav.html
@@ -12,6 +12,25 @@
+