Merge branch 'master' into aureplop/kpsul_js_refactor
This commit is contained in:
commit
b62f0293dd
163 changed files with 7015 additions and 3038 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -9,3 +9,8 @@ venv/
|
||||||
/src
|
/src
|
||||||
media/
|
media/
|
||||||
*.log
|
*.log
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
# PyCharm
|
||||||
|
.idea
|
||||||
|
.cache
|
||||||
|
|
|
@ -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
118
README.md
|
@ -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.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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 }} ?</h3>
|
||||||
{{ show.title }} ?</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 %}
|
||||||
|
|
|
@ -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>
|
28
bda/tests.py
28
bda/tests.py
|
@ -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"),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
35
bda/views.py
35
bda/views.py
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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'),
|
||||||
|
]
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
36
cof/settings/local.py
Normal 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
|
|
@ -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"
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
16
cof/urls.py
16
cof/urls.py
|
@ -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)),
|
||||||
|
]
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'gestioncof.apps.GestioncofConfig'
|
|
@ -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
15
gestioncof/apps.py
Normal 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)
|
|
@ -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
|
||||||
|
)
|
||||||
|
|
19
gestioncof/migrations/0011_longer_clippers.py
Normal file
19
gestioncof/migrations/0011_longer_clippers.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
18
gestioncof/migrations/0011_remove_cofprofile_num.py
Normal file
18
gestioncof/migrations/0011_remove_cofprofile_num.py
Normal 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',
|
||||||
|
),
|
||||||
|
]
|
15
gestioncof/migrations/0012_merge.py
Normal file
15
gestioncof/migrations/0012_merge.py
Normal 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 = [
|
||||||
|
]
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 qu’avec une casse variable. On normalise pour
|
# et à la fin, ainsi qu’avec 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
23
gestioncof/signals.py
Normal 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)
|
|
@ -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 --------------------------------- */
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
23
gestioncof/templates/gestioncof/banner_update.html
Normal file
23
gestioncof/templates/gestioncof/banner_update.html
Normal 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 %}
|
|
@ -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">
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -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
1
kfet/cms/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
default_app_config = 'kfet.cms.apps.KFetCMSAppConfig'
|
10
kfet/cms/apps.py
Normal file
10
kfet/cms/apps.py
Normal 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
|
20
kfet/cms/context_processors.py
Normal file
20
kfet/cms/context_processors.py
Normal 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,
|
||||||
|
}
|
1456
kfet/cms/fixtures/kfet_wagtail_17_05.json
Normal file
1456
kfet/cms/fixtures/kfet_wagtail_17_05.json
Normal file
File diff suppressed because one or more lines are too long
12
kfet/cms/hooks.py
Normal file
12
kfet/cms/hooks.py
Normal 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'),
|
||||||
|
)
|
35
kfet/cms/management/commands/kfet_loadwagtail.py
Normal file
35
kfet/cms/management/commands/kfet_loadwagtail.py
Normal 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'])
|
49
kfet/cms/migrations/0001_initial.py
Normal file
49
kfet/cms/migrations/0001_initial.py
Normal 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',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
19
kfet/cms/migrations/0002_alter_kfetpage_colcount.py
Normal file
19
kfet/cms/migrations/0002_alter_kfetpage_colcount.py
Normal 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."),
|
||||||
|
),
|
||||||
|
]
|
0
kfet/cms/migrations/__init__.py
Normal file
0
kfet/cms/migrations/__init__.py
Normal file
174
kfet/cms/models.py
Normal file
174
kfet/cms/models.py
Normal 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
|
93
kfet/cms/static/kfetcms/css/base.css
Normal file
93
kfet/cms/static/kfetcms/css/base.css
Normal 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;
|
||||||
|
}
|
18
kfet/cms/static/kfetcms/css/editor.css
Normal file
18
kfet/cms/static/kfetcms/css/editor.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
3
kfet/cms/static/kfetcms/css/index.css
Normal file
3
kfet/cms/static/kfetcms/css/index.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
@import url("base.css");
|
||||||
|
@import url("menu.css");
|
||||||
|
@import url("team.css");
|
58
kfet/cms/static/kfetcms/css/menu.css
Normal file
58
kfet/cms/static/kfetcms/css/menu.css
Normal 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;
|
||||||
|
}
|
47
kfet/cms/static/kfetcms/css/team.css
Normal file
47
kfet/cms/static/kfetcms/css/team.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
41
kfet/cms/templates/kfetcms/base.html
Normal file
41
kfet/cms/templates/kfetcms/base.html
Normal 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 %}
|
11
kfet/cms/templates/kfetcms/block_menu.html
Normal file
11
kfet/cms/templates/kfetcms/block_menu.html
Normal 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 %}
|
12
kfet/cms/templates/kfetcms/block_menu_category.html
Normal file
12
kfet/cms/templates/kfetcms/block_menu_category.html
Normal 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>
|
66
kfet/cms/templates/kfetcms/block_teamgroup.html
Normal file
66
kfet/cms/templates/kfetcms/block_teamgroup.html
Normal 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>
|
|
@ -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):
|
||||||
|
|
|
@ -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']
|
||||||
|
|
|
@ -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')
|
||||||
|
|
19
kfet/migrations/0054_update_promos.py
Normal file
19
kfet/migrations/0054_update_promos.py
Normal 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),
|
||||||
|
),
|
||||||
|
]
|
81
kfet/migrations/0055_move_permissions.py
Normal file
81
kfet/migrations/0055_move_permissions.py
Normal 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),
|
||||||
|
]
|
18
kfet/migrations/0056_change_account_meta.py
Normal file
18
kfet/migrations/0056_change_account_meta.py
Normal 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'))},
|
||||||
|
),
|
||||||
|
]
|
15
kfet/migrations/0057_merge.py
Normal file
15
kfet/migrations/0057_merge.py
Normal 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 = [
|
||||||
|
]
|
|
@ -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
1
kfet/open/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from .open import OpenKfet, kfet_open # noqa
|
25
kfet/open/consumers.py
Normal file
25
kfet/open/consumers.py
Normal 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
109
kfet/open/open.py
Normal 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
8
kfet/open/routing.py
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
from channels.routing import route_class
|
||||||
|
|
||||||
|
from . import consumers
|
||||||
|
|
||||||
|
|
||||||
|
routing = [
|
||||||
|
route_class(consumers.OpenKfetConsumer)
|
||||||
|
]
|
69
kfet/open/static/kfetopen/kfet-open.css
Normal file
69
kfet/open/static/kfetopen/kfet-open.css
Normal 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;
|
||||||
|
}
|
113
kfet/open/static/kfetopen/kfet-open.js
Normal file
113
kfet/open/static/kfetopen/kfet-open.js
Normal 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);
|
||||||
|
}
|
||||||
|
};
|
13
kfet/open/templates/kfetopen/init.html
Normal file
13
kfet/open/templates/kfetopen/init.html
Normal 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
322
kfet/open/tests.py
Normal 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
11
kfet/open/urls.py
Normal 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
32
kfet/open/views.py
Normal 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()
|
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
|
|
@ -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'))
|
||||||
|
))
|
||||||
|
|
88
kfet/static/kfet/css/base/buttons.css
Normal file
88
kfet/static/kfet/css/base/buttons.css
Normal 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;
|
||||||
|
}
|
151
kfet/static/kfet/css/base/fixed.css
Normal file
151
kfet/static/kfet/css/base/fixed.css
Normal 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%;
|
||||||
|
}
|
||||||
|
}
|
18
kfet/static/kfet/css/base/footer.css
Normal file
18
kfet/static/kfet/css/base/footer.css
Normal 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;
|
||||||
|
}
|
138
kfet/static/kfet/css/base/main.css
Normal file
138
kfet/static/kfet/css/base/main.css
Normal 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;
|
||||||
|
}
|
36
kfet/static/kfet/css/base/messages.css
Normal file
36
kfet/static/kfet/css/base/messages.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
118
kfet/static/kfet/css/base/misc.css
Normal file
118
kfet/static/kfet/css/base/misc.css
Normal 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); }
|
||||||
|
}
|
151
kfet/static/kfet/css/base/nav.css
Normal file
151
kfet/static/kfet/css/base/nav.css
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
43
kfet/static/kfet/css/libs/columns.css
Normal file
43
kfet/static/kfet/css/libs/columns.css
Normal 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; }
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
14
kfet/static/kfet/css/libs/multiple-select-kfet.css
Normal file
14
kfet/static/kfet/css/libs/multiple-select-kfet.css
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
BIN
kfet/static/kfet/img/favicon.png
Normal file
BIN
kfet/static/kfet/img/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
|
@ -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>'
|
||||||
|
};
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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))) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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
Loading…
Reference in a new issue