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 3857cab5..2384cf87 100644
--- a/cof/settings/common.py
+++ b/cof/settings/common.py
@@ -53,6 +53,7 @@ INSTALLED_APPS = [
'django_cas_ng',
'bootstrapform',
'kfet',
+ 'kfet.open',
'channels',
'widget_tweaks',
'custommail',
@@ -91,7 +92,6 @@ TEMPLATES = [
'djconfig.context_processors.config',
'gestioncof.shared.context_processor',
'kfet.context_processors.auth',
- 'kfet.context_processors.kfet_open',
'kfet.context_processors.config',
],
},
@@ -180,7 +180,7 @@ CHANNEL_LAYERS = {
port=REDIS_PORT, db=REDIS_DB)
)],
},
- "ROUTING": "cof.routing.channel_routing",
+ "ROUTING": "cof.routing.routing",
}
}
diff --git a/kfet/consumers.py b/kfet/consumers.py
index 08b3de25..0f447d2d 100644
--- a/kfet/consumers.py
+++ b/kfet/consumers.py
@@ -1,53 +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()
-
-
-class KfetOpen(JsonWebsocketConsumer):
- def connection_groups(self, **kwargs):
- return ['kfet.is_open']
-
- def connect(self, message, **kwargs):
- pass
-
- def receive(self, content, **kwargs):
- pass
-
- def disconnect(self, message, **kwargs):
- pass
+from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin
class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer):
diff --git a/kfet/context_processors.py b/kfet/context_processors.py
index e4d4bcb5..4c7b4fe4 100644
--- a/kfet/context_processors.py
+++ b/kfet/context_processors.py
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
from django.contrib.auth.context_processors import PermWrapper
-from .views import KFET_OPEN, KFET_FORCE_CLOSE
-
from kfet.config import kfet_config
@@ -16,15 +14,5 @@ def auth(request):
return {}
-def kfet_open(request):
- (kfet_open, kfet_open_date) = KFET_OPEN()
- kfet_force_close = KFET_FORCE_CLOSE()
- return {
- 'kfet_open': kfet_open,
- 'kfet_open_date': kfet_open_date.isoformat(),
- 'kfet_force_close': kfet_force_close,
- }
-
-
def config(request):
return {'kfet_config': kfet_config}
diff --git a/kfet/decorators.py b/kfet/decorators.py
index 3dc76767..0c8a1a76 100644
--- a/kfet/decorators.py
+++ b/kfet/decorators.py
@@ -6,7 +6,4 @@ from django.contrib.auth.decorators import user_passes_test
def kfet_is_team(user):
return user.has_perm('kfet.is_team')
-def can_force_close(user):
- return user.has_perm('force_close_kfet')
-
teamkfet_required = user_passes_test(kfet_is_team)
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 83a0f6b5..a8bf7608 100644
--- a/kfet/models.py
+++ b/kfet/models.py
@@ -73,7 +73,7 @@ class Account(models.Model):
"Modifier le mot de passe d'une personne de l'équipe"),
('special_add_account',
"Créer un compte avec une balance initiale"),
- ('force_close_kfet', "Fermer manuelement la K-Fêt"),
+ ('can_force_close', "Fermer manuelement 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..54eda9e4
--- /dev/null
+++ b/kfet/open/open.py
@@ -0,0 +1,84 @@
+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 multiple places management.
+ Current state persists through cache.
+
+ """
+ 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 _export(self):
+ """Export internal state.
+
+ Used by WS initialization and updates.
+
+ Returns:
+ (tuple): (base, team)
+ - team for team users.
+ - base for others.
+
+ """
+ base = {
+ 'is_open': self.is_open,
+ 'last_update': self.last_update,
+ }
+ restrict = {
+ 'raw_open': self.raw_open,
+ 'force_close': self.force_close,
+ }
+ return base, {**base, **restrict}
+
+ def export(self, user=None):
+ """Export internal state.
+
+ Returns:
+ (dict): Internal state. Only variables visible for the user are
+ exported, according to its permissions. If no user is given, it
+ returns all available values.
+
+ """
+ base, team = self._export()
+ return team if user is None or 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..6edfeb7e
--- /dev/null
+++ b/kfet/open/static/kfetopen/kfet-open.js
@@ -0,0 +1,126 @@
+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: 2,
+ // Maximum interval (seconds) between two UI refresh.
+ refresh_interval: 10,
+
+ // 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.refresh_status();
+ this.refresh_dom();
+ },
+
+ refresh_status: function() {
+ // find new status
+ let status = this.UNKNOWN;
+ if (this.is_recent)
+ status = this.is_open ? this.OPENED : this.CLOSED;
+ this.status = status;
+
+ // admin specific
+ if (this.admin) {
+ let admin_status = status;
+ if (status == this.CLOSED && this.raw_open)
+ admin_status = this.FAKE_CLOSED;
+ this.admin_status = admin_status;
+ }
+ },
+
+ 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(new_value, 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..8cf1b6d0
--- /dev/null
+++ b/kfet/open/tests.py
@@ -0,0 +1,303 @@
+import json
+
+from django.contrib.auth.models import AnonymousUser, Permission, User
+from django.test import Client
+
+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_export_user(self):
+ """Export is limited for an anonymous user."""
+ export = self.kfet_open.export(AnonymousUser())
+ self.assertSetEqual(
+ set(['is_open', 'last_update']),
+ 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(['is_open', 'last_update', 'raw_open', 'force_close']),
+ set(export),
+ )
+
+ def test_export(self):
+ """Export all by default."""
+ export = self.kfet_open.export()
+ self.assertSetEqual(
+ set(['is_open', 'last_update', 'raw_open', '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(['is_open', 'last_update']),
+ set(base),
+ )
+
+ recv_admin = self.get_next_message('test.open.team', require=True)
+ admin = json.loads(recv_admin['text'])
+ self.assertSetEqual(
+ set(['is_open', 'last_update', 'raw_open', '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})
+ 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(['is_open', 'last_update']),
+ set(msg),
+ )
+
+ # test for root user
+ msg = self.ws_connect(self.r_c_ws)
+ self.assertSetEqual(
+ set(['is_open', 'last_update', 'raw_open', '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})
+
+ # anonymous user agree
+ msg = self.c_ws.receive(json=True)
+ self.assertTrue(msg['is_open'])
+
+ # root user too
+ msg = self.r_c_ws.receive(json=True)
+ self.assertTrue(msg['is_open'])
+ self.assertTrue(msg['raw_open'])
+
+ # 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.assertFalse(msg['is_open'])
+
+ # root user too
+ msg = self.r_c_ws.receive(json=True)
+ self.assertFalse(msg['is_open'])
+ # but root knows things
+ self.assertTrue(msg['raw_open'])
+ 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.assertFalse(msg['is_open'])
+
+ msg = self.ws_connect(self.r_c_ws)
+ self.assertFalse(msg['is_open'])
+ self.assertTrue(msg['raw_open'])
+ 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.assertTrue(msg['is_open'])
+
+ msg = self.r_c_ws.receive(json=True)
+ self.assertTrue(msg['is_open'])
+ self.assertTrue(msg['raw_open'])
+ 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..5245b4c4
--- /dev/null
+++ b/kfet/open/views.py
@@ -0,0 +1,27 @@
+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):
+ 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 5db0101f..54de69ae 100644
--- a/kfet/routing.py
+++ b/kfet/routing.py
@@ -1,13 +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/$"),
- route_class(consumers.KfetOpen, path=r"^/ws/k-fet/is_open/$"),
+
+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/home.css b/kfet/static/kfet/css/home.css
index 2b831d9b..718159c3 100644
--- a/kfet/static/kfet/css/home.css
+++ b/kfet/static/kfet/css/home.css
@@ -52,12 +52,3 @@ li.carte-line {
.unbreakable.carte-inverted {
background: #FFDBC7;
}
-
-#open_status {
- color: white;
-}
-
-#open_status_parent {
- padding-left:0px;
- padding-right:0px;
-}
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 4258e123..bec05ccf 100644
--- a/kfet/static/kfet/css/nav.css
+++ b/kfet/static/kfet/css/nav.css
@@ -6,7 +6,7 @@
}
.navbar .navbar-brand {
- padding: 3px 15px 3px 25px;
+ padding: 3px 25px;
}
.navbar .navbar-brand img {
@@ -51,55 +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;
}
-#kfet-open {
- font-weight: bold;
- font-size: 14px;
- width:10px;
- height:10px;
- text-transform: uppercase;
- border-radius: 50%;
- background-color: white;
- display: inline-block;
-}
-
-#kfet-open-wrapper {
- padding-top: 18px;
- margin: 0px 10px;
- display: inline-block;
- line-height: 10px;
-}
-
-.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/static/kfet/js/kfet_open.js b/kfet/static/kfet/js/kfet_open.js
deleted file mode 100644
index 08a030a5..00000000
--- a/kfet/static/kfet/js/kfet_open.js
+++ /dev/null
@@ -1,126 +0,0 @@
-function kfet_open(init_date, init_status, init_force_close, force_close_url, force_open_url) {
- // VARIABLES
- var kfet_open_bullet = $('#kfet-open');
- var open_status = $('#open_status');
- var force_close_button = $('#force_close_button');
- // bullet
- var open_color = "#73C252";
- var closed_color = "#B42B26";
- var unknown_color = "#ECD03E";
- // status bar
- var open_color_status = "#73C252";
- var closed_color_status = "#B42B26";
- var unknown_color_status = "#D4BE4C";
-
- var kfet_open_date = init_date;
- var kfet_open = init_status;
- var force_close = init_force_close;
-
- // EVENT
- force_close_button.click(forceClose);
-
- // INITIALISATION
- update_open();
- update_force_button();
-
- // On recharge toute les 30sec
- // (dans le cas où le statut deviendrait inconnu)
- setInterval(function() {
- update_open();
- }, 30 * 1000); // 30 * 1000 milsec
-
- // FONCTIONS
- function forceClose(password = '') {
- if (force_close) {
- force_url = force_open_url;
- } else {
- force_url = force_close_url;
- }
- $.ajax({
- dataType: "html",
- url : force_url,
- method : "GET",
- beforeSend: function ($xhr) {
- if (password != '')
- $xhr.setRequestHeader("KFetPassword", password);
- },
- })
- .fail(function($xhr) {
- var data = $xhr.responseJSON;
- switch ($xhr.status) {
- case 403:
- requestAuth({'errors':{}}, forceClose);
- break;
- }
- lock = 0;
- });
- }
-
- function kfet_open_min() {
- return moment().diff(kfet_open_date, 'minute');
- }
- function do_kfet_close() {
- kfet_open_bullet.css({'background-color': closed_color});
- open_status.parent().css({'background-color': closed_color_status});
- open_status.html("FERMÉE");
- }
- function do_kfet_open() {
- kfet_open_bullet.css({'background-color': open_color});
- open_status.parent().css({'background-color': open_color_status});
- open_status.html("OUVERTE");
- }
- function do_kfet_unknown() {
- kfet_open_bullet.css({'background-color': unknown_color});
- open_status.parent().css({'background-color': unknown_color_status});
- open_status.html("?????");
- }
- function update_open() {
- var nb_min = kfet_open_min();
- console.log("K-Fêt ouverte : " + (kfet_open&&(!force_close)));
- console.log(nb_min + " minute(s) depuis la dernière mise à jour");
- if (force_close) {
- do_kfet_close();
- } else {
- if (nb_min > 15) {
- do_kfet_unknown();
- } else if (kfet_open){
- do_kfet_open();
- } else {
- do_kfet_close();
- }
- }
- }
- function update_force_button() {
- if (force_close) {
- force_close_button.html('Réouvrir la K-Fêt');
- } else {
- force_close_button.html('Fermer manuellement');
- }
- }
- // SYNCHRONIZATION
-
- 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;
- var socket = new ReconnectingWebSocket(websocket_protocol+"://" + location_url + "/ws/k-fet/is_open/");
-
- socket.onmessage = function(e) {
- var data = JSON.parse(e.data);
-
- if (data['door_action']) {
- console.log("* Message reçu de la part de la porte.");
-
- kfet_open_date = moment.utc(data['door_action']['kfet_open_date']);
- kfet_open = data['door_action']['kfet_open'];
-
- update_open();
- }
- if (data['force_action']) {
- force_close = data['force_action']['force_close'];
- console.log("* Message reçu de la part d'un-e utilisat-rice-eur. Close = " + force_close);
-
- update_open();
- update_force_button();
- }
- }
-}
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 281e261d..a4df77cd 100644
--- a/kfet/templates/kfet/base.html
+++ b/kfet/templates/kfet/base.html
@@ -10,40 +10,23 @@
{# CSS #}
+
{# JS #}
+
+
-
-
-
+
- {# K-Fêt open #}
-
+ {% include "kfetopen/init.html" %}
{% block extra_head %}{% endblock %}
diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html
index f90eb004..512d32e7 100644
--- a/kfet/templates/kfet/base_nav.html
+++ b/kfet/templates/kfet/base_nav.html
@@ -12,7 +12,25 @@
-
+