From 4808650fa05f1ee859885476dd2b82a8315835a5 Mon Sep 17 00:00:00 2001 From: Qwann Date: Thu, 9 Feb 2017 14:05:29 +0100 Subject: [PATCH 001/161] kfet_open is updatable --- kfet/migrations/00048_kfet_open_cache.py | 24 +++++++++++++++++++++++ kfet/models.py | 2 ++ kfet/urls.py | 3 +++ kfet/views.py | 25 +++++++++++++++++++++++- 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 kfet/migrations/00048_kfet_open_cache.py diff --git a/kfet/migrations/00048_kfet_open_cache.py b/kfet/migrations/00048_kfet_open_cache.py new file mode 100644 index 00000000..ed9b7041 --- /dev/null +++ b/kfet/migrations/00048_kfet_open_cache.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0047_auto_20170104_1528'), + ] + + operations = [ + migrations.AddField( + model_name='settings', + name='value_boolean', + field=models.NullBooleanField(default=None), + ), + migrations.AddField( + model_name='settings', + name='value_datetime', + field=models.DateTimeField(blank=True, null=True, default=None), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 419cd0a0..359bdfca 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -569,6 +569,8 @@ class GlobalPermissions(models.Model): ('special_add_account', "Créer un compte avec une balance initiale") ) + + class Settings(models.Model): name = models.CharField( max_length = 45, diff --git a/kfet/urls.py b/kfet/urls.py index 271ed917..11d2a1b9 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -167,6 +167,9 @@ urlpatterns = [ permission_required('kfet.change_settings') (views.SettingsUpdate.as_view()), name='kfet.settings.update'), + url('^settings/kfet_open/$', + views.UpdateKfetOpen.as_view(), + name='kfet.settings.kfet_open'), # ----- # Transfers urls diff --git a/kfet/views.py b/kfet/views.py index 7083d489..023dab55 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -7,7 +7,7 @@ from builtins import * from django.shortcuts import render, get_object_or_404, redirect from django.core.exceptions import PermissionDenied, ValidationError from django.core.cache import cache -from django.views.generic import ListView, DetailView +from django.views.generic import ListView, DetailView, View from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView from django.core.urlresolvers import reverse_lazy from django.contrib import messages @@ -40,6 +40,29 @@ import statistics def home(request): return render(request, "kfet/base.html") + +def KFET_OPEN(): + kfet_open_date = cache.get('KFET_OPEN_DATE') + kfet_open = cache.get('KFET_OPEN') + if not kfet_open_date: + kfet_open_date = timezone.now() + cache.set('KFET_OPEN_DATE', kfet_open_date) + if not kfet_open: + kfet_open = False + cache.set('KFET_OPEN', kfet_open) + return (kfet_open, kfet_open_date) + + +class UpdateKfetOpen(View): + def get(self, request, *args, **kwargs): + open_string = request.GET.get('open') + is_open = not (open_string == "false" or open_string == "False") + cache.set('KFET_OPEN', is_open) + cache.set('KFET_OPEN_DATE', timezone.now()) + (is_open_get, time) = KFET_OPEN() + return HttpResponse("%r at %s" % (is_open_get, time.isoformat())) + + @teamkfet_required def login_genericteam(request): # Check si besoin de déconnecter l'utilisateur de CAS From f87f1ceff138ee2c41853a1538dac16ab79e3269 Mon Sep 17 00:00:00 2001 From: Qwann Date: Sat, 11 Feb 2017 00:29:12 +0100 Subject: [PATCH 002/161] kfetOpen bullet working --- cof/settings_dev.py | 1 + kfet/consumers.py | 18 +++++++++++ kfet/context_processors.py | 10 ++++++ kfet/routing.py | 1 + kfet/static/kfet/css/nav.css | 20 +++++++++++- kfet/static/kfet/js/kfet_open.js | 54 +++++++++++++++++++++++++++++++ kfet/templates/kfet/base.html | 2 ++ kfet/templates/kfet/base_nav.html | 7 ++-- kfet/templates/kfet/kpsul.html | 1 - kfet/views.py | 13 ++++++-- 10 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 kfet/static/kfet/js/kfet_open.js diff --git a/cof/settings_dev.py b/cof/settings_dev.py index f6521222..4d604cac 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -83,6 +83,7 @@ TEMPLATES = [ 'django.core.context_processors.static', 'gestioncof.shared.context_processor', 'kfet.context_processors.auth', + 'kfet.context_processors.kfet_open', ], }, }, diff --git a/kfet/consumers.py b/kfet/consumers.py index dcd69bdf..72e3b21e 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -24,3 +24,21 @@ class KPsul(JsonWebsocketConsumer): def disconnect(self, message, **kwargs): pass + +class KfetOpen(JsonWebsocketConsumer): + + # Set to True if you want them, else leave out + strict_ordering = False + slight_ordering = False + + 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 diff --git a/kfet/context_processors.py b/kfet/context_processors.py index ef4f2e64..f3b8f76f 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -5,6 +5,8 @@ from __future__ import (absolute_import, division, from builtins import * from django.contrib.auth.context_processors import PermWrapper +from .views import KFET_OPEN + def auth(request): if hasattr(request, 'real_user'): @@ -13,3 +15,11 @@ def auth(request): 'perms': PermWrapper(request.real_user), } return {} + + +def kfet_open(resquest): + (kfet_open, kfet_open_date) = KFET_OPEN() + return { + 'kfet_open': kfet_open, + 'kfet_open_date': kfet_open_date, + } diff --git a/kfet/routing.py b/kfet/routing.py index 5ea343cb..5db0101f 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -9,4 +9,5 @@ from kfet 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/$"), ] diff --git a/kfet/static/kfet/css/nav.css b/kfet/static/kfet/css/nav.css index 5ffc7b24..9e2c5462 100644 --- a/kfet/static/kfet/css/nav.css +++ b/kfet/static/kfet/css/nav.css @@ -14,7 +14,7 @@ nav { } nav .navbar-brand { - padding:3px 25px; + padding:3px 15px 3px 25px; } nav .navbar-brand img { @@ -44,6 +44,24 @@ nav a { background-color:#C8102E; } +#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; +} + .dropdown-menu { padding:0; } diff --git a/kfet/static/kfet/js/kfet_open.js b/kfet/static/kfet/js/kfet_open.js new file mode 100644 index 00000000..9f5e4c22 --- /dev/null +++ b/kfet/static/kfet/js/kfet_open.js @@ -0,0 +1,54 @@ +function kfet_open(init_date, init_satus) { + // VARIABLES + var kfet_open_bullet = $('#kfet-open'); + var open_color = "#73C252"; + var closed_color = "#B42B26"; + var unknown_color = "#ECD03E"; + var kfet_open_date = init_date; + var kfet_open = init_status; + // INITIALISAITION + update_open_bullet(); + // FONCTIONS + function nb_min_diff() { + var date_now = new Date(); + // On calcule le nb de minutes depuis le dernier + // envoi d'information + tmp = date_now - kfet_open_date; + + tmp = Math.floor(tmp/1000); // Nombre de secondes entre les 2 dates + diff_sec = tmp % 60; // Extraction du nombre de secondes + + tmp = Math.floor((tmp-diff_sec)/60); // Nombre de minutes (partie entière) + + return tmp; + } + function update_open_bullet() { + nb_min = nb_min_diff(); + console.log("K-Fêt ouverte : " + kfet_open); + console.log(nb_min + " minute(s) depuis la dernière mise à jour"); + if (nb_min > 5) { + kfet_open_bullet.css({'background-color': unknown_color}); + } else if (kfet_open){ + kfet_open_bullet.css({'background-color': open_color}); + } else { + kfet_open_bullet.css({'background-color': closed_color}); + } + } + // SYNCHRONIZATION + websocket_msg_default = {'last_op': 0} + + 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 = $.extend({}, websocket_msg_default, JSON.parse(e.data)); + console.log("Message reçu de la part de la porte."); + + kfet_open_date = new Date(data['kfet_open_date']); + kfet_open = data['kfet_open']; + + update_open_bullet(); + } +} diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index 173a5fb7..7587cb7b 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -18,6 +18,8 @@ + + {% block extra_head %}{% endblock %} diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index b5c98375..d884d110 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -12,6 +12,7 @@ + - - diff --git a/kfet/templates/kfet/home.html b/kfet/templates/kfet/home.html index 40a1debd..6d4f163f 100644 --- a/kfet/templates/kfet/home.html +++ b/kfet/templates/kfet/home.html @@ -20,8 +20,8 @@
?????
diff --git a/kfet/urls.py b/kfet/urls.py index aad89cd4..47f14b52 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -211,7 +211,10 @@ urlpatterns = [ url('^kfet_open/$', views.UpdateKfetOpen.as_view(), - name='kfet.settings.kfet_open'), + name='kfet.kfet_open'), + url('^force_close/$', + views.UpdateForceClose.as_view(), + name='kfet.force_close'), # ----- # Transfers urls diff --git a/kfet/views.py b/kfet/views.py index 7856cf22..77bb061d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -94,15 +94,37 @@ class UpdateKfetOpen(View): cache.set('KFET_OPEN_DATE', timezone.now()) # Websocket - websocket_data = {} - websocket_data['kfet_open'] = is_open - websocket_data['kfet_open_date'] = timezone.now() + websocket_data = { + 'door_action': { + 'kfet_open': is_open, + 'kfet_open_date': timezone.now(), + }, + } consumers.KfetOpen.group_send('kfet.is_open', websocket_data) (is_open_get, time) = KFET_OPEN() return HttpResponse("%r at %s" % (is_open_get, time.isoformat())) +class UpdateForceClose(View): + def get(self, request, *args, **kwargs): + force_close = "close" in request.GET + cache.set('KFET_FORCE_CLOSE', force_close) + + # Websocket + websocket_data = { + 'force_action': { + 'force_close': force_close, + }, + } + consumers.KfetOpen.group_send('kfet.is_open', websocket_data) + + force_close_get = KFET_FORCE_CLOSE() + time = timezone.now() + return HttpResponse("closed : %r at %s" % (force_close_get, + time.isoformat())) + + @teamkfet_required def login_genericteam(request): # Check si besoin de déconnecter l'utilisateur de CAS From f18bb9f336dbca42af6375a227232106325bf083 Mon Sep 17 00:00:00 2001 From: Qwann Date: Thu, 9 Mar 2017 17:27:58 +0100 Subject: [PATCH 012/161] permission added --- kfet/templates/kfet/home.html | 2 ++ kfet/views.py | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/kfet/templates/kfet/home.html b/kfet/templates/kfet/home.html index 6d4f163f..a4f9ee4c 100644 --- a/kfet/templates/kfet/home.html +++ b/kfet/templates/kfet/home.html @@ -19,11 +19,13 @@
?????
+ {% if perms.kfet.is_team %} + {% endif %}
diff --git a/kfet/views.py b/kfet/views.py index 77bb061d..4278aa88 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -124,6 +124,11 @@ class UpdateForceClose(View): return HttpResponse("closed : %r at %s" % (force_close_get, time.isoformat())) + @method_decorator(login_required) + @method_decorator(teamkfet_required) + def dispatch(self, *args, **kwargs): + return super(UpdateForceClose, self).dispatch(*args, **kwargs) + @teamkfet_required def login_genericteam(request): From cd31c5525459b380b2ad0676f84b48a5ac8966a9 Mon Sep 17 00:00:00 2001 From: Qwann Date: Fri, 10 Mar 2017 16:40:36 +0100 Subject: [PATCH 013/161] permission working --- kfet/decorators.py | 4 ++++ kfet/models.py | 2 +- kfet/static/kfet/js/kfet_open.js | 38 ++++++++++++++++++++++++++------ kfet/views.py | 3 ++- 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/kfet/decorators.py b/kfet/decorators.py index 9af9247f..accfa143 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -9,4 +9,8 @@ from django_cas_ng.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(lambda u: kfet_is_team(u)) +force_close_required = user_passes_test(lambda u: can_force_close(u)) diff --git a/kfet/models.py b/kfet/models.py index b95755b7..928b7d88 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -594,7 +594,7 @@ class GlobalPermissions(models.Model): ('edit_balance_account', "Modifier la balance d'un compte"), ('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"), - ('can_close_kfet', "Peut fermer manuelement la K-Fêt"), + ('force_close_kfet', "Fermer manuelement la K-Fêt"), ) diff --git a/kfet/static/kfet/js/kfet_open.js b/kfet/static/kfet/js/kfet_open.js index 93374094..8ea0be70 100644 --- a/kfet/static/kfet/js/kfet_open.js +++ b/kfet/static/kfet/js/kfet_open.js @@ -17,13 +17,7 @@ function kfet_open(init_date, init_satus, init_force_close, force_close_url, for var force_close = init_force_close; // EVENT - force_close_button.click(function() { - if (force_close) { - $.get(force_open_url, function(data) {}); - } else { - $.get(force_close_url, function(data) {}); - } - }); + force_close_button.click(forceClose); // INITIALISAITION update_open(); @@ -36,6 +30,36 @@ function kfet_open(init_date, init_satus, init_force_close, force_close_url, for }, 30 * 1000); // 60 * 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); + }, + }) + .done(function() {}) + .fail(function($xhr) { + var data = $xhr.responseJSON; + switch ($xhr.status) { + case 403: + requestAuth({'errors':{}}, forceClose); + break; + case 400: + alert('lol'); + break; + } + lock = 0; + }); + } + function nb_min_diff() { var date_now = new Date(); // On calcule le nb de minutes depuis le dernier diff --git a/kfet/views.py b/kfet/views.py index 4278aa88..af633cd7 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -22,7 +22,7 @@ from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.decorators import method_decorator from gestioncof.models import CofProfile, Clipper -from kfet.decorators import teamkfet_required +from kfet.decorators import teamkfet_required, force_close_required from kfet.models import ( Account, Checkout, Article, Settings, AccountNegative, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, @@ -126,6 +126,7 @@ class UpdateForceClose(View): @method_decorator(login_required) @method_decorator(teamkfet_required) + @method_decorator(force_close_required) def dispatch(self, *args, **kwargs): return super(UpdateForceClose, self).dispatch(*args, **kwargs) From b0643c09288cdca6edd4c98c53d3884d13334fe1 Mon Sep 17 00:00:00 2001 From: Qwann Date: Sat, 11 Mar 2017 02:04:30 +0100 Subject: [PATCH 014/161] typo --- kfet/context_processors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/context_processors.py b/kfet/context_processors.py index 0d98c7e1..6c053ea9 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -17,7 +17,7 @@ def auth(request): return {} -def kfet_open(resquest): +def kfet_open(request): (kfet_open, kfet_open_date) = KFET_OPEN() kfet_force_close = KFET_FORCE_CLOSE() return { From 85caa6b0581b76b8fb6aa92fd01aaffbec9e9bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 3 Apr 2017 20:32:16 +0200 Subject: [PATCH 015/161] Use django-djconfig for kfet app. Old configuration(/settings), based on Settings model, system is deleted: SettingsForm, Settings. New system use `django-djconfig` module. - `kfet.config` module provides `kfet_config` to access configuration concerning kfet app. - Views, forms, models, etc now use this object to retrieve conf values. - Views no longer add config values to context, instead templates use `kfet_config` provided by a new context_processor. - Enhance list and update views of settings. - Fix: settings can directly be used without having to visit a specific page... Misc - Delete some py2/3 imports - Delete unused imports in kfet.models and kfet.views - Some PEP8 compliance --- cof/settings_dev.py | 4 + kfet/apps.py | 6 ++ kfet/config.py | 25 +++++ kfet/context_processors.py | 11 +- kfet/forms.py | 64 ++++++----- kfet/migrations/0051_delete_settings.py | 52 +++++++++ kfet/models.py | 124 +--------------------- kfet/templates/kfet/account_negative.html | 4 +- kfet/templates/kfet/account_read.html | 2 +- kfet/templates/kfet/history.html | 2 +- kfet/templates/kfet/settings.html | 52 ++++++--- kfet/templates/kfet/settings_update.html | 25 +++-- kfet/templatetags/kfet_tags.py | 5 +- kfet/urls.py | 2 +- kfet/views.py | 95 +++++++++-------- requirements.txt | 1 + 16 files changed, 247 insertions(+), 227 deletions(-) create mode 100644 kfet/config.py create mode 100644 kfet/migrations/0051_delete_settings.py diff --git a/cof/settings_dev.py b/cof/settings_dev.py index 18aadaad..b04165c8 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -47,6 +47,7 @@ INSTALLED_APPS = ( 'channels', 'widget_tweaks', 'custommail', + 'djconfig', ) MIDDLEWARE_CLASSES = ( @@ -60,6 +61,7 @@ MIDDLEWARE_CLASSES = ( 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', + 'djconfig.middleware.DjConfigMiddleware', ) ROOT_URLCONF = 'cof.urls' @@ -78,8 +80,10 @@ TEMPLATES = [ 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.static', + 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', 'kfet.context_processors.auth', + 'kfet.context_processors.config', ], }, }, diff --git a/kfet/apps.py b/kfet/apps.py index 29f9f98e..3dd2c0e8 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -12,3 +12,9 @@ class KFetConfig(AppConfig): def ready(self): import kfet.signals + self.register_config() + + def register_config(self): + import djconfig + from kfet.forms import KFetConfigForm + djconfig.register(KFetConfigForm) diff --git a/kfet/config.py b/kfet/config.py new file mode 100644 index 00000000..deb12504 --- /dev/null +++ b/kfet/config.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- + +from djconfig import config + + +class KFetConfig(object): + """kfet app configuration. + + Enhance dependency with backend used to store config. + Usable after DjConfig middleware was called. + + """ + prefix = 'kfet_' + + def __getattr__(self, key): + dj_key = '{}{}'.format(self.prefix, key) + return getattr(config, dj_key) + + def list(self): + from kfet.forms import KFetConfigForm + return [(field.label, getattr(config, name), ) + for name, field in KFetConfigForm.base_fields.items()] + + +kfet_config = KFetConfig() diff --git a/kfet/context_processors.py b/kfet/context_processors.py index ef4f2e64..4c7b4fe4 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * - from django.contrib.auth.context_processors import PermWrapper +from kfet.config import kfet_config + + def auth(request): if hasattr(request, 'real_user'): return { @@ -13,3 +12,7 @@ def auth(request): 'perms': PermWrapper(request.real_user), } return {} + + +def config(request): + return {'kfet_config': kfet_config} diff --git a/kfet/forms.py b/kfet/forms.py index 0fc02dd3..b6335fb8 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- +from datetime import timedelta from decimal import Decimal + from django import forms from django.core.exceptions import ValidationError from django.core.validators import MinLengthValidator @@ -8,12 +10,16 @@ from django.contrib.auth.models import User, Group, Permission from django.contrib.contenttypes.models import ContentType from django.forms import modelformset_factory from django.utils import timezone + +from djconfig.forms import ConfigForm + from kfet.models import ( Account, Checkout, Article, OperationGroup, Operation, - CheckoutStatement, ArticleCategory, Settings, AccountNegative, Transfer, + CheckoutStatement, ArticleCategory, AccountNegative, Transfer, TransferGroup, Supplier) from gestioncof.models import CofProfile + # ----- # Widgets # ----- @@ -379,40 +385,42 @@ class AddcostForm(forms.Form): self.cleaned_data['amount'] = 0 super(AddcostForm, self).clean() + # ----- # Settings forms # ----- -class SettingsForm(forms.ModelForm): - class Meta: - model = Settings - fields = ['value_decimal', 'value_account', 'value_duration'] - def clean(self): - name = self.instance.name - value_decimal = self.cleaned_data.get('value_decimal') - value_account = self.cleaned_data.get('value_account') - value_duration = self.cleaned_data.get('value_duration') +class KFetConfigForm(ConfigForm): - type_decimal = ['SUBVENTION_COF', 'ADDCOST_AMOUNT', 'OVERDRAFT_AMOUNT'] - type_account = ['ADDCOST_FOR'] - type_duration = ['OVERDRAFT_DURATION', 'CANCEL_DURATION'] + kfet_subvention_cof = forms.DecimalField( + label='Subvention COF', initial=Decimal('25'), + max_digits=6, decimal_places=2, + ) + kfet_addcost_amount = forms.DecimalField( + label='Montant de la majoration', initial=Decimal('0'), required=False, + max_digits=6, decimal_places=2, + ) + kfet_addcost_for = forms.ModelChoiceField( + label='Destinataire de la majoration', initial=None, required=False, + help_text='Laissez vide pour désactiver la majoration', + queryset=(Account.objects + .select_related('cofprofile', 'cofprofile__user') + .all()), + ) + kfet_overdraft_duration = forms.DurationField( + label='Durée du découvert autorisé par défaut', + initial=timedelta(days=1), + ) + kfet_overdraft_amount = forms.DecimalField( + label='Montant du découvert autorisé par défaut', initial=Decimal('20'), + max_digits=6, decimal_places=2, + ) + kfet_cancel_duration = forms.DurationField( + label='Durée pour annuler une commande sans mot de passe', + initial=timedelta(minutes=5), + ) - self.cleaned_data['name'] = name - if name in type_decimal: - if not value_decimal: - raise ValidationError('Renseignez une valeur décimale') - self.cleaned_data['value_account'] = None - self.cleaned_data['value_duration'] = None - elif name in type_account: - self.cleaned_data['value_decimal'] = None - self.cleaned_data['value_duration'] = None - elif name in type_duration: - if not value_duration: - raise ValidationError('Renseignez une durée') - self.cleaned_data['value_decimal'] = None - self.cleaned_data['value_account'] = None - super(SettingsForm, self).clean() class FilterHistoryForm(forms.Form): checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all()) diff --git a/kfet/migrations/0051_delete_settings.py b/kfet/migrations/0051_delete_settings.py new file mode 100644 index 00000000..5addad28 --- /dev/null +++ b/kfet/migrations/0051_delete_settings.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + +from kfet.forms import KFetConfigForm + + +def adapt_settings(apps, schema_editor): + Settings = apps.get_model('kfet', 'Settings') + db_alias = schema_editor.connection.alias + obj = Settings.objects.using(db_alias) + + cfg = {} + + def try_get(new, old, type_field): + try: + value = getattr(obj.get(name=old), type_field) + cfg[new] = value + except Settings.DoesNotExist: + pass + + try_get('kfet_subvention_cof', 'SUBVENTION_COF', 'value_decimal') + try_get('kfet_addcost_amount', 'ADDCOST_AMOUNT', 'value_decimal') + try_get('kfet_addcost_for', 'ADDCOST_FOR', 'value_account') + try_get('kfet_overdraft_duration', 'OVERDRAFT_DURATION', 'value_duration') + try_get('kfet_overdraft_amount', 'OVERDRAFT_AMOUNT', 'value_decimal') + try_get('kfet_cancel_duration', 'CANCEL_DURATION', 'value_duration') + + cfg_form = KFetConfigForm(initial=cfg) + if cfg_form.is_valid(): + cfg_form.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('kfet', '0050_remove_checkout'), + ('djconfig', '0001_initial'), + ] + + operations = [ + migrations.RunPython(adapt_settings), + migrations.RemoveField( + model_name='settings', + name='value_account', + ), + migrations.DeleteModel( + name='Settings', + ), + ] diff --git a/kfet/models.py b/kfet/models.py index c039ab06..de871f0c 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,12 +1,7 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * - from django.db import models from django.core.urlresolvers import reverse -from django.core.exceptions import PermissionDenied, ValidationError from django.core.validators import RegexValidator from django.contrib.auth.models import User from gestioncof.models import CofProfile @@ -15,11 +10,12 @@ from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.db import transaction from django.db.models import F -from django.core.cache import cache -from datetime import date, timedelta +from datetime import date import re import hashlib +from kfet.config import kfet_config + def choices_length(choices): return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) @@ -113,8 +109,8 @@ class Account(models.Model): return data def perms_to_perform_operation(self, amount): - overdraft_duration_max = Settings.OVERDRAFT_DURATION() - overdraft_amount_max = Settings.OVERDRAFT_AMOUNT() + overdraft_duration_max = kfet_config.overdraft_duration + overdraft_amount_max = kfet_config.overdraft_amount perms = set() stop_ope = False # Checking is cash account @@ -620,116 +616,6 @@ class GlobalPermissions(models.Model): ('special_add_account', "Créer un compte avec une balance initiale") ) -class Settings(models.Model): - name = models.CharField( - max_length = 45, - unique = True, - db_index = True) - value_decimal = models.DecimalField( - max_digits = 6, decimal_places = 2, - blank = True, null = True, default = None) - value_account = models.ForeignKey( - Account, on_delete = models.PROTECT, - blank = True, null = True, default = None) - value_duration = models.DurationField( - blank = True, null = True, default = None) - - @staticmethod - def setting_inst(name): - return Settings.objects.get(name=name) - - @staticmethod - def SUBVENTION_COF(): - subvention_cof = cache.get('SUBVENTION_COF') - if subvention_cof: - return subvention_cof - try: - subvention_cof = Settings.setting_inst("SUBVENTION_COF").value_decimal - except Settings.DoesNotExist: - subvention_cof = 0 - cache.set('SUBVENTION_COF', subvention_cof) - return subvention_cof - - @staticmethod - def ADDCOST_AMOUNT(): - try: - return Settings.setting_inst("ADDCOST_AMOUNT").value_decimal - except Settings.DoesNotExist: - return 0 - - @staticmethod - def ADDCOST_FOR(): - try: - return Settings.setting_inst("ADDCOST_FOR").value_account - except Settings.DoesNotExist: - return None; - - @staticmethod - def OVERDRAFT_DURATION(): - overdraft_duration = cache.get('OVERDRAFT_DURATION') - if overdraft_duration: - return overdraft_duration - try: - overdraft_duration = Settings.setting_inst("OVERDRAFT_DURATION").value_duration - except Settings.DoesNotExist: - overdraft_duration = timedelta() - cache.set('OVERDRAFT_DURATION', overdraft_duration) - return overdraft_duration - - @staticmethod - def OVERDRAFT_AMOUNT(): - overdraft_amount = cache.get('OVERDRAFT_AMOUNT') - if overdraft_amount: - return overdraft_amount - try: - overdraft_amount = Settings.setting_inst("OVERDRAFT_AMOUNT").value_decimal - except Settings.DoesNotExist: - overdraft_amount = 0 - cache.set('OVERDRAFT_AMOUNT', overdraft_amount) - return overdraft_amount - - @staticmethod - def CANCEL_DURATION(): - cancel_duration = cache.get('CANCEL_DURATION') - if cancel_duration: - return cancel_duration - try: - cancel_duration = Settings.setting_inst("CANCEL_DURATION").value_duration - except Settings.DoesNotExist: - cancel_duration = timedelta() - cache.set('CANCEL_DURATION', cancel_duration) - return cancel_duration - - @staticmethod - def create_missing(): - s, created = Settings.objects.get_or_create(name='SUBVENTION_COF') - if created: - s.value_decimal = 25 - s.save() - s, created = Settings.objects.get_or_create(name='ADDCOST_AMOUNT') - if created: - s.value_decimal = 0.5 - s.save() - s, created = Settings.objects.get_or_create(name='ADDCOST_FOR') - s, created = Settings.objects.get_or_create(name='OVERDRAFT_DURATION') - if created: - s.value_duration = timedelta(days=1) # 24h - s.save() - s, created = Settings.objects.get_or_create(name='OVERDRAFT_AMOUNT') - if created: - s.value_decimal = 20 - s.save() - s, created = Settings.objects.get_or_create(name='CANCEL_DURATION') - if created: - s.value_duration = timedelta(minutes=5) # 5min - s.save() - - @staticmethod - def empty_cache(): - cache.delete_many([ - 'SUBVENTION_COF', 'OVERDRAFT_DURATION', 'OVERDRAFT_AMOUNT', - 'CANCEL_DURATION', 'ADDCOST_AMOUNT', 'ADDCOST_FOR', - ]) class GenericTeamToken(models.Model): token = models.CharField(max_length = 50, unique = True) diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index 5f77b8f0..af4366ed 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -16,8 +16,8 @@
Découvert autorisé par défaut
-
Montant: {{ settings.overdraft_amount }}€
-
Pendant: {{ settings.overdraft_duration }}
+
Montant: {{ kfet_config.overdraft_amount }}€
+
Pendant: {{ kfet_config.overdraft_duration }}
{% if perms.kfet.change_settings %} diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 50ab7f20..5c627aad 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -95,7 +95,7 @@ - {% if account.user == request.user %} @@ -18,11 +17,11 @@ $(document).ready(function() { var stat_last = new StatsGroup( "{% url 'kfet.account.stat.operation.list' trigramme=account.trigramme %}", - $("#stat_last"), + $("#stat_last") ); var stat_balance = new StatsGroup( "{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}", - $("#stat_balance"), + $("#stat_balance") ); }); From 1e18c4043e4593849ebd982340ea26f04f02a363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 9 Apr 2017 15:47:16 +0200 Subject: [PATCH 057/161] Use last channels & co versions - Use last official releases of channels, asgiref, daphne and asgi-redis packages. - Customization of JsonWebsocketConsumer is now in kfet app through a custom class (and so, doesn't require anymore a forked version of channels). - Clean kfet consumers code. --- kfet/consumers.py | 27 +++++++++++---------------- requirements.txt | 8 ++++---- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/kfet/consumers.py b/kfet/consumers.py index dcd69bdf..6e9dc6ca 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -1,26 +1,21 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * +from django.core.serializers.json import json, DjangoJSONEncoder -from channels import Group from channels.generic.websockets import JsonWebsocketConsumer -class KPsul(JsonWebsocketConsumer): - # Set to True if you want them, else leave out - strict_ordering = False - slight_ordering = False +class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): + """Custom Json Websocket Consumer. - def connection_groups(self, **kwargs): - return ['kfet.kpsul'] + Encode to JSON with DjangoJSONEncoder. - def connect(self, message, **kwargs): - pass + """ - def receive(self, content, **kwargs): - pass + @classmethod + def encode_json(cls, content): + return json.dumps(content, cls=DjangoJSONEncoder) - def disconnect(self, message, **kwargs): - pass + +class KPsul(DjangoJsonWebsocketConsumer): + groups = ['kfet.kpsul'] diff --git a/requirements.txt b/requirements.txt index ce081588..990fba3a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,13 +11,13 @@ six==1.10.0 unicodecsv==0.14.1 icalendar==3.10 django-bootstrap-form==3.2.1 -asgiref==0.14.0 -daphne==0.14.3 -asgi-redis==0.14.0 +asgiref==1.1.1 +daphne==1.2.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 -git+https://github.com/Aureplop/channels.git#egg=channels +channels==1.1.3 python-dateutil From 029d59e615adba19c61f2cd2be72a6f2d71cc23a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 9 Apr 2017 16:10:27 +0200 Subject: [PATCH 058/161] Enable authentication on KPsul websocket. - PermConsumerMixin allows checking permissions on connection to a consumer. - KPsul consumer uses this mixin to check if connecting user has the permission `kfet.is_team`. Fixes #67. --- kfet/consumers.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/kfet/consumers.py b/kfet/consumers.py index 6e9dc6ca..ee096368 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -17,5 +17,25 @@ class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): return json.dumps(content, cls=DjangoJSONEncoder) -class KPsul(DjangoJsonWebsocketConsumer): +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): groups = ['kfet.kpsul'] + perms_connect = ['kfet.is_team'] From 8870b5ace2cad03e9d44e8a730ac6b37a32660f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 9 Apr 2017 17:37:15 +0200 Subject: [PATCH 059/161] Fewer queries on poll view --- gestioncof/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gestioncof/views.py b/gestioncof/views.py index 944d9dc2..457a99c4 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -94,7 +94,10 @@ def logout(request): @login_required def survey(request, survey_id): - survey = get_object_or_404(Survey, id=survey_id) + survey = get_object_or_404( + Survey.objects.prefetch_related('questions', 'questions__answers'), + id=survey_id, + ) if not survey.survey_open or survey.old: raise Http404 success = False From 3dc91e30bd798cdd6c18c928727f5b8da218ea9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 9 Apr 2017 17:51:40 +0200 Subject: [PATCH 060/161] Fewer requests on petit cours list management. --- gestioncof/petits_cours_views.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index 332e156c..ca0b55af 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -24,13 +24,14 @@ from gestioncof.shared import lock_table, unlock_tables class DemandeListView(ListView): - model = PetitCoursDemande + queryset = ( + PetitCoursDemande.objects + .prefetch_related('matieres') + .order_by('traitee', '-id') + ) template_name = "petits_cours_demandes_list.html" paginate_by = 20 - def get_queryset(self): - return PetitCoursDemande.objects.order_by('traitee', '-id').all() - class DemandeDetailView(DetailView): model = PetitCoursDemande From 6ce2f178bf29c4c87a14de858037e3b66b27d4bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sun, 9 Apr 2017 17:57:11 +0200 Subject: [PATCH 061/161] Fewer requests on petit cours details management. --- gestioncof/petits_cours_views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/gestioncof/petits_cours_views.py b/gestioncof/petits_cours_views.py index ca0b55af..3fa0dc57 100644 --- a/gestioncof/petits_cours_views.py +++ b/gestioncof/petits_cours_views.py @@ -35,6 +35,11 @@ class DemandeListView(ListView): class DemandeDetailView(DetailView): model = PetitCoursDemande + queryset = ( + PetitCoursDemande.objects + .prefetch_related('petitcoursattribution_set', + 'matieres') + ) template_name = "gestioncof/details_demande_petit_cours.html" context_object_name = "demande" From 15873085e11425d0cc1ca44ef3fd65c1091bfb8b Mon Sep 17 00:00:00 2001 From: Qwann Date: Sun, 9 Apr 2017 20:01:52 +0200 Subject: [PATCH 062/161] small fixes --- kfet/consumers.py | 5 ----- kfet/decorators.py | 2 -- kfet/static/kfet/js/kfet_open.js | 16 ++++++---------- kfet/urls.py | 5 +++-- kfet/views.py | 13 +++---------- 5 files changed, 12 insertions(+), 29 deletions(-) diff --git a/kfet/consumers.py b/kfet/consumers.py index 72e3b21e..8a0df05f 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -26,11 +26,6 @@ class KPsul(JsonWebsocketConsumer): pass class KfetOpen(JsonWebsocketConsumer): - - # Set to True if you want them, else leave out - strict_ordering = False - slight_ordering = False - def connection_groups(self, **kwargs): return ['kfet.is_open'] diff --git a/kfet/decorators.py b/kfet/decorators.py index 592bf566..3dc76767 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -9,6 +9,4 @@ def kfet_is_team(user): def can_force_close(user): return user.has_perm('force_close_kfet') -teamkfet_required = user_passes_test(lambda u: kfet_is_team(u)) -force_close_required = user_passes_test(lambda u: can_force_close(u)) teamkfet_required = user_passes_test(kfet_is_team) diff --git a/kfet/static/kfet/js/kfet_open.js b/kfet/static/kfet/js/kfet_open.js index 8ea0be70..e2cccf81 100644 --- a/kfet/static/kfet/js/kfet_open.js +++ b/kfet/static/kfet/js/kfet_open.js @@ -1,4 +1,4 @@ -function kfet_open(init_date, init_satus, init_force_close, force_close_url, force_open_url) { +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'); @@ -19,15 +19,15 @@ function kfet_open(init_date, init_satus, init_force_close, force_close_url, for // EVENT force_close_button.click(forceClose); - // INITIALISAITION + // INITIALISATION update_open(); update_force_button(); - // On recherge toute les 30sec - // (dans le cas où le statut deviendrait inconn) + // On recharge toute les 30sec + // (dans le cas où le statut deviendrait inconnu) setInterval(function() { update_open(); - }, 30 * 1000); // 60 * 1000 milsec + }, 30 * 1000); // 30 * 1000 milsec // FONCTIONS function forceClose(password = '') { @@ -45,16 +45,12 @@ function kfet_open(init_date, init_satus, init_force_close, force_close_url, for $xhr.setRequestHeader("KFetPassword", password); }, }) - .done(function() {}) .fail(function($xhr) { var data = $xhr.responseJSON; switch ($xhr.status) { case 403: requestAuth({'errors':{}}, forceClose); break; - case 400: - alert('lol'); - break; } lock = 0; }); @@ -89,7 +85,7 @@ function kfet_open(init_date, init_satus, init_force_close, force_close_url, for open_status.html("?????"); } function update_open() { - nb_min = nb_min_diff(); + var nb_min = nb_min_diff(); 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) { diff --git a/kfet/urls.py b/kfet/urls.py index 718eac40..7b47d8c9 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -205,8 +205,9 @@ urlpatterns = [ url('^kfet_open/$', views.UpdateKfetOpen.as_view(), name='kfet.kfet_open'), - url('^force_close/$', - views.UpdateForceClose.as_view(), + url('^kfet_close/$', + permission_required('kfet.can_force_close') + (views.UpdateForceClose.as_view()), name='kfet.force_close'), # ----- diff --git a/kfet/views.py b/kfet/views.py index 51948de1..7865ec34 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -7,9 +7,8 @@ from django.shortcuts import render, get_object_or_404, redirect from django.core.exceptions import PermissionDenied from django.core.cache import cache from django.views.generic import ListView, DetailView, TemplateView, View -from django.views.generic.list import BaseListView, MultipleObjectTemplateResponseMixin -from django.views.generic.detail import BaseDetailView, SingleObjectTemplateResponseMixin -from django.views.generic.edit import CreateView, UpdateView, DeleteView, FormView +from django.views.generic.detail import BaseDetailView +from django.views.generic.edit import CreateView, UpdateView from django.core.urlresolvers import reverse, reverse_lazy from django.contrib import messages from django.contrib.messages.views import SuccessMessageMixin @@ -25,7 +24,7 @@ from django.utils import timezone from django.utils.crypto import get_random_string from django.utils.decorators import method_decorator from gestioncof.models import CofProfile -from kfet.decorators import teamkfet_required, force_close_required +from kfet.decorators import teamkfet_required from kfet.models import ( Account, Checkout, Article, Settings, AccountNegative, CheckoutStatement, GenericTeamToken, Supplier, SupplierArticle, Inventory, @@ -125,12 +124,6 @@ class UpdateForceClose(View): return HttpResponse("closed : %r at %s" % (force_close_get, time.isoformat())) - @method_decorator(login_required) - @method_decorator(teamkfet_required) - @method_decorator(force_close_required) - def dispatch(self, *args, **kwargs): - return super(UpdateForceClose, self).dispatch(*args, **kwargs) - @teamkfet_required def login_genericteam(request): From 5c6a73c59733dca6e0953b181ba00ab02500967e Mon Sep 17 00:00:00 2001 From: Qwann Date: Sun, 9 Apr 2017 20:54:30 +0200 Subject: [PATCH 063/161] kfet_open uses moment.js --- kfet/context_processors.py | 2 +- kfet/static/kfet/js/kfet_open.js | 18 ++++-------------- kfet/templates/kfet/base.html | 6 +++++- kfet/templates/kfet/kpsul.html | 3 --- kfet/views.py | 2 +- 5 files changed, 11 insertions(+), 20 deletions(-) diff --git a/kfet/context_processors.py b/kfet/context_processors.py index 6c053ea9..9364c724 100644 --- a/kfet/context_processors.py +++ b/kfet/context_processors.py @@ -22,6 +22,6 @@ def kfet_open(request): kfet_force_close = KFET_FORCE_CLOSE() return { 'kfet_open': kfet_open, - 'kfet_open_date': kfet_open_date, + 'kfet_open_date': kfet_open_date.isoformat(), 'kfet_force_close': kfet_force_close, } diff --git a/kfet/static/kfet/js/kfet_open.js b/kfet/static/kfet/js/kfet_open.js index e2cccf81..a8f41757 100644 --- a/kfet/static/kfet/js/kfet_open.js +++ b/kfet/static/kfet/js/kfet_open.js @@ -56,18 +56,8 @@ function kfet_open(init_date, init_status, init_force_close, force_close_url, fo }); } - function nb_min_diff() { - var date_now = new Date(); - // On calcule le nb de minutes depuis le dernier - // envoi d'information - tmp = date_now - kfet_open_date; - - tmp = Math.floor(tmp/1000); // Nombre de secondes entre les 2 dates - diff_sec = tmp % 60; // Extraction du nombre de secondes - - tmp = Math.floor((tmp-diff_sec)/60); // Nombre de minutes (partie entière) - - return tmp; + function kfet_open_min() { + return moment().diff(kfet_open_date, 'minute') } function do_kfet_close() { kfet_open_bullet.css({'background-color': closed_color}); @@ -85,7 +75,7 @@ function kfet_open(init_date, init_status, init_force_close, force_close_url, fo open_status.html("?????"); } function update_open() { - var nb_min = nb_min_diff(); + 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) { @@ -121,7 +111,7 @@ function kfet_open(init_date, init_status, init_force_close, force_close_url, fo if (data['door_action']) { console.log("* Message reçu de la part de la porte."); - kfet_open_date = new Date(data['door_action']['kfet_open_date']); + kfet_open_date = moment.utc(data['door_action']['kfet_open_date']); kfet_open = data['door_action']['kfet_open']; update_open(); diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index 81ca3dfd..d992b209 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -21,13 +21,17 @@ + + + + {# K-Fêt open #} - - - {% endblock %} diff --git a/kfet/views.py b/kfet/views.py index 7865ec34..0a3b6f99 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -97,7 +97,7 @@ class UpdateKfetOpen(View): websocket_data = { 'door_action': { 'kfet_open': is_open, - 'kfet_open_date': timezone.now(), + 'kfet_open_date': timezone.now().isoformat(), }, } consumers.KfetOpen.group_send('kfet.is_open', websocket_data) From c228416809149c7ed39fd42f9d3309b66a127167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 10 Apr 2017 11:36:06 +0200 Subject: [PATCH 064/161] =?UTF-8?q?Subvention=20->=20R=C3=A9duction=20+=20?= =?UTF-8?q?units=20for=20kfet=5Fconfig?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kfet_config gives "reduction_cof" as editable instead of "subvention_cof" - this last one can still be accessed via kfet_config (computed from new "reduction_cof" - add units to numeric values of kfet_config form --- kfet/config.py | 5 +++++ kfet/forms.py | 14 +++++++++----- kfet/migrations/0054_delete_settings.py | 8 +++++++- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/kfet/config.py b/kfet/config.py index 5023e8b0..76da5a79 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -16,6 +16,11 @@ class KFetConfig(object): prefix = 'kfet_' def __getattr__(self, key): + if key == 'subvention_cof': + # Allows accessing to the reduction as a subvention + # Other reason: backward compatibility + reduction_mult = 1 - self.reduction_cof/100 + return (1/reduction_mult - 1) * 100 return getattr(config, self._get_dj_key(key)) def list(self): diff --git a/kfet/forms.py b/kfet/forms.py index 9b098b75..f89b8f08 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -403,17 +403,20 @@ class AddcostForm(forms.Form): class KFetConfigForm(ConfigForm): - kfet_subvention_cof = forms.DecimalField( - label='Subvention COF', initial=Decimal('25'), + kfet_reduction_cof = forms.DecimalField( + label='Réduction COF', initial=Decimal('20'), max_digits=6, decimal_places=2, + help_text="Réduction, à donner en pourcentage, appliquée lors d'un " + "achat par un-e membre du COF sur le montant en euros.", ) kfet_addcost_amount = forms.DecimalField( - label='Montant de la majoration', initial=Decimal('0'), required=False, + label='Montant de la majoration (en €)', initial=Decimal('0'), + required=False, max_digits=6, decimal_places=2, ) kfet_addcost_for = forms.ModelChoiceField( label='Destinataire de la majoration', initial=None, required=False, - help_text='Laissez vide pour désactiver la majoration', + help_text='Laissez vide pour désactiver la majoration.', queryset=(Account.objects .select_related('cofprofile', 'cofprofile__user') .all()), @@ -423,7 +426,8 @@ class KFetConfigForm(ConfigForm): initial=timedelta(days=1), ) kfet_overdraft_amount = forms.DecimalField( - label='Montant du découvert autorisé par défaut', initial=Decimal('20'), + label='Montant du découvert autorisé par défaut (en €)', + initial=Decimal('20'), max_digits=6, decimal_places=2, ) kfet_cancel_duration = forms.DurationField( diff --git a/kfet/migrations/0054_delete_settings.py b/kfet/migrations/0054_delete_settings.py index 7a0b1ab8..80ee1d24 100644 --- a/kfet/migrations/0054_delete_settings.py +++ b/kfet/migrations/0054_delete_settings.py @@ -21,7 +21,13 @@ def adapt_settings(apps, schema_editor): except Settings.DoesNotExist: pass - try_get('kfet_subvention_cof', 'SUBVENTION_COF', 'value_decimal') + try: + subvention = obj.get(name='SUBVENTION_COF').value_decimal + subvention_mult = 1 + subvention/100 + reduction = (1 - 1/subvention_mult) * 100 + cfg['kfet_reduction_cof'] = reduction + except Settings.DoesNotExist: + pass try_get('kfet_addcost_amount', 'ADDCOST_AMOUNT', 'value_decimal') try_get('kfet_addcost_for', 'ADDCOST_FOR', 'value_account') try_get('kfet_overdraft_duration', 'OVERDRAFT_DURATION', 'value_duration') From 5d6012b6bddf44189a1709c85120698117146f17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 10 Apr 2017 11:52:57 +0200 Subject: [PATCH 065/161] Fix kfet tests - and add test for `kfet_config.subvention_cof` --- kfet/tests/test_config.py | 13 ++++++++++--- kfet/{tests.py => tests/test_statistic.py} | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) rename kfet/{tests.py => tests/test_statistic.py} (97%) diff --git a/kfet/tests/test_config.py b/kfet/tests/test_config.py index 79781c0d..03c9cf3c 100644 --- a/kfet/tests/test_config.py +++ b/kfet/tests/test_config.py @@ -22,13 +22,20 @@ class ConfigTest(TestCase): def test_get(self): self.assertTrue(hasattr(kfet_config, 'subvention_cof')) + def test_subvention_cof(self): + reduction_cof = Decimal('20') + subvention_cof = Decimal('25') + kfet_config.set(reduction_cof=reduction_cof) + + self.assertEqual(kfet_config.subvention_cof, subvention_cof) + def test_set_decimal(self): """Test field of decimal type.""" - subvention_cof = Decimal('10') + reduction_cof = Decimal('10') # IUT - kfet_config.set(subvention_cof=subvention_cof) + kfet_config.set(reduction_cof=reduction_cof) # check - self.assertEqual(kfet_config.subvention_cof, subvention_cof) + self.assertEqual(kfet_config.reduction_cof, reduction_cof) def test_set_modelinstance(self): """Test field of model instance type.""" diff --git a/kfet/tests.py b/kfet/tests/test_statistic.py similarity index 97% rename from kfet/tests.py rename to kfet/tests/test_statistic.py index 991b2545..4fb0785d 100644 --- a/kfet/tests.py +++ b/kfet/tests/test_statistic.py @@ -5,7 +5,7 @@ from unittest.mock import patch from django.test import TestCase, Client from django.contrib.auth.models import User, Permission -from .models import Account, Article, ArticleCategory +from kfet.models import Account, Article, ArticleCategory class TestStats(TestCase): From cb9ba76a4f7bde681c92f5f030e945d8b359d652 Mon Sep 17 00:00:00 2001 From: Qwann Date: Mon, 10 Apr 2017 16:47:13 +0200 Subject: [PATCH 066/161] small fixes --- kfet/static/kfet/js/kfet_open.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/static/kfet/js/kfet_open.js b/kfet/static/kfet/js/kfet_open.js index a8f41757..b6886c7b 100644 --- a/kfet/static/kfet/js/kfet_open.js +++ b/kfet/static/kfet/js/kfet_open.js @@ -57,7 +57,7 @@ function kfet_open(init_date, init_status, init_force_close, force_close_url, fo } function kfet_open_min() { - return moment().diff(kfet_open_date, 'minute') + return moment().diff(kfet_open_date, 'minute'); } function do_kfet_close() { kfet_open_bullet.css({'background-color': closed_color}); From e0b0a531125f82378a7aa0182a70d5ccfc4a5b3b Mon Sep 17 00:00:00 2001 From: Qwann Date: Mon, 10 Apr 2017 17:18:43 +0200 Subject: [PATCH 067/161] stupidness removed --- kfet/views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 0a3b6f99..dd4fb29a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -68,20 +68,20 @@ class Home(TemplateView): def KFET_OPEN(): - kfet_open_date = cache.get('KFET_OPEN_DATE') - kfet_open = cache.get('KFET_OPEN') - if not kfet_open_date: + kfet_open_date = cache.get('KFET_OPEN_DATE', None) + kfet_open = cache.get('KFET_OPEN', None) + if kfet_open_date is None: kfet_open_date = timezone.now() cache.set('KFET_OPEN_DATE', kfet_open_date) - if not kfet_open: + if kfet_open is None: kfet_open = False cache.set('KFET_OPEN', kfet_open) return (kfet_open, kfet_open_date) def KFET_FORCE_CLOSE(): - kfet_force_close = cache.get('KFET_FORCE_CLOSE') - if not kfet_force_close: + kfet_force_close = cache.get('KFET_FORCE_CLOSE', None) + if kfet_force_close is None: kfet_force_close = False cache.set('KFET_FORCE_CLOSE', kfet_force_close) return kfet_force_close From be8d249ed7669cace82650856b731b3c8535113a Mon Sep 17 00:00:00 2001 From: Qwann Date: Mon, 10 Apr 2017 17:47:39 +0200 Subject: [PATCH 068/161] remove useless code --- kfet/static/kfet/js/kfet_open.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/kfet/static/kfet/js/kfet_open.js b/kfet/static/kfet/js/kfet_open.js index b6886c7b..08a030a5 100644 --- a/kfet/static/kfet/js/kfet_open.js +++ b/kfet/static/kfet/js/kfet_open.js @@ -98,7 +98,6 @@ function kfet_open(init_date, init_status, init_force_close, force_close_url, fo } } // SYNCHRONIZATION - websocket_msg_default = {'last_op': 0} var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; var location_host = window.location.host; @@ -106,7 +105,7 @@ function kfet_open(init_date, init_status, init_force_close, force_close_url, fo var socket = new ReconnectingWebSocket(websocket_protocol+"://" + location_url + "/ws/k-fet/is_open/"); socket.onmessage = function(e) { - var data = $.extend({}, websocket_msg_default, JSON.parse(e.data)); + var data = JSON.parse(e.data); if (data['door_action']) { console.log("* Message reçu de la part de la porte."); From 2c408389389a701f9e2e15ae1c23f65108d4a942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 10 Apr 2017 18:58:52 +0200 Subject: [PATCH 069/161] Add real cache support - Fix cache per process issue with a real cache system - Configuration seems too easy... but it seems to work --- provisioning/bootstrap.sh | 2 +- requirements.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 269e4f25..e94d3ac4 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -9,7 +9,7 @@ DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" # Installation de paquets utiles apt-get update && apt-get install -y python3-pip python3-dev python3-venv \ - libmysqlclient-dev libjpeg-dev git redis-server + libmysqlclient-dev libjpeg-dev git redis-server memcached pip install -U pip # Configuration et installation de mysql. Le mot de passe root est le même que diff --git a/requirements.txt b/requirements.txt index ce081588..202f5dda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,3 +21,4 @@ git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_customma ldap3 git+https://github.com/Aureplop/channels.git#egg=channels python-dateutil +python-memcached From ab31c20649601514a24411328021daf6a636a46b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 10 Apr 2017 19:07:19 +0200 Subject: [PATCH 070/161] missing CACHES value... --- cof/settings_dev.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cof/settings_dev.py b/cof/settings_dev.py index 18aadaad..3bc4b807 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -115,6 +115,15 @@ USE_L10N = True USE_TZ = True +# Cache system +CACHES = { + 'default': { + 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', + 'LOCATION': '127.0.0.1:11211', + } +} + + # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ From 36771c2c4f6a3c31c0d71e26ce5f2adda8445017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 10 Apr 2017 21:36:00 +0200 Subject: [PATCH 071/161] Use redis for cache. - Cache use db #1 of redis. - Channel layer (of channels) use db #0 of redis. - `settings` try getting redis connection variables from environment. - Drop memcached system --- cof/settings_dev.py | 39 ++++++++++++++++++++++++++++----------- provisioning/bootstrap.sh | 2 +- requirements.txt | 2 +- 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/cof/settings_dev.py b/cof/settings_dev.py index 3bc4b807..97b4bf7c 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -115,15 +115,6 @@ USE_L10N = True USE_TZ = True -# Cache system -CACHES = { - 'default': { - 'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache', - 'LOCATION': '127.0.0.1:11211', - } -} - - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.8/howto/static-files/ @@ -176,13 +167,39 @@ RECAPTCHA_PUBLIC_KEY = "DUMMY" RECAPTCHA_PRIVATE_KEY = "DUMMY" RECAPTCHA_USE_SSL = True -# Channels settings + +# Redis settings (for cache and channel layers) + +REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") +REDIS_USER = os.environ.get("REDIS_USER", "") +REDIS_PASS = os.environ.get("REDIS_PASS", "") +REDIS_PORT = os.environ.get("REDIS_PORT", "6379") + +REDIS_AUTH = REDIS_USER+":"+REDIS_PASS+"@" if REDIS_USER or REDIS_PASS else '' +REDIS_BASE_URL = "redis://" + REDIS_AUTH + REDIS_HOST + ":" + REDIS_PORT + + +# Cache settings (use db #1 of redis) + +CACHE_REDIS_URL = REDIS_BASE_URL + "/1" + +CACHES = { + 'default': { + 'BACKEND': 'redis_cache.RedisCache', + 'LOCATION': CACHE_REDIS_URL, + } +} + + +# Channels settings (use db #0 of redis) + +CHANNEL_REDIS_URL = REDIS_BASE_URL + "/0" CHANNEL_LAYERS = { "default": { "BACKEND": "asgi_redis.RedisChannelLayer", "CONFIG": { - "hosts": [(os.environ.get("REDIS_HOST", "localhost"), 6379)], + "hosts": [CHANNEL_REDIS_URL], }, "ROUTING": "cof.routing.channel_routing", } diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index e94d3ac4..269e4f25 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -9,7 +9,7 @@ DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" # Installation de paquets utiles apt-get update && apt-get install -y python3-pip python3-dev python3-venv \ - libmysqlclient-dev libjpeg-dev git redis-server memcached + libmysqlclient-dev libjpeg-dev git redis-server pip install -U pip # Configuration et installation de mysql. Le mot de passe root est le même que diff --git a/requirements.txt b/requirements.txt index 202f5dda..2012351a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ django-autoslug==1.9.3 django-cas-ng==3.5.5 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 @@ -21,4 +22,3 @@ git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_customma ldap3 git+https://github.com/Aureplop/channels.git#egg=channels python-dateutil -python-memcached From a5fb162aaf412870e86390ca49dd059673308695 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 10 Apr 2017 22:18:00 +0100 Subject: [PATCH 072/161] New organisation of settings files We reproduce what has been done here: https://github.com/dissemin/dissemin The following files can be found under `cof/settings/` - `common.py`: the settings that are shared by all the environments we have + the secrets (see below). - `dev.py`: the settings used by the vagrant VM for local development. - `prod.py`: the production settings (for both www.cof.ens.fr and dev.cof.ens.fr) There is also a notion of "secrets". Some settings like the `SECRET_KEY` or the database's credentials are loaded from an untracked files called `secret.py` in the same directory. This secrets are loaded by the common settings file. --- cof/settings/.gitignore | 1 + cof/{settings_dev.py => settings/common.py} | 93 +++++++-------------- cof/settings/dev.py | 52 ++++++++++++ cof/settings/prod.py | 26 ++++++ cof/settings/secret_example.py | 4 + 5 files changed, 113 insertions(+), 63 deletions(-) create mode 100644 cof/settings/.gitignore rename cof/{settings_dev.py => settings/common.py} (63%) create mode 100644 cof/settings/dev.py create mode 100644 cof/settings/prod.py create mode 100644 cof/settings/secret_example.py diff --git a/cof/settings/.gitignore b/cof/settings/.gitignore new file mode 100644 index 00000000..21425062 --- /dev/null +++ b/cof/settings/.gitignore @@ -0,0 +1 @@ +secret.py diff --git a/cof/settings_dev.py b/cof/settings/common.py similarity index 63% rename from cof/settings_dev.py rename to cof/settings/common.py index b04165c8..4a6f9a12 100644 --- a/cof/settings_dev.py +++ b/cof/settings/common.py @@ -1,32 +1,40 @@ # -*- coding: utf-8 -*- """ -Django settings for cof project. +Django common settings for cof project. -For more information on this file, see -https://docs.djangoproject.com/en/1.8/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/1.8/ref/settings/ +Everything which is supposed to be identical between the production server and +the local development serveur should be here. """ -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +# Database credentials +try: + from .secret import DBNAME, DBUSER, DBPASSWD +except ImportError: + # On the local development VM, theses credentials are in the environment + DBNAME = os.environ["DBNAME"] + DBUSER = os.environ["DBUSER"] + DBPASSWD = os.environ["DBPASSWD"] +except KeyError: + raise RuntimeError("Secrets missing") -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' +# Other secrets +try: + from .secret import ( + SECRET_KEY, RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY, ADMINS + ) +except ImportError: + raise RuntimeError("Secrets missing") -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' +BASE_DIR = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +) -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True # Application definition -INSTALLED_APPS = ( +INSTALLED_APPS = [ 'gestioncof', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -41,17 +49,15 @@ INSTALLED_APPS = ( 'autocomplete_light', 'captcha', 'django_cas_ng', - 'debug_toolbar', 'bootstrapform', 'kfet', 'channels', 'widget_tweaks', 'custommail', 'djconfig', -) +] -MIDDLEWARE_CLASSES = ( - 'debug_toolbar.middleware.DebugToolbarMiddleware', +MIDDLEWARE_CLASSES = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', @@ -62,7 +68,7 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'djconfig.middleware.DjConfigMiddleware', -) +] ROOT_URLCONF = 'cof.urls' @@ -73,7 +79,6 @@ TEMPLATES = [ 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ - 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', @@ -89,17 +94,12 @@ TEMPLATES = [ }, ] -# WSGI_APPLICATION = 'cof.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/1.8/ref/settings/#databases - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', - 'NAME': os.environ['DBNAME'], - 'USER': os.environ['DBUSER'], - 'PASSWORD': os.environ['DBPASSWD'], + 'NAME': DBNAME, + 'USER': DBUSER, + 'PASSWORD': DBPASSWD, 'HOST': os.environ.get('DBHOST', 'localhost'), } } @@ -119,18 +119,9 @@ USE_L10N = True USE_TZ = True -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/1.8/howto/static-files/ - -STATIC_URL = '/static/' -STATIC_ROOT = '/var/www/static/' - # Media upload (through ImageField, SiteField) # https://docs.djangoproject.com/en/1.9/ref/models/fields/ -MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') -MEDIA_URL = '/media/' - # Various additional settings SITE_ID = 1 @@ -163,12 +154,6 @@ AUTHENTICATION_BACKENDS = ( 'kfet.backends.GenericTeamBackend', ) -# LDAP_SERVER_URL = 'ldaps://ldap.spi.ens.fr:636' - -# EMAIL_HOST="nef.ens.fr" - -RECAPTCHA_PUBLIC_KEY = "DUMMY" -RECAPTCHA_PRIVATE_KEY = "DUMMY" RECAPTCHA_USE_SSL = True # Channels settings @@ -183,22 +168,4 @@ CHANNEL_LAYERS = { } } - -def show_toolbar(request): - """ - On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar - car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la - machine physique n'est pas forcément connue, et peut difficilement être - mise dans les INTERNAL_IPS. - """ - if not DEBUG: - return False - if request.is_ajax(): - return False - return True - -DEBUG_TOOLBAR_CONFIG = { - 'SHOW_TOOLBAR_CALLBACK': show_toolbar, -} - FORMAT_MODULE_PATH = 'cof.locale' diff --git a/cof/settings/dev.py b/cof/settings/dev.py new file mode 100644 index 00000000..8f5c9f29 --- /dev/null +++ b/cof/settings/dev.py @@ -0,0 +1,52 @@ +""" +Django development settings for the cof project. +The settings that are not listed here are imported from .common +""" + +import os + +from .common import * + + +EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' + +DEBUG = True + +TEMPLATES[0]["OPTIONS"]["context_processors"] += [ + 'django.template.context_processors.debug' +] + + +# --- +# Apache static/media config +# --- + +STATIC_URL = '/static/' +STATIC_ROOT = '/var/www/static/' + +MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') +MEDIA_URL = '/media/' + + +# --- +# Debug tool bar +# --- + +def show_toolbar(request): + """ + On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar + car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la + machine physique n'est pas forcément connue, et peut difficilement être + mise dans les INTERNAL_IPS. + """ + if not DEBUG: + return False + if request.is_ajax(): + return False + return True + +INSTALLED_APPS += ["debug_toolbar"] +MIDDLEWARE_CLASSES += ["debug_toolbar.middleware.DebugToolbarMiddleware"] +DEBUG_TOOLBAR_CONFIG = { + 'SHOW_TOOLBAR_CALLBACK': show_toolbar, +} diff --git a/cof/settings/prod.py b/cof/settings/prod.py new file mode 100644 index 00000000..5fae5651 --- /dev/null +++ b/cof/settings/prod.py @@ -0,0 +1,26 @@ +""" +Django development settings for the cof project. +The settings that are not listed here are imported from .common +""" + +import os + +from .common import * + + +DEBUG = False + +ALLOWED_HOSTS = [ + "cof.ens.fr", + "www.cof.ens.fr", + "dev.cof.ens.fr" +] + +STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static") +STATIC_URL = "/gestion/static/" +MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") +MEDIA_URL = "/gestion/media/" + +LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636" + +EMAIL_HOST = "nef.ens.fr" diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py new file mode 100644 index 00000000..36a8e932 --- /dev/null +++ b/cof/settings/secret_example.py @@ -0,0 +1,4 @@ +SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' +RECAPTCHA_PUBLIC_KEY = "DUMMY" +RECAPTCHA_PRIVATE_KEY = "DUMMY" +ADMINS = None From 40abe81402649b326a4c8334290ae5177e079f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 10 Apr 2017 22:44:52 +0100 Subject: [PATCH 073/161] Integrate the new settings workflow into vagrant --- provisioning/bootstrap.sh | 7 +++++-- provisioning/cron.dev | 2 +- provisioning/supervisor.conf | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 269e4f25..c8f73ab6 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -36,7 +36,7 @@ chown -R ubuntu:www-data /var/www/static # Mise en place du .bash_profile pour tout configurer lors du `vagrant ssh` cat >> ~ubuntu/.bashrc < Date: Tue, 11 Apr 2017 23:13:54 +0200 Subject: [PATCH 074/161] Fewer queries on stats of an account balance. - Remove labels, should be replaced to an anchor to the relative operation in history. - Add select_related as necessary. --- kfet/views.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 2c4a4f73..cfc58aa0 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2219,10 +2219,13 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): # prepare querysets # TODO: retirer les opgroup dont tous les op sont annulées opegroups = OperationGroup.objects.filter(on_acc=account) - recv_transfers = Transfer.objects.filter(to_acc=account, - canceled_at=None) - sent_transfers = Transfer.objects.filter(from_acc=account, - canceled_at=None) + transfers = ( + Transfer.objects + .filter(canceled_at=None) + .select_related('group') + ) + recv_transfers = transfers.filter(to_acc=account) + sent_transfers = transfers.filter(from_acc=account) # apply filters if begin_date is not None: @@ -2250,13 +2253,11 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): actions.append({ 'at': (begin_date or account.created_at).isoformat(), 'amount': 0, - 'label': 'début', 'balance': 0, }) actions.append({ 'at': (end_date or timezone.now()).isoformat(), 'amount': 0, - 'label': 'fin', 'balance': 0, }) @@ -2264,21 +2265,18 @@ class AccountStatBalance(PkUrlMixin, JSONDetailView): { 'at': ope_grp.at.isoformat(), 'amount': ope_grp.amount, - 'label': str(ope_grp), 'balance': 0, } for ope_grp in opegroups ] + [ { 'at': tr.group.at.isoformat(), 'amount': tr.amount, - 'label': str(tr), 'balance': 0, } for tr in recv_transfers ] + [ { 'at': tr.group.at.isoformat(), 'amount': -tr.amount, - 'label': str(tr), 'balance': 0, } for tr in sent_transfers ] From 3f4a1adbb9cd083731b3ad340c47dae51c461586 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Wed, 12 Apr 2017 18:03:31 +0200 Subject: [PATCH 075/161] Fewer queries on stats/scales + Fix Scales: - Fix #chunks when used with std_chunk=True (there was one too many at the beginning) - Scale.end gives the end of the last chunk (instead of its start) So scale.begin -> scale.end gives the full range of the scale. `kfet_day` now returns an aware datetime. ScaleMixin: - new method `get_by_chunks` which use only one query and ranks elements according to the scale. Elements are returned by a generator for each scale chunk (and all chunks are returned as a generator too). ArticlesStatSales and AccountStatOperations use this new method to avoid issuing #scale_chunks queries. ArticleStat: - fixed on Chrome --- kfet/static/kfet/js/statistic.js | 4 +- kfet/statistic.py | 103 ++++++++++++++++++++++++-- kfet/templates/kfet/article_read.html | 2 +- kfet/views.py | 63 ++++++++++------ 4 files changed, 140 insertions(+), 32 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index f210c11d..db31e0e8 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -61,7 +61,7 @@ var chart = charts[i]; // format the data - var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 1); + var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 0); chart_datasets.push( { @@ -132,7 +132,7 @@ type: 'line', options: chart_options, data: { - labels: (data.labels || []).slice(1), + labels: data.labels || [], datasets: chart_datasets, } }; diff --git a/kfet/statistic.py b/kfet/statistic.py index fe948f73..5ff169ff 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -4,6 +4,7 @@ from datetime import date, datetime, time, timedelta from dateutil.relativedelta import relativedelta from dateutil.parser import parse as dateutil_parse +import pytz from django.utils import timezone from django.db.models import Sum @@ -13,7 +14,8 @@ KFET_WAKES_UP_AT = time(7, 0) def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): """datetime wrapper with time offset.""" - return datetime.combine(date(year, month, day), start_at) + naive = datetime.combine(date(year, month, day), start_at) + return pytz.timezone('Europe/Paris').localize(naive, is_dst=None) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): @@ -32,16 +34,21 @@ class Scale(object): self.std_chunk = std_chunk if last: end = timezone.now() + if std_chunk: + if begin is not None: + begin = self.get_chunk_start(begin) + if end is not None: + end = self.do_step(self.get_chunk_start(end)) if begin is not None and n_steps != 0: - self.begin = self.get_from(begin) + self.begin = begin self.end = self.do_step(self.begin, n_steps=n_steps) elif end is not None and n_steps != 0: - self.end = self.get_from(end) + self.end = end self.begin = self.do_step(self.end, n_steps=-n_steps) elif begin is not None and end is not None: - self.begin = self.get_from(begin) - self.end = self.get_from(end) + self.begin = begin + self.end = end else: raise Exception('Two of these args must be specified: ' 'n_steps, begin, end; ' @@ -71,7 +78,7 @@ class Scale(object): def get_datetimes(self): datetimes = [self.begin] tmp = self.begin - while tmp <= self.end: + while tmp < self.end: tmp = self.do_step(tmp) datetimes.append(tmp) return datetimes @@ -232,3 +239,87 @@ class ScaleMixin(object): qs.filter(**{begin_f: begin, end_f: end}) for begin, end in scale ] + + def get_by_chunks(self, qs, scale, field_callback=None, field_db='at'): + """Objects of queryset ranked according to a given scale. + + Returns a generator whose each item, corresponding to a scale chunk, + is a generator of objects from qs for this chunk. + + Args: + qs: Queryset of source objects, must be ordered *first* on the + same field returned by `field_callback`. + scale: Used to rank objects. + field_callback: Callable which gives value from an object used + to compare against limits of the scale chunks. + Default to: lambda obj: getattr(obj, field_db) + field_db: Used to filter against `scale` limits. + Default to 'at'. + + Examples: + If queryset `qs` use `values()`, `field_callback` must be set and + could be: `lambda d: d['at']` + If `field_db` use foreign attributes (eg with `__`), it should be + something like: `lambda obj: obj.group.at`. + + """ + if field_callback is None: + def field_callback(obj): + return getattr(obj, field_db) + + begin_f = '{}__gte'.format(field_db) + end_f = '{}__lte'.format(field_db) + + qs = ( + qs + .filter(**{begin_f: scale.begin, end_f: scale.end}) + ) + + obj_iter = iter(qs) + + last_obj = None + + def _objects_until(obj_iter, field_callback, end): + """Generator of objects until `end`. + + Ends if objects source is empty or when an object not verifying + field_callback(obj) <= end is met. + + If this object exists, it is stored in `last_obj` which is found + from outer scope. + Also, if this same variable is non-empty when the function is + called, it first yields its content. + + Args: + obj_iter: Source used to get objects. + field_callback: Returned value, when it is called on an object + will be used to test ordering against `end`. + end + + """ + nonlocal last_obj + + if last_obj is not None: + yield last_obj + last_obj = None + + for obj in obj_iter: + if field_callback(obj) <= end: + yield obj + else: + last_obj = obj + return + + for begin, end in scale: + # forward last seen object, if it exists, to the right chunk, + # and fill with empty generators for intermediate chunks of scale + if last_obj is not None: + if field_callback(last_obj) > end: + yield iter(()) + continue + + # yields generator for this chunk + # this set last_obj to None if obj_iter reach its end, otherwise + # it's set to the first met object from obj_iter which doesn't + # belong to this chunk + yield _objects_until(obj_iter, field_callback, end) diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html index 6fe025f6..19a11094 100644 --- a/kfet/templates/kfet/article_read.html +++ b/kfet/templates/kfet/article_read.html @@ -104,7 +104,7 @@ $(document).ready(function() { var stat_last = new StatsGroup( "{% url 'kfet.article.stat.sales.list' article.id %}", - $("#stat_last"), + $("#stat_last") ); }); diff --git a/kfet/views.py b/kfet/views.py index cfc58aa0..1df78d1e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2369,13 +2369,19 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): # à l'article en question et qui ne sont pas annulées # puis on choisi pour chaques intervalle les opérations # effectuées dans ces intervalles de temps - all_operations = (Operation.objects - .filter(group__on_acc=self.object) - .filter(canceled_at=None) - ) + all_operations = ( + Operation.objects + .filter(group__on_acc=self.object, + canceled_at=None) + .values('article_nb', 'group__at') + .order_by('group__at') + ) if types is not None: all_operations = all_operations.filter(type__in=types) - chunks = self.chunkify_qs(all_operations, scale, field='group__at') + chunks = self.get_by_chunks( + all_operations, scale, field_db='group__at', + field_callback=(lambda d: d['group__at']), + ) return chunks def get_context_data(self, *args, **kwargs): @@ -2391,7 +2397,8 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): # On compte les opérations nb_ventes = [] for chunk in operations: - nb_ventes.append(tot_ventes(chunk)) + ventes = sum(ope['article_nb'] for ope in chunk) + nb_ventes.append(ventes) context['charts'] = [{"color": "rgb(255, 99, 132)", "label": "NB items achetés", @@ -2442,29 +2449,39 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): context = {'labels': old_ctx['labels']} scale = self.scale - # On selectionne les opérations qui correspondent - # à l'article en question et qui ne sont pas annulées - # puis on choisi pour chaques intervalle les opérations - # effectuées dans ces intervalles de temps - all_operations = ( + all_purchases = ( Operation.objects - .filter(type=Operation.PURCHASE, - article=self.object, - canceled_at=None, - ) + .filter( + type=Operation.PURCHASE, + article=self.object, + canceled_at=None, + ) + .values('group__at', 'article_nb') + .order_by('group__at') ) - chunks = self.chunkify_qs(all_operations, scale, field='group__at') + liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ') + liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ') + + chunks_liq = self.get_by_chunks( + liq_only, scale, field_db='group__at', + field_callback=lambda d: d['group__at'], + ) + chunks_no_liq = self.get_by_chunks( + liq_exclude, scale, field_db='group__at', + field_callback=lambda d: d['group__at'], + ) + # On compte les opérations nb_ventes = [] nb_accounts = [] nb_liq = [] - for qs in chunks: - nb_ventes.append( - tot_ventes(qs)) - nb_liq.append( - tot_ventes(qs.filter(group__on_acc__trigramme='LIQ'))) - nb_accounts.append( - tot_ventes(qs.exclude(group__on_acc__trigramme='LIQ'))) + for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq): + sum_accounts = sum(ope['article_nb'] for ope in chunk_no_liq) + sum_liq = sum(ope['article_nb'] for ope in chunk_liq) + nb_ventes.append(sum_accounts + sum_liq) + nb_accounts.append(sum_accounts) + nb_liq.append(sum_liq) + context['charts'] = [{"color": "rgb(255, 99, 132)", "label": "Toutes consommations", "values": nb_ventes}, From 06572f0bb5b7994910e7bed6be29dcc90aa7d25c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 13 Apr 2017 14:11:44 +0200 Subject: [PATCH 076/161] Order create use Scale. Order create view use WeekScale. No query improvements, only shorter code. Scale/ScaleMixin: - Two methods directly relative to the Scale class move to... the Scale class. - Fix order create on Chrome. --- kfet/statistic.py | 187 +++++++++++----------- kfet/templates/kfet/inventory_create.html | 2 + kfet/views.py | 113 +++++++------ 3 files changed, 149 insertions(+), 153 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 5ff169ff..8ffb7db5 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -88,6 +88,99 @@ class Scale(object): label_fmt = self.label_fmt return [begin.strftime(label_fmt) for begin, end in self] + def chunkify_qs(self, qs, field=None): + if field is None: + field = 'at' + begin_f = '{}__gte'.format(field) + end_f = '{}__lte'.format(field) + return [ + qs.filter(**{begin_f: begin, end_f: end}) + for begin, end in self + ] + + def get_by_chunks(self, qs, field_callback=None, field_db='at'): + """Objects of queryset ranked according to the scale. + + Returns a generator whose each item, corresponding to a scale chunk, + is a generator of objects from qs for this chunk. + + Args: + qs: Queryset of source objects, must be ordered *first* on the + same field returned by `field_callback`. + field_callback: Callable which gives value from an object used + to compare against limits of the scale chunks. + Default to: lambda obj: getattr(obj, field_db) + field_db: Used to filter against `scale` limits. + Default to 'at'. + + Examples: + If queryset `qs` use `values()`, `field_callback` must be set and + could be: `lambda d: d['at']` + If `field_db` use foreign attributes (eg with `__`), it should be + something like: `lambda obj: obj.group.at`. + + """ + if field_callback is None: + def field_callback(obj): + return getattr(obj, field_db) + + begin_f = '{}__gte'.format(field_db) + end_f = '{}__lte'.format(field_db) + + qs = ( + qs + .filter(**{begin_f: self.begin, end_f: self.end}) + ) + + obj_iter = iter(qs) + + last_obj = None + + def _objects_until(obj_iter, field_callback, end): + """Generator of objects until `end`. + + Ends if objects source is empty or when an object not verifying + field_callback(obj) <= end is met. + + If this object exists, it is stored in `last_obj` which is found + from outer scope. + Also, if this same variable is non-empty when the function is + called, it first yields its content. + + Args: + obj_iter: Source used to get objects. + field_callback: Returned value, when it is called on an object + will be used to test ordering against `end`. + end + + """ + nonlocal last_obj + + if last_obj is not None: + yield last_obj + last_obj = None + + for obj in obj_iter: + if field_callback(obj) <= end: + yield obj + else: + last_obj = obj + return + + for begin, end in self: + # forward last seen object, if it exists, to the right chunk, + # and fill with empty generators for intermediate chunks of scale + if last_obj is not None: + if field_callback(last_obj) > end: + yield iter(()) + continue + + # yields generator for this chunk + # this set last_obj to None if obj_iter reach its end, otherwise + # it's set to the first met object from obj_iter which doesn't + # belong to this chunk + yield _objects_until(obj_iter, field_callback, end) + class DayScale(Scale): name = 'day' @@ -229,97 +322,3 @@ class ScaleMixin(object): def get_default_scale(self): return DayScale(n_steps=7, last=True) - - def chunkify_qs(self, qs, scale, field=None): - if field is None: - field = 'at' - begin_f = '{}__gte'.format(field) - end_f = '{}__lte'.format(field) - return [ - qs.filter(**{begin_f: begin, end_f: end}) - for begin, end in scale - ] - - def get_by_chunks(self, qs, scale, field_callback=None, field_db='at'): - """Objects of queryset ranked according to a given scale. - - Returns a generator whose each item, corresponding to a scale chunk, - is a generator of objects from qs for this chunk. - - Args: - qs: Queryset of source objects, must be ordered *first* on the - same field returned by `field_callback`. - scale: Used to rank objects. - field_callback: Callable which gives value from an object used - to compare against limits of the scale chunks. - Default to: lambda obj: getattr(obj, field_db) - field_db: Used to filter against `scale` limits. - Default to 'at'. - - Examples: - If queryset `qs` use `values()`, `field_callback` must be set and - could be: `lambda d: d['at']` - If `field_db` use foreign attributes (eg with `__`), it should be - something like: `lambda obj: obj.group.at`. - - """ - if field_callback is None: - def field_callback(obj): - return getattr(obj, field_db) - - begin_f = '{}__gte'.format(field_db) - end_f = '{}__lte'.format(field_db) - - qs = ( - qs - .filter(**{begin_f: scale.begin, end_f: scale.end}) - ) - - obj_iter = iter(qs) - - last_obj = None - - def _objects_until(obj_iter, field_callback, end): - """Generator of objects until `end`. - - Ends if objects source is empty or when an object not verifying - field_callback(obj) <= end is met. - - If this object exists, it is stored in `last_obj` which is found - from outer scope. - Also, if this same variable is non-empty when the function is - called, it first yields its content. - - Args: - obj_iter: Source used to get objects. - field_callback: Returned value, when it is called on an object - will be used to test ordering against `end`. - end - - """ - nonlocal last_obj - - if last_obj is not None: - yield last_obj - last_obj = None - - for obj in obj_iter: - if field_callback(obj) <= end: - yield obj - else: - last_obj = obj - return - - for begin, end in scale: - # forward last seen object, if it exists, to the right chunk, - # and fill with empty generators for intermediate chunks of scale - if last_obj is not None: - if field_callback(last_obj) > end: - yield iter(()) - continue - - # yields generator for this chunk - # this set last_obj to None if obj_iter reach its end, otherwise - # it's set to the first met object from obj_iter which doesn't - # belong to this chunk - yield _objects_until(obj_iter, field_callback, end) diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index d8109f8e..0192d4ad 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -161,6 +161,8 @@ $(document).ready(function() { $('input[type="submit"]').on("click", function(e) { e.preventDefault(); + var content; + if (conflicts.size) { content = ''; content += "Conflits possibles :" diff --git a/kfet/views.py b/kfet/views.py index 1df78d1e..45d9d1cb 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -50,7 +50,7 @@ from decimal import Decimal import django_cas_ng import heapq import statistics -from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes +from kfet.statistic import ScaleMixin, last_stats_manifest, tot_ventes, WeekScale class Home(TemplateView): @@ -1798,68 +1798,60 @@ class OrderList(ListView): context['suppliers'] = Supplier.objects.order_by('name') return context + @teamkfet_required def order_create(request, pk): supplier = get_object_or_404(Supplier, pk=pk) - articles = (Article.objects - .filter(suppliers=supplier.pk) - .distinct() - .select_related('category') - .order_by('category__name', 'name')) + articles = ( + Article.objects + .filter(suppliers=supplier.pk) + .distinct() + .select_related('category') + .order_by('category__name', 'name') + ) - initial = [] - today = timezone.now().date() - sales_q = (Operation.objects + # Force hit to cache + articles = list(articles) + + sales_q = ( + Operation.objects .select_related('group') .filter(article__in=articles, canceled_at=None) - .values('article')) - sales_s1 = (sales_q - .filter( - group__at__gte = today-timedelta(weeks=5), - group__at__lt = today-timedelta(weeks=4)) + .values('article') .annotate(nb=Sum('article_nb')) ) - sales_s1 = { d['article']:d['nb'] for d in sales_s1 } - sales_s2 = (sales_q - .filter( - group__at__gte = today-timedelta(weeks=4), - group__at__lt = today-timedelta(weeks=3)) - .annotate(nb=Sum('article_nb')) - ) - sales_s2 = { d['article']:d['nb'] for d in sales_s2 } - sales_s3 = (sales_q - .filter( - group__at__gte = today-timedelta(weeks=3), - group__at__lt = today-timedelta(weeks=2)) - .annotate(nb=Sum('article_nb')) - ) - sales_s3 = { d['article']:d['nb'] for d in sales_s3 } - sales_s4 = (sales_q - .filter( - group__at__gte = today-timedelta(weeks=2), - group__at__lt = today-timedelta(weeks=1)) - .annotate(nb=Sum('article_nb')) - ) - sales_s4 = { d['article']:d['nb'] for d in sales_s4 } - sales_s5 = (sales_q - .filter(group__at__gte = today-timedelta(weeks=1)) - .annotate(nb=Sum('article_nb')) - ) - sales_s5 = { d['article']:d['nb'] for d in sales_s5 } + scale = WeekScale(last=True, n_steps=5, std_chunk=False) + chunks = scale.chunkify_qs(sales_q, field='group__at') + sales = [] + + for chunk in chunks: + sales.append( + {d['article']: d['nb'] for d in chunk} + ) + + initial = [] for article in articles: - v_s1 = sales_s1.get(article.pk, 0) - v_s2 = sales_s2.get(article.pk, 0) - v_s3 = sales_s3.get(article.pk, 0) - v_s4 = sales_s4.get(article.pk, 0) - v_s5 = sales_s5.get(article.pk, 0) + # Get sales for each 5 last weeks + v_s1 = sales[0].get(article.pk, 0) + v_s2 = sales[1].get(article.pk, 0) + v_s3 = sales[2].get(article.pk, 0) + v_s4 = sales[3].get(article.pk, 0) + v_s5 = sales[4].get(article.pk, 0) v_all = [v_s1, v_s2, v_s3, v_s4, v_s5] + # Take the 3 greatest (eg to avoid 2 weeks of vacations) v_3max = heapq.nlargest(3, v_all) + # Get average and standard deviation v_moy = statistics.mean(v_3max) v_et = statistics.pstdev(v_3max, v_moy) + # Expected sales for next week v_prev = v_moy + v_et + # We want to have 1.5 * the expected sales in stock + # (because sometimes some articles are not delivered) c_rec_tot = max(v_prev * 1.5 - article.stock, 0) + # If ordered quantity is close enough to a level which can led to free + # boxes, we increase it to this level. if article.box_capacity: c_rec_temp = c_rec_tot / article.box_capacity if c_rec_temp >= 10: @@ -1889,8 +1881,9 @@ def order_create(request, pk): }) cls_formset = formset_factory( - form = OrderArticleForm, - extra = 0) + form=OrderArticleForm, + extra=0, + ) if request.POST: formset = cls_formset(request.POST, initial=initial) @@ -1907,14 +1900,15 @@ def order_create(request, pk): order.save() saved = True - article = articles.get(pk=form.cleaned_data['article'].pk) + article = form.cleaned_data['article'] q_ordered = form.cleaned_data['quantity_ordered'] if article.box_capacity: q_ordered *= article.box_capacity OrderArticle.objects.create( - order = order, - article = article, - quantity_ordered = q_ordered) + order=order, + article=article, + quantity_ordered=q_ordered, + ) if saved: messages.success(request, 'Commande créée') return redirect('kfet.order.read', order.pk) @@ -1926,9 +1920,10 @@ def order_create(request, pk): return render(request, 'kfet/order_create.html', { 'supplier': supplier, - 'formset' : formset, + 'formset': formset, }) + class OrderRead(DetailView): model = Order template_name = 'kfet/order_read.html' @@ -2378,8 +2373,8 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): ) if types is not None: all_operations = all_operations.filter(type__in=types) - chunks = self.get_by_chunks( - all_operations, scale, field_db='group__at', + chunks = scale.get_by_chunks( + all_operations, field_db='group__at', field_callback=(lambda d: d['group__at']), ) return chunks @@ -2462,12 +2457,12 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): liq_only = all_purchases.filter(group__on_acc__trigramme='LIQ') liq_exclude = all_purchases.exclude(group__on_acc__trigramme='LIQ') - chunks_liq = self.get_by_chunks( - liq_only, scale, field_db='group__at', + chunks_liq = scale.get_by_chunks( + liq_only, field_db='group__at', field_callback=lambda d: d['group__at'], ) - chunks_no_liq = self.get_by_chunks( - liq_exclude, scale, field_db='group__at', + chunks_no_liq = scale.get_by_chunks( + liq_exclude, field_db='group__at', field_callback=lambda d: d['group__at'], ) From 18425b82c21c8c5d1fd0be0e2e7d776e5dd86b77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 13 Apr 2017 15:15:59 +0200 Subject: [PATCH 077/161] Check negative on cancellation. - Like perform operations, cancel_operations can add/remove an account from negative accounts system. - Balances checks are now performed against real_balance instead of balance. So if someone with a balance_offset go, for real, to positive land (ie even without taking into account the balance offset), its account is removed from the negative system. - Fix bug on real_balance when negative exists but balance_offset is not set. Fixes #156. --- kfet/models.py | 25 ++++++++++++++++++++++++- kfet/views.py | 36 ++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index 7c03191a..af24db49 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -81,7 +81,7 @@ class Account(models.Model): # Propriétés supplémentaires @property def real_balance(self): - if (hasattr(self, 'negative')): + if hasattr(self, 'negative') and self.negative.balance_offset: return self.balance - self.negative.balance_offset return self.balance @@ -210,6 +210,29 @@ class Account(models.Model): def delete(self, *args, **kwargs): pass + def check_negative(self): + if self.real_balance < 0: + if hasattr(self, 'negative') and not self.negative.start: + self.negative.start = timezone.now() + self.negative.save() + elif not hasattr(self, 'negative'): + self.negative = ( + AccountNegative.objects.create( + account=self, start=timezone.now(), + ) + ) + elif hasattr(self, 'negative'): + # self.real_balance >= 0 + balance_offset = self.negative.balance_offset + if balance_offset: + ( + Account.objects + .filter(pk=self.pk) + .update(balance=F('balance')-balance_offset) + ) + self.refresh_from_db() + self.negative.delete() + class UserHasAccount(Exception): def __init__(self, trigramme): self.trigramme = trigramme diff --git a/kfet/views.py b/kfet/views.py index 330b195a..82ef8433 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1105,22 +1105,15 @@ def kpsul_perform_operations(request): with transaction.atomic(): # If not cash account, # saving account's balance and adding to Negative if not in - if not operationgroup.on_acc.is_cash: - Account.objects.filter(pk=operationgroup.on_acc.pk).update( - balance=F('balance') + operationgroup.amount) - operationgroup.on_acc.refresh_from_db() - if operationgroup.on_acc.balance < 0: - if hasattr(operationgroup.on_acc, 'negative'): - if not operationgroup.on_acc.negative.start: - operationgroup.on_acc.negative.start = timezone.now() - operationgroup.on_acc.negative.save() - else: - negative = AccountNegative( - account=operationgroup.on_acc, start=timezone.now()) - negative.save() - elif (hasattr(operationgroup.on_acc, 'negative') and - not operationgroup.on_acc.negative.balance_offset): - operationgroup.on_acc.negative.delete() + on_acc = operationgroup.on_acc + if not on_acc.is_cash: + ( + Account.objects + .filter(pk=on_acc.pk) + .update(balance=F('balance') + operationgroup.amount) + ) + on_acc.refresh_from_db() + on_acc.check_negative() # Updating checkout's balance if to_checkout_balance: @@ -1311,8 +1304,15 @@ def kpsul_cancel_operations(request): (Operation.objects.filter(pk__in=opes) .update(canceled_by=canceled_by, canceled_at=canceled_at)) for account in to_accounts_balances: - Account.objects.filter(pk=account.pk).update( - balance = F('balance') + to_accounts_balances[account]) + ( + Account.objects + .filter(pk=account.pk) + .update(balance=F('balance') + to_accounts_balances[account]) + ) + if not account.is_cash: + # Should always be true, but we want to be sure + account.refresh_from_db() + account.check_negative() for checkout in to_checkouts_balances: Checkout.objects.filter(pk=checkout.pk).update( balance = F('balance') + to_checkouts_balances[checkout]) From 9668f1d1ec94dc92d6befa6b61bcffc540eb1c6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 13 Apr 2017 15:48:13 +0200 Subject: [PATCH 078/161] Account: check_negative() -> update_negative() --- kfet/models.py | 2 +- kfet/views.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kfet/models.py b/kfet/models.py index af24db49..6c1f1240 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -210,7 +210,7 @@ class Account(models.Model): def delete(self, *args, **kwargs): pass - def check_negative(self): + def update_negative(self): if self.real_balance < 0: if hasattr(self, 'negative') and not self.negative.start: self.negative.start = timezone.now() diff --git a/kfet/views.py b/kfet/views.py index 82ef8433..60dbb44b 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1113,7 +1113,7 @@ def kpsul_perform_operations(request): .update(balance=F('balance') + operationgroup.amount) ) on_acc.refresh_from_db() - on_acc.check_negative() + on_acc.update_negative() # Updating checkout's balance if to_checkout_balance: @@ -1312,7 +1312,7 @@ def kpsul_cancel_operations(request): if not account.is_cash: # Should always be true, but we want to be sure account.refresh_from_db() - account.check_negative() + account.update_negative() for checkout in to_checkouts_balances: Checkout.objects.filter(pk=checkout.pk).update( balance = F('balance') + to_checkouts_balances[checkout]) From 7db497d09593bf3e5990e399bb110fd012421cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 13 Apr 2017 16:34:29 +0200 Subject: [PATCH 079/161] Less articles prices history - Prices given with order_to_inventory are saved to db only if they are updated (it doesn't create a new price row each time) Fixes #142. --- kfet/views.py | 94 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 33 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 330b195a..a33e3fc0 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1946,6 +1946,7 @@ class OrderRead(DetailView): context['mail'] = mail return context + @teamkfet_required def order_to_inventory(request, pk): order = get_object_or_404(Order, pk=pk) @@ -1953,28 +1954,36 @@ def order_to_inventory(request, pk): if hasattr(order, 'inventory'): raise Http404 - articles = (Article.objects - .filter(orders=order.pk) - .select_related('category') - .prefetch_related(Prefetch('orderarticle_set', - queryset = OrderArticle.objects.filter(order=order), - to_attr = 'order')) - .prefetch_related(Prefetch('supplierarticle_set', - queryset = (SupplierArticle.objects - .filter(supplier=order.supplier) - .order_by('-at')), - to_attr = 'supplier')) - .order_by('category__name', 'name')) + supplier_prefetch = Prefetch( + 'article__supplierarticle_set', + queryset=( + SupplierArticle.objects + .filter(supplier=order.supplier) + .order_by('-at') + ), + to_attr='supplier', + ) + + order_articles = ( + OrderArticle.objects + .filter(order=order.pk) + .select_related('article', 'article__category') + .prefetch_related( + supplier_prefetch, + ) + .order_by('article__category__name', 'article__name') + ) initial = [] - for article in articles: + for order_article in order_articles: + article = order_article.article initial.append({ 'article': article.pk, 'name': article.name, 'category': article.category_id, 'category__name': article.category.name, - 'quantity_ordered': article.order[0].quantity_ordered, - 'quantity_received': article.order[0].quantity_ordered, + 'quantity_ordered': order_article.quantity_ordered, + 'quantity_received': order_article.quantity_ordered, 'price_HT': article.supplier[0].price_HT, 'TVA': article.supplier[0].TVA, 'rights': article.supplier[0].rights, @@ -1989,31 +1998,50 @@ def order_to_inventory(request, pk): messages.error(request, 'Permission refusée') elif formset.is_valid(): with transaction.atomic(): - inventory = Inventory() - inventory.order = order - inventory.by = request.user.profile.account_kfet - inventory.save() + inventory = Inventory.objects.create( + order=order, by=request.user.profile.account_kfet, + ) + new_supplierarticle = [] + new_inventoryarticle = [] for form in formset: q_received = form.cleaned_data['quantity_received'] article = form.cleaned_data['article'] - SupplierArticle.objects.create( - supplier = order.supplier, - article = article, - price_HT = form.cleaned_data['price_HT'], - TVA = form.cleaned_data['TVA'], - rights = form.cleaned_data['rights']) - (OrderArticle.objects - .filter(order=order, article=article) - .update(quantity_received = q_received)) - InventoryArticle.objects.create( - inventory = inventory, - article = article, - stock_old = article.stock, - stock_new = article.stock + q_received) + + price_HT = form.cleaned_data['price_HT'] + TVA = form.cleaned_data['TVA'] + rights = form.cleaned_data['rights'] + + if any((form.initial['price_HT'] != price_HT, + form.initial['TVA'] != TVA, + form.initial['rights'] != rights)): + new_supplierarticle.append( + SupplierArticle( + supplier=order.supplier, + article=article, + price_HT=price_HT, + TVA=TVA, + rights=rights, + ) + ) + ( + OrderArticle.objects + .filter(order=order, article=article) + .update(quantity_received=q_received) + ) + new_inventoryarticle.append( + InventoryArticle( + inventory=inventory, + article=article, + stock_old=article.stock, + stock_new=article.stock + q_received, + ) + ) article.stock += q_received if q_received > 0: article.is_sold = True article.save() + SupplierArticle.objects.bulk_create(new_supplierarticle) + InventoryArticle.objects.bulk_create(new_inventoryarticle) messages.success(request, "C'est tout bon !") return redirect('kfet.order') else: From 0a21858b337c543b46d49a4cae9851dc17f86da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 14 Apr 2017 13:08:03 +0200 Subject: [PATCH 080/161] Use css for scroll positionning - Better rendering on scroll on pages with a left block (- It removes the warning on Firefox about scroll positionning) --- kfet/static/kfet/css/index.css | 7 +++++++ kfet/static/kfet/js/kfet.js | 10 ---------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 0244a57b..ce7e0dff 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -134,6 +134,13 @@ textarea { padding:0; } +@media (min-width: 768px) { + .col-content-left { + position: sticky; + top:50px; + } +} + .content-left-top { background:#fff; padding:10px 30px; diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index cc369e32..72ae675a 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -1,14 +1,4 @@ $(document).ready(function() { - $(window).scroll(function() { - if ($(window).width() >= 768 && $(this).scrollTop() > 72.6) { - $('.col-content-left').css({'position':'fixed', 'top':'50px'}); - $('.col-content-right').addClass('col-sm-offset-4 col-md-offset-3'); - } else { - $('.col-content-left').css({'position':'relative', 'top':'0'}); - $('.col-content-right').removeClass('col-sm-offset-4 col-md-offset-3'); - } - }); - if (typeof Cookies !== 'undefined') { // Retrieving csrf token csrftoken = Cookies.get('csrftoken'); From ff73a635f82dfb85af4922f6d5319e06c8f29e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 15 Apr 2017 11:09:16 +0100 Subject: [PATCH 081/161] Minor fixes in settings/ - Typo - Removes old comments - Moves the template debug context processor back to the common file: it won't be loaded anyway if `DEBUG=False`. - Ddt's middleware should be loaded first --- cof/settings/common.py | 7 ++----- cof/settings/dev.py | 9 ++++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index 4a6f9a12..93b11dae 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -3,7 +3,7 @@ Django common settings for cof project. Everything which is supposed to be identical between the production server and -the local development serveur should be here. +the local development server should be here. """ import os @@ -79,6 +79,7 @@ TEMPLATES = [ 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ + 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', @@ -118,10 +119,6 @@ USE_L10N = True USE_TZ = True - -# Media upload (through ImageField, SiteField) -# https://docs.djangoproject.com/en/1.9/ref/models/fields/ - # Various additional settings SITE_ID = 1 diff --git a/cof/settings/dev.py b/cof/settings/dev.py index 8f5c9f29..6272e6d9 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -12,10 +12,6 @@ EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' DEBUG = True -TEMPLATES[0]["OPTIONS"]["context_processors"] += [ - 'django.template.context_processors.debug' -] - # --- # Apache static/media config @@ -46,7 +42,10 @@ def show_toolbar(request): return True INSTALLED_APPS += ["debug_toolbar"] -MIDDLEWARE_CLASSES += ["debug_toolbar.middleware.DebugToolbarMiddleware"] +MIDDLEWARE_CLASSES = ( + ["debug_toolbar.middleware.DebugToolbarMiddleware"] + + MIDDLEWARE_CLASSES +) DEBUG_TOOLBAR_CONFIG = { 'SHOW_TOOLBAR_CALLBACK': show_toolbar, } From ea81ab7b25b9819505a186c19b3505449b2e8aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 15 Apr 2017 12:36:11 +0200 Subject: [PATCH 082/161] Few display improvements. - Current day remains on the screen on history. - Message for generic team user connection is sended only if user is connecting from a k-fet url. - Less contrast on history. --- kfet/signals.py | 16 +++++++++------- kfet/static/kfet/css/history.css | 5 ++++- kfet/static/kfet/css/index.css | 7 +++++-- kfet/static/kfet/css/kpsul.css | 4 ++++ kfet/templates/kfet/base_messages.html | 2 +- kfet/templates/kfet/history.html | 2 -- 6 files changed, 23 insertions(+), 13 deletions(-) diff --git a/kfet/signals.py b/kfet/signals.py index 3dd4d677..77c114e3 100644 --- a/kfet/signals.py +++ b/kfet/signals.py @@ -1,16 +1,18 @@ # -*- coding: utf-8 -*- -from __future__ import (absolute_import, division, - print_function, unicode_literals) -from builtins import * - from django.contrib import messages from django.contrib.auth.signals import user_logged_in from django.core.urlresolvers import reverse from django.dispatch import receiver + @receiver(user_logged_in) def messages_on_login(sender, request, user, **kwargs): - if (not user.username == 'kfet_genericteam' - and user.has_perm('kfet.is_team')): - messages.info(request, 'Connexion en utilisateur partagé ?' % reverse('kfet.login.genericteam'), extra_tags='safe') + if (not user.username == 'kfet_genericteam' and + user.has_perm('kfet.is_team') and + 'k-fet' in request.GET.get('next', '')): + messages.info( + request, + ('Connexion en utilisateur partagé ?' + .format(reverse('kfet.login.genericteam'))), + extra_tags='safe') diff --git a/kfet/static/kfet/css/history.css b/kfet/static/kfet/css/history.css index 976f5782..77ccaebb 100644 --- a/kfet/static/kfet/css/history.css +++ b/kfet/static/kfet/css/history.css @@ -14,12 +14,15 @@ padding-left:20px; font-size:16px; font-weight:bold; + position:sticky; + top:50px; + z-index:10; } #history .opegroup { height:30px; line-height:30px; - background-color:rgba(200,16,46,0.85); + background-color:rgba(200,16,46,0.75); color:#fff; font-weight:bold; padding-left:20px; diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index ce7e0dff..d241e209 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -186,8 +186,11 @@ textarea { text-align:center; } -.content-right { - margin:0 15px; + +@media (min-width: 768px) { + .content-right { + margin: 0 15px; + } } .content-right-block { diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index ba88e433..3a08a3bb 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -423,3 +423,7 @@ input[type=number]::-webkit-outer-spin-button { .kpsul_middle_right_col { overflow:auto; } + +.kpsul_middle_right_col #history .day { + top: 0; +} diff --git a/kfet/templates/kfet/base_messages.html b/kfet/templates/kfet/base_messages.html index 440b8c10..3ac8512d 100644 --- a/kfet/templates/kfet/base_messages.html +++ b/kfet/templates/kfet/base_messages.html @@ -2,7 +2,7 @@
{% for message in messages %}
-
+
{% if 'safe' in message.tags %} {{ message|safe }} diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index ab1eef72..9e983079 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -14,7 +14,6 @@ - {% endblock %} @@ -30,7 +29,6 @@
opérations
-

Filtres

De
à
Caisses {{ filter_form.checkouts }}
From ce23eece6aff233f330eb4401a6e6d8eaa91c11f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 15 Apr 2017 13:03:01 +0200 Subject: [PATCH 083/161] Fix display on small screen devices. - Remove useless margin on small screens. - Better pills display on small screens. - Revert to transparent background for section title. --- kfet/static/kfet/css/index.css | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index d241e209..d9c44a60 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -154,6 +154,14 @@ textarea { display:block; } +.content-left .buttons ul.nav-pills { + margin-bottom:5px; +} + +.content-left .buttons ul.nav-pills li { + margin:0 0 -5px; +} + .content-left-top.frozen-account { background:#000FBA; color:#fff; @@ -202,14 +210,6 @@ textarea { padding-bottom:15px; } -.content-right-block > div:not(.buttons-title) { - background:#fff; -} - -.content-right-block-transparent > div:not(.buttons-title) { - background-color: transparent; -} - .content-right-block .buttons-title { position:absolute; top:8px; From dbf5844f6a996399b2227f57a136a39897002673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 15 Apr 2017 14:41:55 +0200 Subject: [PATCH 084/161] Clean settings redis --- cof/settings/common.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index 98f4f713..a3b7527f 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -19,6 +19,26 @@ except ImportError: except KeyError: raise RuntimeError("Secrets missing") + +# Redis settings (for cache and channel layers) +try: + from .secret import REDIS_USER, REDIS_PASS +except ImportError: + REDIS_USER = os.environ.get("REDIS_USER", "") + REDIS_PASS = os.environ.get("REDIS_PASS", "") + +if REDIS_USER or REDIS_PASS: + REDIS_AUTH = REDIS_USER+":"+REDIS_PASS+"@" +else: + REDIS_AUTH = '' + +REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") +REDIS_PORT = os.environ.get("REDIS_PORT", "6379") + +REDIS_BASE_URL = "redis://" + REDIS_AUTH + REDIS_HOST + ":" + REDIS_PORT +# To select a specific redis database, do: REDIS_BASE_URL + '/' + + # Other secrets try: from .secret import ( @@ -153,18 +173,6 @@ AUTHENTICATION_BACKENDS = ( RECAPTCHA_USE_SSL = True - -# Redis settings (for cache and channel layers) - -REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") -REDIS_USER = os.environ.get("REDIS_USER", "") -REDIS_PASS = os.environ.get("REDIS_PASS", "") -REDIS_PORT = os.environ.get("REDIS_PORT", "6379") - -REDIS_AUTH = REDIS_USER+":"+REDIS_PASS+"@" if REDIS_USER or REDIS_PASS else '' -REDIS_BASE_URL = "redis://" + REDIS_AUTH + REDIS_HOST + ":" + REDIS_PORT - - # Cache settings (use db #1 of redis) CACHE_REDIS_URL = REDIS_BASE_URL + "/1" From 8622002e8df3f7c98a40b7f68fb45c5222c9e43e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Mon, 17 Apr 2017 20:40:54 +0200 Subject: [PATCH 085/161] minor change --- cof/settings_dev.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cof/settings_dev.py b/cof/settings_dev.py index fe4fcdea..c4a46011 100644 --- a/cof/settings_dev.py +++ b/cof/settings_dev.py @@ -192,9 +192,7 @@ def show_toolbar(request): machine physique n'est pas forcément connue, et peut difficilement être mise dans les INTERNAL_IPS. """ - if not DEBUG: - return False - return True + return DEBUG DEBUG_TOOLBAR_CONFIG = { 'SHOW_TOOLBAR_CALLBACK': show_toolbar, From 0d8a613f28223e756f660caa2ba8c000ba3cc596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 21 Apr 2017 18:22:53 +0200 Subject: [PATCH 086/161] improve bda inscription form/view code --- bda/forms.py | 36 ++++++++++++++++++++++++++++++++---- bda/views.py | 50 +++++++++----------------------------------------- 2 files changed, 41 insertions(+), 45 deletions(-) diff --git a/bda/forms.py b/bda/forms.py index 3565bedf..c0417d1e 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -1,14 +1,42 @@ # -*- coding: utf-8 -*- -from __future__ import division -from __future__ import print_function -from __future__ import unicode_literals - from django import forms +from django.forms.models import BaseInlineFormSet from django.utils import timezone + from bda.models import Attribution, Spectacle +class InscriptionInlineFormSet(BaseInlineFormSet): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # self.instance is a Participant object + tirage = self.instance.tirage + + # set once for all "spectacle" field choices + # - restrict choices to the spectacles of this tirage + # - force_choices avoid many db requests + spectacles = tirage.spectacle_set.select_related('location') + choices = [(sp.pk, str(sp)) for sp in spectacles] + self.force_choices('spectacle', choices) + + def force_choices(self, name, choices): + """Set choices of a field. + + As ModelChoiceIterator (default use to get choices of a + ModelChoiceField), it appends an empty selection if requested. + + """ + for form in self.forms: + field = form.fields[name] + if field.empty_label is not None: + field.choices = [('', field.empty_label)] + choices + else: + field.choices = choices + + class TokenForm(forms.Form): token = forms.CharField(widget=forms.widgets.Textarea()) diff --git a/bda/views.py b/bda/views.py index 6e0e73b6..00a1b300 100644 --- a/bda/views.py +++ b/bda/views.py @@ -32,6 +32,7 @@ from bda.models import ( from bda.algorithm import Algorithm from bda.forms import ( TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm, + InscriptionInlineFormSet, ) @@ -157,60 +158,27 @@ def inscription(request, tirage_id): messages.error(request, "Le tirage n'est pas encore ouvert : " "ouverture le {:s}".format(opening)) return render(request, 'bda/resume-inscription-tirage.html', {}) + + participant, _ = ( + Participant.objects.select_related('tirage') + .get_or_create(user=request.user, tirage=tirage) + ) + if timezone.now() > tirage.fermeture: # Le tirage est fermé. - participant, _ = ( - Participant.objects - .get_or_create(user=request.user, tirage=tirage) - ) choices = participant.choixspectacle_set.order_by("priority") messages.error(request, " C'est fini : tirage au sort dans la journée !") return render(request, "bda/resume-inscription-tirage.html", {"choices": choices}) - def force_for(f, to_choices=None, **kwargs): - """Overrides choices for ModelChoiceField. - - Args: - f (models.Field): To render as forms.Field - to_choices (dict): If a key `f.name` exists, f._choices is set to - its value. - - """ - formfield = f.formfield(**kwargs) - if to_choices: - if f.name in to_choices: - choices = [('', '---------')] + to_choices[f.name] - formfield._choices = choices - return formfield - - # Restrict spectacles choices to spectacles for this tirage. - spectacles = ( - tirage.spectacle_set - .select_related('location') - ) - spectacles_field_choices = [(sp.pk, str(sp)) for sp in spectacles] - - # Allow for spectacle choices to be set once for all. - # Form display use 1 request instead of (#forms of formset * #spectacles). - # FIXME: Validation still generates too much requests... - formfield_callback = partial( - force_for, - to_choices={ - 'spectacle': spectacles_field_choices, - }, - ) BdaFormSet = inlineformset_factory( Participant, ChoixSpectacle, fields=("spectacle", "double_choice", "priority"), - formfield_callback=formfield_callback, - ) - participant, _ = ( - Participant.objects - .get_or_create(user=request.user, tirage=tirage) + formset=InscriptionInlineFormSet, ) + success = False stateerror = False if request.method == "POST": From 739990cdb698ae165c9bcfc7ed35bfc4b679ea05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Sat, 22 Apr 2017 01:17:23 +0200 Subject: [PATCH 087/161] Add total boxes to new inventory view + fix/clean - Add total boxes in cellar and bar to new inventory view. - On this view, table is "minified". - Revert background color for some templates. - Clean some margin (responsively). - Clean tab pills on account read. --- kfet/static/kfet/css/index.css | 59 +++++----- kfet/templates/kfet/account_read.html | 60 +++++----- kfet/templates/kfet/inventory_create.html | 133 ++++++++++++++-------- 3 files changed, 141 insertions(+), 111 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index d9c44a60..04bb438e 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -114,22 +114,6 @@ textarea { padding: 0 !important; } -.panel-md-margin{ - background-color: white; - overflow:hidden; - padding-left: 15px; - padding-right: 15px; - padding-bottom: 15px; - padding-top: 1px; -} - -@media (min-width: 992px) { - .panel-md-margin{ - margin:8px; - background-color: white; - } -} - .col-content-left, .col-content-right { padding:0; } @@ -194,20 +178,18 @@ textarea { text-align:center; } - @media (min-width: 768px) { .content-right { - margin: 0 15px; + margin: 15px; } } .content-right-block { - padding-bottom:5px; position:relative; } -.content-right-block:last-child { - padding-bottom:15px; +.content-right-block > div:not(.buttons-title) { + background: #fff; } .content-right-block .buttons-title { @@ -229,9 +211,8 @@ textarea { .content-right-block h3 { border-bottom: 1px solid #c8102e; - margin: 20px 15px 15px; - padding-bottom: 10px; - padding-left: 20px; + margin: 0px 15px 15px; + padding: 20px 20px 10px; font-size:25px; } @@ -239,12 +220,18 @@ textarea { * Pages tableaux seuls */ -.content-center > div { +.content-center { background:#fff; } +@media (min-width: 992px) { + .content-center { + margin: 15px; + } +} + .content-center tbody tr:not(.section) td { - padding:0px 5px !important; + padding:0px 5px; } .content-center .table .form-control { @@ -252,7 +239,15 @@ textarea { height:28px; margin:3px 0px; } - .content-center .auth-form { + +.content-center .table-condensed input.form-control { + margin: 0 !important; + border-top: 0; + border-bottom: 0; + border-radius: 0; +} + +.content-center .auth-form { margin:15px; } @@ -577,15 +572,21 @@ thead .tooltip { /* Inventaires */ +#inventoryform input[type=number] { + text-align: center; +} + .inventory_modified { background:rgba(236,100,0,0.15); } .stock_diff { padding-left: 5px; - color:#C8102E; + color:#C8102E; } .inventory_update { - display:none; + display: none; + width: 50px; + margin: 0 auto; } diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index b55f2b99..95df85a0 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -23,7 +23,7 @@ $(document).ready(function() { "{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}", $("#stat_balance") ); - }); +}); {% endif %} {% endblock %} @@ -55,39 +55,33 @@ $(document).ready(function() {
{% include "kfet/base_messages.html" %}
-
-
- {% if account.user == request.user %} -
-
-

Statistiques

-
-

Ma balance

-
-

Ma consommation

-
-
-
-
- {% endif %} - {% if addcosts %} -

Gagné des majorations

-
-
    - {% for addcost in addcosts %} -
  • {{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€
  • - {% endfor %} -
-
- {% endif %} -

Historique

-
- {% if account.user == request.user %} -
-
+
+ {% if account.user == request.user %} +
+

Statistiques

+
+

Ma balance

+
+

Ma consommation

+
+
+
{% endif %} -
-
+
+ {% if addcosts %} +

Gagné des majorations

+
+
    + {% for addcost in addcosts %} +
  • {{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€
  • + {% endfor %} +
+
+ {% endif %} +

Historique

+
+
+
diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index d8109f8e..d1fe6e05 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -13,24 +13,25 @@ {% block content %} {% include 'kfet/base_messages.html' %} -
-
-
- - - - - - - - - - - - - - - {% for form in formset %} +
+
+
+ +
ArticleQuantité par caisseStock ThéoriqueCaisses en réserveCaisses en arrièreVracStock totalCompte terminé
+ + + + + + + + + + + + + + {% for form in formset %} {% ifchanged form.category %} @@ -41,42 +42,68 @@ {{ form.article }} - - + + + + + - - - - - {% endfor %} - -
ArticleQuantité par caisseStock théoriqueCaisses en réserveCaisses en arrièreVracStock totalCompte terminé
{{ form.category_name }}{{ form.name }} {{ form.box_capacity }}{{ form.stock_old }} -
-
- +
+ {{ form.stock_old }} + + + + + + + + {{ form.stock_new | attr:"readonly"| add_class:"form-control" }} + +
+ +
+
+
-
-
-
-
-
{{ form.stock_new | attr:"readonly"| add_class:"form-control" }}
-
-
- {{ formset.management_form }} - {% if not perms.kfet.add_inventory %} -
- {% include "kfet/form_authentication_snippet.html" %} -
- {% endif %} - - {% csrf_token %} -
+ {% endfor %} + + Totaux + + + + + + + + {{ formset.management_form }} + {% if not perms.kfet.add_inventory %} +
+ {% include "kfet/form_authentication_snippet.html" %} +
+ {% endif %} + + {% csrf_token %} + +
{% endblock %} diff --git a/kfet/templates/kfet/settings_update.html b/kfet/templates/kfet/settings_update.html index fdd5f5d4..b5f46c23 100644 --- a/kfet/templates/kfet/settings_update.html +++ b/kfet/templates/kfet/settings_update.html @@ -5,21 +5,6 @@ {% block content %} -{% include "kfet/base_messages.html" %} - -
-
-
-
- {% csrf_token %} - {% include 'kfet/form_snippet.html' with form=form %} - {% if not perms.kfet.change_settings %} - {% include 'kfet/form_authentication_snippet.html' %} - {% endif %} - {% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %} -
-
-
-
+{% include "kfet/base_form.html" with authz=perms.kfet.change_settings submit_text="Mettre à jour"%} {% endblock %} diff --git a/kfet/templates/kfet/supplier_form.html b/kfet/templates/kfet/supplier_form.html index 168f74d9..434b9382 100644 --- a/kfet/templates/kfet/supplier_form.html +++ b/kfet/templates/kfet/supplier_form.html @@ -1,27 +1,10 @@ {% extends 'kfet/base.html' %} -{% load widget_tweaks %} -{% load staticfiles %} {% block title %}Fournisseur - Modification{% endblock %} {% block content-header-title %}Fournisseur - Modification{% endblock %} {% block content %} -{% include 'kfet/base_messages.html' %} - -
-
-
-
- {% csrf_token %} - {% include 'kfet/form_snippet.html' with form=form %} - {% if not perms.kfet.change_supplier %} - {% include 'kfet/form_authentication_snippet.html' %} - {% endif %} - {% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %} -
-
-
-
+{% include 'kfet/base_form.html' with authz=perms.kfet.change_supplier submit_text="Mettre à jour" %} {% endblock %} diff --git a/kfet/views.py b/kfet/views.py index 60dbb44b..7b1674c4 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -85,6 +85,8 @@ def login_genericteam(request): user = authenticate(username="kfet_genericteam", token=token.token) login(request, user) + messages.success(request, "Connecté en utilisateur partagé") + if need_cas_logout: # Vue de déconnexion de CAS return logout_cas @@ -514,6 +516,10 @@ def account_update(request, trigramme): return render(request, "kfet/account_update.html", { 'account': account, + 'forms': [ + user_form, cof_form, account_form, group_form, pwd_form, + negative_form, + ], 'account_form': account_form, 'cof_form': cof_form, 'user_form': user_form, From 1a661c1fd3d784a2c784ab2ff8d90442871f004a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 18 May 2017 20:29:29 +0200 Subject: [PATCH 101/161] revert --- kfet/views.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 7b1674c4..fa94bb35 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -516,10 +516,6 @@ def account_update(request, trigramme): return render(request, "kfet/account_update.html", { 'account': account, - 'forms': [ - user_form, cof_form, account_form, group_form, pwd_form, - negative_form, - ], 'account_form': account_form, 'cof_form': cof_form, 'user_form': user_form, From e9073e22654a9908c86854f9bc04ad270a4429bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Thu, 18 May 2017 21:41:23 +0200 Subject: [PATCH 102/161] Improve multiple select inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit + Group edition form gains success message, is prettier + Fix: K-Fêt prefix for group name on this form --- kfet/static/kfet/css/index.css | 13 +++++ kfet/templates/kfet/account_group_form.html | 59 +++++++++++++++------ kfet/templates/kfet/base_form.html | 2 +- kfet/templates/kfet/history.html | 3 ++ kfet/views.py | 2 +- 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 50d2cae8..4fc02014 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -595,3 +595,16 @@ thead .tooltip { width: 50px; margin: 0 auto; } + +/* Multiple select customizations */ + +.ms-choice { + height: 34px !important; + line-height: 34px !important; + border: 1px solid #ccc !important; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important; +} + +.ms-choice > div { + top: 4px !important; +} diff --git a/kfet/templates/kfet/account_group_form.html b/kfet/templates/kfet/account_group_form.html index e37bdd89..00e7809b 100644 --- a/kfet/templates/kfet/account_group_form.html +++ b/kfet/templates/kfet/account_group_form.html @@ -1,37 +1,62 @@ {% extends 'kfet/base.html' %} {% load staticfiles %} +{% load widget_tweaks %} {% block extra_head %} {% endblock %} +{% block title %}Permissions - Édition{% endblock %} +{% block content-header-title %}Modification des permissions{% endblock %} + {% block content %} -
- {% csrf_token %} -
- {{ form.name.errors }} - {{ form.name.label_tag }} -
- K-Fêt - {{ form.name }} +{% include "kfet/base_messages.html" %} + +
+
+
+ + {% csrf_token %} +
+ +
+
+ K-Fêt + {{ form.name|add_class:"form-control" }} +
+ {% if form.name.errors %}{{ form.name.errors }}{% endif %} + {% if form.name.help_text %}{{ form.name.help_text }}{% endif %} +
+
+ {% include "kfet/form_field_snippet.html" with field=form.permissions %} + {% if not perms.kfet.manage_perms %} + {% include "kfet/form_authentication_snippet.html" %} + {% endif %} + {% include "kfet/form_submit_snippet.html" with value="Enregistrer" %} +
-
- {{ form.permissions.errors }} - {{ form.permissions.label_tag }} - {{ form.permissions }} -
- - +
diff --git a/kfet/templates/kfet/base_form.html b/kfet/templates/kfet/base_form.html index b83e888a..ba6a84ac 100644 --- a/kfet/templates/kfet/base_form.html +++ b/kfet/templates/kfet/base_form.html @@ -3,7 +3,7 @@
-
+ {% csrf_token %} {% include "kfet/form_snippet.html" %} {% if not authz %} diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index 9e983079..add461ab 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -128,6 +128,9 @@ $(document).ready(function() { $("select").multipleSelect({ width: '100%', filter: true, + allSelected: " ", + selectAllText: "Tout-te-s", + countSelected: "# sur %" }); $("input").on('dp.change change', function() { diff --git a/kfet/views.py b/kfet/views.py index fa94bb35..0cce165e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -539,7 +539,7 @@ class AccountGroupCreate(SuccessMessageMixin, CreateView): success_message = 'Nouveau groupe : %(name)s' success_url = reverse_lazy('kfet.account.group') -class AccountGroupUpdate(UpdateView): +class AccountGroupUpdate(SuccessMessageMixin, UpdateView): queryset = Group.objects.filter(name__icontains='K-Fêt') template_name = 'kfet/account_group_form.html' form_class = GroupForm From ae270656264da08afe911d3cd3ffa551a7ce117c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 19 May 2017 13:42:41 +0200 Subject: [PATCH 103/161] Group permissions select multiple -> checkboxes - Add handler for CheckboxSelectMultiple in form_field_snippet.html. - Add template filter "widget_type" to get widget class name. - Group permissions selection becomes easier. --- kfet/forms.py | 23 +++++++++++++++++---- kfet/static/kfet/css/index.css | 7 +++++++ kfet/templates/kfet/account_group_form.html | 10 --------- kfet/templates/kfet/form_field_snippet.html | 14 ++++++++++++- kfet/templatetags/kfet_tags.py | 7 +++++-- 5 files changed, 44 insertions(+), 17 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index f89b8f08..296ed4ae 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -8,7 +8,7 @@ from django.core.exceptions import ValidationError from django.core.validators import MinLengthValidator from django.contrib.auth.models import User, Group, Permission from django.contrib.contenttypes.models import ContentType -from django.forms import modelformset_factory +from django.forms import modelformset_factory, widgets from django.utils import timezone from djconfig.forms import ConfigForm @@ -151,10 +151,25 @@ class UserGroupForm(forms.ModelForm): model = User fields = ['groups'] + +class KFetPermissionsField(forms.ModelMultipleChoiceField): + + def __init__(self, *args, **kwargs): + queryset = Permission.objects.filter( + content_type__in=ContentType.objects.filter(app_label="kfet"), + ) + super().__init__( + queryset=queryset, + widget=widgets.CheckboxSelectMultiple, + *args, **kwargs + ) + + def label_from_instance(self, obj): + return obj.name + + class GroupForm(forms.ModelForm): - permissions = forms.ModelMultipleChoiceField( - queryset= Permission.objects.filter(content_type__in= - ContentType.objects.filter(app_label='kfet'))) + permissions = KFetPermissionsField() def clean_name(self): name = self.cleaned_data['name'] diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 4fc02014..036e725e 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -608,3 +608,10 @@ thead .tooltip { .ms-choice > div { top: 4px !important; } + +/* Checkbox select multiple */ + +.checkbox-select-multiple label { + font-weight: normal; + margin-bottom: 0; +} diff --git a/kfet/templates/kfet/account_group_form.html b/kfet/templates/kfet/account_group_form.html index 00e7809b..52b99934 100644 --- a/kfet/templates/kfet/account_group_form.html +++ b/kfet/templates/kfet/account_group_form.html @@ -42,16 +42,6 @@ {% endblock %} -{% block content-header-title %}Création d'un compte{% endblock %} +{% block main-class %}content-form{% endblock %} -{% block content %} +{% block main-content %} -{% include 'kfet/base_messages.html' %} - -
-
-
- - {% csrf_token %} -
- {{ trigramme_form.trigramme.errors }} - {{ trigramme_form.trigramme }} -
-
-

Les mots contenant des caractères non alphanumériques seront ignorés

- -
-
-
-
-
- {% include 'kfet/account_create_form.html' %} -
- {% if not perms.kfet.add_account %} - {% include 'kfet/form_authentication_snippet.html' %} - {% endif %} -
- -
+
+
+

Les mots contenant des caractères non alphanumériques seront ignorés

+ +
+
+
+
+
+ {% include 'kfet/account_create_form.html' %} +
+ {% if not perms.kfet.add_account %} + {% include 'kfet/form_authentication_snippet.html' %} + {% endif %} +
+ + {% endblock %} -{% block content-header-title %}Création d'un compte{% endblock %} +{% block main-class %}content-form{% endblock %} -{% block content %} +{% block main-content %} -
-
- {% include 'kfet/base_messages.html' %} -
- -
+
+
+ +
+
+
+
+
+ {% include 'kfet/account_create_form.html' %} +
+ {% if not perms.kfet.add_account %} + {% include 'kfet/form_authentication_snippet.html' %} + {% endif %} +
+ {% endblock %} -{% block title %}Informations sur l'article {{ article }}{% endblock %} -{% block content-header-title %}Article - {{ article.name }}{% endblock %} +{% block title %}Article - {{ article.name }}{% endblock %} +{% block header-title %}Informations sur l'article {{ article.name }}{% endblock %} -{% block content %} +{% block fixed-content %} -
-
-
-
-
{{ article.name }}
-
{{ article.category }}
-
-
Prix (hors réduc.): {{ article.price }}€
-
Stock: {{ article.stock }}
-
En vente: {{ article.is_sold | yesno:"Oui,Non" }}
-
Affiché: {{ article.hidden | yesno:"Non,Oui" }}
-
-
- -
+
+
{{ article.name }}
+
{{ article.category }}
+
+
Prix (hors réduc.): {{ article.price }}€
+
Stock: {{ article.stock }}
+
En vente: {{ article.is_sold | yesno:"Oui,Non" }}
+
Affiché: {{ article.hidden | yesno:"Non,Oui" }}
-
- {% include 'kfet/base_messages.html' %} -
-
-

Historique

-
-
-

Inventaires

- - - - - - - - - - {% for inventoryart in inventoryarts %} - - - - - - {% endfor %} - -
DateStockErreur
{{ inventoryart.inventory.at }}{{ inventoryart.stock_new }}{{ inventoryart.stock_error }}
-
-
-

Prix fournisseurs

- - - - - - - - - - - - {% for supplierart in supplierarts %} - - - - - - - - {% endfor %} - -
DateFournisseurHTTVADroits
{{ supplierart.at }}{{ supplierart.supplier.name }}{{ supplierart.price_HT }}{{ supplierart.TVA }}{{ supplierart.rights }}
-
-
-
-
-

Statistiques

-
-
-
-

Ventes de {{ article.name }}

-
-
-
-
+
+ + +{% endblock %} + +{% block main-content %} + +
+

Historique

+
+
+

Inventaires

+ + + + + + + + + + {% for inventoryart in inventoryarts %} + + + + + + {% endfor %} + +
DateStockErreur
{{ inventoryart.inventory.at }}{{ inventoryart.stock_new }}{{ inventoryart.stock_error }}
+
+
+

Prix fournisseurs

+
+ + + + + + + + + + + + {% for supplierart in supplierarts %} + + + + + + + + {% endfor %} + +
DateFournisseurHTTVADroits
{{ supplierart.at }}{{ supplierart.supplier.name }}{{ supplierart.price_HT }}{{ supplierart.TVA }}{{ supplierart.rights }}
+
+
+
+

Statistiques

+
+

Ventes

+
diff --git a/kfet/templates/kfet/article_update.html b/kfet/templates/kfet/article_update.html index 65ccec3b..d451df94 100644 --- a/kfet/templates/kfet/article_update.html +++ b/kfet/templates/kfet/article_update.html @@ -1,9 +1,11 @@ -{% extends 'kfet/base.html' %} +{% extends "kfet/base_col_1.html" %} -{% block title %}Édition de l'article {{ article.name }}{% endblock %} -{% block content-header-title %}Article {{ article.name }} - Édition{% endblock %} +{% block title %}{{ article.name }} - Édition{% endblock %} +{% block header-title %}Édition de l'article {{ article.name }}{% endblock %} -{% block content %} +{% block main-class %}content-form{% endblock %} + +{% block main-content %} {% include "kfet/base_form.html" with authz=perms.kfet.change_article submit_text="Mettre à jour"%} diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index 173a5fb7..da37abae 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -30,12 +30,12 @@ {% include "kfet/base_nav.html" %}
- {% block content-header %} -
-
-

{% block content-header-title %}{% endblock %}

-
+ {% block header %} +
+
+

{% block header-title %}{% endblock %}

+
{% endblock %} {% block content %}{% endblock %} {% include "kfet/base_footer.html" %} diff --git a/kfet/templates/kfet/base_col_1.html b/kfet/templates/kfet/base_col_1.html new file mode 100644 index 00000000..a4c26b82 --- /dev/null +++ b/kfet/templates/kfet/base_col_1.html @@ -0,0 +1,14 @@ +{% extends "kfet/base.html" %} + +{% block content %} + +
+
+ {% include "kfet/base_messages.html" %} +
+ {% block main-content %}{% endblock %} +
+
+
+ +{% endblock %} diff --git a/kfet/templates/kfet/base_col_2.html b/kfet/templates/kfet/base_col_2.html new file mode 100644 index 00000000..58c36d14 --- /dev/null +++ b/kfet/templates/kfet/base_col_2.html @@ -0,0 +1,19 @@ +{% extends "kfet/base.html" %} + +{% block content %} + +
+
+
+ {% block fixed-content %}{% endblock %} +
+
+
+ {% include "kfet/base_messages.html" %} +
+ {% block main-content %}{% endblock %} +
+
+
+ +{% endblock %} diff --git a/kfet/templates/kfet/base_form.html b/kfet/templates/kfet/base_form.html index 9fe79e32..1ac4c81b 100644 --- a/kfet/templates/kfet/base_form.html +++ b/kfet/templates/kfet/base_form.html @@ -1,17 +1,10 @@ {% load kfet_tags %} -
-
- {% include "kfet/base_messages.html" %} -
-
- {% csrf_token %} - {% include "kfet/form_snippet.html" %} - {% if not authz %} - {% include "kfet/form_authentication_snippet.html" %} - {% endif %} - {% include "kfet/form_submit_snippet.html" with value=submit_text %} -
-
-
-
+
+ {% csrf_token %} + {% include "kfet/form_snippet.html" %} + {% if not authz %} + {% include "kfet/form_authentication_snippet.html" %} + {% endif %} + {% include "kfet/form_submit_snippet.html" with value=submit_text %} +
diff --git a/kfet/templates/kfet/category.html b/kfet/templates/kfet/category.html index 5393bf59..b7797c49 100644 --- a/kfet/templates/kfet/category.html +++ b/kfet/templates/kfet/category.html @@ -1,52 +1,46 @@ -{% extends 'kfet/base.html' %} +{% extends "kfet/base_col_2.html" %} {% block title %}Categories d'articles{% endblock %} -{% block content-header-title %}Categories d'articles{% endblock %} +{% block header-title %}Categories d'articles{% endblock %} -{% block content %} +{% block fixed-content %} -
-
-
-
-
{{ categories|length }}
-
catégorie{{ categories|length|pluralize }}
-
-
-
-
- {% include 'kfet/base_messages.html' %} -
-
-

Liste des catégories

-
- - - - - - - - - - - {% for category in categories %} - - - - - - - {% endfor %} - -
NomNombre d'articlesPeut être majorée
- - - - {{ category.name }}{{ category.articles.all|length }}{{ category.has_addcost | yesno:"Oui,Non"}}
-
-
-
+
+
{{ categories|length }}
+
catégorie{{ categories|length|pluralize }}
+
+ +{% endblock %} + +{% block main-content %} + +
+

Liste des catégories

+
+ + + + + + + + + + + {% for category in categories %} + + + + + + + {% endfor %} + +
NomNombre d'articlesPeut être majorée
+ + + + {{ category.name }}{{ category.articles.all|length }}{{ category.has_addcost | yesno:"Oui,Non"}}
diff --git a/kfet/templates/kfet/category_update.html b/kfet/templates/kfet/category_update.html index af213e71..c535a31d 100644 --- a/kfet/templates/kfet/category_update.html +++ b/kfet/templates/kfet/category_update.html @@ -1,9 +1,11 @@ -{% extends 'kfet/base.html' %} +{% extends "kfet/base_col_1.html" %} -{% block title %}Édition de la catégorie {{ category.name }}{% endblock %} -{% block content-header-title %}Catégorie {{ category.name }} - Édition{% endblock %} +{% block title %}{{ articlecategory.name }} - Édition{% endblock %} +{% block header-title %}Édition de la catégorie {{ articlecategory.name }}{% endblock %} -{% block content %} +{% block main-class %}content-form{% endblock %} + +{% block main-content %} {% include "kfet/base_form.html" with authz=perms.kfet.edit_articlecategory submit_text="Enregistrer"%} diff --git a/kfet/templates/kfet/checkout.html b/kfet/templates/kfet/checkout.html index fb2d10a7..329b8bef 100644 --- a/kfet/templates/kfet/checkout.html +++ b/kfet/templates/kfet/checkout.html @@ -1,59 +1,53 @@ -{% extends "kfet/base.html" %} +{% extends "kfet/base_col_2.html" %} -{% block title %}Liste des caisses{% endblock %} -{% block content-header-title %}Caisses{% endblock %} +{% block title %}Caisses{% endblock %} +{% block header-title %}Caisses{% endblock %} -{% block content %} +{% block fixed-content %} -
-
-
-
-
{{ checkouts|length }}
-
caisse{{ checkouts|length|pluralize }}
-
- -
-
-
- {% include 'kfet/base_messages.html' %} -
-
-

Liste des caisses

-
- - - - - - - - - - - - - {% for checkout in checkouts %} - - - - - - - - - {% endfor %} - -
NomBalanceDéb. valid.Fin valid.Protégée
- - - - {{ checkout.name }}{{ checkout.balance}}€{{ checkout.valid_from }}{{ checkout.valid_to }}{{ checkout.is_protected }}
-
-
-
+
+
{{ checkouts|length }}
+
caisse{{ checkouts|length|pluralize }}
+
+ + +{% endblock %} + +{% block main-content %} + +
+

Liste des caisses

+
+ + + + + + + + + + + + + {% for checkout in checkouts %} + + + + + + + + + {% endfor %} + +
NomBalanceDéb. valid.Fin valid.Protégée
+ + + + {{ checkout.name }}{{ checkout.balance}}€{{ checkout.valid_from }}{{ checkout.valid_to }}{{ checkout.is_protected }}
diff --git a/kfet/templates/kfet/checkout_create.html b/kfet/templates/kfet/checkout_create.html index 0f254f65..bed1b6ef 100644 --- a/kfet/templates/kfet/checkout_create.html +++ b/kfet/templates/kfet/checkout_create.html @@ -1,28 +1,13 @@ -{% extends "kfet/base.html" %} +{% extends "kfet/base_col_1.html" %} {% block extra_head %}{{ form.media }}{% endblock %} {% block title %}Nouvelle caisse{% endblock %} -{% block content-header-title %}Création d'une caisse{% endblock %} +{% block header-title %}Création d'une caisse{% endblock %} -{% block content %} +{% block main-class %}content-form{% endblock %} +{% block main-content %} -{% include 'kfet/base_messages.html' %} -
- {% csrf_token %} - {{ form.non_field_errors}} - {% for field in form %} - {{ field.errors }} - {{ field.label_tag }} -
{{ field }}
- {% if field.help_text %} -

{{ field.help_text|safe }}

- {% endif %} - {% endfor %} - {% if not perms.kfet.add_checkout %} - - {% endif %} - -
+{% include "kfet/base_form.html" with authz=perms.kfet.add_checkout submit_text="Enregistrer" %} {% block extra_head %}{% endblock %} diff --git a/gestioncof/templates/gestioncof/banner_update.html b/gestioncof/templates/gestioncof/banner_update.html new file mode 100644 index 00000000..b2432eae --- /dev/null +++ b/gestioncof/templates/gestioncof/banner_update.html @@ -0,0 +1,23 @@ +{% extends "base_title.html" %} +{% load bootstrap %} +{% load i18n %} + +{% block page_size %}col-sm-8{%endblock%} + +{% block realcontent %} +

{% trans "Global configuration" %}

+
+
+ {% csrf_token %} + + {% for field in form %} + {{ field | bootstrap }} + {% endfor %} + +
+
+ +
+
+{% endblock %} diff --git a/gestioncof/templates/gestioncof/base_header.html b/gestioncof/templates/gestioncof/base_header.html index a7e29eb7..21441875 100644 --- a/gestioncof/templates/gestioncof/base_header.html +++ b/gestioncof/templates/gestioncof/base_header.html @@ -16,6 +16,14 @@

{% if user.first_name %}{{ user.first_name }}{% else %}{{ user.username }}{% endif %}, {% if user.profile.is_cof %}au COF{% else %}non-COF{% endif %}

+ +{% if config.gestion_banner %} + +{% endif %} + {% if messages %} {% for message in messages %}
diff --git a/gestioncof/views.py b/gestioncof/views.py index 6a728ea6..f98c51df 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -10,6 +10,8 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.views import login as django_login_view from django.contrib.auth.models import User from django.contrib.sites.models import Site +from django.core.urlresolvers import reverse_lazy +from django.views.generic import FormView from django.utils import timezone from django.contrib import messages @@ -21,10 +23,11 @@ from gestioncof.models import EventCommentField, EventCommentValue, \ CalendarSubscription from gestioncof.models import CofProfile, Club from gestioncof.decorators import buro_required, cof_required -from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ - SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ - RegistrationProfileForm, EventForm, CalendarForm, EventFormset, \ - RegistrationPassUserForm, ClubsForm +from gestioncof.forms import ( + UserProfileForm, EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm, + RegistrationUserForm, RegistrationProfileForm, EventForm, CalendarForm, + EventFormset, RegistrationPassUserForm, ClubsForm, GestioncofConfigForm +) from bda.models import Tirage, Spectacle @@ -758,3 +761,18 @@ def calendar_ics(request, token): response = HttpResponse(content=vcal.to_ical()) response['Content-Type'] = "text/calendar" return response + + +class ConfigUpdate(FormView): + form_class = GestioncofConfigForm + template_name = "gestioncof/banner_update.html" + success_url = reverse_lazy("home") + + def dispatch(self, request, *args, **kwargs): + if request.user is None or not request.user.is_superuser: + raise Http404 + return super().dispatch(request, *args, **kwargs) + + def form_valid(self, form): + form.save() + return super().form_valid(form) diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index ba88e433..da1cffa1 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -18,6 +18,17 @@ input[type=number]::-webkit-outer-spin-button { 100% { background: yellow; } } +/* Announcements banner */ + +#banner { + background-color: #d86b01; + width: 100%; + text-align: center; + padding: 10px; + color: white; + font-size: larger; +} + /* * Top row */ diff --git a/kfet/templates/kfet/base_messages.html b/kfet/templates/kfet/base_messages.html index 440b8c10..3af19b31 100644 --- a/kfet/templates/kfet/base_messages.html +++ b/kfet/templates/kfet/base_messages.html @@ -1,3 +1,10 @@ +{% if config.gestion_banner %} + +{% endif %} + {% if messages %}
{% for message in messages %} From 8c6d56b27c405bf4cf81140a55fb1550b26cc030 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Tue, 30 May 2017 20:44:30 +0200 Subject: [PATCH 123/161] Add Wagtail CMS for kfet app. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit K-Fêt - Integrate wagtail to serve "static" pages of old K-Fêt website - Fixture "kfetcms/kfet_wagtail_17_05" contains a copy of old website (as in May 2017). - Media files can be got until end of June 17 at http://partage.eleves.ens.fr//files/604e6dea2ceebc66b1936c6b3f911744/kfet_media.tar.gz Login/logout - Update package django_cas_ng to last version. - Clean COFCASBackend. - Change CAS version to 3 (version used on eleves.ens). This enables the logout redirection (for CAS ofc). - Add messages and clean existing ones on login/logout (for both outsider and cas users). Misc - Update settings to bypass an incompability between debug-toolbar and wagtailmenus packages. - Better management of dev/test-specific urls (if debug-toolbar wasn't in INSTALLED_APPS, media files were not served). - UI improvements. --- README.md | 2 +- cof/settings/common.py | 28 + cof/settings/dev.py | 20 + cof/urls.py | 14 +- gestioncof/__init__.py | 1 + gestioncof/apps.py | 8 + gestioncof/shared.py | 58 +- gestioncof/signals.py | 23 + gestioncof/templates/home.html | 4 +- gestioncof/views.py | 29 +- kfet/cms/__init__.py | 1 + kfet/cms/apps.py | 7 + kfet/cms/context_processors.py | 20 + kfet/cms/fixtures/kfet_wagtail_17_05.json | 1565 +++++++++++++++++ .../management/commands/kfet_loadwagtail.py | 35 + kfet/cms/migrations/0001_initial.py | 75 + kfet/cms/migrations/__init__.py | 0 kfet/cms/models.py | 165 ++ kfet/cms/static/kfetcms/css/index.css | 155 ++ kfet/cms/templates/kfetcms/base.html | 68 + kfet/cms/templates/kfetcms/carte.html | 46 + kfet/cms/templates/kfetcms/equipe.html | 67 + kfet/management/commands/loadkfetdevdata.py | 6 + kfet/models.py | 10 +- kfet/signals.py | 12 +- kfet/static/kfet/css/footer.css | 19 + kfet/static/kfet/css/history.css | 5 +- kfet/static/kfet/css/home.css | 74 +- kfet/static/kfet/css/index.css | 274 ++- kfet/static/kfet/css/jconfirm-kfet.css | 5 +- kfet/static/kfet/css/kpsul.css | 33 +- kfet/static/kfet/css/nav.css | 117 +- kfet/static/kfet/img/favicon.png | Bin 0 -> 3606 bytes kfet/static/kfet/js/statistic.js | 1 - kfet/templates/kfet/account.html | 33 +- kfet/templates/kfet/account_group.html | 8 +- kfet/templates/kfet/account_negative.html | 14 +- kfet/templates/kfet/account_read.html | 4 + kfet/templates/kfet/account_update.html | 6 + kfet/templates/kfet/article.html | 19 +- kfet/templates/kfet/article_read.html | 57 +- kfet/templates/kfet/base.html | 27 +- kfet/templates/kfet/base_col_1.html | 4 +- kfet/templates/kfet/base_col_2.html | 4 +- kfet/templates/kfet/base_footer.html | 17 + kfet/templates/kfet/base_messages.html | 18 +- kfet/templates/kfet/base_nav.html | 147 +- kfet/templates/kfet/category.html | 8 +- kfet/templates/kfet/checkout.html | 15 +- kfet/templates/kfet/checkout_read.html | 13 +- kfet/templates/kfet/home.html | 59 - kfet/templates/kfet/inventory.html | 13 +- kfet/templates/kfet/inventory_create.html | 4 +- kfet/templates/kfet/inventory_read.html | 6 +- kfet/templates/kfet/kpsul.html | 32 +- kfet/templates/kfet/left_account.html | 39 +- kfet/templates/kfet/left_checkout.html | 17 +- kfet/templates/kfet/nav_item.html | 18 + kfet/templates/kfet/order.html | 14 +- kfet/templates/kfet/order_create.html | 4 +- kfet/templates/kfet/order_read.html | 23 +- kfet/templates/kfet/order_to_inventory.html | 10 +- kfet/templates/kfet/settings.html | 4 +- kfet/templates/kfet/transfers.html | 4 +- kfet/urls.py | 7 +- kfet/views.py | 57 +- requirements.txt | 4 +- 67 files changed, 3038 insertions(+), 618 deletions(-) create mode 100644 gestioncof/apps.py create mode 100644 gestioncof/signals.py create mode 100644 kfet/cms/__init__.py create mode 100644 kfet/cms/apps.py create mode 100644 kfet/cms/context_processors.py create mode 100644 kfet/cms/fixtures/kfet_wagtail_17_05.json create mode 100644 kfet/cms/management/commands/kfet_loadwagtail.py create mode 100644 kfet/cms/migrations/0001_initial.py create mode 100644 kfet/cms/migrations/__init__.py create mode 100644 kfet/cms/models.py create mode 100644 kfet/cms/static/kfetcms/css/index.css create mode 100644 kfet/cms/templates/kfetcms/base.html create mode 100644 kfet/cms/templates/kfetcms/carte.html create mode 100644 kfet/cms/templates/kfetcms/equipe.html create mode 100644 kfet/static/kfet/css/footer.css create mode 100644 kfet/static/kfet/img/favicon.png delete mode 100644 kfet/templates/kfet/home.html create mode 100644 kfet/templates/kfet/nav_item.html diff --git a/README.md b/README.md index 1a3d575e..b6017577 100644 --- a/README.md +++ b/README.md @@ -177,7 +177,7 @@ Charger les mails indispensables au bon fonctionnement de GestioCOF : Une base de donnée pré-remplie est disponible en lançant les commandes : - python manage.py loaddata gestion sites accounts groups articles + python manage.py loaddata gestion sites articles python manage.py loaddevdata Vous êtes prêts à développer ! Lancer GestioCOF en faisant diff --git a/cof/settings/common.py b/cof/settings/common.py index 261760d6..f7c7bba6 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -56,6 +56,22 @@ INSTALLED_APPS = [ 'widget_tweaks', 'custommail', 'djconfig', + 'wagtail.wagtailforms', + 'wagtail.wagtailredirects', + 'wagtail.wagtailembeds', + 'wagtail.wagtailsites', + 'wagtail.wagtailusers', + 'wagtail.wagtailsnippets', + 'wagtail.wagtaildocs', + 'wagtail.wagtailimages', + 'wagtail.wagtailsearch', + 'wagtail.wagtailadmin', + 'wagtail.wagtailcore', + 'wagtail.contrib.modeladmin', + 'wagtailmenus', + 'modelcluster', + 'taggit', + 'kfet.cms', ] MIDDLEWARE_CLASSES = [ @@ -69,6 +85,8 @@ MIDDLEWARE_CLASSES = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.security.SecurityMiddleware', 'djconfig.middleware.DjConfigMiddleware', + 'wagtail.wagtailcore.middleware.SiteMiddleware', + 'wagtail.wagtailredirects.middleware.RedirectMiddleware', ] ROOT_URLCONF = 'cof.urls' @@ -87,6 +105,7 @@ TEMPLATES = [ 'django.core.context_processors.i18n', 'django.core.context_processors.media', 'django.core.context_processors.static', + 'wagtailmenus.context_processors.wagtailmenus', 'djconfig.context_processors.config', 'gestioncof.shared.context_processor', 'kfet.context_processors.auth', @@ -143,9 +162,12 @@ LOGIN_URL = "cof-login" LOGIN_REDIRECT_URL = "home" CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' +CAS_VERSION = '3' +CAS_LOGIN_MSG = None CAS_IGNORE_REFERER = True CAS_REDIRECT_URL = '/' CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" + AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'gestioncof.shared.COFCASBackend', @@ -171,3 +193,9 @@ CHANNEL_LAYERS = { } FORMAT_MODULE_PATH = 'cof.locale' + +# Wagtail settings + +WAGTAIL_SITE_NAME = 'GestioCOF' +WAGTAIL_ENABLE_UPDATE_CHECK = False +TAGGIT_CASE_INSENSITIVE = True diff --git a/cof/settings/dev.py b/cof/settings/dev.py index a3a17f99..ffd34c7d 100644 --- a/cof/settings/dev.py +++ b/cof/settings/dev.py @@ -28,6 +28,26 @@ MEDIA_URL = '/media/' # Debug tool bar # --- +# "Versions" panel of django-debug-toolbar <=1.8 is not compatible with +# wagtailmenus. +# See https://github.com/jazzband/django-debug-toolbar/issues/922 +# TODO: Bug should be fixed in ddt 1.9 (not released yet). When fixed, this +# declaration may be removed. +DEBUG_TOOLBAR_PANELS = [ + 'debug_toolbar.panels.timer.TimerPanel', + 'debug_toolbar.panels.settings.SettingsPanel', + 'debug_toolbar.panels.headers.HeadersPanel', + 'debug_toolbar.panels.request.RequestPanel', + 'debug_toolbar.panels.sql.SQLPanel', + 'debug_toolbar.panels.staticfiles.StaticFilesPanel', + 'debug_toolbar.panels.templates.TemplatesPanel', + 'debug_toolbar.panels.cache.CachePanel', + 'debug_toolbar.panels.signals.SignalsPanel', + 'debug_toolbar.panels.logging.LoggingPanel', + 'debug_toolbar.panels.redirects.RedirectsPanel', +] + + def show_toolbar(request): """ On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar diff --git a/cof/urls.py b/cof/urls.py index 06b1087a..886070e9 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -14,6 +14,10 @@ from django.views.generic.base import TemplateView from django.contrib.auth import views as django_views from django_cas_ng import views as django_cas_views +from wagtail.wagtailadmin import urls as wagtailadmin_urls +from wagtail.wagtailcore import urls as wagtail_urls +from wagtail.wagtaildocs import urls as wagtaildocs_urls + from gestioncof import views as gestioncof_views, csv_views from gestioncof.urls import export_patterns, petitcours_patterns, \ surveys_patterns, events_patterns, calendar_patterns, \ @@ -48,7 +52,7 @@ urlpatterns = [ url(r'^outsider/login$', gestioncof_views.login_ext), url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}), url(r'^login$', gestioncof_views.login, name="cof-login"), - url(r'^logout$', gestioncof_views.logout), + url(r'^logout$', gestioncof_views.logout, name="cof-logout"), # Infos persos url(r'^profile$', gestioncof_views.profile), url(r'^outsider/password-change$', django_views.password_change), @@ -82,6 +86,8 @@ urlpatterns = [ url(r'^utile_cof/diff_cof$', gestioncof_views.liste_diffcof), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), url(r'^k-fet/', include('kfet.urls')), + url(r'^cms/', include(wagtailadmin_urls)), + url(r'^documents/', include(wagtaildocs_urls)), ] if 'debug_toolbar' in settings.INSTALLED_APPS: @@ -90,7 +96,13 @@ if 'debug_toolbar' in settings.INSTALLED_APPS: url(r'^__debug__/', include(debug_toolbar.urls)), ] +if settings.DEBUG: # Si on est en production, MEDIA_ROOT est servi par Apache. # Il faut dire à Django de servir MEDIA_ROOT lui-même en développement. urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +# Wagtail for uncatched +urlpatterns += [ + url(r'', include(wagtail_urls)), +] diff --git a/gestioncof/__init__.py b/gestioncof/__init__.py index e69de29b..b77fdb94 100644 --- a/gestioncof/__init__.py +++ b/gestioncof/__init__.py @@ -0,0 +1 @@ +default_app_config = 'gestioncof.apps.GestioncofConfig' diff --git a/gestioncof/apps.py b/gestioncof/apps.py new file mode 100644 index 00000000..6e24b050 --- /dev/null +++ b/gestioncof/apps.py @@ -0,0 +1,8 @@ +from django.apps import AppConfig + + +class GestioncofConfig(AppConfig): + name = 'gestioncof' + + def ready(self): + import gestioncof.signals diff --git a/gestioncof/shared.py b/gestioncof/shared.py index e4180b49..fabcacb9 100644 --- a/gestioncof/shared.py +++ b/gestioncof/shared.py @@ -1,62 +1,26 @@ -from django.contrib.sites.models import Site from django.conf import settings +from django.contrib.sites.models import Site + from django_cas_ng.backends import CASBackend -from django_cas_ng.utils import get_cas_client -from django.contrib.auth import get_user_model from gestioncof.models import CofProfile -User = get_user_model() - class COFCASBackend(CASBackend): - def authenticate_cas(self, ticket, service, request): - """Verifies CAS ticket and gets or creates User object""" - - client = get_cas_client(service_url=service) - username, attributes, _ = client.verify_ticket(ticket) - if attributes: - request.session['attributes'] = attributes - if not username: - return None + def clean_username(self, username): # Le CAS de l'ENS accepte les logins avec des espaces au début # et à la fin, ainsi qu’avec une casse variable. On normalise pour # éviter les doublons. - username = username.strip().lower() + return username.strip().lower() - profiles = CofProfile.objects.filter(login_clipper=username) - if len(profiles) > 0: - profile = profiles.order_by('-is_cof')[0] - user = profile.user - return user - try: - user = User.objects.get(username=username) - except User.DoesNotExist: - # user will have an "unusable" password - user = User.objects.create_user(username, '') - user.save() - return user - - def authenticate(self, ticket, service, request): - """Authenticates CAS ticket and retrieves user data""" - user = self.authenticate_cas(ticket, service, request) - if user is None: - return user - try: - profile = user.profile - except CofProfile.DoesNotExist: - profile, created = CofProfile.objects.get_or_create(user=user) - profile.save() - if not profile.login_clipper: - profile.login_clipper = user.username - profile.save() - if not user.email: - user.email = settings.CAS_EMAIL_FORMAT % profile.login_clipper - user.save() - if profile.is_buro and not user.is_staff: - user.is_staff = True - user.save() + def configure_user(self, user): + # cannot use "defaults" arg + profile, _ = CofProfile.objects.get_or_create(user=user) + profile.login_clipper = user.username + profile.save() + user.email = settings.CAS_EMAIL_FORMAT % profile.login_clipper + user.save() return user diff --git a/gestioncof/signals.py b/gestioncof/signals.py new file mode 100644 index 00000000..11cb55fc --- /dev/null +++ b/gestioncof/signals.py @@ -0,0 +1,23 @@ +from django.contrib import messages +from django.contrib.auth.signals import user_logged_in +from django.dispatch import receiver +from django.utils.translation import ugettext_lazy as _ + +from django_cas_ng.signals import cas_user_authenticated + + +@receiver(user_logged_in) +def messages_on_out_login(request, user, **kwargs): + if user.backend.startswith('django.contrib.auth'): + msg = _('Connexion à GestioCOF réussie. Bienvenue {}.').format( + user.get_short_name(), + ) + messages.success(request, msg) + + +@receiver(cas_user_authenticated) +def mesagges_on_cas_login(request, user, **kwargs): + msg = _('Connexion à GestioCOF par CAS réussie. Bienvenue {}.').format( + user.get_short_name(), + ) + messages.success(request, msg) diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index 5f783a48..acc04f30 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -1,4 +1,5 @@ {% extends "gestioncof/base_header.html" %} +{% load wagtailcore_tags %} {% block homelink %} {% endblock %} @@ -55,7 +56,8 @@

K-Fêt

    -
  • Page d'accueil
  • + {# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #} +
  • Page d'accueil
  • Calendrier
  • {% if perms.kfet.is_team %}
  • K-Psul
  • diff --git a/gestioncof/views.py b/gestioncof/views.py index 6a728ea6..11b1c5ba 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -7,12 +7,17 @@ from custommail.shortcuts import send_custom_mail from django.shortcuts import redirect, get_object_or_404, render from django.http import Http404, HttpResponse, HttpResponseForbidden from django.contrib.auth.decorators import login_required -from django.contrib.auth.views import login as django_login_view +from django.contrib.auth.views import ( + login as django_login_view, logout as django_logout_view, +) from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.utils import timezone +from django.utils.translation import ugettext_lazy as _ from django.contrib import messages +from django_cas_ng.views import logout as cas_logout_view + from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ SurveyQuestionAnswer from gestioncof.models import Event, EventRegistration, EventOption, \ @@ -78,15 +83,21 @@ def login_ext(request): @login_required -def logout(request): - try: - profile = request.user.profile - except CofProfile.DoesNotExist: - profile, created = CofProfile.objects.get_or_create(user=request.user) - if profile.login_clipper: - return redirect("django_cas_ng.views.logout") +def logout(request, next_page=None): + if next_page is None: + next_page = request.GET.get('next', None) + + profile = getattr(request.user, 'profile', None) + + if profile and profile.login_clipper: + msg = _('Déconnexion de GestioCOF et CAS réussie. À bientôt {}.') + logout_view = cas_logout_view else: - return redirect("django.contrib.auth.views.logout") + msg = _('Déconnexion de GestioCOF réussie. À bientôt {}.') + logout_view = django_logout_view + + messages.success(request, msg.format(request.user.get_short_name())) + return logout_view(request, next_page=next_page) @login_required diff --git a/kfet/cms/__init__.py b/kfet/cms/__init__.py new file mode 100644 index 00000000..0f6cab45 --- /dev/null +++ b/kfet/cms/__init__.py @@ -0,0 +1 @@ +default_app_config = 'kfet.cms.apps.KFetCMSAppConfig' diff --git a/kfet/cms/apps.py b/kfet/cms/apps.py new file mode 100644 index 00000000..f7276ae8 --- /dev/null +++ b/kfet/cms/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class KFetCMSAppConfig(AppConfig): + name = 'kfet.cms' + label = 'kfetcms' + verbose_name = 'CMS K-Fêt' diff --git a/kfet/cms/context_processors.py b/kfet/cms/context_processors.py new file mode 100644 index 00000000..0cf7a649 --- /dev/null +++ b/kfet/cms/context_processors.py @@ -0,0 +1,20 @@ +from kfet.models import Article + + +def get_articles(request): + articles = ( + Article.objects + .filter(is_sold=True, hidden=False) + .select_related('category') + .order_by('category__name', 'name') + ) + pressions, others = [], [] + for article in articles: + if article.category.name == 'Pression': + pressions.append(article) + else: + others.append(article) + return { + 'pressions': pressions, + 'articles': others, + } diff --git a/kfet/cms/fixtures/kfet_wagtail_17_05.json b/kfet/cms/fixtures/kfet_wagtail_17_05.json new file mode 100644 index 00000000..a0651c79 --- /dev/null +++ b/kfet/cms/fixtures/kfet_wagtail_17_05.json @@ -0,0 +1,1565 @@ +[ +{ + "fields": { + "root_page": 9, + "port": 8000, + "site_name": "Global", + "hostname": "localhost", + "is_default_site": true + }, + "pk": 2, + "model": "wagtailcore.site" +}, +{ + "fields": { + "live": true, + "show_in_menus": false, + "numchild": 1, + "title": "Root", + "slug": "root", + "depth": 1, + "expire_at": null, + "search_description": "", + "content_type": [ + "wagtailcore", + "page" + ], + "owner": null, + "expired": false, + "first_published_at": null, + "has_unpublished_changes": false, + "go_live_at": null, + "path": "0001", + "latest_revision_created_at": null, + "url_path": "/", + "locked": false, + "seo_title": "" + }, + "pk": 1, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": true, + "numchild": 5, + "title": "Bienvenue en K-Fêt", + "slug": "k-fet", + "depth": 3, + "expire_at": null, + "search_description": "", + "content_type": [ + "kfetcms", + "kfetpage" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "000100010001", + "latest_revision_created_at": null, + "url_path": "/global/k-fet/", + "locked": false, + "seo_title": "Accueil" + }, + "pk": 3, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": true, + "numchild": 0, + "title": "Mode d'emploi", + "slug": "mode-demploi", + "depth": 4, + "expire_at": null, + "search_description": "", + "content_type": [ + "kfetcms", + "kfetpage" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "0001000100010001", + "latest_revision_created_at": null, + "url_path": "/global/k-fet/mode-demploi/", + "locked": false, + "seo_title": "" + }, + "pk": 4, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": true, + "numchild": 0, + "title": "L'\u00e9quipe", + "slug": "equipe", + "depth": 4, + "expire_at": null, + "search_description": "", + "content_type": [ + "kfetcms", + "kfetpage" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "0001000100010002", + "latest_revision_created_at": null, + "url_path": "/global/k-fet/equipe/", + "locked": false, + "seo_title": "" + }, + "pk": 5, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": true, + "numchild": 0, + "title": "La carte", + "slug": "carte", + "depth": 4, + "expire_at": null, + "search_description": "", + "content_type": [ + "kfetcms", + "kfetpage" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "0001000100010003", + "latest_revision_created_at": null, + "url_path": "/global/k-fet/carte/", + "locked": false, + "seo_title": "" + }, + "pk": 6, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": true, + "numchild": 0, + "title": "Les soir\u00e9es", + "slug": "soirees", + "depth": 4, + "expire_at": null, + "search_description": "", + "content_type": [ + "kfetcms", + "kfetpage" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "0001000100010004", + "latest_revision_created_at": null, + "url_path": "/global/k-fet/soirees/", + "locked": false, + "seo_title": "" + }, + "pk": 7, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": true, + "numchild": 0, + "title": "Le flipper", + "slug": "flipper", + "depth": 4, + "expire_at": null, + "search_description": "", + "content_type": [ + "kfetcms", + "kfetpage" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "0001000100010005", + "latest_revision_created_at": null, + "url_path": "/global/k-fet/flipper/", + "locked": false, + "seo_title": "" + }, + "pk": 8, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": true, + "numchild": 1, + "title": "Global", + "slug": "global", + "depth": 2, + "expire_at": null, + "search_description": "", + "content_type": [ + "wagtailcore", + "page" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "00010001", + "latest_revision_created_at": null, + "url_path": "/global/", + "locked": false, + "seo_title": "" + }, + "pk": 9, + "model": "wagtailcore.page" +}, +{ + "fields": { + "live": true, + "show_in_menus": false, + "numchild": 0, + "title": "Mentions l\u00e9gales", + "slug": "mentions-legales", + "depth": 4, + "expire_at": null, + "search_description": "", + "content_type": [ + "kfetcms", + "kfetpage" + ], + "owner": [ + "root" + ], + "expired": false, + "first_published_at": "2017-05-28T04:20:00.000Z", + "has_unpublished_changes": false, + "go_live_at": null, + "path": "0001000100010006", + "latest_revision_created_at": null, + "url_path": "/global/k-fet/mentions-legales/", + "locked": false, + "seo_title": "" + }, + "pk": 10, + "model": "wagtailcore.page" +}, +{ + "fields": { + "name": "Root", + "numchild": 0, + "path": "0001", + "depth": 1 + }, + "pk": 1, + "model": "wagtailcore.collection" +}, +{ + "model": "kfetcms.kfetpage", + "fields": { + "custom_template": "", + "no_header": false, + "content": "

    La K-F\u00eat, c'est quoi ?

    \n\n

    \n Eh bien la K-F\u00eat, c'est le bar des \u00e9l\u00e8ves de l'\u00c9cole normale\n sup\u00e9rieure. Elle est situ\u00e9e dans les locaux de l'\u00c9cole, au pied de\n l'escalier C (plan). On y trouve \u00e0 boire, bien s\u00fbr,\n des bi\u00e8res en nombre pl\u00e9thorique mais aussi caf\u00e9s, th\u00e9s, softs et de quoi\n grignoter. Ah oui un point important, on ne va pas \u00e0 la K-F\u00eat, on va EN K-F\u00eat.\n

    \n\n

    Mais on n'y fait que boire et manger ?

    \n\n

    \n Que nenni, \u00f4 jeune et innocent conscrit-e ! La K-F\u00eat n'est pas un bouge\n sordide o\u00f9 des piliers de bar passent leurs journ\u00e9es \u00e0 picoler. Enfin pas\n uniquement. C'est aussi un lieu de divertissement avec\n son flipper (la mythique, la seule, l'unique,\n la g\u00e9niale Amazon Hunt), son baby-foot et le lieu d'\u00e9lection des \nbridgeur-se-s, du club jeux, des joueur-se-s de poker voire des quelques\n irr\u00e9ductibles du boulot qui y viennent bosser en profitant du point \nd'acc\u00e8s wifi. \n

    \n\n

    Ah \u00e7a a l'air bien mais... qui s'en occupe ? C'est ouvert quand ?

    \n\n

    \n L'\u00e9quipe d'\u00e9l\u00e8ves motiv\u00e9-e-s qui s'occupent de la K-F\u00eat s'appelle, en toute logique, l'\u00e9quipe K-F\u00eat.\n Elle est men\u00e9e par un-e leader charismatique et bien-aim\u00e9-e, \naccompagn\u00e9-e de ses troupes de fid\u00e8les, les K-F\u00eat wo-men, boys et girls.\n Le local de la K-F\u00eat n'est ouvert que si un-e K-F\u00eat wo-man est \npr\u00e9sente. \u00c0 savoir la plupart du temps entre 12h et 3h du matin.\n

    \n\n

    Et je peux y faire ce que je veux ?

    \n\n

    \n Oui et non. Nous ne sommes pas ta grand-m\u00e8re et nous n'allons\n certainement pas t'emp\u00eacher de faire la f\u00eate, ni de d\u00e9guster des pintes\n jusqu'au petit p\u00f4t. Par contre nous attendons de toi que tu ne sois pas\n un-e gros-se con-ne. A priori pas de raison de le croire, mais jette tout de m\u00eame\n un \u0153il sur le mode d'emploi de la K-F\u00eat, \u00e7a\n pourrait t'\u00e9viter de perdre un genoux ou deux...\n

    \n\n

    J'adore la K-F\u00eat, j'aimerais y organiser une soir\u00e9e, c'est possible ?

    \n\n

    \n Bien s\u00fbr\u00a0! Pour cela commence par lire ce petit\n guide histoire de savoir dans quoi tu t'engages puis contacte ton-ta chef-fe K-F\u00eat ador\u00e9-e pour v\u00e9rifier que la date de ta\n soir\u00e9e n'est pas d\u00e9j\u00e0 prise par une autre f\u00eate et obtenir son\n accord.\n

    \n\n

    J'ai une question \u00e0 vous poser. O\u00f9 puis-je vous contacter ?

    \n\n

    \n Commence d\u00e9j\u00e0 par jeter un oeil sur le mode\n d'emploi de la K-F\u00eat. Si la r\u00e9ponse \u00e0 tes interrogations ne s'y\n trouve pas, rien n'est perdu. En effet le service informatique de \nl'\u00c9cole, dans sa grande mansu\u00e9tude, a mis \u00e0 disposition de l'\u00e9quipe \nK-F\u00eat une adresse e-mail, k-fet@ens.fr. Mais sinon, passe en K-F\u00eat, il y aura sans doute un K-F\u00eat wo-man qui saura r\u00e9pondre \u00e0 ta question.\n

    " + }, + "pk": 3 +}, +{ + "model": "kfetcms.kfetpage", + "fields": { + "custom_template": "", + "no_header": false, + "content": "

    Article 0 : La K-F\u00eat n'existe pas.

    La K-F\u00eat, c'est magique, comment \u00e7a marche ?

    La K-F\u00eat n'a rien de magique, il n'y a pas de petits d\u00e9mons qui font le m\u00e9nage, pas plus que d'arbustes g\u00e9n\u00e9tiquement modifi\u00e9s aux OGM sur lesquels poussent les bouteilles de bi\u00e8res. La K-F\u00eat c'est avant tout une \u00e9quipe qui sacrifie une partie de son temps libre pour que tout se passe pour le mieux.

    Que puis-je faire pour vous aider un peu ?

    D\u00e9j\u00e0 ne pas poser de probl\u00e8mes, c'est \u00e0 dire ne pas r\u00e9veiller tout l'internat en sortant, essayer de ne pas finir dans un \u00e9tat trop avanc\u00e9 d'alcoolisation, etc... Mine de rien \u00e7a nous \u00e9viterait quelques probl\u00e8mes.

    Ensuite, comme tu le sais s\u00fbrement les bi\u00e8res sont consign\u00e9es, il est donc pr\u00e9f\u00e9rable pour nous que tu n'embarques pas les bouteilles en souvenir dans ta thurne. Mieux, tu peux nous faire gagner du temps de rangement en les ramenant au bar en partant. Et encore mieux, tu peux jeter tes d\u00e9chets (gobelets, boite de pringles, etc...). Si tu fais d\u00e9j\u00e0 tout \u00e7a tu nous simplifieras grandement la vie.

    Le syst\u00e8me mon\u00e9taire de la K-F\u00eat

    En bon \u00e9tat souverain et ind\u00e9pendant, la K-F\u00eat a sa propre monnaie : l'unit\u00e9 K-F\u00eat (UKF). Elle vaut 10 centimes d'euro. La K-F\u00eat ne battant pas monnaie, les UKF que tu poss\u00e8des sont not\u00e9es sur ton compte, identifi\u00e9 par un trigramme (une suite de trois caract\u00e8res) et que tu peux recharger en liquide ou par ch\u00e8que. Note que si tu y tiens vraiment, tu peux payer en liquide, mais poss\u00e9der un compte est bien plus pratique.

    Comment commander \u00e0 boire ou \u00e0 manger ?

    Pour commander \u00e0 boire ou \u00e0 manger, il suffit de demander \u00e0 un membre de l'\u00e9quipe K-F\u00eat. Et \u00e7a marche encore mieux si la demande est effectu\u00e9e avec le sourire au d\u00e9but et un merci \u00e0 la fin : l'\u00e9quipe est constitu\u00e9e de volontaires b\u00e9n\u00e9voles, et mieux vaut ne pas les prendre pour des chiens. EN AUCUN CAS on ne passe derri\u00e8re le bar si on n'est pas membre de l'\u00e9quipe K-F\u00eat.

    Puis-je fumer en K-F\u00eat ?

    Non ! Imagine-toi les jours de soir\u00e9es, la K-F\u00eat remplie et tout le monde qui fume... On finirait tous avec des poumons aussi crades que le sol de la K-F\u00eat. Ce serait quand m\u00eame dommage pour la recherche fran\u00e7aise qu'on cr\u00e8ve tous avant 30 ans, non ?

    Par contre tu peux fumer dehors, il y a m\u00eame des cendriers juste pour toi, par contre tu remarqueras que les chambres de l'internat se trouvent juste au dessus de toi. T\u00e2che donc de ne pas faire trop de bruit.

    Et amener ma propre bouteille ?

    D\u00e9j\u00e0 c'est apporter, enfin en tout cas avant de la boire. Ensuite la K-F\u00eat est un lieu de convivialit\u00e9 o\u00f9 les bi\u00e8res te sont vendues au prix co\u00fbtant,\n franchement ce serait pas fair-play de te la jouer solo. Alors \u00e9videment il y a des exceptions, par exemple si tu reviens de Belgique et que tu veux faire go\u00fbter de la Wesvleteren \u00e0 tes amis de l'\u00e9quipe K-F\u00eat, ou si tu veux organiser une d\u00e9gustation de vins avec la charcuterie qui va bien. Tu comprendras qu'un pack de Kro c'est quand m\u00eame pas la m\u00eame classe...

    Je peux passer ma musique ?

    Bien s\u00fbr, nous sommes tr\u00e8s loin de penser tout conna\u00eetre en mati\u00e8re de musique. Mais comme nous sommes entre gens civilis\u00e9s, et que je te rappelle que tu n'as pas le droit de passer derri\u00e8re le bar, il convient de demander \u00e0 un-e membre de l'\u00e9quipe K-F\u00eat afin qu'ille t'indique qui est \u00e0 l'origine de ces chansons que tu n'appr\u00e9cies apparemment pas. Apr\u00e8s avoir obtenu son accord tu peux demander \u00e0 quelqu'un de mettre ta playlist, qui peut-\u00eatre sur un lecteur mp3, sur Deezer ou juste sur l'ordi, mais dans ce dernier cas ce sera plus dur puisque tu n'y aura pas acc\u00e8s directement.

    Le plus simple pour toi (et pour nous) est donc de pr\u00e9voir des playlists sur Deezer d'avance et de nous les proposer. Par contre, sois gentil-le, n'insiste pas si nous ne voulons pas de ta musique traditionnelle hongroise. Par ailleurs, si un trop grand nombre de personnes nous demande de passer de la musique, l'\u00e9quipe K-F\u00eat peut ne pas acc\u00e9der \u00e0 ta requ\u00eate.

    Comment organiser une soir\u00e9e en K-F\u00eat ?

    Tout membre du COF peut organiser une soir\u00e9e en K-F\u00eat \u00e0 la condition qu'elle soit publique et annonc\u00e9e une semaine \u00e0 l'avance par des affiches dans l'\u00e9cole et un mot dans le BOcal. Il faut bien sur aussi l'accord du COF qui s'occupe de voir si \u00e7a ne pose pas de probl\u00e8me \u00e0 l'admin, celui de la K-F\u00eat team pour qu'il y ait des K-F\u00eat wo-men pour servir et s\u00fbrement du BOUM pour qu'il s'occupe de la musique. Nous t'avons tout r\u00e9sum\u00e9 ici ; merci qui ? Une fois que tu as accompli ces formalit\u00e9s, il ne te reste plus qu'\u00e0 imprimer et coller des affiches pour que ta soir\u00e9e soit un succ\u00e8s !

    D'autres remarques ?

    Des tonnes, en voici quelques unes :

    • Ce n'est pas caf\u00e8t, ni kfet, ni caf\u00e9t\u00e9ria, c'est K-F\u00eat, avec les majuscules.
    • On dit \"en K-F\u00eat\".
    • On ne passe pas derri\u00e8re le bar, je sais je l'ai d\u00e9j\u00e0 dit, mais \u00e7a a du mal \u00e0 rentrer. S'il n'y a personne pour servir c'est que les K-F\u00eat people sont soit occup\u00e9-e-s quelque chose d'important en arri\u00e8re-K-F\u00eat, soit sont pos\u00e9-e-s dans le canap\u00e9 \u00e0 c\u00f4t\u00e9 du bar, soit sont en train de jouer \u00e0 l'Amazon. Demande-leur, ou prends ton mal en patience.
    • La K-F\u00eat n'est pas une porcherie, tu n'es pas oblig\u00e9-e de laisser tout ton bordel quand tu pars.
    • Merci d'avoir lu jusque l\u00e0.
    " + }, + "pk": 4 +}, +{ + "model": "kfetcms.kfetpage", + "fields": { + "custom_template": "kfetcms/equipe.html", + "no_header": false, + "content": "

    Les K-F\u00eat boys et girls

    \n\n

    Les K-F\u00eat boys and girls font de main d'\u0153uvre bon march\u00e9 pour la \nK-F\u00eat, illes peuvent passer derri\u00e8re le bar, prendre vos commandes et \nrecharger votre compte si par malheur il est \u00e0 sec. La liste de \ncelleux-ci est trop longue pour tou-te-s les citer, pour les reconna\u00eetre\n regarde les gens qui passent derri\u00e8re le bar tout en conservant leur \nint\u00e9grit\u00e9 physique.

    \n\n

    Comment devient-on K-F\u00eat people ?

    \n\n

    Grande question que tout le monde se pose un jour ou l'autre. Pour \nacc\u00e9der au titre prestigieux de K-F\u00eat boy-girl, il est n\u00e9cessaire mais \npas suffisant d'\u00eatre assid\u00fbment pr\u00e9sent-e en K-F\u00eat, et d'\u00eatre pr\u00eat-e \u00e0 \ntrimer pour elle. Si tu es souvent en K-F\u00eat, que tu es sympathique et \nmotiv\u00e9-e, et surtout en fin de compte si le-la chef-fe le veut bien, tu \npourras devenir K-F\u00eat boy-girl et passer derri\u00e8re le bar pour servir. \nEnsuite, si tu es motiv\u00e9-e et efficace, ou simplement si t'es un-e pote \ndu-de la chef-fe et qu'ille n'a aucun scrupule, tu pourras devenir K-F\u00eat\n wo-man et avoir la cl\u00e9.

    \n\n

    Et comme la K-F\u00eat c'est avant tout beaucoup d'emmerdes on a pas envie\n de te forcer la main, on veut que cela vienne de toi. Donc si tu te \nsens pr\u00eat-e \u00e0 participer \u00e0 la vie mouvement\u00e9e de la K-F\u00eat fais-en part \nau-\u00e0 la chef-fe. Ille ne va pas te manger.

    " + }, + "pk": 5 +}, +{ + "model": "kfetcms.kfetpage", + "fields": { + "custom_template": "kfetcms/carte.html", + "no_header": false, + "content": "

    \n\tLe service de la bi\u00e8re est, historiquement, la mission et le sacerdoce \ndu K-F\u00eat people. Ille y est d\u00e9vou\u00e9-e corps et \u00e2me, et accomplit sa t\u00e2che\n avec ardeur et passion. Voyons comment se d\u00e9clinent les occasions \nd'approcher du nirvana brassicole. Les prix donn\u00e9s sont en UKF. Si tu \nn'as pas compris, va voir par ici.

    " + }, + "pk": 6 +}, +{ + "model": "kfetcms.kfetpage", + "fields": { + "custom_template": "", + "no_header": false, + "content": "

    \n Tu veux organiser une soir\u00e9e en K-F\u00eat ? Pas de probl\u00e8me !\n

    \n\n

    Quand puis-je organiser une soir\u00e9e ?

    \n\n

    \n Tu peux organiser une soir\u00e9e le jour que tu souhaites, \u00e0 condition que la\n date ne soit pas d\u00e9j\u00e0 prise par quelqu'un d'autre. Sache par contre que la\n K-F\u00eat ne te sera pas enti\u00e8rement d\u00e9di\u00e9e et que les utilisateur-rice-s habituel-le-s\n continueront de la fr\u00e9quenter (et risquent fortement de squatter ta\n soir\u00e9e). Donc si tu veux un peu d'intimit\u00e9 les soir\u00e9es du week-end sont plus\n conseill\u00e9es (mais l'\u00e9quipe risque de ne pas \u00eatre pr\u00e9sente), mais aussi\n plus pris\u00e9es, d\u00e9p\u00eache-toi de r\u00e9server la tienne.\n

    \n\n

    Quelles d\u00e9marches dois-je effectuer ?

    \n

    \n D\u00e9j\u00e0 pr\u00e9venir poliment l'\u00e9quipe K-F\u00eat, et\n surtout le-la chef-fe pour v\u00e9rifier que la date est encore libre, et qu'il y\n aura au moins quelqu'un pour t'ouvrir la K-F\u00eat. Ensuite, si ta soir\u00e9e\n n'est pas une simple bouffe qui finit avant minuit il faut pr\u00e9venir les\n vigiles via l'administration au moyen d'une demande d'autorisation de\n soir\u00e9e qui se trouve sur la section du p\u00f4le Pr\u00e9vention et S\u00e9curit\u00e9 sur l'intranet : demande d'autorisation de soir\u00e9e.\n \u00c0 faire au moins une semaine avant ta soir\u00e9e.\n

    \n

    \n Si en plus tu as besoin que le BOUM s'occupe de la musique et/ou PLS des\n lumi\u00e8res c'est elleux qu'il faut contacter. Histoire de t'\u00e9viter\n d'avoir \u00e0 chercher voici leur adresse\u00a0: boum (at) ens (point) fr\n et pls (at) ens (point) fr.\n

    \n\n

    C'est enfin le grand jour, je fais quoi ?

    \n

    \n D\u00e9j\u00e0 le m\u00e9nage, oui je sais c'est chiant mais c'est le prix \u00e0 payer pour\n profiter du local. Demande \u00e0 ce qu'un-e K-F\u00eat wo-man t'ouvre et tu devrais avoir\n \u00e0 ta disposition tout ce qu'il faut pour faire briller la K-F\u00eat (ou au moins on essaiera de\n trouver ce qu'il faut). Fais par\n contre attention aux bouteilles de bi\u00e8re qui sont consign\u00e9es, s'il n'y a\n personne pour les ranger contente-toi de les mettre sur le bar, quelqu'un\n s'en chargera plus tard. Les meubles peuvent \u00eatre d\u00e9plac\u00e9s dans une salle\n voisine si tu le souhaites, il faudra juste penser \u00e0 les remettre en place.\n

    \n

    \n Ensuite dans l'id\u00e9al tu connais tous tes potes, donc en donner une liste \u00e0\n la loge permet d'\u00e9viter quelques probl\u00e8mes et quelques aller-retours.\n Au-del\u00e0 de 21h, les ext\u00e9rieur-e-s ne peuvent rentrer qu'avec un-e Ulmien-ne ayant sa carte\n sur lui-elle.\n

    \n\n

    Je pourrai passer ma musique ?

    \n\n

    \n Si le BOUM est pr\u00e9sent, faut voir avec elleux : boum (at) ens (point) fr
    \n Sinon, pr\u00e9pare ta musique sur un lecteur mp3 ou une playlist\n Deezer. Lors de la soir\u00e9e,\n demande \u00e0 un-e K-F\u00eat wo-man de passer ce que tu as pr\u00e9par\u00e9.\n

    \n\n\n

    Et pour ce qui est de la nourriture, des boissons ?

    \n

    \n Tu peux apporter toute la nourriture que tu souhaites\u00a0; pr\u00e9vois assez\n large, il y a beaucoup de K-F\u00eat people \u00e0 nourrir. Pour ce qui est de la\n boisson, il faut te limiter aux boissons de cat\u00e9gorie 2, c'est \u00e0 dire\n bi\u00e8res, vins et boissons \u00e0 base de vin, champagne et bien s\u00fbr les boissons sans alcool.\n

    \n\n

    Et pendant la soir\u00e9e ?

    \n

    \n Ce soir c'est ton soir, il est donc bien s\u00fbr \u00e9vident que tu dois\n rester pr\u00e9sent-e et joignable du d\u00e9but \u00e0 la fin de la soir\u00e9e. Id\u00e9alement ce\n doit aussi \u00eatre le cas de tes \"Responsables ordre et discipline\". Vous ne serez pas\n trop de deux ou trois pour r\u00e9gler les probl\u00e8mes qui pourraient survenir,\n tes potes bourr\u00e9-e-s, tes potes qui fument, tes potes qui font du bordel dans la cage d'escalier,\n etc... Tous les probl\u00e8mes qui pourraient survenir te seront imput\u00e9s donc\n pr\u00e9viens-les, c'est tes potes apr\u00e8s tout, non ?\n

    \n\n

    Apr\u00e8s c'est bon ?

    \n

    \n Eh non, pas encore, apr\u00e8s (ou le lendemain de) ta soir\u00e9e il te faudra encore ranger,\n faire le m\u00e9nage et passer un coup de javel. Oui encore, mais bon, pense \u00e0 toutes les fois o\u00f9\n c'est nous qui le faisons pour le bien de tou-te-s. Une fois n'est pas\n coutume demande \u00e0 un-e K-F\u00eat wo-man de t'ouvrir et de te fournir tout le\n mat\u00e9riel dont tu pourrais avoir besoin, et l\u00e0 o\u00f9 c'est vraiment classe\n c'est que tu peux m\u00eame faire \u00e7a en musique si tu le souhaites. N'oublie\n pas non plus de rapporter les meubles que tu pourrais avoir sortis et que\n les poubelles ne disparaissent pas toutes seules.\n

    \n\n

    Une derni\u00e8re remarque ?

    \n\n

    \n Ouais, la K-F\u00eat c'est pas chez m\u00e9m\u00e9, alors c'est peut-\u00eatre ta soir\u00e9e mais\n si un-e membre de l'\u00e9quipe K-F\u00eat te dit quelque\n chose (de baisser le son, de virer telle ou telle personne...) tu acceptes avec le sourire.\n En particulier tu ne passes pas derri\u00e8re le bar.\n

    \n\n

    Je ne parle pas bien fran\u00e7ais, vous pourriez me faire un r\u00e9sum\u00e9 ?

    \n

    \n Organiser ta soir\u00e9e c'est facile :\n

    \n
    • Envoie un mail \u00e0 la K-F\u00eat pour demander l'autorisation\n : k-fet (at) ens (point) fr.
    • Lorsque c'est bon, remplis\n le papier de l'admin, et\n donne-le au-\u00e0 la chef-fe K-F\u00eat.
    • Pour la musique, l'alcool, contacte la K-F\u00eat.
    • Le jour de la soir\u00e9e, viens faire le m\u00e9nage et donne une liste des\n ext\u00e9rieur-e-s \u00e0 la loge.
    • Pendant la soir\u00e9e, surveille tes invit\u00e9s (pas trop de bruit \u00e0\n l'ext\u00e9rieur de la K-F\u00eat, pas de gens trop bourr\u00e9s qui font des b\u00eatises\n avec les alarmes ou les sorties de secours...)
    • Apr\u00e8s la soir\u00e9e (le lendemain si tu veux) reviens faire le m\u00e9nage.
    \n

    \n Voila, facile, non ?\n

    " + }, + "pk": 7 +}, +{ + "model": "kfetcms.kfetpage", + "fields": { + "custom_template": "", + "no_header": false, + "content": "

    Et le baby-foot

    \n\n

    LE flipper

    \n\n\n\n

    \n\tIl existe en K-F\u00eat une machine unique, inimitable, tout droit venue des\n ann\u00e9es folles et b\u00e9nies o\u00f9 les concepteurs de flippers connaissaient \nencore leur m\u00e9tier, o\u00f9 les tilts \u00e9taient m\u00e9rit\u00e9s et le jeu un art de \nvivre. L'esth\u00e8te appr\u00e9cie et reconna\u00eet imm\u00e9diatement la beaut\u00e9 sobre et \nsauvage de l'Amazon Hunt II. D'admirateur, il se m\u00e9tamorphose \nin\u00e9luctablement en joueur-se, puis en amant-e. Car l'Amazon est une \nfemme, fatale \u00e0 bien des \u00e9gards, tou-te-s les grand-e-s joueur-euse-s \nvous le diront. Dans la pr\u00e9histoire de la K-F\u00eat, des demi-dieux-d\u00e9esses \ndu flipper d\u00e9sormais pass\u00e9-e-s dans la l\u00e9gende ont r\u00e9dig\u00e9 un trait\u00e9 \n(certain-e-s diront une bible, voire la Bible) disponible ici (en pdf).

    Le baby-foot

    \n\n

    \n\t\n

    \n\n\n

    La d\u00e9funte fun machine

    \n\n\n\nCette machine n'est plus. Mais elle reste dans le coeur de ceux qui \nl'ont connue. C'est pourquoi cette section n'a pas \u00e9t\u00e9 retir\u00e9e.

    Pour attaquer le cas \u00e9trange de la machine bizarre qui tra\u00eene \u00e0 c\u00f4t\u00e9, \ndisons simplement qu'elle s'appelle Monster Bash. On me souffle en r\u00e9gie\n que pour une fun machine, elle n'est pas si mal. De fait, elle t\u00e9moigne\n d'un humour d\u00e9cal\u00e9, absurde et parfois involontaire : ainsi, la \ntraduction oscille entre le path\u00e9tique et l'ignoble, en passant par le \nburlesque. Le but est de r\u00e9veiller et vaincre six monstres, parmi \nlesquels Dracula et Frankenstein, pour les asservir et les rassembler \ndans le plus grand groupe de rock de l'histoire : les \u00abmonsters of rock\u00bb\n (traduction : \u00abmonstres du rocher\u00bb). Il n'y a pas pour le moment de \ntrait\u00e9 th\u00e9orique de la Monster Bash, la jeu se r\u00e9sumant de toute fa\u00e7on \u00e0\n \u00abmoi voir, moi actionner flip\u00bb. Ce qui n'emp\u00eache pas la machine en \nquestion d'avoir son public d'habitu\u00e9-e-s, bien au contraire. \n

    " + }, + "pk": 8 +}, +{ + "model": "kfetcms.kfetpage", + "fields": { + "custom_template": "", + "no_header": false, + "content": "

    Responsable de la publication

    • Il s'agit de la pr\u00e9sidence du COF :
      Association des \u00c9l\u00e8ves de l'\u00c9cole Normale Sup\u00e9rieure
      45 rue d'Ulm
      75005 Paris

    Informations prestataires

    • L'h\u00e9bergement est fourni \u00e0 titre gracieux par le CRI :
      \u00c9cole Normale Sup\u00e9rieure

      Centre de Ressources Informatiques
      45 rue d'Ulm
      75005 Paris
    • Le d\u00e9veloppement est assur\u00e9 par COF-Geek.
    " + }, + "pk": 10 +}, +{ + "model": "kfetcms.kfetpagegroupteam", + "fields": { + "content": "

    Les ancien-ne-s Chef-fe-s K-F\u00eat doivent bien \u00eatre pr\u00e9sent\u00e9-e-s avant \nl'\u00e9quipe actuelle. C'est gr\u00e2ce \u00e0 elleux qu'elle tourne encore, gr\u00e2ce \u00e0 \nelleux qu'elle a bien tourn\u00e9, et puis, de pr\u00e8s comme de loin, illes \nveillent encore sur nous. Ce sont les diff\u00e9rentes facettes de la K-F\u00eat \nhistorique, bien que d'un certain point de vue, illes se ressemblent \ntou-te-s : les Chef-fe-s K-F\u00eat sont une dynastie, ils n'ont pas \u00e9t\u00e9 \nChef-fe-s apr\u00e8s avoir prouv\u00e9 quoi que ce soit, illes l'ont \u00e9t\u00e9 parce que\n ce r\u00f4le leur revenait de droit. On na\u00eet Chef-fe K-F\u00eat, on ne le devient\n pas. Et on le reste toujours, dans l'\u00e2me.

    ", + "sort_order": 0, + "page": 5, + "title": "Les ancien-ne-s Chef-fe-s K-F\u00eat", + "group": 3, + "show_only": 12 + }, + "pk": 1 +}, +{ + "model": "kfetcms.kfetpagegroupteam", + "fields": { + "content": "

    Le-la chef-fe K-F\u00eat, celui-celle qui a le droit de vie et de mort sur les \u00e2mes \u00e9gar\u00e9es qui fr\u00e9quentent la K-F\u00eat.

    ", + "sort_order": 1, + "page": 5, + "title": "Le chef", + "group": 1, + "show_only": null + }, + "pk": 2 +}, +{ + "model": "kfetcms.kfetpagegroupteam", + "fields": { + "content": "

    Les K-F\u00eat wo-men poss\u00e8dent les cl\u00e9s de la K-F\u00eat. Ce sont elleux qui peuvent d\u00e9cider ou non d'ouvrir la K-F\u00eat.

    ", + "sort_order": 2, + "page": 5, + "title": "Les K-F\u00eat Wo-Men", + "group": 2, + "show_only": null + }, + "pk": 3 +}, +{ + "model": "kfetcms.kfetpagegroupteam", + "fields": { + "content": "

    Les vieux-illes sont d'ancien-ne-s K-F\u00eat wo-men qui ne viennent plus \naussi souvent qu'avant, illes servent toujours, mais n'ont en g\u00e9n\u00e9ral \nplus les cl\u00e9s. Illes existent n\u00e9anmoins, et on les garde (pour \ncertain-e-s) parce qu'au fond, on les aime quand m\u00eame, et qu'en plus, \nilles en savent plus que n'importe qui sur la K-F\u00eat.

    ", + "sort_order": 3, + "page": 5, + "title": "Les Vieux-illes", + "group": 4, + "show_only": 12 + }, + "pk": 4 +}, +{ + "model": "kfetcms.groupteam", + "fields": { + "name": "Chef-fe-s" + }, + "pk": 1 +}, +{ + "model": "kfetcms.groupteam", + "fields": { + "name": "Wo-Men" + }, + "pk": 2 +}, +{ + "model": "kfetcms.groupteam", + "fields": { + "name": "Ancien-ne-s chef-fe-s" + }, + "pk": 3 +}, +{ + "model": "kfetcms.groupteam", + "fields": { + "name": "Ancien-ne-s wo-men" + }, + "pk": 4 +}, +{ + "pk": 1, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 0, + "first_name": "Hugo", + "group": 1, + "nick_name": "", + "photo": 3, + "last_name": "Manet" + } +}, +{ + "pk": 2, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 1, + "first_name": "Lisa", + "group": 1, + "nick_name": "", + "photo": 4, + "last_name": "Gourdon" + } +}, +{ + "pk": 3, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 0, + "first_name": "Pierre", + "group": 2, + "nick_name": "", + "photo": 5, + "last_name": "Quesselaire" + } +}, +{ + "pk": 4, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 1, + "first_name": "Thibault", + "group": 2, + "nick_name": "", + "photo": 6, + "last_name": "Scoquard" + } +}, +{ + "pk": 5, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 2, + "first_name": "Arnaud", + "group": 2, + "nick_name": "", + "photo": 7, + "last_name": "Fanthomme" + } +}, +{ + "pk": 6, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 3, + "first_name": "Vincent", + "group": 2, + "nick_name": "", + "photo": 8, + "last_name": "Balerdi" + } +}, +{ + "pk": 7, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 4, + "first_name": "Nathana\u00ebl", + "group": 2, + "nick_name": "", + "photo": 9, + "last_name": "Willaime" + } +}, +{ + "pk": 8, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 5, + "first_name": "\u00c9lisabeth", + "group": 2, + "nick_name": "", + "photo": 10, + "last_name": "Miller" + } +}, +{ + "pk": 9, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 6, + "first_name": "Arthur", + "group": 2, + "nick_name": "B2O", + "photo": 11, + "last_name": "Lesage" + } +}, +{ + "pk": 10, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 7, + "first_name": "Sarah", + "group": 2, + "nick_name": "", + "photo": 12, + "last_name": "Asset" + } +}, +{ + "pk": 11, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 8, + "first_name": "Alexandre", + "group": 2, + "nick_name": "", + "photo": 13, + "last_name": "Legrand" + } +}, +{ + "pk": 12, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 9, + "first_name": "\u00c9tienne", + "group": 2, + "nick_name": "", + "photo": 14, + "last_name": "Baudel" + } +}, +{ + "pk": 13, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 0, + "first_name": "Marine", + "group": 3, + "nick_name": "", + "photo": 15, + "last_name": "Snape" + } +}, +{ + "pk": 14, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 1, + "first_name": "Anatole", + "group": 3, + "nick_name": "", + "photo": 16, + "last_name": "Gosset" + } +}, +{ + "pk": 15, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 2, + "first_name": "Jacko", + "group": 3, + "nick_name": "", + "photo": 17, + "last_name": "Rastikian" + } +}, +{ + "pk": 16, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 3, + "first_name": "Alexandre", + "group": 3, + "nick_name": "", + "photo": 18, + "last_name": "Jannaud" + } +}, +{ + "pk": 17, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 4, + "first_name": "Aur\u00e9lien", + "group": 3, + "nick_name": "", + "photo": 19, + "last_name": "Delobelle" + } +}, +{ + "pk": 18, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 5, + "first_name": "Sylvain", + "group": 3, + "nick_name": "", + "photo": 20, + "last_name": "Douteau" + } +}, +{ + "pk": 19, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 6, + "first_name": "Rapha\u00ebl", + "group": 3, + "nick_name": "", + "photo": 21, + "last_name": "Lescanne" + } +}, +{ + "pk": 20, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 7, + "first_name": "Romain", + "group": 3, + "nick_name": "", + "photo": 22, + "last_name": "Gourvil" + } +}, +{ + "pk": 21, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 8, + "first_name": "Marie", + "group": 3, + "nick_name": "", + "photo": 23, + "last_name": "Labeye" + } +}, +{ + "pk": 22, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 9, + "first_name": "Oscar", + "group": 3, + "nick_name": "", + "photo": 24, + "last_name": "Blumberg" + } +}, +{ + "pk": 23, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 10, + "first_name": "Za\u00efd", + "group": 3, + "nick_name": "", + "photo": 25, + "last_name": "Allybokus" + } +}, +{ + "pk": 24, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 11, + "first_name": "Damien", + "group": 3, + "nick_name": "", + "photo": 26, + "last_name": "Garreau" + } +}, +{ + "pk": 25, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 12, + "first_name": "Andr\u00e9a", + "group": 3, + "nick_name": "", + "photo": 27, + "last_name": "Londonez-Lopez" + } +}, +{ + "pk": 26, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 13, + "first_name": "Tristan", + "group": 3, + "nick_name": "", + "photo": 28, + "last_name": "Roussel" + } +}, +{ + "pk": 27, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 14, + "first_name": "Guillaume", + "group": 3, + "nick_name": "", + "photo": 29, + "last_name": "Vernade" + } +}, +{ + "pk": 28, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 15, + "first_name": "Lucas", + "group": 3, + "nick_name": "", + "photo": 30, + "last_name": "Mercier" + } +}, +{ + "pk": 29, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 16, + "first_name": "Fran\u00e7ois", + "group": 3, + "nick_name": "M\u00e9talleux", + "photo": 31, + "last_name": "Maillot" + } +}, +{ + "pk": 30, + "model": "kfetcms.memberteam", + "fields": { + "sort_order": 17, + "first_name": "Fabrice", + "group": 3, + "nick_name": "", + "photo": 32, + "last_name": "Catoire" + } +}, +{ + "model": "wagtaildocs.document", + "pk": 1, + "fields": { + "created_at": "2017-05-30T04:20:00.000Z", + "uploaded_by_user": [ + "root" + ], + "collection": 1, + "title": "K-F\u00eat - Plan d'acc\u00e8s", + "file": "documents/kfet_acces.pdf" + } +}, +{ + "model": "wagtaildocs.document", + "pk": 2, + "fields": { + "created_at": "2017-05-30T04:20:00.000Z", + "uploaded_by_user": [ + "root" + ], + "collection": 1, + "title": "K-F\u00eat - Demande d'autorisation", + "file": "documents/kfet_autorisation.pdf" + } +}, +{ + "model": "wagtaildocs.document", + "pk": 3, + "fields": { + "created_at": "2017-05-30T04:20:00.000Z", + "uploaded_by_user": [ + "root" + ], + "collection": 1, + "title": "K-F\u00eat - Trait\u00e9 de Flipper Th\u00e9orique", + "file": "documents/kfet_flipper.pdf" + } +}, +{ + "model": "wagtailimages.image", + "pk": 1, + "fields": { + "created_at": "2017-05-30T04:20:00.000Z", + "focal_point_width": null, + "height": 300, + "file": "original_images/kfet_amazon.jpg", + "collection": 1, + "focal_point_x": null, + "file_size": null, + "focal_point_height": null, + "focal_point_y": null, + "title": "K-F\u00eat - Amazon Hunt", + "width": 200, + "uploaded_by_user": [ + "root" + ] + } +}, +{ + "model": "wagtailimages.image", + "pk": 2, + "fields": { + "created_at": "2017-05-30T04:20:00.000Z", + "focal_point_width": null, + "height": 300, + "file": "original_images/kfet_funmachine.jpg", + "collection": 1, + "focal_point_x": null, + "file_size": null, + "focal_point_height": null, + "focal_point_y": null, + "title": "K-F\u00eat - Fun Machine", + "width": 200, + "uploaded_by_user": [ + "root" + ] + } +}, +{ + "fields": { + "width": 3020, + "file_size": null, + "file": "original_images/hugo_manet.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 3020, + "focal_point_width": null, + "focal_point_y": null, + "title": "Hugo Manet", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 3 +}, +{ + "fields": { + "width": 1566, + "file_size": null, + "file": "original_images/lisa_gourdon.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 1634, + "focal_point_width": null, + "focal_point_y": null, + "title": "Lisa Gourdon", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 4 +}, +{ + "fields": { + "width": 117, + "file_size": null, + "file": "original_images/pierre_quesselaire.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 153, + "focal_point_width": null, + "focal_point_y": null, + "title": "Pierre Quesselaire", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 5 +}, +{ + "fields": { + "width": 606, + "file_size": null, + "file": "original_images/thibault_scoquart.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 487, + "focal_point_width": null, + "focal_point_y": null, + "title": "Thibault Scoquard", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 6 +}, +{ + "fields": { + "width": 640, + "file_size": null, + "file": "original_images/arnaud_fanthomme.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 320, + "focal_point_width": null, + "focal_point_y": null, + "title": "Arnaud Fanthomme", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 7 +}, +{ + "fields": { + "width": 125, + "file_size": null, + "file": "original_images/vincent_balerdi.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 163, + "focal_point_width": null, + "focal_point_y": null, + "title": "Vincent Balerdi", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 8 +}, +{ + "fields": { + "width": 125, + "file_size": null, + "file": "original_images/nathanel_willaime.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 176, + "focal_point_width": null, + "focal_point_y": null, + "title": "Nathana\u00ebl Willaime", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 9 +}, +{ + "fields": { + "width": 125, + "file_size": null, + "file": "original_images/elisabeth_miller.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 146, + "focal_point_width": null, + "focal_point_y": null, + "title": "\u00c9lisabeth Miller", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 10 +}, +{ + "fields": { + "width": 720, + "file_size": null, + "file": "original_images/arthur_lesage.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 720, + "focal_point_width": null, + "focal_point_y": null, + "title": "Arthur Lesage", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 11 +}, +{ + "fields": { + "width": 445, + "file_size": null, + "file": "original_images/sarah_asset.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 436, + "focal_point_width": null, + "focal_point_y": null, + "title": "Sarah Asset", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 12 +}, +{ + "fields": { + "width": 480, + "file_size": null, + "file": "original_images/alexandre_legrand.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 360, + "focal_point_width": null, + "focal_point_y": null, + "title": "Alexandre Legrand", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 13 +}, +{ + "fields": { + "width": 4608, + "file_size": null, + "file": "original_images/etienne_baudel.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 3456, + "focal_point_width": null, + "focal_point_y": null, + "title": "\u00c9tienne Baudel", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 14 +}, +{ + "fields": { + "width": 358, + "file_size": null, + "file": "original_images/marine_snape.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 435, + "focal_point_width": null, + "focal_point_y": null, + "title": "Marine Snape", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 15 +}, +{ + "fields": { + "width": 121, + "file_size": null, + "file": "original_images/anatole_gosset.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 200, + "focal_point_width": null, + "focal_point_y": null, + "title": "Anatole Gosset", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 16 +}, +{ + "fields": { + "width": 253, + "file_size": null, + "file": "original_images/jacko_rastikian.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 338, + "focal_point_width": null, + "focal_point_y": null, + "title": "Jacko Rastikian", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 17 +}, +{ + "fields": { + "width": 285, + "file_size": null, + "file": "original_images/alexandre_jannaud.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 380, + "focal_point_width": null, + "focal_point_y": null, + "title": "Alexandre Jannaud", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 18 +}, +{ + "fields": { + "width": 283, + "file_size": null, + "file": "original_images/aurelien_delobelle.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 371, + "focal_point_width": null, + "focal_point_y": null, + "title": "Aur\u00e9lien Delobelle", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 19 +}, +{ + "fields": { + "width": 125, + "file_size": null, + "file": "original_images/sylvain_douteau.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 161, + "focal_point_width": null, + "focal_point_y": null, + "title": "Sylvain Douteau", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 20 +}, +{ + "fields": { + "width": 125, + "file_size": null, + "file": "original_images/raphael_lescanne.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 176, + "focal_point_width": null, + "focal_point_y": null, + "title": "Rapha\u00ebl Lescanne", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 21 +}, +{ + "fields": { + "width": 124, + "file_size": null, + "file": "original_images/romain_gourvil.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 157, + "focal_point_width": null, + "focal_point_y": null, + "title": "Romain Gourvil", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 22 +}, +{ + "fields": { + "width": 133, + "file_size": null, + "file": "original_images/marie_labeye.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 163, + "focal_point_width": null, + "focal_point_y": null, + "title": "Marie Labeye", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 23 +}, +{ + "fields": { + "width": 127, + "file_size": null, + "file": "original_images/oscar_blumberg.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 159, + "focal_point_width": null, + "focal_point_y": null, + "title": "Oscar Blumberg", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 24 +}, +{ + "fields": { + "width": 210, + "file_size": null, + "file": "original_images/zaid_allybokus.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 311, + "focal_point_width": null, + "focal_point_y": null, + "title": "Za\u00efd Allybokus", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 25 +}, +{ + "fields": { + "width": 495, + "file_size": null, + "file": "original_images/damien_garreau.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 548, + "focal_point_width": null, + "focal_point_y": null, + "title": "Damien Garreau", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 26 +}, +{ + "fields": { + "width": 323, + "file_size": null, + "file": "original_images/andrea_londono.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 458, + "focal_point_width": null, + "focal_point_y": null, + "title": "Andr\u00e9a Londono-Lopez", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 27 +}, +{ + "fields": { + "width": 120, + "file_size": null, + "file": "original_images/tristan_roussel.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 155, + "focal_point_width": null, + "focal_point_y": null, + "title": "Tristan Roussel", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 28 +}, +{ + "fields": { + "width": 427, + "file_size": null, + "file": "original_images/guillaume_vernade.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 640, + "focal_point_width": null, + "focal_point_y": null, + "title": "Guillaume Vernade", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 29 +}, +{ + "fields": { + "width": 2304, + "file_size": null, + "file": "original_images/lucas_mercier.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 3020, + "focal_point_width": null, + "focal_point_y": null, + "title": "Lucas Mercier", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 30 +}, +{ + "fields": { + "width": 199, + "file_size": null, + "file": "original_images/francois_maillot.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 240, + "focal_point_width": null, + "focal_point_y": null, + "title": "Fran\u00e7ois Maillot", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 31 +}, +{ + "fields": { + "width": 965, + "file_size": null, + "file": "original_images/fabrice_catoire.jpg", + "focal_point_height": null, + "focal_point_x": null, + "height": 1255, + "focal_point_width": null, + "focal_point_y": null, + "title": "Fabrice Catoire", + "collection": 1, + "uploaded_by_user": [ + "root" + ], + "created_at": "2017-05-30T04:20:00.000Z" + }, + "model": "wagtailimages.image", + "pk": 32 +}, +{ + "pk": 1, + "fields": { + "site": [ + "localhost", + 8000 + ], + "use_specific": 1, + "max_levels": 2 + }, + "model": "wagtailmenus.mainmenu" +}, +{ + "pk": 1, + "fields": { + "link_page": 3, + "menu": 1, + "allow_subnav": true, + "handle": "", + "link_text": "Accueil", + "link_url": "", + "sort_order": 0, + "url_append": "" + }, + "model": "wagtailmenus.mainmenuitem" +}, +{ + "pk": 2, + "fields": { + "link_page": 4, + "menu": 1, + "allow_subnav": true, + "handle": "", + "link_text": "", + "link_url": "", + "sort_order": 1, + "url_append": "" + }, + "model": "wagtailmenus.mainmenuitem" +}, +{ + "pk": 3, + "fields": { + "link_page": 5, + "menu": 1, + "allow_subnav": true, + "handle": "", + "link_text": "", + "link_url": "", + "sort_order": 2, + "url_append": "" + }, + "model": "wagtailmenus.mainmenuitem" +}, +{ + "pk": 4, + "fields": { + "link_page": 6, + "menu": 1, + "allow_subnav": true, + "handle": "", + "link_text": "", + "link_url": "", + "sort_order": 3, + "url_append": "" + }, + "model": "wagtailmenus.mainmenuitem" +}, +{ + "pk": 5, + "fields": { + "link_page": 7, + "menu": 1, + "allow_subnav": true, + "handle": "", + "link_text": "", + "link_url": "", + "sort_order": 4, + "url_append": "" + }, + "model": "wagtailmenus.mainmenuitem" +}, +{ + "pk": 6, + "fields": { + "link_page": 8, + "menu": 1, + "allow_subnav": true, + "handle": "", + "link_text": "", + "link_url": "", + "sort_order": 5, + "url_append": "" + }, + "model": "wagtailmenus.mainmenuitem" +} +] diff --git a/kfet/cms/management/commands/kfet_loadwagtail.py b/kfet/cms/management/commands/kfet_loadwagtail.py new file mode 100644 index 00000000..86b94d3e --- /dev/null +++ b/kfet/cms/management/commands/kfet_loadwagtail.py @@ -0,0 +1,35 @@ +from django.contrib.auth.models import Group +from django.core.management import call_command +from django.core.management.base import BaseCommand + +from wagtail.wagtailcore.models import Page, Site + + +class Command(BaseCommand): + help = "Importe des données pour Wagtail" + + def add_arguments(self, parser): + parser.add_argument('--file', default='kfet_wagtail_17_05') + + def handle(self, *args, **options): + + self.stdout.write("Import des données wagtail") + + # Nettoyage des données initiales posées par Wagtail dans la migration + # wagtailcore/0002 + + Group.objects.filter(name__in=('Moderators', 'Editors')).delete() + + try: + homepage = Page.objects.get( + title="Welcome to your new Wagtail site!" + ) + homepage.delete() + Site.objects.filter(root_page=homepage).delete() + except Page.DoesNotExist: + pass + + # Import des données + # Par défaut, il s'agit d'une copie du site K-Fêt (17-05) + + call_command('loaddata', options['file']) diff --git a/kfet/cms/migrations/0001_initial.py b/kfet/cms/migrations/0001_initial.py new file mode 100644 index 00000000..6798aecc --- /dev/null +++ b/kfet/cms/migrations/0001_initial.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import modelcluster.fields +import wagtail.wagtailcore.fields +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0033_remove_golive_expiry_help_text'), + ('wagtailimages', '0019_delete_filter'), + ] + + operations = [ + migrations.CreateModel( + name='GroupTeam', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), + ('name', models.CharField(max_length=255, verbose_name='Nom')), + ], + options={ + 'verbose_name': 'Groupe de K-Fêt-eux-ses', + 'verbose_name_plural': 'Groupes de K-Fêt-eux-ses', + }, + ), + migrations.CreateModel( + name='KFetPage', + fields=[ + ('page_ptr', models.OneToOneField(primary_key=True, to='wagtailcore.Page', parent_link=True, auto_created=True, serialize=False)), + ('no_header', models.BooleanField(verbose_name='Sans en-tête', help_text="Coché, l'en-tête (avec le titre) de la page n'est pas affiché.", default=False)), + ('content', wagtail.wagtailcore.fields.RichTextField(verbose_name='Contenu')), + ('custom_template', models.CharField(max_length=255, verbose_name='Template personnalisé', blank=True)), + ], + options={ + 'verbose_name': 'page K-Fêt', + 'verbose_name_plural': 'pages K-Fêt', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='KFetPageGroupTeam', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), + ('sort_order', models.IntegerField(editable=False, null=True, blank=True)), + ('title', models.CharField(max_length=255, verbose_name='Titre du groupe', blank=True)), + ('content', wagtail.wagtailcore.fields.RichTextField(verbose_name='Texte de présentation du groupe')), + ('group', models.ForeignKey(related_name='+', verbose_name='Groupe de K-Fêt-eux-ses', to='kfetcms.GroupTeam')), + ('page', modelcluster.fields.ParentalKey(related_name='team_groups', to='kfetcms.KFetPage')), + ('show_only', models.IntegerField(default=None, verbose_name='Montrer seulement', blank=True, null=True, help_text='Nombre de membres du groupe affichés initialement. Laisser vide pour tou-te-s les afficher.')), + + ], + options={ + 'abstract': False, + 'ordering': ['sort_order'], + }, + ), + migrations.CreateModel( + name='MemberTeam', + fields=[ + ('id', models.AutoField(primary_key=True, verbose_name='ID', auto_created=True, serialize=False)), + ('sort_order', models.IntegerField(editable=False, null=True, blank=True)), + ('first_name', models.CharField(max_length=255, verbose_name='Prénom', blank=True, default='')), + ('last_name', models.CharField(max_length=255, verbose_name='Nom', blank=True, default='')), + ('nick_name', models.CharField(max_length=255, verbose_name='Alias', blank=True, default='')), + ('group', modelcluster.fields.ParentalKey(related_name='members', verbose_name='Groupe de K-Fêt-eux-ses', to='kfetcms.GroupTeam')), + ('photo', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, blank=True, verbose_name='Photo', to='wagtailimages.Image', null=True)), + ], + options={ + 'verbose_name': 'K-Fêt-eux-se', + }, + ), + ] diff --git a/kfet/cms/migrations/__init__.py b/kfet/cms/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kfet/cms/models.py b/kfet/cms/models.py new file mode 100644 index 00000000..8b80f175 --- /dev/null +++ b/kfet/cms/models.py @@ -0,0 +1,165 @@ +from django.db import models +from django.utils.translation import ugettext_lazy as _ + +from modelcluster.models import ClusterableModel, ParentalKey +from wagtail.wagtailadmin.edit_handlers import FieldPanel, InlinePanel +from wagtail.wagtailcore.fields import RichTextField +from wagtail.wagtailcore.models import Orderable, Page +from wagtail.wagtailimages.edit_handlers import ImageChooserPanel +from wagtail.wagtailsnippets.edit_handlers import SnippetChooserPanel +from wagtail.wagtailsnippets.models import register_snippet + +from kfet.cms.context_processors import get_articles + + +class KFetPage(Page): + no_header = models.BooleanField( + verbose_name=_('Sans en-tête'), + default=False, + help_text=_( + "Coché, l'en-tête (avec le titre) de la page n'est pas affiché." + ), + ) + content = RichTextField(verbose_name=_('Contenu')) + custom_template = models.CharField( + verbose_name=_('Template personnalisé'), + max_length=255, + blank=True, + ) + + content_panels = Page.content_panels + [ + FieldPanel('no_header'), + FieldPanel('content', classname='full'), + InlinePanel('team_groups', label=_("Groupes de K-Fêt-eux-ses")), + ] + + settings_panels = Page.settings_panels + [ + FieldPanel('custom_template'), + ] + + class Meta: + verbose_name = _('page K-Fêt') + verbose_name_plural = _('pages K-Fêt') + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.template = "kfetcms/base.html" + + def get_context(self, request, *args, **kwargs): + context = super().get_context(request, *args, **kwargs) + + page = context['page'] + + if not page.seo_title: + page.seo_title = page.title + + if self.slug == "carte": + context.update(get_articles(request)) + + return context + + def get_template(self, request, *args, **kwargs): + return self.custom_template or ( + super().get_template(request, *args, **kwargs)) + + +class KFetPageGroupTeam(Orderable, models.Model): + page = ParentalKey(KFetPage, related_name='team_groups') + group = models.ForeignKey( + 'kfetcms.GroupTeam', + verbose_name=_('Groupe de K-Fêt-eux-ses'), + related_name='+', + ) + title = models.CharField( + verbose_name=_('Titre du groupe'), + max_length=255, + blank=True, + ) + content = RichTextField( + verbose_name=_('Texte de présentation du groupe'), + ) + show_only = models.IntegerField( + verbose_name=_('Montrer seulement'), + blank=True, null=True, default=None, + help_text=_( + 'Nombre de membres du groupe affichés initialement. Laisser vide ' + 'pour tou-te-s les afficher.' + ), + ) + + panels = [ + FieldPanel('title', classname='full'), + FieldPanel('show_only', classname='full'), + FieldPanel('content', classname='full'), + SnippetChooserPanel('group'), + ] + + +@register_snippet +class GroupTeam(ClusterableModel): + name = models.CharField( + verbose_name=_('Nom'), + max_length=255, + ) + + class Meta: + verbose_name = _('Groupe de K-Fêt-eux-ses') + verbose_name_plural = _('Groupes de K-Fêt-eux-ses') + + def __str__(self): + return self.name + + panels = [ + FieldPanel('name', classname='full'), + InlinePanel('members', label=_('Membres du groupe')), + ] + + +@register_snippet +class MemberTeam(Orderable, models.Model): + group = ParentalKey( + GroupTeam, + verbose_name=_("Groupe de K-Fêt-eux-ses"), + on_delete=models.CASCADE, + related_name='members', + ) + first_name = models.CharField( + verbose_name=_('Prénom'), + max_length=255, + blank=True, default='', + ) + last_name = models.CharField( + verbose_name=_('Nom'), + max_length=255, + blank=True, default='', + ) + nick_name = models.CharField( + verbose_name=_('Alias'), + max_length=255, + blank=True, default='', + ) + photo = models.ForeignKey( + 'wagtailimages.Image', + verbose_name=_('Photo'), + on_delete=models.SET_NULL, + null=True, blank=True, + related_name='+', + ) + + class Meta: + verbose_name = _('K-Fêt-eux-se') + + def __str__(self): + return self.get_full_name() + + panels = [ + FieldPanel('first_name'), + FieldPanel('last_name'), + FieldPanel('nick_name'), + FieldPanel('group'), + ImageChooserPanel('photo'), + ] + + def get_full_name(self): + full_name = '{} {}'.format(self.first_name, self.last_name) + return full_name.strip() diff --git a/kfet/cms/static/kfetcms/css/index.css b/kfet/cms/static/kfetcms/css/index.css new file mode 100644 index 00000000..ff175df6 --- /dev/null +++ b/kfet/cms/static/kfetcms/css/index.css @@ -0,0 +1,155 @@ +.cms-content { + text-align: justify; + font-size: 1.1em; +} + +@media (min-width:768px) { + .cms-content { + font-size: 1.2em; + line-height: 1.6em; + } +} + +.cms-column { + column-gap: 45px; + + padding: 20px 15px; + background: white; +} + +@media (min-width: 768px) { + .cms-column { + padding: 35px 30px; + } +} + +@media (min-width: 992px) { + .cms-column { + margin: 0 15px; + } +} + + +/* Titles */ + +.cms-content h2, .cms-content h3 { + clear: both; + margin: 0 0 15px; + padding-bottom: 10px; + border-bottom: 1px solid #c8102e; + text-align: left; + font-weight: bold; +} + +@media (min-width: 768px) { + .cms-content h2, .cms-content h3 { + padding-bottom: 15px; + } +} + +/* Paragraphs */ + +.cms-content p { + margin-bottom: 20px; + text-indent: 2em; +} + +.cms-content p + :not(h2):not(h3):not(div) { + margin-top: -10px; +} + +@media (min-width: 768px) { + .cms-content p { + padding-bottom: 15px; + } + + .cms-content p + :not(h2):not(h3):not(div) { + margin-top: -30px; + } +} + + +/* Lists */ + +.cms-content ol, .cms-content ul { + padding: 0 0 0 15px; + margin: 0 0 10px; +} + +.cms-content ul { + list-style-type: square; +} + +.cms-content ol > li, .cms-content ul > li { + padding-left: 5px; +} + + +/* Images */ + +.cms-content .richtext-image { + max-height: 100%; + margin: 5px 0 15px; +} + +.cms-content .richtext-image.left { + float: left; + margin-right: 30px; +} + +.cms-content .richtext-image.right { + float: right; + margin-left: 30px; +} + + +/* Team groups & members */ + +.team-group { + margin-bottom: 20px; +} + +.team-group .col-btn { + margin-bottom: 20px; +} + +.team-group .member-more { + display: none; +} + +.team-member { + padding: 0; + margin-bottom: 20px; + min-height: 190px; + background-color: inherit; + border: 0; +} + +.team-member img { + max-width: 100%; + max-height: 125px; + width: auto; + height: auto; + display: block; +} + +.team-member .infos { + height: 50px; + margin-top: 15px; +} + +@media (min-width: 768px) { + .team-group { + margin-left: 20px; + margin-right: 20px; + } + + .team-member { + min-height: 215px; + } + + .team-member img { + max-height: 150px; + } +} + diff --git a/kfet/cms/templates/kfetcms/base.html b/kfet/cms/templates/kfetcms/base.html new file mode 100644 index 00000000..043c9ba5 --- /dev/null +++ b/kfet/cms/templates/kfetcms/base.html @@ -0,0 +1,68 @@ +{% extends "kfet/base.html" %} +{% load static %} +{% load wagtailuserbar %} +{% load wagtailcore_tags %} + +{% block extra_head %} + +{% endblock %} + +{% block title %}{{ page.seo_title }}{% endblock %} + +{% block header-class %}text-center{% endblock %} +{% block header-title %}{{ page.title }}{% endblock %} + +{% block content %} + +
    + {% include "kfet/base_messages.html" %} +
    + +
    +
    +
    + {% block block1-content %} + {% endblock %} + + {% block block2-content %} + {{ page.content|richtext }} + {% endblock %} + + {% block block3-content %} + {% endblock %} +
    +
    +
    + +{% wagtailuserbar %} + + + +{% endblock %} + +{% block footer %} +{% include "kfet/base_footer.html" %} +{% endblock %} diff --git a/kfet/cms/templates/kfetcms/carte.html b/kfet/cms/templates/kfetcms/carte.html new file mode 100644 index 00000000..3a32624b --- /dev/null +++ b/kfet/cms/templates/kfetcms/carte.html @@ -0,0 +1,46 @@ +{% extends "kfetcms/base.html" %} +{% load static %} +{% load kfet_tags %} + +{% block extra_head %} +{{ block.super }} + +{% endblock %} + +{% block col-size %}column-sm-2 column-md-3{% endblock %} + +{% block block3-content %} + +{% if pressions %} +
    +

    Pressions du moment

    +
      + {% for article in pressions %} +
    • +
      + {{ article.name }} + {{ article.price | ukf:False}} UKF +
    • + {% endfor %} +
    +
    +{% endif %} + +{% regroup articles by category as categories %} + +{% for category in categories %} +
    +

    {{ category.grouper.name }}

    +
      + {% for article in category.list %} +
    • +
      + {{ article.name }} + {{ article.price | ukf:False}} UKF +
    • + {% endfor %} +
    +
    +{% endfor %} + +{% endblock %} diff --git a/kfet/cms/templates/kfetcms/equipe.html b/kfet/cms/templates/kfetcms/equipe.html new file mode 100644 index 00000000..2295957f --- /dev/null +++ b/kfet/cms/templates/kfetcms/equipe.html @@ -0,0 +1,67 @@ +{% extends "kfetcms/base.html" %} +{% load wagtailcore_tags %} +{% load wagtailimages_tags %} + +{% block block1-content %} + +{% for group_block in page.team_groups.all %} +

    {{ group_block.title }}

    +
    + {{ group_block.content|richtext }} +
    + +{% with members=group_block.group.members.all %} +{% with len=members|length %} + +{% if len > 0 %} +
    + + {% if len == 2 %} +
    + {% endif %} + + {% for member in members %} +
    +
    + {% image member.photo max-200x500 %} +
    + {{ member.get_full_name }} +
    + {% if member.nick_name %} + alias {{ member.nick_name }} + {% endif %} +
    +
    +
    + {% endfor %} + + {% if group_block.show_only != None and len > group_block.show_only %} +
    + +
    + {% endif %} + +
    +{% endif %} + +{% endwith %} +{% endwith %} + +{% endfor %} + + + +{% endblock %} diff --git a/kfet/management/commands/loadkfetdevdata.py b/kfet/management/commands/loadkfetdevdata.py index 7f2ec9a3..6dd25f29 100644 --- a/kfet/management/commands/loadkfetdevdata.py +++ b/kfet/management/commands/loadkfetdevdata.py @@ -147,3 +147,9 @@ class Command(MyBaseCommand): # --- call_command('createopes', '100', '7', '--transfers=20') + + # --- + # Wagtail CMS + # --- + + call_command('kfet_loadwagtail') diff --git a/kfet/models.py b/kfet/models.py index d218dd2d..eb33daf7 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -78,7 +78,7 @@ class Account(models.Model): def __str__(self): return '%s (%s)' % (self.trigramme, self.name) - # Propriétés pour accéder aux attributs de user et cofprofile et user + # Propriétés pour accéder aux attributs de cofprofile et user @property def user(self): return self.cofprofile.user @@ -120,6 +120,14 @@ class Account(models.Model): def need_comment(self): return self.trigramme == '#13' + @property + def readable(self): + return self.trigramme != 'GNR' + + @property + def is_team(self): + return self.has_perm('kfet.is_team') + @staticmethod def is_validandfree(trigramme): data = { 'is_valid' : False, 'is_free' : False } diff --git a/kfet/signals.py b/kfet/signals.py index e81f264a..6bbbbbb0 100644 --- a/kfet/signals.py +++ b/kfet/signals.py @@ -4,6 +4,7 @@ from django.contrib import messages from django.contrib.auth.signals import user_logged_in from django.core.urlresolvers import reverse from django.dispatch import receiver +from django.utils.html import mark_safe @receiver(user_logged_in) @@ -11,8 +12,9 @@ def messages_on_login(sender, request, user, **kwargs): if (not user.username == 'kfet_genericteam' and user.has_perm('kfet.is_team') and 'k-fet' in request.GET.get('next', '')): - messages.info( - request, - ('Connexion en utilisateur partagé ?' - .format(reverse('kfet.login.genericteam'))), - extra_tags='safe') + messages.info(request, mark_safe( + '' + ' Connexion en utilisateur partagé ?' + '' + .format(reverse('kfet.login.genericteam')) + )) diff --git a/kfet/static/kfet/css/footer.css b/kfet/static/kfet/css/footer.css new file mode 100644 index 00000000..5e8d1474 --- /dev/null +++ b/kfet/static/kfet/css/footer.css @@ -0,0 +1,19 @@ +.footer { + line-height: 40px; + + background: #c63b52; + color: white; + + text-align: center; + font-size: 14px; + font-family: Roboto; +} + +.footer a { + color: inherit; +} + +.footer a:hover, .footer a:focus { + color: inherit; + text-decoration: underline; +} diff --git a/kfet/static/kfet/css/history.css b/kfet/static/kfet/css/history.css index dff7a455..9cd4cd28 100644 --- a/kfet/static/kfet/css/history.css +++ b/kfet/static/kfet/css/history.css @@ -9,9 +9,10 @@ #history .day { height:40px; line-height:40px; - background-color:rgba(200,16,46,0.9); + background-color:rgba(200,16,46,1); color:#fff; padding-left:20px; + font-family:"Roboto Slab"; font-size:16px; font-weight:bold; position:sticky; @@ -22,7 +23,7 @@ #history .opegroup { height:30px; line-height:30px; - background-color:rgba(200,16,46,0.75); + background-color: #c63b52; color:#fff; font-weight:bold; padding-left:20px; diff --git a/kfet/static/kfet/css/home.css b/kfet/static/kfet/css/home.css index 718159c3..f1952c69 100644 --- a/kfet/static/kfet/css/home.css +++ b/kfet/static/kfet/css/home.css @@ -1,54 +1,58 @@ -ul.carte { +.carte { + margin-bottom: 15px; + font-family: "Roboto Slab"; +} + +.carte .carte-title { + padding-top: 0; + margin-top: 0; + margin-bottom: 0; +} + +.carte .carte-list { width: 100%; + padding: 15px; list-style-type: none; - padding-left: 15px; - padding-right: 15px; - display: inline-block; - *display: inline; - zoom: 1; - position: relative; - clip: auto; - overflow: hidden; } -/* -ul.carte > li { - border-style: none none solid none; - border-width: 1px; - border-color: #DDD; -} -*/ -li.carte-line { + +.carte .carte-item { position: relative; text-align: right; white-space: nowrap; + padding: 0; } -.filler { + +.carte .carte-item .filler { position: absolute; left: 0; right: 0; - border-bottom: 3px dotted #333; - height: 70%; + border-bottom: 2px dotted #333; + height: 75%; } -.carte-label { + +.carte .carte-item > span { + position: relative; +} + +.carte .carte-item .carte-label { background: white; float: left; - padding-right: 4px; - position: relative; - max-width: calc(100% - 40px); - overflow: hidden; + padding-right: 10px; text-overflow: ellipsis; + overflow: hidden; white-space: nowrap; } -.carte-ukf { +.carte .carte-item .carte-ukf { + padding: 0 10px; + background: #ffdbc7; +} + +.carte-inverted .carte-list, +.carte-inverted .carte-item .carte-label { + background: #ffdbc7; +} + +.carte-inverted .carte-item .carte-ukf { background: white; - padding-left: 4px; - position: relative; -} - - -.unbreakable.carte-inverted .carte-ukf, -.unbreakable.carte-inverted .carte-label, -.unbreakable.carte-inverted { - background: #FFDBC7; } diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 15b425e2..04fbaeb1 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -1,4 +1,5 @@ @import url("nav.css"); +@import url("footer.css"); @import url("kpsul.css"); @import url("jconfirm-kfet.css"); @import url("history.css"); @@ -10,11 +11,11 @@ body { } h1,h2,h3,h4,h5,h6 { - font-family:Oswald; + font-family:"Roboto Slab"; } a { - color:#333; + color:#C8202E; } a:focus, a:hover { @@ -30,6 +31,10 @@ textarea { border-radius:0 !important; } +.glyphicon + span, span + .glyphicon { + margin-left: 10px; +} + .table { margin-bottom:0; border-bottom:1px solid #ddd; @@ -57,7 +62,7 @@ textarea { } .table tr.section { - background:rgba(200,16,46,0.9); + background:#c8102e; color:#fff; font-weight:bold; } @@ -68,14 +73,26 @@ textarea { padding:8px 30px; } +.table-hover > tbody > tr.section:hover { + background:#c8102e; +} + .table-responsive { border: 0; margin-bottom: 0; } .btn { + border: 0; + transition: background-color, color; transition-duration: 0.15s; + + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + font-family: "Roboto Slab"; } .btn, .btn-lg, .btn-group-lg>.btn { @@ -83,8 +100,7 @@ textarea { } .btn-primary { - font-family:Oswald; - background-color:rgba(200,16,46,0.9); + background-color:#c63b52; color:#FFF; border:0; } @@ -95,7 +111,8 @@ textarea { .btn-primary:active.focus, .btn-primary:active:focus, .btn-primary:active:hover, .nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover { outline: 0; - background-color:rgba(200,16,46,1); + background-color:#bf0f2c; + background-color:#c8102e; color:#FFF; } @@ -115,17 +132,15 @@ textarea { color:#FFF; } -.row-page-header { +.header-row { background-color:rgba(200,16,46,1); color:#FFF; - border-bottom:3px solid #000; } .page-header { border:0; padding:0; margin:15px 20px; - text-transform:uppercase; font-weight:bold; } @@ -137,41 +152,49 @@ textarea { padding:0; } + @media (min-width: 768px) { .col-content-left { position: sticky; top:50px; + margin-bottom: 15px; } } .content-left-top { background:#fff; - padding:10px 30px; + padding:15px; } -.content-left .buttons { - background:#fff; +@media (min-width: 1200px) { + .content-left-top { + padding: 30px; + } } -.content-left .buttons .btn { - display:block; +.content-left .btn-lg { + font-size: 16px; } -.content-left .buttons ul.nav-pills { - margin-bottom:5px; +.btn-actions { + margin: 0 -15px; } -.content-left .buttons ul.nav-pills li { - margin:0 0 -5px; +.btn-actions .btn { + color: inherit; +} + +.btn-actions .btn:not([disabled]):hover, .btn-actions .btn:not([disabled]):focus { + color: #c8102e; } .content-left-top.frozen-account { - background:#000FBA; + background:#5072e0; color:#fff; } .content-left .block { - padding-top:25px; + padding-top:15px; } .content-left .block .line { @@ -180,10 +203,11 @@ textarea { } .content-left .line.line-big { - font-family:Oswald; + font-family:"Roboto Slab"; font-size:60px; font-weight:bold; text-align:center; + overflow:hidden; } .content-left .line.line-bigsub { @@ -197,6 +221,12 @@ textarea { text-align:center; } +@media (min-width: 1200px) { + .content-left .line.line-big { + margin-top: -15px; + } +} + @media (min-width: 768px) { .content-right { margin: 15px; @@ -204,6 +234,7 @@ textarea { } .content-right-block { + margin-top: 15px; position:relative; } @@ -239,6 +270,15 @@ textarea { font-size:25px; } +.content-right-block a:not(.btn) { + color:#000; +} + +.content-right-block a:not(.btn):focus , +.content-right-block a:not(.btn):hover { + color:#C81022; +} + /* * Pages tableaux seuls */ @@ -251,6 +291,11 @@ textarea { .content-center { margin: 15px 0; } + + .column-row { + margin-top: 15px; + margin-bottom: 15px; + } } .content-center tbody tr:not(.section) td { @@ -261,6 +306,11 @@ textarea { padding: 1px 12px ; height:28px; margin:3px 0px; + background: transparent; +} + +.table .form-control[disabled], .table .form-control[readonly] { + background: #f5f5f5; } .table-condensed input.form-control { @@ -349,10 +399,6 @@ textarea { * Messages */ -.messages { - margin: 0; -} - .messages .alert { padding:10px 15px; margin:0; @@ -360,10 +406,6 @@ textarea { border-radius:0; } -.messages .alert-dismissible { - padding-right:35px; -} - .messages .alert .close { top:0; right:0; @@ -375,12 +417,18 @@ textarea { } .messages .alert-error { - color:inherit; - background-color:rgba(200,16,46,0.2); + color: white; + background-color: #c63b52; } .messages .alert-success { - color:#333; + color: white; + background: #3d9947; +} + +.messages a { + font-weight: bold; + text-decoration: none; } /* @@ -403,7 +451,7 @@ textarea { margin-top:30px; padding-top:1px; padding-bottom:15px; - background:rgba(51,51,51,0.7); + background:rgba(51,51,51,0.9); color:#fff; } @@ -449,145 +497,43 @@ thead .tooltip { width: 100%; } - -.column-row { - padding: 15px 20px; -} - -.column-xs-1, -.column-sm-1, -.column-md-1, -.column-lg-1, -.column-xs-2, -.column-sm-2, -.column-md-2, -.column-lg-2, -.column-xs-3, -.column-sm-3, -.column-md-3, -.column-lg-3, -.column-xs-4, -.column-sm-4, -.column-md-4, -.column-lg-4, -.column-xs-5, -.column-sm-5, -.column-md-5, -.column-lg-5 { - -webkit-column-count: 1; /* Chrome, Safari, Opera */ - -moz-column-count: 1; /* Firefox */ +.column-xs-1, .column-sm-1, .column-md-1, .column-lg-1, +.column-xs-2, .column-sm-2, .column-md-2, .column-lg-2, +.column-xs-3, .column-sm-3, .column-md-3, .column-lg-3, +.column-xs-4, .column-sm-4, .column-md-4, .column-lg-4, +.column-xs-5, .column-sm-5, .column-md-5, .column-lg-5 { column-count: 1; + column-gap: 0; } - -.column-xs-1 { - -webkit-column-count: 1; /* Chrome, Safari, Opera */ - -moz-column-count: 1; /* Firefox */ - column-count: 1; -} -.column-xs-2 { - -webkit-column-count: 2; /* Chrome, Safari, Opera */ - -moz-column-count: 2; /* Firefox */ - column-count: 2; -} -.column-xs-3 { - -webkit-column-count: 3; /* Chrome, Safari, Opera */ - -moz-column-count: 3; /* Firefox */ - column-count: 3; -} -.column-xs-4 { - -webkit-column-count: 4; /* Chrome, Safari, Opera */ - -moz-column-count: 4; /* Firefox */ - column-count: 4; -} -.column-xs-5 { - -webkit-column-count: 5; /* Chrome, Safari, Opera */ - -moz-column-count: 5; /* Firefox */ - column-count: 5; -} - -@media (min-width: 576px) { - .column-sm-1 { - -webkit-column-count: 1; /* Chrome, Safari, Opera */ - -moz-column-count: 1; /* Firefox */ - column-count: 1; - } - .column-sm-2 { - -webkit-column-count: 2; /* Chrome, Safari, Opera */ - -moz-column-count: 2; /* Firefox */ - column-count: 2; - } - .column-sm-3 { - -webkit-column-count: 3; /* Chrome, Safari, Opera */ - -moz-column-count: 3; /* Firefox */ - column-count: 3; - } - .column-sm-4 { - -webkit-column-count: 4; /* Chrome, Safari, Opera */ - -moz-column-count: 4; /* Firefox */ - column-count: 4; - } - .column-sm-5 { - -webkit-column-count: 5; /* Chrome, Safari, Opera */ - -moz-column-count: 5; /* Firefox */ - column-count: 5; - } -} +.column-xs-1 { column-count: 1; } +.column-xs-2 { column-count: 2; } +.column-xs-3 { column-count: 3; } +.column-xs-4 { column-count: 4; } +.column-xs-5 { column-count: 5; } @media (min-width: 768px) { - .column-md-1 { - -webkit-column-count: 1; /* Chrome, Safari, Opera */ - -moz-column-count: 1; /* Firefox */ - column-count: 1; - } - .column-md-2 { - -webkit-column-count: 2; /* Chrome, Safari, Opera */ - -moz-column-count: 2; /* Firefox */ - column-count: 2; - } - .column-md-3 { - -webkit-column-count: 3; /* Chrome, Safari, Opera */ - -moz-column-count: 3; /* Firefox */ - column-count: 3; - } - .column-md-4 { - -webkit-column-count: 4; /* Chrome, Safari, Opera */ - -moz-column-count: 4; /* Firefox */ - column-count: 4; - } - .column-md-5 { - -webkit-column-count: 5; /* Chrome, Safari, Opera */ - -moz-column-count: 5; /* Firefox */ - column-count: 5; - } + .column-sm-1 { column-count: 1; } + .column-sm-2 { column-count: 2; } + .column-sm-3 { column-count: 3; } + .column-sm-4 { column-count: 4; } + .column-sm-5 { column-count: 5; } } @media (min-width: 992px) { - .column-lg-1 { - -webkit-column-count: 1; /* Chrome, Safari, Opera */ - -moz-column-count: 1; /* Firefox */ - column-count: 1; - } - .column-lg-2 { - -webkit-column-count: 2; /* Chrome, Safari, Opera */ - -moz-column-count: 2; /* Firefox */ - column-count: 2; - } - .column-lg-3 { - -webkit-column-count: 3; /* Chrome, Safari, Opera */ - -moz-column-count: 3; /* Firefox */ - column-count: 3; - } - .column-lg-4 { - -webkit-column-count: 4; /* Chrome, Safari, Opera */ - -moz-column-count: 4; /* Firefox */ - column-count: 4; - } - .column-lg-5 { - -webkit-column-count: 5; /* Chrome, Safari, Opera */ - -moz-column-count: 5; /* Firefox */ - column-count: 5; - } + .column-md-1 { column-count: 1; } + .column-md-2 { column-count: 2; } + .column-md-3 { column-count: 3; } + .column-md-4 { column-count: 4; } + .column-md-5 { column-count: 5; } +} + +@media (min-width: 1200px) { + .column-lg-1 { column-count: 1; } + .column-lg-2 { column-count: 2; } + .column-lg-3 { column-count: 3; } + .column-lg-4 { column-count: 4; } + .column-lg-5 { column-count: 5; } } /* Inventaires */ diff --git a/kfet/static/kfet/css/jconfirm-kfet.css b/kfet/static/kfet/css/jconfirm-kfet.css index 1aee70f1..1e05a816 100644 --- a/kfet/static/kfet/css/jconfirm-kfet.css +++ b/kfet/static/kfet/css/jconfirm-kfet.css @@ -5,7 +5,7 @@ .jconfirm .jconfirm-box { padding:0; border-radius:0 !important; - font-family:"Roboto Mono"; + font-family:Roboto; } .jconfirm .jconfirm-box div.title-c .title { @@ -28,7 +28,6 @@ .jconfirm .jconfirm-box .content { border-bottom:1px solid #ddd; - padding:5px 10px; } .jconfirm .jconfirm-box input { @@ -37,6 +36,7 @@ border:0; + font-family:"Roboto Mono"; font-size:40px; text-align:center; @@ -49,6 +49,7 @@ } .jconfirm .jconfirm-box .buttons button { + width:40px; height:100%; margin:0; margin:0 !important; diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index 4b60aa7b..351ac0aa 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -143,9 +143,10 @@ input[type=number]::-webkit-outer-spin-button { height:50px; padding:0 15px; - background:rgba(200,16,46,0.9); + background:rgba(200,16,46,1); color:#fff; + font-family:"Roboto Slab"; font-weight:bold; font-size:18px; } @@ -226,16 +227,10 @@ input[type=number]::-webkit-outer-spin-button { height:40px; } -#special_operations button { - height:100%; - width:25%; +#special_operations .btn { + height:40px; - float:left; - - background: rgba(200,16,46,0.9); - color:#FFF; - - font-size:18px; + font-size:15px; font-weight:bold; text-overflow: ellipsis; @@ -243,12 +238,6 @@ input[type=number]::-webkit-outer-spin-button { overflow: hidden; } -#special_operations button:focus, -#special_operations button:hover { - outline:none; - background: rgba(200,16,46,1); - color:#fff; -} /* Article autocomplete */ @@ -317,16 +306,22 @@ input[type=number]::-webkit-outer-spin-button { font-size:14px; } -#articles_data table tr.article>td:first-child { +#articles_data table tr.article td:first-child { padding-left:10px; } +#articles_data table tr.article td + td { + padding-right:10px; + text-align:right; +} + #articles_data table tr.category { height:35px; - background-color:rgba(200,16,46,0.9); + background-color:#c8102e; + font-family:"Roboto Slab"; font-size:16px; - color:#FFF; font-weight:bold; + color:#FFF; } #articles_data table tr.category>td:first-child { diff --git a/kfet/static/kfet/css/nav.css b/kfet/static/kfet/css/nav.css index a4dabed2..0efc2873 100644 --- a/kfet/static/kfet/css/nav.css +++ b/kfet/static/kfet/css/nav.css @@ -1,54 +1,98 @@ .navbar { background: #000; color: #DDD; - font-family: Oswald; border: 0; + font-family: Roboto; +} + +.navbar .navbar-header { + float: left; + display: none; + margin-left: -15px; + margin-right: 0; } .navbar .navbar-brand { - padding: 3px 25px; + padding: 3px 0; + margin: 0 15px !important; } .navbar .navbar-brand img { height: 44px; } +.navbar .navbar-toggle { + border: 0; + border-radius: 0; + padding: 18px 15px; + margin: 0; + min-width: auto; +} + .navbar .navbar-toggle .icon-bar { - background-color: #FFF; + background: #fff; } .navbar-nav { - font-weight: bold; font-size: 14px; - text-transform: uppercase; - margin: 0 -15px; + margin: 0 0 0 -15px; + float: left; } -@media (min-width: 768px) { +@media (min-width: 460px) { + .navbar .navbar-header { + display: block; + } + .navbar-nav { - margin: 0px; + margin-left: 0; } - .navbar-right { - margin-right: -15px; + + .navbar-nav .nav-pages.dropdown .dropdown-menu > li:first-child { + display: none; } } +.navbar-right { + float: right !important; + margin: 0 -15px 0 0; +} + .navbar-nav a { transition: background-color, box-shadow, color; transition-duration: 0.15s; } +.navbar-nav > li { + float: left; + text-align: center; +} + .navbar-nav > li > a { + min-width: 50px; + padding: 15px 10px; color: #FFF; } -.navbar-nav > li:hover > a, -.navbar-nav > li > a:focus, -.nav .open > a:hover, -.nav .open > a:focus { +.navbar-nav > .divider { + height: 1px; + background: rgba(255, 255, 255, 0.1); +} + +@media (min-width: 1200px) { + .navbar-nav > li > a { + padding-left: 15px; + padding-right: 15px; + } +} + +.navbar-nav > li > a:hover, .navbar-nav > li > a:focus, +.nav .open > a:hover, .nav .open > a:focus, +.navbar-nav > li.active > a, +.navbar-nav .dropdown:hover > a, .navbar-nav .dropdown:focus > a { background-color: #C8102E; color: #FFF; - box-shadow: inset 0 5px 5px -5px #000; + box-shadow: inset 0 3px 3px -4px #000; } .navbar-nav .dropdown .dropdown-menu { @@ -56,15 +100,22 @@ border: 0; border-radius: 0; background-color: #FFF; + font-size: 14px; + + /* override max-width: 767px of bs */ + position: absolute; + float: left; + box-shadow: 0 6px 12px rgba(0,0,0,.175); } .navbar-nav .dropdown .dropdown-menu > li > a { - padding: 8px 20px; + padding: 8px 10px; + line-height: inherit; color: #000; } .navbar-nav .dropdown .dropdown-menu > li > a:hover, -.navbar-nav .dropdown .dropdown-meny > li > a:focus { +.navbar-nav .dropdown .dropdown-menu > li > a:focus { color: #c8102e; background-color: transparent; } @@ -73,16 +124,28 @@ margin: 0; } -@media (min-width: 768px) { - .navbar-nav .dropdown .dropdown-menu { - display: block; - visibility: hidden; - opacity: 0; - transition: opacity 0.15s; - } +.navbar-nav .dropdown .dropdown-menu { + display: block; + visibility: hidden; + opacity: 0; + transition: opacity 0.15s; +} - .nav .dropdown:hover .dropdown-menu { - visibility: visible; - opacity: 1; +.navbar-nav .dropdown:hover > .dropdown-menu, +.navbar-nav .dropdown:focus > .dropdown-menu, +.navbar-nav .dropdown.open > .dropdown-menu { + visibility: visible; + opacity: 1; +} + +@media (min-width: 992px) { + .navbar-nav .dropdown .dropdown-menu > li > a { + padding-left: 20px; + padding-right: 20px; } } + +.nav-app .dropdown-menu { + right: 0; + left: auto; +} diff --git a/kfet/static/kfet/img/favicon.png b/kfet/static/kfet/img/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..56fb42995770e3b66dcb1d383c634ebfb81fa8d4 GIT binary patch literal 3606 zcmV+x4(aiUP)9NB^z?L`GiT0&C4lG!3y==~ z*s_IlzNhl@vAeq)Nnkdc(QG!O$z(#EPKR2p7Be$5F(xJk)6&xL_U+sEgolW@`(H`o z7eFshPfSTkA*z6r`KLMKE0@c$uC5N9iLS0L@d{wh+_`w>>{&G3*J9{UT3X5(|JK%4 zbT-UpGx`r1Vw1nCkhA}{X=(8MbI%FFTOyIbl`B`k)6fK)1l zmfu@I>7``#e_Ka~!+F@aX%mbXF@m=;2L=R)XCIK9oXooN0RZyyPCCSYa%d=guw@Is zYkfrV2p~2#)}jAT96#pJ{}~w>k9We3w4{r_{>u9QMQ^?dzJmsdraZ)Rz^IWU;mXyk zoG@LqXc6q+zn`@LIvkh-)~#DdoYrJAL3VaFR8&+zMMVYNxN!qQLPB6>WF!Ox1VHBZ z-@}?UYe1n;(9$Zsa)rGr!q?Xq5fM{TQaD@wWo2ixTGFK+IKY~xzrQ~*pR~h=iD*hB z68tDRnUa>tWMaP$m_B0$Ha0dQBBIe~L{CpoP6XiP<;A%2jYcE(@7Iqt&z#&`VqP8! z1tZ$~zx>iJt(KM+b^_S3V}}*7gv3Nn1fWu>7z^M(-+bc`&)V8rB0fz`O^j)~xw)Z6 zqp?Zv=+UFh1+aeodb=6qLMzB*I6y=^ecEyLgTY`R;+2z=!wcA5tt>xR5mui9~{hg@u0%nI;ns4-RH7fTW}($_)DX`8iY(3J(wB z%D8Z$h?B}^(&M<`=kD%KI#O7^aB*>=RZ#VDP#?c^=~Cj#2n-D5q(TCNf{6GvHa7CD zknGVq6$&(&OipVda=Dzi@^f-?IVIJ5~{v2rDG+P}Z> z0%to;qsc^+POH@ltBR0HsbzBVRK7zD|G8}&QMw&FcS3V>GiL?$@bDl?$0bj8x*&;! zdP`k;nKiT2)KqYFrPqG6wY5R~h7Ft+w5hR?Ox4}iCaeG?GW)CGLB}C~qz4sodFJ2T}0D|`bs%2RXaF=NKi zrb|mths1;gQJhfKH#CTM4#@u>`6f)MRKi(;$+W$_9o~5Tb*C4UbnepLzRkPPEw;wq z-YN^TI<_i^ix)2vZTX#>3Wsri@tsP+RR){O$|}zG<9`$hp<0& z{1*hVL+$MBq|DcU^YU!|H*elNT(xQyexH?v20{z!`w<=5NCO*Hf#aD=I8n`JO#{I5EzpIb*gS!)zfVBZIPV_J*#W7(95eU;=1vZl=xOhaY~3 z)z#I)nWx(mKzw|>T_PTYM~ssuO%l`;zOB6-g@D;?#=xON9XpUID=)YE?Wdm#X#bx* zdp7+1^Us2AvLrOBc4zV(5gbgm5%cr+x0&$9_3Jj@zqNQVgoKQv%)r)d+aND5Pw-u? zf>0ys41|V;l5NDMgoWAM1N5|nkVqsrVZsF3?)UlUpNql;>MzecgF+%JD~oInh>DK3 z`njW{17$LqrB7F{Uf4misyKJg($T6+l8lBAJU)-@bjVx`uwW>-BK!)-BM~)cTV88$X062K?AlVSKdd(Uu>0K_dbdLVvhaV`VeVL^*bx~2# z9~+m52u@nQQmG_1N2Ei0dpq8`bqlv`-_AH6qsNXVUxseqzMaxmGA=HTa=BGiRpqd; z?)mfQIZ=wO->`v`%6I1S*_GrxK&kx0pKC*WJZw!&wA+=`B_%K+B!pF$UbbwR#frgQ zE{9IN9>T)IVOC_M%|rUy+FD3WO|?sN+t#hD^5Ws)0ZOG3%w{vRwY7m>ujfx4?dRub za}qvz@)Tt&DK9T~s3m;y#TTqvXY$7%+cc1kjg4hR@6e$`;x|p~3B6wLa7Om+-OH-N zjvqg6litmnH(Ak(Ua&w|(?p}u$oSmXQLje;fW4o6#_GT7ni{KjqehJaUteDU0MOLc z!K&4(K_Zb58C{)qf=LXcAxP=%O{-4siOkGQPFh1~=oHM!$-!TK`30|CyN1mzE%%%p zDk>^atyZI2t;TEDuVX_)1FA2nQ6ZPxq_twj3Y;~2w#B=OiV9v#(;XUu>?z}@U@#a& z8FQ%TQ&m+ZIssS>L4iYuGVVe$nGE)^976bno0}Ua2lr03im(^cfDW&|@(Oqm_{$23 zmfwG;{@&6eyZ}@xl~tl2kI{z)aCdiyNs}gtS^)Mmy7#MGgn-4Ev}WyEt8www*Iu*f zvf2~p&z~3d91vzh)7ukLsZ`iWc!Q1Y`}pzWi5;uC@SqgZGGfKbmBLQKw&EV#ym=E^ zTU%lE+O@RZY5DTy@E~JS=oDe+fX>cNt3*vrO#lFpoRmZxC}ZQsjl{P2e8ZC<=BS&C zR9FGr-9g%d$z+0$Kly}~=z^FSQ3{}qXXgu60`RwUCVUrh@4joZm)wtaadDy)fVYhm zp4AA@)g`O|da9!}8jaOY2M>KqNi-xRM3hTXSDB1xQz@tbdMe^Fcq%P}=>^#F?Mg}cHzna2GiDI`=JJJLJGMFZ!@$xBkuw1pdbI4| z;lpkIn3zcK(ejzd^h0H3WwdErgzf=d1_N1lZfb&EyLM5gnLBr`=!T(7mo8C$4+;tr zRuy5(xdJGnMA0E8CPq{{;UAA3qYaBDs4Bw8$A^)QeEIU_wB{U6io1`4hYZ2u;$m!U zY{bf{Dpd9EP5aNXWy{2NCP42W05rk(=uXgHc=1I}GyT`7D4X%w?&x&1<}8u3W{Fz> zJ)PNTY}*k4@THetqAXBnXQ!2EBirY5PdyGvkks__bm*ek zLAtxULsV3hLlae0R1_eh-BlR?z~#&Ikz7a&4%~S@ShA2vpFVw<#Jv2|{BHDUr?srj z@4rtJQj#E9VJ$&)Dyr)zCx9EWq%s8O^&SWIZrc}xE7`RBzEw6DFr-KI8_3PnXluxHO6 zyWhP$J)xLE`1y#bPeXBWG3Df$l9IwGk95(vHXsxNsq3gQ~jvdKgL(!OPcg(1YCr002jf9O-nwt0ha8IOXnwufF<9 zyn$Ou@!Qp_u!~|Ya1;p%39xV9K34zc&_sS@+6zV50{{T!3I$_JYZ7eQw8^1-g-1kC z(!WSCKk>Q@<>Gnr$tO9j{1-V;?0M5%9-+}_9v%Sz00smE(1wbkLUnZwCL|={=+UEj zv5?bhwGWp7?mihA8N?yvGmad=m2a=YC!To1DS0_?-~ih@UpRdv!1I;nzP`Q?A0H2a zK|$almqT~A5wy3pP+3_C7YYmE*s)`jnpLi_WXTfPyLYe6Z(Utoyju6+qxj5!_F0-+ zLi``>WvJLudE*AkB~CQm`cd7p&E0);{W4Gj(V!Z7rN zUa#lHvhR`Y0eZcj(kZWbJn z?(S|Yp9q7&0PgPYkB1;#x7&G-lmn c0f-#`2L&H@H(bT$kN^Mx07*qoM6N<$f@V$TK>z>% literal 0 HcmV?d00001 diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index db31e0e8..d185b604 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -67,7 +67,6 @@ { label: chart.label, borderColor: chart.color, - backgroundColor: chart.color, fill: is_time_chart, lineTension: 0, data: chart_data, diff --git a/kfet/templates/kfet/account.html b/kfet/templates/kfet/account.html index c8d9b4f9..81a7f4ed 100644 --- a/kfet/templates/kfet/account.html +++ b/kfet/templates/kfet/account.html @@ -10,14 +10,27 @@
    compte{{ accounts|length|add:-1|pluralize }}
+{% if perms.kfet.manage_perms or perms.kfet.view_negs %} +
{% if perms.kfet.manage_perms %} + {% endif %} {% if perms.kfet.view_negs %} + {% endif %}
+{% endif %} {% endblock %} @@ -26,16 +39,15 @@

Liste des comptes

- +
- - + - - + + - + @@ -43,15 +55,14 @@ - - + - + {% endfor %} diff --git a/kfet/templates/kfet/account_group.html b/kfet/templates/kfet/account_group.html index 96508aa4..e7243258 100644 --- a/kfet/templates/kfet/account_group.html +++ b/kfet/templates/kfet/account_group.html @@ -5,7 +5,7 @@ {% block fixed-content %} -
+ @@ -40,7 +40,11 @@

Comptes

diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index e92f3f70..741ad9dd 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -18,7 +18,7 @@ {% if perms.kfet.change_settings %} -
+ {% endif %} @@ -30,14 +30,13 @@

Liste des comptes en négatifs

-
TrigrammeTri. NomBalanceCOFBalanceCOF DptPromoPromo
- + {{ account.trigramme }} {{ account.trigramme }} {{ account.name }} {{ account.balance }}€{{ account.is_cof|yesno:"Oui,Non" }}{{ account.is_cof|yesno }} {{ account.departement }}{{ account.promo|default_if_none:'' }}{{ account.promo|default_if_none:'' }}
+
- - + - - + + @@ -49,10 +48,9 @@ -
TriTri. NomBalanceRéelleBalanceRéelle Début Découvert autorisé Jusqu'au
- + {{ neg.account.trigramme }} {{ neg.account.trigramme }} {{ neg.account.name }} {{ neg.account.balance|floatformat:2 }}€ diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 282e035f..fc8babc5 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -44,6 +44,10 @@ $(document).ready(function() { {% endif %} {% endblock %} +{% block footer %} +{% include "kfet/base_footer.html" %} +{% endblock %} + {% block fixed-content %} {% include "kfet/left_account.html" %} {% endblock %} diff --git a/kfet/templates/kfet/account_update.html b/kfet/templates/kfet/account_update.html index 86c27b48..47a043a4 100644 --- a/kfet/templates/kfet/account_update.html +++ b/kfet/templates/kfet/account_update.html @@ -20,6 +20,12 @@ {% endif %} {% endblock %} +{% block footer %} +{% if not account.user.is_team %} +{% include "kfet/base_footer.html" %} +{% endif %} +{% endblock %} + {% block main-class %}content-form{% endblock %} {% block main-content %} diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index 398ef658..86bdc518 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -9,13 +9,18 @@
{{ articles|length }}
article{{ articles|length|pluralize }}
-
+ {% endblock %} @@ -24,10 +29,9 @@

Liste des articles

- +
- @@ -40,16 +44,15 @@ {% for article in articles %} {% ifchanged article.category %} - + {% endifchanged %} - - diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html index 134fb104..8cd84ded 100644 --- a/kfet/templates/kfet/article_read.html +++ b/kfet/templates/kfet/article_read.html @@ -14,6 +14,14 @@
{{ article.name }}
{{ article.category }}
+
Prix (hors réduc.): {{ article.price }}€
Stock: {{ article.stock }}
@@ -21,11 +29,6 @@
Affiché: {{ article.hidden | yesno:"Non,Oui" }}
- {% endblock %} @@ -36,29 +39,35 @@

Inventaires

-
Nom Prix Stock
{{ article.category.name }}{{ article.category.name }}
+ - + {{ article.name }} {{ article.name }} {{ article.price }}€ {{ article.stock }} {{ article.is_sold | yesno:"En vente,Non vendu"}}
- - - - - - - - - {% for inventoryart in inventoryarts %} - - - - - - {% endfor %} - -
DateStockErreur
{{ inventoryart.inventory.at }}{{ inventoryart.stock_new }}{{ inventoryart.stock_error }}
+
+ + + + + + + + + + {% for inventoryart in inventoryarts %} + + + + + + {% endfor %} + +
DateStockErreur
+ + {{ inventoryart.inventory.at }} + + {{ inventoryart.stock_new }}{{ inventoryart.stock_error }}
+

Prix fournisseurs

- +
diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index da37abae..b18ee719 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -1,4 +1,5 @@ {% load staticfiles %} +{% load menu_tags %} @@ -7,9 +8,13 @@ {% block title %}{% endblock %} | K-Fêt - ENS Ulm + + + + {# CSS #} - + @@ -28,18 +33,26 @@ - {% include "kfet/base_nav.html" %} + {% main_menu template="kfet/base_nav.html" %}
{% block header %} -
-
-

{% block header-title %}{% endblock %}

+ {% if not page or not page.no_header %} +
+
+

+ {% block header-title %}{% endblock %} +

+
-
+ {% endif %} {% endblock %} + {% block content %}{% endblock %} - {% include "kfet/base_footer.html" %} + + {% block footer %} + {% endblock footer %}
+
diff --git a/kfet/templates/kfet/base_col_1.html b/kfet/templates/kfet/base_col_1.html index a4c26b82..6bd6dd5c 100644 --- a/kfet/templates/kfet/base_col_1.html +++ b/kfet/templates/kfet/base_col_1.html @@ -1,5 +1,7 @@ {% extends "kfet/base.html" %} +{% block header-class %}text-center{% endblock %} + {% block content %}
@@ -9,6 +11,6 @@ {% block main-content %}{% endblock %}
-
+
{% endblock %} diff --git a/kfet/templates/kfet/base_col_2.html b/kfet/templates/kfet/base_col_2.html index 58c36d14..2a1f8cd4 100644 --- a/kfet/templates/kfet/base_col_2.html +++ b/kfet/templates/kfet/base_col_2.html @@ -3,12 +3,12 @@ {% block content %}
-
+
{% block fixed-content %}{% endblock %}
-
+
{% include "kfet/base_messages.html" %}
{% block main-content %}{% endblock %} diff --git a/kfet/templates/kfet/base_footer.html b/kfet/templates/kfet/base_footer.html index e69de29b..be524139 100644 --- a/kfet/templates/kfet/base_footer.html +++ b/kfet/templates/kfet/base_footer.html @@ -0,0 +1,17 @@ +{% load wagtailcore_tags %} + +{% with "k-fet@ens.fr" as kfet_mail %} + + + +{% endwith %} diff --git a/kfet/templates/kfet/base_messages.html b/kfet/templates/kfet/base_messages.html index 66b67162..5dd68e69 100644 --- a/kfet/templates/kfet/base_messages.html +++ b/kfet/templates/kfet/base_messages.html @@ -1,16 +1,12 @@ {% if messages %} -
+
{% for message in messages %} -
-
- - {% if 'safe' in message.tags %} - {{ message|safe }} - {% else %} - {{ message }} - {% endif %} -
-
+
+ + {{ message }} +
{% endfor %}
{% endif %} diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index 273cdc0b..629b41da 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,70 +1,107 @@ -{% load staticfiles %} +{% load static %} +{% load wagtailcore_tags %} - {% endblock %} +{# Footer #} + {% block footer %} {% include "kfet/base_footer.html" %} {% endblock %} diff --git a/kfet/cms/templates/kfetcms/block_menu.html b/kfet/cms/templates/kfetcms/block_menu.html new file mode 100644 index 00000000..382a7770 --- /dev/null +++ b/kfet/cms/templates/kfetcms/block_menu.html @@ -0,0 +1,11 @@ +{% load static %} + +{% if pressions %} + {% include "kfetcms/block_menu_category.html" with title="Pressions du moment" articles=pressions class="carte-inverted" %} +{% endif %} + +{% regroup articles by category as categories %} + +{% for category in categories %} + {% include "kfetcms/block_menu_category.html" with title=category.grouper.name articles=category.list %} +{% endfor %} diff --git a/kfet/cms/templates/kfetcms/block_menu_category.html b/kfet/cms/templates/kfetcms/block_menu_category.html new file mode 100644 index 00000000..ef7d4ce0 --- /dev/null +++ b/kfet/cms/templates/kfetcms/block_menu_category.html @@ -0,0 +1,12 @@ +
+

{{ title }}

+
    + {% for article in articles %} +
  • +
    + {{ article.name }} + {{ article.price_ukf }} UKF +
  • + {% endfor %} +
+
diff --git a/kfet/cms/templates/kfetcms/equipe.html b/kfet/cms/templates/kfetcms/block_teamgroup.html similarity index 51% rename from kfet/cms/templates/kfetcms/equipe.html rename to kfet/cms/templates/kfetcms/block_teamgroup.html index 2295957f..fab43d68 100644 --- a/kfet/cms/templates/kfetcms/equipe.html +++ b/kfet/cms/templates/kfetcms/block_teamgroup.html @@ -1,27 +1,32 @@ -{% extends "kfetcms/base.html" %} -{% load wagtailcore_tags %} -{% load wagtailimages_tags %} +{% load wagtailcore_tags wagtailimages_tags %} -{% block block1-content %} -{% for group_block in page.team_groups.all %} -

{{ group_block.title }}

-
- {{ group_block.content|richtext }} -
+{% with groupteam=value len=value.members|length %} -{% with members=group_block.group.members.all %} -{% with len=members|length %} - -{% if len > 0 %}
{% if len == 2 %}
{% endif %} - {% for member in members %} -
+ {% for member in groupteam.members %} +
{% image member.photo max-200x500 %}
@@ -35,10 +40,10 @@
{% endfor %} - {% if group_block.show_only != None and len > group_block.show_only %} + {% if groupteam.show_only != None and len > groupteam.show_only %}
-{% endif %} {% endwith %} -{% endwith %} - -{% endfor %} - -{% endblock %} diff --git a/kfet/cms/templates/kfetcms/carte.html b/kfet/cms/templates/kfetcms/carte.html deleted file mode 100644 index 3a32624b..00000000 --- a/kfet/cms/templates/kfetcms/carte.html +++ /dev/null @@ -1,46 +0,0 @@ -{% extends "kfetcms/base.html" %} -{% load static %} -{% load kfet_tags %} - -{% block extra_head %} -{{ block.super }} - -{% endblock %} - -{% block col-size %}column-sm-2 column-md-3{% endblock %} - -{% block block3-content %} - -{% if pressions %} -
-

Pressions du moment

-
    - {% for article in pressions %} -
  • -
    - {{ article.name }} - {{ article.price | ukf:False}} UKF -
  • - {% endfor %} -
-
-{% endif %} - -{% regroup articles by category as categories %} - -{% for category in categories %} -
-

{{ category.grouper.name }}

-
    - {% for article in category.list %} -
  • -
    - {{ article.name }} - {{ article.price | ukf:False}} UKF -
  • - {% endfor %} -
-
-{% endfor %} - -{% endblock %} diff --git a/kfet/forms.py b/kfet/forms.py index 95e97d56..85391992 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -25,18 +25,22 @@ from gestioncof.models import CofProfile # ----- class DateTimeWidget(forms.DateTimeInput): - def __init__(self, attrs = None): - super(DateTimeWidget, self).__init__(attrs) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) 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/moment.js', + 'kfet/js/moment-fr.js', + 'kfet/js/moment-timezone-with-data-2010-2020.js', + 'kfet/js/bootstrap-datetimepicker.min.js', + ) + + # ----- # Account forms # ----- @@ -459,8 +463,11 @@ class KFetConfigForm(ConfigForm): class FilterHistoryForm(forms.Form): - checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all()) - accounts = forms.ModelMultipleChoiceField(queryset = Account.objects.all()) + checkouts = forms.ModelMultipleChoiceField(queryset=Checkout.objects.all()) + accounts = forms.ModelMultipleChoiceField(queryset=Account.objects.all()) + from_date = forms.DateTimeField(widget=DateTimeWidget) + to_date = forms.DateTimeField(widget=DateTimeWidget) + # ----- # Transfer forms diff --git a/kfet/models.py b/kfet/models.py index eb33daf7..162d1e90 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -14,7 +14,8 @@ from datetime import date import re import hashlib -from kfet.config import kfet_config +from .config import kfet_config +from .utils import to_ukf def choices_length(choices): return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) @@ -102,6 +103,10 @@ class Account(models.Model): return self.cofprofile.is_cof # Propriétés supplémentaires + @property + def balance_ukf(self): + return to_ukf(self.balance, is_cof=self.is_cof) + @property def real_balance(self): if hasattr(self, 'negative') and self.negative.balance_offset: @@ -309,6 +314,10 @@ class AccountNegative(models.Model): ('view_negs', 'Voir la liste des négatifs'), ) + @property + def until_default(self): + return self.start + kfet_config.overdraft_duration + class Checkout(models.Model): created_by = models.ForeignKey( @@ -461,6 +470,10 @@ class Article(models.Model): def get_absolute_url(self): return reverse('kfet.article.read', kwargs={'pk': self.pk}) + def price_ukf(self): + return to_ukf(self.price) + + class ArticleRule(models.Model): article_on = models.OneToOneField( Article, on_delete = models.PROTECT, diff --git a/kfet/signals.py b/kfet/signals.py index 6bbbbbb0..374e0dca 100644 --- a/kfet/signals.py +++ b/kfet/signals.py @@ -4,7 +4,7 @@ from django.contrib import messages from django.contrib.auth.signals import user_logged_in from django.core.urlresolvers import reverse from django.dispatch import receiver -from django.utils.html import mark_safe +from django.utils.safestring import mark_safe @receiver(user_logged_in) diff --git a/kfet/static/kfet/css/base/buttons.css b/kfet/static/kfet/css/base/buttons.css new file mode 100644 index 00000000..e7498022 --- /dev/null +++ b/kfet/static/kfet/css/base/buttons.css @@ -0,0 +1,88 @@ +/* General ------------------------- */ + +.btn { + border: 0; + outline: none !important; + + transition: background-color, border, color, opacity; + transition-duration: 0.15s; + + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + + font-family: "Roboto Slab"; +} + +.btn, .btn-lg, .btn-group-lg>.btn { + border-radius:0; +} + + +/* Default ------------------------- */ + +.btn-default { + background-color: transparent !important; + color: #555; +} + +.btn-default:hover, +.btn-default.focus, .btn-default:focus { + color: #c8102e; +} + +.btn-default[disabled]:hover, .btn-default.disabled:hover { + color: inherit !important; +} + + +/* Primary ------------------------- */ + +.btn-primary { + background-color:#c63b52; + color:#FFF; +} + +.btn-primary:hover, +.btn-primary.focus, .btn-primary:focus, +.btn-primary.active.focus, .btn-primary.active:focus, .btn-primary.active:hover, +.btn-primary:active.focus, .btn-primary:active:focus, .btn-primary:active:hover { + background-color:#c8102e; + color:#FFF; +} + + +/* Primary + White background ------ */ + +.btn-primary-w { + background: white; + color: black; +} + +.btn-primary-w:hover { + background: #c63b52; + color: white; +} + +.btn-primary-w.focus, .btn-primary-w:focus, +.btn-primary-w.active.focus, .btn-primary-w.active:focus, .btn-primary-w.active:hover, +.btn-primary-w:active.focus, .btn-primary-w:active:focus, .btn-primary-w:active:hover { + background: #c8102e; + color: white; +} + + +/* Nav ----------------------------- */ + +.btn-nav { + background-color: transparent !important; + color: inherit; + border-bottom: 1px solid #ddd; +} + +.btn-nav:hover, +.btn-nav.focus, .btn-nav:focus, +.btn-nav.active.focus, .btn-nav.active:focus, .btn-nav.active:hover, +.btn-nav:active.focus, .btn-nav:active:focus, .btn-nav:active:hover { + border-bottom: 1px solid #c8102e; +} diff --git a/kfet/static/kfet/css/base/fixed.css b/kfet/static/kfet/css/base/fixed.css new file mode 100644 index 00000000..d198c50f --- /dev/null +++ b/kfet/static/kfet/css/base/fixed.css @@ -0,0 +1,151 @@ +.fixed > * + * { + margin-top: 15px; +} + + +/* Aside --------------------------- */ + +/* Aside - Block */ + +aside { + background: white; + padding: 15px; +} + +aside > * + * { + margin-top: 15px; +} + +/* Aside - Misc */ + +aside .glyphicon-question-sign { + font-size: 0.8; +} + +aside h4 { + font-weight: bold; +} + +/* Aside - Heading */ + +aside .heading { + font-family: "Roboto Slab"; + font-size: 25px; + font-weight: bold; + line-height: 1.1; + text-align: center; +} + +aside .heading .big { + font-size: 2em; +} + +aside .heading .sub { + font-size: 0.7em; + font-weight: normal; +} + +@media (min-width: 992px) { + aside .heading { + font-size: 32px; + line-height: 1.3; + } +} + +/* Aside - Buttons */ + +aside .buttons { + margin-left: -15px; + margin-right: -15px; +} + +aside .buttons > * { + flex: 0 1 auto !important; +} + + +/* Aside - Text */ + +aside .text { + line-height: 1.3; + font-size: 14px; +} + +@media (min-width: 992px) { + aside .text { + line-height: 1.6; + font-size: 16px; + } +} + +aside .text ul { + margin-bottom: 0; +} + + +/* Buttons ------------------------- */ + +.fixed .buttons { + display: flex; + flex-flow: row wrap; + justify-content: center; + + text-align: center; +} + +.fixed .buttons > * { + flex: 0 1 auto; + overflow: hidden; +} + +.fixed .buttons > .solo { + flex: 1 100%; +} + +@media (min-width: 768px) { + .fixed .buttons > * { + flex: 1 auto; + } + + .fixed .buttons > .full > * { + width: 100%; + } +} + +.fixed .buttons .btn { + padding: 8px 12px; +} + +@media (min-width: 992px) { + .fixed .buttons .btn { + font-size: 16px; + } +} + + +/* Tabs ---------------------------- */ + +.fixed .tabs-buttons { + margin-bottom: -5px; +} + +.fixed .tabs-buttons > * { + margin-bottom: 5px; +} + +.fixed .tabs-buttons .glyphicon-chevron-right { + margin-left: 5px; + line-height: 1.4; + color: white; +} + +@media (min-width: 768px) { + .fixed .tabs-buttons { + text-align: right; + justify-content: flex-end; + } + + .fixed .tabs-buttons > * { + flex: 1 100%; + } +} diff --git a/kfet/static/kfet/css/footer.css b/kfet/static/kfet/css/base/footer.css similarity index 85% rename from kfet/static/kfet/css/footer.css rename to kfet/static/kfet/css/base/footer.css index 5e8d1474..abdf98ed 100644 --- a/kfet/static/kfet/css/footer.css +++ b/kfet/static/kfet/css/base/footer.css @@ -10,10 +10,9 @@ } .footer a { - color: inherit; + color: inherit !important; } .footer a:hover, .footer a:focus { - color: inherit; text-decoration: underline; } diff --git a/kfet/static/kfet/css/base/main.css b/kfet/static/kfet/css/base/main.css new file mode 100644 index 00000000..2ebc90d8 --- /dev/null +++ b/kfet/static/kfet/css/base/main.css @@ -0,0 +1,138 @@ +/* Global layout ------------------- */ + +.main-col, .fixed-col { + padding: 0 0 15px; +} + +@media (min-width: 768px) { + .fixed-col { + position: sticky; + top: 35px; + padding-top: 15px; + } + + .fixed-col + .main-col { + padding: 15px 0 15px 15px; + } +} + +@media (min-width: 992px) { + .main-col { + padding: 15px; + } +} + +.main-col-mult { + column-gap: 45px; +} + +.main-bg { + background: white; +} + +.main-padding { + padding: 15px; +} + +@media (min-width: 768px) { + .main-padding { + padding: 30px; + } +} + + +/* Section ------------------------- */ + +section { + margin-bottom: 15px; + position:relative; +} + +section:last-child { + margin-bottom: 0; +} + + +/* Section - Elements -------------- */ + +section > * { + background: white; + padding: 15px; +} + +section > .full, +section > table, +section > .table-responsive { + padding: 0 !important; + margin-left: 0 !important; + margin-right: 0 !important; +} + +section .full { + margin-left: -15px; + margin-right: -15px; +} + +@media (min-width: 992px) { + section > * { + padding: 30px; + } + + section .full { + margin-left: -30px; + margin-right: -30px; + } +} + +section .row > div:last-child { + margin-bottom: 0 !important; +} + +@media (max-width: 768px) { + section .row > div { + margin-bottom: 10px; + } +} + +@media (max-width: 1200px) { + section .row > div { + margin-bottom: 20px; + } +} + +section ul ul { + padding-left: 30px; +} + +/* Titles & Heading */ + +section h2, +section .heading { + background: transparent; + margin: 20px 15px 15px; + padding: 0; + border-bottom: 3px solid #c8102e; + font-family: "Roboto Slab"; + font-size: 40px; + line-height: 1.1; +} + +section h3 { + border-bottom: 2px solid #c8102e; + margin: 0 0 10px; + padding: 10px 0 10px; + font-size: 25px; + font-weight: bold; +} + +section .heading .buttons { + opacity: 0.7; + top: 10px; + float: right; +} + +section h2:first-child, +section h3:first-child { + padding-top: 0; + margin-top: 0; +} diff --git a/kfet/static/kfet/css/base/messages.css b/kfet/static/kfet/css/base/messages.css new file mode 100644 index 00000000..268f514d --- /dev/null +++ b/kfet/static/kfet/css/base/messages.css @@ -0,0 +1,36 @@ +.messages .alert { + padding:10px 15px; + margin:0; + border:0; + border-radius:0; +} + +.messages .alert:last-child { + margin-bottom: 15px; +} + +.messages .alert .close { + top:0; + right:0; +} + +.messages .alert-info { + color:inherit; + background-color:#ccc; +} + +.messages .alert-error { + color: white; + background-color: #c63b52; +} + +.messages .alert-success { + color: white; + background: #3d9947; +} + +.messages a { + font-weight: bold; + text-decoration: none; +} + diff --git a/kfet/static/kfet/css/base/misc.css b/kfet/static/kfet/css/base/misc.css new file mode 100644 index 00000000..9b09edae --- /dev/null +++ b/kfet/static/kfet/css/base/misc.css @@ -0,0 +1,107 @@ +/* General ------------------------- */ + +body { + margin-top:50px; + font-family:Roboto; + background:#ddd; +} + +.glyphicon + span, span + .glyphicon { + margin-left: 10px; +} + +/* Titles */ + +h1,h2,h3,h4,h5,h6 { + font-family:"Roboto Slab"; +} + +/* Links */ + +a { + color:#C8202E; +} + +a:focus, a:hover { + color:#C8102E; +} + +/* Inputs */ + +:focus { + outline:none; +} + +textarea { + font-family:'Roboto Mono'; + border-radius:0 !important; +} + +/* Lists */ + +ul, ol { + padding-left: 30px; +} + +ul { + list-style-type: square; +} + +/* Tables */ + +.table { + margin-bottom:0; + border-bottom:1px solid #ddd; + width:100%; + background-color: #FFF; +} + +.table td { + vertical-align:middle !important; +} + +.table td.no-padding { + padding:0; +} + +.table thead { + background:#c8102e; + color:#fff; + font-weight:bold; + font-size:16px; +} + +.table thead td { + padding:8px !important; +} + +.table tr.section { + background: #c63b52 !important; + color:#fff; + font-weight:bold; +} + +.table tr.section td { + border-top:0; + font-size:16px; + padding:8px 30px; +} + +.table tr.more td { + padding: 0; +} + +.table-responsive { + border: 0; + margin-bottom: 0; +} + +/* Toggle on hover ----------------- */ + +.toggle:not(:hover) .hover { + display: none; +} + +.toggle:hover .base { + display: none; +} diff --git a/kfet/static/kfet/css/nav.css b/kfet/static/kfet/css/base/nav.css similarity index 100% rename from kfet/static/kfet/css/nav.css rename to kfet/static/kfet/css/base/nav.css diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 04fbaeb1..8e28cce0 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -1,306 +1,64 @@ -@import url("nav.css"); -@import url("footer.css"); +/* Libs */ +@import url("libs/columns.css"); + +/* Libs customizations */ +@import url("libs/jconfirm-kfet.css"); +@import url("libs/multiple-select-kfet.css"); + +/* Base */ +@import url("base/misc.css"); +@import url("base/buttons.css"); + +/* Blocks */ +@import url("base/main.css"); +@import url("base/nav.css"); +@import url("base/messages.css"); +@import url("base/fixed.css"); +@import url("base/footer.css"); + +/* Components */ @import url("kpsul.css"); -@import url("jconfirm-kfet.css"); @import url("history.css"); -body { - margin-top:50px; - font-family:Roboto; - background:#ddd; + + +.header { + padding: 15px 20px; + + background-color: rgba(200,16,46,1); + color: #FFF; } -h1,h2,h3,h4,h5,h6 { - font-family:"Roboto Slab"; -} - -a { - color:#C8202E; -} - -a:focus, a:hover { - color:#C8102E; -} - -:focus { - outline:none; -} - -textarea { - font-family:'Roboto Mono'; - border-radius:0 !important; -} - -.glyphicon + span, span + .glyphicon { - margin-left: 10px; -} - -.table { - margin-bottom:0; - border-bottom:1px solid #ddd; - width:100%; - background-color: #FFF; -} - -.table td { - vertical-align:middle !important; -} - -.table td.no-padding { - padding:0; -} - -.table thead { - background:#c8102e; - color:#fff; - font-weight:bold; - font-size:16px; -} - -.table thead td { - padding:8px !important; -} - -.table tr.section { - background:#c8102e; - color:#fff; - font-weight:bold; -} - -.table tr.section td { - border-top:0; - font-size:16px; - padding:8px 30px; -} - -.table-hover > tbody > tr.section:hover { - background:#c8102e; -} - -.table-responsive { - border: 0; - margin-bottom: 0; -} - -.btn { - border: 0; - - transition: background-color, color; - transition-duration: 0.15s; - - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - - font-family: "Roboto Slab"; -} - -.btn, .btn-lg, .btn-group-lg>.btn { - border-radius:0; -} - -.btn-primary { - background-color:#c63b52; - color:#FFF; - border:0; -} - -.btn-primary:hover, -.btn-primary.focus, .btn-primary:focus, -.btn-primary.active.focus, .btn-primary.active:focus, .btn-primary.active:hover, -.btn-primary:active.focus, .btn-primary:active:focus, .btn-primary:active:hover, -.nav-pills>li.active>a, .nav-pills>li.active>a:focus, .nav-pills>li.active>a:hover { - outline: 0; - background-color:#bf0f2c; - background-color:#c8102e; - color:#FFF; -} - -.btn-primary[disabled]:hover, -.btn-primary[disabled]:focus { - background-color: #000; - color: #666; -} - -.nav-pills>li>a { - border-radius:0; -} - -.nav-pills>li>a:focus, .nav-pills>li>a:hover { - outline: 0; - background-color:rgba(200,16,46,1); - color:#FFF; -} - -.header-row { - background-color:rgba(200,16,46,1); - color:#FFF; -} - -.page-header { - border:0; - padding:0; - margin:15px 20px; - font-weight:bold; +.header h1 { + padding: 0; + margin: 0; + font-weight: bold; } .nopadding { padding: 0 !important; } -.col-content-left, .col-content-right { - padding:0; -} - - -@media (min-width: 768px) { - .col-content-left { - position: sticky; - top:50px; - margin-bottom: 15px; - } -} - -.content-left-top { - background:#fff; - padding:15px; -} - -@media (min-width: 1200px) { - .content-left-top { - padding: 30px; - } -} - -.content-left .btn-lg { - font-size: 16px; -} - -.btn-actions { - margin: 0 -15px; -} - -.btn-actions .btn { - color: inherit; -} - -.btn-actions .btn:not([disabled]):hover, .btn-actions .btn:not([disabled]):focus { - color: #c8102e; -} - -.content-left-top.frozen-account { +.frozen-account { background:#5072e0; color:#fff; } -.content-left .block { - padding-top:15px; + +.main .table a:not(.btn) { + color: inherit; } -.content-left .block .line { - font-size:16px; - line-height:30px; +.main .table a:not(.btn):focus , +.main .table a:not(.btn):hover { + color: #C81022; } -.content-left .line.line-big { - font-family:"Roboto Slab"; - font-size:60px; - font-weight:bold; - text-align:center; - overflow:hidden; -} - -.content-left .line.line-bigsub { - font-size:25px; - font-weight:bold; - text-align:center; -} - -.content-left .line.balance { - font-size:45px; - text-align:center; -} - -@media (min-width: 1200px) { - .content-left .line.line-big { - margin-top: -15px; - } -} - -@media (min-width: 768px) { - .content-right { - margin: 15px; - } -} - -.content-right-block { - margin-top: 15px; - position:relative; -} - -.content-right-block > *:not(.buttons-title) { - background: #fff; -} - -.content-right-block > h2 { - background: transparent !important; -} - -.content-right-block .buttons-title { - position:absolute; - top:8px; - right:20px; -} - -.content-right-block > div.row { - margin:0; -} - -.content-right-block h2 { - margin:20px 20px 15px; - padding-bottom:5px; - border-bottom:3px solid #c8102e; - font-size:40px; -} - -.content-right-block h3 { - border-bottom: 1px solid #c8102e; - margin: 0px 15px 15px; - padding: 20px 20px 10px; - font-size:25px; -} - -.content-right-block a:not(.btn) { - color:#000; -} - -.content-right-block a:not(.btn):focus , -.content-right-block a:not(.btn):hover { - color:#C81022; -} /* * Pages tableaux seuls */ -.content-center > *:not(.content-right-block) { - background: #fff; -} - -@media (min-width: 992px) { - .content-center { - margin: 15px 0; - } - - .column-row { - margin-top: 15px; - margin-bottom: 15px; - } -} - -.content-center tbody tr:not(.section) td { - padding:0px 5px; -} .table .form-control { padding: 1px 12px ; @@ -313,6 +71,10 @@ textarea { background: #f5f5f5; } +.table-condensed-input tbody tr:not(.section) td { + padding:0px 5px; +} + .table-condensed input.form-control { margin: 0 !important; border-top: 0; @@ -320,19 +82,34 @@ textarea { border-radius: 0; } -.content-center .auth-form { - margin:15px; +.auth-form { + padding: 15px 0; + background: #d86c7e; + color: white; +} + +.auth-form.form-horizontal { + padding: 0; + margin: 0; +} + +.auth-form .form-group { + margin-bottom: 0; +} + +.auth-form input { + box-shadow: none !important; + background: transparent; + color: white; + border: 0 !important; + border-radius: 0; + border-bottom: 1px solid white !important; } /* * Pages formulaires seuls */ -.content-form { - background-color: #fff; - padding: 15px; -} - .account_create #id_trigramme { display:block; width:200px; @@ -395,40 +172,48 @@ textarea { padding:5px 20px; } -/* - * Messages +/* Account autocomplete window */ + +#account_results ul { + list-style-type:none; + background:rgba(255,255,255,0.9); + padding:0; +} + +#account_results li { + display:block; + padding:5px 20px; + height:100%; + width:100%; +} + +#account_results .hilight { + background:rgba(200,16,46,0.9); + color:#fff; + text-decoration:none; +} + +/** + * Stats (graphs) */ -.messages .alert { - padding:10px 15px; - margin:0; - border:0; - border-radius:0; +.stat-nav { + margin-bottom: 10px; + font-family: Roboto; } -.messages .alert .close { - top:0; - right:0; +.stat-nav li { + float: left; } -.messages .alert-info { - color:inherit; - background-color:#ccc; +.stat-nav a { + opacity: 0.6; + font-family: Roboto; } -.messages .alert-error { - color: white; - background-color: #c63b52; -} - -.messages .alert-success { - color: white; - background: #3d9947; -} - -.messages a { - font-weight: bold; - text-decoration: none; +.stat-nav a:hover, +.stat-nav a.focus, .stat-nav a:focus { + opacity: 1; } /* @@ -488,57 +273,10 @@ thead .tooltip { height: 100px; } -/* - * Responsive Columns - */ - -.unbreakable { - display:inline-block; - width: 100%; -} - -.column-xs-1, .column-sm-1, .column-md-1, .column-lg-1, -.column-xs-2, .column-sm-2, .column-md-2, .column-lg-2, -.column-xs-3, .column-sm-3, .column-md-3, .column-lg-3, -.column-xs-4, .column-sm-4, .column-md-4, .column-lg-4, -.column-xs-5, .column-sm-5, .column-md-5, .column-lg-5 { - column-count: 1; - column-gap: 0; -} - -.column-xs-1 { column-count: 1; } -.column-xs-2 { column-count: 2; } -.column-xs-3 { column-count: 3; } -.column-xs-4 { column-count: 4; } -.column-xs-5 { column-count: 5; } - -@media (min-width: 768px) { - .column-sm-1 { column-count: 1; } - .column-sm-2 { column-count: 2; } - .column-sm-3 { column-count: 3; } - .column-sm-4 { column-count: 4; } - .column-sm-5 { column-count: 5; } -} - -@media (min-width: 992px) { - .column-md-1 { column-count: 1; } - .column-md-2 { column-count: 2; } - .column-md-3 { column-count: 3; } - .column-md-4 { column-count: 4; } - .column-md-5 { column-count: 5; } -} - -@media (min-width: 1200px) { - .column-lg-1 { column-count: 1; } - .column-lg-2 { column-count: 2; } - .column-lg-3 { column-count: 3; } - .column-lg-4 { column-count: 4; } - .column-lg-5 { column-count: 5; } -} /* Inventaires */ -#inventoryform input[type=number] { +.table-condensed-input input[type=number] { text-align: center; } @@ -557,18 +295,6 @@ thead .tooltip { margin: 0 auto; } -/* Multiple select customizations */ - -.ms-choice { - height: 34px !important; - line-height: 34px !important; - border: 1px solid #ccc !important; - box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important; -} - -.ms-choice > div { - top: 4px !important; -} /* Checkbox select multiple */ diff --git a/kfet/static/kfet/css/libs/columns.css b/kfet/static/kfet/css/libs/columns.css new file mode 100644 index 00000000..34591061 --- /dev/null +++ b/kfet/static/kfet/css/libs/columns.css @@ -0,0 +1,43 @@ +.unbreakable { + display:inline-block; + width: 100%; +} + +.column-xs-1, .column-sm-1, .column-md-1, .column-lg-1, +.column-xs-2, .column-sm-2, .column-md-2, .column-lg-2, +.column-xs-3, .column-sm-3, .column-md-3, .column-lg-3, +.column-xs-4, .column-sm-4, .column-md-4, .column-lg-4, +.column-xs-5, .column-sm-5, .column-md-5, .column-lg-5 { + column-count: 1; +} + +.column-xs-1 { column-count: 1; } +.column-xs-2 { column-count: 2; } +.column-xs-3 { column-count: 3; } +.column-xs-4 { column-count: 4; } +.column-xs-5 { column-count: 5; } + +@media (min-width: 768px) { + .column-sm-1 { column-count: 1; } + .column-sm-2 { column-count: 2; } + .column-sm-3 { column-count: 3; } + .column-sm-4 { column-count: 4; } + .column-sm-5 { column-count: 5; } +} + +@media (min-width: 992px) { + .column-md-1 { column-count: 1; } + .column-md-2 { column-count: 2; } + .column-md-3 { column-count: 3; } + .column-md-4 { column-count: 4; } + .column-md-5 { column-count: 5; } +} + +@media (min-width: 1200px) { + .column-lg-1 { column-count: 1; } + .column-lg-2 { column-count: 2; } + .column-lg-3 { column-count: 3; } + .column-lg-4 { column-count: 4; } + .column-lg-5 { column-count: 5; } +} + diff --git a/kfet/static/kfet/css/jconfirm-kfet.css b/kfet/static/kfet/css/libs/jconfirm-kfet.css similarity index 81% rename from kfet/static/kfet/css/jconfirm-kfet.css rename to kfet/static/kfet/css/libs/jconfirm-kfet.css index 1e05a816..d2803434 100644 --- a/kfet/static/kfet/css/jconfirm-kfet.css +++ b/kfet/static/kfet/css/libs/jconfirm-kfet.css @@ -49,7 +49,7 @@ } .jconfirm .jconfirm-box .buttons button { - width:40px; + min-width:40px; height:100%; margin:0; margin:0 !important; @@ -85,24 +85,3 @@ padding-right: 50px; padding-left: 50px; } - -/* Account autocomplete window */ - -#account_results ul { - list-style-type:none; - background:rgba(255,255,255,0.9); - padding:0; -} - -#account_results li { - display:block; - padding:5px 20px; - height:100%; - width:100%; -} - -#account_results .hilight { - background:rgba(200,16,46,0.9); - color:#fff; - text-decoration:none; -} diff --git a/kfet/static/kfet/css/libs/multiple-select-kfet.css b/kfet/static/kfet/css/libs/multiple-select-kfet.css new file mode 100644 index 00000000..145968d3 --- /dev/null +++ b/kfet/static/kfet/css/libs/multiple-select-kfet.css @@ -0,0 +1,14 @@ +/** + * Multiple Select plugin customizations + */ + +.ms-choice { + height: 34px !important; + line-height: 34px !important; + border: 1px solid #ccc !important; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075) !important; +} + +.ms-choice > div { + top: 4px !important; +} diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 72ae675a..c977d534 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -30,7 +30,7 @@ class KfetWebsocket { constructor(data) { $.extend(this, this.constructor.defaults, data); } - + get url() { var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; var location_host = window.location.host; @@ -184,3 +184,13 @@ function requestAuth(data, callback, focus_next = null) { }); } + + +/** + * Setup jquery-confirm + */ + +jconfirm.defaults = { + confirmButton: '', + cancelButton: '' +}; diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index d185b604..9baa08c4 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -7,7 +7,7 @@ var self = this; var element = $(target); - var content = $("
"); + var content = $("
"); var buttons; function dictToArray (dict, start) { @@ -153,9 +153,8 @@ // initialize the interface function initialize (data) { // creates the bar with the buttons - buttons = $("
", - {class: "btn-group btn-group-justified", - role: "group", + buttons = $("
Date
@@ -68,6 +66,6 @@
-
+ {% endblock %} diff --git a/kfet/templates/kfet/account_create.html b/kfet/templates/kfet/account_create.html index b9e050b4..59fc1d56 100644 --- a/kfet/templates/kfet/account_create.html +++ b/kfet/templates/kfet/account_create.html @@ -1,4 +1,4 @@ -{% extends "kfet/base_col_1.html" %} +{% extends "kfet/base_form.html" %} {% load staticfiles %} {% block title %}Nouveau compte{% endblock %} @@ -8,9 +8,7 @@ {% endblock %} -{% block main-class %}content-form{% endblock %} - -{% block main-content %} +{% block main %}
-

Les mots contenant des caractères non alphanumériques seront ignorés

@@ -62,7 +59,6 @@ // et de ladisponibilité du trigramme choisi $('#id_trigramme').on('input', function() { var trigramme = $('#id_trigramme').val().toUpperCase(); - var container = '#trigramme_valid'; var pattern = /^[^a-z]{3}$/; if (!(trigramme.match(pattern))) { diff --git a/kfet/templates/kfet/account_create_special.html b/kfet/templates/kfet/account_create_special.html index e7a14f87..ecd6ac22 100644 --- a/kfet/templates/kfet/account_create_special.html +++ b/kfet/templates/kfet/account_create_special.html @@ -10,7 +10,7 @@ {% block main-class %}content-form{% endblock %} -{% block main-content %} +{% block main %} {% csrf_token %} diff --git a/kfet/templates/kfet/account_group.html b/kfet/templates/kfet/account_group.html index e7243258..6663bc0e 100644 --- a/kfet/templates/kfet/account_group.html +++ b/kfet/templates/kfet/account_group.html @@ -3,53 +3,59 @@ {% block title %}Groupes de comptes{% endblock %} {% block header-title %}Groupes de comptes{% endblock %} -{% block fixed-content %} +{% block fixed %} -
- Créer un groupe + {% endblock %} -{% block main-content %} +{% block main %} {% for group in groups %} -
-
- - - +
+
+ {{ group.name }} +
-

{{ group.name }}

-
-
-

Permissions

+
+

Comptes

+
+ +
+

Permissions

+
{% regroup group.permissions.all by content_type as grouped_perms %}
    {% for perms_group in grouped_perms %} -
  • {{ perms_group.grouper|title }} -
      - {% for perm in perms_group.list %} -
    • {{ perm.name }}
    • - {% endfor %} +
    • + {{ perms_group.grouper|title }} +
        + {% for perm in perms_group.list %} +
      • {{ perm.name }}
      • + {% endfor %}
      - {% endfor %} -
    -
-
-

Comptes

-
-
+
{% endfor %} {% endblock %} diff --git a/kfet/templates/kfet/account_group_form.html b/kfet/templates/kfet/account_group_form.html index 45c3ac5b..90e3aa36 100644 --- a/kfet/templates/kfet/account_group_form.html +++ b/kfet/templates/kfet/account_group_form.html @@ -1,4 +1,4 @@ -{% extends 'kfet/base_col_1.html' %} +{% extends 'kfet/base_form.html' %} {% load staticfiles %} {% load widget_tweaks %} @@ -10,9 +10,7 @@ {% block title %}Permissions - Édition{% endblock %} {% block header-title %}Modification des permissions{% endblock %} -{% block main-class %}content-form{% endblock %} - -{% block main-content %} +{% block main %} {% csrf_token %} @@ -23,8 +21,12 @@ K-Fêt {{ form.name|add_class:"form-control" }}
- {% if form.name.errors %}{{ form.name.errors }}{% endif %} - {% if form.name.help_text %}{{ form.name.help_text }}{% endif %} + {% if form.name.errors %} + {{ form.name.errors }} + {% endif %} + {% if form.name.help_text %} + {{ form.name.help_text }} + {% endif %}
{% include "kfet/form_field_snippet.html" with field=form.permissions %} diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index 741ad9dd..066cb832 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -1,72 +1,76 @@ {% extends "kfet/base_col_2.html" %} {% block title %}Comptes - Négatifs{% endblock %} -{% block header-title %}Comptes en négatifs{% endblock %} +{% block header-title %}Comptes en négatif{% endblock %} -{% block fixed-content %} +{% block fixed %} -
-
{{ negatives|length }}
-
compte{{ negatives|length|pluralize }} en négatif
-
-
Total: {{ negatives_sum|floatformat:2 }}€
+ + {% if perms.kfet.change_settings %} -
- Modifier les valeurs par défaut +
+
+
{% endif %} {% endblock %} -{% block main-content %} +{% block main %} -
-

Liste des comptes en négatifs

-
- - - - - - - - - - - - - - - {% for neg in negatives %} - - - - - - - - - - - {% endfor %} - -
Tri.NomBalanceRéelleDébutDécouvert autoriséJusqu'auBalance offset
- - {{ neg.account.trigramme }} - - {{ neg.account.name }}{{ neg.account.balance|floatformat:2 }}€ - {% if neg.balance_offset %} - {{ neg.account.real_balance|floatformat:2 }}€ - {% endif %} - {{ neg.start|date:'d/m/Y H:i:s'}}{{ neg.authz_overdraft_amount|default_if_none:'' }}{{ neg.authz_overdrafy_until|default_if_none:'' }}{{ neg.balance_offset|default_if_none:'' }}
-
+
+ + + + + + + + + + + + + + + {% for neg in negatives %} + + + + + + + + + + + {% endfor %} + +
Tri.NomBalanceRéelleDébutDécouvert autoriséJusqu'auBalance offset
+ + {{ neg.account.trigramme }} + + {{ neg.account.name }}{{ neg.account.balance|floatformat:2 }}€ + {% if neg.balance_offset %} + {{ neg.account.real_balance|floatformat:2 }}€ + {% endif %} + {{ neg.start|date:'d/m/Y H:i:s'}}{{ neg.authz_overdraft_amount|default_if_none:'' }}{{ neg.authz_overdrafy_until|default_if_none:'' }}{{ neg.balance_offset|default_if_none:'' }}
{% endblock %} diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index fc8babc5..15238b64 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -48,38 +48,43 @@ $(document).ready(function() { {% include "kfet/base_footer.html" %} {% endblock %} -{% block fixed-content %} +{% block fixed %} {% include "kfet/left_account.html" %} {% endblock %} -{% block main-content %} +{% block main %}
+ {% if account.user == request.user %} -
-

Statistiques

-
-

Ma balance

-
-

Ma consommation

-
-
-
+
+
+
+

Ma balance

+
+

Ma consommation

+
+
+
+
{% endif %} -
- {% if addcosts %} -

Gagné des majorations

-
-
    - {% for addcost in addcosts %} -
  • {{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€
  • - {% endfor %} -
-
- {% endif %} -

Historique

-
-
+ +
+
+ {% if addcosts %} +

Gagné des majorations

+
+
    + {% for addcost in addcosts %} +
  • {{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€
  • + {% endfor %} +
+
+ {% endif %} +
+
+
+
@@ -9,96 +9,104 @@ {% block title %}Article - {{ article.name }}{% endblock %} {% block header-title %}Informations sur l'article {{ article.name }}{% endblock %} -{% block fixed-content %} +{% block fixed %} -
-
{{ article.name }}
-
{{ article.category }}
-
- + + + {% endblock %} -{% block main-content %} +{% block main %} -
-

Historique

-
-
-

Inventaires

-
- - - - - - - - - - {% for inventoryart in inventoryarts %} - - - - - - {% endfor %} - -
DateStockErreur
- - {{ inventoryart.inventory.at }} - - {{ inventoryart.stock_new }}{{ inventoryart.stock_error }}
-
-
-
-

Prix fournisseurs

-
- - - - - - - - - - - - {% for supplierart in supplierarts %} - - - - - - - - {% endfor %} - -
DateFournisseurHTTVADroits
{{ supplierart.at }}{{ supplierart.supplier.name }}{{ supplierart.price_HT }}{{ supplierart.TVA }}{{ supplierart.rights }}
-
-
-
-
-
-

Statistiques

+
+ +
+ +
+
+
+
+ +

Inventaires récents

+
+ {% include "kfet/article_inventories_snippet.html" with inventoryarts=inventoryarts|slice:5 %} +
+ +
+
+ +

Derniers prix fournisseurs

+
+ {% include "kfet/article_suppliers_snippet.html" with supplierarts=supplierarts|slice:5 %} +
+ +
+
+
+
+ +

Ventes

+
+ +
+ +
+
+ {% include "kfet/article_inventories_snippet.html" %} +
+
+ +
+
+ {% include "kfet/article_suppliers_snippet.html" %} +
+
+
diff --git a/kfet/templates/kfet/article_suppliers_snippet.html b/kfet/templates/kfet/article_suppliers_snippet.html new file mode 100644 index 00000000..bd5970fd --- /dev/null +++ b/kfet/templates/kfet/article_suppliers_snippet.html @@ -0,0 +1,22 @@ + + + + + + + + + + + + {% for supplierart in supplierarts %} + + + + + + + + {% endfor %} + +
DateFournisseurHTTVADroits
{{ supplierart.at }}{{ supplierart.supplier.name }}{{ supplierart.price_HT|default_if_none:"" }}{{ supplierart.TVA|default_if_none:"" }}{{ supplierart.rights|default_if_none:"" }}
diff --git a/kfet/templates/kfet/article_update.html b/kfet/templates/kfet/article_update.html index d451df94..3f09e48e 100644 --- a/kfet/templates/kfet/article_update.html +++ b/kfet/templates/kfet/article_update.html @@ -1,12 +1,10 @@ -{% extends "kfet/base_col_1.html" %} +{% extends "kfet/base_form.html" %} {% block title %}{{ article.name }} - Édition{% endblock %} {% block header-title %}Édition de l'article {{ article.name }}{% endblock %} -{% block main-class %}content-form{% endblock %} +{% block main %} -{% block main-content %} - -{% include "kfet/base_form.html" with authz=perms.kfet.change_article submit_text="Mettre à jour"%} +{% include "kfet/form_full_snippet.html" with authz=perms.kfet.change_article submit_text="Mettre à jour"%} {% endblock %} diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index b18ee719..ecd69c3d 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -1,5 +1,4 @@ -{% load staticfiles %} -{% load menu_tags %} +{% load static menu_tags %} @@ -33,17 +32,17 @@ - {% main_menu template="kfet/base_nav.html" %} + {% flat_menu "kfet-nav" template="kfet/base_nav.html" apply_active_classes=True %}
{% block header %} {% if not page or not page.no_header %} -
-
-

+
+
+

{% block header-title %}{% endblock %}

-

+ {% endif %} {% endblock %} diff --git a/kfet/templates/kfet/base_col_1.html b/kfet/templates/kfet/base_col_1.html index 6bd6dd5c..03fea5e7 100644 --- a/kfet/templates/kfet/base_col_1.html +++ b/kfet/templates/kfet/base_col_1.html @@ -4,11 +4,11 @@ {% block content %} -
-
+
+
{% include "kfet/base_messages.html" %} -
- {% block main-content %}{% endblock %} +
+ {% block main %}{% endblock %}
diff --git a/kfet/templates/kfet/base_col_2.html b/kfet/templates/kfet/base_col_2.html index 2a1f8cd4..d0ac45b7 100644 --- a/kfet/templates/kfet/base_col_2.html +++ b/kfet/templates/kfet/base_col_2.html @@ -2,16 +2,16 @@ {% block content %} -
-
-
- {% block fixed-content %}{% endblock %} +
+
+
+ {% block fixed %}{% endblock %}
-
+
{% include "kfet/base_messages.html" %} -
- {% block main-content %}{% endblock %} +
+ {% block main %}{% endblock %}
diff --git a/kfet/templates/kfet/base_col_mult.html b/kfet/templates/kfet/base_col_mult.html new file mode 100644 index 00000000..e5bafc19 --- /dev/null +++ b/kfet/templates/kfet/base_col_mult.html @@ -0,0 +1,18 @@ +{% extends "kfet/base.html" %} + +{% block header-class %}text-center{% endblock %} + +{% block content %} + +
+
+ {% include "kfet/base_messages.html" %} +
+
+ {% block main %}{% endblock %} +
+
+
+
+ +{% endblock %} diff --git a/kfet/templates/kfet/base_footer.html b/kfet/templates/kfet/base_footer.html index be524139..c5333476 100644 --- a/kfet/templates/kfet/base_footer.html +++ b/kfet/templates/kfet/base_footer.html @@ -2,7 +2,7 @@ {% with "k-fet@ens.fr" as kfet_mail %} -