Merge branch 'master' into aureplop/kpsul_js_refactor

This commit is contained in:
Ludovic Stephan 2018-01-10 18:55:33 +01:00
commit b62f0293dd
163 changed files with 7015 additions and 3038 deletions

5
.gitignore vendored
View file

@ -9,3 +9,8 @@ venv/
/src /src
media/ media/
*.log *.log
*.sqlite3
# PyCharm
.idea
.cache

View file

@ -1,24 +1,20 @@
services: services:
- mysql:latest - postgres:latest
- redis:latest - redis:latest
variables: variables:
# GestioCOF settings # GestioCOF settings
DJANGO_SETTINGS_MODULE: "cof.settings_dev" DJANGO_SETTINGS_MODULE: "cof.settings.prod"
DBNAME: "cof_gestion" DBHOST: "postgres"
DBUSER: "cof_gestion"
DBPASSWD: "cof_password"
DBHOST: "mysql"
REDIS_HOST: "redis" REDIS_HOST: "redis"
# Cached packages # Cached packages
PYTHONPATH: "$CI_PROJECT_DIR/vendor/python" PYTHONPATH: "$CI_PROJECT_DIR/vendor/python"
# mysql service configuration # postgres service configuration
MYSQL_DATABASE: "$DBNAME" POSTGRES_PASSWORD: "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
MYSQL_USER: "$DBUSER" POSTGRES_USER: "cof_gestion"
MYSQL_PASSWORD: "$DBPASSWD" POSTGRES_DB: "cof_gestion"
MYSQL_ROOT_PASSWORD: "root_password"
cache: cache:
@ -29,13 +25,12 @@ cache:
before_script: before_script:
- mkdir -p vendor/{python,pip,apt} - mkdir -p vendor/{python,pip,apt}
- apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq mysql-client - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client
- mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST" - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py
-e "GRANT ALL ON test_$DBNAME.* TO '$DBUSER'@'%'"
# Remove the old test database if it has not been done yet # Remove the old test database if it has not been done yet
- mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host="$DBHOST" - psql --username=cof_gestion --password="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" --host="$DBHOST"
-e "DROP DATABASE test_$DBNAME" || true -e "DROP DATABASE test_$DBNAME" || true
- pip install --cache-dir vendor/pip -t vendor/python -r requirements-devel.txt - pip install --cache-dir vendor/pip -t vendor/python -r requirements.txt
test: test:
stage: test stage: test

118
README.md
View file

@ -66,119 +66,65 @@ car par défaut Django n'écoute que sur l'adresse locale de la machine virtuell
or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à or vous voudrez accéder à GestioCOF depuis votre machine physique. L'url à
entrer dans le navigateur est `localhost:8000`. entrer dans le navigateur est `localhost:8000`.
#### Serveur de développement type production #### Serveur de développement type production
Sur la VM Vagrant, un serveur apache est configuré pour servir GestioCOF de Juste histoire de jouer, pas indispensable pour développer :
façon similaire à la version en production : on utilise
La VM Vagrant héberge en plus un serveur nginx configuré pour servir GestioCOF
comme en production : on utilise
[Daphne](https://github.com/django/daphne/) et `python manage.py runworker` [Daphne](https://github.com/django/daphne/) et `python manage.py runworker`
derrière un reverse-proxy apache. Le tout est monitoré par derrière un reverse-proxy nginx.
[supervisor](http://supervisord.org/).
Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url Ce serveur se lance tout seul et est accessible en dehors de la VM à l'url
`localhost:8080`. Toutefois il ne se recharge pas tout seul lorsque le code `localhost:8080/gestion/`. Toutefois il ne se recharge pas tout seul lorsque le
change, il faut relancer le worker avec `sudo supervisorctl restart worker` pour code change, il faut relancer le worker avec `sudo systemctl restart
visualiser la dernière version du code. worker.service` pour visualiser la dernière version du code.
### Installation manuelle ### Installation manuelle
Si vous optez pour une installation manuelle plutôt que d'utiliser Vagrant, il Vous pouvez opter pour une installation manuelle plutôt que d'utiliser Vagrant,
est fortement conseillé d'utiliser un environnement virtuel pour Python. il est fortement conseillé d'utiliser un environnement virtuel pour Python.
Il vous faudra installer pip, les librairies de développement de python, un Il vous faudra installer pip, les librairies de développement de python ainsi
client et un serveur MySQL ainsi qu'un serveur redis ; sous Debian et dérivées que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous
(Ubuntu, ...) : Debian et dérivées (Ubuntu, ...) :
sudo apt-get install python-pip python-dev libmysqlclient-dev redis-server sudo apt-get install python3-pip python3-dev sqlite3
Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; Si vous décidez d'utiliser un environnement virtuel Python (virtualenv;
fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF
(le dossier où se trouve ce README), et créez-le maintenant : (le dossier où se trouve ce README), et créez-le maintenant :
virtualenv env -p $(which python3) python3 -m venv venv
L'option `-p` sert à préciser l'exécutable python à utiliser. Vous devez choisir Pour l'activer, il faut faire
python3, si c'est la version de python par défaut sur votre système, ceci n'est
pas nécessaire. Pour l'activer, il faut faire
. env/bin/activate . venv/bin/activate
dans le même dossier. dans le même dossier.
Vous pouvez maintenant installer les dépendances Python depuis le fichier Vous pouvez maintenant installer les dépendances Python depuis le fichier
`requirements-devel.txt` : `requirements-devel.txt` :
pip install -U pip
pip install -r requirements-devel.txt pip install -r requirements-devel.txt
Copiez le fichier `cof/settings_dev.py` dans `cof/settings.py`. Pour terminer, copier le fichier `cof/settings/secret_example.py` vers
`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique
pour profiter de façon transparente des mises à jour du fichier:
#### Installation avec MySQL ln -s secret_example.py cof/settings/secret.py
Il faut maintenant installer MySQL. Si vous n'avez pas déjà MySQL installé sur
votre serveur, il faut l'installer ; sous Debian et dérivées (Ubuntu, ...) :
sudo apt-get install mysql-server
Il vous demandera un mot de passe pour le compte d'administration MySQL,
notez-le quelque part (ou n'en mettez pas, le serveur n'est accessible que
localement par défaut). Si vous utilisez une autre distribution, consultez la
documentation de votre distribution pour savoir comment changer ce mot de passe
et démarrer le serveur MySQL (c'est automatique sous Ubuntu).
Vous devez alors créer un utilisateur local et une base `cof_gestion`, avec le
mot de passe de votre choix (remplacez `mot_de_passe`) :
mysql -uroot -e "CREATE DATABASE cof_gestion; GRANT ALL PRIVILEGES ON cof_gestion.* TO 'cof_gestion'@'localhost' IDENTIFIER BY 'mot_de_passe'"
Éditez maintenant le fichier `cof/settings.py` pour y intégrer ces changements ;
la définition de `DATABASES` doit ressembler à (à nouveau, remplacez
`mot_de_passe` de façon appropriée) :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'cof_gestion',
'USER': 'cof_gestion',
'PASSWORD': 'mot_de_passe',
}
}
#### Installation avec SQLite
GestioCOF est installé avec MySQL sur la VM COF, et afin d'avoir un
environnement de développement aussi proche que possible de ce qui tourne en
vrai pour éviter les mauvaises surprises, il est conseillé d'utiliser MySQL sur
votre machine de développement également. Toutefois, GestioCOF devrait
fonctionner avec d'autres moteurs SQL, et certains préfèrent utiliser SQLite
pour sa légèreté et facilité d'installation.
Si vous décidez d'utiliser SQLite, il faut l'installer ; sous Debian et dérivées :
sudo apt-get install sqlite3
puis éditer le fichier `cof/settings.py` pour que la définition de `DATABASES`
ressemble à :
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
#### Fin d'installation #### Fin d'installation
Il ne vous reste plus qu'à initialiser les modèles de Django avec la commande suivante : Il ne vous reste plus qu'à initialiser les modèles de Django et peupler la base
de donnée avec les données nécessaires au bon fonctionnement de GestioCOF + des
données bidons bien pratiques pour développer avec la commande suivante :
python manage.py migrate bash provisioning/prepare_django.sh
Charger les mails indispensables au bon fonctionnement de GestioCOF :
python manage.py syncmails
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 loaddevdata
Vous êtes prêts à développer ! Lancer GestioCOF en faisant Vous êtes prêts à développer ! Lancer GestioCOF en faisant
@ -188,7 +134,7 @@ Vous êtes prêts à développer ! Lancer GestioCOF en faisant
Pour mettre à jour les paquets Python, utiliser la commande suivante : Pour mettre à jour les paquets Python, utiliser la commande suivante :
pip install --upgrade -r requirements.txt -r requirements-devel.txt pip install --upgrade -r requirements-devel.txt
Pour mettre à jour les modèles après une migration, il faut ensuite faire : Pour mettre à jour les modèles après une migration, il faut ensuite faire :
@ -197,6 +143,6 @@ Pour mettre à jour les modèles après une migration, il faut ensuite faire :
## Documentation utilisateur ## Documentation utilisateur
Une brève documentation utilisateur pour se familiariser plus vite avec l'outil Une brève documentation utilisateur est accessible sur le
est accessible sur le [wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home) pour avoir une
[wiki](https://git.eleves.ens.fr/cof-geek/gestioCOF/wikis/home). idée de la façon dont le COF utilise GestioCOF.

View file

@ -0,0 +1 @@

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,25 +8,43 @@ the local development server should be here.
import os import os
# Database credentials
try: try:
from .secret import DBNAME, DBUSER, DBPASSWD from . import secret
except ImportError: except ImportError:
# On the local development VM, theses credentials are in the environment raise ImportError(
DBNAME = os.environ["DBNAME"] "The secret.py file is missing.\n"
DBUSER = os.environ["DBUSER"] "For a development environment, simply copy secret_example.py"
DBPASSWD = os.environ["DBPASSWD"]
except KeyError:
raise RuntimeError("Secrets missing")
# Other secrets
try:
from .secret import (
SECRET_KEY, RECAPTCHA_PUBLIC_KEY, RECAPTCHA_PRIVATE_KEY, ADMINS,
REDIS_PASSWD, REDIS_DB, REDIS_HOST, REDIS_PORT
) )
except ImportError:
raise RuntimeError("Secrets missing")
def import_secret(name):
"""
Shorthand for importing a value from the secret module and raising an
informative exception if a secret is missing.
"""
try:
return getattr(secret, name)
except AttributeError:
raise RuntimeError("Secret missing: {}".format(name))
SECRET_KEY = import_secret("SECRET_KEY")
ADMINS = import_secret("ADMINS")
SERVER_EMAIL = import_secret("SERVER_EMAIL")
DBNAME = import_secret("DBNAME")
DBUSER = import_secret("DBUSER")
DBPASSWD = import_secret("DBPASSWD")
REDIS_PASSWD = import_secret("REDIS_PASSWD")
REDIS_DB = import_secret("REDIS_DB")
REDIS_HOST = import_secret("REDIS_HOST")
REDIS_PORT = import_secret("REDIS_PORT")
RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY")
RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY")
KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN")
BASE_DIR = os.path.dirname( BASE_DIR = os.path.dirname(
@ -52,11 +70,28 @@ INSTALLED_APPS = [
'django_cas_ng', 'django_cas_ng',
'bootstrapform', 'bootstrapform',
'kfet', 'kfet',
'kfet.open',
'channels', 'channels',
'widget_tweaks', 'widget_tweaks',
'django_js_reverse', 'django_js_reverse',
'custommail', 'custommail',
'djconfig', '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 = [ MIDDLEWARE_CLASSES = [
@ -70,6 +105,8 @@ MIDDLEWARE_CLASSES = [
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
'djconfig.middleware.DjConfigMiddleware', 'djconfig.middleware.DjConfigMiddleware',
'wagtail.wagtailcore.middleware.SiteMiddleware',
'wagtail.wagtailredirects.middleware.RedirectMiddleware',
] ]
ROOT_URLCONF = 'cof.urls' ROOT_URLCONF = 'cof.urls'
@ -88,6 +125,7 @@ TEMPLATES = [
'django.core.context_processors.i18n', 'django.core.context_processors.i18n',
'django.core.context_processors.media', 'django.core.context_processors.media',
'django.core.context_processors.static', 'django.core.context_processors.static',
'wagtailmenus.context_processors.wagtailmenus',
'djconfig.context_processors.config', 'djconfig.context_processors.config',
'gestioncof.shared.context_processor', 'gestioncof.shared.context_processor',
'kfet.context_processors.auth', 'kfet.context_processors.auth',
@ -99,7 +137,7 @@ TEMPLATES = [
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.mysql', 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': DBNAME, 'NAME': DBNAME,
'USER': DBUSER, 'USER': DBUSER,
'PASSWORD': DBPASSWD, 'PASSWORD': DBPASSWD,
@ -144,9 +182,12 @@ LOGIN_URL = "cof-login"
LOGIN_REDIRECT_URL = "home" LOGIN_REDIRECT_URL = "home"
CAS_SERVER_URL = 'https://cas.eleves.ens.fr/' CAS_SERVER_URL = 'https://cas.eleves.ens.fr/'
CAS_VERSION = '3'
CAS_LOGIN_MSG = None
CAS_IGNORE_REFERER = True CAS_IGNORE_REFERER = True
CAS_REDIRECT_URL = '/' CAS_REDIRECT_URL = '/'
CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" CAS_EMAIL_FORMAT = "%s@clipper.ens.fr"
AUTHENTICATION_BACKENDS = ( AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 'django.contrib.auth.backends.ModelBackend',
'gestioncof.shared.COFCASBackend', 'gestioncof.shared.COFCASBackend',
@ -155,6 +196,18 @@ AUTHENTICATION_BACKENDS = (
RECAPTCHA_USE_SSL = True RECAPTCHA_USE_SSL = True
# Cache settings
CACHES = {
'default': {
'BACKEND': 'redis_cache.RedisCache',
'LOCATION': 'redis://:{passwd}@{host}:{port}/db'
.format(passwd=REDIS_PASSWD, host=REDIS_HOST,
port=REDIS_PORT, db=REDIS_DB),
}
}
# Channels settings # Channels settings
CHANNEL_LAYERS = { CHANNEL_LAYERS = {
@ -167,8 +220,14 @@ CHANNEL_LAYERS = {
port=REDIS_PORT, db=REDIS_DB) port=REDIS_PORT, db=REDIS_DB)
)], )],
}, },
"ROUTING": "cof.routing.channel_routing", "ROUTING": "cof.routing.routing",
} }
} }
FORMAT_MODULE_PATH = 'cof.locale' FORMAT_MODULE_PATH = 'cof.locale'
# Wagtail settings
WAGTAIL_SITE_NAME = 'GestioCOF'
WAGTAIL_ENABLE_UPDATE_CHECK = False
TAGGIT_CASE_INSENSITIVE = True

View file

@ -3,9 +3,8 @@ Django development settings for the cof project.
The settings that are not listed here are imported from .common The settings that are not listed here are imported from .common
""" """
import os from .common import * # NOQA
from .common import INSTALLED_APPS, MIDDLEWARE_CLASSES
from .common import *
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
@ -18,9 +17,9 @@ DEBUG = True
# --- # ---
STATIC_URL = '/static/' STATIC_URL = '/static/'
STATIC_ROOT = '/var/www/static/' STATIC_ROOT = '/srv/gestiocof/static/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media/') MEDIA_ROOT = '/srv/gestiocof/media/'
MEDIA_URL = '/media/' MEDIA_URL = '/media/'

36
cof/settings/local.py Normal file
View file

@ -0,0 +1,36 @@
"""
Django local settings for the cof project.
The settings that are not listed here are imported from .common
"""
import os
from .dev import * # NOQA
from .dev import BASE_DIR
# Use sqlite for local development
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3")
}
}
# Use the default cache backend for local development
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache"
}
}
# Use the default in memory asgi backend for local development
CHANNEL_LAYERS = {
"default": {
"BACKEND": "asgiref.inmemory.ChannelLayer",
"ROUTING": "cof.routing.routing",
}
}
# No need to run collectstatic -> unset STATIC_ROOT
STATIC_ROOT = None

View file

@ -5,7 +5,8 @@ The settings that are not listed here are imported from .common
import os import os
from .common import * from .common import * # NOQA
from .common import BASE_DIR
DEBUG = False DEBUG = False
@ -16,11 +17,14 @@ ALLOWED_HOSTS = [
"dev.cof.ens.fr" "dev.cof.ens.fr"
] ]
STATIC_ROOT = os.path.join(os.path.dirname(BASE_DIR), "static")
STATIC_ROOT = os.path.join(
os.path.dirname(os.path.dirname(BASE_DIR)),
"public",
"gestion",
"static",
)
STATIC_URL = "/gestion/static/" STATIC_URL = "/gestion/static/"
MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media")
MEDIA_URL = "/gestion/media/" MEDIA_URL = "/gestion/media/"
LDAP_SERVER_URL = "ldaps://ldap.spi.ens.fr:636"
EMAIL_HOST = "nef.ens.fr"

View file

@ -1,8 +1,20 @@
SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah'
RECAPTCHA_PUBLIC_KEY = "DUMMY" ADMINS = None
RECAPTCHA_PRIVATE_KEY = "DUMMY" SERVER_EMAIL = "root@vagrant"
DBUSER = "cof_gestion"
DBNAME = "cof_gestion"
DBPASSWD = "4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4"
REDIS_PASSWD = "dummy" REDIS_PASSWD = "dummy"
REDIS_PORT = 6379 REDIS_PORT = 6379
REDIS_DB = 0 REDIS_DB = 0
REDIS_HOST = "127.0.0.1" REDIS_HOST = "127.0.0.1"
ADMINS = None
RECAPTCHA_PUBLIC_KEY = "DUMMY"
RECAPTCHA_PRIVATE_KEY = "DUMMY"
EMAIL_HOST = None
KFETOPEN_TOKEN = "plop"
LDAP_SERVER_URL = None

View file

@ -16,6 +16,10 @@ from django.contrib.auth import views as django_views
from django_cas_ng import views as django_cas_views from django_cas_ng import views as django_cas_views
from django_js_reverse.views import urls_js from django_js_reverse.views import urls_js
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 import views as gestioncof_views, csv_views
from gestioncof.urls import export_patterns, petitcours_patterns, \ from gestioncof.urls import export_patterns, petitcours_patterns, \
surveys_patterns, events_patterns, calendar_patterns, \ surveys_patterns, events_patterns, calendar_patterns, \
@ -50,7 +54,7 @@ urlpatterns = [
url(r'^outsider/login$', gestioncof_views.login_ext), url(r'^outsider/login$', gestioncof_views.login_ext),
url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}), url(r'^outsider/logout$', django_views.logout, {'next_page': 'home'}),
url(r'^login$', gestioncof_views.login, name="cof-login"), 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 # Infos persos
url(r'^profile$', gestioncof_views.profile), url(r'^profile$', gestioncof_views.profile),
url(r'^outsider/password-change$', django_views.password_change), url(r'^outsider/password-change$', django_views.password_change),
@ -85,6 +89,10 @@ urlpatterns = [
url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente), url(r'^utile_bda/bda_revente$', gestioncof_views.liste_bdarevente),
url(r'^k-fet/', include('kfet.urls')), url(r'^k-fet/', include('kfet.urls')),
url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'), url(r'^jsreverse/$', cache_page(3600)(urls_js), name='js_reverse'),
url(r'^cms/', include(wagtailadmin_urls)),
url(r'^documents/', include(wagtaildocs_urls)),
# djconfig
url(r"^config", gestioncof_views.ConfigUpdate.as_view()),
] ]
if 'debug_toolbar' in settings.INSTALLED_APPS: if 'debug_toolbar' in settings.INSTALLED_APPS:
@ -93,7 +101,13 @@ if 'debug_toolbar' in settings.INSTALLED_APPS:
url(r'^__debug__/', include(debug_toolbar.urls)), url(r'^__debug__/', include(debug_toolbar.urls)),
] ]
if settings.DEBUG:
# Si on est en production, MEDIA_ROOT est servi par Apache. # 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. # Il faut dire à Django de servir MEDIA_ROOT lui-même en développement.
urlpatterns += static(settings.MEDIA_URL, urlpatterns += static(settings.MEDIA_URL,
document_root=settings.MEDIA_ROOT) document_root=settings.MEDIA_ROOT)
# Wagtail for uncatched
urlpatterns += [
url(r'', include(wagtail_urls)),
]

View file

@ -0,0 +1 @@
default_app_config = 'gestioncof.apps.GestioncofConfig'

View file

@ -1,9 +1,3 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms from django import forms
from django.contrib import admin from django.contrib import admin
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
@ -18,13 +12,12 @@ from django.contrib.auth.admin import UserAdmin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.db.models import Q from django.db.models import Q
import django.utils.six as six
import autocomplete_light import autocomplete_light
def add_link_field(target_model='', field='', link_text=six.text_type, def add_link_field(target_model='', field='', link_text=str,
desc_text=six.text_type): desc_text=str):
def add_link(cls): def add_link(cls):
reverse_name = target_model or cls.model.__name__.lower() reverse_name = target_model or cls.model.__name__.lower()
@ -139,7 +132,6 @@ def ProfileInfo(field, short_description, boolean=False):
User.profile_login_clipper = FkeyLookup("profile__login_clipper", User.profile_login_clipper = FkeyLookup("profile__login_clipper",
"Login clipper") "Login clipper")
User.profile_num = FkeyLookup("profile__num", "Numéro")
User.profile_phone = ProfileInfo("phone", "Téléphone") User.profile_phone = ProfileInfo("phone", "Téléphone")
User.profile_occupation = ProfileInfo("occupation", "Occupation") User.profile_occupation = ProfileInfo("occupation", "Occupation")
User.profile_departement = ProfileInfo("departement", "Departement") User.profile_departement = ProfileInfo("departement", "Departement")
@ -166,10 +158,12 @@ class UserProfileAdmin(UserAdmin):
is_cof.short_description = 'Membre du COF' is_cof.short_description = 'Membre du COF'
is_cof.boolean = True is_cof.boolean = True
list_display = ('profile_num',) + UserAdmin.list_display \ list_display = (
UserAdmin.list_display
+ ('profile_login_clipper', 'profile_phone', 'profile_occupation', + ('profile_login_clipper', 'profile_phone', 'profile_occupation',
'profile_mailing_cof', 'profile_mailing_bda', 'profile_mailing_cof', 'profile_mailing_bda',
'profile_mailing_bda_revente', 'is_cof', 'is_buro', ) 'profile_mailing_bda_revente', 'is_cof', 'is_buro', )
)
list_display_links = ('username', 'email', 'first_name', 'last_name') list_display_links = ('username', 'email', 'first_name', 'last_name')
list_filter = UserAdmin.list_filter \ list_filter = UserAdmin.list_filter \
+ ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof', + ('profile__is_cof', 'profile__is_buro', 'profile__mailing_cof',
@ -215,21 +209,17 @@ class UserProfileAdmin(UserAdmin):
# FIXME: This is absolutely horrible. # FIXME: This is absolutely horrible.
def user_unicode(self): def user_str(self):
if self.first_name and self.last_name: if self.first_name and self.last_name:
return "%s %s (%s)" % (self.first_name, self.last_name, self.username) return "{} ({})".format(self.get_full_name(), self.username)
else: else:
return self.username return self.username
if six.PY2: User.__str__ = user_str
User.__unicode__ = user_unicode
else:
User.__str__ = user_unicode
class EventRegistrationAdmin(admin.ModelAdmin): class EventRegistrationAdmin(admin.ModelAdmin):
form = autocomplete_light.modelform_factory(EventRegistration, exclude=[]) form = autocomplete_light.modelform_factory(EventRegistration, exclude=[])
list_display = ('__unicode__' if six.PY2 else '__str__', 'event', 'user', list_display = ('__str__', 'event', 'user', 'paid')
'paid')
list_filter = ('paid',) list_filter = ('paid',)
search_fields = ('user__username', 'user__first_name', 'user__last_name', search_fields = ('user__username', 'user__first_name', 'user__last_name',
'user__email', 'event__title') 'user__email', 'event__title')

15
gestioncof/apps.py Normal file
View file

@ -0,0 +1,15 @@
from django.apps import AppConfig
class GestioncofConfig(AppConfig):
name = 'gestioncof'
verbose_name = "Gestion des adhérents du COF"
def ready(self):
from . import signals
self.register_config()
def register_config(self):
import djconfig
from .forms import GestioncofConfigForm
djconfig.register(GestioncofConfigForm)

View file

@ -1,21 +1,14 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django import forms from django import forms
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.forms.widgets import RadioSelect, CheckboxSelectMultiple from django.forms.widgets import RadioSelect, CheckboxSelectMultiple
from django.forms.formsets import BaseFormSet, formset_factory from django.forms.formsets import BaseFormSet, formset_factory
from django.db.models import Max
from django.core.validators import MinLengthValidator from djconfig.forms import ConfigForm
from gestioncof.models import CofProfile, EventCommentValue, \ from gestioncof.models import CofProfile, EventCommentValue, \
CalendarSubscription, Club CalendarSubscription, Club
from gestioncof.widgets import TriStateCheckbox from gestioncof.widgets import TriStateCheckbox
from gestioncof.shared import lock_table, unlock_table
from bda.models import Spectacle from bda.models import Spectacle
@ -203,9 +196,6 @@ class RegistrationUserForm(forms.ModelForm):
super(RegistrationUserForm, self).__init__(*args, **kw) super(RegistrationUserForm, self).__init__(*args, **kw)
self.fields['username'].help_text = "" self.fields['username'].help_text = ""
def force_long_username(self):
self.fields['username'].validators = [MinLengthValidator(9)]
class Meta: class Meta:
model = User model = User
fields = ("username", "first_name", "last_name", "email") fields = ("username", "first_name", "last_name", "email")
@ -243,7 +233,6 @@ class RegistrationProfileForm(forms.ModelForm):
self.fields['mailing_cof'].initial = True self.fields['mailing_cof'].initial = True
self.fields['mailing_bda'].initial = True self.fields['mailing_bda'].initial = True
self.fields['mailing_bda_revente'].initial = True self.fields['mailing_bda_revente'].initial = True
self.fields['num'].widget.attrs['readonly'] = True
self.fields.keyOrder = [ self.fields.keyOrder = [
'login_clipper', 'login_clipper',
@ -251,7 +240,6 @@ class RegistrationProfileForm(forms.ModelForm):
'occupation', 'occupation',
'departement', 'departement',
'is_cof', 'is_cof',
'num',
'type_cotiz', 'type_cotiz',
'mailing_cof', 'mailing_cof',
'mailing_bda', 'mailing_bda',
@ -259,24 +247,9 @@ class RegistrationProfileForm(forms.ModelForm):
'comments' 'comments'
] ]
def save(self, *args, **kw):
instance = super(RegistrationProfileForm, self).save(*args, **kw)
if instance.is_cof and not instance.num:
# Generate new number
try:
lock_table(CofProfile)
aggregate = CofProfile.objects.aggregate(Max('num'))
instance.num = aggregate['num__max'] + 1
instance.save()
self.cleaned_data['num'] = instance.num
self.data['num'] = instance.num
finally:
unlock_table(CofProfile)
return instance
class Meta: class Meta:
model = CofProfile model = CofProfile
fields = ("login_clipper", "num", "phone", "occupation", fields = ("login_clipper", "phone", "occupation",
"departement", "is_cof", "type_cotiz", "mailing_cof", "departement", "is_cof", "type_cotiz", "mailing_cof",
"mailing_bda", "mailing_bda_revente", "comments") "mailing_bda", "mailing_bda_revente", "comments")
@ -403,3 +376,16 @@ class ClubsForm(forms.Form):
queryset=Club.objects.all(), queryset=Club.objects.all(),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False) required=False)
# ---
# Announcements banner
# TODO: move this to the `gestion` app once the supportBDS branch is merged
# ---
class GestioncofConfigForm(ConfigForm):
gestion_banner = forms.CharField(
label=_("Announcements banner"),
help_text=_("An empty banner disables annoucements"),
max_length=2048
)

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0010_delete_custommail'),
]
operations = [
migrations.AlterField(
model_name='cofprofile',
name='login_clipper',
field=models.CharField(verbose_name='Login clipper', blank=True, max_length=32),
),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0010_delete_custommail'),
]
operations = [
migrations.RemoveField(
model_name='cofprofile',
name='num',
),
]

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('gestioncof', '0011_remove_cofprofile_num'),
('gestioncof', '0011_longer_clippers'),
]
operations = [
]

View file

@ -1,11 +1,7 @@
# -*- coding: utf-8 -*-
from django.db import models from django.db import models
from django.dispatch import receiver from django.dispatch import receiver
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.encoding import python_2_unicode_compatible
import django.utils.six as six
from django.db.models.signals import post_save, post_delete from django.db.models.signals import post_save, post_delete
from gestioncof.petits_cours_models import choices_length from gestioncof.petits_cours_models import choices_length
@ -35,12 +31,12 @@ TYPE_COMMENT_FIELD = (
) )
@python_2_unicode_compatible
class CofProfile(models.Model): class CofProfile(models.Model):
user = models.OneToOneField(User, related_name="profile") user = models.OneToOneField(User, related_name="profile")
login_clipper = models.CharField("Login clipper", max_length=8, blank=True) login_clipper = models.CharField(
"Login clipper", max_length=32, blank=True
)
is_cof = models.BooleanField("Membre du COF", default=False) is_cof = models.BooleanField("Membre du COF", default=False)
num = models.IntegerField("Numéro d'adhérent", blank=True, default=0)
phone = models.CharField("Téléphone", max_length=20, blank=True) phone = models.CharField("Téléphone", max_length=20, blank=True)
occupation = models.CharField(_("Occupation"), occupation = models.CharField(_("Occupation"),
default="1A", default="1A",
@ -72,7 +68,7 @@ class CofProfile(models.Model):
verbose_name_plural = "Profils COF" verbose_name_plural = "Profils COF"
def __str__(self): def __str__(self):
return six.text_type(self.user.username) return self.user.username
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
@ -86,7 +82,6 @@ def post_delete_user(sender, instance, *args, **kwargs):
instance.user.delete() instance.user.delete()
@python_2_unicode_compatible
class Club(models.Model): class Club(models.Model):
name = models.CharField("Nom", max_length=200, unique=True) name = models.CharField("Nom", max_length=200, unique=True)
description = models.TextField("Description", blank=True) description = models.TextField("Description", blank=True)
@ -98,7 +93,6 @@ class Club(models.Model):
return self.name return self.name
@python_2_unicode_compatible
class Event(models.Model): class Event(models.Model):
title = models.CharField("Titre", max_length=200) title = models.CharField("Titre", max_length=200)
location = models.CharField("Lieu", max_length=200) location = models.CharField("Lieu", max_length=200)
@ -115,10 +109,9 @@ class Event(models.Model):
verbose_name = "Événement" verbose_name = "Événement"
def __str__(self): def __str__(self):
return six.text_type(self.title) return self.title
@python_2_unicode_compatible
class EventCommentField(models.Model): class EventCommentField(models.Model):
event = models.ForeignKey(Event, related_name="commentfields") event = models.ForeignKey(Event, related_name="commentfields")
name = models.CharField("Champ", max_length=200) name = models.CharField("Champ", max_length=200)
@ -130,10 +123,9 @@ class EventCommentField(models.Model):
verbose_name = "Champ" verbose_name = "Champ"
def __str__(self): def __str__(self):
return six.text_type(self.name) return self.name
@python_2_unicode_compatible
class EventCommentValue(models.Model): class EventCommentValue(models.Model):
commentfield = models.ForeignKey(EventCommentField, related_name="values") commentfield = models.ForeignKey(EventCommentField, related_name="values")
registration = models.ForeignKey("EventRegistration", registration = models.ForeignKey("EventRegistration",
@ -144,7 +136,6 @@ class EventCommentValue(models.Model):
return "Commentaire de %s" % self.commentfield return "Commentaire de %s" % self.commentfield
@python_2_unicode_compatible
class EventOption(models.Model): class EventOption(models.Model):
event = models.ForeignKey(Event, related_name="options") event = models.ForeignKey(Event, related_name="options")
name = models.CharField("Option", max_length=200) name = models.CharField("Option", max_length=200)
@ -154,10 +145,9 @@ class EventOption(models.Model):
verbose_name = "Option" verbose_name = "Option"
def __str__(self): def __str__(self):
return six.text_type(self.name) return self.name
@python_2_unicode_compatible
class EventOptionChoice(models.Model): class EventOptionChoice(models.Model):
event_option = models.ForeignKey(EventOption, related_name="choices") event_option = models.ForeignKey(EventOption, related_name="choices")
value = models.CharField("Valeur", max_length=200) value = models.CharField("Valeur", max_length=200)
@ -167,10 +157,9 @@ class EventOptionChoice(models.Model):
verbose_name_plural = "Choix" verbose_name_plural = "Choix"
def __str__(self): def __str__(self):
return six.text_type(self.value) return self.value
@python_2_unicode_compatible
class EventRegistration(models.Model): class EventRegistration(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
event = models.ForeignKey(Event) event = models.ForeignKey(Event)
@ -184,11 +173,9 @@ class EventRegistration(models.Model):
unique_together = ("user", "event") unique_together = ("user", "event")
def __str__(self): def __str__(self):
return "Inscription de %s à %s" % (six.text_type(self.user), return "Inscription de {} à {}".format(self.user, self.event.title)
six.text_type(self.event.title))
@python_2_unicode_compatible
class Survey(models.Model): class Survey(models.Model):
title = models.CharField("Titre", max_length=200) title = models.CharField("Titre", max_length=200)
details = models.TextField("Détails", blank=True) details = models.TextField("Détails", blank=True)
@ -199,10 +186,9 @@ class Survey(models.Model):
verbose_name = "Sondage" verbose_name = "Sondage"
def __str__(self): def __str__(self):
return six.text_type(self.title) return self.title
@python_2_unicode_compatible
class SurveyQuestion(models.Model): class SurveyQuestion(models.Model):
survey = models.ForeignKey(Survey, related_name="questions") survey = models.ForeignKey(Survey, related_name="questions")
question = models.CharField("Question", max_length=200) question = models.CharField("Question", max_length=200)
@ -212,10 +198,9 @@ class SurveyQuestion(models.Model):
verbose_name = "Question" verbose_name = "Question"
def __str__(self): def __str__(self):
return six.text_type(self.question) return self.question
@python_2_unicode_compatible
class SurveyQuestionAnswer(models.Model): class SurveyQuestionAnswer(models.Model):
survey_question = models.ForeignKey(SurveyQuestion, related_name="answers") survey_question = models.ForeignKey(SurveyQuestion, related_name="answers")
answer = models.CharField("Réponse", max_length=200) answer = models.CharField("Réponse", max_length=200)
@ -224,10 +209,9 @@ class SurveyQuestionAnswer(models.Model):
verbose_name = "Réponse" verbose_name = "Réponse"
def __str__(self): def __str__(self):
return six.text_type(self.answer) return self.answer
@python_2_unicode_compatible
class SurveyAnswer(models.Model): class SurveyAnswer(models.Model):
user = models.ForeignKey(User) user = models.ForeignKey(User)
survey = models.ForeignKey(Survey) survey = models.ForeignKey(Survey)
@ -244,7 +228,6 @@ class SurveyAnswer(models.Model):
self.survey.title) self.survey.title)
@python_2_unicode_compatible
class CalendarSubscription(models.Model): class CalendarSubscription(models.Model):
token = models.UUIDField() token = models.UUIDField()
user = models.OneToOneField(User) user = models.OneToOneField(User)

View file

@ -12,15 +12,15 @@ from django.views.decorators.csrf import csrf_exempt
from django.conf import settings from django.conf import settings
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib import messages from django.contrib import messages
from django.db import transaction
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
from gestioncof.petits_cours_models import ( from gestioncof.petits_cours_models import (
PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter, PetitCoursDemande, PetitCoursAttribution, PetitCoursAttributionCounter,
PetitCoursAbility, PetitCoursSubject PetitCoursAbility
) )
from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet from gestioncof.petits_cours_forms import DemandeForm, MatieresFormSet
from gestioncof.decorators import buro_required from gestioncof.decorators import buro_required
from gestioncof.shared import lock_table, unlock_tables
class DemandeListView(ListView): class DemandeListView(ListView):
@ -274,17 +274,17 @@ def _traitement_post(request, demande):
headers={'Reply-To': replyto})) headers={'Reply-To': replyto}))
connection = mail.get_connection(fail_silently=False) connection = mail.get_connection(fail_silently=False)
connection.send_messages(mails_to_send) connection.send_messages(mails_to_send)
lock_table(PetitCoursAttributionCounter, PetitCoursAttribution, User) with transaction.atomic():
for matiere in proposals: for matiere in proposals:
for rank, user in enumerate(proposals[matiere]): for rank, user in enumerate(proposals[matiere]):
counter = PetitCoursAttributionCounter.objects.get(user=user, counter = PetitCoursAttributionCounter.objects.get(
matiere=matiere) user=user, matiere=matiere
counter.count += 1 )
counter.save() counter.count += 1
attrib = PetitCoursAttribution(user=user, matiere=matiere, counter.save()
demande=demande, rank=rank + 1) attrib = PetitCoursAttribution(user=user, matiere=matiere,
attrib.save() demande=demande, rank=rank + 1)
unlock_tables() attrib.save()
demande.traitee = True demande.traitee = True
demande.traitee_par = request.user demande.traitee_par = request.user
demande.processed = datetime.now() demande.processed = datetime.now()
@ -309,17 +309,15 @@ def inscription(request):
profile.petits_cours_accept = "receive_proposals" in request.POST profile.petits_cours_accept = "receive_proposals" in request.POST
profile.petits_cours_remarques = request.POST["remarques"] profile.petits_cours_remarques = request.POST["remarques"]
profile.save() profile.save()
lock_table(PetitCoursAttributionCounter, PetitCoursAbility, User, with transaction.atomic():
PetitCoursSubject) abilities = (
abilities = ( PetitCoursAbility.objects.filter(user=request.user).all()
PetitCoursAbility.objects.filter(user=request.user).all()
)
for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
) )
unlock_tables() for ability in abilities:
PetitCoursAttributionCounter.get_uptodate(
ability.user,
ability.matiere
)
success = True success = True
formset = MatieresFormSet(instance=request.user) formset = MatieresFormSet(instance=request.user)
else: else:

View file

@ -1,69 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import division
from __future__ import print_function
from __future__ import unicode_literals
from django.contrib.sites.models import Site
from django.conf import settings from django.conf import settings
from django.contrib.sites.models import Site
from django_cas_ng.backends import CASBackend 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 django.db import connection
from gestioncof.models import CofProfile from gestioncof.models import CofProfile
User = get_user_model()
class COFCASBackend(CASBackend): 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 # Le CAS de l'ENS accepte les logins avec des espaces au début
# et à la fin, ainsi quavec une casse variable. On normalise pour # et à la fin, ainsi quavec une casse variable. On normalise pour
# éviter les doublons. # éviter les doublons.
username = username.strip().lower() return username.strip().lower()
profiles = CofProfile.objects.filter(login_clipper=username) def configure_user(self, user):
if len(profiles) > 0: clipper = user.username
profile = profiles.order_by('-is_cof')[0] user.profile.login_clipper = clipper
user = profile.user user.profile.save()
return user user.email = settings.CAS_EMAIL_FORMAT % clipper
try: user.save()
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()
return user return user
@ -74,25 +30,3 @@ def context_processor(request):
"site": Site.objects.get_current(), "site": Site.objects.get_current(),
} }
return data return data
def lock_table(*models):
query = "LOCK TABLES "
for i, model in enumerate(models):
table = model._meta.db_table
if i > 0:
query += ", "
query += "%s WRITE" % table
cursor = connection.cursor()
cursor.execute(query)
row = cursor.fetchone()
return row
def unlock_tables(*models):
cursor = connection.cursor()
cursor.execute("UNLOCK TABLES")
row = cursor.fetchone()
return row
unlock_table = unlock_tables

23
gestioncof/signals.py Normal file
View file

@ -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)

View file

@ -40,8 +40,9 @@ a {
background: transparent; background: transparent;
} }
div.empty-form {
padding-bottom: 2em;
}
a:hover { a:hover {
color: #444; color: #444;
@ -341,10 +342,12 @@ fieldset legend {
font-weight: 700; font-weight: 700;
font-size: large; font-size: large;
color:#DE826B; color:#DE826B;
padding-bottom: .5em;
} }
#main-content h4 { #main-content h4 {
color:#DE826B; color:#DE826B;
padding-bottom: .5em;
} }
#main-content h2, #main-content h2,
@ -778,6 +781,17 @@ header .open > .dropdown-toggle.btn-default {
border-color: white; border-color: white;
} }
/* Announcements banner ------------------ */
#banner {
background-color: #d86b01;
width: 100%;
text-align: center;
padding: 10px;
color: white;
font-size: larger;
}
/* FORMS --------------------------------- */ /* FORMS --------------------------------- */

View file

@ -8,13 +8,13 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
{# CSS #} {# CSS #}
<link type="text/css" rel="stylesheet" href="{% static "css/bootstrap.min.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "css/bootstrap.min.css" %}" />
<link type="text/css" rel="stylesheet" href="{% static "css/cof.css" %}" /> <link type="text/css" rel="stylesheet" href="{% static "css/cof.css" %}" />
<link href="https://fonts.googleapis.com/css?family=Dosis|Dosis:700|Raleway|Roboto:300,300i,700" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Dosis|Dosis:700|Raleway|Roboto:300,300i,700" rel="stylesheet">
<link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}"> <link rel="stylesheet" href="{% static "font-awesome/css/font-awesome.min.css" %}">
{# JS #} {# JS #}
<script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script> <script src="https://code.jquery.com/jquery-3.1.0.min.js" integrity="sha256-cCueBR6CsyA4/9szpPfrX3s49M9vUU5BgtiJj06wt/s=" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}

View file

@ -0,0 +1,23 @@
{% extends "base_title.html" %}
{% load bootstrap %}
{% load i18n %}
{% block page_size %}col-sm-8{%endblock%}
{% block realcontent %}
<h2>{% trans "Global configuration" %}</h2>
<form id="profile form-horizontal" method="post" action="">
<div class="row" style="margin: 0 15%;">
{% csrf_token %}
<fieldset"center-block">
{% for field in form %}
{{ field | bootstrap }}
{% endfor %}
</fieldset>
</div>
<div class="form-actions">
<input type="submit" class="btn btn-primary pull-right"
value={% trans "Save" %} />
</div>
</form>
{% endblock %}

View file

@ -16,6 +16,14 @@
<h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2> <h2 class="member-status">{% if user.first_name %}{{ user.first_name }}{% else %}<tt>{{ user.username }}</tt>{% endif %}, {% if user.profile.is_cof %}<tt class="user-is-cof">au COF{% else %}<tt class="user-is-not-cof">non-COF{% endif %}</tt></h2>
</div><!-- /.container --> </div><!-- /.container -->
</header> </header>
{% if config.gestion_banner %}
<div id="banner" class="container">
<span class="glyphicon glyphicon-bullhorn"></span>
<span>{{ config.gestion_banner }}</span>
</div>
{% endif %}
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="messages"> <div class="messages">

View file

@ -1,4 +1,5 @@
{% extends "gestioncof/base_header.html" %} {% extends "gestioncof/base_header.html" %}
{% load wagtailcore_tags %}
{% block homelink %} {% block homelink %}
{% endblock %} {% endblock %}
@ -55,7 +56,8 @@
<h3 class="block-title">K-Fêt<span class="pull-right"><i class="fa fa-coffee"></i></span></h3> <h3 class="block-title">K-Fêt<span class="pull-right"><i class="fa fa-coffee"></i></span></h3>
<div class="hm-block"> <div class="hm-block">
<ul> <ul>
<li><a href="{% url "kfet.home" %}">Page d'accueil</a></li> {# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #}
<li><a href="{% slugurl "k-fet" %}">Page d'accueil</a></li>
<li><a href="https://www.cof.ens.fr/k-fet/calendrier">Calendrier</a></li> <li><a href="https://www.cof.ens.fr/k-fet/calendrier">Calendrier</a></li>
{% if perms.kfet.is_team %} {% if perms.kfet.is_team %}
<li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li> <li><a href="{% url 'kfet.kpsul' %}">K-Psul</a></li>

View file

@ -10,7 +10,7 @@ export_patterns = [
url(r'^mega/avecremarques$', views.export_mega_remarksonly), url(r'^mega/avecremarques$', views.export_mega_remarksonly),
url(r'^mega/participants$', views.export_mega_participants), url(r'^mega/participants$', views.export_mega_participants),
url(r'^mega/orgas$', views.export_mega_orgas), url(r'^mega/orgas$', views.export_mega_orgas),
url(r'^mega/(?P<type>.+)$', views.export_mega_bytype), # url(r'^mega/(?P<type>.+)$', views.export_mega_bytype),
url(r'^mega$', views.export_mega), url(r'^mega$', views.export_mega),
] ]

View file

@ -1,5 +1,3 @@
# -*- coding: utf-8 -*-
import unicodecsv import unicodecsv
import uuid import uuid
from datetime import timedelta from datetime import timedelta
@ -9,12 +7,18 @@ from custommail.shortcuts import send_custom_mail
from django.shortcuts import redirect, get_object_or_404, render from django.shortcuts import redirect, get_object_or_404, render
from django.http import Http404, HttpResponse, HttpResponseForbidden from django.http import Http404, HttpResponse, HttpResponseForbidden
from django.contrib.auth.decorators import login_required 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.auth.models import User
from django.contrib.sites.models import Site 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.utils import timezone
from django.utils.translation import ugettext_lazy as _
from django.contrib import messages from django.contrib import messages
import django.utils.six as six
from django_cas_ng.views import logout as cas_logout_view
from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \
SurveyQuestionAnswer SurveyQuestionAnswer
@ -24,10 +28,11 @@ from gestioncof.models import EventCommentField, EventCommentValue, \
CalendarSubscription CalendarSubscription
from gestioncof.models import CofProfile, Club from gestioncof.models import CofProfile, Club
from gestioncof.decorators import buro_required, cof_required from gestioncof.decorators import buro_required, cof_required
from gestioncof.forms import UserProfileForm, EventStatusFilterForm, \ from gestioncof.forms import (
SurveyForm, SurveyStatusFilterForm, RegistrationUserForm, \ UserProfileForm, EventStatusFilterForm, SurveyForm, SurveyStatusFilterForm,
RegistrationProfileForm, EventForm, CalendarForm, EventFormset, \ RegistrationUserForm, RegistrationProfileForm, EventForm, CalendarForm,
RegistrationPassUserForm, ClubsForm EventFormset, RegistrationPassUserForm, ClubsForm, GestioncofConfigForm
)
from bda.models import Tirage, Spectacle from bda.models import Tirage, Spectacle
@ -81,15 +86,21 @@ def login_ext(request):
@login_required @login_required
def logout(request): def logout(request, next_page=None):
try: if next_page is None:
profile = request.user.profile next_page = request.GET.get('next', None)
except CofProfile.DoesNotExist:
profile, created = CofProfile.objects.get_or_create(user=request.user) profile = getattr(request.user, 'profile', None)
if profile.login_clipper:
return redirect("django_cas_ng.views.logout") if profile and profile.login_clipper:
msg = _('Déconnexion de GestioCOF et CAS réussie. À bientôt {}.')
logout_view = cas_logout_view
else: 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 @login_required
@ -387,7 +398,6 @@ def registration_form2(request, login_clipper=None, username=None,
elif not login_clipper: elif not login_clipper:
# new user # new user
user_form = RegistrationPassUserForm() user_form = RegistrationPassUserForm()
user_form.force_long_username()
profile_form = RegistrationProfileForm() profile_form = RegistrationProfileForm()
event_formset = EventFormset(events=events, prefix='events') event_formset = EventFormset(events=events, prefix='events')
clubs_form = ClubsForm() clubs_form = ClubsForm()
@ -403,12 +413,8 @@ def registration_form2(request, login_clipper=None, username=None,
def registration(request): def registration(request):
if request.POST: if request.POST:
request_dict = request.POST.copy() request_dict = request.POST.copy()
# num ne peut pas être défini manuellement
if "num" in request_dict:
del request_dict["num"]
member = None member = None
login_clipper = None login_clipper = None
success = False
# ----- # -----
# Remplissage des formulaires # Remplissage des formulaires
@ -430,12 +436,10 @@ def registration(request):
user_form = RegistrationUserForm(request_dict, instance=member) user_form = RegistrationUserForm(request_dict, instance=member)
if member.profile.login_clipper: if member.profile.login_clipper:
login_clipper = member.profile.login_clipper login_clipper = member.profile.login_clipper
else:
user_form.force_long_username()
except User.DoesNotExist: except User.DoesNotExist:
user_form.force_long_username() pass
else: else:
user_form.force_long_username() pass
# ----- # -----
# Validation des formulaires # Validation des formulaires
@ -445,7 +449,6 @@ def registration(request):
member = user_form.save() member = user_form.save()
profile, _ = CofProfile.objects.get_or_create(user=member) profile, _ = CofProfile.objects.get_or_create(user=member)
was_cof = profile.is_cof was_cof = profile.is_cof
request_dict["num"] = profile.num
# Maintenant on remplit le formulaire de profil # Maintenant on remplit le formulaire de profil
profile_form = RegistrationProfileForm(request_dict, profile_form = RegistrationProfileForm(request_dict,
instance=profile) instance=profile)
@ -499,16 +502,18 @@ def registration(request):
for club in clubs_form.cleaned_data['clubs']: for club in clubs_form.cleaned_data['clubs']:
club.membres.add(member) club.membres.add(member)
club.save() club.save()
success = True
# Messages # ---
if success: # Success
msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été " # ---
"enregistrée avec succès"
.format(member.get_full_name(), member.email)) msg = ("L'inscription de {:s} (<tt>{:s}</tt>) a été "
if member.profile.is_cof: "enregistrée avec succès."
msg += "Il est désormais membre du COF n°{:d} !".format( .format(member.get_full_name(), member.email))
member.profile.num) if profile.is_cof:
messages.success(request, msg, extra_tags='safe') msg += "\nIl est désormais membre du COF n°{:d} !".format(
member.profile.id)
messages.success(request, msg, extra_tags='safe')
return render(request, "gestioncof/registration_post.html", return render(request, "gestioncof/registration_post.html",
{"user_form": user_form, {"user_form": user_form,
"profile_form": profile_form, "profile_form": profile_form,
@ -572,10 +577,10 @@ def export_members(request):
writer = unicodecsv.writer(response) writer = unicodecsv.writer(response)
for profile in CofProfile.objects.filter(is_cof=True).all(): for profile in CofProfile.objects.filter(is_cof=True).all():
user = profile.user user = profile.user
bits = [profile.num, user.username, user.first_name, user.last_name, bits = [profile.id, user.username, user.first_name, user.last_name,
user.email, profile.phone, profile.occupation, user.email, profile.phone, profile.occupation,
profile.departement, profile.type_cotiz] profile.departement, profile.type_cotiz]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@ -591,78 +596,80 @@ def csv_export_mega(filename, qs):
comments = "---".join( comments = "---".join(
[comment.content for comment in reg.comments.all()]) [comment.content for comment in reg.comments.all()])
bits = [user.username, user.first_name, user.last_name, user.email, bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num, profile.phone, profile.id,
profile.comments if profile.comments else "", comments] profile.comments if profile.comments else "", comments]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@buro_required @buro_required
def export_mega_remarksonly(request): def export_mega_remarksonly(request):
filename = 'remarques_mega_2016.csv' filename = 'remarques_mega_2017.csv'
response = HttpResponse(content_type='text/csv') response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename=' + filename response['Content-Disposition'] = 'attachment; filename=' + filename
writer = unicodecsv.writer(response) writer = unicodecsv.writer(response)
event = Event.objects.get(title="Mega 2016") event = Event.objects.get(title="MEGA 2017")
commentfield = event.commentfields.get(name="Commentaires") commentfield = event.commentfields.get(name="Commentaire")
for val in commentfield.values.all(): for val in commentfield.values.all():
reg = val.registration reg = val.registration
user = reg.user user = reg.user
profile = user.profile profile = user.profile
bits = [user.username, user.first_name, user.last_name, user.email, bits = [user.username, user.first_name, user.last_name, user.email,
profile.phone, profile.num, profile.comments, val.content] profile.phone, profile.id, profile.comments, val.content]
writer.writerow([six.text_type(bit) for bit in bits]) writer.writerow([str(bit) for bit in bits])
return response return response
@buro_required # @buro_required
def export_mega_bytype(request, type): # def export_mega_bytype(request, type):
types = {"orga-actif": "Orga élève", # types = {"orga-actif": "Orga élève",
"orga-branleur": "Orga étudiant", # "orga-branleur": "Orga étudiant",
"conscrit-eleve": "Conscrit élève", # "conscrit-eleve": "Conscrit élève",
"conscrit-etudiant": "Conscrit étudiant"} # "conscrit-etudiant": "Conscrit étudiant"}
#
if type not in types: # if type not in types:
raise Http404 # raise Http404
#
event = Event.objects.get(title="Mega 2016") # event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Type") # type_option = event.options.get(name="Type")
participant_type = type_option.choices.get(value=types[type]).id # participant_type = type_option.choices.get(value=types[type]).id
qs = EventRegistration.objects.filter(event=event).filter( # qs = EventRegistration.objects.filter(event=event).filter(
options__id__exact=participant_type) # options__id__exact=participant_type)
return csv_export_mega(type + '_mega_2016.csv', qs) # return csv_export_mega(type + '_mega_2017.csv', qs)
@buro_required @buro_required
def export_mega_orgas(request): def export_mega_orgas(request):
event = Event.objects.get(title="Mega 2016") event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit ou orga ?") type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Vieux").id participant_type = type_option.choices.get(value="Orga").id
qs = EventRegistration.objects.filter(event=event).exclude( qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type) options__id=participant_type
return csv_export_mega('orgas_mega_2016.csv', qs) )
return csv_export_mega('orgas_mega_2017.csv', qs)
@buro_required @buro_required
def export_mega_participants(request): def export_mega_participants(request):
event = Event.objects.get(title="Mega 2016") event = Event.objects.get(title="MEGA 2017")
type_option = event.options.get(name="Conscrit ou orga ?") type_option = event.options.get(name="Conscrit/Orga ?")
participant_type = type_option.choices.get(value="Conscrit").id participant_type = type_option.choices.get(value="Conscrit").id
qs = EventRegistration.objects.filter(event=event).filter( qs = EventRegistration.objects.filter(event=event).filter(
options__id=participant_type) options__id=participant_type
return csv_export_mega('participants_mega_2016.csv', qs) )
return csv_export_mega('participants_mega_2017.csv', qs)
@buro_required @buro_required
def export_mega(request): def export_mega(request):
event = Event.objects.filter(title="Mega 2016") event = Event.objects.filter(title="MEGA 2017")
qs = EventRegistration.objects.filter(event=event) \ qs = EventRegistration.objects.filter(event=event) \
.order_by("user__username") .order_by("user__username")
return csv_export_mega('all_mega_2016.csv', qs) return csv_export_mega('all_mega_2017.csv', qs)
@buro_required @buro_required
@ -764,3 +771,18 @@ def calendar_ics(request, token):
response = HttpResponse(content=vcal.to_ical()) response = HttpResponse(content=vcal.to_ical())
response['Content-Type'] = "text/calendar" response['Content-Type'] = "text/calendar"
return response 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)

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

@ -0,0 +1 @@
default_app_config = 'kfet.cms.apps.KFetCMSAppConfig'

10
kfet/cms/apps.py Normal file
View file

@ -0,0 +1,10 @@
from django.apps import AppConfig
class KFetCMSAppConfig(AppConfig):
name = 'kfet.cms'
label = 'kfetcms'
verbose_name = 'CMS K-Fêt'
def ready(self):
from . import hooks

View file

@ -0,0 +1,20 @@
from kfet.models import Article
def get_articles(request=None):
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,
}

File diff suppressed because one or more lines are too long

12
kfet/cms/hooks.py Normal file
View file

@ -0,0 +1,12 @@
from django.contrib.staticfiles.templatetags.staticfiles import static
from django.utils.html import format_html
from wagtail.wagtailcore import hooks
@hooks.register('insert_editor_css')
def editor_css():
return format_html(
'<link rel="stylesheet" href="{}">',
static('kfetcms/css/editor.css'),
)

View file

@ -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'])

View file

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import wagtail.wagtailsnippets.blocks
import wagtail.wagtailcore.blocks
import wagtail.wagtailcore.fields
import django.db.models.deletion
import kfet.cms.models
class Migration(migrations.Migration):
dependencies = [
('wagtailcore', '0033_remove_golive_expiry_help_text'),
('wagtailimages', '0019_delete_filter'),
]
operations = [
migrations.CreateModel(
name='KFetPage',
fields=[
('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page')),
('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.StreamField((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses'))))), ('group', wagtail.wagtailcore.blocks.StreamBlock((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses')))))), label='Contenu groupé'))), verbose_name='Contenu')),
('layout', models.CharField(max_length=255, choices=[('kfet/base_col_1.html', 'Une colonne : centrée sur la page'), ('kfet/base_col_2.html', 'Deux colonnes : fixe à gauche, contenu à droite'), ('kfet/base_col_mult.html', 'Contenu scindé sur plusieurs colonnes')], help_text='Comment cette page devrait être affichée ?', verbose_name='Template', default='kfet/base_col_mult.html')),
('main_size', models.CharField(max_length=255, blank=True, verbose_name='Taille de la colonne de contenu')),
('col_count', models.CharField(max_length=255, blank=True, verbose_name='Nombre de colonnes', help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes")),
],
options={
'verbose_name': 'page K-Fêt',
'verbose_name_plural': 'pages K-Fêt',
},
bases=('wagtailcore.page',),
),
migrations.CreateModel(
name='MemberTeam',
fields=[
('id', models.AutoField(verbose_name='ID', auto_created=True, serialize=False, primary_key=True)),
('first_name', models.CharField(blank=True, max_length=255, verbose_name='Prénom', default='')),
('last_name', models.CharField(blank=True, max_length=255, verbose_name='Nom', default='')),
('nick_name', models.CharField(verbose_name='Alias', blank=True, default='', max_length=255)),
('photo', models.ForeignKey(null=True, related_name='+', on_delete=django.db.models.deletion.SET_NULL, verbose_name='Photo', blank=True, to='wagtailimages.Image')),
],
options={
'verbose_name': 'K-Fêt-eux-se',
},
),
]

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfetcms', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='kfetpage',
name='col_count',
field=models.CharField(blank=True, max_length=255, verbose_name='Nombre de colonnes', help_text="S'applique au page dont le contenu est scindé sur plusieurs colonnes."),
),
]

View file

174
kfet/cms/models.py Normal file
View file

@ -0,0 +1,174 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from wagtail.wagtailadmin.edit_handlers import (
FieldPanel, FieldRowPanel, MultiFieldPanel, StreamFieldPanel
)
from wagtail.wagtailcore import blocks
from wagtail.wagtailcore.fields import StreamField
from wagtail.wagtailcore.models import Page
from wagtail.wagtailimages.edit_handlers import ImageChooserPanel
from wagtail.wagtailsnippets.blocks import SnippetChooserBlock
from wagtail.wagtailsnippets.models import register_snippet
from kfet.cms.context_processors import get_articles
@register_snippet
class MemberTeam(models.Model):
first_name = models.CharField(
verbose_name=_('Prénom'),
blank=True, default='', max_length=255,
)
last_name = models.CharField(
verbose_name=_('Nom'),
blank=True, default='', max_length=255,
)
nick_name = models.CharField(
verbose_name=_('Alias'),
blank=True, default='', max_length=255,
)
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')
panels = [
FieldPanel('first_name'),
FieldPanel('last_name'),
FieldPanel('nick_name'),
ImageChooserPanel('photo'),
]
def __str__(self):
return self.get_full_name()
def get_full_name(self):
return '{} {}'.format(self.first_name, self.last_name).strip()
class MenuBlock(blocks.StaticBlock):
class Meta:
icon = 'list-ul'
label = _('Carte')
template = 'kfetcms/block_menu.html'
def get_context(self, *args, **kwargs):
context = super().get_context(*args, **kwargs)
context.update(get_articles())
return context
class GroupTeamBlock(blocks.StructBlock):
show_only = blocks.IntegerBlock(
label=_('Montrer seulement'),
required=False,
help_text=_(
'Nombre initial de membres affichés. Laisser vide pour tou-te-s '
'les afficher.'
),
)
members = blocks.ListBlock(
SnippetChooserBlock(MemberTeam),
label=_('K-Fêt-eux-ses'),
classname='team-group',
)
class Meta:
icon = 'group'
label = _('Groupe de K-Fêt-eux-ses')
template = 'kfetcms/block_teamgroup.html'
class ChoicesStreamBlock(blocks.StreamBlock):
rich = blocks.RichTextBlock(label=_('Éditeur'))
carte = MenuBlock()
group_team = GroupTeamBlock()
class KFetStreamBlock(ChoicesStreamBlock):
group = ChoicesStreamBlock(label=_('Contenu groupé'))
class KFetPage(Page):
content = StreamField(KFetStreamBlock, verbose_name=_('Contenu'))
# Layout fields
TEMPLATE_COL_1 = 'kfet/base_col_1.html'
TEMPLATE_COL_2 = 'kfet/base_col_2.html'
TEMPLATE_COL_MULT = 'kfet/base_col_mult.html'
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é."
),
)
layout = models.CharField(
verbose_name=_('Template'),
choices=[
(TEMPLATE_COL_1, _('Une colonne : centrée sur la page')),
(TEMPLATE_COL_2, _('Deux colonnes : fixe à gauche, contenu à droite')),
(TEMPLATE_COL_MULT, _('Contenu scindé sur plusieurs colonnes')),
],
default=TEMPLATE_COL_MULT, max_length=255,
help_text=_(
"Comment cette page devrait être affichée ?"
),
)
main_size = models.CharField(
verbose_name=_('Taille de la colonne de contenu'),
blank=True, max_length=255,
)
col_count = models.CharField(
verbose_name=_('Nombre de colonnes'),
blank=True, max_length=255,
help_text=_(
"S'applique au page dont le contenu est scindé sur plusieurs colonnes."
),
)
# Panels
content_panels = Page.content_panels + [
StreamFieldPanel('content'),
]
layout_panel = [
FieldPanel('no_header'),
FieldPanel('layout'),
FieldRowPanel([
FieldPanel('main_size'),
FieldPanel('col_count'),
]),
]
settings_panels = [
MultiFieldPanel(layout_panel, _('Affichage'))
] + Page.settings_panels
# Base template
template = "kfetcms/base.html"
class Meta:
verbose_name = _('page K-Fêt')
verbose_name_plural = _('pages K-Fêt')
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
return context

View file

@ -0,0 +1,93 @@
.main.cms {
padding: 20px 15px;
}
@media (min-width: 768px) {
.main.cms {
padding: 35px 30px;
}
}
.cms {
text-align: justify;
font-size: 1.1em;
}
@media (min-width:768px) {
.cms {
font-size: 1.2em;
line-height: 1.6em;
}
}
/* Titles */
.cms h2, .cms 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 h2, .cms h3 {
padding-bottom: 15px;
}
}
/* Paragraphs */
.cms p {
margin-bottom: 20px;
text-indent: 2em;
}
.cms p + :not(h2):not(h3):not(div) {
margin-top: -10px;
}
@media (min-width: 768px) {
.cms p {
padding-bottom: 15px;
}
.cms p + :not(h2):not(h3):not(div) {
margin-top: -30px;
}
}
/* Lists */
.cms ol, .cms ul {
padding: 0 0 0 15px;
margin: 0 0 10px;
}
.cms ul {
list-style-type: square;
}
.cms ol > li, .cms ul > li {
padding-left: 5px;
}
/* Images */
.cms .richtext-image {
max-height: 100%;
margin: 5px 0 15px;
}
.cms .richtext-image.left {
float: left;
margin-right: 30px;
}
.cms .richtext-image.right {
float: right;
margin-left: 30px;
}

View file

@ -0,0 +1,18 @@
.snippets.listing thead, .snippets.listing thead tr {
border: 0;
}
.snippets.listing tbody {
display: block;
column-count: 2;
}
.snippets.listing tbody tr {
display: block;
}
@media (min-width: 992px) {
.snippets.listing tbody {
column-count: 3;
}
}

View file

@ -0,0 +1,3 @@
@import url("base.css");
@import url("menu.css");
@import url("team.css");

View file

@ -0,0 +1,58 @@
.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;
}
.carte .carte-item {
position: relative;
text-align: right;
white-space: nowrap;
padding: 0;
}
.carte .carte-item .filler {
position: absolute;
left: 0;
right: 0;
border-bottom: 2px dotted #333;
height: 75%;
}
.carte .carte-item > span {
position: relative;
}
.carte .carte-item .carte-label {
background: white;
float: left;
padding-right: 10px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.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;
}

View file

@ -0,0 +1,47 @@
.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;
}
}

View file

@ -0,0 +1,41 @@
{% extends page.layout %}
{% load static wagtailcore_tags wagtailuserbar %}
{# CSS/JS #}
{% block extra_head %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "kfetcms/css/index.css" %}">
{% endblock %}
{# Titles #}
{% block title %}{{ page.seo_title }}{% endblock %}
{% block header-title %}{{ page.title }}{% endblock %}
{# Layout #}
{% block main-size %}{{ page.main_size|default:block.super }}{% endblock %}
{% block mult-count %}{{ page.col_count|default:block.super }}{% endblock %}
{% block main-class %}cms main-bg{% endblock %}
{# Content #}
{% block main %}
{% for block in page.content %}
<div class="{% if block.block_type == "rich" or block.block_type == "group" %}unbreakable{% endif %}">
{% include_block block %}
</div>
{% endfor %}
{% wagtailuserbar %}
{% endblock %}
{# Footer #}
{% block footer %}
{% include "kfet/base_footer.html" %}
{% endblock %}

View file

@ -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 %}

View file

@ -0,0 +1,12 @@
<div class="carte {{ class }} unbreakable">
<h3 class="carte-title">{{ title }}</h3>
<ul class="carte-list">
{% for article in articles %}
<li class="carte-item">
<div class="filler"></div>
<span class="carte-label">{{ article.name }}</span>
<span class="carte-ukf">{{ article.price_ukf }} UKF</span>
</li>
{% endfor %}
</ul>
</div>

View file

@ -0,0 +1,66 @@
{% load wagtailcore_tags wagtailimages_tags %}
{% with groupteam=value len=value.members|length %}
<div class="team-group row">
{% if len == 2 %}
<div class="visible-sm col-sm-3"></div>
{% endif %}
{% for member in groupteam.members %}
<div class="
{% if len == 1 %}
col-xs-12
{% else %}
col-xs-6
{% if len == 3 %}
col-sm-4
{% elif len == 2 %}
col-sm-3 col-md-6
{% else %}
col-sm-3 col-md-4 col-lg-3
{% endif %}
{% endif %}
{% if groupteam.show_only != None and forloop.counter0 >= groupteam.show_only %}
member-more
{% endif %}
">
<div class="team-member thumbnail text-center">
{% image member.photo max-200x500 %}
<div class="infos">
<b>{{ member.get_full_name }}</b>
<br>
{% if member.nick_name %}
<i>alias</i> {{ member.nick_name }}
{% endif %}
</div>
</div>
</div>
{% endfor %}
{% if groupteam.show_only != None and len > groupteam.show_only %}
<div class="col-xs-12 col-btn text-center">
<button class="btn btn-primary btn-lg more">
{% if groupteam.show_only %}
Y'en a plus !
{% else %}
Les voir
{% endif %}
</button>
</div>
{% endif %}
</div>
{% endwith %}
<script type="text/javascript">
$( function() {
$('.more').click( function() {
$(this).closest('.col-btn').hide();
$(this).closest('.team-group').children('.member-more').show();
});
});
</script>

View file

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

View file

@ -5,10 +5,9 @@ from decimal import Decimal
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinLengthValidator
from django.contrib.auth.models import User, Group, Permission from django.contrib.auth.models import User, Group, Permission
from django.contrib.contenttypes.models import ContentType 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 django.utils import timezone
from djconfig.forms import ConfigForm from djconfig.forms import ConfigForm
@ -25,18 +24,17 @@ from gestioncof.models import CofProfile
# ----- # -----
class DateTimeWidget(forms.DateTimeInput): class DateTimeWidget(forms.DateTimeInput):
def __init__(self, attrs = None): def __init__(self, *args, **kwargs):
super(DateTimeWidget, self).__init__(attrs) super().__init__(*args, **kwargs)
self.attrs['format'] = '%Y-%m-%d %H:%M' self.attrs['format'] = '%Y-%m-%d %H:%M'
class Media: class Media:
css = { css = {
'all': ('kfet/css/bootstrap-datetimepicker.min.css',) 'all': ('kfet/css/bootstrap-datetimepicker.min.css',)
} }
js = ( js = ('kfet/js/bootstrap-datetimepicker.min.js',)
'kfet/js/moment.js',
'kfet/js/moment-fr.js',
'kfet/js/bootstrap-datetimepicker.min.js',
)
# ----- # -----
# Account forms # Account forms
# ----- # -----
@ -111,21 +109,16 @@ class CofRestrictForm(CofForm):
class Meta(CofForm.Meta): class Meta(CofForm.Meta):
fields = ['departement'] fields = ['departement']
class UserForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
from_clipper = kwargs.pop('from_clipper', False)
new_user = kwargs.get('instance') is None and not from_clipper
super(UserForm, self).__init__(*args, **kwargs)
if new_user:
self.fields['username'].validators = [MinLengthValidator(9)]
class UserForm(forms.ModelForm):
class Meta: class Meta:
model = User model = User
fields = ['username', 'first_name', 'last_name', 'email'] fields = ['username', 'first_name', 'last_name', 'email']
help_texts = { help_texts = {
'username': '' 'username': ''
} }
class UserRestrictForm(UserForm): class UserRestrictForm(UserForm):
class Meta(UserForm.Meta): class Meta(UserForm.Meta):
fields = ['first_name', 'last_name'] fields = ['first_name', 'last_name']
@ -151,19 +144,45 @@ class UserGroupForm(forms.ModelForm):
fields = ['groups'] 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): class GroupForm(forms.ModelForm):
permissions = forms.ModelMultipleChoiceField( permissions = KFetPermissionsField()
queryset= Permission.objects.filter(content_type__in=
ContentType.objects.filter(app_label='kfet')))
def clean_name(self): def clean_name(self):
name = self.cleaned_data['name'] name = self.cleaned_data['name']
return 'K-Fêt %s' % name return 'K-Fêt %s' % name
def clean_permissions(self):
kfet_perms = self.cleaned_data['permissions']
# TODO: With Django >=1.11, the QuerySet method 'difference' can be used.
# other_groups = self.instance.permissions.difference(
# self.fields['permissions'].queryset
# )
other_perms = self.instance.permissions.exclude(
pk__in=[p.pk for p in self.fields['permissions'].queryset],
)
return list(kfet_perms) + list(other_perms)
class Meta: class Meta:
model = Group model = Group
fields = ['name', 'permissions'] fields = ['name', 'permissions']
class AccountNegativeForm(forms.ModelForm): class AccountNegativeForm(forms.ModelForm):
class Meta: class Meta:
model = AccountNegative model = AccountNegative
@ -445,8 +464,11 @@ class KFetConfigForm(ConfigForm):
class FilterHistoryForm(forms.Form): class FilterHistoryForm(forms.Form):
checkouts = forms.ModelMultipleChoiceField(queryset = Checkout.objects.all()) checkouts = forms.ModelMultipleChoiceField(queryset=Checkout.objects.all())
accounts = forms.ModelMultipleChoiceField(queryset = Account.objects.all()) accounts = forms.ModelMultipleChoiceField(queryset=Account.objects.all())
from_date = forms.DateTimeField(widget=DateTimeWidget)
to_date = forms.DateTimeField(widget=DateTimeWidget)
# ----- # -----
# Transfer forms # Transfer forms
@ -525,11 +547,7 @@ class OrderArticleForm(forms.Form):
self.category = kwargs['initial']['category'] self.category = kwargs['initial']['category']
self.category_name = kwargs['initial']['category__name'] self.category_name = kwargs['initial']['category__name']
self.box_capacity = kwargs['initial']['box_capacity'] self.box_capacity = kwargs['initial']['box_capacity']
self.v_s1 = kwargs['initial']['v_s1'] self.v_all = kwargs['initial']['v_all']
self.v_s2 = kwargs['initial']['v_s2']
self.v_s3 = kwargs['initial']['v_s3']
self.v_s4 = kwargs['initial']['v_s4']
self.v_s5 = kwargs['initial']['v_s5']
self.v_moy = kwargs['initial']['v_moy'] self.v_moy = kwargs['initial']['v_moy']
self.v_et = kwargs['initial']['v_et'] self.v_et = kwargs['initial']['v_et']
self.v_prev = kwargs['initial']['v_prev'] self.v_prev = kwargs['initial']['v_prev']

View file

@ -147,3 +147,9 @@ class Command(MyBaseCommand):
# --- # ---
call_command('createopes', '100', '7', '--transfers=20') call_command('createopes', '100', '7', '--transfers=20')
# ---
# Wagtail CMS
# ---
call_command('kfet_loadwagtail')

View file

@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0053_created_at'),
]
operations = [
migrations.AlterField(
model_name='account',
name='promo',
field=models.IntegerField(blank=True, choices=[(1980, 1980), (1981, 1981), (1982, 1982), (1983, 1983), (1984, 1984), (1985, 1985), (1986, 1986), (1987, 1987), (1988, 1988), (1989, 1989), (1990, 1990), (1991, 1991), (1992, 1992), (1993, 1993), (1994, 1994), (1995, 1995), (1996, 1996), (1997, 1997), (1998, 1998), (1999, 1999), (2000, 2000), (2001, 2001), (2002, 2002), (2003, 2003), (2004, 2004), (2005, 2005), (2006, 2006), (2007, 2007), (2008, 2008), (2009, 2009), (2010, 2010), (2011, 2011), (2012, 2012), (2013, 2013), (2014, 2014), (2015, 2015), (2016, 2016), (2017, 2017)], default=2017, null=True),
),
]

View file

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
def forwards_perms(apps, schema_editor):
"""Safely delete content type for old kfet.GlobalPermissions model.
Any permissions (except defaults) linked to this content type are updated
to link at its new content type.
Then, delete the content type. This will delete the three defaults
permissions which are assumed unused.
"""
ContentType = apps.get_model('contenttypes', 'contenttype')
try:
ctype_global = ContentType.objects.get(
app_label="kfet", model="globalpermissions",
)
except ContentType.DoesNotExist:
# We are not migrating from existing data, nothing to do.
return
perms = {
'account': (
'is_team', 'manage_perms', 'manage_addcosts',
'edit_balance_account', 'change_account_password',
'special_add_account',
),
'accountnegative': ('view_negs',),
'inventory': ('order_to_inventory',),
'operation': (
'perform_deposit', 'perform_negative_operations',
'override_frozen_protection', 'cancel_old_operations',
'perform_commented_operations',
),
}
Permission = apps.get_model('auth', 'permission')
global_perms = Permission.objects.filter(content_type=ctype_global)
for modelname, codenames in perms.items():
model = apps.get_model('kfet', modelname)
ctype = ContentType.objects.get_for_model(model)
(
global_perms
.filter(codename__in=codenames)
.update(content_type=ctype)
)
ctype_global.delete()
class Migration(migrations.Migration):
dependencies = [
('kfet', '0054_delete_settings'),
('contenttypes', '__latest__'),
('auth', '__latest__'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('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'))},
),
migrations.AlterModelOptions(
name='accountnegative',
options={'permissions': (('view_negs', 'Voir la liste des négatifs'),)},
),
migrations.AlterModelOptions(
name='inventory',
options={'ordering': ['-at'], 'permissions': (('order_to_inventory', "Générer un inventaire à partir d'une commande"),)},
),
migrations.AlterModelOptions(
name='operation',
options={'permissions': (('perform_deposit', 'Effectuer une charge'), ('perform_negative_operations', 'Enregistrer des commandes en négatif'), ('override_frozen_protection', "Forcer le gel d'un compte"), ('cancel_old_operations', 'Annuler des commandes non récentes'), ('perform_commented_operations', 'Enregistrer des commandes avec commentaires'))},
),
migrations.RunPython(forwards_perms),
]

View file

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0055_move_permissions'),
]
operations = [
migrations.AlterModelOptions(
name='account',
options={'permissions': (('is_team', 'Is part of the team'), ('manage_perms', 'Gérer les permissions K-Fêt'), ('manage_addcosts', 'Gérer les majorations'), ('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_force_close', 'Fermer manuellement la K-Fêt'))},
),
]

View file

@ -0,0 +1,15 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('kfet', '0056_change_account_meta'),
('kfet', '0054_update_promos'),
]
operations = [
]

View file

@ -14,7 +14,8 @@ from datetime import date
import re import re
import hashlib import hashlib
from kfet.config import kfet_config from .config import kfet_config
from .utils import to_ukf
def choices_length(choices): def choices_length(choices):
return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0) return reduce(lambda m, choice: max(m, len(choice[0])), choices, 0)
@ -63,10 +64,23 @@ class Account(models.Model):
unique = True, unique = True,
blank = True, null = True, default = None) blank = True, null = True, default = None)
class Meta:
permissions = (
('is_team', 'Is part of the team'),
('manage_perms', 'Gérer les permissions K-Fêt'),
('manage_addcosts', 'Gérer les majorations'),
('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_force_close', "Fermer manuellement la K-Fêt"),
)
def __str__(self): def __str__(self):
return '%s (%s)' % (self.trigramme, self.name) 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 @property
def user(self): def user(self):
return self.cofprofile.user return self.cofprofile.user
@ -90,6 +104,10 @@ class Account(models.Model):
return self.cofprofile.is_cof return self.cofprofile.is_cof
# Propriétés supplémentaires # Propriétés supplémentaires
@property
def balance_ukf(self):
return to_ukf(self.balance, is_cof=self.is_cof)
@property @property
def real_balance(self): def real_balance(self):
if hasattr(self, 'negative') and self.negative.balance_offset: if hasattr(self, 'negative') and self.negative.balance_offset:
@ -108,6 +126,14 @@ class Account(models.Model):
def need_comment(self): def need_comment(self):
return self.trigramme == '#13' 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 @staticmethod
def is_validandfree(trigramme): def is_validandfree(trigramme):
data = { 'is_valid' : False, 'is_free' : False } data = { 'is_valid' : False, 'is_free' : False }
@ -284,6 +310,15 @@ class AccountNegative(models.Model):
) )
comment = models.CharField("commentaire", max_length=255, blank=True) comment = models.CharField("commentaire", max_length=255, blank=True)
class Meta:
permissions = (
('view_negs', 'Voir la liste des négatifs'),
)
@property
def until_default(self):
return self.start + kfet_config.overdraft_duration
class Checkout(models.Model): class Checkout(models.Model):
created_by = models.ForeignKey( created_by = models.ForeignKey(
@ -436,6 +471,10 @@ class Article(models.Model):
def get_absolute_url(self): def get_absolute_url(self):
return reverse('kfet.article.read', kwargs={'pk': self.pk}) return reverse('kfet.article.read', kwargs={'pk': self.pk})
def price_ukf(self):
return to_ukf(self.price)
class ArticleRule(models.Model): class ArticleRule(models.Model):
article_on = models.OneToOneField( article_on = models.OneToOneField(
Article, on_delete = models.PROTECT, Article, on_delete = models.PROTECT,
@ -462,6 +501,10 @@ class Inventory(models.Model):
class Meta: class Meta:
ordering = ['-at'] ordering = ['-at']
permissions = (
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
)
class InventoryArticle(models.Model): class InventoryArticle(models.Model):
inventory = models.ForeignKey( inventory = models.ForeignKey(
@ -638,6 +681,17 @@ class Operation(models.Model):
max_digits=6, decimal_places=2, max_digits=6, decimal_places=2,
blank=True, null=True, default=None) blank=True, null=True, default=None)
class Meta:
permissions = (
('perform_deposit', 'Effectuer une charge'),
('perform_negative_operations',
'Enregistrer des commandes en négatif'),
('override_frozen_protection', "Forcer le gel d'un compte"),
('cancel_old_operations', 'Annuler des commandes non récentes'),
('perform_commented_operations',
'Enregistrer des commandes avec commentaires'),
)
@property @property
def is_checkout(self): def is_checkout(self):
return (self.type == Operation.DEPOSIT or return (self.type == Operation.DEPOSIT or
@ -658,26 +712,5 @@ class Operation(models.Model):
amount=self.amount) amount=self.amount)
class GlobalPermissions(models.Model):
class Meta:
managed = False
permissions = (
('is_team', 'Is part of the team'),
('perform_deposit', 'Effectuer une charge'),
('perform_negative_operations',
'Enregistrer des commandes en négatif'),
('override_frozen_protection', "Forcer le gel d'un compte"),
('cancel_old_operations', 'Annuler des commandes non récentes'),
('manage_perms', 'Gérer les permissions K-Fêt'),
('manage_addcosts', 'Gérer les majorations'),
('perform_commented_operations', 'Enregistrer des commandes avec commentaires'),
('view_negs', 'Voir la liste des négatifs'),
('order_to_inventory', "Générer un inventaire à partir d'une commande"),
('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")
)
class GenericTeamToken(models.Model): class GenericTeamToken(models.Model):
token = models.CharField(max_length = 50, unique = True) token = models.CharField(max_length = 50, unique = True)

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

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

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

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

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

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

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

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

View file

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

View file

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

View file

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

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

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

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

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

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

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

View file

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

View file

@ -1,16 +1,21 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
print_function, unicode_literals)
from builtins import *
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.dispatch import receiver from django.dispatch import receiver
from django.utils.safestring import mark_safe
@receiver(user_logged_in) @receiver(user_logged_in)
def messages_on_login(sender, request, user, **kwargs): def messages_on_login(sender, request, user, **kwargs):
if (not user.username == 'kfet_genericteam' if (not user.username == 'kfet_genericteam' and
and user.has_perm('kfet.is_team')): user.has_perm('kfet.is_team') and
messages.info(request, '<a href="%s">Connexion en utilisateur partagé ?</a>' % reverse('kfet.login.genericteam'), extra_tags='safe') hasattr(request, 'GET') and
'k-fet' in request.GET.get('next', '')):
messages.info(request, mark_safe(
'<a href="{}" class="genericteam" target="_blank">'
' Connexion en utilisateur partagé ?'
'</a>'
.format(reverse('kfet.login.genericteam'))
))

View file

@ -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;
}

View file

@ -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%;
}
}

View file

@ -0,0 +1,18 @@
.footer {
line-height: 40px;
background: #c63b52;
color: white;
text-align: center;
font-size: 14px;
font-family: Roboto;
}
.footer a {
color: inherit !important;
}
.footer a:hover, .footer a:focus {
text-decoration: underline;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -0,0 +1,118 @@
/* 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;
}
/* Spinning animation -------------- */
.glyphicon.spinning {
animation: spin 1s infinite linear;
}
@keyframes spin {
from { transform: scale(1) rotate(0deg); }
to { transform: scale(1) rotate(360deg); }
}

View file

@ -0,0 +1,151 @@
.navbar {
background: #000;
color: #DDD;
border: 0;
font-family: Roboto;
}
.navbar .navbar-header {
float: left;
display: none;
margin-left: -15px;
margin-right: 0;
}
.navbar .navbar-brand {
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: #fff;
}
.navbar-nav {
font-size: 14px;
margin: 0 0 0 -15px;
float: left;
}
@media (min-width: 460px) {
.navbar .navbar-header {
display: block;
}
.navbar-nav {
margin-left: 0;
}
.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 > .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 3px 3px -4px #000;
}
.navbar-nav .dropdown .dropdown-menu {
padding: 0;
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 10px;
line-height: inherit;
color: #000;
}
.navbar-nav .dropdown .dropdown-menu > li > a:hover,
.navbar-nav .dropdown .dropdown-menu > li > a:focus {
color: #c8102e;
background-color: transparent;
}
.navbar-nav .dropdown .dropdown-menu .divider {
margin: 0;
}
.navbar-nav .dropdown .dropdown-menu {
display: block;
visibility: hidden;
opacity: 0;
transition: opacity 0.15s;
}
.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;
}

View file

@ -9,17 +9,21 @@
#history .day { #history .day {
height:40px; height:40px;
line-height:40px; line-height:40px;
background-color:#c8102e; background-color:rgba(200,16,46,1);
color:#fff; color:#fff;
padding-left:20px; padding-left:20px;
font-family:"Roboto Slab";
font-size:16px; font-size:16px;
font-weight:bold; font-weight:bold;
position:sticky;
top:50px;
z-index:10;
} }
#history .opegroup { #history .opegroup {
height:30px; height:30px;
line-height:30px; line-height:30px;
background-color:rgba(200,16,46,0.85); background-color: #c63b52;
color:#fff; color:#fff;
font-weight:bold; font-weight:bold;
padding-left:20px; padding-left:20px;

View file

@ -1,54 +0,0 @@
ul.carte {
width: 100%;
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 {
position: relative;
text-align: right;
white-space: nowrap;
}
.filler {
position: absolute;
left: 0;
right: 0;
border-bottom: 3px dotted #333;
height: 70%;
}
.carte-label {
background: white;
float: left;
padding-right: 4px;
position: relative;
max-width: calc(100% - 40px);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.carte-ukf {
background: white;
padding-left: 4px;
position: relative;
}
.unbreakable.carte-inverted .carte-ukf,
.unbreakable.carte-inverted .carte-label,
.unbreakable.carte-inverted {
background: #FFDBC7;
}

View file

@ -1,264 +1,116 @@
@import url("nav.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("kpsul.css");
@import url("jconfirm-kfet.css");
@import url("history.css"); @import url("history.css");
body {
margin-top:50px;
font-family:Roboto; .header {
background:#ddd; padding: 15px 20px;
background-color: rgba(200,16,46,1);
color: #FFF;
} }
h1,h2,h3,h4,h5,h6 { .header h1 {
font-family:Oswald; padding: 0;
} margin: 0;
font-weight: bold;
a {
color:#333;
}
a:focus, a:hover {
color:#C8102E;
}
:focus {
outline:none;
}
textarea {
font-family:'Roboto Mono';
border-radius:0 !important;
}
.table {
margin-bottom:0;
border-bottom:1px solid #ddd;
}
.table {
width:100%;
}
.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:rgba(200,16,46,0.9);
color:#fff;
font-weight:bold;
}
.table tr.section td {
border-top:0;
font-size:16px;
padding:8px 30px;
}
.btn, .btn-lg, .btn-group-lg>.btn {
border-radius:0;
}
.btn-primary {
font-family:Oswald;
background-color:rgba(200,16,46,0.9);
color:#FFF;
border:0;
}
.btn-primary:hover, .btn-primary.focus, .btn-primary:focus {
background-color:#000;
color:#FFF;
}
.buttons .nav-pills > li > a {
border-radius:0;
border:1px solid rgba(200,16,46,0.9);
}
.buttons .nav-pills > li.active > a {
background-color:rgba(200,16,46,0.9);
background-clip:padding-box;
}
.row-page-header {
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;
} }
.nopadding { .nopadding {
padding: 0 !important; padding: 0 !important;
} }
.panel-md-margin{ .frozen-account {
background-color: white; background:#5072e0;
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;
}
.content-left-top {
background:#fff;
padding:10px 30px;
}
.content-left .buttons {
background:#fff;
}
.content-left .buttons .btn {
display:block;
}
.content-left-top.frozen-account {
background:#000FBA;
color:#fff; color:#fff;
} }
.content-left .block {
padding-top:25px; .main .table a:not(.btn) {
color: inherit;
} }
.content-left .block .line { .main .table a:not(.btn):focus ,
font-size:16px; .main .table a:not(.btn):hover {
line-height:30px; color: #C81022;
} }
.content-left .line.line-big {
font-family:Oswald;
font-size:60px;
font-weight:bold;
text-align:center;
}
.content-left .line.line-bigsub {
font-size:25px;
font-weight:bold;
text-align:center;
}
.content-left .line.balance {
font-size:45px;
text-align:center;
}
.content-right {
margin:0 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-transparent > div:not(.buttons-title) {
background-color: transparent;
}
.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: 20px 15px 15px;
padding-bottom: 10px;
padding-left: 20px;
font-size:25px;
}
/* /*
* Pages tableaux seuls * Pages tableaux seuls
*/ */
.content-center > div {
background:#fff;
}
.content-center tbody tr:not(.section) td { .table .form-control {
padding:0px 5px !important;
}
.content-center .table .form-control {
padding: 1px 12px ; padding: 1px 12px ;
height:28px; height:28px;
margin:3px 0px; margin:3px 0px;
background: transparent;
} }
.content-center .auth-form {
margin:15px; .table .form-control[disabled], .table .form-control[readonly] {
background: #f5f5f5;
}
.table-condensed-input tbody tr:not(.section) td {
padding:0px 5px;
}
.table-condensed input.form-control {
margin: 0 !important;
border-top: 0;
border-bottom: 0;
border-radius: 0;
}
.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 * Pages formulaires seuls
*/ */
.form-only .content-form { .account_create #id_trigramme {
margin:15px;
background:#fff;
padding:15px;
}
.form-only .account_create #id_trigramme {
display:block; display:block;
width:200px; width:200px;
height:80px; height:80px;
@ -320,38 +172,48 @@ textarea {
padding:5px 20px; padding:5px 20px;
} }
/* /* Account autocomplete window */
* Messages
#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 { .stat-nav {
padding:10px 15px; margin-bottom: 10px;
margin:0; font-family: Roboto;
border:0;
border-radius:0;
} }
.messages .alert-dismissible { .stat-nav li {
padding-right:35px; float: left;
} }
.messages .alert .close { .stat-nav a {
top:0; opacity: 0.6;
right:0; font-family: Roboto;
} }
.messages .alert-info { .stat-nav a:hover,
color:inherit; .stat-nav a.focus, .stat-nav a:focus {
background-color:#ccc; opacity: 1;
}
.messages .alert-error {
color:inherit;
background-color:rgba(200,16,46,0.2);
}
.messages .alert-success {
color:#333;
} }
/* /*
@ -374,7 +236,7 @@ textarea {
margin-top:30px; margin-top:30px;
padding-top:1px; padding-top:1px;
padding-bottom:15px; padding-bottom:15px;
background:rgba(51,51,51,0.7); background:rgba(51,51,51,0.9);
color:#fff; color:#fff;
} }
@ -411,171 +273,56 @@ thead .tooltip {
height: 100px; height: 100px;
} }
/*
* Responsive Columns
*/
.unbreakable {
display:inline-block;
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-count: 1;
}
.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;
}
}
@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;
}
}
@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;
}
}
.help-block {
padding-top: 15px;
}
/* Inventaires */ /* Inventaires */
.table-condensed-input input[type=number] {
text-align: center;
}
.inventory_modified { .inventory_modified {
background:rgba(236,100,0,0.15); background:rgba(236,100,0,0.15);
} }
.stock_diff { .stock_diff {
padding-left: 5px; padding-left: 5px;
color:#C8102E; color:#C8102E;
} }
.inventory_update { .inventory_update {
display:none; display: none;
width: 50px;
margin: 0 auto;
}
/* Checkbox select multiple */
.checkbox-select-multiple label {
font-weight: normal;
margin-bottom: 0;
}
/* Statement creation */
.statement-create-summary table {
margin: 0 auto;
}
.statement-create-summary tr td {
text-align: right;
}
.statement-create-summary tr td:first-child {
padding-right: 15px;
font-weight: bold;
}
.statement-create-summary tr td:last-child {
width: 80px;
}
#detail_taken table td,
#detail_balance table td {
padding: 0;
} }

View file

@ -18,6 +18,17 @@ input[type=number]::-webkit-outer-spin-button {
100% { background: yellow; } 100% { background: yellow; }
} }
/* Announcements banner */
#banner {
background-color: #d86b01;
width: 100%;
text-align: center;
padding: 10px;
color: white;
font-size: larger;
}
/* /*
* Top row * Top row
*/ */
@ -147,9 +158,10 @@ input[type=number]::-webkit-outer-spin-button {
height:50px; height:50px;
padding:0 15px; padding:0 15px;
background:#c8102e; background:rgba(200,16,46,1);
color:#fff; color:#fff;
font-family:"Roboto Slab";
font-weight:bold; font-weight:bold;
font-size:18px; font-size:18px;
} }
@ -230,24 +242,17 @@ input[type=number]::-webkit-outer-spin-button {
height:40px; height:40px;
} }
#special_operations button { #special_operations .btn {
height:100%; height:40px;
width:25%;
float:left; font-size:15px;
background:#c8102e;
color:#FFF;
font-size:18px;
font-weight:bold; font-weight:bold;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
} }
#special_operations button:focus, #special_operations button:hover {
outline:none;
background:#000;
color:#fff;
}
/* Article autocomplete */ /* Article autocomplete */
@ -260,15 +265,14 @@ input[type=number]::-webkit-outer-spin-button {
height:100%; height:100%;
float:left; float:left;
border:0; border:0;
border-right:1px solid #c8102e;
border-bottom:1px solid #c8102e;
border-radius:0; border-radius:0;
border-bottom: 1px solid rgba(200,16,46,0.9);
font-size:16px; font-size:16px;
font-weight:bold; font-weight:bold;
} }
#article_selection input+input #article_selection input+span { #article_selection input:first-child {
border-right:0; border-right: 1px dashed rgba(200,16,46,0.9);
} }
#article_autocomplete { #article_autocomplete {
@ -337,14 +341,14 @@ input[type=number]::-webkit-outer-spin-button {
width:14%; width:14%;
} }
#articles_data div.category { #articles_data div.category {
height:35px; height:35px;
line-height:35px; line-height:35px;
background-color:#c8102e; background-color:#c8102e;
font-family:"Roboto Slab";
font-size:16px; font-size:16px;
color:#FFF;
font-weight:bold; font-weight:bold;
color:#FFF;
} }
#articles_data div.category>span:first-child { #articles_data div.category>span:first-child {
@ -445,3 +449,7 @@ input[type=number]::-webkit-outer-spin-button {
.kpsul_middle_right_col { .kpsul_middle_right_col {
overflow:auto; overflow:auto;
} }
.kpsul_middle_right_col #history .day {
top: 0;
}

View file

@ -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; }
}

View file

@ -5,7 +5,7 @@
.jconfirm .jconfirm-box { .jconfirm .jconfirm-box {
padding:0; padding:0;
border-radius:0 !important; border-radius:0 !important;
font-family:"Roboto Mono"; font-family:Roboto;
} }
.jconfirm .jconfirm-box div.title-c .title { .jconfirm .jconfirm-box div.title-c .title {
@ -28,7 +28,6 @@
.jconfirm .jconfirm-box .content { .jconfirm .jconfirm-box .content {
border-bottom:1px solid #ddd; border-bottom:1px solid #ddd;
padding:5px 10px;
} }
.jconfirm .jconfirm-box input { .jconfirm .jconfirm-box input {
@ -37,6 +36,7 @@
border:0; border:0;
font-family:"Roboto Mono";
font-size:40px; font-size:40px;
text-align:center; text-align:center;
@ -49,6 +49,7 @@
} }
.jconfirm .jconfirm-box .buttons button { .jconfirm .jconfirm-box .buttons button {
min-width:40px;
height:100%; height:100%;
margin:0; margin:0;
margin:0 !important; margin:0 !important;
@ -89,24 +90,3 @@
padding-right: 50px; padding-right: 50px;
padding-left: 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;
}

View file

@ -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;
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -135,16 +135,6 @@ class Config {
$(document).ready(function() { $(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') { if (typeof Cookies !== 'undefined') {
// Retrieving csrf token // Retrieving csrf token
csrftoken = Cookies.get('csrftoken'); csrftoken = Cookies.get('csrftoken');
@ -194,19 +184,24 @@ $(document).on('keydown', function(e) {
class KfetWebsocket { class KfetWebsocket {
static get defaults() { static get defaults() {
return {"relative_url": "", "default_msg": {}, "handlers": []}; return {
relative_url: '',
default_msg: {},
handlers: [],
base_path: '/ws/k-fet/'
};
} }
constructor(data) { constructor(data) {
$.extend(this, this.constructor.defaults, data); $.extend(this, this.constructor.defaults, data);
if (window.location.pathname.startsWith('/gestion/'))
this.base_path = '/gestion' + this.base_path;
} }
get url() { get url() {
var websocket_protocol = window.location.protocol == 'https:' ? 'wss' : 'ws'; var protocol = window.location.protocol == 'https:' ? 'wss' : 'ws';
var location_host = window.location.host; var host = window.location.host;
var location_url = window.location.pathname.startsWith('/gestion/') ? location_host + '/gestion' : location_host; return protocol + "://" + host + this.base_path + this.relative_url;
return websocket_protocol+"://" + location_url + this.relative_url ;
} }
add_handler(handler) { add_handler(handler) {
@ -230,7 +225,7 @@ class KfetWebsocket {
} }
var OperationWebSocket = new KfetWebsocket({ var OperationWebSocket = new KfetWebsocket({
'relative_url': '/ws/k-fet/k-psul/', 'relative_url': 'k-psul/',
'default_msg': {'opegroups':[],'opes':[],'checkouts':[],'articles':[]}, 'default_msg': {'opegroups':[],'opes':[],'checkouts':[],'articles':[]},
}); });
@ -425,3 +420,12 @@ String.prototype.pluralize = function(count, irreg_plural) {
return irreg_plural ? irreg_plural : this+'s' ; return irreg_plural ? irreg_plural : this+'s' ;
return this ; return this ;
} }
/**
* Setup jquery-confirm
*/
jconfirm.defaults = {
confirmButton: '<span class="glyphicon glyphicon-ok"></span>',
cancelButton: '<span class="glyphicon glyphicon-remove"></span>'
};

View file

@ -7,7 +7,7 @@
var self = this; var self = this;
var element = $(target); var element = $(target);
var content = $("<div>"); var content = $("<div class='full'>");
var buttons; var buttons;
function dictToArray (dict, start) { function dictToArray (dict, start) {
@ -67,7 +67,6 @@
{ {
label: chart.label, label: chart.label,
borderColor: chart.color, borderColor: chart.color,
backgroundColor: chart.color,
fill: is_time_chart, fill: is_time_chart,
lineTension: 0, lineTension: 0,
data: chart_data, data: chart_data,
@ -154,9 +153,8 @@
// initialize the interface // initialize the interface
function initialize (data) { function initialize (data) {
// creates the bar with the buttons // creates the bar with the buttons
buttons = $("<div>", buttons = $("<ul>",
{class: "btn-group btn-group-justified", {class: "nav stat-nav",
role: "group",
"aria-label": "select-period"}); "aria-label": "select-period"});
var to_click; var to_click;
@ -164,11 +162,9 @@
for (var i = 0; i < context.length; i++) { for (var i = 0; i < context.length; i++) {
// creates the button // creates the button
var btn_wrapper = $("<div>", var btn_wrapper = $("<li>", {role:"presentation"});
{class: "btn-group", var btn = $("<a>",
role:"group"}); {class: "btn btn-nav",
var btn = $("<button>",
{class: "btn btn-primary",
type: "button"}) type: "button"})
.text(context[i].label) .text(context[i].label)
.prop("stats_target_url", context[i].url) .prop("stats_target_url", context[i].url)

View file

@ -86,7 +86,10 @@ class Scale(object):
def get_labels(self, label_fmt=None): def get_labels(self, label_fmt=None):
if label_fmt is None: if label_fmt is None:
label_fmt = self.label_fmt label_fmt = self.label_fmt
return [begin.strftime(label_fmt) for begin, end in self] return [
begin.strftime(label_fmt.format(i=i, rev_i=len(self)-i))
for i, (begin, end) in enumerate(self)
]
def chunkify_qs(self, qs, field=None): def chunkify_qs(self, qs, field=None):
if field is None: if field is None:

View file

@ -1,68 +1,71 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_2.html" %}
{% block title %}Liste des comptes{% endblock %} {% block title %}Comptes{% endblock %}
{% block content-header-title %}Comptes{% endblock %} {% block header-title %}Comptes{% endblock %}
{% block content %} {% block fixed %}
<div class="row"> <aside>
<div class="col-sm-4 col-md-3 col-content-left"> <div class="heading">
<div class="content-left"> {% with n_accounts=accounts|length|add:-1 %}
<div class="content-left-top"> {{ n_accounts }}
<div class="line line-big">{{ accounts|length|add:-1 }}</div> <span class="sub">compte{{ n_accounts|pluralize }}</span>
<div class="line line-bigsub">compte{{ accounts|length|add:-1|pluralize }}</div> {% endwith %}
</div>
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.create' %}">Créer un compte</a>
{% if perms.kfet.manage_perms %}
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.group' %}">Permissions</a>
{% endif %}
{% if perms.kfet.view_negs %}
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.negative' %}">Négatifs</a>
{% endif %}
</div>
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
<div class="content-right-block">
<h2>Liste des comptes</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td></td>
<td>Trigramme</td>
<td>Nom</td>
<td>Balance</td>
<td>COF</td>
<td>Dpt</td>
<td>Promo</td>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td class="text-center">
<a href="{% url 'kfet.account.read' account.trigramme %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</td>
<td>{{ account.trigramme }}</td>
<td>{{ account.name }}</td>
<td class="text-right">{{ account.balance }}€</td>
<td>{{ account.is_cof }}</td>
<td>{{ account.departement }}</td>
<td>{{ account.promo|default_if_none:'' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
</aside>
<div class="buttons">
<div class="solo full">
<a class="btn btn-primary" href="{% url 'kfet.account.create' %}">
<span class="glyphicon glyphicon-plus"></span>
<span>Créer un compte</span>
</a>
</div>
{% if perms.kfet.manage_perms %}
<a class="btn btn-primary" href="{% url 'kfet.account.group' %}">Permissions</a>
{% endif %}
{% if perms.kfet.view_negs %}
<a class="btn btn-primary" href="{% url 'kfet.account.negative' %}">Négatifs</a>
{% endif %}
</div> </div>
{% endblock %} {% endblock %}
{% block main %}
<section>
<div class="table-responsive">
<table class="table table-hover table-condensed">
<thead>
<tr>
<td class="text-center">Tri.</td>
<td>Nom</td>
<td class="text-right">Balance</td>
<td class="text-center">COF</td>
<td>Dpt</td>
<td class="text-center">Promo</td>
</tr>
</thead>
<tbody>
{% for account in accounts %}
<tr>
<td class="text-center">
<a href="{% url 'kfet.account.read' account.trigramme %}">
{{ account.trigramme }}
</a>
</td>
<td>{{ account.name }}</td>
<td class="text-right">{{ account.balance }}€</td>
<td class="text-center">{{ account.is_cof|yesno }}</td>
<td>{{ account.departement }}</td>
<td class="text-center">{{ account.promo|default_if_none:'' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</section>
{% endblock %}

View file

@ -1,45 +1,36 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_form.html" %}
{% load staticfiles %} {% load staticfiles %}
{% block title %}Nouveau compte{% endblock %} {% block title %}Nouveau compte{% endblock %}
{% block header-title %}Création d'un compte{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
{% endblock %} {% endblock %}
{% block content-header-title %}Création d'un compte{% endblock %} {% block main %}
{% block content %} <form action="" method="post" class="account_create">
{% csrf_token %}
{% include 'kfet/base_messages.html' %} <div>
{{ trigramme_form.trigramme.errors }}
<div class="row form-only"> {{ trigramme_form.trigramme }}
<div class="col-sm-12 col-md-8 col-md-offset-2">
<div class="content-form">
<form action="{% url "kfet.account.create" %}" method="post" class="account_create">
{% csrf_token %}
<div>
{{ trigramme_form.trigramme.errors }}
{{ trigramme_form.trigramme }}
</div>
<div id="trigramme_valid"></div>
<p class="help-block">Les mots contenant des caractères non alphanumériques seront ignorés</p>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
</div>
</div> </div>
</div> <p class="help-block">Les mots contenant des caractères non alphanumériques seront ignorés</p>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
// Affichage des résultats d'autocomplétion // Affichage des résultats d'autocomplétion
@ -68,7 +59,6 @@
// et de ladisponibilité du trigramme choisi // et de ladisponibilité du trigramme choisi
$('#id_trigramme').on('input', function() { $('#id_trigramme').on('input', function() {
var trigramme = $('#id_trigramme').val().toUpperCase(); var trigramme = $('#id_trigramme').val().toUpperCase();
var container = '#trigramme_valid';
var pattern = /^[^a-z]{3}$/; var pattern = /^[^a-z]{3}$/;
if (!(trigramme.match(pattern))) { if (!(trigramme.match(pattern))) {

View file

@ -2,44 +2,37 @@
{% load staticfiles %} {% load staticfiles %}
{% block title %}Nouveau compte{% endblock %} {% block title %}Nouveau compte{% endblock %}
{% block header-title %}Création d'un compte{% endblock %}
{% block extra_head %} {% block extra_head %}
<script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script> <script src="{% static "autocomplete_light/autocomplete.js" %}" type="text/javascript"></script>
{% endblock %} {% endblock %}
{% block content-header-title %}Création d'un compte{% endblock %} {% block main-class %}content-form{% endblock %}
{% block content %} {% block main %}
{% include 'kfet/base_messages.html' %} <form action="" method="post" class="account_create">
{% csrf_token %}
<div class="row form-only"> <div>
<div class="col-sm-12 col-md-8 col-md-offset-2"> {{ trigramme_form.trigramme.errors }}
<div class="content-form"> {{ trigramme_form.trigramme }}
<form action="{% url "kfet.account.create_special" %}" method="post" class="account_create"> {{ balance_form }}
{% csrf_token %}
<div>
{{ trigramme_form.trigramme.errors }}
{{ trigramme_form.trigramme }}
{{ balance_form }}
</div>
<div id="trigramme_valid"></div>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
</div>
</div> </div>
</div> <div id="trigramme_valid"></div>
<input type="text" name="q" id="search_autocomplete" spellcheck="false" placeholder="Chercher un utilisateur par nom, prénom ou identifiant clipper" class="form-control">
<div style="position:relative;">
<div id="search_results"></div>
</div>
<div class="form-horizontal">
<div id="form-placeholder">
{% include 'kfet/account_create_form.html' %}
</div>
{% if not perms.kfet.add_account %}
{% include 'kfet/form_authentication_snippet.html' %}
{% endif %}
</div>
</form>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
// Affichage des résultats d'autocomplétion // Affichage des résultats d'autocomplétion

View file

@ -1,54 +1,61 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Groupes de comptes{% endblock %} {% block title %}Groupes de comptes{% endblock %}
{% block content-header-title %}Groupes de comptes{% endblock %} {% block header-title %}Groupes de comptes{% endblock %}
{% block content %} {% block fixed %}
<div class="row"> <div class="buttons">
<div class="col-sm-4 col-md-3 col-content-left"> <a class="btn btn-primary" href="{% url 'kfet.account.group.create' %}">
<div class="content-left"> <span class="glyphicon glyphicon-plus"></span><span>Créer un groupe</span>
<div class="content-left-top text-center"> </a>
<div class="line"></div>
</div>
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.account.group.create' %}">Créer un groupe</a>
</div>
</div>
</div>
<div class="col-sm-8 col-md-9 col-content-right">
{% include 'kfet/base_messages.html' %}
<div class="content-right">
{% for group in groups %}
<div class="content-right-block">
<div class="buttons-title">
<a class="btn btn-primary" href="{% url 'kfet.account.group.update' group.pk %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</div>
<h2>{{ group.name }}</h2>
<div class="row">
<div class="col-sm-6">
<h3>Permissions</h3>
<ul>
{% for perm in group.permissions.all %}
<li>{{ perm.name }}</li>
{% endfor %}
</ul>
</div>
<div class="col-sm-6">
<h3>Comptes</h3>
<ul>
{% for user in group.user_set.all %}
<li>{{ user.profile.account_kfet }}</li>
{% endfor %}
</ul>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div> </div>
{% endblock %} {% endblock %}
{% block main %}
{% for group in groups %}
<section>
<div class="heading">
{{ group.name }}
<div class="buttons">
<a class="btn btn-default" href="{% url 'kfet.account.group.update' group.pk %}">
<span class="glyphicon glyphicon-cog"></span><span class="hidden-xs">Éditer</span>
</a>
</div>
</div>
<div>
<h3>Comptes</h3>
<div class="sub-block column-sm-2 column-md-3">
<ul>
{% for user in group.user_set.all %}
<li>
<a href="{% url "kfet.account.update" user.profile.account_kfet.trigramme %}">
{{ user.profile.account_kfet }}
</a>
</li>
{% endfor %}
</ul>
</div>
<h3>Permissions</h3>
<div class="column-sm-2 column-lg-3">
{% regroup group.permissions.all by content_type as grouped_perms %}
<ul class="list-unstyled">
{% for perms_group in grouped_perms %}
<li class="unbreakable">
<b>{{ perms_group.grouper|title }}</b>
<ul>
{% for perm in perms_group.list %}
<li>{{ perm.name }}</li>
{% endfor %}
</ul>
</li>
{% endfor %}
</ul>
</div>
</div>
</section>
{% endfor %}
{% endblock %}

View file

@ -1,37 +1,48 @@
{% extends 'kfet/base.html' %} {% extends 'kfet/base_form.html' %}
{% load staticfiles %} {% load staticfiles %}
{% load widget_tweaks %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" text="text/css" href="{% static 'kfet/css/multiple-select.css' %}"> <link rel="stylesheet" text="text/css" href="{% static 'kfet/css/multiple-select.css' %}">
<script src="{% static 'kfet/js/multiple-select.js' %}"></script> <script src="{% static 'kfet/js/multiple-select.js' %}"></script>
{% endblock %} {% endblock %}
{% block content %} {% block title %}Permissions - Édition{% endblock %}
{% block header-title %}Modification des permissions{% endblock %}
<form action="" method="post"> {% block main %}
<form action="" method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<div> <div class="form-group">
{{ form.name.errors }} <label for="{{ form.name.id_for_label }}" class="col-sm-2 control-label">{{ form.name.label }}</label>
{{ form.name.label_tag }} <div class="col-sm-10">
<div class="input-group"> <div class="input-group">
<span class="input-group-addon">K-Fêt</span> <span class="input-group-addon">K-Fêt</span>
{{ form.name }} {{ form.name|add_class:"form-control" }}
</div>
{% if form.name.errors %}
<span class="help-block">{{ form.name.errors }}</span>
{% endif %}
{% if form.name.help_text %}
<span class="help-block">{{ form.name.help_text }}</span>
{% endif %}
</div> </div>
</div> </div>
<div> {% include "kfet/form_field_snippet.html" with field=form.permissions %}
{{ form.permissions.errors }} {% if not perms.kfet.manage_perms %}
{{ form.permissions.label_tag }} {% include "kfet/form_authentication_snippet.html" %}
{{ form.permissions }} {% endif %}
</div> {% include "kfet/form_submit_snippet.html" with value="Enregistrer" %}
<input type="submit" value="Enregistrer">
</form> </form>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {
$("select").multipleSelect({ let $name_input = $("#id_name");
width: 500, let raw_name = $name_input.val();
filter: true, let prefix = "K-Fêt ";
}); if (raw_name.startsWith(prefix))
$name_input.val(raw_name.substring(prefix.length));
}); });
</script> </script>

View file

@ -1,80 +1,76 @@
{% extends 'kfet/base.html' %} {% extends "kfet/base_col_2.html" %}
{% block title %}Comptes en négatifs{% endblock %} {% block title %}Comptes - Négatifs{% endblock %}
{% block content-header-title %}Comptes - Négatifs{% endblock %} {% block header-title %}Comptes en négatif{% endblock %}
{% block content %} {% block fixed %}
<div class="row"> <aside>
<div class="col-sm-4 col-md-3 col-content-left"> <div class="heading">
<div class="content-left"> {{ negatives|length }}
<div class="content-left-top"> <span class="sub">compte{{ negatives|length|pluralize }} en négatif</span>
<div class="line line-big">{{ negatives|length }}</div>
<div class="line line-bigsub">compte{{ negatives|length|pluralize }} en négatif</div>
<div class="block">
<div class="line"><b>Total:</b> {{ negatives_sum|floatformat:2 }}€</div>
</div>
<div class="block">
<div class="line"><b>Découvert autorisé par défaut</b></div>
<div class="line">Montant: {{ kfet_config.overdraft_amount }}€</div>
<div class="line">Pendant: {{ kfet_config.overdraft_duration }}</div>
</div>
</div>
{% if perms.kfet.change_settings %}
<div class="buttons">
<a class="btn btn-primary btn-lg" href="{% url 'kfet.settings' %}">Modifier les valeurs par défaut</a>
</div>
{% endif %}
</div>
</div> </div>
<div class="col-sm-8 col-md-9 col-content-right"> <div class="text">
{% include 'kfet/base_messages.html' %} <b>Total:</b> {{ negatives_sum|floatformat:2 }}€
<div class="content-right">
<div class="content-right-block">
<h2>Liste des comptes en négatifs</h2>
<div class="table-responsive">
<table class="table table-condensed">
<thead>
<tr>
<td></td>
<td>Tri</td>
<td>Nom</td>
<td>Balance</td>
<td>Réelle</td>
<td>Début</td>
<td>Découvert autorisé</td>
<td>Jusqu'au</td>
<td>Balance offset</td>
</tr>
</thead>
<tbody>
{% for neg in negatives %}
<tr>
<td class="text-center">
<a href="{% url 'kfet.account.update' neg.account.trigramme %}">
<span class="glyphicon glyphicon-cog"></span>
</a>
</td>
<td>{{ neg.account.trigramme }}</td>
<td>{{ neg.account.name }}</td>
<td class="text-right">{{ neg.account.balance|floatformat:2 }}€</td>
<td class="text-right">
{% if neg.balance_offset %}
{{ neg.account.real_balance|floatformat:2 }}€
{% endif %}
</td>
<td>{{ neg.start|date:'d/m/Y H:i:s'}}</td>
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td>
<td>{{ neg.authz_overdrafy_until|default_if_none:'' }}</td>
<td>{{ neg.balance_offset|default_if_none:'' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div> </div>
<div class="text">
<b>Plafond par défaut</b>
<ul class="list-unstyled">
<li>Montant: {{ kfet_config.overdraft_amount }}€</li>
<li>Pendant: {{ kfet_config.overdraft_duration }}</li>
</ul>
</div>
</aside>
{% if perms.kfet.change_settings %}
<div class="buttons">
<div class="full">
<button type="button" class="btn btn-primary" href="{% url 'kfet.settings' %}">Modifier les valeurs par défaut</a>
</div>
</div>
{% endif %}
{% endblock %}
{% block main %}
<div class="table-responsive">
<table class="table table-hover table-condensed">
<thead>
<tr>
<td class="text-center">Tri.</td>
<td>Nom</td>
<td class="text-right">Balance</td>
<td class="text-right">Réelle</td>
<td>Début</td>
<td>Découvert autorisé</td>
<td>Jusqu'au</td>
<td>Balance offset</td>
</tr>
</thead>
<tbody>
{% for neg in negatives %}
<tr>
<td class="text-center">
<a href="{% url 'kfet.account.update' neg.account.trigramme %}">
{{ neg.account.trigramme }}
</a>
</td>
<td>{{ neg.account.name }}</td>
<td class="text-right">{{ neg.account.balance|floatformat:2 }}€</td>
<td class="text-right">
{% if neg.balance_offset %}
{{ neg.account.real_balance|floatformat:2 }}€
{% endif %}
</td>
<td>{{ neg.start|date:'d/m/Y H:i:s'}}</td>
<td>{{ neg.authz_overdraft_amount|default_if_none:'' }}</td>
<td>{{ neg.authz_overdrafy_until|default_if_none:'' }}</td>
<td>{{ neg.balance_offset|default_if_none:'' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> </div>
{% endblock %} {% endblock %}

View file

@ -1,18 +1,9 @@
{% extends "kfet/base.html" %} {% extends "kfet/base_col_2.html" %}
{% load staticfiles %} {% load staticfiles %}
{% load kfet_tags %} {% load kfet_tags %}
{% load l10n %} {% load l10n %}
{% block extra_head %} {% block extra_head %}
<link rel="stylesheet" type="text/css" href="{% static 'kfet/css/jquery-ui.min.css' %}">
<script type="text/javascript" src="{% static 'kfet/js/js.cookie.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/reconnecting-websocket.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/jquery-confirm.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-fr.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/moment-timezone-with-data-2010-2020.js' %}"></script>
<script type="text/javascript" src="{% url 'js_reverse' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/kfet.api.js' %}"></script>
<script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script> <script type="text/javascript" src="{% static 'kfet/js/history.js' %}"></script>
{% if account.user == request.user %} {% if account.user == request.user %}
@ -29,20 +20,12 @@ $(document).ready(function() {
"{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}", "{% url 'kfet.account.stat.balance.list' trigramme=account.trigramme %}",
$("#stat_balance") $("#stat_balance")
); );
}); });
</script> </script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block title %} {% block title %}
{% if account.user == request.user %}
Mon compte
{% else %}
Informations du compte {{ account.trigramme }}
{% endif %}
{% endblock %}
{% block content-header-title %}
{% if account.user == request.user %} {% if account.user == request.user %}
Mon compte Mon compte
{% else %} {% else %}
@ -50,53 +33,56 @@ $(document).ready(function() {
{% endif %} {% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block header-title %}
{% if account.user == request.user %}
Mon compte
{% else %}
Informations du compte {{ account.trigramme }}
{% endif %}
{% endblock %}
<div class="row"> {% block footer %}
<div class="col-sm-4 col-md-3 col-content-left"> {% include "kfet/base_footer.html" %}
<div class="content-left"> {% endblock %}
{% include 'kfet/left_account.html' %}
</div> {% block fixed %}
</div> {% include "kfet/left_account.html" %}
<div class="col-sm-8 col-md-9 col-content-right"> {% endblock %}
{% include "kfet/base_messages.html" %}
<div class="content-right"> {% block main %}
<div class="content-right-block">
<div class="col-sm-12 nopadding"> <div class="tab-content">
{% if account.user == request.user %}
<div class='tab-content'> {% if account.user == request.user %}
<div class="tab-pane fade in active" id="tab_stats"> <div id="tab_stats" class="tab-pane fade in active">
<h2>Statistiques</h2> <section>
<div class="panel-md-margin"> <div>
<h3>Ma balance</h3> <h3>Ma balance</h3>
<div id="stat_balance"></div> <div id="stat_balance"></div>
<h3>Ma consommation</h3> <h3>Ma consommation</h3>
<div id="stat_last"></div> <div id="stat_last"></div>
</div> </div>
</div> </section>
<div class="tab-pane fade" id="tab_history"> </div><!-- stats tab -->
{% endif %} {% endif %}
{% if addcosts %}
<h2>Gagné des majorations</h2> <div id="tab_history" class="tab-pane fade {% if account.user != request.user %}in active{% endif %}">
<div> <section>
<ul> {% if addcosts %}
{% for addcost in addcosts %} <h2>Gagné des majorations</h2>
<li>{{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€</li> <div>
{% endfor %} <ul>
</ul> {% for addcost in addcosts %}
</div> <li>{{ addcost.date|date:'l j F' }}: +{{ addcost.sum_addcosts }}€</li>
{% endif %} {% endfor %}
<h2>Historique</h2> </ul>
<div id="history"></div> </div>
{% if account.user == request.user %} {% endif %}
</div> <div id="history" class="full"></div>
</div><!-- tab-content --> </section>
{% endif %} </div><!-- history tab -->
</div><!-- col-sm-12 -->
</div><!-- content-right-block --> </div><!-- tab-content -->
</div><!-- content-right-->
</div>
</div>
<script type="text/javascript"> <script type="text/javascript">
$(document).ready(function() { $(document).ready(function() {

Some files were not shown because too many files have changed in this diff Show more