diff --git a/.credentials/HCAPTCHA_SECRET b/.credentials/HCAPTCHA_SECRET deleted file mode 100644 index 7daa69d3..00000000 --- a/.credentials/HCAPTCHA_SECRET +++ /dev/null @@ -1 +0,0 @@ -0x0000000000000000000000000000000000000000 diff --git a/.credentials/HCAPTCHA_SITEKEY b/.credentials/HCAPTCHA_SITEKEY deleted file mode 100644 index f5093057..00000000 --- a/.credentials/HCAPTCHA_SITEKEY +++ /dev/null @@ -1 +0,0 @@ -10000000-ffff-ffff-ffff-000000000001 diff --git a/.credentials/KFETOPEN_TOKEN b/.credentials/KFETOPEN_TOKEN deleted file mode 100644 index 4cbb2bf5..00000000 --- a/.credentials/KFETOPEN_TOKEN +++ /dev/null @@ -1 +0,0 @@ -k-feste_token diff --git a/.credentials/SECRET_KEY b/.credentials/SECRET_KEY deleted file mode 100644 index de873cc2..00000000 --- a/.credentials/SECRET_KEY +++ /dev/null @@ -1 +0,0 @@ -insecure-key diff --git a/.credentials/SYMPA_PASSWORD b/.credentials/SYMPA_PASSWORD deleted file mode 100644 index fbcf12d5..00000000 --- a/.credentials/SYMPA_PASSWORD +++ /dev/null @@ -1 +0,0 @@ -toto diff --git a/.credentials/SYMPA_USERNAME b/.credentials/SYMPA_USERNAME deleted file mode 100644 index d525803f..00000000 --- a/.credentials/SYMPA_USERNAME +++ /dev/null @@ -1 +0,0 @@ -sympa diff --git a/.envrc b/.envrc deleted file mode 100644 index 1d953f4b..00000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use nix diff --git a/.gitignore b/.gitignore index 9122298b..347d4b78 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ cof/settings.py settings.py *~ venv/ -.venv/ .vagrant /src media/ @@ -19,6 +18,4 @@ media/ .cache # VSCode -.vscode/ -.direnv -.static +.vscode/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ce3bd041..a8bece7d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,7 +1,8 @@ -image: "python:3.7" +image: "python:3.5" variables: # GestioCOF settings + DJANGO_SETTINGS_MODULE: "cof.settings.prod" DBHOST: "postgres" REDIS_HOST: "redis" REDIS_PASSWD: "dummy" @@ -17,23 +18,22 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD - # apps to check migrations for - MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared" - .test_template: before_script: - mkdir -p vendor/{pip,apt} - - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev - - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' gestioasso/settings/secret_example.py > gestioasso/settings/secret.py - - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' gestioasso/settings/secret.py + - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client + - sed -E 's/^REDIS_HOST.*/REDIS_HOST = "redis"/' cof/settings/secret_example.py > cof/settings/secret.py + - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py # Remove the old test database if it has not been done yet - psql --username=$POSTGRES_USER --host=$DBHOST -c "DROP DATABASE IF EXISTS test_$POSTGRES_DB" - pip install --upgrade -r requirements-prod.txt coverage tblib - python --version + script: + - coverage run manage.py test --parallel after_script: - coverage report services: - - postgres:11.7 + - postgres:9.6 - redis:latest cache: key: test @@ -43,40 +43,27 @@ variables: # Keep this disabled for now, as it may kill GitLab... # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' -kfettest: - stage: test - extends: .test_template - variables: - DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" - script: - - coverage run manage.py test kfet +test35: + extends: ".test_template" + image: "python:3.5" + stage: test -coftest: - stage: test - extends: .test_template - variables: - DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" - script: - - coverage run manage.py test gestioncof bda petitscours shared --parallel - -bdstest: - stage: test - extends: .test_template - variables: - DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod" - script: - - coverage run manage.py test bds clubs events --parallel +test37: + extends: ".test_template" + image: "python:3.7" + stage: test linters: + image: python:3.6 stage: test before_script: - mkdir -p vendor/pip - pip install --upgrade black isort flake8 script: - black --check . - - isort --check --diff . + - isort --recursive --check-only --diff bda bds clubs cof events gestioncof kfet petitscours provisioning shared utils # Print errors only - - flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared + - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared utils cache: key: linters paths: @@ -85,18 +72,16 @@ linters: # Check whether there are some missing migrations. migration_checks: stage: test - variables: - DJANGO_SETTINGS_MODULE: "gestioasso.settings.local" before_script: - mkdir -p vendor/{pip,apt} - - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev - - cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py - - pip install --upgrade -r requirements-devel.txt + - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client + - cp cof/settings/secret_example.py cof/settings/secret.py + - pip install --upgrade -r requirements-prod.txt - python --version - script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS + script: python manage.py makemigrations --dry-run --check services: # this should not be necessary… - - postgres:11.7 + - postgres:9.6 cache: key: migration_checks paths: diff --git a/.pre-commit.sh b/.pre-commit.sh deleted file mode 100755 index abf1fe7d..00000000 --- a/.pre-commit.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env bash -# pre-commit hook for gestioCOF project. -# -# Run formatters first, then checkers. -# Formatters which changed a file must set the flag 'formatter_updated'. - -exit_code=0 -formatter_updated=0 -checker_dirty=0 - -# TODO(AD): We should check only staged changes. -# Working? -> Stash unstaged changes, run it, pop stash -STAGED_PYTHON_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep ".py$") - - -# Formatter: black - -printf "> black ... " - -if type black &>/dev/null; then - if [ -z "$STAGED_PYTHON_FILES" ]; then - printf "OK\n" - else - BLACK_OUTPUT="/tmp/gc-black-output.log" - touch $BLACK_OUTPUT - - if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black --check &>$BLACK_OUTPUT; then - echo "$STAGED_PYTHON_FILES" | xargs -d'\n' black &>$BLACK_OUTPUT - tail -1 $BLACK_OUTPUT - formatter_updated=1 - else - printf "OK\n" - fi - fi -else - printf "SKIP: program not found\n" - printf "HINT: Install black with 'pip3 install black' (black requires Python>=3.6)\n" -fi - -# Formatter: isort - -printf "> isort ... " - -if type isort &>/dev/null; then - if [ -z "$STAGED_PYTHON_FILES" ]; then - printf "OK\n" - else - ISORT_OUTPUT="/tmp/gc-isort-output.log" - touch $ISORT_OUTPUT - - if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort --check &>$ISORT_OUTPUT; then - echo "$STAGED_PYTHON_FILES" | xargs -d'\n' isort &>$ISORT_OUTPUT - printf "Reformatted.\n" - formatter_updated=1 - else - printf "OK\n" - fi - fi -else - printf "SKIP: program not found\n" - printf "HINT: Install isort with 'pip install isort'\n" -fi - -# Checker: flake8 - -printf "> flake8 ... " - -if type flake8 &>/dev/null; then - if [ -z "$STAGED_PYTHON_FILES" ]; then - printf "OK\n" - else - FLAKE8_OUTPUT="/tmp/gc-flake8-output.log" - touch $FLAKE8_OUTPUT - - if ! echo "$STAGED_PYTHON_FILES" | xargs -d'\n' flake8 &>$FLAKE8_OUTPUT; then - printf "FAIL\n" - cat $FLAKE8_OUTPUT - checker_dirty=1 - else - printf "OK\n" - fi - fi -else - printf "SKIP: program not found\n" - printf "HINT: Install flake8 with 'pip install flake8'\n" -fi - -# End - -if [ $checker_dirty -ne 0 ] -then - printf ">>> Checker(s) detect(s) issue(s)\n" - printf " You can still commit and push :)\n" - printf " Be warned that our CI may cause you more trouble.\n" -fi - -if [ $formatter_updated -ne 0 ] -then - printf ">>> Working tree updated by formatter(s)\n" - printf " Add changes to staging area and retry.\n" - exit_code=1 -fi - -printf "\n" - -exit $exit_code diff --git a/CHANGELOG.md b/CHANGELOG.md index b68fb40c..269e5194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,234 +5,20 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Le FUTUR ! (pas prêt pour la prod) -### Nouveau module de gestion des événements +- Nouveau module de gestion des événements +- Nouveau module BDS +- Nouveau module clubs -- Désormais complet niveau modèles -- Export des participants implémenté - -#### TODO - -- Vue de création d'événements ergonomique -- Vue d'inscription à un événement **ou** intégration propre dans la vue - "inscription d'un nouveau membre" - -### Nouveau module de gestion des clubs - -Uniquement un modèle simple de clubs avec des respos. Aucune gestion des -adhérents ni des cotisations. - -## TODO Prod - -- Lancer `python manage.py update_translation_fields` après la migration -- Mettre à jour les units systemd `daphne.service` et `worker.service` - -- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés - -## Version ??? - ??/??/???? - -## Version 0.15.1 - 15/06/2023 - -### K-Fêt - -- Rattrape les erreurs d'envoi de mail de négatif -- Utilise l'adresse chefs pour les envois de négatifs - -## Version 0.15 - 22/05/2023 - -### K-Fêt - -- Rajoute un formulaire de contact -- Rajoute un formulaire de demande de soirée -- Désactive les mails d'envoi de négatifs sur les comptes gelés - -## Version 0.14 - 19/05/2023 - -- Répare les dépendances en spécifiant toutes les versions - -### K-Fêt - -- Répare la gestion des changement d'heure via moment.js - -## Version 0.13 - 19/02/2023 - -### K-Fêt - -- Rajoute la valeur des inventaires -- Résout les problèmes de négatif ne disparaissant pas -- Affiche son surnom s'il y en a un -- Bugfixes - -## Version 0.12.1 - 03/10/2022 - -### K-Fêt - -- Fixe un problème de rendu causé par l'agrandissement du menu - -- Mise à jour vers Channels 3.x et Django 3.2 - -## Version 0.12 - 17/06/2022 - -### K-Fêt - -- Ajoute une exception à la limite d'historique pour les comptes `LIQ` et `#13` -- Répare le problème des étiquettes LIQ/Comptes K-Fêt inversées dans les stats des articles K-Fêt - -## Version 0.11 - 26/10/2021 - -### COF - -- Répare un problème de rendu sur le wagtail du COF - -### K-Fêt - -- Ajoute de mails de rappels pour les comptes en négatif -- La recherche de comptes sur K-Psul remarche normalement -- Le pointeur de la souris change de forme quand on survole un item d'autocomplétion -- Modification du gel de compte: - - on ne peut plus geler/dégeler son compte soi-même (il faut la permission "Gérer les permissions K-Fêt") - - on ne peut rien compter sur un compte gelé (aucune override possible), et les K-Fêteux·ses dont le compte est gelé perdent tout accès à K-Psul - - les comptes actuellement gelés (sur l'ancien système) sont dégelés automatiquement -- Modification du fonctionnement des négatifs - - impossible d'avoir des négatifs inférieurs à `kfet_config.overdraft_amount` - - il n'y a plus de limite de temps sur les négatifs - - supression des autorisations de négatif - - il n'est plus possible de réinitialiser la durée d'un négatif en faisant puis en annulant une charge -- La gestion des erreurs passe du client au serveur, ce qui permet d'avoir des messages plus explicites -- La supression d'opérations anciennes est réparée - -## Version 0.10 - 18/04/2021 - -### K-Fêt - -- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à - la fois. -- L'interface indique plus clairement quand on fait une erreur en modifiant un - compte. -- On supprime la fonction "décalage de balance". -- L'accès à l'historique est maintenant limité à 7 jours pour raison de - confidentialité. Les chefs/trez peuvent disposer d'une permission - supplémentaire pour accéder à jusqu'à 30 jours en cas de problème de compta. - L'accès à son historique personnel n'est pas limité. Les durées sont - configurables dans `settings/cof_prod.py`. - -### COF - -- Le Captcha sur la page de demande de petits cours utilise maintenant hCaptcha - au lieu de ReCaptcha, pour mieux respecter la vie privée des utilisateur·ices - -## Version 0.9 - 06/02/2020 - -### COF / BdA - -- Le COF peut remettre à zéro la liste de ses adhérents en août (sans passer par - KDE). -- La page d'accueil affiche la date de fermeture des tirages BdA. -- On peut revendre une place dès qu'on l'a payée, plus besoin de payer toutes - ses places pour pouvoir revendre. -- On s'assure que l'email fourni lors d'une demande de petit cours est valide. - -### BDS - -- Le burô peut maintenant accorder ou révoquer le statut de membre du Burô - en modifiant le profil d'un membre du BDS. -- Le burô peut exporter la liste de ses membres avec email au format CSV depuis - la page d'accueil. - -### K-Fêt - -- On affiche les articles actuellement en vente en premier lors des inventaires - et des commandes. -- On peut supprimer un inventaire. Seuls les articles dont c'est le dernier - inventaire sont affectés. - -## Version 0.8 - 03/12/2020 - -### COF - -- La page "Mes places" dans la section BdA indique quelles places sont sur - listing. -- ergonomie de l'interface admin du BdA : moins d'options inutiles lors de - la sélection de participants. -- les tirages sont maintenant archivables pour éviter d'avoir encore d'autres - options inutiles. -- l'autocomplétion dans l'admin BdA est réparée. -- Les icones de la page de gestion des petits cours sont (à nouveau) réparées. -- On a supprimé la possibilité de modifier les mails automatiques depuis - l'interface admin car trop problématique. Faute de mieux, envoyer un mail à - KDE pour modifier ces mails. -- corrige un crash sporadique sur la page d'inscription au système de petits - cours - -### K-Fêt - -- (fix partiel) Empêche la K-Fêt de modifier des données COF (e.g. nom, prénom, - username) lors de la création d'un nouveau compte. -- Les statistiques de conso globales montrent deux courbes COF / non-COF au - lieu de LIQ / sur compte. -- Un bug empêchait de fermer manuellement la K-Fêt depuis un compte non - privilégié en tapant un mot de passe. C'est corrigé. - -## Version 0.7.2 - 08/09/2020 - -- Nouvelle page 404 -- Correction de bug en K-Fêt : le lien pour créer un nouveau compte exté apparaît - à nouveau dans l'autocomplétion - -## Version 0.7.1 - 05/09/2020 - -Petits ajustements sur le site du COF : - -- Possibilité d'ajouter des champs d'infos supplémentaires en plus de l'email et - de la page web dans les annuaires (clubs et partenaires). -- Corrige un bug d'affichage des adresses emails de clubs - -## Version 0.7 - 29/08/2020 - -### GestioBDS - -- Ajout d'un bouton pour supprimer un compte -- Le nombre d'adhérent⋅es est affiché sur la page d'accueil -- le groupe BDS a les bonnes permissions - -### Site du COF - -- Captcha fonctionnel pour les mailing-listes - -### K-Fêt - -- L'autocomplétion pour la création de compte K-Fêt se lance à 3 caractères seulement, -donc est plus rapide. - -## Version 0.6 - 27/07/2020 - -Arrivée du BDS ! -GestioCOF et GestioBDS ont du code en commun mais tournent de façon séparée, les -deux bases de données sont distinctes. - -## Version 0.5 - 11/07/2020 +## Upcoming ### Problèmes corrigés -- La recherche d'utilisateurices (COF + K-Fêt) fonctionne de nouveau -- Bug d'affichage quand on a beaucoup de clubs dans le cadre "Accès rapide" sur - la page des clubs (nouveau site du COF) -- Version mobile plus ergonimique sur le nouveau site du COF - Cliquer sur "visualiser" sur les pages de clubs dans wagtail ne provoque plus - d'erreurs 500 (nouveau site du COF) -- L'historique des ventes des articles K-Fêt fonctionne à nouveau + d'erreurs 500. +- L'historique des ventes des articles fonctionne à nouveau - Les montants en K-Fêt sont à nouveau affichés en UKF (et non en €). - Les boutons "afficher/cacher" des mails et noms des participant⋅e⋅s à un spectacle BdA fonctionnent à nouveau. -- on ne peut plus compter de consos sur ☠☠☠, ni éditer les comptes spéciaux -(LIQ, GNR, ☠☠☠, #13). - -### Nouvelles fonctionnalités - -- On n'affiche que 4 articles sur la pages "nouveautés" (nouveau site du COF) -- Plus de traductions sur le nouveau site du COF -- Les transferts apparaissent maintenant dans l'historique K-Fêt et l'historique - personnel. -- les statistiques K-Fêt remontent à plus d'un an (et le code est simplifié) ## Version 0.4.1 - 17/01/2020 diff --git a/README.md b/README.md index 5708277c..ffe680db 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GestioCOF / GestioBDS +# GestioCOF [![pipeline status](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/pipeline.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) [![coverage report](https://git.eleves.ens.fr/cof-geek/gestioCOF/badges/master/coverage.svg)](https://git.eleves.ens.fr/cof-geek/gestioCOF/commits/master) @@ -18,7 +18,7 @@ Il vous faudra installer pip, les librairies de développement de python ainsi que sqlite3, un moteur de base de données léger et simple d'utilisation. Sous Debian et dérivées (Ubuntu, ...) : - sudo apt-get install python3-pip python3-dev python3-venv sqlite3 libsasl2-dev python-dev-is-python3 libldap2-dev libssl-dev + sudo apt-get install python3-pip python3-dev python3-venv sqlite3 Si vous décidez d'utiliser un environnement virtuel Python (virtualenv; fortement conseillé), déplacez-vous dans le dossier où est installé GestioCOF @@ -30,15 +30,7 @@ Pour l'activer, il faut taper . venv/bin/activate -depuis le même dossier. Pour préparer l'environnement à l'utilisation de `./manage.py` -(qui permet de faire des tests en local), il faut également taper - - export CREDENTIALS_DIRECTORY=$(realpath .credentials) - export DJANGO_SETTINGS_MODULE=gestioasso.settings.local - export GESTIOCOF_DEBUG=true - export GESTIOCOF_STATIC_ROOT=$(realpath .static) - export GESTIOBDS_DEBUG=true - export GESTIOBDS_STATIC_ROOT=$(realpath .static) +depuis le même dossier. Vous pouvez maintenant installer les dépendances Python depuis le fichier `requirements-devel.txt` : @@ -46,11 +38,11 @@ Vous pouvez maintenant installer les dépendances Python depuis le fichier pip install -U pip # parfois nécessaire la première fois pip install -r requirements-devel.txt -Pour terminer, copier le fichier `gestioasso/settings/secret_example.py` vers -`gestioasso/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique +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: - ln -s secret_example.py gestioasso/settings/secret.py + ln -s secret_example.py cof/settings/secret.py Nous avons un git hook de pre-commit pour formatter et vérifier que votre code vérifie nos conventions. Pour bénéficier des mises à jour du hook, préférez diff --git a/Vagrantfile b/Vagrantfile index f34653a5..e12a45ed 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,19 +1,47 @@ # -*- mode: ruby -*- # vi: set ft=ruby : -# Configuration de base pour GestioCOF. -# Voir https://docs.vagrantup.com pour plus d'informations. +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. Vagrant.configure(2) do |config| - # On se base sur Debian 10 (Buster) pour avoir le même environnement qu'en - # production. - config.vm.box = "debian/contrib-buster64" + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + config.vm.box = "ubuntu/xenial64" # On associe le port 80 dans la machine virtuelle avec le port 8080 de notre # ordinateur, et le port 8000 avec le port 8000. config.vm.network :forwarded_port, guest: 80, host: 8080 config.vm.network :forwarded_port, guest: 8000, host: 8000 - # Le restes de la configuration (installation de paquets, etc) est géré un - # script shell. + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + # config.vm.provision "shell", inline: <<-SHELL + # sudo apt-get update + # sudo apt-get install -y apache2 + # SHELL config.vm.provision :shell, path: "provisioning/bootstrap.sh" end diff --git a/bda/admin.py b/bda/admin.py index f52f721d..7f626c7a 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -1,11 +1,10 @@ from datetime import timedelta +from custommail.shortcuts import send_mass_custom_mail from dal.autocomplete import ModelSelect2 from django import forms from django.contrib import admin -from django.core.mail import send_mass_mail from django.db.models import Count, Q, Sum -from django.template import loader from django.template.defaultfilters import pluralize from django.utils import timezone @@ -33,6 +32,20 @@ class ReadOnlyMixin(object): return readonly_fields + self.readonly_fields_update +class ChoixSpectacleAdminForm(forms.ModelForm): + class Meta: + widgets = { + "participant": ModelSelect2(url="bda-participant-autocomplete"), + "spectacle": ModelSelect2(url="bda-spectacle-autocomplete"), + } + + +class ChoixSpectacleInline(admin.TabularInline): + model = ChoixSpectacle + form = ChoixSpectacleAdminForm + sortable_field_name = "priority" + + class AttributionTabularAdminForm(forms.ModelForm): listing = None @@ -80,17 +93,14 @@ class WithoutListingAttributionInline(AttributionInline): class ParticipantAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - queryset = Spectacle.objects.select_related("location") - - if self.instance.pk is not None: - queryset = queryset.filter(tirage=self.instance.tirage) - - self.fields["choicesrevente"].queryset = queryset + self.fields["choicesrevente"].queryset = Spectacle.objects.select_related( + "location" + ) class ParticipantPaidFilter(admin.SimpleListFilter): """ - Permet de filtrer les participants sur s'ils ont payé leurs places ou pas + Permet de filtrer les participants sur s'ils ont payé leurs places ou pas """ title = "A payé" @@ -159,23 +169,19 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): form = ParticipantAdminForm def send_attribs(self, request, queryset): - emails = [] + datatuple = [] for member in queryset.all(): - subject = "Résultats du tirage au sort" attribs = member.attributions.all() context = {"member": member.user} - - template_name = "" + shortname = "" if len(attribs) == 0: - template_name = "bda/mails/attributions-decus.txt" + shortname = "bda-attributions-decus" else: - template_name = "bda/mails/attributions.txt" + shortname = "bda-attributions" context["places"] = attribs - - message = loader.render_to_string(template_name, context) - emails.append((subject, message, "bda@ens.fr", [member.user.email])) - - send_mass_mail(emails) + print(context) + datatuple.append((shortname, context, "bda@ens.fr", [member.user.email])) + send_mass_custom_mail(datatuple) count = len(queryset.all()) if count == 1: message_bit = "1 membre a" @@ -191,6 +197,17 @@ class ParticipantAdmin(ReadOnlyMixin, admin.ModelAdmin): class AttributionAdminForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if "spectacle" in self.fields: + self.fields["spectacle"].queryset = Spectacle.objects.select_related( + "location" + ) + if "participant" in self.fields: + self.fields["participant"].queryset = Participant.objects.select_related( + "user", "tirage" + ) + def clean(self): cleaned_data = super().clean() participant = cleaned_data.get("participant") @@ -203,14 +220,9 @@ class AttributionAdminForm(forms.ModelForm): ) return cleaned_data - class Meta: - widgets = { - "participant": ModelSelect2(url="bda-participant-autocomplete"), - "spectacle": ModelSelect2(url="bda-spectacle-autocomplete"), - } - class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): + list_display = ("id", "spectacle", "participant", "given", "paid") search_fields = ( "spectacle__title", @@ -223,7 +235,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin): - autocomplete_fields = ["participant", "spectacle"] + form = ChoixSpectacleAdminForm def tirage(self, obj): return obj.participant.tirage @@ -267,14 +279,15 @@ class SalleAdmin(admin.ModelAdmin): class SpectacleReventeAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - qset = Participant.objects.select_related("user", "tirage") - - if self.instance.pk is not None: - qset = qset.filter(tirage=self.instance.seller.tirage) - - self.fields["confirmed_entry"].queryset = qset - self.fields["seller"].queryset = qset - self.fields["soldTo"].queryset = qset + self.fields["confirmed_entry"].queryset = Participant.objects.select_related( + "user", "tirage" + ) + self.fields["seller"].queryset = Participant.objects.select_related( + "user", "tirage" + ) + self.fields["soldTo"].queryset = Participant.objects.select_related( + "user", "tirage" + ) class SpectacleReventeAdmin(admin.ModelAdmin): diff --git a/bda/algorithm.py b/bda/algorithm.py index 078f2be8..add09335 100644 --- a/bda/algorithm.py +++ b/bda/algorithm.py @@ -2,6 +2,7 @@ import random class Algorithm(object): + shows = None ranks = None origranks = None @@ -9,10 +10,10 @@ class Algorithm(object): def __init__(self, shows, members, choices): """Initialisation : - - on aggrège toutes les demandes pour chaque spectacle dans - show.requests - - on crée des tables de demandes pour chaque personne, afin de - pouvoir modifier les rankings""" + - on aggrège toutes les demandes pour chaque spectacle dans + show.requests + - on crée des tables de demandes pour chaque personne, afin de + pouvoir modifier les rankings""" self.max_group = 2 * max(choice.priority for choice in choices) self.shows = [] showdict = {} diff --git a/bda/forms.py b/bda/forms.py index d1d0f74f..bb79932e 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -3,7 +3,7 @@ from django.forms.models import BaseInlineFormSet from django.template import loader from django.utils import timezone -from bda.models import SpectacleRevente +from bda.models import Attribution, Spectacle, SpectacleRevente class InscriptionInlineFormSet(BaseInlineFormSet): @@ -77,7 +77,7 @@ class ResellForm(forms.Form): super().__init__(*args, **kwargs) self.fields["attributions"] = TemplateLabelField( queryset=participant.attribution_set.filter( - spectacle__date__gte=timezone.now(), paid=True + spectacle__date__gte=timezone.now() ) .exclude(revente__seller=participant) .select_related("spectacle", "spectacle__location", "participant__user"), diff --git a/bda/management/commands/loadbdadevdata.py b/bda/management/commands/loadbdadevdata.py index 186e1da7..a608db6a 100644 --- a/bda/management/commands/loadbdadevdata.py +++ b/bda/management/commands/loadbdadevdata.py @@ -81,7 +81,7 @@ class Command(MyBaseCommand): shows = random.sample( list(tirage.spectacle_set.all()), tirage.spectacle_set.count() // 2 ) - for rank, show in enumerate(shows): + for (rank, show) in enumerate(shows): choices.append( ChoixSpectacle( participant=part, diff --git a/bda/migrations/0001_initial.py b/bda/migrations/0001_initial.py index 5bc848c8..077ddd4e 100644 --- a/bda/migrations/0001_initial.py +++ b/bda/migrations/0001_initial.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] operations = [ diff --git a/bda/migrations/0002_add_tirage.py b/bda/migrations/0002_add_tirage.py index c2c6bd3c..f4b01ed2 100644 --- a/bda/migrations/0002_add_tirage.py +++ b/bda/migrations/0002_add_tirage.py @@ -35,6 +35,7 @@ def fill_tirage_fields(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("bda", "0001_initial")] operations = [ diff --git a/bda/migrations/0003_update_tirage_and_spectacle.py b/bda/migrations/0003_update_tirage_and_spectacle.py index 07f3742e..3548eb88 100644 --- a/bda/migrations/0003_update_tirage_and_spectacle.py +++ b/bda/migrations/0003_update_tirage_and_spectacle.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0002_add_tirage")] operations = [ diff --git a/bda/migrations/0004_mails-rappel.py b/bda/migrations/0004_mails-rappel.py index 407353a4..d331568a 100644 --- a/bda/migrations/0004_mails-rappel.py +++ b/bda/migrations/0004_mails-rappel.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0003_update_tirage_and_spectacle")] operations = [ diff --git a/bda/migrations/0005_encoding.py b/bda/migrations/0005_encoding.py index 29ee0027..eedfcee4 100644 --- a/bda/migrations/0005_encoding.py +++ b/bda/migrations/0005_encoding.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0004_mails-rappel")] operations = [ diff --git a/bda/migrations/0006_add_tirage_switch.py b/bda/migrations/0006_add_tirage_switch.py index 1535a5fe..ccfe7505 100644 --- a/bda/migrations/0006_add_tirage_switch.py +++ b/bda/migrations/0006_add_tirage_switch.py @@ -18,6 +18,7 @@ def forwards_func(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("bda", "0005_encoding")] operations = [ diff --git a/bda/migrations/0007_extends_spectacle.py b/bda/migrations/0007_extends_spectacle.py index 48865acb..87182ff7 100644 --- a/bda/migrations/0007_extends_spectacle.py +++ b/bda/migrations/0007_extends_spectacle.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0006_add_tirage_switch")] operations = [ diff --git a/bda/migrations/0008_py3.py b/bda/migrations/0008_py3.py index 3a7dfeb1..6aa69abd 100644 --- a/bda/migrations/0008_py3.py +++ b/bda/migrations/0008_py3.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0007_extends_spectacle")] operations = [ diff --git a/bda/migrations/0009_revente.py b/bda/migrations/0009_revente.py index 7a547f85..d888140f 100644 --- a/bda/migrations/0009_revente.py +++ b/bda/migrations/0009_revente.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0008_py3")] operations = [ diff --git a/bda/migrations/0010_spectaclerevente_shotgun.py b/bda/migrations/0010_spectaclerevente_shotgun.py index ae0fdff1..da5c014c 100644 --- a/bda/migrations/0010_spectaclerevente_shotgun.py +++ b/bda/migrations/0010_spectaclerevente_shotgun.py @@ -12,15 +12,15 @@ def forwards_func(apps, schema_editor): for revente in SpectacleRevente.objects.all(): is_expired = timezone.now() > revente.date_tirage() - is_direct = ( - revente.attribution.spectacle.date >= revente.date - and timezone.now() > revente.date + timedelta(minutes=15) + is_direct = revente.attribution.spectacle.date >= revente.date and timezone.now() > revente.date + timedelta( + minutes=15 ) revente.shotgun = is_expired or is_direct revente.save() class Migration(migrations.Migration): + dependencies = [("bda", "0009_revente")] operations = [ diff --git a/bda/migrations/0011_tirage_appear_catalogue.py b/bda/migrations/0011_tirage_appear_catalogue.py index a8c49e2d..446be392 100644 --- a/bda/migrations/0011_tirage_appear_catalogue.py +++ b/bda/migrations/0011_tirage_appear_catalogue.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0010_spectaclerevente_shotgun")] operations = [ diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py index 78ef8dce..96853a24 100644 --- a/bda/migrations/0012_notif_time.py +++ b/bda/migrations/0012_notif_time.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0011_tirage_appear_catalogue")] operations = [ diff --git a/bda/migrations/0012_swap_double_choice.py b/bda/migrations/0012_swap_double_choice.py index dcb8056d..e712f2ff 100644 --- a/bda/migrations/0012_swap_double_choice.py +++ b/bda/migrations/0012_swap_double_choice.py @@ -13,6 +13,7 @@ def swap_double_choice(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("bda", "0011_tirage_appear_catalogue")] operations = [ diff --git a/bda/migrations/0013_merge_20180524_2123.py b/bda/migrations/0013_merge_20180524_2123.py index b974abf2..8f78b6a9 100644 --- a/bda/migrations/0013_merge_20180524_2123.py +++ b/bda/migrations/0013_merge_20180524_2123.py @@ -6,6 +6,7 @@ from django.db import migrations class Migration(migrations.Migration): + dependencies = [("bda", "0012_notif_time"), ("bda", "0012_swap_double_choice")] operations = [] diff --git a/bda/migrations/0014_attribution_paid_field.py b/bda/migrations/0014_attribution_paid_field.py index e5ef2b2d..b5bb6208 100644 --- a/bda/migrations/0014_attribution_paid_field.py +++ b/bda/migrations/0014_attribution_paid_field.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0013_merge_20180524_2123")] operations = [ diff --git a/bda/migrations/0015_move_bda_payment.py b/bda/migrations/0015_move_bda_payment.py index a39a159c..93f121a1 100644 --- a/bda/migrations/0015_move_bda_payment.py +++ b/bda/migrations/0015_move_bda_payment.py @@ -29,6 +29,7 @@ def set_participant_payment(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("bda", "0014_attribution_paid_field")] operations = [ diff --git a/bda/migrations/0016_delete_participant_paid.py b/bda/migrations/0016_delete_participant_paid.py index 86a17b24..f59d1eb9 100644 --- a/bda/migrations/0016_delete_participant_paid.py +++ b/bda/migrations/0016_delete_participant_paid.py @@ -4,6 +4,7 @@ from django.db import migrations class Migration(migrations.Migration): + dependencies = [("bda", "0015_move_bda_payment")] operations = [ diff --git a/bda/migrations/0017_participant_accepte_charte.py b/bda/migrations/0017_participant_accepte_charte.py index 3157654b..6bd32d8f 100644 --- a/bda/migrations/0017_participant_accepte_charte.py +++ b/bda/migrations/0017_participant_accepte_charte.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("bda", "0016_delete_participant_paid")] operations = [ diff --git a/bda/migrations/0018_auto_20201021_1818.py b/bda/migrations/0018_auto_20201021_1818.py deleted file mode 100644 index 444f32d8..00000000 --- a/bda/migrations/0018_auto_20201021_1818.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 2.2.12 on 2020-10-21 16:18 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("bda", "0017_participant_accepte_charte"), - ] - - operations = [ - migrations.AlterModelOptions( - name="participant", - options={"ordering": ("-tirage", "user__last_name", "user__first_name")}, - ), - migrations.AddField( - model_name="tirage", - name="archived", - field=models.BooleanField(default=False, verbose_name="Archivé"), - ), - migrations.AlterField( - model_name="participant", - name="tirage", - field=models.ForeignKey( - limit_choices_to={"archived": False}, - on_delete=django.db.models.deletion.CASCADE, - to="bda.Tirage", - ), - ), - migrations.AddConstraint( - model_name="participant", - constraint=models.UniqueConstraint( - fields=("tirage", "user"), name="unique_tirage" - ), - ), - ] diff --git a/bda/migrations/0019_auto_20220630_1245.py b/bda/migrations/0019_auto_20220630_1245.py deleted file mode 100644 index 12b7149d..00000000 --- a/bda/migrations/0019_auto_20220630_1245.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.13 on 2022-06-30 10:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("bda", "0018_auto_20201021_1818"), - ] - - operations = [ - migrations.AlterUniqueTogether( - name="choixspectacle", - unique_together=set(), - ), - migrations.AddConstraint( - model_name="choixspectacle", - constraint=models.UniqueConstraint( - fields=("participant", "spectacle"), name="unique_participation" - ), - ), - ] diff --git a/bda/migrations/0019_auto_20240707_1359.py b/bda/migrations/0019_auto_20240707_1359.py deleted file mode 100644 index ad25e0d6..00000000 --- a/bda/migrations/0019_auto_20240707_1359.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.2.28 on 2024-07-07 11:59 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bda', '0018_auto_20201021_1818'), - ] - - operations = [ - migrations.AlterField( - model_name='attribution', - name='paymenttype', - field=models.CharField(blank=True, choices=[('cash', 'Cash'), ('cb', 'CB'), ('cheque', 'Chèque'), ('virement', 'Virement'), ('autre', 'Autre')], max_length=8, verbose_name='Moyen de paiement'), - ), - ] diff --git a/bda/migrations/0020_merge_0019_auto_20220630_1245_0019_auto_20240707_1359.py b/bda/migrations/0020_merge_0019_auto_20220630_1245_0019_auto_20240707_1359.py deleted file mode 100644 index a8c7a72e..00000000 --- a/bda/migrations/0020_merge_0019_auto_20220630_1245_0019_auto_20240707_1359.py +++ /dev/null @@ -1,13 +0,0 @@ -# Generated by Django 4.2.16 on 2025-02-26 08:23 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("bda", "0019_auto_20220630_1245"), - ("bda", "0019_auto_20240707_1359"), - ] - - operations = [] diff --git a/bda/models.py b/bda/models.py index af0d49fb..f4a0fac6 100644 --- a/bda/models.py +++ b/bda/models.py @@ -2,14 +2,14 @@ import calendar import random from datetime import timedelta +from custommail.models import CustomMail +from custommail.shortcuts import send_mass_custom_mail from django.conf import settings from django.contrib.auth.models import User from django.contrib.sites.models import Site from django.core import mail -from django.core.mail import EmailMessage, send_mass_mail from django.db import models from django.db.models import Count, Exists -from django.template import loader from django.utils import formats, timezone @@ -31,7 +31,6 @@ class Tirage(models.Model): "Tirage à afficher dans le catalogue", default=False ) enable_do_tirage = models.BooleanField("Le tirage peut être lancé", default=False) - archived = models.BooleanField("Archivé", default=False) def __str__(self): return "%s - %s" % ( @@ -117,19 +116,16 @@ class Spectacle(models.Model): bda_generic.nb_attr = 1 members.append(bda_generic) # On écrit un mail personnalisé à chaque participant - mails = [ + datatuple = [ ( - str(self), - loader.render_to_string( - "bda/mails/rappel.txt", - context={"member": member, "nb_attr": member.nb_attr, "show": self}, - ), + "bda-rappel", + {"member": member, "nb_attr": member.nb_attr, "show": self}, settings.MAIL_DATA["rappels"]["FROM"], [member.email], ) for member in members ] - send_mass_mail(mails) + send_mass_custom_mail(datatuple) # On enregistre le fait que l'envoi a bien eu lieu self.rappel_sent = timezone.now() self.save() @@ -151,7 +147,6 @@ PAYMENT_TYPES = ( ("cash", "Cash"), ("cb", "CB"), ("cheque", "Chèque"), - ("virement", "Virement"), ("autre", "Autre"), ) @@ -164,7 +159,7 @@ class Attribution(models.Model): given = models.BooleanField("Donnée", default=False) paid = models.BooleanField("Payée", default=False) paymenttype = models.CharField( - "Moyen de paiement", max_length=8, choices=PAYMENT_TYPES, blank=True + "Moyen de paiement", max_length=6, choices=PAYMENT_TYPES, blank=True ) def __str__(self): @@ -177,8 +172,8 @@ class Attribution(models.Model): class ParticipantPaidQueryset(models.QuerySet): """ - Un manager qui annote le queryset avec un champ `paid`, - indiquant si un participant a payé toutes ses attributions. + Un manager qui annote le queryset avec un champ `paid`, + indiquant si un participant a payé toutes ses attributions. """ def annotate_paid(self): @@ -199,9 +194,7 @@ class Participant(models.Model): attributions = models.ManyToManyField( Spectacle, through="Attribution", related_name="attributed_to" ) - tirage = models.ForeignKey( - Tirage, on_delete=models.CASCADE, limit_choices_to={"archived": False} - ) + tirage = models.ForeignKey(Tirage, on_delete=models.CASCADE) accepte_charte = models.BooleanField("A accepté la charte BdA", default=False) choicesrevente = models.ManyToManyField( Spectacle, related_name="subscribed", blank=True @@ -212,12 +205,6 @@ class Participant(models.Model): def __str__(self): return "%s - %s" % (self.user, self.tirage.title) - class Meta: - ordering = ("-tirage", "user__last_name", "user__first_name") - constraints = [ - models.UniqueConstraint(fields=("tirage", "user"), name="unique_tirage"), - ] - DOUBLE_CHOICES = ( ("1", "1 place"), @@ -254,11 +241,7 @@ class ChoixSpectacle(models.Model): class Meta: ordering = ("priority",) - constraints = [ - models.UniqueConstraint( - fields=["participant", "spectacle"], name="unique_participation" - ) - ] + unique_together = (("participant", "spectacle"),) verbose_name = "voeu" verbose_name_plural = "voeux" @@ -365,24 +348,21 @@ class SpectacleRevente(models.Model): BdA-Revente à tous les intéressés. """ inscrits = self.attribution.spectacle.subscribed.select_related("user") - mails = [ + datatuple = [ ( - "BdA-Revente : {}".format(self.attribution.spectacle.title), - loader.render_to_string( - "bda/mails/revente-new.txt", - context={ - "member": participant.user, - "show": self.attribution.spectacle, - "revente": self, - "site": Site.objects.get_current(), - }, - ), + "bda-revente", + { + "member": participant.user, + "show": self.attribution.spectacle, + "revente": self, + "site": Site.objects.get_current(), + }, settings.MAIL_DATA["revente"]["FROM"], [participant.user.email], ) for participant in inscrits ] - send_mass_mail(mails) + send_mass_custom_mail(datatuple) self.notif_sent = True self.notif_time = timezone.now() self.save() @@ -393,23 +373,20 @@ class SpectacleRevente(models.Model): leur indiquer qu'il est désormais disponible au shotgun. """ inscrits = self.attribution.spectacle.subscribed.select_related("user") - mails = [ + datatuple = [ ( - "BdA-Revente : {}".format(self.attribution.spectacle.title), - loader.render_to_string( - "bda/mails/revente-shotgun.txt", - context={ - "member": participant.user, - "show": self.attribution.spectacle, - "site": Site.objects.get_current(), - }, - ), + "bda-shotgun", + { + "member": participant.user, + "show": self.attribution.spectacle, + "site": Site.objects.get_current(), + }, settings.MAIL_DATA["revente"]["FROM"], [participant.user.email], ) for participant in inscrits ] - send_mass_mail(mails) + send_mass_custom_mail(datatuple) self.notif_sent = True self.notif_time = timezone.now() # Flag inutile, sauf si l'horloge interne merde @@ -441,30 +418,31 @@ class SpectacleRevente(models.Model): "show": spectacle, } - subject = "BdA-Revente : {}".format(spectacle.title) + c_mails_qs = CustomMail.objects.filter( + shortname__in=[ + "bda-revente-winner", + "bda-revente-loser", + "bda-revente-seller", + ] + ) + + c_mails = {cm.shortname: cm for cm in c_mails_qs} mails.append( - EmailMessage( - subject=subject, - body=loader.render_to_string( - "bda/mails/revente-tirage-winner.txt", - context=context, - ), + c_mails["bda-revente-winner"].get_message( + context, from_email=settings.MAIL_DATA["revente"]["FROM"], to=[winner.user.email], ) ) + mails.append( - EmailMessage( - subject=subject, - body=loader.render_to_string( - "bda/mails/revente-tirage-seller.txt", - context=context, - ), + c_mails["bda-revente-seller"].get_message( + context, from_email=settings.MAIL_DATA["revente"]["FROM"], to=[seller.user.email], reply_to=[winner.user.email], - ), + ) ) # Envoie un mail aux perdants @@ -474,15 +452,11 @@ class SpectacleRevente(models.Model): new_context["acheteur"] = inscrit.user mails.append( - EmailMessage( - subject=subject, - body=loader.render_to_string( - "bda/mails/revente-tirage-loser.txt", - context=new_context, - ), + c_mails["bda-revente-loser"].get_message( + new_context, from_email=settings.MAIL_DATA["revente"]["FROM"], to=[inscrit.user.email], - ), + ) ) mail_conn = mail.get_connection() diff --git a/bda/templates/bda-attrib.html b/bda/templates/bda-attrib.html index 057cacb4..fac0de67 100644 --- a/bda/templates/bda-attrib.html +++ b/bda/templates/bda-attrib.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block extra_head %} diff --git a/bda/templates/bda/etat-places.html b/bda/templates/bda/etat-places.html index d1af0667..401cc856 100644 --- a/bda/templates/bda/etat-places.html +++ b/bda/templates/bda/etat-places.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %}

État des inscriptions BdA

diff --git a/bda/templates/bda/inscription-tirage.html b/bda/templates/bda/inscription-tirage.html index 1eecd7af..3f8091df 100644 --- a/bda/templates/bda/inscription-tirage.html +++ b/bda/templates/bda/inscription-tirage.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block extra_head %} diff --git a/bda/templates/bda/mails-rappel.html b/bda/templates/bda/mails-rappel.html index c0770e47..c10503b0 100644 --- a/bda/templates/bda/mails-rappel.html +++ b/bda/templates/bda/mails-rappel.html @@ -26,6 +26,13 @@
+

+ Note : le template de ce mail peut être modifié à + cette adresse +

+ +
+

Forme des mails

Une seule place

diff --git a/bda/templates/bda/mails/attributions-decus.txt b/bda/templates/bda/mails/attributions-decus.txt deleted file mode 100644 index 69fadff6..00000000 --- a/bda/templates/bda/mails/attributions-decus.txt +++ /dev/null @@ -1,10 +0,0 @@ -Cher-e {{ member.first_name }}, - -Tu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as -obtenu aucune place. - -Nous proposons cependant de nombreuses offres hors-tirage tout au long de -l'année, et nous t'invitons à nous contacter si l'une d'entre elles -t'intéresse ! --- -Le Bureau des Arts \ No newline at end of file diff --git a/bda/templates/bda/mails/attributions.txt b/bda/templates/bda/mails/attributions.txt deleted file mode 100644 index 002763ae..00000000 --- a/bda/templates/bda/mails/attributions.txt +++ /dev/null @@ -1,29 +0,0 @@ -Cher-e {{ member.first_name }}, - -Tu t'es inscrit-e pour le tirage au sort du BdA. Tu as été sélectionné-e -pour les spectacles suivants : -{% for place in places %} -- 1 place pour {{ place }}{% endfor %} - -*Paiement* -Au burô : -L'intégralité de ces places de spectacles est à régler dès maintenant, au bureau du COF pendant les heures de permanences (lundi, mardi, jeudi entre 12h et 14h et entre 18h30 et 19h30, mercredi entre 18h30 et 19h30, vendredi entre 12h et 14h). Les places sont à régler AVANT les représentations. Si vous êtes en vacances, vous pourrez venir les régler dès votre retour. Il est demandé à chacun·e de prendre garde à honorer l’ensemble des places qui lui sont attribuées et de s'engager de fait à payer la ou les place(s) qui lui sont attribuées. - -Par virements : -L'intégralité de ces places de spectacles est à régler dès maintenant par virements. Il vous sera demandé d'envoyer une confirmation de l'envoi de virement à bda@ens.fr. -IBAN AEENS : FR76 4255 9100 0008 0263 8331 927 -Motif de virements : AVR25(ou MAI25)-tirageprintemps-NOM-prénom - -Les places sont à régler AVANT les représentations. Il est demandé à chacun·e de prendre garde à honorer l’ensemble des places qui lui sont attribuées et de s'engager de fait à payer la ou les place(s) qui lui sont attribuées. -Des facilités de paiement sont bien évidemment possibles : nous pouvons ne pas encaisser le chèque immédiatement, ou bien découper votre paiement en deux fois. Pour ceux qui ne pourraient pas venir payer au bureau, merci de nous contacter par mail. - -*Mode de retrait des places* -Au moment du paiement, certaines places vous seront remises directement, d'autres seront à récupérer au cours de l'année, d'autres encore seront nominatives et à retirer le soir même dans les théâtres correspondants. Pour chaque spectacle, vous recevrez un mail quelques jours avant la représentation vous indiquant le mode de retrait. -Nous vous rappelons que l'obtention de places du BdA vous engage à respecter les règles de fonctionnement : -https://bda.ens.fr/lequipe/charte-bda/ - -Un système de revente des places via les mails BdA-revente est disponible directement sur votre compte GestioCOF. Pour pouvoir l'utiliser, il faut que vous ayez payé vos places en amont. - -En vous souhaitant de très beaux spectacles tout au long de l'année, --- -Le Bureau des Arts diff --git a/bda/templates/bda/mails/rappel.txt b/bda/templates/bda/mails/rappel.txt deleted file mode 100644 index 74614cbb..00000000 --- a/bda/templates/bda/mails/rappel.txt +++ /dev/null @@ -1,23 +0,0 @@ -Bonjour {{ member.first_name }}, - -Nous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:"une place,deux places" }} -pour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre ! -{% if nb_attr == 2 %} -Tu as obtenu deux places pour ce spectacle. Nous te rappelons que -ces places sont strictement réservées aux personnes de moins de 28 ans. -{% endif %} -{% if show.listing %}Pour ce spectacle, tu as reçu {{ nb_attr|pluralize:"une place,des places" }} sur -listing. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la représentation -pour {{ nb_attr|pluralize:"la,les" }} retirer. -{% else %}Pour assister à ce spectacle, tu dois présenter les billets qui ont -été distribués au burô. -{% endif %} - -Si tu ne peux plus assister à cette représentation, tu peux -revendre ta place via BdA-revente, accessible directement sur -GestioCOF (lien "revendre une place du premier tirage" sur la page -d'accueil https://www.cof.ens.fr/gestion/). - -En te souhaitant un excellent spectacle, --- -Le Bureau des Arts \ No newline at end of file diff --git a/bda/templates/bda/mails/revente-new.txt b/bda/templates/bda/mails/revente-new.txt deleted file mode 100644 index 7344011a..00000000 --- a/bda/templates/bda/mails/revente-new.txt +++ /dev/null @@ -1,12 +0,0 @@ -Bonjour {{ member.first_name }} - -Une place pour le spectacle {{ show.title }} ({{ show.date }}) -a été postée sur BdA-Revente. - -Si ce spectacle t'intéresse toujours, merci de nous le signaler en cliquant -sur ce lien : https://{{ site }}{% url "bda-revente-confirm" revente.id %}. -Dans le cas où plusieurs personnes seraient intéressées, nous procèderons à -un tirage au sort le {{ revente.date_tirage|date:"DATE_FORMAT" }}. - -Chaleureusement, -Le BdA \ No newline at end of file diff --git a/bda/templates/bda/mails/revente-seller.txt b/bda/templates/bda/mails/revente-seller.txt deleted file mode 100644 index 851ac09c..00000000 --- a/bda/templates/bda/mails/revente-seller.txt +++ /dev/null @@ -1,13 +0,0 @@ -Bonjour {{ vendeur.first_name }}, - -Tu t’es bien inscrit·e pour revendre une place pour {{ show.title }}. - -{% with revente.date_tirage as time %} -Le tirage au sort entre tout·e·s les racheteuse·eur·s potentiel·le·s aura lieu -le {{ time|date:"DATE_FORMAT" }} à {{ time|time:"TIME_FORMAT" }} (dans {{time|timeuntil }}). -Si personne ne s’est inscrit pour racheter la place, celle-ci apparaîtra parmi -les « Places disponibles immédiatement à la revente » sur GestioCOF. -{% endwith %} - -Bonne revente ! -Le Bureau des Arts \ No newline at end of file diff --git a/bda/templates/bda/mails/revente-shotgun-seller.txt b/bda/templates/bda/mails/revente-shotgun-seller.txt deleted file mode 100644 index e67083fc..00000000 --- a/bda/templates/bda/mails/revente-shotgun-seller.txt +++ /dev/null @@ -1,6 +0,0 @@ -Bonjour {{ vendeur.first_name }} ! - -Je souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€. -Contacte-moi si tu es toujours intéressé·e ! - -{{ acheteur.get_full_name }} ({{ acheteur.email }}) \ No newline at end of file diff --git a/bda/templates/bda/mails/revente-shotgun.txt b/bda/templates/bda/mails/revente-shotgun.txt deleted file mode 100644 index e7b1ce29..00000000 --- a/bda/templates/bda/mails/revente-shotgun.txt +++ /dev/null @@ -1,11 +0,0 @@ -Bonjour {{ member.first_name }} - -Une place pour le spectacle {{ show.title }} ({{ show.date }}) -a été postée sur BdA-Revente. - -Puisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour -cette place : elle est disponible immédiatement à l'adresse -https://{{ site }}{% url "bda-revente-buy" show.id %}, à la disposition de tous. - -Chaleureusement, -Le BdA \ No newline at end of file diff --git a/bda/templates/bda/mails/revente-tirage-loser.txt b/bda/templates/bda/mails/revente-tirage-loser.txt deleted file mode 100644 index c1d49a01..00000000 --- a/bda/templates/bda/mails/revente-tirage-loser.txt +++ /dev/null @@ -1,9 +0,0 @@ -Bonjour {{ acheteur.first_name }}, - -Tu t'étais inscrit·e pour la revente de la place de {{ vendeur.get_full_name }} -pour {{ show.title }}. -Malheureusement, une autre personne a été tirée au sort pour racheter la place. -Tu pourras certainement retenter ta chance pour une autre revente ! - -À très bientôt, -Le Bureau des Arts \ No newline at end of file diff --git a/bda/templates/bda/mails/revente-tirage-seller.txt b/bda/templates/bda/mails/revente-tirage-seller.txt deleted file mode 100644 index 7abff7ca..00000000 --- a/bda/templates/bda/mails/revente-tirage-seller.txt +++ /dev/null @@ -1,7 +0,0 @@ -Bonjour {{ vendeur.first_name }}, - -La personne tirée au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}. -Tu peux le/la contacter à l'adresse {{ acheteur.email }}, ou en répondant à ce mail. - -Chaleureusement, -Le BdA \ No newline at end of file diff --git a/bda/templates/bda/mails/revente-tirage-winner.txt b/bda/templates/bda/mails/revente-tirage-winner.txt deleted file mode 100644 index 11428ef7..00000000 --- a/bda/templates/bda/mails/revente-tirage-winner.txt +++ /dev/null @@ -1,7 +0,0 @@ -Bonjour {{ acheteur.first_name }}, - -Tu as été tiré·e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) à {{ show.price|floatformat:2 }}€. -Tu peux contacter le/la vendeur·se à l'adresse {{ vendeur.email }}. - -Chaleureusement, -Le BdA \ No newline at end of file diff --git a/bda/templates/bda/participants.html b/bda/templates/bda/participants.html index c99e5182..492569c9 100644 --- a/bda/templates/bda/participants.html +++ b/bda/templates/bda/participants.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %}

{{ spectacle }}

@@ -16,7 +16,7 @@ {% for participant in participants %} - {{participant.name}} + {{participant.name}} {{participant.nb_places}} place{{participant.nb_places|pluralize}} {{participant.email}} diff --git a/bda/templates/bda/resume_places.html b/bda/templates/bda/resume_places.html index 6c5a2d51..7cbd06ea 100644 --- a/bda/templates/bda/resume_places.html +++ b/bda/templates/bda/resume_places.html @@ -10,20 +10,9 @@ {{place.spectacle.location}} {{place.spectacle.date}} {% if place.double %}deux places{%else%}une place{% endif %} - {% if place.spectacle.listing %}sur listing{% else %}place physique{% endif %} - - {% if place.unpaid == 0 %} - Payé - {% elif place.unpaid == 1 %} - Une place à payer ({{place.unpaid_price|floatformat}}€) - {% else %} - Deux places à payer ({{place.unpaid_price|floatformat}}€) - {% endif %} - {% endfor %} -

Reste à payer : {{ unpaid|floatformat }}€

Total à payer : {{ total|floatformat }}€


Ne manque pas un spectacle avec le diff --git a/bda/templates/bda/revente/confirm-shotgun.html b/bda/templates/bda/revente/confirm-shotgun.html index bf8dccba..d7614c25 100644 --- a/bda/templates/bda/revente/confirm-shotgun.html +++ b/bda/templates/bda/revente/confirm-shotgun.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {%block realcontent %} diff --git a/bda/templates/bda/revente/confirmed.html b/bda/templates/bda/revente/confirmed.html index 6f8ee583..780330bd 100644 --- a/bda/templates/bda/revente/confirmed.html +++ b/bda/templates/bda/revente/confirmed.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %}

Inscription à une revente

diff --git a/bda/templates/bda/revente/mail-success.html b/bda/templates/bda/revente/mail-success.html index 6340a451..5e970eb7 100644 --- a/bda/templates/bda/revente/mail-success.html +++ b/bda/templates/bda/revente/mail-success.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %} diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html index c42e0203..cd09f997 100644 --- a/bda/templates/bda/revente/manage.html +++ b/bda/templates/bda/revente/manage.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %} diff --git a/bda/templates/bda/revente/notpaid.html b/bda/templates/bda/revente/notpaid.html new file mode 100644 index 00000000..0dd4e4df --- /dev/null +++ b/bda/templates/bda/revente/notpaid.html @@ -0,0 +1,6 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

Nope

+

Avant de revendre des places, il faut aller les payer !

+{% endblock %} diff --git a/bda/templates/bda/revente/subscribe.html b/bda/templates/bda/revente/subscribe.html index c91fff15..e0a7176c 100644 --- a/bda/templates/bda/revente/subscribe.html +++ b/bda/templates/bda/revente/subscribe.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles%} {% block realcontent %}

Inscriptions pour BdA-Revente

diff --git a/bda/templates/bda/revente/tirages.html b/bda/templates/bda/revente/tirages.html index 6ef55e03..4d9ac126 100644 --- a/bda/templates/bda/revente/tirages.html +++ b/bda/templates/bda/revente/tirages.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %} diff --git a/bda/templates/spectacle_list.html b/bda/templates/spectacle_list.html index 1ffd7cc3..4539d730 100644 --- a/bda/templates/spectacle_list.html +++ b/bda/templates/spectacle_list.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block extra_head %} diff --git a/bda/tests/test_models.py b/bda/tests/test_models.py index abff407b..ba96238a 100644 --- a/bda/tests/test_models.py +++ b/bda/tests/test_models.py @@ -19,6 +19,8 @@ User = get_user_model() class SpectacleReventeTests(TestCase): + fixtures = ["gestioncof/management/data/custommail.json"] + def setUp(self): now = timezone.now() diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 47cbd2bd..d13fcf6c 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -8,7 +8,7 @@ from django.urls import reverse from django.utils import formats, timezone from ..models import Participant, Tirage -from .mixins import BdATestHelpers, BdAViewTestCaseMixin +from .testcases import BdATestHelpers, BdAViewTestCaseMixin User = get_user_model() @@ -29,7 +29,7 @@ class InscriptionViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/inscription/{}".format(self.tirage.id) + return "/bda/inscription/{}".format(self.tirage.id) def test_get_opened(self): self.tirage.ouverture = timezone.now() - timedelta(days=1) @@ -149,7 +149,7 @@ class PlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/places/{}".format(self.tirage.id) + return "/bda/places/{}".format(self.tirage.id) class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @@ -166,7 +166,7 @@ class EtatPlacesViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/etat-places/{}".format(self.tirage.id) + return "/bda/etat-places/{}".format(self.tirage.id) class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @@ -185,7 +185,7 @@ class TirageViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/tirage/{}".format(self.tirage.id) + return "/bda/tirage/{}".format(self.tirage.id) def test_perform_tirage_disabled(self): # Cannot be performed if disabled @@ -225,7 +225,7 @@ class SpectacleListViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/spectacles/{}".format(self.tirage.id) + return "/bda/spectacles/{}".format(self.tirage.id) class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @@ -242,7 +242,7 @@ class SpectacleViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id) + return "/bda/spectacles/{}/{}".format(self.tirage.id, self.show1.id) class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @@ -259,7 +259,7 @@ class UnpaidViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/spectacles/unpaid/{}".format(self.tirage.id) + return "/bda/spectacles/unpaid/{}".format(self.tirage.id) class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @@ -276,9 +276,10 @@ class SendRemindersViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/bda/mails-rappel/{}".format(self.show1.id) + return "/bda/mails-rappel/{}".format(self.show1.id) def test_post(self): + self.require_custommails() resp = self.client.post(self.url) self.assertEqual(200, resp.status_code) # TODO: check that emails are sent @@ -291,7 +292,7 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): bda_testdata = True def test_api_list(self): - url_list = "/gestion/bda/catalogue/list" + url_list = "/bda/catalogue/list" resp = self.client.get(url_list) self.assertJSONEqual( resp.content.decode("utf-8"), @@ -299,7 +300,7 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): ) def test_api_details(self): - url_details = "/gestion/bda/catalogue/details?id={}".format(self.tirage.id) + url_details = "/bda/catalogue/details?id={}".format(self.tirage.id) resp = self.client.get(url_details) self.assertJSONEqual( resp.content.decode("utf-8"), @@ -310,9 +311,7 @@ class CatalogueViewTestCase(BdATestHelpers, BdAViewTestCaseMixin, TestCase): ) def test_api_descriptions(self): - url_descriptions = "/gestion/bda/catalogue/descriptions?id={}".format( - self.tirage.id - ) + url_descriptions = "/bda/catalogue/descriptions?id={}".format(self.tirage.id) resp = self.client.get(url_descriptions) raw = resp.content.decode("utf-8") try: @@ -356,9 +355,7 @@ class TestReventeManageTest(TestCase): def test_can_get(self): client = Client() - client.force_login( - self.user, backend="django.contrib.auth.backends.ModelBackend" - ) + client.force_login(self.user) r = client.get(self.url) self.assertEqual(r.status_code, 200) diff --git a/bda/tests/mixins.py b/bda/tests/testcases.py similarity index 83% rename from bda/tests/mixins.py rename to bda/tests/testcases.py index 1f690172..f5ac7f83 100644 --- a/bda/tests/mixins.py +++ b/bda/tests/testcases.py @@ -1,6 +1,10 @@ +import os + +from django.conf import settings +from django.core.management import call_command from django.utils import timezone -from shared.tests.mixins import ViewTestCaseMixin +from shared.tests.testcases import ViewTestCaseMixin from ..models import CategorieSpectacle, Salle, Spectacle, Tirage from .utils import create_user @@ -24,6 +28,12 @@ class BdATestHelpers: if self.bda_testdata: self.load_bda_testdata() + def require_custommails(self): + data_file = os.path.join( + settings.BASE_DIR, "gestioncof", "management", "data", "custommail.json" + ) + call_command("syncmails", data_file, verbosity=0) + def load_bda_testdata(self): self.tirage = Tirage.objects.create( title="Test tirage", diff --git a/bda/urls.py b/bda/urls.py index 726c4057..5b452362 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -1,80 +1,74 @@ -from django.urls import re_path +from django.conf.urls import url from bda import views from bda.views import SpectacleListView from gestioncof.decorators import buro_required urlpatterns = [ - re_path( + url( r"^inscription/(?P\d+)$", views.inscription, name="bda-tirage-inscription", ), - re_path(r"^places/(?P\d+)$", views.places, name="bda-places-attribuees"), - re_path( - r"^etat-places/(?P\d+)$", views.etat_places, name="bda-etat-places" - ), - re_path(r"^tirage/(?P\d+)$", views.tirage, name="bda-tirage"), - re_path( + url(r"^places/(?P\d+)$", views.places, name="bda-places-attribuees"), + url(r"^etat-places/(?P\d+)$", views.etat_places, name="bda-etat-places"), + url(r"^tirage/(?P\d+)$", views.tirage, name="bda-tirage"), + url( r"^spectacles/(?P\d+)$", buro_required(SpectacleListView.as_view()), name="bda-liste-spectacles", ), - re_path( + url( r"^spectacles/(?P\d+)/(?P\d+)$", views.spectacle, name="bda-spectacle", ), - re_path( + url( r"^spectacles/unpaid/(?P\d+)$", views.UnpaidParticipants.as_view(), name="bda-unpaid", ), - re_path( + url( r"^spectacles/autocomplete$", views.spectacle_autocomplete, name="bda-spectacle-autocomplete", ), - re_path( + url( r"^participants/autocomplete$", views.participant_autocomplete, name="bda-participant-autocomplete", ), # Urls BdA-Revente - re_path( + url( r"^revente/(?P\d+)/manage$", views.revente_manage, name="bda-revente-manage", ), - re_path( + url( r"^revente/(?P\d+)/subscribe$", views.revente_subscribe, name="bda-revente-subscribe", ), - re_path( + url( r"^revente/(?P\d+)/tirages$", views.revente_tirages, name="bda-revente-tirages", ), - re_path( + url( r"^revente/(?P\d+)/buy$", views.revente_buy, name="bda-revente-buy", ), - re_path( + url( r"^revente/(?P\d+)/confirm$", views.revente_confirm, name="bda-revente-confirm", ), - re_path( + url( r"^revente/(?P\d+)/shotgun$", views.revente_shotgun, name="bda-revente-shotgun", ), - re_path( - r"^mails-rappel/(?P\d+)$", views.send_rappel, name="bda-rappels" - ), - re_path( - r"^catalogue/(?P[a-z]+)$", views.catalogue, name="bda-catalogue" - ), + url(r"^mails-rappel/(?P\d+)$", views.send_rappel, name="bda-rappels"), + url(r"^catalogue/(?P[a-z]+)$", views.catalogue, name="bda-catalogue"), ] diff --git a/bda/views.py b/bda/views.py index 1f949181..f33b7013 100644 --- a/bda/views.py +++ b/bda/views.py @@ -4,17 +4,17 @@ import random import time from collections import defaultdict +from custommail.models import CustomMail +from custommail.shortcuts import send_custom_mail, send_mass_custom_mail from django.conf import settings from django.contrib import messages from django.core import serializers from django.core.exceptions import NON_FIELD_ERRORS -from django.core.mail import send_mail, send_mass_mail from django.db import transaction from django.db.models import Count, Prefetch from django.forms.models import inlineformset_factory from django.http import HttpResponseBadRequest, HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render -from django.template import loader from django.template.defaultfilters import pluralize from django.urls import reverse from django.utils import formats, timezone @@ -42,7 +42,7 @@ from bda.models import ( Tirage, ) from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required -from shared.views import Select2QuerySetView +from utils.views.autocomplete import Select2QuerySetView @cof_required @@ -114,7 +114,6 @@ def places(request, tirage_id): "spectacle__date", "spectacle" ).select_related("spectacle", "spectacle__location") total = sum(place.spectacle.price for place in places) - unpaid = 0 filtered_places = [] places_dict = {} spectacles = [] @@ -125,8 +124,6 @@ def places(request, tirage_id): places_dict[place.spectacle].double = True else: place.double = False - place.unpaid = 0 - place.unpaid_price = 0 places_dict[place.spectacle] = place spectacles.append(place.spectacle) filtered_places.append(place) @@ -135,12 +132,6 @@ def places(request, tirage_id): warning = True else: dates.append(date) - - if not place.paid: - unpaid += place.spectacle.price - places_dict[place.spectacle].unpaid += 1 - places_dict[place.spectacle].unpaid_price += place.spectacle.price - # On prévient l'utilisateur s'il a deux places à la même date if warning: messages.warning( @@ -156,7 +147,6 @@ def places(request, tirage_id): "places": filtered_places, "tirage": tirage, "total": total, - "unpaid": unpaid, }, ) @@ -284,13 +274,13 @@ def do_tirage(tirage_elt, token): results = Algorithm(data["shows"], data["members"], choices)(token) # On compte les places attribuées et les déçus - for _, members, losers in results: + for (_, members, losers) in results: data["total_slots"] += len(members) data["total_losers"] += len(losers) # On calcule le déficit et les bénéfices pour le BdA # FIXME: le traitement de l'opéra est sale - for show, members, _ in results: + for (show, members, _) in results: deficit = (show.slots - len(members)) * show.price data["total_sold"] += show.slots * show.price if deficit >= 0: @@ -303,8 +293,8 @@ def do_tirage(tirage_elt, token): # so assign a single object for each Participant id members_uniq = {} members2 = {} - for show, members, _ in results: - for member, _, _, _ in members: + for (show, members, _) in results: + for (member, _, _, _) in members: if member.id not in members_uniq: members_uniq[member.id] = member members2[member] = [] @@ -395,6 +385,12 @@ def revente_manage(request, tirage_id): user=request.user, tirage=tirage ) + # If the participant has just been created, the `paid` field is not + # automatically added by our custom ObjectManager. Skip the check in this + # scenario. + if not created and not participant.paid: + return render(request, "bda/revente/notpaid.html", {}) + resellform = ResellForm(participant, prefix="resell") annulform = AnnulForm(participant, prefix="annul") soldform = SoldForm(participant, prefix="sold") @@ -404,7 +400,7 @@ def revente_manage(request, tirage_id): if "resell" in request.POST: resellform = ResellForm(participant, request.POST, prefix="resell") if resellform.is_valid(): - mails = [] + datatuple = [] attributions = resellform.cleaned_data["attributions"] with transaction.atomic(): for attribution in attributions: @@ -419,17 +415,16 @@ def revente_manage(request, tirage_id): "show": attribution.spectacle, "revente": revente, } - mails.append( + datatuple.append( ( - "BdA-Revente : {}".format(attribution.spectacle), - loader.render_to_string( - "bda/mails/revente-seller.txt", context=context - ), + "bda-revente-new", + context, settings.MAIL_DATA["revente"]["FROM"], [participant.user.email], ) ) - send_mass_mail(mails) + revente.save() + send_mass_custom_mail(datatuple) # On annule une revente elif "annul" in request.POST: annulform = AnnulForm(participant, request.POST, prefix="annul") @@ -648,16 +643,12 @@ def revente_buy(request, spectacle_id): "acheteur": request.user, "vendeur": revente.seller.user, } - - send_mail( - "BdA-Revente : {}".format(spectacle.title), - loader.render_to_string( - "bda/mails/revente-shotgun-seller.txt", context=context - ), - request.user.email, + send_custom_mail( + "bda-buy-shotgun", + "bda@ens.fr", [revente.seller.user.email], + context=context, ) - return render( request, "bda/revente/mail-success.html", @@ -710,7 +701,7 @@ def spectacle(request, tirage_id, spectacle_id): "username": participant.user.username, "email": participant.user.email, "given": int(attrib.given), - "paid": attrib.paid, + "paid": True, "nb_places": 1, } if participant.id in participants: @@ -760,21 +751,19 @@ class UnpaidParticipants(BuroRequiredMixin, ListView): def send_rappel(request, spectacle_id): show = get_object_or_404(Spectacle, id=spectacle_id) # Mails d'exemples - subject = show.title - body_mail_1place = loader.render_to_string( - "bda/mails/rappel.txt", - context={"member": request.user, "show": show, "nb_attr": 1}, + custommail = CustomMail.objects.get(shortname="bda-rappel") + exemple_mail_1place = custommail.render( + {"member": request.user, "show": show, "nb_attr": 1} ) - body_mail_2places = loader.render_to_string( - "bda/mails/rappel.txt", - context={"member": request.user, "show": show, "nb_attr": 2}, + exemple_mail_2places = custommail.render( + {"member": request.user, "show": show, "nb_attr": 2} ) - # Contexte ctxt = { "show": show, - "exemple_mail_1place": (subject, body_mail_1place), - "exemple_mail_2places": (subject, body_mail_2places), + "exemple_mail_1place": exemple_mail_1place, + "exemple_mail_2places": exemple_mail_2places, + "custommail": custommail, } # Envoi confirmé if request.method == "POST": diff --git a/bds/apps.py b/bds/apps.py index 740d3559..db0cafcb 100644 --- a/bds/apps.py +++ b/bds/apps.py @@ -1,28 +1,5 @@ -from django.apps import AppConfig, apps as global_apps -from django.db.models import Q -from django.db.models.signals import post_migrate - - -def bds_group_perms(app_config, apps=global_apps, **kwargs): - try: - Permission = apps.get_model("auth", "Permission") - Group = apps.get_model("auth", "Group") - - group = Group.objects.get(name="Burô du BDS") - perms = Permission.objects.filter( - Q(content_type__app_label="bds") - | Q(content_type__app_label="auth") & Q(content_type__model="user") - ) - group.permissions.set(perms) - group.save() - - except (LookupError, Group.DoesNotExist): - return +from django.apps import AppConfig class BdsConfig(AppConfig): name = "bds" - verbose_name = "Gestion des adhérent·e·s du BDS" - - def ready(self): - post_migrate.connect(bds_group_perms, sender=self) diff --git a/bds/autocomplete.py b/bds/autocomplete.py deleted file mode 100644 index a9308cb2..00000000 --- a/bds/autocomplete.py +++ /dev/null @@ -1,63 +0,0 @@ -from urllib.parse import urlencode - -from django.contrib.auth import get_user_model -from django.db.models import Q -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from shared import autocomplete - -User = get_user_model() - - -class BDSMemberSearch(autocomplete.ModelSearch): - model = User - search_fields = ["username", "first_name", "last_name"] - verbose_name = _("Membres du BDS") - - def get_queryset_filter(self, *args, **kwargs): - qset_filter = super().get_queryset_filter(*args, **kwargs) - qset_filter &= Q(bds__is_member=True) - return qset_filter - - def result_uuid(self, user): - return user.username - - def result_link(self, user): - return reverse("bds:user.update", args=(user.pk,)) - - -class BDSOthersSearch(autocomplete.ModelSearch): - model = User - search_fields = ["username", "first_name", "last_name"] - verbose_name = _("Non-membres du BDS") - - def get_queryset_filter(self, *args, **kwargs): - qset_filter = super().get_queryset_filter(*args, **kwargs) - qset_filter &= Q(bds__isnull=True) | Q(bds__is_member=False) - return qset_filter - - def result_uuid(self, user): - return user.username - - def result_link(self, user): - return reverse("bds:user.update", args=(user.pk,)) - - -class BDSLDAPSearch(autocomplete.LDAPSearch): - def result_link(self, clipper): - url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,)) - get = {"fullname": clipper.fullname, "mail": clipper.mail} - - return "{}?{}".format(url, urlencode(get)) - - -class BDSSearch(autocomplete.Compose): - search_units = [ - ("members", BDSMemberSearch()), - ("others", BDSOthersSearch()), - ("clippers", BDSLDAPSearch()), - ] - - -bds_search = BDSSearch() diff --git a/bds/forms.py b/bds/forms.py deleted file mode 100644 index 9be0fb5b..00000000 --- a/bds/forms.py +++ /dev/null @@ -1,41 +0,0 @@ -from django import forms -from django.contrib.auth import get_user_model -from django.contrib.auth.forms import UserCreationForm -from django.utils.translation import gettext_lazy as _ - -from bds.models import BDSProfile - -User = get_user_model() - - -class UserForm(forms.ModelForm): - is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False) - - class Meta: - model = User - fields = ["email", "first_name", "last_name"] - - -class UserFromClipperForm(forms.ModelForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields["username"].disabled = True - - class Meta: - model = User - fields = ["username", "email", "first_name", "last_name"] - - -class UserFromScratchForm(UserCreationForm): - class Meta: - model = User - fields = ["username", "email", "first_name", "last_name"] - - -class ProfileForm(forms.ModelForm): - class Meta: - model = BDSProfile - exclude = ["user"] - widgets = { - "birthdate": forms.DateInput(attrs={"type": "date"}, format="%Y-%m-%d") - } diff --git a/bds/migrations/0001_initial.py b/bds/migrations/0001_initial.py index b78a752b..e6fe5377 100644 --- a/bds/migrations/0001_initial.py +++ b/bds/migrations/0001_initial.py @@ -8,6 +8,7 @@ import bds.models class Migration(migrations.Migration): + initial = True dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] diff --git a/bds/migrations/0002_bds_group.py b/bds/migrations/0002_bds_group.py index 73f57885..9b54d35c 100644 --- a/bds/migrations/0002_bds_group.py +++ b/bds/migrations/0002_bds_group.py @@ -1,14 +1,28 @@ # Generated by Django 2.2 on 2019-07-17 14:56 +from django.contrib.auth.management import create_permissions from django.db import migrations +from django.db.models import Q def create_bds_buro_group(apps, schema_editor): + for app_config in apps.get_app_configs(): + create_permissions(app_config, apps=apps, verbosity=0) + Group = apps.get_model("auth", "Group") - Group.objects.get_or_create(name="Burô du BDS") + Permission = apps.get_model("auth", "Permission") + group, created = Group.objects.get_or_create(name="Burô du BDS") + if created: + perms = Permission.objects.filter( + Q(content_type__app_label="bds") + | Q(content_type__app_label="auth") & Q(content_type__model="user") + ) + group.permissions.set(perms) + group.save() class Migration(migrations.Migration): + dependencies = [("bds", "0001_initial")] operations = [ diff --git a/bds/migrations/0003_staff_permission.py b/bds/migrations/0003_staff_permission.py index 7f501af1..1f038eaa 100644 --- a/bds/migrations/0003_staff_permission.py +++ b/bds/migrations/0003_staff_permission.py @@ -4,6 +4,7 @@ from django.db import migrations class Migration(migrations.Migration): + dependencies = [ ("bds", "0002_bds_group"), ] @@ -17,8 +18,5 @@ class Migration(migrations.Migration): "verbose_name_plural": "Profils BDS", }, ), - migrations.RemoveField( - model_name="bdsprofile", - name="is_buro", - ), + migrations.RemoveField(model_name="bdsprofile", name="is_buro",), ] diff --git a/bds/migrations/0004_is_member_cotiz_type.py b/bds/migrations/0004_is_member_cotiz_type.py index 3b550fdf..2910ee85 100644 --- a/bds/migrations/0004_is_member_cotiz_type.py +++ b/bds/migrations/0004_is_member_cotiz_type.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ ("bds", "0003_staff_permission"), ] diff --git a/bds/migrations/0005_remove_bdsprofile_certificate_file.py b/bds/migrations/0005_remove_bdsprofile_certificate_file.py deleted file mode 100644 index 3b7232ff..00000000 --- a/bds/migrations/0005_remove_bdsprofile_certificate_file.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2.14 on 2020-07-27 20:14 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("bds", "0004_is_member_cotiz_type"), - ] - - operations = [ - migrations.RemoveField( - model_name="bdsprofile", - name="certificate_file", - ), - ] diff --git a/bds/migrations/0006_bdsprofile_comments.py b/bds/migrations/0006_bdsprofile_comments.py deleted file mode 100644 index 514c4d55..00000000 --- a/bds/migrations/0006_bdsprofile_comments.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 2.2.12 on 2020-08-28 12:14 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("bds", "0005_remove_bdsprofile_certificate_file"), - ] - - operations = [ - migrations.AddField( - model_name="bdsprofile", - name="comments", - field=models.TextField( - blank=True, - help_text="Attention : l'utilisateur·ice dispose d'un droit d'accès" - " aux données le/la concernant, dont le contenu de ce champ !", - verbose_name="commentaires", - ), - ), - ] diff --git a/bds/mixins.py b/bds/mixins.py deleted file mode 100644 index 43607055..00000000 --- a/bds/mixins.py +++ /dev/null @@ -1,122 +0,0 @@ -from django.contrib.auth.mixins import PermissionRequiredMixin -from django.core.exceptions import ImproperlyConfigured -from django.http import HttpResponseRedirect -from django.views.generic.base import ContextMixin, TemplateResponseMixin, View - - -class StaffRequiredMixin(PermissionRequiredMixin): - permission_required = "bds.is_team" - - -class MultipleFormMixin(ContextMixin): - """Mixin pour gérer plusieurs formulaires dans la même vue. - Le fonctionnement est relativement identique à celui de - FormMixin, dont la documentation est disponible ici : - https://docs.djangoproject.com/en/3.0/ref/class-based-views/mixins-editing/ - - Les principales différences sont : - - au lieu de form_class, il faut donner comme attribut un dict de la forme - {: }, avec tous les formulaires à instancier. On - peut aussi redéfinir `get_form_classes` - - - les données initiales se récupèrent pour chaque form via l'attribut - `_initial` ou la fonction `get__initial`. De même, - si certaines forms sont des `ModelForm`s, on peut définir la fonction - `get__instance`. - - - chaque form a un préfixe rajouté, par défaut , mais qui peut - être customisé via `prefixes` ou `get_prefixes`. - """ - - form_classes = {} - prefixes = {} - initial = {} - - success_url = None - - def get_form_classes(self): - return self.form_classes - - def get_initial(self, form_name): - initial_attr = "%s_initial" % form_name - - initial_method = "get_%s_initial" % form_name - initial_method = getattr(self, initial_method, None) - - if hasattr(self, initial_attr): - return getattr(self, initial_attr) - elif callable(initial_method): - return initial_method() - else: - return self.initial.copy() - - def get_prefix(self, form_name): - return self.prefixes.get(form_name, form_name) - - def get_instance(self, form_name): - # Au cas où certaines des forms soient des ModelForms - instance_method = "get_%s_instance" % form_name - instance_method = getattr(self, instance_method, None) - - if callable(instance_method): - return instance_method() - else: - return None - - def get_form_kwargs(self, form_name): - kwargs = { - "initial": self.get_initial(form_name), - "prefix": self.get_prefix(form_name), - "instance": self.get_instance(form_name), - } - - if self.request.method in ("POST", "PUT"): - kwargs.update({"data": self.request.POST, "files": self.request.FILES}) - - return kwargs - - def get_forms(self): - form_classes = self.get_form_classes() - return { - form_name: form_class(**self.get_form_kwargs(form_name)) - for form_name, form_class in form_classes.items() - } - - def get_success_url(self): - if not self.success_url: - raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.") - return str(self.success_url) - - def form_valid(self, forms): - # on garde le nom form_valid pour l'interface avec SuccessMessageMixin - return HttpResponseRedirect(self.get_success_url()) - - def form_invalid(self, forms): - """If the form is invalid, render the invalid form.""" - return self.render_to_response(self.get_context_data(forms=forms)) - - -class ProcessMultipleFormView(View): - """Équivalent de `ProcessFormView` pour plusieurs forms. - Note : il faut que *tous* les formulaires soient valides pour - qu'ils soient sauvegardés ! - """ - - def get(self, request, *args, **kwargs): - forms = self.get_forms() - return self.render_to_response(self.get_context_data(forms=forms)) - - def post(self, request, *args, **kwargs): - forms = self.get_forms() - if all(form.is_valid() for form in forms.values()): - return self.form_valid(forms) - else: - return self.form_invalid(forms) - - -class BaseMultipleFormView(MultipleFormMixin, ProcessMultipleFormView): - pass - - -class MultipleFormView(TemplateResponseMixin, BaseMultipleFormView): - pass diff --git a/bds/models.py b/bds/models.py index 39540653..1d0072a6 100644 --- a/bds/models.py +++ b/bds/models.py @@ -3,7 +3,6 @@ from os.path import splitext from django.contrib.auth import get_user_model from django.db import models -from django.utils import timezone from django.utils.translation import gettext_lazy as _ from shared.utils import choices_length @@ -63,11 +62,14 @@ class BDSProfile(models.Model): null=True, ) - is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False) - mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False) has_certificate = models.BooleanField(_("certificat médical"), default=False) + certificate_file = models.FileField( + _("fichier de certificat médical"), + upload_to=get_certificate_filename, + blank=True, + ) ASPSL_number = models.CharField( _("numéro AS PSL"), max_length=50, blank=True, null=True @@ -75,6 +77,8 @@ class BDSProfile(models.Model): FFSU_number = models.CharField( _("numéro FFSU"), max_length=50, blank=True, null=True ) + + is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False) cotisation_period = models.CharField( _("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3 ) @@ -85,25 +89,6 @@ class BDSProfile(models.Model): _("type de cotisation"), choices=TYPE_COTIZ_CHOICES, max_length=9 ) - comments = models.TextField( - _("commentaires"), - blank=True, - help_text=_( - "Attention : l'utilisateur·ice dispose d'un droit d'accès aux données " - "le/la concernant, dont le contenu de ce champ !" - ), - ) - - @classmethod - def expired_members(cls): - now = timezone.now() - qs = cls.objects.filter(is_member=True) - if now.month > 1 and now.month < 7: - return qs.filter(cotisation_period="SE1") - elif now.month < 2 or now.month > 8: - return qs.none() - return qs - class Meta: verbose_name = _("Profil BDS") verbose_name_plural = _("Profils BDS") diff --git a/bds/static/bds/css/bds.css b/bds/static/bds/css/bds.css deleted file mode 100644 index 86491213..00000000 --- a/bds/static/bds/css/bds.css +++ /dev/null @@ -1 +0,0 @@ -/*! bulma.io v0.9.0 | MIT License | github.com/jgthms/bulma */@keyframes spinAround{from{transform:rotate(0deg)}to{transform:rotate(359deg)}}.is-unselectable,.tabs,.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis,.breadcrumb,.file,.button,.modal-close,.delete{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.navbar-link:not(.is-arrowless)::after,.select:not(.is-multiple):not(.is-loading)::after{border:3px solid transparent;border-radius:2px;border-right:0;border-top:0;content:" ";display:block;height:.625em;margin-top:-0.4375em;pointer-events:none;position:absolute;top:50%;transform:rotate(-45deg);transform-origin:center;width:.625em}.tabs:not(:last-child),.pagination:not(:last-child),.message:not(:last-child),.level:not(:last-child),.breadcrumb:not(:last-child),.highlight:not(:last-child),.block:not(:last-child),.title:not(:last-child),.subtitle:not(:last-child),.table-container:not(:last-child),.table:not(:last-child),.progress:not(:last-child),.notification:not(:last-child),.content:not(:last-child),.box:not(:last-child){margin-bottom:1.5rem}.modal-close,.delete{-moz-appearance:none;-webkit-appearance:none;background-color:rgba(10,10,10,.2);border:none;border-radius:290486px;cursor:pointer;pointer-events:auto;display:inline-block;flex-grow:0;flex-shrink:0;font-size:0;height:20px;max-height:20px;max-width:20px;min-height:20px;min-width:20px;outline:none;position:relative;vertical-align:top;width:20px}.modal-close::before,.delete::before,.modal-close::after,.delete::after{background-color:#fff;content:"";display:block;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%) rotate(45deg);transform-origin:center center}.modal-close::before,.delete::before{height:2px;width:50%}.modal-close::after,.delete::after{height:50%;width:2px}.modal-close:hover,.delete:hover,.modal-close:focus,.delete:focus{background-color:rgba(10,10,10,.3)}.modal-close:active,.delete:active{background-color:rgba(10,10,10,.4)}.is-small.modal-close,.is-small.delete{height:16px;max-height:16px;max-width:16px;min-height:16px;min-width:16px;width:16px}.is-medium.modal-close,.is-medium.delete{height:24px;max-height:24px;max-width:24px;min-height:24px;min-width:24px;width:24px}.is-large.modal-close,.is-large.delete{height:32px;max-height:32px;max-width:32px;min-height:32px;min-width:32px;width:32px}.control.is-loading::after,.select.is-loading::after,.loader,.button.is-loading::after{animation:spinAround 500ms infinite linear;border:2px solid #dbdbdb;border-radius:290486px;border-right-color:transparent;border-top-color:transparent;content:"";display:block;height:1em;position:relative;width:1em}.hero-video,.is-overlay,.modal-background,.modal,.image.is-square img,.image.is-square .has-ratio,.image.is-1by1 img,.image.is-1by1 .has-ratio,.image.is-5by4 img,.image.is-5by4 .has-ratio,.image.is-4by3 img,.image.is-4by3 .has-ratio,.image.is-3by2 img,.image.is-3by2 .has-ratio,.image.is-5by3 img,.image.is-5by3 .has-ratio,.image.is-16by9 img,.image.is-16by9 .has-ratio,.image.is-2by1 img,.image.is-2by1 .has-ratio,.image.is-3by1 img,.image.is-3by1 .has-ratio,.image.is-4by5 img,.image.is-4by5 .has-ratio,.image.is-3by4 img,.image.is-3by4 .has-ratio,.image.is-2by3 img,.image.is-2by3 .has-ratio,.image.is-3by5 img,.image.is-3by5 .has-ratio,.image.is-9by16 img,.image.is-9by16 .has-ratio,.image.is-1by2 img,.image.is-1by2 .has-ratio,.image.is-1by3 img,.image.is-1by3 .has-ratio{bottom:0;left:0;position:absolute;right:0;top:0}.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis,.file-cta,.file-name,.select select,.textarea,.input,.button{-moz-appearance:none;-webkit-appearance:none;align-items:center;border:1px solid transparent;border-radius:4px;box-shadow:none;display:inline-flex;font-size:1rem;height:2.5em;justify-content:flex-start;line-height:1.5;padding-bottom:calc(0.5em - 1px);padding-left:calc(0.75em - 1px);padding-right:calc(0.75em - 1px);padding-top:calc(0.5em - 1px);position:relative;vertical-align:top}.pagination-previous:focus,.pagination-next:focus,.pagination-link:focus,.pagination-ellipsis:focus,.file-cta:focus,.file-name:focus,.select select:focus,.textarea:focus,.input:focus,.button:focus,.is-focused.pagination-previous,.is-focused.pagination-next,.is-focused.pagination-link,.is-focused.pagination-ellipsis,.is-focused.file-cta,.is-focused.file-name,.select select.is-focused,.is-focused.textarea,.is-focused.input,.is-focused.button,.pagination-previous:active,.pagination-next:active,.pagination-link:active,.pagination-ellipsis:active,.file-cta:active,.file-name:active,.select select:active,.textarea:active,.input:active,.button:active,.is-active.pagination-previous,.is-active.pagination-next,.is-active.pagination-link,.is-active.pagination-ellipsis,.is-active.file-cta,.is-active.file-name,.select select.is-active,.is-active.textarea,.is-active.input,.is-active.button{outline:none}[disabled].pagination-previous,[disabled].pagination-next,[disabled].pagination-link,[disabled].pagination-ellipsis,[disabled].file-cta,[disabled].file-name,.select select[disabled],[disabled].textarea,[disabled].input,[disabled].button,fieldset[disabled] .pagination-previous,fieldset[disabled] .pagination-next,fieldset[disabled] .pagination-link,fieldset[disabled] .pagination-ellipsis,fieldset[disabled] .file-cta,fieldset[disabled] .file-name,fieldset[disabled] .select select,.select fieldset[disabled] select,fieldset[disabled] .textarea,fieldset[disabled] .input,fieldset[disabled] .button{cursor:not-allowed}/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */html,body,p,ol,ul,li,dl,dt,dd,blockquote,figure,fieldset,legend,textarea,pre,iframe,hr,h1,h2,h3,h4,h5,h6{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,*::before,*::after{box-sizing:inherit}img,video{height:auto;max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}td:not([align]),th:not([align]){text-align:inherit}html{background-color:#fff;font-size:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:300px;overflow-x:hidden;overflow-y:scroll;text-rendering:optimizeLegibility;text-size-adjust:100%}article,aside,figure,footer,header,hgroup,section{display:block}body,button,input,select,textarea{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif}code,pre{-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-family:monospace}body{color:#000;font-size:1em;font-weight:400;line-height:1.5}a{color:#3273dc;cursor:pointer;text-decoration:none}a strong{color:currentColor}a:hover{color:#363636}code{background-color:#f5f5f5;color:#f14668;font-size:.875em;font-weight:normal;padding:.25em .5em .25em}hr{background-color:#f5f5f5;border:none;display:block;height:2px;margin:1.5rem 0}img{height:auto;max-width:100%}input[type=checkbox],input[type=radio]{vertical-align:baseline}small{font-size:.875em}span{font-style:inherit;font-weight:inherit}strong{color:#363636;font-weight:700}fieldset{border:none}pre{-webkit-overflow-scrolling:touch;background-color:#f5f5f5;color:#000;font-size:.875em;overflow-x:auto;padding:1.25rem 1.5rem;white-space:pre;word-wrap:normal}pre code{background-color:transparent;color:currentColor;font-size:1em;padding:0}table td,table th{vertical-align:top}table td:not([align]),table th:not([align]){text-align:inherit}table th{color:#363636}.box{background-color:#fff;border-radius:6px;box-shadow:0 .5em 1em -0.125em rgba(10,10,10,.1),0 0px 0 1px rgba(10,10,10,.02);color:#000;display:block;padding:1.25rem}a.box:hover,a.box:focus{box-shadow:0 .5em 1em -0.125em rgba(10,10,10,.1),0 0 0 1px #3273dc}a.box:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2),0 0 0 1px #3273dc}.button{background-color:#fff;border-color:#dbdbdb;border-width:1px;color:#363636;cursor:pointer;justify-content:center;padding-bottom:calc(0.5em - 1px);padding-left:1em;padding-right:1em;padding-top:calc(0.5em - 1px);text-align:center;white-space:nowrap}.button strong{color:inherit}.button .icon,.button .icon.is-small,.button .icon.is-medium,.button .icon.is-large{height:1.5em;width:1.5em}.button .icon:first-child:not(:last-child){margin-left:calc(-0.5em - 1px);margin-right:.25em}.button .icon:last-child:not(:first-child){margin-left:.25em;margin-right:calc(-0.5em - 1px)}.button .icon:first-child:last-child{margin-left:calc(-0.5em - 1px);margin-right:calc(-0.5em - 1px)}.button:hover,.button.is-hovered{border-color:#b5b5b5;color:#363636}.button:focus,.button.is-focused{border-color:#3273dc;color:#363636}.button:focus:not(:active),.button.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button:active,.button.is-active{border-color:#4a4a4a;color:#363636}.button.is-text{background-color:transparent;border-color:transparent;color:#000;text-decoration:underline}.button.is-text:hover,.button.is-text.is-hovered,.button.is-text:focus,.button.is-text.is-focused{background-color:#f5f5f5;color:#363636}.button.is-text:active,.button.is-text.is-active{background-color:#e8e8e8;color:#363636}.button.is-text[disabled],fieldset[disabled] .button.is-text{background-color:transparent;border-color:transparent;box-shadow:none}.button.is-white{background-color:#fff;border-color:transparent;color:#0a0a0a}.button.is-white:hover,.button.is-white.is-hovered{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.button.is-white:focus,.button.is-white.is-focused{border-color:transparent;color:#0a0a0a}.button.is-white:focus:not(:active),.button.is-white.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.button.is-white:active,.button.is-white.is-active{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.button.is-white[disabled],fieldset[disabled] .button.is-white{background-color:#fff;border-color:transparent;box-shadow:none}.button.is-white.is-inverted{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted:hover,.button.is-white.is-inverted.is-hovered{background-color:#000}.button.is-white.is-inverted[disabled],fieldset[disabled] .button.is-white.is-inverted{background-color:#0a0a0a;border-color:transparent;box-shadow:none;color:#fff}.button.is-white.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a !important}.button.is-white.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-white.is-outlined:hover,.button.is-white.is-outlined.is-hovered,.button.is-white.is-outlined:focus,.button.is-white.is-outlined.is-focused{background-color:#fff;border-color:#fff;color:#0a0a0a}.button.is-white.is-outlined.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-white.is-outlined.is-loading:hover::after,.button.is-white.is-outlined.is-loading.is-hovered::after,.button.is-white.is-outlined.is-loading:focus::after,.button.is-white.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #0a0a0a #0a0a0a !important}.button.is-white.is-outlined[disabled],fieldset[disabled] .button.is-white.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-white.is-inverted.is-outlined:hover,.button.is-white.is-inverted.is-outlined.is-hovered,.button.is-white.is-inverted.is-outlined:focus,.button.is-white.is-inverted.is-outlined.is-focused{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted.is-outlined.is-loading:hover::after,.button.is-white.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-white.is-inverted.is-outlined.is-loading:focus::after,.button.is-white.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-white.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black{background-color:#0a0a0a;border-color:transparent;color:#fff}.button.is-black:hover,.button.is-black.is-hovered{background-color:#040404;border-color:transparent;color:#fff}.button.is-black:focus,.button.is-black.is-focused{border-color:transparent;color:#fff}.button.is-black:focus:not(:active),.button.is-black.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.button.is-black:active,.button.is-black.is-active{background-color:#000;border-color:transparent;color:#fff}.button.is-black[disabled],fieldset[disabled] .button.is-black{background-color:#0a0a0a;border-color:transparent;box-shadow:none}.button.is-black.is-inverted{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted:hover,.button.is-black.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-black.is-inverted[disabled],fieldset[disabled] .button.is-black.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#0a0a0a}.button.is-black.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-black.is-outlined:hover,.button.is-black.is-outlined.is-hovered,.button.is-black.is-outlined:focus,.button.is-black.is-outlined.is-focused{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.button.is-black.is-outlined.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a !important}.button.is-black.is-outlined.is-loading:hover::after,.button.is-black.is-outlined.is-loading.is-hovered::after,.button.is-black.is-outlined.is-loading:focus::after,.button.is-black.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-black.is-outlined[disabled],fieldset[disabled] .button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-black.is-inverted.is-outlined:hover,.button.is-black.is-inverted.is-outlined.is-hovered,.button.is-black.is-inverted.is-outlined:focus,.button.is-black.is-inverted.is-outlined.is-focused{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted.is-outlined.is-loading:hover::after,.button.is-black.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-black.is-inverted.is-outlined.is-loading:focus::after,.button.is-black.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #0a0a0a #0a0a0a !important}.button.is-black.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-light{background-color:#f5f5f5;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:hover,.button.is-light.is-hovered{background-color:#eee;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:focus,.button.is-light.is-focused{border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light:focus:not(:active),.button.is-light.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.button.is-light:active,.button.is-light.is-active{background-color:#e8e8e8;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-light[disabled],fieldset[disabled] .button.is-light{background-color:#f5f5f5;border-color:transparent;box-shadow:none}.button.is-light.is-inverted{background-color:rgba(0,0,0,.7);color:#f5f5f5}.button.is-light.is-inverted:hover,.button.is-light.is-inverted.is-hovered{background-color:rgba(0,0,0,.7)}.button.is-light.is-inverted[disabled],fieldset[disabled] .button.is-light.is-inverted{background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#f5f5f5}.button.is-light.is-loading::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7) !important}.button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-light.is-outlined:hover,.button.is-light.is-outlined.is-hovered,.button.is-light.is-outlined:focus,.button.is-light.is-outlined.is-focused{background-color:#f5f5f5;border-color:#f5f5f5;color:rgba(0,0,0,.7)}.button.is-light.is-outlined.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5 !important}.button.is-light.is-outlined.is-loading:hover::after,.button.is-light.is-outlined.is-loading.is-hovered::after,.button.is-light.is-outlined.is-loading:focus::after,.button.is-light.is-outlined.is-loading.is-focused::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7) !important}.button.is-light.is-outlined[disabled],fieldset[disabled] .button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-light.is-inverted.is-outlined:hover,.button.is-light.is-inverted.is-outlined.is-hovered,.button.is-light.is-inverted.is-outlined:focus,.button.is-light.is-inverted.is-outlined.is-focused{background-color:rgba(0,0,0,.7);color:#f5f5f5}.button.is-light.is-inverted.is-outlined.is-loading:hover::after,.button.is-light.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-light.is-inverted.is-outlined.is-loading:focus::after,.button.is-light.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #f5f5f5 #f5f5f5 !important}.button.is-light.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-dark{background-color:#363636;border-color:transparent;color:#fff}.button.is-dark:hover,.button.is-dark.is-hovered{background-color:#2f2f2f;border-color:transparent;color:#fff}.button.is-dark:focus,.button.is-dark.is-focused{border-color:transparent;color:#fff}.button.is-dark:focus:not(:active),.button.is-dark.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.button.is-dark:active,.button.is-dark.is-active{background-color:#292929;border-color:transparent;color:#fff}.button.is-dark[disabled],fieldset[disabled] .button.is-dark{background-color:#363636;border-color:transparent;box-shadow:none}.button.is-dark.is-inverted{background-color:#fff;color:#363636}.button.is-dark.is-inverted:hover,.button.is-dark.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-dark.is-inverted[disabled],fieldset[disabled] .button.is-dark.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#363636}.button.is-dark.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-dark.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-dark.is-outlined:hover,.button.is-dark.is-outlined.is-hovered,.button.is-dark.is-outlined:focus,.button.is-dark.is-outlined.is-focused{background-color:#363636;border-color:#363636;color:#fff}.button.is-dark.is-outlined.is-loading::after{border-color:transparent transparent #363636 #363636 !important}.button.is-dark.is-outlined.is-loading:hover::after,.button.is-dark.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-outlined.is-loading:focus::after,.button.is-dark.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-dark.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-outlined{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-dark.is-inverted.is-outlined:hover,.button.is-dark.is-inverted.is-outlined.is-hovered,.button.is-dark.is-inverted.is-outlined:focus,.button.is-dark.is-inverted.is-outlined.is-focused{background-color:#fff;color:#363636}.button.is-dark.is-inverted.is-outlined.is-loading:hover::after,.button.is-dark.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-dark.is-inverted.is-outlined.is-loading:focus::after,.button.is-dark.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #363636 #363636 !important}.button.is-dark.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-primary{background-color:#00d1b2;border-color:transparent;color:#fff}.button.is-primary:hover,.button.is-primary.is-hovered{background-color:#00c4a7;border-color:transparent;color:#fff}.button.is-primary:focus,.button.is-primary.is-focused{border-color:transparent;color:#fff}.button.is-primary:focus:not(:active),.button.is-primary.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.button.is-primary:active,.button.is-primary.is-active{background-color:#00b89c;border-color:transparent;color:#fff}.button.is-primary[disabled],fieldset[disabled] .button.is-primary{background-color:#00d1b2;border-color:transparent;box-shadow:none}.button.is-primary.is-inverted{background-color:#fff;color:#00d1b2}.button.is-primary.is-inverted:hover,.button.is-primary.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-primary.is-inverted[disabled],fieldset[disabled] .button.is-primary.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#00d1b2}.button.is-primary.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-primary.is-outlined{background-color:transparent;border-color:#00d1b2;color:#00d1b2}.button.is-primary.is-outlined:hover,.button.is-primary.is-outlined.is-hovered,.button.is-primary.is-outlined:focus,.button.is-primary.is-outlined.is-focused{background-color:#00d1b2;border-color:#00d1b2;color:#fff}.button.is-primary.is-outlined.is-loading::after{border-color:transparent transparent #00d1b2 #00d1b2 !important}.button.is-primary.is-outlined.is-loading:hover::after,.button.is-primary.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-outlined.is-loading:focus::after,.button.is-primary.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-primary.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-outlined{background-color:transparent;border-color:#00d1b2;box-shadow:none;color:#00d1b2}.button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-primary.is-inverted.is-outlined:hover,.button.is-primary.is-inverted.is-outlined.is-hovered,.button.is-primary.is-inverted.is-outlined:focus,.button.is-primary.is-inverted.is-outlined.is-focused{background-color:#fff;color:#00d1b2}.button.is-primary.is-inverted.is-outlined.is-loading:hover::after,.button.is-primary.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-primary.is-inverted.is-outlined.is-loading:focus::after,.button.is-primary.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #00d1b2 #00d1b2 !important}.button.is-primary.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-primary.is-light{background-color:#ebfffc;color:#00947e}.button.is-primary.is-light:hover,.button.is-primary.is-light.is-hovered{background-color:#defffa;border-color:transparent;color:#00947e}.button.is-primary.is-light:active,.button.is-primary.is-light.is-active{background-color:#d1fff8;border-color:transparent;color:#00947e}.button.is-link{background-color:#3273dc;border-color:transparent;color:#fff}.button.is-link:hover,.button.is-link.is-hovered{background-color:#276cda;border-color:transparent;color:#fff}.button.is-link:focus,.button.is-link.is-focused{border-color:transparent;color:#fff}.button.is-link:focus:not(:active),.button.is-link.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.button.is-link:active,.button.is-link.is-active{background-color:#2366d1;border-color:transparent;color:#fff}.button.is-link[disabled],fieldset[disabled] .button.is-link{background-color:#3273dc;border-color:transparent;box-shadow:none}.button.is-link.is-inverted{background-color:#fff;color:#3273dc}.button.is-link.is-inverted:hover,.button.is-link.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-link.is-inverted[disabled],fieldset[disabled] .button.is-link.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#3273dc}.button.is-link.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;color:#3273dc}.button.is-link.is-outlined:hover,.button.is-link.is-outlined.is-hovered,.button.is-link.is-outlined:focus,.button.is-link.is-outlined.is-focused{background-color:#3273dc;border-color:#3273dc;color:#fff}.button.is-link.is-outlined.is-loading::after{border-color:transparent transparent #3273dc #3273dc !important}.button.is-link.is-outlined.is-loading:hover::after,.button.is-link.is-outlined.is-loading.is-hovered::after,.button.is-link.is-outlined.is-loading:focus::after,.button.is-link.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-link.is-outlined[disabled],fieldset[disabled] .button.is-link.is-outlined{background-color:transparent;border-color:#3273dc;box-shadow:none;color:#3273dc}.button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-link.is-inverted.is-outlined:hover,.button.is-link.is-inverted.is-outlined.is-hovered,.button.is-link.is-inverted.is-outlined:focus,.button.is-link.is-inverted.is-outlined.is-focused{background-color:#fff;color:#3273dc}.button.is-link.is-inverted.is-outlined.is-loading:hover::after,.button.is-link.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-link.is-inverted.is-outlined.is-loading:focus::after,.button.is-link.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #3273dc #3273dc !important}.button.is-link.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-link.is-light{background-color:#eef3fc;color:#2160c4}.button.is-link.is-light:hover,.button.is-link.is-light.is-hovered{background-color:#e3ecfa;border-color:transparent;color:#2160c4}.button.is-link.is-light:active,.button.is-link.is-light.is-active{background-color:#d8e4f8;border-color:transparent;color:#2160c4}.button.is-info{background-color:#3298dc;border-color:transparent;color:#fff}.button.is-info:hover,.button.is-info.is-hovered{background-color:#2793da;border-color:transparent;color:#fff}.button.is-info:focus,.button.is-info.is-focused{border-color:transparent;color:#fff}.button.is-info:focus:not(:active),.button.is-info.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(50,152,220,.25)}.button.is-info:active,.button.is-info.is-active{background-color:#238cd1;border-color:transparent;color:#fff}.button.is-info[disabled],fieldset[disabled] .button.is-info{background-color:#3298dc;border-color:transparent;box-shadow:none}.button.is-info.is-inverted{background-color:#fff;color:#3298dc}.button.is-info.is-inverted:hover,.button.is-info.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-info.is-inverted[disabled],fieldset[disabled] .button.is-info.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#3298dc}.button.is-info.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-info.is-outlined{background-color:transparent;border-color:#3298dc;color:#3298dc}.button.is-info.is-outlined:hover,.button.is-info.is-outlined.is-hovered,.button.is-info.is-outlined:focus,.button.is-info.is-outlined.is-focused{background-color:#3298dc;border-color:#3298dc;color:#fff}.button.is-info.is-outlined.is-loading::after{border-color:transparent transparent #3298dc #3298dc !important}.button.is-info.is-outlined.is-loading:hover::after,.button.is-info.is-outlined.is-loading.is-hovered::after,.button.is-info.is-outlined.is-loading:focus::after,.button.is-info.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-info.is-outlined[disabled],fieldset[disabled] .button.is-info.is-outlined{background-color:transparent;border-color:#3298dc;box-shadow:none;color:#3298dc}.button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-info.is-inverted.is-outlined:hover,.button.is-info.is-inverted.is-outlined.is-hovered,.button.is-info.is-inverted.is-outlined:focus,.button.is-info.is-inverted.is-outlined.is-focused{background-color:#fff;color:#3298dc}.button.is-info.is-inverted.is-outlined.is-loading:hover::after,.button.is-info.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-info.is-inverted.is-outlined.is-loading:focus::after,.button.is-info.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #3298dc #3298dc !important}.button.is-info.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-info.is-light{background-color:#eef6fc;color:#1d72aa}.button.is-info.is-light:hover,.button.is-info.is-light.is-hovered{background-color:#e3f1fa;border-color:transparent;color:#1d72aa}.button.is-info.is-light:active,.button.is-info.is-light.is-active{background-color:#d8ebf8;border-color:transparent;color:#1d72aa}.button.is-success{background-color:#48c774;border-color:transparent;color:#fff}.button.is-success:hover,.button.is-success.is-hovered{background-color:#3ec46d;border-color:transparent;color:#fff}.button.is-success:focus,.button.is-success.is-focused{border-color:transparent;color:#fff}.button.is-success:focus:not(:active),.button.is-success.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(72,199,116,.25)}.button.is-success:active,.button.is-success.is-active{background-color:#3abb67;border-color:transparent;color:#fff}.button.is-success[disabled],fieldset[disabled] .button.is-success{background-color:#48c774;border-color:transparent;box-shadow:none}.button.is-success.is-inverted{background-color:#fff;color:#48c774}.button.is-success.is-inverted:hover,.button.is-success.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-success.is-inverted[disabled],fieldset[disabled] .button.is-success.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#48c774}.button.is-success.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-success.is-outlined{background-color:transparent;border-color:#48c774;color:#48c774}.button.is-success.is-outlined:hover,.button.is-success.is-outlined.is-hovered,.button.is-success.is-outlined:focus,.button.is-success.is-outlined.is-focused{background-color:#48c774;border-color:#48c774;color:#fff}.button.is-success.is-outlined.is-loading::after{border-color:transparent transparent #48c774 #48c774 !important}.button.is-success.is-outlined.is-loading:hover::after,.button.is-success.is-outlined.is-loading.is-hovered::after,.button.is-success.is-outlined.is-loading:focus::after,.button.is-success.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-success.is-outlined[disabled],fieldset[disabled] .button.is-success.is-outlined{background-color:transparent;border-color:#48c774;box-shadow:none;color:#48c774}.button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-success.is-inverted.is-outlined:hover,.button.is-success.is-inverted.is-outlined.is-hovered,.button.is-success.is-inverted.is-outlined:focus,.button.is-success.is-inverted.is-outlined.is-focused{background-color:#fff;color:#48c774}.button.is-success.is-inverted.is-outlined.is-loading:hover::after,.button.is-success.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-success.is-inverted.is-outlined.is-loading:focus::after,.button.is-success.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #48c774 #48c774 !important}.button.is-success.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-success.is-light{background-color:#effaf3;color:#257942}.button.is-success.is-light:hover,.button.is-success.is-light.is-hovered{background-color:#e6f7ec;border-color:transparent;color:#257942}.button.is-success.is-light:active,.button.is-success.is-light.is-active{background-color:#dcf4e4;border-color:transparent;color:#257942}.button.is-warning{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning:hover,.button.is-warning.is-hovered{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning:focus,.button.is-warning.is-focused{border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning:focus:not(:active),.button.is-warning.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.button.is-warning:active,.button.is-warning.is-active{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning[disabled],fieldset[disabled] .button.is-warning{background-color:#ffdd57;border-color:transparent;box-shadow:none}.button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted:hover,.button.is-warning.is-inverted.is-hovered{background-color:rgba(0,0,0,.7)}.button.is-warning.is-inverted[disabled],fieldset[disabled] .button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#ffdd57}.button.is-warning.is-loading::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7) !important}.button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;color:#ffdd57}.button.is-warning.is-outlined:hover,.button.is-warning.is-outlined.is-hovered,.button.is-warning.is-outlined:focus,.button.is-warning.is-outlined.is-focused{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.button.is-warning.is-outlined.is-loading::after{border-color:transparent transparent #ffdd57 #ffdd57 !important}.button.is-warning.is-outlined.is-loading:hover::after,.button.is-warning.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-outlined.is-loading:focus::after,.button.is-warning.is-outlined.is-loading.is-focused::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7) !important}.button.is-warning.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;box-shadow:none;color:#ffdd57}.button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-warning.is-inverted.is-outlined:hover,.button.is-warning.is-inverted.is-outlined.is-hovered,.button.is-warning.is-inverted.is-outlined:focus,.button.is-warning.is-inverted.is-outlined.is-focused{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted.is-outlined.is-loading:hover::after,.button.is-warning.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-warning.is-inverted.is-outlined.is-loading:focus::after,.button.is-warning.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #ffdd57 #ffdd57 !important}.button.is-warning.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-warning.is-light{background-color:#fffbeb;color:#947600}.button.is-warning.is-light:hover,.button.is-warning.is-light.is-hovered{background-color:#fff8de;border-color:transparent;color:#947600}.button.is-warning.is-light:active,.button.is-warning.is-light.is-active{background-color:#fff6d1;border-color:transparent;color:#947600}.button.is-danger{background-color:#f14668;border-color:transparent;color:#fff}.button.is-danger:hover,.button.is-danger.is-hovered{background-color:#f03a5f;border-color:transparent;color:#fff}.button.is-danger:focus,.button.is-danger.is-focused{border-color:transparent;color:#fff}.button.is-danger:focus:not(:active),.button.is-danger.is-focused:not(:active){box-shadow:0 0 0 .125em rgba(241,70,104,.25)}.button.is-danger:active,.button.is-danger.is-active{background-color:#ef2e55;border-color:transparent;color:#fff}.button.is-danger[disabled],fieldset[disabled] .button.is-danger{background-color:#f14668;border-color:transparent;box-shadow:none}.button.is-danger.is-inverted{background-color:#fff;color:#f14668}.button.is-danger.is-inverted:hover,.button.is-danger.is-inverted.is-hovered{background-color:#f2f2f2}.button.is-danger.is-inverted[disabled],fieldset[disabled] .button.is-danger.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#f14668}.button.is-danger.is-loading::after{border-color:transparent transparent #fff #fff !important}.button.is-danger.is-outlined{background-color:transparent;border-color:#f14668;color:#f14668}.button.is-danger.is-outlined:hover,.button.is-danger.is-outlined.is-hovered,.button.is-danger.is-outlined:focus,.button.is-danger.is-outlined.is-focused{background-color:#f14668;border-color:#f14668;color:#fff}.button.is-danger.is-outlined.is-loading::after{border-color:transparent transparent #f14668 #f14668 !important}.button.is-danger.is-outlined.is-loading:hover::after,.button.is-danger.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-outlined.is-loading:focus::after,.button.is-danger.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #fff #fff !important}.button.is-danger.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-outlined{background-color:transparent;border-color:#f14668;box-shadow:none;color:#f14668}.button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-danger.is-inverted.is-outlined:hover,.button.is-danger.is-inverted.is-outlined.is-hovered,.button.is-danger.is-inverted.is-outlined:focus,.button.is-danger.is-inverted.is-outlined.is-focused{background-color:#fff;color:#f14668}.button.is-danger.is-inverted.is-outlined.is-loading:hover::after,.button.is-danger.is-inverted.is-outlined.is-loading.is-hovered::after,.button.is-danger.is-inverted.is-outlined.is-loading:focus::after,.button.is-danger.is-inverted.is-outlined.is-loading.is-focused::after{border-color:transparent transparent #f14668 #f14668 !important}.button.is-danger.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-danger.is-light{background-color:#feecf0;color:#cc0f35}.button.is-danger.is-light:hover,.button.is-danger.is-light.is-hovered{background-color:#fde0e6;border-color:transparent;color:#cc0f35}.button.is-danger.is-light:active,.button.is-danger.is-light.is-active{background-color:#fcd4dc;border-color:transparent;color:#cc0f35}.button.is-small{border-radius:2px;font-size:.75rem}.button.is-normal{font-size:1rem}.button.is-medium{font-size:1.25rem}.button.is-large{font-size:1.5rem}.button[disabled],fieldset[disabled] .button{background-color:#fff;border-color:#dbdbdb;box-shadow:none;opacity:.5}.button.is-fullwidth{display:flex;width:100%}.button.is-loading{color:transparent !important;pointer-events:none}.button.is-loading::after{position:absolute;left:calc(50% - (1em / 2));top:calc(50% - (1em / 2));position:absolute !important}.button.is-static{background-color:#f5f5f5;border-color:#dbdbdb;color:#7a7a7a;box-shadow:none;pointer-events:none}.button.is-rounded{border-radius:290486px;padding-left:calc(1em + 0.25em);padding-right:calc(1em + 0.25em)}.buttons{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.buttons .button{margin-bottom:.5rem}.buttons .button:not(:last-child):not(.is-fullwidth){margin-right:.5rem}.buttons:last-child{margin-bottom:-0.5rem}.buttons:not(:last-child){margin-bottom:1rem}.buttons.are-small .button:not(.is-normal):not(.is-medium):not(.is-large){border-radius:2px;font-size:.75rem}.buttons.are-medium .button:not(.is-small):not(.is-normal):not(.is-large){font-size:1.25rem}.buttons.are-large .button:not(.is-small):not(.is-normal):not(.is-medium){font-size:1.5rem}.buttons.has-addons .button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.buttons.has-addons .button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.buttons.has-addons .button:last-child{margin-right:0}.buttons.has-addons .button:hover,.buttons.has-addons .button.is-hovered{z-index:2}.buttons.has-addons .button:focus,.buttons.has-addons .button.is-focused,.buttons.has-addons .button:active,.buttons.has-addons .button.is-active,.buttons.has-addons .button.is-selected{z-index:3}.buttons.has-addons .button:focus:hover,.buttons.has-addons .button.is-focused:hover,.buttons.has-addons .button:active:hover,.buttons.has-addons .button.is-active:hover,.buttons.has-addons .button.is-selected:hover{z-index:4}.buttons.has-addons .button.is-expanded{flex-grow:1;flex-shrink:1}.buttons.is-centered{justify-content:center}.buttons.is-centered:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.buttons.is-right{justify-content:flex-end}.buttons.is-right:not(.has-addons) .button:not(.is-fullwidth){margin-left:.25rem;margin-right:.25rem}.container{flex-grow:1;margin:0 auto;position:relative;width:auto}.container.is-fluid{max-width:none;padding-left:32px;padding-right:32px;width:100%}@media screen and (min-width: 1024px){.container{max-width:960px}}@media screen and (max-width: 1215px){.container.is-widescreen{max-width:1152px}}@media screen and (max-width: 1407px){.container.is-fullhd{max-width:1344px}}@media screen and (min-width: 1216px){.container{max-width:1152px}}@media screen and (min-width: 1408px){.container{max-width:1344px}}.content li+li{margin-top:.25em}.content p:not(:last-child),.content dl:not(:last-child),.content ol:not(:last-child),.content ul:not(:last-child),.content blockquote:not(:last-child),.content pre:not(:last-child),.content table:not(:last-child){margin-bottom:1em}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{color:#363636;font-weight:600;line-height:1.125}.content h1{font-size:2em;margin-bottom:.5em}.content h1:not(:first-child){margin-top:1em}.content h2{font-size:1.75em;margin-bottom:.5714em}.content h2:not(:first-child){margin-top:1.1428em}.content h3{font-size:1.5em;margin-bottom:.6666em}.content h3:not(:first-child){margin-top:1.3333em}.content h4{font-size:1.25em;margin-bottom:.8em}.content h5{font-size:1.125em;margin-bottom:.8888em}.content h6{font-size:1em;margin-bottom:1em}.content blockquote{background-color:#f5f5f5;border-left:5px solid #dbdbdb;padding:1.25em 1.5em}.content ol{list-style-position:outside;margin-left:2em;margin-top:1em}.content ol:not([type]){list-style-type:decimal}.content ol:not([type]).is-lower-alpha{list-style-type:lower-alpha}.content ol:not([type]).is-lower-roman{list-style-type:lower-roman}.content ol:not([type]).is-upper-alpha{list-style-type:upper-alpha}.content ol:not([type]).is-upper-roman{list-style-type:upper-roman}.content ul{list-style:disc outside;margin-left:2em;margin-top:1em}.content ul ul{list-style-type:circle;margin-top:.5em}.content ul ul ul{list-style-type:square}.content dd{margin-left:2em}.content figure{margin-left:2em;margin-right:2em;text-align:center}.content figure:not(:first-child){margin-top:2em}.content figure:not(:last-child){margin-bottom:2em}.content figure img{display:inline-block}.content figure figcaption{font-style:italic}.content pre{-webkit-overflow-scrolling:touch;overflow-x:auto;padding:1.25em 1.5em;white-space:pre;word-wrap:normal}.content sup,.content sub{font-size:75%}.content table{width:100%}.content table td,.content table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.content table th{color:#363636}.content table th:not([align]){text-align:inherit}.content table thead td,.content table thead th{border-width:0 0 2px;color:#363636}.content table tfoot td,.content table tfoot th{border-width:2px 0 0;color:#363636}.content table tbody tr:last-child td,.content table tbody tr:last-child th{border-bottom-width:0}.content .tabs li+li{margin-top:0}.content.is-small{font-size:.75rem}.content.is-medium{font-size:1.25rem}.content.is-large{font-size:1.5rem}.icon{align-items:center;display:inline-flex;justify-content:center;height:1.5rem;width:1.5rem}.icon.is-small{height:1rem;width:1rem}.icon.is-medium{height:2rem;width:2rem}.icon.is-large{height:3rem;width:3rem}.image{display:block;position:relative}.image img{display:block;height:auto;width:100%}.image img.is-rounded{border-radius:290486px}.image.is-fullwidth{width:100%}.image.is-square img,.image.is-square .has-ratio,.image.is-1by1 img,.image.is-1by1 .has-ratio,.image.is-5by4 img,.image.is-5by4 .has-ratio,.image.is-4by3 img,.image.is-4by3 .has-ratio,.image.is-3by2 img,.image.is-3by2 .has-ratio,.image.is-5by3 img,.image.is-5by3 .has-ratio,.image.is-16by9 img,.image.is-16by9 .has-ratio,.image.is-2by1 img,.image.is-2by1 .has-ratio,.image.is-3by1 img,.image.is-3by1 .has-ratio,.image.is-4by5 img,.image.is-4by5 .has-ratio,.image.is-3by4 img,.image.is-3by4 .has-ratio,.image.is-2by3 img,.image.is-2by3 .has-ratio,.image.is-3by5 img,.image.is-3by5 .has-ratio,.image.is-9by16 img,.image.is-9by16 .has-ratio,.image.is-1by2 img,.image.is-1by2 .has-ratio,.image.is-1by3 img,.image.is-1by3 .has-ratio{height:100%;width:100%}.image.is-square,.image.is-1by1{padding-top:100%}.image.is-5by4{padding-top:80%}.image.is-4by3{padding-top:75%}.image.is-3by2{padding-top:66.6666%}.image.is-5by3{padding-top:60%}.image.is-16by9{padding-top:56.25%}.image.is-2by1{padding-top:50%}.image.is-3by1{padding-top:33.3333%}.image.is-4by5{padding-top:125%}.image.is-3by4{padding-top:133.3333%}.image.is-2by3{padding-top:150%}.image.is-3by5{padding-top:166.6666%}.image.is-9by16{padding-top:177.7777%}.image.is-1by2{padding-top:200%}.image.is-1by3{padding-top:300%}.image.is-16x16{height:16px;width:16px}.image.is-24x24{height:24px;width:24px}.image.is-32x32{height:32px;width:32px}.image.is-48x48{height:48px;width:48px}.image.is-64x64{height:64px;width:64px}.image.is-96x96{height:96px;width:96px}.image.is-128x128{height:128px;width:128px}.notification{background-color:#f5f5f5;border-radius:4px;position:relative;padding:1.25rem 2.5rem 1.25rem 1.5rem}.notification a:not(.button):not(.dropdown-item){color:currentColor;text-decoration:underline}.notification strong{color:currentColor}.notification code,.notification pre{background:#fff}.notification pre code{background:transparent}.notification>.delete{right:.5rem;position:absolute;top:.5rem}.notification .title,.notification .subtitle,.notification .content{color:currentColor}.notification.is-white{background-color:#fff;color:#0a0a0a}.notification.is-black{background-color:#0a0a0a;color:#fff}.notification.is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.notification.is-dark{background-color:#363636;color:#fff}.notification.is-primary{background-color:#00d1b2;color:#fff}.notification.is-primary.is-light{background-color:#ebfffc;color:#00947e}.notification.is-link{background-color:#3273dc;color:#fff}.notification.is-link.is-light{background-color:#eef3fc;color:#2160c4}.notification.is-info{background-color:#3298dc;color:#fff}.notification.is-info.is-light{background-color:#eef6fc;color:#1d72aa}.notification.is-success{background-color:#48c774;color:#fff}.notification.is-success.is-light{background-color:#effaf3;color:#257942}.notification.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.notification.is-warning.is-light{background-color:#fffbeb;color:#947600}.notification.is-danger{background-color:#f14668;color:#fff}.notification.is-danger.is-light{background-color:#feecf0;color:#cc0f35}.progress{-moz-appearance:none;-webkit-appearance:none;border:none;border-radius:290486px;display:block;height:1rem;overflow:hidden;padding:0;width:100%}.progress::-webkit-progress-bar{background-color:#ededed}.progress::-webkit-progress-value{background-color:#000}.progress::-moz-progress-bar{background-color:#000}.progress::-ms-fill{background-color:#000;border:none}.progress.is-white::-webkit-progress-value{background-color:#fff}.progress.is-white::-moz-progress-bar{background-color:#fff}.progress.is-white::-ms-fill{background-color:#fff}.progress.is-white:indeterminate{background-image:linear-gradient(to right, white 30%, #ededed 30%)}.progress.is-black::-webkit-progress-value{background-color:#0a0a0a}.progress.is-black::-moz-progress-bar{background-color:#0a0a0a}.progress.is-black::-ms-fill{background-color:#0a0a0a}.progress.is-black:indeterminate{background-image:linear-gradient(to right, #0a0a0a 30%, #ededed 30%)}.progress.is-light::-webkit-progress-value{background-color:#f5f5f5}.progress.is-light::-moz-progress-bar{background-color:#f5f5f5}.progress.is-light::-ms-fill{background-color:#f5f5f5}.progress.is-light:indeterminate{background-image:linear-gradient(to right, whitesmoke 30%, #ededed 30%)}.progress.is-dark::-webkit-progress-value{background-color:#363636}.progress.is-dark::-moz-progress-bar{background-color:#363636}.progress.is-dark::-ms-fill{background-color:#363636}.progress.is-dark:indeterminate{background-image:linear-gradient(to right, #363636 30%, #ededed 30%)}.progress.is-primary::-webkit-progress-value{background-color:#00d1b2}.progress.is-primary::-moz-progress-bar{background-color:#00d1b2}.progress.is-primary::-ms-fill{background-color:#00d1b2}.progress.is-primary:indeterminate{background-image:linear-gradient(to right, #00d1b2 30%, #ededed 30%)}.progress.is-link::-webkit-progress-value{background-color:#3273dc}.progress.is-link::-moz-progress-bar{background-color:#3273dc}.progress.is-link::-ms-fill{background-color:#3273dc}.progress.is-link:indeterminate{background-image:linear-gradient(to right, #3273dc 30%, #ededed 30%)}.progress.is-info::-webkit-progress-value{background-color:#3298dc}.progress.is-info::-moz-progress-bar{background-color:#3298dc}.progress.is-info::-ms-fill{background-color:#3298dc}.progress.is-info:indeterminate{background-image:linear-gradient(to right, #3298dc 30%, #ededed 30%)}.progress.is-success::-webkit-progress-value{background-color:#48c774}.progress.is-success::-moz-progress-bar{background-color:#48c774}.progress.is-success::-ms-fill{background-color:#48c774}.progress.is-success:indeterminate{background-image:linear-gradient(to right, #48c774 30%, #ededed 30%)}.progress.is-warning::-webkit-progress-value{background-color:#ffdd57}.progress.is-warning::-moz-progress-bar{background-color:#ffdd57}.progress.is-warning::-ms-fill{background-color:#ffdd57}.progress.is-warning:indeterminate{background-image:linear-gradient(to right, #ffdd57 30%, #ededed 30%)}.progress.is-danger::-webkit-progress-value{background-color:#f14668}.progress.is-danger::-moz-progress-bar{background-color:#f14668}.progress.is-danger::-ms-fill{background-color:#f14668}.progress.is-danger:indeterminate{background-image:linear-gradient(to right, #f14668 30%, #ededed 30%)}.progress:indeterminate{animation-duration:1.5s;animation-iteration-count:infinite;animation-name:moveIndeterminate;animation-timing-function:linear;background-color:#ededed;background-image:linear-gradient(to right, black 30%, #ededed 30%);background-position:top left;background-repeat:no-repeat;background-size:150% 150%}.progress:indeterminate::-webkit-progress-bar{background-color:transparent}.progress:indeterminate::-moz-progress-bar{background-color:transparent}.progress.is-small{height:.75rem}.progress.is-medium{height:1.25rem}.progress.is-large{height:1.5rem}@keyframes moveIndeterminate{from{background-position:200% 0}to{background-position:-200% 0}}.table{background-color:#fff;color:#363636}.table td,.table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.table td.is-white,.table th.is-white{background-color:#fff;border-color:#fff;color:#0a0a0a}.table td.is-black,.table th.is-black{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.table td.is-light,.table th.is-light{background-color:#f5f5f5;border-color:#f5f5f5;color:rgba(0,0,0,.7)}.table td.is-dark,.table th.is-dark{background-color:#363636;border-color:#363636;color:#fff}.table td.is-primary,.table th.is-primary{background-color:#00d1b2;border-color:#00d1b2;color:#fff}.table td.is-link,.table th.is-link{background-color:#3273dc;border-color:#3273dc;color:#fff}.table td.is-info,.table th.is-info{background-color:#3298dc;border-color:#3298dc;color:#fff}.table td.is-success,.table th.is-success{background-color:#48c774;border-color:#48c774;color:#fff}.table td.is-warning,.table th.is-warning{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.table td.is-danger,.table th.is-danger{background-color:#f14668;border-color:#f14668;color:#fff}.table td.is-narrow,.table th.is-narrow{white-space:nowrap;width:1%}.table td.is-selected,.table th.is-selected{background-color:#00d1b2;color:#fff}.table td.is-selected a,.table td.is-selected strong,.table th.is-selected a,.table th.is-selected strong{color:currentColor}.table td.is-vcentered,.table th.is-vcentered{vertical-align:middle}.table th{color:#363636}.table th:not([align]){text-align:inherit}.table tr.is-selected{background-color:#00d1b2;color:#fff}.table tr.is-selected a,.table tr.is-selected strong{color:currentColor}.table tr.is-selected td,.table tr.is-selected th{border-color:#fff;color:currentColor}.table thead{background-color:transparent}.table thead td,.table thead th{border-width:0 0 2px;color:#363636}.table tfoot{background-color:transparent}.table tfoot td,.table tfoot th{border-width:2px 0 0;color:#363636}.table tbody{background-color:transparent}.table tbody tr:last-child td,.table tbody tr:last-child th{border-bottom-width:0}.table.is-bordered td,.table.is-bordered th{border-width:1px}.table.is-bordered tr:last-child td,.table.is-bordered tr:last-child th{border-bottom-width:1px}.table.is-fullwidth{width:100%}.table.is-hoverable tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover{background-color:#fafafa}.table.is-hoverable.is-striped tbody tr:not(.is-selected):hover:nth-child(even){background-color:#f5f5f5}.table.is-narrow td,.table.is-narrow th{padding:.25em .5em}.table.is-striped tbody tr:not(.is-selected):nth-child(even){background-color:#fafafa}.table-container{-webkit-overflow-scrolling:touch;overflow:auto;overflow-y:hidden;max-width:100%}.tags{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.tags .tag{margin-bottom:.5rem}.tags .tag:not(:last-child){margin-right:.5rem}.tags:last-child{margin-bottom:-0.5rem}.tags:not(:last-child){margin-bottom:1rem}.tags.are-medium .tag:not(.is-normal):not(.is-large){font-size:1rem}.tags.are-large .tag:not(.is-normal):not(.is-medium){font-size:1.25rem}.tags.is-centered{justify-content:center}.tags.is-centered .tag{margin-right:.25rem;margin-left:.25rem}.tags.is-right{justify-content:flex-end}.tags.is-right .tag:not(:first-child){margin-left:.5rem}.tags.is-right .tag:not(:last-child){margin-right:0}.tags.has-addons .tag{margin-right:0}.tags.has-addons .tag:not(:first-child){margin-left:0;border-top-left-radius:0;border-bottom-left-radius:0}.tags.has-addons .tag:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.tag:not(body){align-items:center;background-color:#f5f5f5;border-radius:4px;color:#000;display:inline-flex;font-size:.75rem;height:2em;justify-content:center;line-height:1.5;padding-left:.75em;padding-right:.75em;white-space:nowrap}.tag:not(body) .delete{margin-left:.25rem;margin-right:-0.375rem}.tag:not(body).is-white{background-color:#fff;color:#0a0a0a}.tag:not(body).is-black{background-color:#0a0a0a;color:#fff}.tag:not(body).is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.tag:not(body).is-dark{background-color:#363636;color:#fff}.tag:not(body).is-primary{background-color:#00d1b2;color:#fff}.tag:not(body).is-primary.is-light{background-color:#ebfffc;color:#00947e}.tag:not(body).is-link{background-color:#3273dc;color:#fff}.tag:not(body).is-link.is-light{background-color:#eef3fc;color:#2160c4}.tag:not(body).is-info{background-color:#3298dc;color:#fff}.tag:not(body).is-info.is-light{background-color:#eef6fc;color:#1d72aa}.tag:not(body).is-success{background-color:#48c774;color:#fff}.tag:not(body).is-success.is-light{background-color:#effaf3;color:#257942}.tag:not(body).is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.tag:not(body).is-warning.is-light{background-color:#fffbeb;color:#947600}.tag:not(body).is-danger{background-color:#f14668;color:#fff}.tag:not(body).is-danger.is-light{background-color:#feecf0;color:#cc0f35}.tag:not(body).is-normal{font-size:.75rem}.tag:not(body).is-medium{font-size:1rem}.tag:not(body).is-large{font-size:1.25rem}.tag:not(body) .icon:first-child:not(:last-child){margin-left:-0.375em;margin-right:.1875em}.tag:not(body) .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:-0.375em}.tag:not(body) .icon:first-child:last-child{margin-left:-0.375em;margin-right:-0.375em}.tag:not(body).is-delete{margin-left:1px;padding:0;position:relative;width:2em}.tag:not(body).is-delete::before,.tag:not(body).is-delete::after{background-color:currentColor;content:"";display:block;left:50%;position:absolute;top:50%;transform:translateX(-50%) translateY(-50%) rotate(45deg);transform-origin:center center}.tag:not(body).is-delete::before{height:1px;width:50%}.tag:not(body).is-delete::after{height:50%;width:1px}.tag:not(body).is-delete:hover,.tag:not(body).is-delete:focus{background-color:#e8e8e8}.tag:not(body).is-delete:active{background-color:#dbdbdb}.tag:not(body).is-rounded{border-radius:290486px}a.tag:hover{text-decoration:underline}.title,.subtitle{word-break:break-word}.title em,.title span,.subtitle em,.subtitle span{font-weight:inherit}.title sub,.subtitle sub{font-size:.75em}.title sup,.subtitle sup{font-size:.75em}.title .tag,.subtitle .tag{vertical-align:middle}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.title+.highlight{margin-top:-0.75rem}.title:not(.is-spaced)+.subtitle{margin-top:-1.25rem}.title.is-1{font-size:3rem}.title.is-2{font-size:2.5rem}.title.is-3{font-size:2rem}.title.is-4{font-size:1.5rem}.title.is-5{font-size:1.25rem}.title.is-6{font-size:1rem}.title.is-7{font-size:.75rem}.subtitle{color:#000;font-size:1.25rem;font-weight:400;line-height:1.25}.subtitle strong{color:#363636;font-weight:600}.subtitle:not(.is-spaced)+.title{margin-top:-1.25rem}.subtitle.is-1{font-size:3rem}.subtitle.is-2{font-size:2.5rem}.subtitle.is-3{font-size:2rem}.subtitle.is-4{font-size:1.5rem}.subtitle.is-5{font-size:1.25rem}.subtitle.is-6{font-size:1rem}.subtitle.is-7{font-size:.75rem}.heading{display:block;font-size:11px;letter-spacing:1px;margin-bottom:5px;text-transform:uppercase}.highlight{font-weight:400;max-width:100%;overflow:hidden;padding:0}.highlight pre{overflow:auto;max-width:100%}.number{align-items:center;background-color:#f5f5f5;border-radius:290486px;display:inline-flex;font-size:1.25rem;height:2em;justify-content:center;margin-right:1.5rem;min-width:2.5em;padding:.25rem .5rem;text-align:center;vertical-align:top}.select select,.textarea,.input{background-color:#fff;border-color:#dbdbdb;border-radius:4px;color:#363636}.select select::-moz-placeholder,.textarea::-moz-placeholder,.input::-moz-placeholder{color:rgba(54,54,54,.3)}.select select::-webkit-input-placeholder,.textarea::-webkit-input-placeholder,.input::-webkit-input-placeholder{color:rgba(54,54,54,.3)}.select select:-moz-placeholder,.textarea:-moz-placeholder,.input:-moz-placeholder{color:rgba(54,54,54,.3)}.select select:-ms-input-placeholder,.textarea:-ms-input-placeholder,.input:-ms-input-placeholder{color:rgba(54,54,54,.3)}.select select:hover,.textarea:hover,.input:hover,.select select.is-hovered,.is-hovered.textarea,.is-hovered.input{border-color:#b5b5b5}.select select:focus,.textarea:focus,.input:focus,.select select.is-focused,.is-focused.textarea,.is-focused.input,.select select:active,.textarea:active,.input:active,.select select.is-active,.is-active.textarea,.is-active.input{border-color:#3273dc;box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.select select[disabled],[disabled].textarea,[disabled].input,fieldset[disabled] .select select,.select fieldset[disabled] select,fieldset[disabled] .textarea,fieldset[disabled] .input{background-color:#f5f5f5;border-color:#f5f5f5;box-shadow:none;color:#7a7a7a}.select select[disabled]::-moz-placeholder,[disabled].textarea::-moz-placeholder,[disabled].input::-moz-placeholder,fieldset[disabled] .select select::-moz-placeholder,.select fieldset[disabled] select::-moz-placeholder,fieldset[disabled] .textarea::-moz-placeholder,fieldset[disabled] .input::-moz-placeholder{color:rgba(122,122,122,.3)}.select select[disabled]::-webkit-input-placeholder,[disabled].textarea::-webkit-input-placeholder,[disabled].input::-webkit-input-placeholder,fieldset[disabled] .select select::-webkit-input-placeholder,.select fieldset[disabled] select::-webkit-input-placeholder,fieldset[disabled] .textarea::-webkit-input-placeholder,fieldset[disabled] .input::-webkit-input-placeholder{color:rgba(122,122,122,.3)}.select select[disabled]:-moz-placeholder,[disabled].textarea:-moz-placeholder,[disabled].input:-moz-placeholder,fieldset[disabled] .select select:-moz-placeholder,.select fieldset[disabled] select:-moz-placeholder,fieldset[disabled] .textarea:-moz-placeholder,fieldset[disabled] .input:-moz-placeholder{color:rgba(122,122,122,.3)}.select select[disabled]:-ms-input-placeholder,[disabled].textarea:-ms-input-placeholder,[disabled].input:-ms-input-placeholder,fieldset[disabled] .select select:-ms-input-placeholder,.select fieldset[disabled] select:-ms-input-placeholder,fieldset[disabled] .textarea:-ms-input-placeholder,fieldset[disabled] .input:-ms-input-placeholder{color:rgba(122,122,122,.3)}.textarea,.input{box-shadow:inset 0 .0625em .125em rgba(10,10,10,.05);max-width:100%;width:100%}[readonly].textarea,[readonly].input{box-shadow:none}.is-white.textarea,.is-white.input{border-color:#fff}.is-white.textarea:focus,.is-white.input:focus,.is-white.is-focused.textarea,.is-white.is-focused.input,.is-white.textarea:active,.is-white.input:active,.is-white.is-active.textarea,.is-white.is-active.input{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.is-black.textarea,.is-black.input{border-color:#0a0a0a}.is-black.textarea:focus,.is-black.input:focus,.is-black.is-focused.textarea,.is-black.is-focused.input,.is-black.textarea:active,.is-black.input:active,.is-black.is-active.textarea,.is-black.is-active.input{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.is-light.textarea,.is-light.input{border-color:#f5f5f5}.is-light.textarea:focus,.is-light.input:focus,.is-light.is-focused.textarea,.is-light.is-focused.input,.is-light.textarea:active,.is-light.input:active,.is-light.is-active.textarea,.is-light.is-active.input{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.is-dark.textarea,.is-dark.input{border-color:#363636}.is-dark.textarea:focus,.is-dark.input:focus,.is-dark.is-focused.textarea,.is-dark.is-focused.input,.is-dark.textarea:active,.is-dark.input:active,.is-dark.is-active.textarea,.is-dark.is-active.input{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.is-primary.textarea,.is-primary.input{border-color:#00d1b2}.is-primary.textarea:focus,.is-primary.input:focus,.is-primary.is-focused.textarea,.is-primary.is-focused.input,.is-primary.textarea:active,.is-primary.input:active,.is-primary.is-active.textarea,.is-primary.is-active.input{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.is-link.textarea,.is-link.input{border-color:#3273dc}.is-link.textarea:focus,.is-link.input:focus,.is-link.is-focused.textarea,.is-link.is-focused.input,.is-link.textarea:active,.is-link.input:active,.is-link.is-active.textarea,.is-link.is-active.input{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.is-info.textarea,.is-info.input{border-color:#3298dc}.is-info.textarea:focus,.is-info.input:focus,.is-info.is-focused.textarea,.is-info.is-focused.input,.is-info.textarea:active,.is-info.input:active,.is-info.is-active.textarea,.is-info.is-active.input{box-shadow:0 0 0 .125em rgba(50,152,220,.25)}.is-success.textarea,.is-success.input{border-color:#48c774}.is-success.textarea:focus,.is-success.input:focus,.is-success.is-focused.textarea,.is-success.is-focused.input,.is-success.textarea:active,.is-success.input:active,.is-success.is-active.textarea,.is-success.is-active.input{box-shadow:0 0 0 .125em rgba(72,199,116,.25)}.is-warning.textarea,.is-warning.input{border-color:#ffdd57}.is-warning.textarea:focus,.is-warning.input:focus,.is-warning.is-focused.textarea,.is-warning.is-focused.input,.is-warning.textarea:active,.is-warning.input:active,.is-warning.is-active.textarea,.is-warning.is-active.input{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.is-danger.textarea,.is-danger.input{border-color:#f14668}.is-danger.textarea:focus,.is-danger.input:focus,.is-danger.is-focused.textarea,.is-danger.is-focused.input,.is-danger.textarea:active,.is-danger.input:active,.is-danger.is-active.textarea,.is-danger.is-active.input{box-shadow:0 0 0 .125em rgba(241,70,104,.25)}.is-small.textarea,.is-small.input{border-radius:2px;font-size:.75rem}.is-medium.textarea,.is-medium.input{font-size:1.25rem}.is-large.textarea,.is-large.input{font-size:1.5rem}.is-fullwidth.textarea,.is-fullwidth.input{display:block;width:100%}.is-inline.textarea,.is-inline.input{display:inline;width:auto}.input.is-rounded{border-radius:290486px;padding-left:calc(calc(0.75em - 1px) + 0.375em);padding-right:calc(calc(0.75em - 1px) + 0.375em)}.input.is-static{background-color:transparent;border-color:transparent;box-shadow:none;padding-left:0;padding-right:0}.textarea{display:block;max-width:100%;min-width:100%;padding:calc(0.75em - 1px);resize:vertical}.textarea:not([rows]){max-height:40em;min-height:8em}.textarea[rows]{height:initial}.textarea.has-fixed-size{resize:none}.radio,.checkbox{cursor:pointer;display:inline-block;line-height:1.25;position:relative}.radio input,.checkbox input{cursor:pointer}.radio:hover,.checkbox:hover{color:#363636}[disabled].radio,[disabled].checkbox,fieldset[disabled] .radio,fieldset[disabled] .checkbox{color:#7a7a7a;cursor:not-allowed}.radio+.radio{margin-left:.5em}.select{display:inline-block;max-width:100%;position:relative;vertical-align:top}.select:not(.is-multiple){height:2.5em}.select:not(.is-multiple):not(.is-loading)::after{border-color:#3273dc;right:1.125em;z-index:4}.select.is-rounded select{border-radius:290486px;padding-left:1em}.select select{cursor:pointer;display:block;font-size:1em;max-width:100%;outline:none}.select select::-ms-expand{display:none}.select select[disabled]:hover,fieldset[disabled] .select select:hover{border-color:#f5f5f5}.select select:not([multiple]){padding-right:2.5em}.select select[multiple]{height:auto;padding:0}.select select[multiple] option{padding:.5em 1em}.select:not(.is-multiple):not(.is-loading):hover::after{border-color:#363636}.select.is-white:not(:hover)::after{border-color:#fff}.select.is-white select{border-color:#fff}.select.is-white select:hover,.select.is-white select.is-hovered{border-color:#f2f2f2}.select.is-white select:focus,.select.is-white select.is-focused,.select.is-white select:active,.select.is-white select.is-active{box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.select.is-black:not(:hover)::after{border-color:#0a0a0a}.select.is-black select{border-color:#0a0a0a}.select.is-black select:hover,.select.is-black select.is-hovered{border-color:#000}.select.is-black select:focus,.select.is-black select.is-focused,.select.is-black select:active,.select.is-black select.is-active{box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.select.is-light:not(:hover)::after{border-color:#f5f5f5}.select.is-light select{border-color:#f5f5f5}.select.is-light select:hover,.select.is-light select.is-hovered{border-color:#e8e8e8}.select.is-light select:focus,.select.is-light select.is-focused,.select.is-light select:active,.select.is-light select.is-active{box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.select.is-dark:not(:hover)::after{border-color:#363636}.select.is-dark select{border-color:#363636}.select.is-dark select:hover,.select.is-dark select.is-hovered{border-color:#292929}.select.is-dark select:focus,.select.is-dark select.is-focused,.select.is-dark select:active,.select.is-dark select.is-active{box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.select.is-primary:not(:hover)::after{border-color:#00d1b2}.select.is-primary select{border-color:#00d1b2}.select.is-primary select:hover,.select.is-primary select.is-hovered{border-color:#00b89c}.select.is-primary select:focus,.select.is-primary select.is-focused,.select.is-primary select:active,.select.is-primary select.is-active{box-shadow:0 0 0 .125em rgba(0,209,178,.25)}.select.is-link:not(:hover)::after{border-color:#3273dc}.select.is-link select{border-color:#3273dc}.select.is-link select:hover,.select.is-link select.is-hovered{border-color:#2366d1}.select.is-link select:focus,.select.is-link select.is-focused,.select.is-link select:active,.select.is-link select.is-active{box-shadow:0 0 0 .125em rgba(50,115,220,.25)}.select.is-info:not(:hover)::after{border-color:#3298dc}.select.is-info select{border-color:#3298dc}.select.is-info select:hover,.select.is-info select.is-hovered{border-color:#238cd1}.select.is-info select:focus,.select.is-info select.is-focused,.select.is-info select:active,.select.is-info select.is-active{box-shadow:0 0 0 .125em rgba(50,152,220,.25)}.select.is-success:not(:hover)::after{border-color:#48c774}.select.is-success select{border-color:#48c774}.select.is-success select:hover,.select.is-success select.is-hovered{border-color:#3abb67}.select.is-success select:focus,.select.is-success select.is-focused,.select.is-success select:active,.select.is-success select.is-active{box-shadow:0 0 0 .125em rgba(72,199,116,.25)}.select.is-warning:not(:hover)::after{border-color:#ffdd57}.select.is-warning select{border-color:#ffdd57}.select.is-warning select:hover,.select.is-warning select.is-hovered{border-color:#ffd83d}.select.is-warning select:focus,.select.is-warning select.is-focused,.select.is-warning select:active,.select.is-warning select.is-active{box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.select.is-danger:not(:hover)::after{border-color:#f14668}.select.is-danger select{border-color:#f14668}.select.is-danger select:hover,.select.is-danger select.is-hovered{border-color:#ef2e55}.select.is-danger select:focus,.select.is-danger select.is-focused,.select.is-danger select:active,.select.is-danger select.is-active{box-shadow:0 0 0 .125em rgba(241,70,104,.25)}.select.is-small{border-radius:2px;font-size:.75rem}.select.is-medium{font-size:1.25rem}.select.is-large{font-size:1.5rem}.select.is-disabled::after{border-color:#7a7a7a}.select.is-fullwidth{width:100%}.select.is-fullwidth select{width:100%}.select.is-loading::after{margin-top:0;position:absolute;right:.625em;top:.625em;transform:none}.select.is-loading.is-small:after{font-size:.75rem}.select.is-loading.is-medium:after{font-size:1.25rem}.select.is-loading.is-large:after{font-size:1.5rem}.file{align-items:stretch;display:flex;justify-content:flex-start;position:relative}.file.is-white .file-cta{background-color:#fff;border-color:transparent;color:#0a0a0a}.file.is-white:hover .file-cta,.file.is-white.is-hovered .file-cta{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.file.is-white:focus .file-cta,.file.is-white.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,255,255,.25);color:#0a0a0a}.file.is-white:active .file-cta,.file.is-white.is-active .file-cta{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.file.is-black .file-cta{background-color:#0a0a0a;border-color:transparent;color:#fff}.file.is-black:hover .file-cta,.file.is-black.is-hovered .file-cta{background-color:#040404;border-color:transparent;color:#fff}.file.is-black:focus .file-cta,.file.is-black.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(10,10,10,.25);color:#fff}.file.is-black:active .file-cta,.file.is-black.is-active .file-cta{background-color:#000;border-color:transparent;color:#fff}.file.is-light .file-cta{background-color:#f5f5f5;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-light:hover .file-cta,.file.is-light.is-hovered .file-cta{background-color:#eee;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-light:focus .file-cta,.file.is-light.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(245,245,245,.25);color:rgba(0,0,0,.7)}.file.is-light:active .file-cta,.file.is-light.is-active .file-cta{background-color:#e8e8e8;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-dark .file-cta{background-color:#363636;border-color:transparent;color:#fff}.file.is-dark:hover .file-cta,.file.is-dark.is-hovered .file-cta{background-color:#2f2f2f;border-color:transparent;color:#fff}.file.is-dark:focus .file-cta,.file.is-dark.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(54,54,54,.25);color:#fff}.file.is-dark:active .file-cta,.file.is-dark.is-active .file-cta{background-color:#292929;border-color:transparent;color:#fff}.file.is-primary .file-cta{background-color:#00d1b2;border-color:transparent;color:#fff}.file.is-primary:hover .file-cta,.file.is-primary.is-hovered .file-cta{background-color:#00c4a7;border-color:transparent;color:#fff}.file.is-primary:focus .file-cta,.file.is-primary.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(0,209,178,.25);color:#fff}.file.is-primary:active .file-cta,.file.is-primary.is-active .file-cta{background-color:#00b89c;border-color:transparent;color:#fff}.file.is-link .file-cta{background-color:#3273dc;border-color:transparent;color:#fff}.file.is-link:hover .file-cta,.file.is-link.is-hovered .file-cta{background-color:#276cda;border-color:transparent;color:#fff}.file.is-link:focus .file-cta,.file.is-link.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(50,115,220,.25);color:#fff}.file.is-link:active .file-cta,.file.is-link.is-active .file-cta{background-color:#2366d1;border-color:transparent;color:#fff}.file.is-info .file-cta{background-color:#3298dc;border-color:transparent;color:#fff}.file.is-info:hover .file-cta,.file.is-info.is-hovered .file-cta{background-color:#2793da;border-color:transparent;color:#fff}.file.is-info:focus .file-cta,.file.is-info.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(50,152,220,.25);color:#fff}.file.is-info:active .file-cta,.file.is-info.is-active .file-cta{background-color:#238cd1;border-color:transparent;color:#fff}.file.is-success .file-cta{background-color:#48c774;border-color:transparent;color:#fff}.file.is-success:hover .file-cta,.file.is-success.is-hovered .file-cta{background-color:#3ec46d;border-color:transparent;color:#fff}.file.is-success:focus .file-cta,.file.is-success.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(72,199,116,.25);color:#fff}.file.is-success:active .file-cta,.file.is-success.is-active .file-cta{background-color:#3abb67;border-color:transparent;color:#fff}.file.is-warning .file-cta{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning:hover .file-cta,.file.is-warning.is-hovered .file-cta{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-warning:focus .file-cta,.file.is-warning.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(255,221,87,.25);color:rgba(0,0,0,.7)}.file.is-warning:active .file-cta,.file.is-warning.is-active .file-cta{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.file.is-danger .file-cta{background-color:#f14668;border-color:transparent;color:#fff}.file.is-danger:hover .file-cta,.file.is-danger.is-hovered .file-cta{background-color:#f03a5f;border-color:transparent;color:#fff}.file.is-danger:focus .file-cta,.file.is-danger.is-focused .file-cta{border-color:transparent;box-shadow:0 0 .5em rgba(241,70,104,.25);color:#fff}.file.is-danger:active .file-cta,.file.is-danger.is-active .file-cta{background-color:#ef2e55;border-color:transparent;color:#fff}.file.is-small{font-size:.75rem}.file.is-medium{font-size:1.25rem}.file.is-medium .file-icon .fa{font-size:21px}.file.is-large{font-size:1.5rem}.file.is-large .file-icon .fa{font-size:28px}.file.has-name .file-cta{border-bottom-right-radius:0;border-top-right-radius:0}.file.has-name .file-name{border-bottom-left-radius:0;border-top-left-radius:0}.file.has-name.is-empty .file-cta{border-radius:4px}.file.has-name.is-empty .file-name{display:none}.file.is-boxed .file-label{flex-direction:column}.file.is-boxed .file-cta{flex-direction:column;height:auto;padding:1em 3em}.file.is-boxed .file-name{border-width:0 1px 1px}.file.is-boxed .file-icon{height:1.5em;width:1.5em}.file.is-boxed .file-icon .fa{font-size:21px}.file.is-boxed.is-small .file-icon .fa{font-size:14px}.file.is-boxed.is-medium .file-icon .fa{font-size:28px}.file.is-boxed.is-large .file-icon .fa{font-size:35px}.file.is-boxed.has-name .file-cta{border-radius:4px 4px 0 0}.file.is-boxed.has-name .file-name{border-radius:0 0 4px 4px;border-width:0 1px 1px}.file.is-centered{justify-content:center}.file.is-fullwidth .file-label{width:100%}.file.is-fullwidth .file-name{flex-grow:1;max-width:none}.file.is-right{justify-content:flex-end}.file.is-right .file-cta{border-radius:0 4px 4px 0}.file.is-right .file-name{border-radius:4px 0 0 4px;border-width:1px 0 1px 1px;order:-1}.file-label{align-items:stretch;display:flex;cursor:pointer;justify-content:flex-start;overflow:hidden;position:relative}.file-label:hover .file-cta{background-color:#eee;color:#363636}.file-label:hover .file-name{border-color:#d5d5d5}.file-label:active .file-cta{background-color:#e8e8e8;color:#363636}.file-label:active .file-name{border-color:#cfcfcf}.file-input{height:100%;left:0;opacity:0;outline:none;position:absolute;top:0;width:100%}.file-cta,.file-name{border-color:#dbdbdb;border-radius:4px;font-size:1em;padding-left:1em;padding-right:1em;white-space:nowrap}.file-cta{background-color:#f5f5f5;color:#000}.file-name{border-color:#dbdbdb;border-style:solid;border-width:1px 1px 1px 0;display:block;max-width:16em;overflow:hidden;text-align:inherit;text-overflow:ellipsis}.file-icon{align-items:center;display:flex;height:1em;justify-content:center;margin-right:.5em;width:1em}.file-icon .fa{font-size:14px}.label{color:#363636;display:block;font-size:1rem;font-weight:700}.label:not(:last-child){margin-bottom:.5em}.label.is-small{font-size:.75rem}.label.is-medium{font-size:1.25rem}.label.is-large{font-size:1.5rem}.help{display:block;font-size:.75rem;margin-top:.25rem}.help.is-white{color:#fff}.help.is-black{color:#0a0a0a}.help.is-light{color:#f5f5f5}.help.is-dark{color:#363636}.help.is-primary{color:#00d1b2}.help.is-link{color:#3273dc}.help.is-info{color:#3298dc}.help.is-success{color:#48c774}.help.is-warning{color:#ffdd57}.help.is-danger{color:#f14668}.field:not(:last-child){margin-bottom:.75rem}.field.has-addons{display:flex;justify-content:flex-start}.field.has-addons .control:not(:last-child){margin-right:-1px}.field.has-addons .control:not(:first-child):not(:last-child) .button,.field.has-addons .control:not(:first-child):not(:last-child) .input,.field.has-addons .control:not(:first-child):not(:last-child) .select select{border-radius:0}.field.has-addons .control:first-child:not(:only-child) .button,.field.has-addons .control:first-child:not(:only-child) .input,.field.has-addons .control:first-child:not(:only-child) .select select{border-bottom-right-radius:0;border-top-right-radius:0}.field.has-addons .control:last-child:not(:only-child) .button,.field.has-addons .control:last-child:not(:only-child) .input,.field.has-addons .control:last-child:not(:only-child) .select select{border-bottom-left-radius:0;border-top-left-radius:0}.field.has-addons .control .button:not([disabled]):hover,.field.has-addons .control .button:not([disabled]).is-hovered,.field.has-addons .control .input:not([disabled]):hover,.field.has-addons .control .input:not([disabled]).is-hovered,.field.has-addons .control .select select:not([disabled]):hover,.field.has-addons .control .select select:not([disabled]).is-hovered{z-index:2}.field.has-addons .control .button:not([disabled]):focus,.field.has-addons .control .button:not([disabled]).is-focused,.field.has-addons .control .button:not([disabled]):active,.field.has-addons .control .button:not([disabled]).is-active,.field.has-addons .control .input:not([disabled]):focus,.field.has-addons .control .input:not([disabled]).is-focused,.field.has-addons .control .input:not([disabled]):active,.field.has-addons .control .input:not([disabled]).is-active,.field.has-addons .control .select select:not([disabled]):focus,.field.has-addons .control .select select:not([disabled]).is-focused,.field.has-addons .control .select select:not([disabled]):active,.field.has-addons .control .select select:not([disabled]).is-active{z-index:3}.field.has-addons .control .button:not([disabled]):focus:hover,.field.has-addons .control .button:not([disabled]).is-focused:hover,.field.has-addons .control .button:not([disabled]):active:hover,.field.has-addons .control .button:not([disabled]).is-active:hover,.field.has-addons .control .input:not([disabled]):focus:hover,.field.has-addons .control .input:not([disabled]).is-focused:hover,.field.has-addons .control .input:not([disabled]):active:hover,.field.has-addons .control .input:not([disabled]).is-active:hover,.field.has-addons .control .select select:not([disabled]):focus:hover,.field.has-addons .control .select select:not([disabled]).is-focused:hover,.field.has-addons .control .select select:not([disabled]):active:hover,.field.has-addons .control .select select:not([disabled]).is-active:hover{z-index:4}.field.has-addons .control.is-expanded{flex-grow:1;flex-shrink:1}.field.has-addons.has-addons-centered{justify-content:center}.field.has-addons.has-addons-right{justify-content:flex-end}.field.has-addons.has-addons-fullwidth .control{flex-grow:1;flex-shrink:0}.field.is-grouped{display:flex;justify-content:flex-start}.field.is-grouped>.control{flex-shrink:0}.field.is-grouped>.control:not(:last-child){margin-bottom:0;margin-right:.75rem}.field.is-grouped>.control.is-expanded{flex-grow:1;flex-shrink:1}.field.is-grouped.is-grouped-centered{justify-content:center}.field.is-grouped.is-grouped-right{justify-content:flex-end}.field.is-grouped.is-grouped-multiline{flex-wrap:wrap}.field.is-grouped.is-grouped-multiline>.control:last-child,.field.is-grouped.is-grouped-multiline>.control:not(:last-child){margin-bottom:.75rem}.field.is-grouped.is-grouped-multiline:last-child{margin-bottom:-0.75rem}.field.is-grouped.is-grouped-multiline:not(:last-child){margin-bottom:0}@media screen and (min-width: 769px),print{.field.is-horizontal{display:flex}}.field-label .label{font-size:inherit}@media screen and (max-width: 768px){.field-label{margin-bottom:.5rem}}@media screen and (min-width: 769px),print{.field-label{flex-basis:0;flex-grow:1;flex-shrink:0;margin-right:1.5rem;text-align:right}.field-label.is-small{font-size:.75rem;padding-top:.375em}.field-label.is-normal{padding-top:.375em}.field-label.is-medium{font-size:1.25rem;padding-top:.375em}.field-label.is-large{font-size:1.5rem;padding-top:.375em}}.field-body .field .field{margin-bottom:0}@media screen and (min-width: 769px),print{.field-body{display:flex;flex-basis:0;flex-grow:5;flex-shrink:1}.field-body .field{margin-bottom:0}.field-body>.field{flex-shrink:1}.field-body>.field:not(.is-narrow){flex-grow:1}.field-body>.field:not(:last-child){margin-right:.75rem}}.control{box-sizing:border-box;clear:both;font-size:1rem;position:relative;text-align:inherit}.control.has-icons-left .input:focus~.icon,.control.has-icons-left .select:focus~.icon,.control.has-icons-right .input:focus~.icon,.control.has-icons-right .select:focus~.icon{color:#000}.control.has-icons-left .input.is-small~.icon,.control.has-icons-left .select.is-small~.icon,.control.has-icons-right .input.is-small~.icon,.control.has-icons-right .select.is-small~.icon{font-size:.75rem}.control.has-icons-left .input.is-medium~.icon,.control.has-icons-left .select.is-medium~.icon,.control.has-icons-right .input.is-medium~.icon,.control.has-icons-right .select.is-medium~.icon{font-size:1.25rem}.control.has-icons-left .input.is-large~.icon,.control.has-icons-left .select.is-large~.icon,.control.has-icons-right .input.is-large~.icon,.control.has-icons-right .select.is-large~.icon{font-size:1.5rem}.control.has-icons-left .icon,.control.has-icons-right .icon{color:#dbdbdb;height:2.5em;pointer-events:none;position:absolute;top:0;width:2.5em;z-index:4}.control.has-icons-left .input,.control.has-icons-left .select select{padding-left:2.5em}.control.has-icons-left .icon.is-left{left:0}.control.has-icons-right .input,.control.has-icons-right .select select{padding-right:2.5em}.control.has-icons-right .icon.is-right{right:0}.control.is-loading::after{position:absolute !important;right:.625em;top:.625em;z-index:4}.control.is-loading.is-small:after{font-size:.75rem}.control.is-loading.is-medium:after{font-size:1.25rem}.control.is-loading.is-large:after{font-size:1.5rem}.breadcrumb{font-size:1rem;white-space:nowrap}.breadcrumb a{align-items:center;color:#3273dc;display:flex;justify-content:center;padding:0 .75em}.breadcrumb a:hover{color:#363636}.breadcrumb li{align-items:center;display:flex}.breadcrumb li:first-child a{padding-left:0}.breadcrumb li.is-active a{color:#363636;cursor:default;pointer-events:none}.breadcrumb li+li::before{color:#b5b5b5;content:"/"}.breadcrumb ul,.breadcrumb ol{align-items:flex-start;display:flex;flex-wrap:wrap;justify-content:flex-start}.breadcrumb .icon:first-child{margin-right:.5em}.breadcrumb .icon:last-child{margin-left:.5em}.breadcrumb.is-centered ol,.breadcrumb.is-centered ul{justify-content:center}.breadcrumb.is-right ol,.breadcrumb.is-right ul{justify-content:flex-end}.breadcrumb.is-small{font-size:.75rem}.breadcrumb.is-medium{font-size:1.25rem}.breadcrumb.is-large{font-size:1.5rem}.breadcrumb.has-arrow-separator li+li::before{content:"→"}.breadcrumb.has-bullet-separator li+li::before{content:"•"}.breadcrumb.has-dot-separator li+li::before{content:"·"}.breadcrumb.has-succeeds-separator li+li::before{content:"≻"}.card{background-color:#fff;box-shadow:0 .5em 1em -0.125em rgba(10,10,10,.1),0 0px 0 1px rgba(10,10,10,.02);color:#000;max-width:100%;position:relative}.card-header{background-color:transparent;align-items:stretch;box-shadow:0 .125em .25em rgba(10,10,10,.1);display:flex}.card-header-title{align-items:center;color:#363636;display:flex;flex-grow:1;font-weight:700;padding:.75rem 1rem}.card-header-title.is-centered{justify-content:center}.card-header-icon{align-items:center;cursor:pointer;display:flex;justify-content:center;padding:.75rem 1rem}.card-image{display:block;position:relative}.card-content{background-color:transparent;padding:1.5rem}.card-footer{background-color:transparent;border-top:1px solid #ededed;align-items:stretch;display:flex}.card-footer-item{align-items:center;display:flex;flex-basis:0;flex-grow:1;flex-shrink:0;justify-content:center;padding:.75rem}.card-footer-item:not(:last-child){border-right:1px solid #ededed}.card .media:not(:last-child){margin-bottom:1.5rem}.dropdown{display:inline-flex;position:relative;vertical-align:top}.dropdown.is-active .dropdown-menu,.dropdown.is-hoverable:hover .dropdown-menu{display:block}.dropdown.is-right .dropdown-menu{left:auto;right:0}.dropdown.is-up .dropdown-menu{bottom:100%;padding-bottom:4px;padding-top:initial;top:auto}.dropdown-menu{display:none;left:0;min-width:12rem;padding-top:4px;position:absolute;top:100%;z-index:20}.dropdown-content{background-color:#fff;border-radius:4px;box-shadow:0 .5em 1em -0.125em rgba(10,10,10,.1),0 0px 0 1px rgba(10,10,10,.02);padding-bottom:.5rem;padding-top:.5rem}.dropdown-item{color:#000;display:block;font-size:.875rem;line-height:1.5;padding:.375rem 1rem;position:relative}a.dropdown-item,button.dropdown-item{padding-right:3rem;text-align:inherit;white-space:nowrap;width:100%}a.dropdown-item:hover,button.dropdown-item:hover{background-color:#f5f5f5;color:#0a0a0a}a.dropdown-item.is-active,button.dropdown-item.is-active{background-color:#3273dc;color:#fff}.dropdown-divider{background-color:#ededed;border:none;display:block;height:1px;margin:.5rem 0}.level{align-items:center;justify-content:space-between}.level code{border-radius:4px}.level img{display:inline-block;vertical-align:top}.level.is-mobile{display:flex}.level.is-mobile .level-left,.level.is-mobile .level-right{display:flex}.level.is-mobile .level-left+.level-right{margin-top:0}.level.is-mobile .level-item:not(:last-child){margin-bottom:0;margin-right:.75rem}.level.is-mobile .level-item:not(.is-narrow){flex-grow:1}@media screen and (min-width: 769px),print{.level{display:flex}.level>.level-item:not(.is-narrow){flex-grow:1}}.level-item{align-items:center;display:flex;flex-basis:auto;flex-grow:0;flex-shrink:0;justify-content:center}.level-item .title,.level-item .subtitle{margin-bottom:0}@media screen and (max-width: 768px){.level-item:not(:last-child){margin-bottom:.75rem}}.level-left,.level-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.level-left .level-item.is-flexible,.level-right .level-item.is-flexible{flex-grow:1}@media screen and (min-width: 769px),print{.level-left .level-item:not(:last-child),.level-right .level-item:not(:last-child){margin-right:.75rem}}.level-left{align-items:center;justify-content:flex-start}@media screen and (max-width: 768px){.level-left+.level-right{margin-top:1.5rem}}@media screen and (min-width: 769px),print{.level-left{display:flex}}.level-right{align-items:center;justify-content:flex-end}@media screen and (min-width: 769px),print{.level-right{display:flex}}.media{align-items:flex-start;display:flex;text-align:inherit}.media .content:not(:last-child){margin-bottom:.75rem}.media .media{border-top:1px solid rgba(219,219,219,.5);display:flex;padding-top:.75rem}.media .media .content:not(:last-child),.media .media .control:not(:last-child){margin-bottom:.5rem}.media .media .media{padding-top:.5rem}.media .media .media+.media{margin-top:.5rem}.media+.media{border-top:1px solid rgba(219,219,219,.5);margin-top:1rem;padding-top:1rem}.media.is-large+.media{margin-top:1.5rem;padding-top:1.5rem}.media-left,.media-right{flex-basis:auto;flex-grow:0;flex-shrink:0}.media-left{margin-right:1rem}.media-right{margin-left:1rem}.media-content{flex-basis:auto;flex-grow:1;flex-shrink:1;text-align:inherit}@media screen and (max-width: 768px){.media-content{overflow-x:auto}}.menu{font-size:1rem}.menu.is-small{font-size:.75rem}.menu.is-medium{font-size:1.25rem}.menu.is-large{font-size:1.5rem}.menu-list{line-height:1.25}.menu-list a{border-radius:2px;color:#000;display:block;padding:.5em .75em}.menu-list a:hover{background-color:#f5f5f5;color:#363636}.menu-list a.is-active{background-color:#3273dc;color:#fff}.menu-list li ul{border-left:1px solid #dbdbdb;margin:.75em;padding-left:.75em}.menu-label{color:#7a7a7a;font-size:.75em;letter-spacing:.1em;text-transform:uppercase}.menu-label:not(:first-child){margin-top:1em}.menu-label:not(:last-child){margin-bottom:1em}.message{background-color:#f5f5f5;border-radius:4px;font-size:1rem}.message strong{color:currentColor}.message a:not(.button):not(.tag):not(.dropdown-item){color:currentColor;text-decoration:underline}.message.is-small{font-size:.75rem}.message.is-medium{font-size:1.25rem}.message.is-large{font-size:1.5rem}.message.is-white{background-color:#fff}.message.is-white .message-header{background-color:#fff;color:#0a0a0a}.message.is-white .message-body{border-color:#fff}.message.is-black{background-color:#fafafa}.message.is-black .message-header{background-color:#0a0a0a;color:#fff}.message.is-black .message-body{border-color:#0a0a0a}.message.is-light{background-color:#fafafa}.message.is-light .message-header{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.message.is-light .message-body{border-color:#f5f5f5}.message.is-dark{background-color:#fafafa}.message.is-dark .message-header{background-color:#363636;color:#fff}.message.is-dark .message-body{border-color:#363636}.message.is-primary{background-color:#ebfffc}.message.is-primary .message-header{background-color:#00d1b2;color:#fff}.message.is-primary .message-body{border-color:#00d1b2;color:#00947e}.message.is-link{background-color:#eef3fc}.message.is-link .message-header{background-color:#3273dc;color:#fff}.message.is-link .message-body{border-color:#3273dc;color:#2160c4}.message.is-info{background-color:#eef6fc}.message.is-info .message-header{background-color:#3298dc;color:#fff}.message.is-info .message-body{border-color:#3298dc;color:#1d72aa}.message.is-success{background-color:#effaf3}.message.is-success .message-header{background-color:#48c774;color:#fff}.message.is-success .message-body{border-color:#48c774;color:#257942}.message.is-warning{background-color:#fffbeb}.message.is-warning .message-header{background-color:#ffdd57;color:rgba(0,0,0,.7)}.message.is-warning .message-body{border-color:#ffdd57;color:#947600}.message.is-danger{background-color:#feecf0}.message.is-danger .message-header{background-color:#f14668;color:#fff}.message.is-danger .message-body{border-color:#f14668;color:#cc0f35}.message-header{align-items:center;background-color:#000;border-radius:4px 4px 0 0;color:#fff;display:flex;font-weight:700;justify-content:space-between;line-height:1.25;padding:.75em 1em;position:relative}.message-header .delete{flex-grow:0;flex-shrink:0;margin-left:.75em}.message-header+.message-body{border-width:0;border-top-left-radius:0;border-top-right-radius:0}.message-body{border-color:#dbdbdb;border-radius:4px;border-style:solid;border-width:0 0 0 4px;color:#000;padding:1.25em 1.5em}.message-body code,.message-body pre{background-color:#fff}.message-body pre code{background-color:transparent}.modal{align-items:center;display:none;flex-direction:column;justify-content:center;overflow:hidden;position:fixed;z-index:40}.modal.is-active{display:flex}.modal-background{background-color:rgba(10,10,10,.86)}.modal-content,.modal-card{margin:0 20px;max-height:calc(100vh - 160px);overflow:auto;position:relative;width:100%}@media screen and (min-width: 769px),print{.modal-content,.modal-card{margin:0 auto;max-height:calc(100vh - 40px);width:640px}}.modal-close{background:none;height:40px;position:fixed;right:20px;top:20px;width:40px}.modal-card{display:flex;flex-direction:column;max-height:calc(100vh - 40px);overflow:hidden;-ms-overflow-y:visible}.modal-card-head,.modal-card-foot{align-items:center;background-color:#f5f5f5;display:flex;flex-shrink:0;justify-content:flex-start;padding:20px;position:relative}.modal-card-head{border-bottom:1px solid #dbdbdb;border-top-left-radius:6px;border-top-right-radius:6px}.modal-card-title{color:#363636;flex-grow:1;flex-shrink:0;font-size:1.5rem;line-height:1}.modal-card-foot{border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:1px solid #dbdbdb}.modal-card-foot .button:not(:last-child){margin-right:.5em}.modal-card-body{-webkit-overflow-scrolling:touch;background-color:#fff;flex-grow:1;flex-shrink:1;overflow:auto;padding:20px}.navbar{background-color:#fff;min-height:3.25rem;position:relative;z-index:30}.navbar.is-white{background-color:#fff;color:#0a0a0a}.navbar.is-white .navbar-brand>.navbar-item,.navbar.is-white .navbar-brand .navbar-link{color:#0a0a0a}.navbar.is-white .navbar-brand>a.navbar-item:focus,.navbar.is-white .navbar-brand>a.navbar-item:hover,.navbar.is-white .navbar-brand>a.navbar-item.is-active,.navbar.is-white .navbar-brand .navbar-link:focus,.navbar.is-white .navbar-brand .navbar-link:hover,.navbar.is-white .navbar-brand .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-burger{color:#0a0a0a}@media screen and (min-width: 1024px){.navbar.is-white .navbar-start>.navbar-item,.navbar.is-white .navbar-start .navbar-link,.navbar.is-white .navbar-end>.navbar-item,.navbar.is-white .navbar-end .navbar-link{color:#0a0a0a}.navbar.is-white .navbar-start>a.navbar-item:focus,.navbar.is-white .navbar-start>a.navbar-item:hover,.navbar.is-white .navbar-start>a.navbar-item.is-active,.navbar.is-white .navbar-start .navbar-link:focus,.navbar.is-white .navbar-start .navbar-link:hover,.navbar.is-white .navbar-start .navbar-link.is-active,.navbar.is-white .navbar-end>a.navbar-item:focus,.navbar.is-white .navbar-end>a.navbar-item:hover,.navbar.is-white .navbar-end>a.navbar-item.is-active,.navbar.is-white .navbar-end .navbar-link:focus,.navbar.is-white .navbar-end .navbar-link:hover,.navbar.is-white .navbar-end .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-start .navbar-link::after,.navbar.is-white .navbar-end .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-dropdown a.navbar-item.is-active{background-color:#fff;color:#0a0a0a}}.navbar.is-black{background-color:#0a0a0a;color:#fff}.navbar.is-black .navbar-brand>.navbar-item,.navbar.is-black .navbar-brand .navbar-link{color:#fff}.navbar.is-black .navbar-brand>a.navbar-item:focus,.navbar.is-black .navbar-brand>a.navbar-item:hover,.navbar.is-black .navbar-brand>a.navbar-item.is-active,.navbar.is-black .navbar-brand .navbar-link:focus,.navbar.is-black .navbar-brand .navbar-link:hover,.navbar.is-black .navbar-brand .navbar-link.is-active{background-color:#000;color:#fff}.navbar.is-black .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-burger{color:#fff}@media screen and (min-width: 1024px){.navbar.is-black .navbar-start>.navbar-item,.navbar.is-black .navbar-start .navbar-link,.navbar.is-black .navbar-end>.navbar-item,.navbar.is-black .navbar-end .navbar-link{color:#fff}.navbar.is-black .navbar-start>a.navbar-item:focus,.navbar.is-black .navbar-start>a.navbar-item:hover,.navbar.is-black .navbar-start>a.navbar-item.is-active,.navbar.is-black .navbar-start .navbar-link:focus,.navbar.is-black .navbar-start .navbar-link:hover,.navbar.is-black .navbar-start .navbar-link.is-active,.navbar.is-black .navbar-end>a.navbar-item:focus,.navbar.is-black .navbar-end>a.navbar-item:hover,.navbar.is-black .navbar-end>a.navbar-item.is-active,.navbar.is-black .navbar-end .navbar-link:focus,.navbar.is-black .navbar-end .navbar-link:hover,.navbar.is-black .navbar-end .navbar-link.is-active{background-color:#000;color:#fff}.navbar.is-black .navbar-start .navbar-link::after,.navbar.is-black .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-black .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link{background-color:#000;color:#fff}.navbar.is-black .navbar-dropdown a.navbar-item.is-active{background-color:#0a0a0a;color:#fff}}.navbar.is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand>.navbar-item,.navbar.is-light .navbar-brand .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand>a.navbar-item:focus,.navbar.is-light .navbar-brand>a.navbar-item:hover,.navbar.is-light .navbar-brand>a.navbar-item.is-active,.navbar.is-light .navbar-brand .navbar-link:focus,.navbar.is-light .navbar-brand .navbar-link:hover,.navbar.is-light .navbar-brand .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-light .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width: 1024px){.navbar.is-light .navbar-start>.navbar-item,.navbar.is-light .navbar-start .navbar-link,.navbar.is-light .navbar-end>.navbar-item,.navbar.is-light .navbar-end .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-light .navbar-start>a.navbar-item:focus,.navbar.is-light .navbar-start>a.navbar-item:hover,.navbar.is-light .navbar-start>a.navbar-item.is-active,.navbar.is-light .navbar-start .navbar-link:focus,.navbar.is-light .navbar-start .navbar-link:hover,.navbar.is-light .navbar-start .navbar-link.is-active,.navbar.is-light .navbar-end>a.navbar-item:focus,.navbar.is-light .navbar-end>a.navbar-item:hover,.navbar.is-light .navbar-end>a.navbar-item.is-active,.navbar.is-light .navbar-end .navbar-link:focus,.navbar.is-light .navbar-end .navbar-link:hover,.navbar.is-light .navbar-end .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-start .navbar-link::after,.navbar.is-light .navbar-end .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-light .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-light .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.navbar.is-light .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:rgba(0,0,0,.7)}}.navbar.is-dark{background-color:#363636;color:#fff}.navbar.is-dark .navbar-brand>.navbar-item,.navbar.is-dark .navbar-brand .navbar-link{color:#fff}.navbar.is-dark .navbar-brand>a.navbar-item:focus,.navbar.is-dark .navbar-brand>a.navbar-item:hover,.navbar.is-dark .navbar-brand>a.navbar-item.is-active,.navbar.is-dark .navbar-brand .navbar-link:focus,.navbar.is-dark .navbar-brand .navbar-link:hover,.navbar.is-dark .navbar-brand .navbar-link.is-active{background-color:#292929;color:#fff}.navbar.is-dark .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-dark .navbar-burger{color:#fff}@media screen and (min-width: 1024px){.navbar.is-dark .navbar-start>.navbar-item,.navbar.is-dark .navbar-start .navbar-link,.navbar.is-dark .navbar-end>.navbar-item,.navbar.is-dark .navbar-end .navbar-link{color:#fff}.navbar.is-dark .navbar-start>a.navbar-item:focus,.navbar.is-dark .navbar-start>a.navbar-item:hover,.navbar.is-dark .navbar-start>a.navbar-item.is-active,.navbar.is-dark .navbar-start .navbar-link:focus,.navbar.is-dark .navbar-start .navbar-link:hover,.navbar.is-dark .navbar-start .navbar-link.is-active,.navbar.is-dark .navbar-end>a.navbar-item:focus,.navbar.is-dark .navbar-end>a.navbar-item:hover,.navbar.is-dark .navbar-end>a.navbar-item.is-active,.navbar.is-dark .navbar-end .navbar-link:focus,.navbar.is-dark .navbar-end .navbar-link:hover,.navbar.is-dark .navbar-end .navbar-link.is-active{background-color:#292929;color:#fff}.navbar.is-dark .navbar-start .navbar-link::after,.navbar.is-dark .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-dark .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link{background-color:#292929;color:#fff}.navbar.is-dark .navbar-dropdown a.navbar-item.is-active{background-color:#363636;color:#fff}}.navbar.is-primary{background-color:#00d1b2;color:#fff}.navbar.is-primary .navbar-brand>.navbar-item,.navbar.is-primary .navbar-brand .navbar-link{color:#fff}.navbar.is-primary .navbar-brand>a.navbar-item:focus,.navbar.is-primary .navbar-brand>a.navbar-item:hover,.navbar.is-primary .navbar-brand>a.navbar-item.is-active,.navbar.is-primary .navbar-brand .navbar-link:focus,.navbar.is-primary .navbar-brand .navbar-link:hover,.navbar.is-primary .navbar-brand .navbar-link.is-active{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-burger{color:#fff}@media screen and (min-width: 1024px){.navbar.is-primary .navbar-start>.navbar-item,.navbar.is-primary .navbar-start .navbar-link,.navbar.is-primary .navbar-end>.navbar-item,.navbar.is-primary .navbar-end .navbar-link{color:#fff}.navbar.is-primary .navbar-start>a.navbar-item:focus,.navbar.is-primary .navbar-start>a.navbar-item:hover,.navbar.is-primary .navbar-start>a.navbar-item.is-active,.navbar.is-primary .navbar-start .navbar-link:focus,.navbar.is-primary .navbar-start .navbar-link:hover,.navbar.is-primary .navbar-start .navbar-link.is-active,.navbar.is-primary .navbar-end>a.navbar-item:focus,.navbar.is-primary .navbar-end>a.navbar-item:hover,.navbar.is-primary .navbar-end>a.navbar-item.is-active,.navbar.is-primary .navbar-end .navbar-link:focus,.navbar.is-primary .navbar-end .navbar-link:hover,.navbar.is-primary .navbar-end .navbar-link.is-active{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-start .navbar-link::after,.navbar.is-primary .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link{background-color:#00b89c;color:#fff}.navbar.is-primary .navbar-dropdown a.navbar-item.is-active{background-color:#00d1b2;color:#fff}}.navbar.is-link{background-color:#3273dc;color:#fff}.navbar.is-link .navbar-brand>.navbar-item,.navbar.is-link .navbar-brand .navbar-link{color:#fff}.navbar.is-link .navbar-brand>a.navbar-item:focus,.navbar.is-link .navbar-brand>a.navbar-item:hover,.navbar.is-link .navbar-brand>a.navbar-item.is-active,.navbar.is-link .navbar-brand .navbar-link:focus,.navbar.is-link .navbar-brand .navbar-link:hover,.navbar.is-link .navbar-brand .navbar-link.is-active{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-burger{color:#fff}@media screen and (min-width: 1024px){.navbar.is-link .navbar-start>.navbar-item,.navbar.is-link .navbar-start .navbar-link,.navbar.is-link .navbar-end>.navbar-item,.navbar.is-link .navbar-end .navbar-link{color:#fff}.navbar.is-link .navbar-start>a.navbar-item:focus,.navbar.is-link .navbar-start>a.navbar-item:hover,.navbar.is-link .navbar-start>a.navbar-item.is-active,.navbar.is-link .navbar-start .navbar-link:focus,.navbar.is-link .navbar-start .navbar-link:hover,.navbar.is-link .navbar-start .navbar-link.is-active,.navbar.is-link .navbar-end>a.navbar-item:focus,.navbar.is-link .navbar-end>a.navbar-item:hover,.navbar.is-link .navbar-end>a.navbar-item.is-active,.navbar.is-link .navbar-end .navbar-link:focus,.navbar.is-link .navbar-end .navbar-link:hover,.navbar.is-link .navbar-end .navbar-link.is-active{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-start .navbar-link::after,.navbar.is-link .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-link .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link{background-color:#2366d1;color:#fff}.navbar.is-link .navbar-dropdown a.navbar-item.is-active{background-color:#3273dc;color:#fff}}.navbar.is-info{background-color:#3298dc;color:#fff}.navbar.is-info .navbar-brand>.navbar-item,.navbar.is-info .navbar-brand .navbar-link{color:#fff}.navbar.is-info .navbar-brand>a.navbar-item:focus,.navbar.is-info .navbar-brand>a.navbar-item:hover,.navbar.is-info .navbar-brand>a.navbar-item.is-active,.navbar.is-info .navbar-brand .navbar-link:focus,.navbar.is-info .navbar-brand .navbar-link:hover,.navbar.is-info .navbar-brand .navbar-link.is-active{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-burger{color:#fff}@media screen and (min-width: 1024px){.navbar.is-info .navbar-start>.navbar-item,.navbar.is-info .navbar-start .navbar-link,.navbar.is-info .navbar-end>.navbar-item,.navbar.is-info .navbar-end .navbar-link{color:#fff}.navbar.is-info .navbar-start>a.navbar-item:focus,.navbar.is-info .navbar-start>a.navbar-item:hover,.navbar.is-info .navbar-start>a.navbar-item.is-active,.navbar.is-info .navbar-start .navbar-link:focus,.navbar.is-info .navbar-start .navbar-link:hover,.navbar.is-info .navbar-start .navbar-link.is-active,.navbar.is-info .navbar-end>a.navbar-item:focus,.navbar.is-info .navbar-end>a.navbar-item:hover,.navbar.is-info .navbar-end>a.navbar-item.is-active,.navbar.is-info .navbar-end .navbar-link:focus,.navbar.is-info .navbar-end .navbar-link:hover,.navbar.is-info .navbar-end .navbar-link.is-active{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-start .navbar-link::after,.navbar.is-info .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-info .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link{background-color:#238cd1;color:#fff}.navbar.is-info .navbar-dropdown a.navbar-item.is-active{background-color:#3298dc;color:#fff}}.navbar.is-success{background-color:#48c774;color:#fff}.navbar.is-success .navbar-brand>.navbar-item,.navbar.is-success .navbar-brand .navbar-link{color:#fff}.navbar.is-success .navbar-brand>a.navbar-item:focus,.navbar.is-success .navbar-brand>a.navbar-item:hover,.navbar.is-success .navbar-brand>a.navbar-item.is-active,.navbar.is-success .navbar-brand .navbar-link:focus,.navbar.is-success .navbar-brand .navbar-link:hover,.navbar.is-success .navbar-brand .navbar-link.is-active{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-burger{color:#fff}@media screen and (min-width: 1024px){.navbar.is-success .navbar-start>.navbar-item,.navbar.is-success .navbar-start .navbar-link,.navbar.is-success .navbar-end>.navbar-item,.navbar.is-success .navbar-end .navbar-link{color:#fff}.navbar.is-success .navbar-start>a.navbar-item:focus,.navbar.is-success .navbar-start>a.navbar-item:hover,.navbar.is-success .navbar-start>a.navbar-item.is-active,.navbar.is-success .navbar-start .navbar-link:focus,.navbar.is-success .navbar-start .navbar-link:hover,.navbar.is-success .navbar-start .navbar-link.is-active,.navbar.is-success .navbar-end>a.navbar-item:focus,.navbar.is-success .navbar-end>a.navbar-item:hover,.navbar.is-success .navbar-end>a.navbar-item.is-active,.navbar.is-success .navbar-end .navbar-link:focus,.navbar.is-success .navbar-end .navbar-link:hover,.navbar.is-success .navbar-end .navbar-link.is-active{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-start .navbar-link::after,.navbar.is-success .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-success .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link{background-color:#3abb67;color:#fff}.navbar.is-success .navbar-dropdown a.navbar-item.is-active{background-color:#48c774;color:#fff}}.navbar.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand>.navbar-item,.navbar.is-warning .navbar-brand .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand>a.navbar-item:focus,.navbar.is-warning .navbar-brand>a.navbar-item:hover,.navbar.is-warning .navbar-brand>a.navbar-item.is-active,.navbar.is-warning .navbar-brand .navbar-link:focus,.navbar.is-warning .navbar-brand .navbar-link:hover,.navbar.is-warning .navbar-brand .navbar-link.is-active{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width: 1024px){.navbar.is-warning .navbar-start>.navbar-item,.navbar.is-warning .navbar-start .navbar-link,.navbar.is-warning .navbar-end>.navbar-item,.navbar.is-warning .navbar-end .navbar-link{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-start>a.navbar-item:focus,.navbar.is-warning .navbar-start>a.navbar-item:hover,.navbar.is-warning .navbar-start>a.navbar-item.is-active,.navbar.is-warning .navbar-start .navbar-link:focus,.navbar.is-warning .navbar-start .navbar-link:hover,.navbar.is-warning .navbar-start .navbar-link.is-active,.navbar.is-warning .navbar-end>a.navbar-item:focus,.navbar.is-warning .navbar-end>a.navbar-item:hover,.navbar.is-warning .navbar-end>a.navbar-item.is-active,.navbar.is-warning .navbar-end .navbar-link:focus,.navbar.is-warning .navbar-end .navbar-link:hover,.navbar.is-warning .navbar-end .navbar-link.is-active{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-start .navbar-link::after,.navbar.is-warning .navbar-end .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-dropdown a.navbar-item.is-active{background-color:#ffdd57;color:rgba(0,0,0,.7)}}.navbar.is-danger{background-color:#f14668;color:#fff}.navbar.is-danger .navbar-brand>.navbar-item,.navbar.is-danger .navbar-brand .navbar-link{color:#fff}.navbar.is-danger .navbar-brand>a.navbar-item:focus,.navbar.is-danger .navbar-brand>a.navbar-item:hover,.navbar.is-danger .navbar-brand>a.navbar-item.is-active,.navbar.is-danger .navbar-brand .navbar-link:focus,.navbar.is-danger .navbar-brand .navbar-link:hover,.navbar.is-danger .navbar-brand .navbar-link.is-active{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-burger{color:#fff}@media screen and (min-width: 1024px){.navbar.is-danger .navbar-start>.navbar-item,.navbar.is-danger .navbar-start .navbar-link,.navbar.is-danger .navbar-end>.navbar-item,.navbar.is-danger .navbar-end .navbar-link{color:#fff}.navbar.is-danger .navbar-start>a.navbar-item:focus,.navbar.is-danger .navbar-start>a.navbar-item:hover,.navbar.is-danger .navbar-start>a.navbar-item.is-active,.navbar.is-danger .navbar-start .navbar-link:focus,.navbar.is-danger .navbar-start .navbar-link:hover,.navbar.is-danger .navbar-start .navbar-link.is-active,.navbar.is-danger .navbar-end>a.navbar-item:focus,.navbar.is-danger .navbar-end>a.navbar-item:hover,.navbar.is-danger .navbar-end>a.navbar-item.is-active,.navbar.is-danger .navbar-end .navbar-link:focus,.navbar.is-danger .navbar-end .navbar-link:hover,.navbar.is-danger .navbar-end .navbar-link.is-active{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-start .navbar-link::after,.navbar.is-danger .navbar-end .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-item.has-dropdown:focus .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link,.navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link{background-color:#ef2e55;color:#fff}.navbar.is-danger .navbar-dropdown a.navbar-item.is-active{background-color:#f14668;color:#fff}}.navbar>.container{align-items:stretch;display:flex;min-height:3.25rem;width:100%}.navbar.has-shadow{box-shadow:0 2px 0 0 #f5f5f5}.navbar.is-fixed-bottom,.navbar.is-fixed-top{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom{bottom:0}.navbar.is-fixed-bottom.has-shadow{box-shadow:0 -2px 0 0 #f5f5f5}.navbar.is-fixed-top{top:0}html.has-navbar-fixed-top,body.has-navbar-fixed-top{padding-top:3.25rem}html.has-navbar-fixed-bottom,body.has-navbar-fixed-bottom{padding-bottom:3.25rem}.navbar-brand,.navbar-tabs{align-items:stretch;display:flex;flex-shrink:0;min-height:3.25rem}.navbar-brand a.navbar-item:focus,.navbar-brand a.navbar-item:hover{background-color:transparent}.navbar-tabs{-webkit-overflow-scrolling:touch;max-width:100vw;overflow-x:auto;overflow-y:hidden}.navbar-burger{color:#000;cursor:pointer;display:block;height:3.25rem;position:relative;width:3.25rem;margin-left:auto}.navbar-burger span{background-color:currentColor;display:block;height:1px;left:calc(50% - 8px);position:absolute;transform-origin:center;transition-duration:86ms;transition-property:background-color,opacity,transform;transition-timing-function:ease-out;width:16px}.navbar-burger span:nth-child(1){top:calc(50% - 6px)}.navbar-burger span:nth-child(2){top:calc(50% - 1px)}.navbar-burger span:nth-child(3){top:calc(50% + 4px)}.navbar-burger:hover{background-color:rgba(0,0,0,.05)}.navbar-burger.is-active span:nth-child(1){transform:translateY(5px) rotate(45deg)}.navbar-burger.is-active span:nth-child(2){opacity:0}.navbar-burger.is-active span:nth-child(3){transform:translateY(-5px) rotate(-45deg)}.navbar-menu{display:none}.navbar-item,.navbar-link{color:#000;display:block;line-height:1.5;padding:.5rem .75rem;position:relative}.navbar-item .icon:only-child,.navbar-link .icon:only-child{margin-left:-0.25rem;margin-right:-0.25rem}a.navbar-item,.navbar-link{cursor:pointer}a.navbar-item:focus,a.navbar-item:focus-within,a.navbar-item:hover,a.navbar-item.is-active,.navbar-link:focus,.navbar-link:focus-within,.navbar-link:hover,.navbar-link.is-active{background-color:#fafafa;color:#3273dc}.navbar-item{flex-grow:0;flex-shrink:0}.navbar-item img{max-height:1.75rem}.navbar-item.has-dropdown{padding:0}.navbar-item.is-expanded{flex-grow:1;flex-shrink:1}.navbar-item.is-tab{border-bottom:1px solid transparent;min-height:3.25rem;padding-bottom:calc(0.5rem - 1px)}.navbar-item.is-tab:focus,.navbar-item.is-tab:hover{background-color:transparent;border-bottom-color:#3273dc}.navbar-item.is-tab.is-active{background-color:transparent;border-bottom-color:#3273dc;border-bottom-style:solid;border-bottom-width:3px;color:#3273dc;padding-bottom:calc(0.5rem - 3px)}.navbar-content{flex-grow:1;flex-shrink:1}.navbar-link:not(.is-arrowless){padding-right:2.5em}.navbar-link:not(.is-arrowless)::after{border-color:#3273dc;margin-top:-0.375em;right:1.125em}.navbar-dropdown{font-size:.875rem;padding-bottom:.5rem;padding-top:.5rem}.navbar-dropdown .navbar-item{padding-left:1.5rem;padding-right:1.5rem}.navbar-divider{background-color:#f5f5f5;border:none;display:none;height:2px;margin:.5rem 0}@media screen and (max-width: 1023px){.navbar>.container{display:block}.navbar-brand .navbar-item,.navbar-tabs .navbar-item{align-items:center;display:flex}.navbar-link::after{display:none}.navbar-menu{background-color:#fff;box-shadow:0 8px 16px rgba(10,10,10,.1);padding:.5rem 0}.navbar-menu.is-active{display:block}.navbar.is-fixed-bottom-touch,.navbar.is-fixed-top-touch{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-touch{bottom:0}.navbar.is-fixed-bottom-touch.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-touch{top:0}.navbar.is-fixed-top .navbar-menu,.navbar.is-fixed-top-touch .navbar-menu{-webkit-overflow-scrolling:touch;max-height:calc(100vh - 3.25rem);overflow:auto}html.has-navbar-fixed-top-touch,body.has-navbar-fixed-top-touch{padding-top:3.25rem}html.has-navbar-fixed-bottom-touch,body.has-navbar-fixed-bottom-touch{padding-bottom:3.25rem}}@media screen and (min-width: 1024px){.navbar,.navbar-menu,.navbar-start,.navbar-end{align-items:stretch;display:flex}.navbar{min-height:3.25rem}.navbar.is-spaced{padding:1rem 2rem}.navbar.is-spaced .navbar-start,.navbar.is-spaced .navbar-end{align-items:center}.navbar.is-spaced a.navbar-item,.navbar.is-spaced .navbar-link{border-radius:4px}.navbar.is-transparent a.navbar-item:focus,.navbar.is-transparent a.navbar-item:hover,.navbar.is-transparent a.navbar-item.is-active,.navbar.is-transparent .navbar-link:focus,.navbar.is-transparent .navbar-link:hover,.navbar.is-transparent .navbar-link.is-active{background-color:transparent !important}.navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:focus-within .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link{background-color:transparent !important}.navbar.is-transparent .navbar-dropdown a.navbar-item:focus,.navbar.is-transparent .navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar.is-transparent .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar-burger{display:none}.navbar-item,.navbar-link{align-items:center;display:flex}.navbar-item.has-dropdown{align-items:stretch}.navbar-item.has-dropdown-up .navbar-link::after{transform:rotate(135deg) translate(0.25em, -0.25em)}.navbar-item.has-dropdown-up .navbar-dropdown{border-bottom:2px solid #dbdbdb;border-radius:6px 6px 0 0;border-top:none;bottom:100%;box-shadow:0 -8px 8px rgba(10,10,10,.1);top:auto}.navbar-item.is-active .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown{display:block}.navbar.is-spaced .navbar-item.is-active .navbar-dropdown,.navbar-item.is-active .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:focus .navbar-dropdown,.navbar-item.is-hoverable:focus .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:focus-within .navbar-dropdown,.navbar-item.is-hoverable:focus-within .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-hoverable:hover .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed{opacity:1;pointer-events:auto;transform:translateY(0)}.navbar-menu{flex-grow:1;flex-shrink:0}.navbar-start{justify-content:flex-start;margin-right:auto}.navbar-end{justify-content:flex-end;margin-left:auto}.navbar-dropdown{background-color:#fff;border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:2px solid #dbdbdb;box-shadow:0 8px 8px rgba(10,10,10,.1);display:none;font-size:.875rem;left:0;min-width:100%;position:absolute;top:100%;z-index:20}.navbar-dropdown .navbar-item{padding:.375rem 1rem;white-space:nowrap}.navbar-dropdown a.navbar-item{padding-right:3rem}.navbar-dropdown a.navbar-item:focus,.navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#3273dc}.navbar.is-spaced .navbar-dropdown,.navbar-dropdown.is-boxed{border-radius:6px;border-top:none;box-shadow:0 8px 8px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);display:block;opacity:0;pointer-events:none;top:calc(100% + (-4px));transform:translateY(-5px);transition-duration:86ms;transition-property:opacity,transform}.navbar-dropdown.is-right{left:auto;right:0}.navbar-divider{display:block}.navbar>.container .navbar-brand,.container>.navbar .navbar-brand{margin-left:-0.75rem}.navbar>.container .navbar-menu,.container>.navbar .navbar-menu{margin-right:-0.75rem}.navbar.is-fixed-bottom-desktop,.navbar.is-fixed-top-desktop{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-desktop{bottom:0}.navbar.is-fixed-bottom-desktop.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-desktop{top:0}html.has-navbar-fixed-top-desktop,body.has-navbar-fixed-top-desktop{padding-top:3.25rem}html.has-navbar-fixed-bottom-desktop,body.has-navbar-fixed-bottom-desktop{padding-bottom:3.25rem}html.has-spaced-navbar-fixed-top,body.has-spaced-navbar-fixed-top{padding-top:5.25rem}html.has-spaced-navbar-fixed-bottom,body.has-spaced-navbar-fixed-bottom{padding-bottom:5.25rem}a.navbar-item.is-active,.navbar-link.is-active{color:#0a0a0a}a.navbar-item.is-active:not(:focus):not(:hover),.navbar-link.is-active:not(:focus):not(:hover){background-color:transparent}.navbar-item.has-dropdown:focus .navbar-link,.navbar-item.has-dropdown:hover .navbar-link,.navbar-item.has-dropdown.is-active .navbar-link{background-color:#fafafa}}.hero.is-fullheight-with-navbar{min-height:calc(100vh - 3.25rem)}.pagination{font-size:1rem;margin:-0.25rem}.pagination.is-small{font-size:.75rem}.pagination.is-medium{font-size:1.25rem}.pagination.is-large{font-size:1.5rem}.pagination.is-rounded .pagination-previous,.pagination.is-rounded .pagination-next{padding-left:1em;padding-right:1em;border-radius:290486px}.pagination.is-rounded .pagination-link{border-radius:290486px}.pagination,.pagination-list{align-items:center;display:flex;justify-content:center;text-align:center}.pagination-previous,.pagination-next,.pagination-link,.pagination-ellipsis{font-size:1em;justify-content:center;margin:.25rem;padding-left:.5em;padding-right:.5em;text-align:center}.pagination-previous,.pagination-next,.pagination-link{border-color:#dbdbdb;color:#363636;min-width:2.5em}.pagination-previous:hover,.pagination-next:hover,.pagination-link:hover{border-color:#b5b5b5;color:#363636}.pagination-previous:focus,.pagination-next:focus,.pagination-link:focus{border-color:#3273dc}.pagination-previous:active,.pagination-next:active,.pagination-link:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2)}.pagination-previous[disabled],.pagination-next[disabled],.pagination-link[disabled]{background-color:#dbdbdb;border-color:#dbdbdb;box-shadow:none;color:#7a7a7a;opacity:.5}.pagination-previous,.pagination-next{padding-left:.75em;padding-right:.75em;white-space:nowrap}.pagination-link.is-current{background-color:#3273dc;border-color:#3273dc;color:#fff}.pagination-ellipsis{color:#b5b5b5;pointer-events:none}.pagination-list{flex-wrap:wrap}@media screen and (max-width: 768px){.pagination{flex-wrap:wrap}.pagination-previous,.pagination-next{flex-grow:1;flex-shrink:1}.pagination-list li{flex-grow:1;flex-shrink:1}}@media screen and (min-width: 769px),print{.pagination-list{flex-grow:1;flex-shrink:1;justify-content:flex-start;order:1}.pagination-previous{order:2}.pagination-next{order:3}.pagination{justify-content:space-between}.pagination.is-centered .pagination-previous{order:1}.pagination.is-centered .pagination-list{justify-content:center;order:2}.pagination.is-centered .pagination-next{order:3}.pagination.is-right .pagination-previous{order:1}.pagination.is-right .pagination-next{order:2}.pagination.is-right .pagination-list{justify-content:flex-end;order:3}}.panel{border-radius:6px;box-shadow:0 .5em 1em -0.125em rgba(10,10,10,.1),0 0px 0 1px rgba(10,10,10,.02);font-size:1rem}.panel:not(:last-child){margin-bottom:1.5rem}.panel.is-white .panel-heading{background-color:#fff;color:#0a0a0a}.panel.is-white .panel-tabs a.is-active{border-bottom-color:#fff}.panel.is-white .panel-block.is-active .panel-icon{color:#fff}.panel.is-black .panel-heading{background-color:#0a0a0a;color:#fff}.panel.is-black .panel-tabs a.is-active{border-bottom-color:#0a0a0a}.panel.is-black .panel-block.is-active .panel-icon{color:#0a0a0a}.panel.is-light .panel-heading{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.panel.is-light .panel-tabs a.is-active{border-bottom-color:#f5f5f5}.panel.is-light .panel-block.is-active .panel-icon{color:#f5f5f5}.panel.is-dark .panel-heading{background-color:#363636;color:#fff}.panel.is-dark .panel-tabs a.is-active{border-bottom-color:#363636}.panel.is-dark .panel-block.is-active .panel-icon{color:#363636}.panel.is-primary .panel-heading{background-color:#00d1b2;color:#fff}.panel.is-primary .panel-tabs a.is-active{border-bottom-color:#00d1b2}.panel.is-primary .panel-block.is-active .panel-icon{color:#00d1b2}.panel.is-link .panel-heading{background-color:#3273dc;color:#fff}.panel.is-link .panel-tabs a.is-active{border-bottom-color:#3273dc}.panel.is-link .panel-block.is-active .panel-icon{color:#3273dc}.panel.is-info .panel-heading{background-color:#3298dc;color:#fff}.panel.is-info .panel-tabs a.is-active{border-bottom-color:#3298dc}.panel.is-info .panel-block.is-active .panel-icon{color:#3298dc}.panel.is-success .panel-heading{background-color:#48c774;color:#fff}.panel.is-success .panel-tabs a.is-active{border-bottom-color:#48c774}.panel.is-success .panel-block.is-active .panel-icon{color:#48c774}.panel.is-warning .panel-heading{background-color:#ffdd57;color:rgba(0,0,0,.7)}.panel.is-warning .panel-tabs a.is-active{border-bottom-color:#ffdd57}.panel.is-warning .panel-block.is-active .panel-icon{color:#ffdd57}.panel.is-danger .panel-heading{background-color:#f14668;color:#fff}.panel.is-danger .panel-tabs a.is-active{border-bottom-color:#f14668}.panel.is-danger .panel-block.is-active .panel-icon{color:#f14668}.panel-tabs:not(:last-child),.panel-block:not(:last-child){border-bottom:1px solid #ededed}.panel-heading{background-color:#ededed;border-radius:6px 6px 0 0;color:#363636;font-size:1.25em;font-weight:700;line-height:1.25;padding:.75em 1em}.panel-tabs{align-items:flex-end;display:flex;font-size:.875em;justify-content:center}.panel-tabs a{border-bottom:1px solid #dbdbdb;margin-bottom:-1px;padding:.5em}.panel-tabs a.is-active{border-bottom-color:#4a4a4a;color:#363636}.panel-list a{color:#000}.panel-list a:hover{color:#3273dc}.panel-block{align-items:center;color:#363636;display:flex;justify-content:flex-start;padding:.5em .75em}.panel-block input[type=checkbox]{margin-right:.75em}.panel-block>.control{flex-grow:1;flex-shrink:1;width:100%}.panel-block.is-wrapped{flex-wrap:wrap}.panel-block.is-active{border-left-color:#3273dc;color:#363636}.panel-block.is-active .panel-icon{color:#3273dc}.panel-block:last-child{border-bottom-left-radius:6px;border-bottom-right-radius:6px}a.panel-block,label.panel-block{cursor:pointer}a.panel-block:hover,label.panel-block:hover{background-color:#f5f5f5}.panel-icon{display:inline-block;font-size:14px;height:1em;line-height:1em;text-align:center;vertical-align:top;width:1em;color:#7a7a7a;margin-right:.75em}.panel-icon .fa{font-size:inherit;line-height:inherit}.tabs{-webkit-overflow-scrolling:touch;align-items:stretch;display:flex;font-size:1rem;justify-content:space-between;overflow:hidden;overflow-x:auto;white-space:nowrap}.tabs a{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;color:#000;display:flex;justify-content:center;margin-bottom:-1px;padding:.5em 1em;vertical-align:top}.tabs a:hover{border-bottom-color:#363636;color:#363636}.tabs li{display:block}.tabs li.is-active a{border-bottom-color:#3273dc;color:#3273dc}.tabs ul{align-items:center;border-bottom-color:#dbdbdb;border-bottom-style:solid;border-bottom-width:1px;display:flex;flex-grow:1;flex-shrink:0;justify-content:flex-start}.tabs ul.is-left{padding-right:.75em}.tabs ul.is-center{flex:none;justify-content:center;padding-left:.75em;padding-right:.75em}.tabs ul.is-right{justify-content:flex-end;padding-left:.75em}.tabs .icon:first-child{margin-right:.5em}.tabs .icon:last-child{margin-left:.5em}.tabs.is-centered ul{justify-content:center}.tabs.is-right ul{justify-content:flex-end}.tabs.is-boxed a{border:1px solid transparent;border-radius:4px 4px 0 0}.tabs.is-boxed a:hover{background-color:#f5f5f5;border-bottom-color:#dbdbdb}.tabs.is-boxed li.is-active a{background-color:#fff;border-color:#dbdbdb;border-bottom-color:transparent !important}.tabs.is-fullwidth li{flex-grow:1;flex-shrink:0}.tabs.is-toggle a{border-color:#dbdbdb;border-style:solid;border-width:1px;margin-bottom:0;position:relative}.tabs.is-toggle a:hover{background-color:#f5f5f5;border-color:#b5b5b5;z-index:2}.tabs.is-toggle li+li{margin-left:-1px}.tabs.is-toggle li:first-child a{border-top-left-radius:4px;border-bottom-left-radius:4px}.tabs.is-toggle li:last-child a{border-top-right-radius:4px;border-bottom-right-radius:4px}.tabs.is-toggle li.is-active a{background-color:#3273dc;border-color:#3273dc;color:#fff;z-index:1}.tabs.is-toggle ul{border-bottom:none}.tabs.is-toggle.is-toggle-rounded li:first-child a{border-bottom-left-radius:290486px;border-top-left-radius:290486px;padding-left:1.25em}.tabs.is-toggle.is-toggle-rounded li:last-child a{border-bottom-right-radius:290486px;border-top-right-radius:290486px;padding-right:1.25em}.tabs.is-small{font-size:.75rem}.tabs.is-medium{font-size:1.25rem}.tabs.is-large{font-size:1.5rem}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{flex:none}.columns.is-mobile>.column.is-full{flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{flex:none;width:50%}.columns.is-mobile>.column.is-one-third{flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-0{flex:none;width:0%}.columns.is-mobile>.column.is-offset-0{margin-left:0%}.columns.is-mobile>.column.is-1{flex:none;width:8.3333333333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.3333333333%}.columns.is-mobile>.column.is-2{flex:none;width:16.6666666667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.6666666667%}.columns.is-mobile>.column.is-3{flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{flex:none;width:33.3333333333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.3333333333%}.columns.is-mobile>.column.is-5{flex:none;width:41.6666666667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.6666666667%}.columns.is-mobile>.column.is-6{flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{flex:none;width:58.3333333333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.3333333333%}.columns.is-mobile>.column.is-8{flex:none;width:66.6666666667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.6666666667%}.columns.is-mobile>.column.is-9{flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{flex:none;width:83.3333333333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.3333333333%}.columns.is-mobile>.column.is-11{flex:none;width:91.6666666667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.6666666667%}.columns.is-mobile>.column.is-12{flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width: 768px){.column.is-narrow-mobile{flex:none}.column.is-full-mobile{flex:none;width:100%}.column.is-three-quarters-mobile{flex:none;width:75%}.column.is-two-thirds-mobile{flex:none;width:66.6666%}.column.is-half-mobile{flex:none;width:50%}.column.is-one-third-mobile{flex:none;width:33.3333%}.column.is-one-quarter-mobile{flex:none;width:25%}.column.is-one-fifth-mobile{flex:none;width:20%}.column.is-two-fifths-mobile{flex:none;width:40%}.column.is-three-fifths-mobile{flex:none;width:60%}.column.is-four-fifths-mobile{flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-0-mobile{flex:none;width:0%}.column.is-offset-0-mobile{margin-left:0%}.column.is-1-mobile{flex:none;width:8.3333333333%}.column.is-offset-1-mobile{margin-left:8.3333333333%}.column.is-2-mobile{flex:none;width:16.6666666667%}.column.is-offset-2-mobile{margin-left:16.6666666667%}.column.is-3-mobile{flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{flex:none;width:33.3333333333%}.column.is-offset-4-mobile{margin-left:33.3333333333%}.column.is-5-mobile{flex:none;width:41.6666666667%}.column.is-offset-5-mobile{margin-left:41.6666666667%}.column.is-6-mobile{flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{flex:none;width:58.3333333333%}.column.is-offset-7-mobile{margin-left:58.3333333333%}.column.is-8-mobile{flex:none;width:66.6666666667%}.column.is-offset-8-mobile{margin-left:66.6666666667%}.column.is-9-mobile{flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{flex:none;width:83.3333333333%}.column.is-offset-10-mobile{margin-left:83.3333333333%}.column.is-11-mobile{flex:none;width:91.6666666667%}.column.is-offset-11-mobile{margin-left:91.6666666667%}.column.is-12-mobile{flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width: 769px),print{.column.is-narrow,.column.is-narrow-tablet{flex:none}.column.is-full,.column.is-full-tablet{flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-0,.column.is-0-tablet{flex:none;width:0%}.column.is-offset-0,.column.is-offset-0-tablet{margin-left:0%}.column.is-1,.column.is-1-tablet{flex:none;width:8.3333333333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.3333333333%}.column.is-2,.column.is-2-tablet{flex:none;width:16.6666666667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.6666666667%}.column.is-3,.column.is-3-tablet{flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{flex:none;width:33.3333333333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.3333333333%}.column.is-5,.column.is-5-tablet{flex:none;width:41.6666666667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.6666666667%}.column.is-6,.column.is-6-tablet{flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{flex:none;width:58.3333333333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.3333333333%}.column.is-8,.column.is-8-tablet{flex:none;width:66.6666666667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.6666666667%}.column.is-9,.column.is-9-tablet{flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{flex:none;width:83.3333333333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.3333333333%}.column.is-11,.column.is-11-tablet{flex:none;width:91.6666666667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.6666666667%}.column.is-12,.column.is-12-tablet{flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width: 1023px){.column.is-narrow-touch{flex:none}.column.is-full-touch{flex:none;width:100%}.column.is-three-quarters-touch{flex:none;width:75%}.column.is-two-thirds-touch{flex:none;width:66.6666%}.column.is-half-touch{flex:none;width:50%}.column.is-one-third-touch{flex:none;width:33.3333%}.column.is-one-quarter-touch{flex:none;width:25%}.column.is-one-fifth-touch{flex:none;width:20%}.column.is-two-fifths-touch{flex:none;width:40%}.column.is-three-fifths-touch{flex:none;width:60%}.column.is-four-fifths-touch{flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-0-touch{flex:none;width:0%}.column.is-offset-0-touch{margin-left:0%}.column.is-1-touch{flex:none;width:8.3333333333%}.column.is-offset-1-touch{margin-left:8.3333333333%}.column.is-2-touch{flex:none;width:16.6666666667%}.column.is-offset-2-touch{margin-left:16.6666666667%}.column.is-3-touch{flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{flex:none;width:33.3333333333%}.column.is-offset-4-touch{margin-left:33.3333333333%}.column.is-5-touch{flex:none;width:41.6666666667%}.column.is-offset-5-touch{margin-left:41.6666666667%}.column.is-6-touch{flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{flex:none;width:58.3333333333%}.column.is-offset-7-touch{margin-left:58.3333333333%}.column.is-8-touch{flex:none;width:66.6666666667%}.column.is-offset-8-touch{margin-left:66.6666666667%}.column.is-9-touch{flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{flex:none;width:83.3333333333%}.column.is-offset-10-touch{margin-left:83.3333333333%}.column.is-11-touch{flex:none;width:91.6666666667%}.column.is-offset-11-touch{margin-left:91.6666666667%}.column.is-12-touch{flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width: 1024px){.column.is-narrow-desktop{flex:none}.column.is-full-desktop{flex:none;width:100%}.column.is-three-quarters-desktop{flex:none;width:75%}.column.is-two-thirds-desktop{flex:none;width:66.6666%}.column.is-half-desktop{flex:none;width:50%}.column.is-one-third-desktop{flex:none;width:33.3333%}.column.is-one-quarter-desktop{flex:none;width:25%}.column.is-one-fifth-desktop{flex:none;width:20%}.column.is-two-fifths-desktop{flex:none;width:40%}.column.is-three-fifths-desktop{flex:none;width:60%}.column.is-four-fifths-desktop{flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-0-desktop{flex:none;width:0%}.column.is-offset-0-desktop{margin-left:0%}.column.is-1-desktop{flex:none;width:8.3333333333%}.column.is-offset-1-desktop{margin-left:8.3333333333%}.column.is-2-desktop{flex:none;width:16.6666666667%}.column.is-offset-2-desktop{margin-left:16.6666666667%}.column.is-3-desktop{flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{flex:none;width:33.3333333333%}.column.is-offset-4-desktop{margin-left:33.3333333333%}.column.is-5-desktop{flex:none;width:41.6666666667%}.column.is-offset-5-desktop{margin-left:41.6666666667%}.column.is-6-desktop{flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{flex:none;width:58.3333333333%}.column.is-offset-7-desktop{margin-left:58.3333333333%}.column.is-8-desktop{flex:none;width:66.6666666667%}.column.is-offset-8-desktop{margin-left:66.6666666667%}.column.is-9-desktop{flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{flex:none;width:83.3333333333%}.column.is-offset-10-desktop{margin-left:83.3333333333%}.column.is-11-desktop{flex:none;width:91.6666666667%}.column.is-offset-11-desktop{margin-left:91.6666666667%}.column.is-12-desktop{flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width: 1216px){.column.is-narrow-widescreen{flex:none}.column.is-full-widescreen{flex:none;width:100%}.column.is-three-quarters-widescreen{flex:none;width:75%}.column.is-two-thirds-widescreen{flex:none;width:66.6666%}.column.is-half-widescreen{flex:none;width:50%}.column.is-one-third-widescreen{flex:none;width:33.3333%}.column.is-one-quarter-widescreen{flex:none;width:25%}.column.is-one-fifth-widescreen{flex:none;width:20%}.column.is-two-fifths-widescreen{flex:none;width:40%}.column.is-three-fifths-widescreen{flex:none;width:60%}.column.is-four-fifths-widescreen{flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-0-widescreen{flex:none;width:0%}.column.is-offset-0-widescreen{margin-left:0%}.column.is-1-widescreen{flex:none;width:8.3333333333%}.column.is-offset-1-widescreen{margin-left:8.3333333333%}.column.is-2-widescreen{flex:none;width:16.6666666667%}.column.is-offset-2-widescreen{margin-left:16.6666666667%}.column.is-3-widescreen{flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{flex:none;width:33.3333333333%}.column.is-offset-4-widescreen{margin-left:33.3333333333%}.column.is-5-widescreen{flex:none;width:41.6666666667%}.column.is-offset-5-widescreen{margin-left:41.6666666667%}.column.is-6-widescreen{flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{flex:none;width:58.3333333333%}.column.is-offset-7-widescreen{margin-left:58.3333333333%}.column.is-8-widescreen{flex:none;width:66.6666666667%}.column.is-offset-8-widescreen{margin-left:66.6666666667%}.column.is-9-widescreen{flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{flex:none;width:83.3333333333%}.column.is-offset-10-widescreen{margin-left:83.3333333333%}.column.is-11-widescreen{flex:none;width:91.6666666667%}.column.is-offset-11-widescreen{margin-left:91.6666666667%}.column.is-12-widescreen{flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width: 1408px){.column.is-narrow-fullhd{flex:none}.column.is-full-fullhd{flex:none;width:100%}.column.is-three-quarters-fullhd{flex:none;width:75%}.column.is-two-thirds-fullhd{flex:none;width:66.6666%}.column.is-half-fullhd{flex:none;width:50%}.column.is-one-third-fullhd{flex:none;width:33.3333%}.column.is-one-quarter-fullhd{flex:none;width:25%}.column.is-one-fifth-fullhd{flex:none;width:20%}.column.is-two-fifths-fullhd{flex:none;width:40%}.column.is-three-fifths-fullhd{flex:none;width:60%}.column.is-four-fifths-fullhd{flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-0-fullhd{flex:none;width:0%}.column.is-offset-0-fullhd{margin-left:0%}.column.is-1-fullhd{flex:none;width:8.3333333333%}.column.is-offset-1-fullhd{margin-left:8.3333333333%}.column.is-2-fullhd{flex:none;width:16.6666666667%}.column.is-offset-2-fullhd{margin-left:16.6666666667%}.column.is-3-fullhd{flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{flex:none;width:33.3333333333%}.column.is-offset-4-fullhd{margin-left:33.3333333333%}.column.is-5-fullhd{flex:none;width:41.6666666667%}.column.is-offset-5-fullhd{margin-left:41.6666666667%}.column.is-6-fullhd{flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{flex:none;width:58.3333333333%}.column.is-offset-7-fullhd{margin-left:58.3333333333%}.column.is-8-fullhd{flex:none;width:66.6666666667%}.column.is-offset-8-fullhd{margin-left:66.6666666667%}.column.is-9-fullhd{flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{flex:none;width:83.3333333333%}.column.is-offset-10-fullhd{margin-left:83.3333333333%}.column.is-11-fullhd{flex:none;width:91.6666666667%}.column.is-offset-11-fullhd{margin-left:91.6666666667%}.column.is-12-fullhd{flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-0.75rem;margin-right:-0.75rem;margin-top:-0.75rem}.columns:last-child{margin-bottom:-0.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - 0.75rem)}.columns.is-centered{justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0 !important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:flex}.columns.is-multiline{flex-wrap:wrap}.columns.is-vcentered{align-items:center}@media screen and (min-width: 769px),print{.columns:not(.is-desktop){display:flex}}@media screen and (min-width: 1024px){.columns.is-desktop{display:flex}}.columns.is-variable{--columnGap: 0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap: 0rem}@media screen and (max-width: 768px){.columns.is-variable.is-0-mobile{--columnGap: 0rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-0-tablet{--columnGap: 0rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-0-tablet-only{--columnGap: 0rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-0-touch{--columnGap: 0rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-0-desktop{--columnGap: 0rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-0-desktop-only{--columnGap: 0rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-0-widescreen{--columnGap: 0rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-0-widescreen-only{--columnGap: 0rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-0-fullhd{--columnGap: 0rem}}.columns.is-variable.is-1{--columnGap: 0.25rem}@media screen and (max-width: 768px){.columns.is-variable.is-1-mobile{--columnGap: 0.25rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-1-tablet{--columnGap: 0.25rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-1-tablet-only{--columnGap: 0.25rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-1-touch{--columnGap: 0.25rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-1-desktop{--columnGap: 0.25rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-1-desktop-only{--columnGap: 0.25rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-1-widescreen{--columnGap: 0.25rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-1-widescreen-only{--columnGap: 0.25rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-1-fullhd{--columnGap: 0.25rem}}.columns.is-variable.is-2{--columnGap: 0.5rem}@media screen and (max-width: 768px){.columns.is-variable.is-2-mobile{--columnGap: 0.5rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-2-tablet{--columnGap: 0.5rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-2-tablet-only{--columnGap: 0.5rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-2-touch{--columnGap: 0.5rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-2-desktop{--columnGap: 0.5rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-2-desktop-only{--columnGap: 0.5rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-2-widescreen{--columnGap: 0.5rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-2-widescreen-only{--columnGap: 0.5rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-2-fullhd{--columnGap: 0.5rem}}.columns.is-variable.is-3{--columnGap: 0.75rem}@media screen and (max-width: 768px){.columns.is-variable.is-3-mobile{--columnGap: 0.75rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-3-tablet{--columnGap: 0.75rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-3-tablet-only{--columnGap: 0.75rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-3-touch{--columnGap: 0.75rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-3-desktop{--columnGap: 0.75rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-3-desktop-only{--columnGap: 0.75rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-3-widescreen{--columnGap: 0.75rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-3-widescreen-only{--columnGap: 0.75rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-3-fullhd{--columnGap: 0.75rem}}.columns.is-variable.is-4{--columnGap: 1rem}@media screen and (max-width: 768px){.columns.is-variable.is-4-mobile{--columnGap: 1rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-4-tablet{--columnGap: 1rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-4-tablet-only{--columnGap: 1rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-4-touch{--columnGap: 1rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-4-desktop{--columnGap: 1rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-4-desktop-only{--columnGap: 1rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-4-widescreen{--columnGap: 1rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-4-widescreen-only{--columnGap: 1rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-4-fullhd{--columnGap: 1rem}}.columns.is-variable.is-5{--columnGap: 1.25rem}@media screen and (max-width: 768px){.columns.is-variable.is-5-mobile{--columnGap: 1.25rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-5-tablet{--columnGap: 1.25rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-5-tablet-only{--columnGap: 1.25rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-5-touch{--columnGap: 1.25rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-5-desktop{--columnGap: 1.25rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-5-desktop-only{--columnGap: 1.25rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-5-widescreen{--columnGap: 1.25rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-5-widescreen-only{--columnGap: 1.25rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-5-fullhd{--columnGap: 1.25rem}}.columns.is-variable.is-6{--columnGap: 1.5rem}@media screen and (max-width: 768px){.columns.is-variable.is-6-mobile{--columnGap: 1.5rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-6-tablet{--columnGap: 1.5rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-6-tablet-only{--columnGap: 1.5rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-6-touch{--columnGap: 1.5rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-6-desktop{--columnGap: 1.5rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-6-desktop-only{--columnGap: 1.5rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-6-widescreen{--columnGap: 1.5rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-6-widescreen-only{--columnGap: 1.5rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-6-fullhd{--columnGap: 1.5rem}}.columns.is-variable.is-7{--columnGap: 1.75rem}@media screen and (max-width: 768px){.columns.is-variable.is-7-mobile{--columnGap: 1.75rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-7-tablet{--columnGap: 1.75rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-7-tablet-only{--columnGap: 1.75rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-7-touch{--columnGap: 1.75rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-7-desktop{--columnGap: 1.75rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-7-desktop-only{--columnGap: 1.75rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-7-widescreen{--columnGap: 1.75rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-7-widescreen-only{--columnGap: 1.75rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-7-fullhd{--columnGap: 1.75rem}}.columns.is-variable.is-8{--columnGap: 2rem}@media screen and (max-width: 768px){.columns.is-variable.is-8-mobile{--columnGap: 2rem}}@media screen and (min-width: 769px),print{.columns.is-variable.is-8-tablet{--columnGap: 2rem}}@media screen and (min-width: 769px)and (max-width: 1023px){.columns.is-variable.is-8-tablet-only{--columnGap: 2rem}}@media screen and (max-width: 1023px){.columns.is-variable.is-8-touch{--columnGap: 2rem}}@media screen and (min-width: 1024px){.columns.is-variable.is-8-desktop{--columnGap: 2rem}}@media screen and (min-width: 1024px)and (max-width: 1215px){.columns.is-variable.is-8-desktop-only{--columnGap: 2rem}}@media screen and (min-width: 1216px){.columns.is-variable.is-8-widescreen{--columnGap: 2rem}}@media screen and (min-width: 1216px)and (max-width: 1407px){.columns.is-variable.is-8-widescreen-only{--columnGap: 2rem}}@media screen and (min-width: 1408px){.columns.is-variable.is-8-fullhd{--columnGap: 2rem}}.tile{align-items:stretch;display:block;flex-basis:0;flex-grow:1;flex-shrink:1;min-height:min-content}.tile.is-ancestor{margin-left:-0.75rem;margin-right:-0.75rem;margin-top:-0.75rem}.tile.is-ancestor:last-child{margin-bottom:-0.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:.75rem}.tile.is-child{margin:0 !important}.tile.is-parent{padding:.75rem}.tile.is-vertical{flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem !important}@media screen and (min-width: 769px),print{.tile:not(.is-child){display:flex}.tile.is-1{flex:none;width:8.3333333333%}.tile.is-2{flex:none;width:16.6666666667%}.tile.is-3{flex:none;width:25%}.tile.is-4{flex:none;width:33.3333333333%}.tile.is-5{flex:none;width:41.6666666667%}.tile.is-6{flex:none;width:50%}.tile.is-7{flex:none;width:58.3333333333%}.tile.is-8{flex:none;width:66.6666666667%}.tile.is-9{flex:none;width:75%}.tile.is-10{flex:none;width:83.3333333333%}.tile.is-11{flex:none;width:91.6666666667%}.tile.is-12{flex:none;width:100%}}.has-text-white{color:#fff !important}a.has-text-white:hover,a.has-text-white:focus{color:#e6e6e6 !important}.has-background-white{background-color:#fff !important}.has-text-black{color:#0a0a0a !important}a.has-text-black:hover,a.has-text-black:focus{color:#000 !important}.has-background-black{background-color:#0a0a0a !important}.has-text-light{color:#f5f5f5 !important}a.has-text-light:hover,a.has-text-light:focus{color:#dbdbdb !important}.has-background-light{background-color:#f5f5f5 !important}.has-text-dark{color:#363636 !important}a.has-text-dark:hover,a.has-text-dark:focus{color:#1c1c1c !important}.has-background-dark{background-color:#363636 !important}.has-text-primary{color:#00d1b2 !important}a.has-text-primary:hover,a.has-text-primary:focus{color:#009e86 !important}.has-background-primary{background-color:#00d1b2 !important}.has-text-primary-light{color:#ebfffc !important}a.has-text-primary-light:hover,a.has-text-primary-light:focus{color:#b8fff4 !important}.has-background-primary-light{background-color:#ebfffc !important}.has-text-primary-dark{color:#00947e !important}a.has-text-primary-dark:hover,a.has-text-primary-dark:focus{color:#00c7a9 !important}.has-background-primary-dark{background-color:#00947e !important}.has-text-link{color:#3273dc !important}a.has-text-link:hover,a.has-text-link:focus{color:#205bbc !important}.has-background-link{background-color:#3273dc !important}.has-text-link-light{color:#eef3fc !important}a.has-text-link-light:hover,a.has-text-link-light:focus{color:#c2d5f5 !important}.has-background-link-light{background-color:#eef3fc !important}.has-text-link-dark{color:#2160c4 !important}a.has-text-link-dark:hover,a.has-text-link-dark:focus{color:#3b79de !important}.has-background-link-dark{background-color:#2160c4 !important}.has-text-info{color:#3298dc !important}a.has-text-info:hover,a.has-text-info:focus{color:#207dbc !important}.has-background-info{background-color:#3298dc !important}.has-text-info-light{color:#eef6fc !important}a.has-text-info-light:hover,a.has-text-info-light:focus{color:#c2e0f5 !important}.has-background-info-light{background-color:#eef6fc !important}.has-text-info-dark{color:#1d72aa !important}a.has-text-info-dark:hover,a.has-text-info-dark:focus{color:#248fd6 !important}.has-background-info-dark{background-color:#1d72aa !important}.has-text-success{color:#48c774 !important}a.has-text-success:hover,a.has-text-success:focus{color:#34a85c !important}.has-background-success{background-color:#48c774 !important}.has-text-success-light{color:#effaf3 !important}a.has-text-success-light:hover,a.has-text-success-light:focus{color:#c8eed6 !important}.has-background-success-light{background-color:#effaf3 !important}.has-text-success-dark{color:#257942 !important}a.has-text-success-dark:hover,a.has-text-success-dark:focus{color:#31a058 !important}.has-background-success-dark{background-color:#257942 !important}.has-text-warning{color:#ffdd57 !important}a.has-text-warning:hover,a.has-text-warning:focus{color:#ffd324 !important}.has-background-warning{background-color:#ffdd57 !important}.has-text-warning-light{color:#fffbeb !important}a.has-text-warning-light:hover,a.has-text-warning-light:focus{color:#fff1b8 !important}.has-background-warning-light{background-color:#fffbeb !important}.has-text-warning-dark{color:#947600 !important}a.has-text-warning-dark:hover,a.has-text-warning-dark:focus{color:#c79f00 !important}.has-background-warning-dark{background-color:#947600 !important}.has-text-danger{color:#f14668 !important}a.has-text-danger:hover,a.has-text-danger:focus{color:#ee1742 !important}.has-background-danger{background-color:#f14668 !important}.has-text-danger-light{color:#feecf0 !important}a.has-text-danger-light:hover,a.has-text-danger-light:focus{color:#fabdc9 !important}.has-background-danger-light{background-color:#feecf0 !important}.has-text-danger-dark{color:#cc0f35 !important}a.has-text-danger-dark:hover,a.has-text-danger-dark:focus{color:#ee2049 !important}.has-background-danger-dark{background-color:#cc0f35 !important}.has-text-black-bis{color:#121212 !important}.has-background-black-bis{background-color:#121212 !important}.has-text-black-ter{color:#242424 !important}.has-background-black-ter{background-color:#242424 !important}.has-text-grey-darker{color:#363636 !important}.has-background-grey-darker{background-color:#363636 !important}.has-text-grey-dark{color:#4a4a4a !important}.has-background-grey-dark{background-color:#4a4a4a !important}.has-text-grey{color:#7a7a7a !important}.has-background-grey{background-color:#7a7a7a !important}.has-text-grey-light{color:#b5b5b5 !important}.has-background-grey-light{background-color:#b5b5b5 !important}.has-text-grey-lighter{color:#dbdbdb !important}.has-background-grey-lighter{background-color:#dbdbdb !important}.has-text-white-ter{color:#f5f5f5 !important}.has-background-white-ter{background-color:#f5f5f5 !important}.has-text-white-bis{color:#fafafa !important}.has-background-white-bis{background-color:#fafafa !important}.is-clearfix::after{clear:both;content:" ";display:table}.is-pulled-left{float:left !important}.is-pulled-right{float:right !important}.is-radiusless{border-radius:0 !important}.is-shadowless{box-shadow:none !important}.is-clipped{overflow:hidden !important}.is-relative{position:relative !important}.is-marginless{margin:0 !important}.is-paddingless{padding:0 !important}.mt-0{margin-top:0 !important}.mr-0{margin-right:0 !important}.mb-0{margin-bottom:0 !important}.ml-0{margin-left:0 !important}.mx-0{margin-left:0 !important;margin-right:0 !important}.my-0{margin-top:0 !important;margin-bottom:0 !important}.mt-1{margin-top:.25rem !important}.mr-1{margin-right:.25rem !important}.mb-1{margin-bottom:.25rem !important}.ml-1{margin-left:.25rem !important}.mx-1{margin-left:.25rem !important;margin-right:.25rem !important}.my-1{margin-top:.25rem !important;margin-bottom:.25rem !important}.mt-2{margin-top:.5rem !important}.mr-2{margin-right:.5rem !important}.mb-2{margin-bottom:.5rem !important}.ml-2{margin-left:.5rem !important}.mx-2{margin-left:.5rem !important;margin-right:.5rem !important}.my-2{margin-top:.5rem !important;margin-bottom:.5rem !important}.mt-3{margin-top:.75rem !important}.mr-3{margin-right:.75rem !important}.mb-3{margin-bottom:.75rem !important}.ml-3{margin-left:.75rem !important}.mx-3{margin-left:.75rem !important;margin-right:.75rem !important}.my-3{margin-top:.75rem !important;margin-bottom:.75rem !important}.mt-4{margin-top:1rem !important}.mr-4{margin-right:1rem !important}.mb-4{margin-bottom:1rem !important}.ml-4{margin-left:1rem !important}.mx-4{margin-left:1rem !important;margin-right:1rem !important}.my-4{margin-top:1rem !important;margin-bottom:1rem !important}.mt-5{margin-top:1.5rem !important}.mr-5{margin-right:1.5rem !important}.mb-5{margin-bottom:1.5rem !important}.ml-5{margin-left:1.5rem !important}.mx-5{margin-left:1.5rem !important;margin-right:1.5rem !important}.my-5{margin-top:1.5rem !important;margin-bottom:1.5rem !important}.mt-6{margin-top:3rem !important}.mr-6{margin-right:3rem !important}.mb-6{margin-bottom:3rem !important}.ml-6{margin-left:3rem !important}.mx-6{margin-left:3rem !important;margin-right:3rem !important}.my-6{margin-top:3rem !important;margin-bottom:3rem !important}.pt-0{padding-top:0 !important}.pr-0{padding-right:0 !important}.pb-0{padding-bottom:0 !important}.pl-0{padding-left:0 !important}.px-0{padding-left:0 !important;padding-right:0 !important}.py-0{padding-top:0 !important;padding-bottom:0 !important}.pt-1{padding-top:.25rem !important}.pr-1{padding-right:.25rem !important}.pb-1{padding-bottom:.25rem !important}.pl-1{padding-left:.25rem !important}.px-1{padding-left:.25rem !important;padding-right:.25rem !important}.py-1{padding-top:.25rem !important;padding-bottom:.25rem !important}.pt-2{padding-top:.5rem !important}.pr-2{padding-right:.5rem !important}.pb-2{padding-bottom:.5rem !important}.pl-2{padding-left:.5rem !important}.px-2{padding-left:.5rem !important;padding-right:.5rem !important}.py-2{padding-top:.5rem !important;padding-bottom:.5rem !important}.pt-3{padding-top:.75rem !important}.pr-3{padding-right:.75rem !important}.pb-3{padding-bottom:.75rem !important}.pl-3{padding-left:.75rem !important}.px-3{padding-left:.75rem !important;padding-right:.75rem !important}.py-3{padding-top:.75rem !important;padding-bottom:.75rem !important}.pt-4{padding-top:1rem !important}.pr-4{padding-right:1rem !important}.pb-4{padding-bottom:1rem !important}.pl-4{padding-left:1rem !important}.px-4{padding-left:1rem !important;padding-right:1rem !important}.py-4{padding-top:1rem !important;padding-bottom:1rem !important}.pt-5{padding-top:1.5rem !important}.pr-5{padding-right:1.5rem !important}.pb-5{padding-bottom:1.5rem !important}.pl-5{padding-left:1.5rem !important}.px-5{padding-left:1.5rem !important;padding-right:1.5rem !important}.py-5{padding-top:1.5rem !important;padding-bottom:1.5rem !important}.pt-6{padding-top:3rem !important}.pr-6{padding-right:3rem !important}.pb-6{padding-bottom:3rem !important}.pl-6{padding-left:3rem !important}.px-6{padding-left:3rem !important;padding-right:3rem !important}.py-6{padding-top:3rem !important;padding-bottom:3rem !important}.is-size-1{font-size:3rem !important}.is-size-2{font-size:2.5rem !important}.is-size-3{font-size:2rem !important}.is-size-4{font-size:1.5rem !important}.is-size-5{font-size:1.25rem !important}.is-size-6{font-size:1rem !important}.is-size-7{font-size:.75rem !important}@media screen and (max-width: 768px){.is-size-1-mobile{font-size:3rem !important}.is-size-2-mobile{font-size:2.5rem !important}.is-size-3-mobile{font-size:2rem !important}.is-size-4-mobile{font-size:1.5rem !important}.is-size-5-mobile{font-size:1.25rem !important}.is-size-6-mobile{font-size:1rem !important}.is-size-7-mobile{font-size:.75rem !important}}@media screen and (min-width: 769px),print{.is-size-1-tablet{font-size:3rem !important}.is-size-2-tablet{font-size:2.5rem !important}.is-size-3-tablet{font-size:2rem !important}.is-size-4-tablet{font-size:1.5rem !important}.is-size-5-tablet{font-size:1.25rem !important}.is-size-6-tablet{font-size:1rem !important}.is-size-7-tablet{font-size:.75rem !important}}@media screen and (max-width: 1023px){.is-size-1-touch{font-size:3rem !important}.is-size-2-touch{font-size:2.5rem !important}.is-size-3-touch{font-size:2rem !important}.is-size-4-touch{font-size:1.5rem !important}.is-size-5-touch{font-size:1.25rem !important}.is-size-6-touch{font-size:1rem !important}.is-size-7-touch{font-size:.75rem !important}}@media screen and (min-width: 1024px){.is-size-1-desktop{font-size:3rem !important}.is-size-2-desktop{font-size:2.5rem !important}.is-size-3-desktop{font-size:2rem !important}.is-size-4-desktop{font-size:1.5rem !important}.is-size-5-desktop{font-size:1.25rem !important}.is-size-6-desktop{font-size:1rem !important}.is-size-7-desktop{font-size:.75rem !important}}@media screen and (min-width: 1216px){.is-size-1-widescreen{font-size:3rem !important}.is-size-2-widescreen{font-size:2.5rem !important}.is-size-3-widescreen{font-size:2rem !important}.is-size-4-widescreen{font-size:1.5rem !important}.is-size-5-widescreen{font-size:1.25rem !important}.is-size-6-widescreen{font-size:1rem !important}.is-size-7-widescreen{font-size:.75rem !important}}@media screen and (min-width: 1408px){.is-size-1-fullhd{font-size:3rem !important}.is-size-2-fullhd{font-size:2.5rem !important}.is-size-3-fullhd{font-size:2rem !important}.is-size-4-fullhd{font-size:1.5rem !important}.is-size-5-fullhd{font-size:1.25rem !important}.is-size-6-fullhd{font-size:1rem !important}.is-size-7-fullhd{font-size:.75rem !important}}.has-text-centered{text-align:center !important}.has-text-justified{text-align:justify !important}.has-text-left{text-align:left !important}.has-text-right{text-align:right !important}@media screen and (max-width: 768px){.has-text-centered-mobile{text-align:center !important}}@media screen and (min-width: 769px),print{.has-text-centered-tablet{text-align:center !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.has-text-centered-tablet-only{text-align:center !important}}@media screen and (max-width: 1023px){.has-text-centered-touch{text-align:center !important}}@media screen and (min-width: 1024px){.has-text-centered-desktop{text-align:center !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.has-text-centered-desktop-only{text-align:center !important}}@media screen and (min-width: 1216px){.has-text-centered-widescreen{text-align:center !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.has-text-centered-widescreen-only{text-align:center !important}}@media screen and (min-width: 1408px){.has-text-centered-fullhd{text-align:center !important}}@media screen and (max-width: 768px){.has-text-justified-mobile{text-align:justify !important}}@media screen and (min-width: 769px),print{.has-text-justified-tablet{text-align:justify !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.has-text-justified-tablet-only{text-align:justify !important}}@media screen and (max-width: 1023px){.has-text-justified-touch{text-align:justify !important}}@media screen and (min-width: 1024px){.has-text-justified-desktop{text-align:justify !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.has-text-justified-desktop-only{text-align:justify !important}}@media screen and (min-width: 1216px){.has-text-justified-widescreen{text-align:justify !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.has-text-justified-widescreen-only{text-align:justify !important}}@media screen and (min-width: 1408px){.has-text-justified-fullhd{text-align:justify !important}}@media screen and (max-width: 768px){.has-text-left-mobile{text-align:left !important}}@media screen and (min-width: 769px),print{.has-text-left-tablet{text-align:left !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.has-text-left-tablet-only{text-align:left !important}}@media screen and (max-width: 1023px){.has-text-left-touch{text-align:left !important}}@media screen and (min-width: 1024px){.has-text-left-desktop{text-align:left !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.has-text-left-desktop-only{text-align:left !important}}@media screen and (min-width: 1216px){.has-text-left-widescreen{text-align:left !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.has-text-left-widescreen-only{text-align:left !important}}@media screen and (min-width: 1408px){.has-text-left-fullhd{text-align:left !important}}@media screen and (max-width: 768px){.has-text-right-mobile{text-align:right !important}}@media screen and (min-width: 769px),print{.has-text-right-tablet{text-align:right !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.has-text-right-tablet-only{text-align:right !important}}@media screen and (max-width: 1023px){.has-text-right-touch{text-align:right !important}}@media screen and (min-width: 1024px){.has-text-right-desktop{text-align:right !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.has-text-right-desktop-only{text-align:right !important}}@media screen and (min-width: 1216px){.has-text-right-widescreen{text-align:right !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.has-text-right-widescreen-only{text-align:right !important}}@media screen and (min-width: 1408px){.has-text-right-fullhd{text-align:right !important}}.is-capitalized{text-transform:capitalize !important}.is-lowercase{text-transform:lowercase !important}.is-uppercase{text-transform:uppercase !important}.is-italic{font-style:italic !important}.has-text-weight-light{font-weight:300 !important}.has-text-weight-normal{font-weight:400 !important}.has-text-weight-medium{font-weight:500 !important}.has-text-weight-semibold{font-weight:600 !important}.has-text-weight-bold{font-weight:700 !important}.is-family-primary{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif !important}.is-family-secondary{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif !important}.is-family-sans-serif{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI","Roboto","Oxygen","Ubuntu","Cantarell","Fira Sans","Droid Sans","Helvetica Neue","Helvetica","Arial",sans-serif !important}.is-family-monospace{font-family:monospace !important}.is-family-code{font-family:monospace !important}.is-block{display:block !important}@media screen and (max-width: 768px){.is-block-mobile{display:block !important}}@media screen and (min-width: 769px),print{.is-block-tablet{display:block !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.is-block-tablet-only{display:block !important}}@media screen and (max-width: 1023px){.is-block-touch{display:block !important}}@media screen and (min-width: 1024px){.is-block-desktop{display:block !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.is-block-desktop-only{display:block !important}}@media screen and (min-width: 1216px){.is-block-widescreen{display:block !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.is-block-widescreen-only{display:block !important}}@media screen and (min-width: 1408px){.is-block-fullhd{display:block !important}}.is-flex{display:flex !important}@media screen and (max-width: 768px){.is-flex-mobile{display:flex !important}}@media screen and (min-width: 769px),print{.is-flex-tablet{display:flex !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.is-flex-tablet-only{display:flex !important}}@media screen and (max-width: 1023px){.is-flex-touch{display:flex !important}}@media screen and (min-width: 1024px){.is-flex-desktop{display:flex !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.is-flex-desktop-only{display:flex !important}}@media screen and (min-width: 1216px){.is-flex-widescreen{display:flex !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.is-flex-widescreen-only{display:flex !important}}@media screen and (min-width: 1408px){.is-flex-fullhd{display:flex !important}}.is-inline{display:inline !important}@media screen and (max-width: 768px){.is-inline-mobile{display:inline !important}}@media screen and (min-width: 769px),print{.is-inline-tablet{display:inline !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.is-inline-tablet-only{display:inline !important}}@media screen and (max-width: 1023px){.is-inline-touch{display:inline !important}}@media screen and (min-width: 1024px){.is-inline-desktop{display:inline !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.is-inline-desktop-only{display:inline !important}}@media screen and (min-width: 1216px){.is-inline-widescreen{display:inline !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.is-inline-widescreen-only{display:inline !important}}@media screen and (min-width: 1408px){.is-inline-fullhd{display:inline !important}}.is-inline-block{display:inline-block !important}@media screen and (max-width: 768px){.is-inline-block-mobile{display:inline-block !important}}@media screen and (min-width: 769px),print{.is-inline-block-tablet{display:inline-block !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.is-inline-block-tablet-only{display:inline-block !important}}@media screen and (max-width: 1023px){.is-inline-block-touch{display:inline-block !important}}@media screen and (min-width: 1024px){.is-inline-block-desktop{display:inline-block !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.is-inline-block-desktop-only{display:inline-block !important}}@media screen and (min-width: 1216px){.is-inline-block-widescreen{display:inline-block !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.is-inline-block-widescreen-only{display:inline-block !important}}@media screen and (min-width: 1408px){.is-inline-block-fullhd{display:inline-block !important}}.is-inline-flex{display:inline-flex !important}@media screen and (max-width: 768px){.is-inline-flex-mobile{display:inline-flex !important}}@media screen and (min-width: 769px),print{.is-inline-flex-tablet{display:inline-flex !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.is-inline-flex-tablet-only{display:inline-flex !important}}@media screen and (max-width: 1023px){.is-inline-flex-touch{display:inline-flex !important}}@media screen and (min-width: 1024px){.is-inline-flex-desktop{display:inline-flex !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.is-inline-flex-desktop-only{display:inline-flex !important}}@media screen and (min-width: 1216px){.is-inline-flex-widescreen{display:inline-flex !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.is-inline-flex-widescreen-only{display:inline-flex !important}}@media screen and (min-width: 1408px){.is-inline-flex-fullhd{display:inline-flex !important}}.is-hidden{display:none !important}.is-sr-only{border:none !important;clip:rect(0, 0, 0, 0) !important;height:.01em !important;overflow:hidden !important;padding:0 !important;position:absolute !important;white-space:nowrap !important;width:.01em !important}@media screen and (max-width: 768px){.is-hidden-mobile{display:none !important}}@media screen and (min-width: 769px),print{.is-hidden-tablet{display:none !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.is-hidden-tablet-only{display:none !important}}@media screen and (max-width: 1023px){.is-hidden-touch{display:none !important}}@media screen and (min-width: 1024px){.is-hidden-desktop{display:none !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.is-hidden-desktop-only{display:none !important}}@media screen and (min-width: 1216px){.is-hidden-widescreen{display:none !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.is-hidden-widescreen-only{display:none !important}}@media screen and (min-width: 1408px){.is-hidden-fullhd{display:none !important}}.is-invisible{visibility:hidden !important}@media screen and (max-width: 768px){.is-invisible-mobile{visibility:hidden !important}}@media screen and (min-width: 769px),print{.is-invisible-tablet{visibility:hidden !important}}@media screen and (min-width: 769px)and (max-width: 1023px){.is-invisible-tablet-only{visibility:hidden !important}}@media screen and (max-width: 1023px){.is-invisible-touch{visibility:hidden !important}}@media screen and (min-width: 1024px){.is-invisible-desktop{visibility:hidden !important}}@media screen and (min-width: 1024px)and (max-width: 1215px){.is-invisible-desktop-only{visibility:hidden !important}}@media screen and (min-width: 1216px){.is-invisible-widescreen{visibility:hidden !important}}@media screen and (min-width: 1216px)and (max-width: 1407px){.is-invisible-widescreen-only{visibility:hidden !important}}@media screen and (min-width: 1408px){.is-invisible-fullhd{visibility:hidden !important}}.hero{align-items:stretch;display:flex;flex-direction:column;justify-content:space-between}.hero .navbar{background:none}.hero .tabs ul{border-bottom:none}.hero.is-white{background-color:#fff;color:#0a0a0a}.hero.is-white a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-white strong{color:inherit}.hero.is-white .title{color:#0a0a0a}.hero.is-white .subtitle{color:rgba(10,10,10,.9)}.hero.is-white .subtitle a:not(.button),.hero.is-white .subtitle strong{color:#0a0a0a}@media screen and (max-width: 1023px){.hero.is-white .navbar-menu{background-color:#fff}}.hero.is-white .navbar-item,.hero.is-white .navbar-link{color:rgba(10,10,10,.7)}.hero.is-white a.navbar-item:hover,.hero.is-white a.navbar-item.is-active,.hero.is-white .navbar-link:hover,.hero.is-white .navbar-link.is-active{background-color:#f2f2f2;color:#0a0a0a}.hero.is-white .tabs a{color:#0a0a0a;opacity:.9}.hero.is-white .tabs a:hover{opacity:1}.hero.is-white .tabs li.is-active a{opacity:1}.hero.is-white .tabs.is-boxed a,.hero.is-white .tabs.is-toggle a{color:#0a0a0a}.hero.is-white .tabs.is-boxed a:hover,.hero.is-white .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-white .tabs.is-boxed li.is-active a,.hero.is-white .tabs.is-boxed li.is-active a:hover,.hero.is-white .tabs.is-toggle li.is-active a,.hero.is-white .tabs.is-toggle li.is-active a:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.hero.is-white.is-bold{background-image:linear-gradient(141deg, #e8e3e4 0%, white 71%, white 100%)}@media screen and (max-width: 768px){.hero.is-white.is-bold .navbar-menu{background-image:linear-gradient(141deg, #e8e3e4 0%, white 71%, white 100%)}}.hero.is-black{background-color:#0a0a0a;color:#fff}.hero.is-black a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-black strong{color:inherit}.hero.is-black .title{color:#fff}.hero.is-black .subtitle{color:rgba(255,255,255,.9)}.hero.is-black .subtitle a:not(.button),.hero.is-black .subtitle strong{color:#fff}@media screen and (max-width: 1023px){.hero.is-black .navbar-menu{background-color:#0a0a0a}}.hero.is-black .navbar-item,.hero.is-black .navbar-link{color:rgba(255,255,255,.7)}.hero.is-black a.navbar-item:hover,.hero.is-black a.navbar-item.is-active,.hero.is-black .navbar-link:hover,.hero.is-black .navbar-link.is-active{background-color:#000;color:#fff}.hero.is-black .tabs a{color:#fff;opacity:.9}.hero.is-black .tabs a:hover{opacity:1}.hero.is-black .tabs li.is-active a{opacity:1}.hero.is-black .tabs.is-boxed a,.hero.is-black .tabs.is-toggle a{color:#fff}.hero.is-black .tabs.is-boxed a:hover,.hero.is-black .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-black .tabs.is-boxed li.is-active a,.hero.is-black .tabs.is-boxed li.is-active a:hover,.hero.is-black .tabs.is-toggle li.is-active a,.hero.is-black .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.hero.is-black.is-bold{background-image:linear-gradient(141deg, black 0%, #0a0a0a 71%, #181616 100%)}@media screen and (max-width: 768px){.hero.is-black.is-bold .navbar-menu{background-image:linear-gradient(141deg, black 0%, #0a0a0a 71%, #181616 100%)}}.hero.is-light{background-color:#f5f5f5;color:rgba(0,0,0,.7)}.hero.is-light a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-light strong{color:inherit}.hero.is-light .title{color:rgba(0,0,0,.7)}.hero.is-light .subtitle{color:rgba(0,0,0,.9)}.hero.is-light .subtitle a:not(.button),.hero.is-light .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width: 1023px){.hero.is-light .navbar-menu{background-color:#f5f5f5}}.hero.is-light .navbar-item,.hero.is-light .navbar-link{color:rgba(0,0,0,.7)}.hero.is-light a.navbar-item:hover,.hero.is-light a.navbar-item.is-active,.hero.is-light .navbar-link:hover,.hero.is-light .navbar-link.is-active{background-color:#e8e8e8;color:rgba(0,0,0,.7)}.hero.is-light .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-light .tabs a:hover{opacity:1}.hero.is-light .tabs li.is-active a{opacity:1}.hero.is-light .tabs.is-boxed a,.hero.is-light .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-light .tabs.is-boxed a:hover,.hero.is-light .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-light .tabs.is-boxed li.is-active a,.hero.is-light .tabs.is-boxed li.is-active a:hover,.hero.is-light .tabs.is-toggle li.is-active a,.hero.is-light .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#f5f5f5}.hero.is-light.is-bold{background-image:linear-gradient(141deg, #dfd8d9 0%, whitesmoke 71%, white 100%)}@media screen and (max-width: 768px){.hero.is-light.is-bold .navbar-menu{background-image:linear-gradient(141deg, #dfd8d9 0%, whitesmoke 71%, white 100%)}}.hero.is-dark{background-color:#363636;color:#fff}.hero.is-dark a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-dark strong{color:inherit}.hero.is-dark .title{color:#fff}.hero.is-dark .subtitle{color:rgba(255,255,255,.9)}.hero.is-dark .subtitle a:not(.button),.hero.is-dark .subtitle strong{color:#fff}@media screen and (max-width: 1023px){.hero.is-dark .navbar-menu{background-color:#363636}}.hero.is-dark .navbar-item,.hero.is-dark .navbar-link{color:rgba(255,255,255,.7)}.hero.is-dark a.navbar-item:hover,.hero.is-dark a.navbar-item.is-active,.hero.is-dark .navbar-link:hover,.hero.is-dark .navbar-link.is-active{background-color:#292929;color:#fff}.hero.is-dark .tabs a{color:#fff;opacity:.9}.hero.is-dark .tabs a:hover{opacity:1}.hero.is-dark .tabs li.is-active a{opacity:1}.hero.is-dark .tabs.is-boxed a,.hero.is-dark .tabs.is-toggle a{color:#fff}.hero.is-dark .tabs.is-boxed a:hover,.hero.is-dark .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-dark .tabs.is-boxed li.is-active a,.hero.is-dark .tabs.is-boxed li.is-active a:hover,.hero.is-dark .tabs.is-toggle li.is-active a,.hero.is-dark .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#363636}.hero.is-dark.is-bold{background-image:linear-gradient(141deg, #1f191a 0%, #363636 71%, #46403f 100%)}@media screen and (max-width: 768px){.hero.is-dark.is-bold .navbar-menu{background-image:linear-gradient(141deg, #1f191a 0%, #363636 71%, #46403f 100%)}}.hero.is-primary{background-color:#00d1b2;color:#fff}.hero.is-primary a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-primary strong{color:inherit}.hero.is-primary .title{color:#fff}.hero.is-primary .subtitle{color:rgba(255,255,255,.9)}.hero.is-primary .subtitle a:not(.button),.hero.is-primary .subtitle strong{color:#fff}@media screen and (max-width: 1023px){.hero.is-primary .navbar-menu{background-color:#00d1b2}}.hero.is-primary .navbar-item,.hero.is-primary .navbar-link{color:rgba(255,255,255,.7)}.hero.is-primary a.navbar-item:hover,.hero.is-primary a.navbar-item.is-active,.hero.is-primary .navbar-link:hover,.hero.is-primary .navbar-link.is-active{background-color:#00b89c;color:#fff}.hero.is-primary .tabs a{color:#fff;opacity:.9}.hero.is-primary .tabs a:hover{opacity:1}.hero.is-primary .tabs li.is-active a{opacity:1}.hero.is-primary .tabs.is-boxed a,.hero.is-primary .tabs.is-toggle a{color:#fff}.hero.is-primary .tabs.is-boxed a:hover,.hero.is-primary .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-primary .tabs.is-boxed li.is-active a,.hero.is-primary .tabs.is-boxed li.is-active a:hover,.hero.is-primary .tabs.is-toggle li.is-active a,.hero.is-primary .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#00d1b2}.hero.is-primary.is-bold{background-image:linear-gradient(141deg, #009e6c 0%, #00d1b2 71%, #00e7eb 100%)}@media screen and (max-width: 768px){.hero.is-primary.is-bold .navbar-menu{background-image:linear-gradient(141deg, #009e6c 0%, #00d1b2 71%, #00e7eb 100%)}}.hero.is-link{background-color:#3273dc;color:#fff}.hero.is-link a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-link strong{color:inherit}.hero.is-link .title{color:#fff}.hero.is-link .subtitle{color:rgba(255,255,255,.9)}.hero.is-link .subtitle a:not(.button),.hero.is-link .subtitle strong{color:#fff}@media screen and (max-width: 1023px){.hero.is-link .navbar-menu{background-color:#3273dc}}.hero.is-link .navbar-item,.hero.is-link .navbar-link{color:rgba(255,255,255,.7)}.hero.is-link a.navbar-item:hover,.hero.is-link a.navbar-item.is-active,.hero.is-link .navbar-link:hover,.hero.is-link .navbar-link.is-active{background-color:#2366d1;color:#fff}.hero.is-link .tabs a{color:#fff;opacity:.9}.hero.is-link .tabs a:hover{opacity:1}.hero.is-link .tabs li.is-active a{opacity:1}.hero.is-link .tabs.is-boxed a,.hero.is-link .tabs.is-toggle a{color:#fff}.hero.is-link .tabs.is-boxed a:hover,.hero.is-link .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-link .tabs.is-boxed li.is-active a,.hero.is-link .tabs.is-boxed li.is-active a:hover,.hero.is-link .tabs.is-toggle li.is-active a,.hero.is-link .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3273dc}.hero.is-link.is-bold{background-image:linear-gradient(141deg, #1577c6 0%, #3273dc 71%, #4366e5 100%)}@media screen and (max-width: 768px){.hero.is-link.is-bold .navbar-menu{background-image:linear-gradient(141deg, #1577c6 0%, #3273dc 71%, #4366e5 100%)}}.hero.is-info{background-color:#3298dc;color:#fff}.hero.is-info a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-info strong{color:inherit}.hero.is-info .title{color:#fff}.hero.is-info .subtitle{color:rgba(255,255,255,.9)}.hero.is-info .subtitle a:not(.button),.hero.is-info .subtitle strong{color:#fff}@media screen and (max-width: 1023px){.hero.is-info .navbar-menu{background-color:#3298dc}}.hero.is-info .navbar-item,.hero.is-info .navbar-link{color:rgba(255,255,255,.7)}.hero.is-info a.navbar-item:hover,.hero.is-info a.navbar-item.is-active,.hero.is-info .navbar-link:hover,.hero.is-info .navbar-link.is-active{background-color:#238cd1;color:#fff}.hero.is-info .tabs a{color:#fff;opacity:.9}.hero.is-info .tabs a:hover{opacity:1}.hero.is-info .tabs li.is-active a{opacity:1}.hero.is-info .tabs.is-boxed a,.hero.is-info .tabs.is-toggle a{color:#fff}.hero.is-info .tabs.is-boxed a:hover,.hero.is-info .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-info .tabs.is-boxed li.is-active a,.hero.is-info .tabs.is-boxed li.is-active a:hover,.hero.is-info .tabs.is-toggle li.is-active a,.hero.is-info .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#3298dc}.hero.is-info.is-bold{background-image:linear-gradient(141deg, #159dc6 0%, #3298dc 71%, #4389e5 100%)}@media screen and (max-width: 768px){.hero.is-info.is-bold .navbar-menu{background-image:linear-gradient(141deg, #159dc6 0%, #3298dc 71%, #4389e5 100%)}}.hero.is-success{background-color:#48c774;color:#fff}.hero.is-success a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-success strong{color:inherit}.hero.is-success .title{color:#fff}.hero.is-success .subtitle{color:rgba(255,255,255,.9)}.hero.is-success .subtitle a:not(.button),.hero.is-success .subtitle strong{color:#fff}@media screen and (max-width: 1023px){.hero.is-success .navbar-menu{background-color:#48c774}}.hero.is-success .navbar-item,.hero.is-success .navbar-link{color:rgba(255,255,255,.7)}.hero.is-success a.navbar-item:hover,.hero.is-success a.navbar-item.is-active,.hero.is-success .navbar-link:hover,.hero.is-success .navbar-link.is-active{background-color:#3abb67;color:#fff}.hero.is-success .tabs a{color:#fff;opacity:.9}.hero.is-success .tabs a:hover{opacity:1}.hero.is-success .tabs li.is-active a{opacity:1}.hero.is-success .tabs.is-boxed a,.hero.is-success .tabs.is-toggle a{color:#fff}.hero.is-success .tabs.is-boxed a:hover,.hero.is-success .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-success .tabs.is-boxed li.is-active a,.hero.is-success .tabs.is-boxed li.is-active a:hover,.hero.is-success .tabs.is-toggle li.is-active a,.hero.is-success .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#48c774}.hero.is-success.is-bold{background-image:linear-gradient(141deg, #29b342 0%, #48c774 71%, #56d296 100%)}@media screen and (max-width: 768px){.hero.is-success.is-bold .navbar-menu{background-image:linear-gradient(141deg, #29b342 0%, #48c774 71%, #56d296 100%)}}.hero.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.hero.is-warning a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-warning strong{color:inherit}.hero.is-warning .title{color:rgba(0,0,0,.7)}.hero.is-warning .subtitle{color:rgba(0,0,0,.9)}.hero.is-warning .subtitle a:not(.button),.hero.is-warning .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width: 1023px){.hero.is-warning .navbar-menu{background-color:#ffdd57}}.hero.is-warning .navbar-item,.hero.is-warning .navbar-link{color:rgba(0,0,0,.7)}.hero.is-warning a.navbar-item:hover,.hero.is-warning a.navbar-item.is-active,.hero.is-warning .navbar-link:hover,.hero.is-warning .navbar-link.is-active{background-color:#ffd83d;color:rgba(0,0,0,.7)}.hero.is-warning .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-warning .tabs a:hover{opacity:1}.hero.is-warning .tabs li.is-active a{opacity:1}.hero.is-warning .tabs.is-boxed a,.hero.is-warning .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-warning .tabs.is-boxed a:hover,.hero.is-warning .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-warning .tabs.is-boxed li.is-active a,.hero.is-warning .tabs.is-boxed li.is-active a:hover,.hero.is-warning .tabs.is-toggle li.is-active a,.hero.is-warning .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#ffdd57}.hero.is-warning.is-bold{background-image:linear-gradient(141deg, #ffaf24 0%, #ffdd57 71%, #fffa70 100%)}@media screen and (max-width: 768px){.hero.is-warning.is-bold .navbar-menu{background-image:linear-gradient(141deg, #ffaf24 0%, #ffdd57 71%, #fffa70 100%)}}.hero.is-danger{background-color:#f14668;color:#fff}.hero.is-danger a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current),.hero.is-danger strong{color:inherit}.hero.is-danger .title{color:#fff}.hero.is-danger .subtitle{color:rgba(255,255,255,.9)}.hero.is-danger .subtitle a:not(.button),.hero.is-danger .subtitle strong{color:#fff}@media screen and (max-width: 1023px){.hero.is-danger .navbar-menu{background-color:#f14668}}.hero.is-danger .navbar-item,.hero.is-danger .navbar-link{color:rgba(255,255,255,.7)}.hero.is-danger a.navbar-item:hover,.hero.is-danger a.navbar-item.is-active,.hero.is-danger .navbar-link:hover,.hero.is-danger .navbar-link.is-active{background-color:#ef2e55;color:#fff}.hero.is-danger .tabs a{color:#fff;opacity:.9}.hero.is-danger .tabs a:hover{opacity:1}.hero.is-danger .tabs li.is-active a{opacity:1}.hero.is-danger .tabs.is-boxed a,.hero.is-danger .tabs.is-toggle a{color:#fff}.hero.is-danger .tabs.is-boxed a:hover,.hero.is-danger .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-danger .tabs.is-boxed li.is-active a,.hero.is-danger .tabs.is-boxed li.is-active a:hover,.hero.is-danger .tabs.is-toggle li.is-active a,.hero.is-danger .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#f14668}.hero.is-danger.is-bold{background-image:linear-gradient(141deg, #fa0a62 0%, #f14668 71%, #f7595f 100%)}@media screen and (max-width: 768px){.hero.is-danger.is-bold .navbar-menu{background-image:linear-gradient(141deg, #fa0a62 0%, #f14668 71%, #f7595f 100%)}}.hero.is-small .hero-body{padding:1.5rem}@media screen and (min-width: 769px),print{.hero.is-medium .hero-body{padding:9rem 1.5rem}}@media screen and (min-width: 769px),print{.hero.is-large .hero-body{padding:18rem 1.5rem}}.hero.is-halfheight .hero-body,.hero.is-fullheight .hero-body,.hero.is-fullheight-with-navbar .hero-body{align-items:center;display:flex}.hero.is-halfheight .hero-body>.container,.hero.is-fullheight .hero-body>.container,.hero.is-fullheight-with-navbar .hero-body>.container{flex-grow:1;flex-shrink:1}.hero.is-halfheight{min-height:50vh}.hero.is-fullheight{min-height:100vh}.hero-video{overflow:hidden}.hero-video video{left:50%;min-height:100%;min-width:100%;position:absolute;top:50%;transform:translate3d(-50%, -50%, 0)}.hero-video.is-transparent{opacity:.3}@media screen and (max-width: 768px){.hero-video{display:none}}.hero-buttons{margin-top:1.5rem}@media screen and (max-width: 768px){.hero-buttons .button{display:flex}.hero-buttons .button:not(:last-child){margin-bottom:.75rem}}@media screen and (min-width: 769px),print{.hero-buttons{display:flex;justify-content:center}.hero-buttons .button:not(:last-child){margin-right:1.5rem}}.hero-head,.hero-foot{flex-grow:0;flex-shrink:0}.hero-body{flex-grow:1;flex-shrink:0;padding:3rem 1.5rem}.section{padding:3rem 1.5rem}@media screen and (min-width: 1024px){.section.is-medium{padding:9rem 1.5rem}.section.is-large{padding:18rem 1.5rem}}.footer{background-color:#fafafa;padding:3rem 1.5rem 6rem}html,body{background:#ddcecc;font-size:18px}a{text-decoration:none;color:#a82305}#search-bar{background-color:#3e2263;padding:0 1em;margin-bottom:0}#search-bar #logout-mobile{display:none}@media screen and (max-width: 768px){#search-bar{display:flex;flex-wrap:wrap}#search-bar #logout-mobile{display:flex;flex-direction:row-reverse;justify-content:space-between}#search-bar #logout{display:none}#search-bar #search-input{flex:0 1 100%}}#search-bar :first-child,#search-bar :last-child{justify-content:space-between}#search-bar :last-child{flex-direction:row-reverse}#search-bar input{border-radius:0;margin:10px 0}.highlight{text-decoration:underline;font-weight:bold}.yourlabs-autocomplete ul{list-style:none;padding:0;margin:0}.yourlabs-autocomplete ul li{height:2em;line-height:2em;padding:0}.yourlabs-autocomplete ul li a{color:inherit}.yourlabs-autocomplete ul li.hilight{background:#e8554e}.autocomplete-item{display:block;width:480px;height:100%;padding:2px 10px;margin:0}.autocomplete-header{background:#b497e1}.autocomplete-value,.autocomplete-new,.autocomplete-more{background:#fff}input[type=submit]{background-color:#562f89;color:#fff}input[type=submit]:hover{background-color:#3e2263;color:#fff}.button.is-primary{background-color:#562f89;color:#fff}.button.is-primary:hover{background-color:#3e2263;color:#fff}.notification{padding:.5em 0;font-size:1.2em;text-align:center}.modal-card-head{background-color:#3e2263}.modal-card-head .modal-card-title{color:#fff}/*# sourceMappingURL=bds.css.map */ diff --git a/bds/static/bds/css/bds.css.map b/bds/static/bds/css/bds.css.map deleted file mode 100644 index bcea8c10..00000000 --- a/bds/static/bds/css/bds.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sourceRoot":"","sources":["../../../../shared/static/src/bulma/bulma.sass","../../../../shared/static/src/bulma/sass/utilities/animations.sass","../../../../shared/static/src/bulma/sass/utilities/mixins.sass","../../../../shared/static/src/bulma/sass/utilities/initial-variables.sass","../../../../shared/static/src/bulma/sass/utilities/controls.sass","../../../../shared/static/src/bulma/sass/base/minireset.sass","../../../../shared/static/src/bulma/sass/base/generic.sass","../../src/sass/bds.scss","../../../../shared/static/src/bulma/sass/elements/box.sass","../../../../shared/static/src/bulma/sass/elements/button.sass","../../../../shared/static/src/bulma/sass/elements/container.sass","../../../../shared/static/src/bulma/sass/elements/content.sass","../../../../shared/static/src/bulma/sass/elements/icon.sass","../../../../shared/static/src/bulma/sass/elements/image.sass","../../../../shared/static/src/bulma/sass/elements/notification.sass","../../../../shared/static/src/bulma/sass/elements/progress.sass","../../../../shared/static/src/bulma/sass/elements/table.sass","../../../../shared/static/src/bulma/sass/utilities/derived-variables.scss","../../../../shared/static/src/bulma/sass/elements/tag.sass","../../../../shared/static/src/bulma/sass/elements/title.sass","../../../../shared/static/src/bulma/sass/elements/other.sass","../../../../shared/static/src/bulma/sass/form/shared.sass","../../../../shared/static/src/bulma/sass/form/input-textarea.sass","../../../../shared/static/src/bulma/sass/form/checkbox-radio.sass","../../../../shared/static/src/bulma/sass/form/select.sass","../../../../shared/static/src/bulma/sass/form/file.sass","../../../../shared/static/src/bulma/sass/form/tools.sass","../../../../shared/static/src/bulma/sass/components/breadcrumb.sass","../../../../shared/static/src/bulma/sass/components/card.sass","../../../../shared/static/src/bulma/sass/components/dropdown.sass","../../../../shared/static/src/bulma/sass/components/level.sass","../../../../shared/static/src/bulma/sass/components/media.sass","../../../../shared/static/src/bulma/sass/components/menu.sass","../../../../shared/static/src/bulma/sass/components/message.sass","../../../../shared/static/src/bulma/sass/components/modal.sass","../../../../shared/static/src/bulma/sass/components/navbar.sass","../../../../shared/static/src/bulma/sass/components/pagination.sass","../../../../shared/static/src/bulma/sass/components/panel.sass","../../../../shared/static/src/bulma/sass/components/tabs.sass","../../../../shared/static/src/bulma/sass/grid/columns.sass","../../../../shared/static/src/bulma/sass/grid/tiles.sass","../../../../shared/static/src/bulma/sass/helpers/color.sass","../../../../shared/static/src/bulma/sass/helpers/float.sass","../../../../shared/static/src/bulma/sass/helpers/other.sass","../../../../shared/static/src/bulma/sass/helpers/overflow.sass","../../../../shared/static/src/bulma/sass/helpers/position.sass","../../../../shared/static/src/bulma/sass/helpers/spacing.sass","../../../../shared/static/src/bulma/sass/helpers/typography.sass","../../../../shared/static/src/bulma/sass/helpers/visibility.sass","../../../../shared/static/src/bulma/sass/layout/hero.sass","../../../../shared/static/src/bulma/sass/layout/section.sass","../../../../shared/static/src/bulma/sass/layout/footer.sass"],"names":[],"mappings":"CACA,8DCDA,sBACE,KACE,uBACF,GACE,0BC+JJ,kJANE,2BACA,yBACA,sBACA,qBACA,iBAqBF,yFAfE,6BACA,kBACA,eACA,aACA,YACA,cACA,cACA,qBACA,oBACA,kBACA,QACA,yBACA,wBACA,aAMA,8YACE,cC3IY,ODkNhB,qBAhEE,qBACA,wBACA,mCACA,YACA,cC/He,SDgIf,eACA,oBACA,qBACA,YACA,cACA,YACA,YACA,gBACA,eACA,gBACA,eACA,aACA,kBACA,mBACA,WACA,wEAEE,iBCzMW,KD0MX,WACA,cACA,SACA,kBACA,QACA,0DACA,+BACF,qCACE,WACA,UACF,mCACE,WACA,UACF,kEAEE,mCACF,mCACE,mCAEF,uCACE,YACA,gBACA,eACA,gBACA,eACA,WACF,yCACE,YACA,gBACA,eACA,gBACA,eACA,WACF,uCACE,YACA,gBACA,eACA,gBACA,eACA,WAiBJ,uFAXE,2CACA,yBACA,cCjMe,SDkMf,+BACA,6BACA,WACA,cACA,WACA,kBACA,UAYF,ywBANE,OADgB,EAEhB,KAFgB,EAGhB,kBACA,MAJgB,EAKhB,IALgB,EE7OlB,yIA3BE,qBACA,wBACA,mBACA,6BACA,cDqDO,ICpDP,gBACA,oBACA,UDkBO,KCjBP,OAfe,MAgBf,2BACA,YAhBoB,IAiBpB,eAfyB,kBAgBzB,aAf2B,mBAgB3B,cAhB2B,mBAiB3B,YAlByB,kBAmBzB,kBACA,mBAEA,w3BAIE,aACF,slBAEE,mBCrCJ,2EAEA,yGAuBE,SACA,UAGF,kBAME,eACA,mBAGF,GACE,gBAGF,6BAIE,SAGF,KACE,sBAGA,qBAGE,mBAGJ,UAEE,YACA,eAGF,OACE,SAGF,MACE,yBACA,iBAEF,MAEE,UACA,gCACE,mBC/CJ,KACE,iBHjBa,KGkBb,UAhCU,KAiCV,kCACA,mCACA,UAlCe,MAmCf,WAhCgB,OAiChB,WAhCgB,OAiChB,eApCe,mBAqCf,sBAEF,kDAOE,cAEF,kCAKE,YH5BkB,4JG8BpB,SAEE,6BACA,4BACA,YHjCiB,UGmCnB,KACE,MC9DK,KD+DL,UAzDe,IA0Df,YH1Bc,IG2Bd,YAzDiB,IA6DnB,EACE,MHnDa,QGoDb,eACA,qBACA,SACE,mBACF,QACE,MHzEW,QG2Ef,KACE,iBHrEa,QGsEb,MH3Da,QG4Db,UApEU,OAqEV,YAtEY,OAuEZ,QAxEa,iBA0Ef,GACE,iBH5Ea,QG6Eb,YACA,cACA,OAvEU,IAwEV,OAvEU,SAyEZ,IACE,YACA,eAEF,uCAEE,wBAEF,MACE,UAtFgB,OAwFlB,KACE,mBACA,oBAEF,OACE,MHzGa,QG0Gb,YHpEY,IGwEd,SACE,YAEF,IJzDE,iCI2DA,iBH5Ga,QG6Gb,MCvHK,KDwHL,UAhGc,OAiGd,gBACA,QAjGY,eAkGZ,gBACA,iBACA,SACE,6BACA,mBACA,UAtGiB,IAuGjB,UAGF,kBAEE,mBACA,4CACE,mBACJ,SACE,MHvIW,QKGf,KAEE,iBLIa,KKHb,cL0Da,IKzDb,WAVW,qEAWX,MDXK,KCYL,cACA,QAZY,QAeZ,wBAEE,WAfoB,wDAgBtB,aACE,WAhBqB,oDCuCzB,QAGE,iBNlCa,KMmCb,aNxCa,QMyCb,aLhDqB,IKiDrB,MN9Ca,QM+Cb,eAGA,uBACA,eAlDwB,kBAmDxB,aAlD0B,IAmD1B,cAnD0B,IAoD1B,YArDwB,kBAsDxB,kBACA,mBACA,eACE,cAEA,oFAIE,aACA,YACF,2CPwEA,YOvE0B,mBPuE1B,aOtE0B,MAC1B,2CPqEA,YOpE0B,MPoE1B,aOnE0B,mBAC1B,qCACE,+BACA,gCAEJ,iCAEE,aN3EW,QM4EX,MN/EW,QMgFb,iCAEE,aNlEW,QMmEX,MNnFW,QMoFX,2DACE,6CACJ,iCAEE,aNvFW,QMwFX,MNzFW,QM2Fb,gBACE,6BACA,yBACA,MFjGG,KEkGH,gBA/EqB,UAgFrB,kGAIE,iBN7FS,QM8FT,MNrGS,QMsGX,iDAEE,yBACA,MNzGS,QM0GX,6DAEE,6BACA,yBACA,gBAIF,iBACE,iBAHM,KAIN,yBACA,MAJa,QAKb,mDAEE,yBACA,yBACA,MATW,QAUb,mDAEE,yBACA,MAbW,QAcX,6EACE,8CACJ,mDAEE,yBACA,yBACA,MApBW,QAqBb,+DAEE,iBAxBI,KAyBJ,yBACA,gBACF,6BACE,iBA3BW,QA4BX,MA7BI,KA8BJ,2EAEE,sBACF,uFAEE,iBAlCS,QAmCT,yBACA,gBACA,MAtCE,KAwCJ,mCACE,gEACJ,6BACE,6BACA,aA5CI,KA6CJ,MA7CI,KA8CJ,sJAIE,iBAlDE,KAmDF,aAnDE,KAoDF,MAnDS,QAqDT,+CACE,0DAKA,8NACE,gEACN,uFAEE,6BACA,aAjEE,KAkEF,gBACA,MAnEE,KAoEN,yCACE,6BACA,aArEW,QAsEX,MAtEW,QAuEX,sMAIE,iBA3ES,QA4ET,MA7EE,KAmFA,8QACE,0DACN,+GAEE,6BACA,aAvFS,QAwFT,gBACA,MAzFS,QACf,iBACE,iBAHM,QAIN,yBACA,MAJa,KAKb,mDAEE,yBACA,yBACA,MATW,KAUb,mDAEE,yBACA,MAbW,KAcX,6EACE,2CACJ,mDAEE,sBACA,yBACA,MApBW,KAqBb,+DAEE,iBAxBI,QAyBJ,yBACA,gBACF,6BACE,iBA3BW,KA4BX,MA7BI,QA8BJ,2EAEE,yBACF,uFAEE,iBAlCS,KAmCT,yBACA,gBACA,MAtCE,QAwCJ,mCACE,0DACJ,6BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,sJAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,KAqDT,+CACE,gEAKA,8NACE,0DACN,uFAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,yCACE,6BACA,aArEW,KAsEX,MAtEW,KAuEX,sMAIE,iBA3ES,KA4ET,MA7EE,QAmFA,8QACE,gEACN,+GAEE,6BACA,aAvFS,KAwFT,gBACA,MAzFS,KACf,iBACE,iBAHM,QAIN,yBACA,MAJa,eAKb,mDAEE,sBACA,yBACA,MATW,eAUb,mDAEE,yBACA,MAbW,eAcX,6EACE,8CACJ,mDAEE,yBACA,yBACA,MApBW,eAqBb,+DAEE,iBAxBI,QAyBJ,yBACA,gBACF,6BACE,iBA3BW,eA4BX,MA7BI,QA8BJ,2EAEE,gCACF,uFAEE,iBAlCS,eAmCT,yBACA,gBACA,MAtCE,QAwCJ,mCACE,8EACJ,6BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,sJAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,eAqDT,+CACE,gEAKA,8NACE,8EACN,uFAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,yCACE,6BACA,aArEW,eAsEX,MAtEW,eAuEX,sMAIE,iBA3ES,eA4ET,MA7EE,QAmFA,8QACE,gEACN,+GAEE,6BACA,aAvFS,eAwFT,gBACA,MAzFS,eACf,gBACE,iBAHM,QAIN,yBACA,MAJa,KAKb,iDAEE,yBACA,yBACA,MATW,KAUb,iDAEE,yBACA,MAbW,KAcX,2EACE,2CACJ,iDAEE,yBACA,yBACA,MApBW,KAqBb,6DAEE,iBAxBI,QAyBJ,yBACA,gBACF,4BACE,iBA3BW,KA4BX,MA7BI,QA8BJ,yEAEE,yBACF,qFAEE,iBAlCS,KAmCT,yBACA,gBACA,MAtCE,QAwCJ,kCACE,0DACJ,4BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,kJAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,KAqDT,8CACE,gEAKA,0NACE,0DACN,qFAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,wCACE,6BACA,aArEW,KAsEX,MAtEW,KAuEX,kMAIE,iBA3ES,KA4ET,MA7EE,QAmFA,0QACE,gEACN,6GAEE,6BACA,aAvFS,KAwFT,gBACA,MAzFS,KACf,mBACE,iBAHM,QAIN,yBACA,MAJa,KAKb,uDAEE,yBACA,yBACA,MATW,KAUb,uDAEE,yBACA,MAbW,KAcX,iFACE,4CACJ,uDAEE,yBACA,yBACA,MApBW,KAqBb,mEAEE,iBAxBI,QAyBJ,yBACA,gBACF,+BACE,iBA3BW,KA4BX,MA7BI,QA8BJ,+EAEE,yBACF,2FAEE,iBAlCS,KAmCT,yBACA,gBACA,MAtCE,QAwCJ,qCACE,0DACJ,+BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,8JAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,KAqDT,iDACE,gEAKA,sOACE,0DACN,2FAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,2CACE,6BACA,aArEW,KAsEX,MAtEW,KAuEX,8MAIE,iBA3ES,KA4ET,MA7EE,QAmFA,sRACE,gEACN,mHAEE,6BACA,aAvFS,KAwFT,gBACA,MAzFS,KA8FX,4BACE,iBAHY,QAIZ,MAHW,QAIX,yEAEE,yBACA,yBACA,MARS,QASX,yEAEE,yBACA,yBACA,MAbS,QA5FjB,gBACE,iBAHM,QAIN,yBACA,MAJa,KAKb,iDAEE,yBACA,yBACA,MATW,KAUb,iDAEE,yBACA,MAbW,KAcX,2EACE,6CACJ,iDAEE,yBACA,yBACA,MApBW,KAqBb,6DAEE,iBAxBI,QAyBJ,yBACA,gBACF,4BACE,iBA3BW,KA4BX,MA7BI,QA8BJ,yEAEE,yBACF,qFAEE,iBAlCS,KAmCT,yBACA,gBACA,MAtCE,QAwCJ,kCACE,0DACJ,4BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,kJAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,KAqDT,8CACE,gEAKA,0NACE,0DACN,qFAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,wCACE,6BACA,aArEW,KAsEX,MAtEW,KAuEX,kMAIE,iBA3ES,KA4ET,MA7EE,QAmFA,0QACE,gEACN,6GAEE,6BACA,aAvFS,KAwFT,gBACA,MAzFS,KA8FX,yBACE,iBAHY,QAIZ,MAHW,QAIX,mEAEE,yBACA,yBACA,MARS,QASX,mEAEE,yBACA,yBACA,MAbS,QA5FjB,gBACE,iBAHM,QAIN,yBACA,MAJa,KAKb,iDAEE,yBACA,yBACA,MATW,KAUb,iDAEE,yBACA,MAbW,KAcX,2EACE,6CACJ,iDAEE,yBACA,yBACA,MApBW,KAqBb,6DAEE,iBAxBI,QAyBJ,yBACA,gBACF,4BACE,iBA3BW,KA4BX,MA7BI,QA8BJ,yEAEE,yBACF,qFAEE,iBAlCS,KAmCT,yBACA,gBACA,MAtCE,QAwCJ,kCACE,0DACJ,4BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,kJAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,KAqDT,8CACE,gEAKA,0NACE,0DACN,qFAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,wCACE,6BACA,aArEW,KAsEX,MAtEW,KAuEX,kMAIE,iBA3ES,KA4ET,MA7EE,QAmFA,0QACE,gEACN,6GAEE,6BACA,aAvFS,KAwFT,gBACA,MAzFS,KA8FX,yBACE,iBAHY,QAIZ,MAHW,QAIX,mEAEE,yBACA,yBACA,MARS,QASX,mEAEE,yBACA,yBACA,MAbS,QA5FjB,mBACE,iBAHM,QAIN,yBACA,MAJa,KAKb,uDAEE,yBACA,yBACA,MATW,KAUb,uDAEE,yBACA,MAbW,KAcX,iFACE,6CACJ,uDAEE,yBACA,yBACA,MApBW,KAqBb,mEAEE,iBAxBI,QAyBJ,yBACA,gBACF,+BACE,iBA3BW,KA4BX,MA7BI,QA8BJ,+EAEE,yBACF,2FAEE,iBAlCS,KAmCT,yBACA,gBACA,MAtCE,QAwCJ,qCACE,0DACJ,+BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,8JAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,KAqDT,iDACE,gEAKA,sOACE,0DACN,2FAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,2CACE,6BACA,aArEW,KAsEX,MAtEW,KAuEX,8MAIE,iBA3ES,KA4ET,MA7EE,QAmFA,sRACE,gEACN,mHAEE,6BACA,aAvFS,KAwFT,gBACA,MAzFS,KA8FX,4BACE,iBAHY,QAIZ,MAHW,QAIX,yEAEE,yBACA,yBACA,MARS,QASX,yEAEE,yBACA,yBACA,MAbS,QA5FjB,mBACE,iBAHM,QAIN,yBACA,MAJa,eAKb,uDAEE,yBACA,yBACA,MATW,eAUb,uDAEE,yBACA,MAbW,eAcX,iFACE,6CACJ,uDAEE,yBACA,yBACA,MApBW,eAqBb,mEAEE,iBAxBI,QAyBJ,yBACA,gBACF,+BACE,iBA3BW,eA4BX,MA7BI,QA8BJ,+EAEE,gCACF,2FAEE,iBAlCS,eAmCT,yBACA,gBACA,MAtCE,QAwCJ,qCACE,8EACJ,+BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,8JAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,eAqDT,iDACE,gEAKA,sOACE,8EACN,2FAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,2CACE,6BACA,aArEW,eAsEX,MAtEW,eAuEX,8MAIE,iBA3ES,eA4ET,MA7EE,QAmFA,sRACE,gEACN,mHAEE,6BACA,aAvFS,eAwFT,gBACA,MAzFS,eA8FX,4BACE,iBAHY,QAIZ,MAHW,QAIX,yEAEE,yBACA,yBACA,MARS,QASX,yEAEE,yBACA,yBACA,MAbS,QA5FjB,kBACE,iBAHM,QAIN,yBACA,MAJa,KAKb,qDAEE,yBACA,yBACA,MATW,KAUb,qDAEE,yBACA,MAbW,KAcX,+EACE,6CACJ,qDAEE,yBACA,yBACA,MApBW,KAqBb,iEAEE,iBAxBI,QAyBJ,yBACA,gBACF,8BACE,iBA3BW,KA4BX,MA7BI,QA8BJ,6EAEE,yBACF,yFAEE,iBAlCS,KAmCT,yBACA,gBACA,MAtCE,QAwCJ,oCACE,0DACJ,8BACE,6BACA,aA5CI,QA6CJ,MA7CI,QA8CJ,0JAIE,iBAlDE,QAmDF,aAnDE,QAoDF,MAnDS,KAqDT,gDACE,gEAKA,kOACE,0DACN,yFAEE,6BACA,aAjEE,QAkEF,gBACA,MAnEE,QAoEN,0CACE,6BACA,aArEW,KAsEX,MAtEW,KAuEX,0MAIE,iBA3ES,KA4ET,MA7EE,QAmFA,kRACE,gEACN,iHAEE,6BACA,aAvFS,KAwFT,gBACA,MAzFS,KA8FX,2BACE,iBAHY,QAIZ,MAHW,QAIX,uEAEE,yBACA,yBACA,MARS,QASX,uEAEE,yBACA,yBACA,MAbS,QAenB,iBA9LA,cN+Ba,IM9Bb,iBA+LA,kBA7LA,UNHO,KMkMP,kBA7LA,UNNO,QMqMP,iBA7LA,UNTO,OMyMP,6CAEE,iBN/NW,KMgOX,aNrOW,QMsOX,WApNqB,KAqNrB,QApNsB,GAqNxB,qBACE,aACA,WACF,mBACE,6BACA,oBACA,0BP/OF,kBAKE,2BACA,0BO4OE,6BACJ,kBACE,iBNhPW,QMiPX,aNpPW,QMqPX,MNvPW,QMwPX,gBACA,oBACF,mBACE,cN5La,SM6Lb,gCACA,iCAEJ,SACE,mBACA,aACA,eACA,2BACA,iBACE,oBACA,qDP9HA,aO+H0B,MAC5B,oBACE,sBACF,0BACE,mBAGA,0EAjPF,cN+Ba,IM9Bb,iBAmPE,0EA/OF,UNNO,QMwPL,0EAhPF,UNTO,OM6PH,8CACE,4BACA,yBACF,6CACE,6BACA,0BPrJJ,aOsJ4B,KAC1B,uCPvJF,aOwJ4B,EAC1B,yEAEE,UACF,0LAKE,UACA,wNACE,UACJ,wCACE,YACA,cACN,qBACE,uBAEE,iEACE,mBACA,oBACN,kBACE,yBAEE,8DACE,mBACA,oBChUR,WACE,YACA,cACA,kBACA,WACA,oBACE,eACA,aP4CE,KO3CF,cP2CE,KO1CF,WRsFF,sCQ/FF,WAWI,iBR8FA,sCQ5FA,yBACE,kBR0GF,sCQxGA,qBACE,kBR6FF,sCQ9GJ,WAmBI,kBR0GA,sCQ7HJ,WAqBI,kBCDF,eACE,iBASA,sNACE,kBACJ,wEAME,MRlCW,QQmCX,YREc,IQDd,YAxC0B,MAyC5B,YACE,cACA,mBACA,8BACE,eACJ,YACE,iBACA,sBACA,8BACE,oBACJ,YACE,gBACA,sBACA,8BACE,oBACJ,YACE,iBACA,mBACF,YACE,kBACA,sBACF,YACE,cACA,kBACF,oBACE,iBRvDW,QDmIX,YS3I6B,kBAiE7B,QAhEyB,aAiE3B,YACE,4BTwEA,YSvEwB,IACxB,eACA,wBACE,wBACA,uCACE,4BACF,uCACE,4BACF,uCACE,4BACF,uCACE,4BACN,YACE,wBT0DA,YSzDwB,IACxB,eACA,eACE,uBACA,gBACA,kBACE,uBACN,YTkDE,YSjDwB,IAC1B,gBACE,gBACA,iBACA,kBACA,kCACE,eACF,iCACE,kBACF,oBACE,qBACF,2BACE,kBACJ,aT9CA,iCSgDE,gBACA,QAvGkB,aAwGlB,gBACA,iBACF,0BAEE,cACF,eACE,WACA,oCAEE,OA/GsB,kBAgHtB,aA/G4B,QAgH5B,QA/GuB,WAgHvB,mBACF,kBACE,MRxHS,QQyHT,+BACE,mBAEF,gDAEE,aAtH+B,QAuH/B,MR/HO,QQiIT,gDAEE,aAzH+B,QA0H/B,MRpIO,QQwIL,4EAEE,sBAER,qBACE,aAEJ,kBACE,URhHK,OQiHP,mBACE,URpHK,QQqHP,kBACE,URvHK,OS9BT,MACE,mBACA,oBACA,uBACA,OATgB,OAUhB,MAVgB,OAYhB,eACE,OAZoB,KAapB,MAboB,KActB,gBACE,OAdqB,KAerB,MAfqB,KAgBvB,eACE,OAhBoB,KAiBpB,MAjBoB,KCDxB,OACE,cACA,kBACA,WACE,cACA,YACA,WACA,sBACE,cV6DW,SU5Df,oBACE,WAkBA,wtBAGE,YACA,WACJ,gCAEE,iBACF,eACE,gBACF,eACE,gBACF,eACE,qBACF,eACE,gBACF,gBACE,mBACF,eACE,gBACF,eACE,qBACF,eACE,iBACF,eACE,sBACF,eACE,iBACF,eACE,sBACF,gBACE,sBACF,eACE,iBACF,eACE,iBAGA,gBACE,YACA,WAFF,gBACE,YACA,WAFF,gBACE,YACA,WAFF,gBACE,YACA,WAFF,gBACE,YACA,WAFF,gBACE,YACA,WAFF,kBACE,aACA,YC/DN,cAEE,iBXIa,QWHb,cX2DO,IW1DP,kBAEE,QATuB,8BAYzB,iDACE,mBACA,0BACF,qBACE,mBACF,qCAEE,WXRW,KWSb,uBACE,uBACF,sBZ8HE,MY7Hc,MACd,kBACA,UACF,oEAGE,mBAKA,uBACE,iBAHM,KAIN,MAHa,QACf,uBACE,iBAHM,QAIN,MAHa,KACf,uBACE,iBAHM,QAIN,MAHa,eACf,sBACE,iBAHM,QAIN,MAHa,KACf,yBACE,iBAHM,QAIN,MAHa,KAQX,kCACE,iBAHY,QAIZ,MAHW,QANjB,sBACE,iBAHM,QAIN,MAHa,KAQX,+BACE,iBAHY,QAIZ,MAHW,QANjB,sBACE,iBAHM,QAIN,MAHa,KAQX,+BACE,iBAHY,QAIZ,MAHW,QANjB,yBACE,iBAHM,QAIN,MAHa,KAQX,kCACE,iBAHY,QAIZ,MAHW,QANjB,yBACE,iBAHM,QAIN,MAHa,eAQX,kCACE,iBAHY,QAIZ,MAHW,QANjB,wBACE,iBAHM,QAIN,MAHa,KAQX,iCACE,iBAHY,QAIZ,MAHW,QCtCrB,UAEE,qBACA,wBACA,YACA,cZ4De,SY3Df,cACA,OZwBO,KYvBP,gBACA,UACA,WACA,gCACE,iBZPY,QYQd,kCACE,iBRjBG,KQkBL,6BACE,iBRnBG,KQoBL,oBACE,iBRrBG,KQsBH,YAKE,2CACE,iBAHI,KAIN,sCACE,iBALI,KAMN,6BACE,iBAPI,KAQN,iCACE,mEAPF,2CACE,iBAHI,QAIN,sCACE,iBALI,QAMN,6BACE,iBAPI,QAQN,iCACE,qEAPF,2CACE,iBAHI,QAIN,sCACE,iBALI,QAMN,6BACE,iBAPI,QAQN,iCACE,wEAPF,0CACE,iBAHI,QAIN,qCACE,iBALI,QAMN,4BACE,iBAPI,QAQN,gCACE,qEAPF,6CACE,iBAHI,QAIN,wCACE,iBALI,QAMN,+BACE,iBAPI,QAQN,mCACE,qEAPF,0CACE,iBAHI,QAIN,qCACE,iBALI,QAMN,4BACE,iBAPI,QAQN,gCACE,qEAPF,0CACE,iBAHI,QAIN,qCACE,iBALI,QAMN,4BACE,iBAPI,QAQN,gCACE,qEAPF,6CACE,iBAHI,QAIN,wCACE,iBALI,QAMN,+BACE,iBAPI,QAQN,mCACE,qEAPF,6CACE,iBAHI,QAIN,wCACE,iBALI,QAMN,+BACE,iBAPI,QAQN,mCACE,qEAPF,4CACE,iBAHI,QAIN,uCACE,iBALI,QAMN,8BACE,iBAPI,QAQN,kCACE,qEAEN,wBACE,mBApC8B,KAqC9B,mCACA,iCACA,iCACA,iBZjCY,QYkCZ,mEACA,6BACA,4BACA,0BACA,8CACE,6BACF,2CACE,6BAGJ,mBACE,OZlBK,OYmBP,oBACE,OZtBK,QYuBP,mBACE,OZzBK,OY2BT,6BACE,KACE,2BACF,GACE,6BCzCJ,OAEE,iBbZa,Kaab,MbtBa,QauBb,oBAEE,OA5BgB,kBA6BhB,aA5BsB,QA6BtB,QA5BiB,WA6BjB,mBAKE,sCACE,iBAHM,KAIN,aAJM,KAKN,MAJa,QACf,sCACE,iBAHM,QAIN,aAJM,QAKN,MAJa,KACf,sCACE,iBAHM,QAIN,aAJM,QAKN,MAJa,eACf,oCACE,iBAHM,QAIN,aAJM,QAKN,MAJa,KACf,0CACE,iBAHM,QAIN,aAJM,QAKN,MAJa,KACf,oCACE,iBAHM,QAIN,aAJM,QAKN,MAJa,KACf,oCACE,iBAHM,QAIN,aAJM,QAKN,MAJa,KACf,0CACE,iBAHM,QAIN,aAJM,QAKN,MAJa,KACf,0CACE,iBAHM,QAIN,aAJM,QAKN,MAJa,eACf,wCACE,iBAHM,QAIN,aAJM,QAKN,MAJa,KAMjB,wCACE,mBACA,SACF,4CACE,iBb5BS,Qa6BT,MC5Ba,KD6Bb,0GAEE,mBACJ,8CACE,sBACJ,UACE,MblDW,QamDX,uBACE,mBAEF,sBACE,iBbzCS,Qa0CT,MCzCa,KD0Cb,qDAEE,mBACF,kDAEE,aC/CW,KDgDX,mBACN,aACE,iBA1D0B,YA2D1B,gCAEE,aAlEyB,QAmEzB,MbrES,QasEb,aACE,iBA9D0B,YA+D1B,gCAEE,aAtEyB,QAuEzB,Mb3ES,Qa4Eb,aACE,iBArE0B,YAwEtB,4DAEE,sBAGN,4CAEE,iBAGE,wEAEE,wBACR,oBACE,WAII,qDACE,iBb3FK,Qa+FL,gEACE,iBbhGG,QaiGH,gFACE,iBbnGC,QaqGX,wCAEE,mBAIE,6DACE,iBb3GK,Qa6Gf,iBd7DE,iCcgEA,cACA,kBACA,eE3HF,MACE,mBACA,aACA,eACA,2BACA,WACE,oBACA,4BhBoIA,agBnI0B,MAC5B,iBACE,sBACF,uBACE,mBAGA,qDACE,UfgBG,KedL,qDACE,UfYG,QeXP,kBACE,uBACA,uBACE,oBACA,mBACJ,eACE,yBAEE,sCACE,kBACF,qCACE,eAEJ,sBhB0GA,agBzG0B,EACxB,wChBwGF,YgBvG4B,EAEtB,yBACA,4BAIJ,uCAEI,0BACA,6BAKV,eACE,mBACA,iBf7Ca,Qe8Cb,cfUO,IeTP,MXzDK,KW0DL,oBACA,UfxBO,OeyBP,WACA,uBACA,gBACA,mBACA,oBACA,mBACA,uBhB2EE,YgB1EwB,OhB0ExB,agBzEwB,UAKxB,wBACE,iBAHM,KAIN,MAHa,QACf,wBACE,iBAHM,QAIN,MAHa,KACf,wBACE,iBAHM,QAIN,MAHa,eACf,uBACE,iBAHM,QAIN,MAHa,KACf,0BACE,iBAHM,QAIN,MAHa,KAQX,mCACE,iBAHY,QAIZ,MAHW,QANjB,uBACE,iBAHM,QAIN,MAHa,KAQX,gCACE,iBAHY,QAIZ,MAHW,QANjB,uBACE,iBAHM,QAIN,MAHa,KAQX,gCACE,iBAHY,QAIZ,MAHW,QANjB,0BACE,iBAHM,QAIN,MAHa,KAQX,mCACE,iBAHY,QAIZ,MAHW,QANjB,0BACE,iBAHM,QAIN,MAHa,eAQX,mCACE,iBAHY,QAIZ,MAHW,QANjB,yBACE,iBAHM,QAIN,MAHa,KAQX,kCACE,iBAHY,QAIZ,MAHW,QAKnB,yBACE,UflDK,OemDP,yBACE,UfrDK,KesDP,wBACE,UfxDK,Qe0DL,kDhBkDA,YgBjD0B,ShBiD1B,agBhD0B,QAC1B,kDhB+CA,YgB9C0B,QhB8C1B,agB7C0B,SAC1B,4ChB4CA,YgB3C0B,ShB2C1B,agB1C0B,SAE5B,yBhBwCE,YgB7IgB,IAuGhB,UACA,kBACA,UACA,iEAEE,8BACA,WACA,cACA,SACA,kBACA,QACA,0DACA,+BACF,iCACE,WACA,UACF,gCACE,WACA,UACF,8DAEE,yBACF,gCACE,yBACJ,0BACE,cf5Da,Se+Df,YACE,0BCpHJ,iBAGE,sBACA,kDAEE,oBACF,yBACE,UApBa,MAqBf,yBACE,UArBa,MAsBf,2BACE,sBAEJ,OACE,MhB5Ba,QgB+Bb,UhBHO,KgBIP,YhBKgB,IgBJhB,YAnCkB,MAoClB,cACE,MApCiB,QAqCjB,YApCkB,QAqCpB,kBACE,oBACF,iCACE,WA7BuB,SAiCvB,YACE,UFgFE,KEjFJ,YACE,UFgFE,OEjFJ,YACE,UFgFE,KEjFJ,YACE,UFgFE,OEjFJ,YACE,UFgFE,QEjFJ,YACE,UFgFE,KEjFJ,YACE,UFgFE,OE9ER,UACE,MZnDK,KYsDL,UhBrBO,QgBsBP,YhBjBc,IgBkBd,YA7CqB,KA8CrB,iBACE,MhBvDW,QgBwDX,YhBnBc,IgBoBhB,iCACE,WA/CuB,SAmDvB,eACE,UF8DE,KE/DJ,eACE,UF8DE,OE/DJ,eACE,UF8DE,KE/DJ,eACE,UF8DE,OE/DJ,eACE,UF8DE,QE/DJ,eACE,UF8DE,KE/DJ,eACE,UF8DE,OG7HR,SACE,cACA,eACA,mBACA,kBACA,yBAEF,WAEE,YjB0Bc,IiBzBd,eACA,gBACA,UACA,eACE,cACA,eAKJ,QACE,mBACA,iBjBfa,QiBgBb,cjB0Ce,SiBzCf,oBACA,UjBKO,QiBJP,WACA,uBACA,oBACA,gBACA,qBACA,kBACA,mBCeF,gCAxBE,iBlBda,KkBeb,alBpBa,QkBqBb,clBsCO,IkBrCP,MlB1Ba,QD6DX,sFmBjCA,MA7BsB,kBnB8DtB,iHmBjCA,MA7BsB,kBnB8DtB,mFmBjCA,MA7BsB,kBnB8DtB,kGmBjCA,MA7BsB,kBA8BxB,mHAEE,alB5BW,QkB6Bb,sOAIE,alBpBW,QkBqBX,6CACF,yLAEE,iBlBjCW,QkBkCX,alBlCW,QkBmCX,gBACA,MlBzCW,QD2DX,uTmBhBE,MAjC6B,qBnBiD/B,sXmBhBE,MAjC6B,qBnBiD/B,gTmBhBE,MAjC6B,qBnBiD/B,mVmBhBE,MAjC6B,qBCdnC,iBAEE,WDFa,0CCGb,eACA,WACA,qCACE,gBAIA,mCACE,aAFM,KAGN,gNAIE,8CANJ,mCACE,aAFM,QAGN,gNAIE,2CANJ,mCACE,aAFM,QAGN,gNAIE,8CANJ,iCACE,aAFM,QAGN,wMAIE,2CANJ,uCACE,aAFM,QAGN,gOAIE,4CANJ,iCACE,aAFM,QAGN,wMAIE,6CANJ,iCACE,aAFM,QAGN,wMAIE,6CANJ,uCACE,aAFM,QAGN,gOAIE,6CANJ,uCACE,aAFM,QAGN,gOAIE,6CANJ,qCACE,aAFM,QAGN,wNAIE,6CAEN,mClBsBA,cDwBa,ICvBb,UDPO,OmBdP,qClBuBA,UDXO,QmBVP,mClBuBA,UDdO,OmBNP,2CACE,cACA,WACF,qCACE,eACA,WAIF,kBACE,cnBgCa,SmB/Bb,gDACA,iDACF,iBACE,6BACA,yBACA,gBACA,eACA,gBAEJ,UAEE,cACA,eACA,eACA,QlB7C2B,mBkB8C3B,gBACA,sBACE,WAxDkB,KAyDlB,WAxDkB,IAyDpB,gBACE,eAEF,yBACE,YC/DJ,iBACE,eACA,qBACA,iBACA,kBACA,6BACE,eACF,6BACE,MpBFW,QoBGb,4FAEE,MpBHW,QoBIX,mBAOF,crB6HE,YqB5HwB,KCpB5B,QACE,qBACA,eACA,kBACA,mBACA,0BACE,OpBDa,MoBGb,kDAEE,arBYS,QDkIX,MsB7IgB,QACd,UAEF,0BACE,crBwDW,SDyEb,asBhI2B,IAC7B,eAEE,eACA,cACA,cACA,eACA,aACA,2BACE,aACF,uEAEE,arBfS,QqBgBX,+BtBmHA,csBlH2B,MAC3B,yBACE,YACA,UACA,gCACE,iBAGJ,wDACE,arBjCS,QqBsCT,oCACE,aAHI,KAIN,wBACE,aALI,KAMJ,iEAEE,qBACF,kIAIE,8CAXJ,oCACE,aAHI,QAIN,wBACE,aALI,QAMJ,iEAEE,kBACF,kIAIE,2CAXJ,oCACE,aAHI,QAIN,wBACE,aALI,QAMJ,iEAEE,qBACF,kIAIE,8CAXJ,mCACE,aAHI,QAIN,uBACE,aALI,QAMJ,+DAEE,qBACF,8HAIE,2CAXJ,sCACE,aAHI,QAIN,0BACE,aALI,QAMJ,qEAEE,qBACF,0IAIE,4CAXJ,mCACE,aAHI,QAIN,uBACE,aALI,QAMJ,+DAEE,qBACF,8HAIE,6CAXJ,mCACE,aAHI,QAIN,uBACE,aALI,QAMJ,+DAEE,qBACF,8HAIE,6CAXJ,sCACE,aAHI,QAIN,0BACE,aALI,QAMJ,qEAEE,qBACF,0IAIE,6CAXJ,sCACE,aAHI,QAIN,0BACE,aALI,QAMJ,qEAEE,qBACF,0IAIE,6CAXJ,qCACE,aAHI,QAIN,yBACE,aALI,QAMJ,mEAEE,qBACF,sIAIE,6CAER,iBpBbA,cDwBa,ICvBb,UDPO,OqBqBP,kBpBZA,UDXO,QqByBP,iBpBZA,UDdO,OqB8BL,2BACE,arB1DS,QqB2Db,qBACE,WACA,4BACE,WAEF,0BAEE,aACA,kBtB6EF,MsB5EgB,OACd,WACA,eACF,kCACE,UrB1CG,OqB2CL,mCACE,UrB9CG,QqB+CL,kCACE,UrBjDG,OsBtBT,MAEE,oBACA,aACA,2BACA,kBAMI,yBACE,iBAJI,KAKJ,yBACA,MALW,QAQX,mEACE,yBACA,yBACA,MAXS,QAcX,mEACE,yBACA,0CACA,MAjBS,QAoBX,mEACE,yBACA,yBACA,MAvBS,QAEb,yBACE,iBAJI,QAKJ,yBACA,MALW,KAQX,mEACE,yBACA,yBACA,MAXS,KAcX,mEACE,yBACA,uCACA,MAjBS,KAoBX,mEACE,sBACA,yBACA,MAvBS,KAEb,yBACE,iBAJI,QAKJ,yBACA,MALW,eAQX,mEACE,sBACA,yBACA,MAXS,eAcX,mEACE,yBACA,0CACA,MAjBS,eAoBX,mEACE,yBACA,yBACA,MAvBS,eAEb,wBACE,iBAJI,QAKJ,yBACA,MALW,KAQX,iEACE,yBACA,yBACA,MAXS,KAcX,iEACE,yBACA,uCACA,MAjBS,KAoBX,iEACE,yBACA,yBACA,MAvBS,KAEb,2BACE,iBAJI,QAKJ,yBACA,MALW,KAQX,uEACE,yBACA,yBACA,MAXS,KAcX,uEACE,yBACA,wCACA,MAjBS,KAoBX,uEACE,yBACA,yBACA,MAvBS,KAEb,wBACE,iBAJI,QAKJ,yBACA,MALW,KAQX,iEACE,yBACA,yBACA,MAXS,KAcX,iEACE,yBACA,yCACA,MAjBS,KAoBX,iEACE,yBACA,yBACA,MAvBS,KAEb,wBACE,iBAJI,QAKJ,yBACA,MALW,KAQX,iEACE,yBACA,yBACA,MAXS,KAcX,iEACE,yBACA,yCACA,MAjBS,KAoBX,iEACE,yBACA,yBACA,MAvBS,KAEb,2BACE,iBAJI,QAKJ,yBACA,MALW,KAQX,uEACE,yBACA,yBACA,MAXS,KAcX,uEACE,yBACA,yCACA,MAjBS,KAoBX,uEACE,yBACA,yBACA,MAvBS,KAEb,2BACE,iBAJI,QAKJ,yBACA,MALW,eAQX,uEACE,yBACA,yBACA,MAXS,eAcX,uEACE,yBACA,yCACA,MAjBS,eAoBX,uEACE,yBACA,yBACA,MAvBS,eAEb,0BACE,iBAJI,QAKJ,yBACA,MALW,KAQX,qEACE,yBACA,yBACA,MAXS,KAcX,qEACE,yBACA,yCACA,MAjBS,KAoBX,qEACE,yBACA,yBACA,MAvBS,KAyBjB,eACE,UtBVK,OsBWP,gBACE,UtBdK,QsBgBH,+BACE,eACN,eACE,UtBpBK,OsBsBH,8BACE,eAGJ,yBACE,6BACA,0BACF,0BACE,4BACA,yBAEA,kCACE,kBACF,mCACE,aAEJ,2BACE,sBACF,yBACE,sBACA,YACA,gBACF,0BACE,uBACF,0BACE,aACA,YACA,8BACE,eAEF,uCACE,eAEF,wCACE,eAEF,uCACE,eAEF,kCACE,0BACF,mCACE,0BACA,uBACN,kBACE,uBAEA,+BACE,WACF,8BACE,YACA,eACJ,eACE,yBACA,yBACE,0BACF,0BACE,0BACA,2BACA,SAEN,YACE,oBACA,aACA,eACA,2BACA,gBACA,kBAEE,4BACE,sBACA,MtB1HS,QsB2HX,6BACE,qBAEF,6BACE,yBACA,MtBhIS,QsBiIX,8BACE,qBAEN,YACE,YACA,OACA,UACA,aACA,kBACA,MACA,WAEF,qBAGE,atB5Ia,QsB6Ib,ctBlFO,IsBmFP,cACA,iBACA,kBACA,mBAEF,UACE,iBtBjJa,QsBkJb,MlB5JK,KkB8JP,WACE,atBxJa,QsByJb,aA1JuB,MA2JvB,aA1JuB,cA2JvB,cACA,UA3JoB,KA4JpB,gBACA,mBACA,uBAEF,WACE,mBACA,aACA,WACA,uBvB/BE,auBgCsB,KACxB,UACA,eACE,eC9KJ,OACE,cACA,cACA,UvB6BO,KuB5BP,YvBmCY,IuBlCZ,wBACE,mBAEF,gBACE,UvBwBK,OuBvBP,iBACE,UvBoBK,QuBnBP,gBACE,UvBiBK,OuBfT,MACE,cACA,UvBgBO,OuBfP,kBAGE,eACE,MAFM,KACR,eACE,MAFM,QACR,eACE,MAFM,QACR,cACE,MAFM,QACR,iBACE,MAFM,QACR,cACE,MAFM,QACR,cACE,MAFM,QACR,iBACE,MAFM,QACR,iBACE,MAFM,QACR,gBACE,MAFM,QAOV,wBACE,qBAEF,kBACE,aACA,2BAEE,4CxByGF,awBxG4B,KAExB,wNAGE,gBAEF,sMAII,6BACA,0BAKJ,mMAII,4BACA,yBAQF,iXAEE,UACF,kuBAIE,UACA,0yBACE,UACR,uCACE,YACA,cACJ,sCACE,uBACF,mCACE,yBAEA,gDACE,YACA,cACN,kBACE,aACA,2BACA,2BACE,cACA,4CACE,gBxB+CJ,awB9C4B,OAC1B,uCACE,YACA,cACJ,sCACE,uBACF,mCACE,yBACF,uCACE,eAEE,4HAEE,qBACJ,kDACE,uBACF,wDACE,gBxB9BN,2CwB+BA,qBAEI,cAGJ,oBACE,kBxBzCF,qCwBuCF,aAII,qBxBvCF,2CwBmCF,aAMI,aACA,YACA,cxBgBA,awBfwB,OACxB,iBACA,sBACE,UvB9FG,OuB+FH,mBACF,uBACE,mBACF,uBACE,UvBrGG,QuBsGH,mBACF,sBACE,UvBzGG,OuB0GH,oBAGJ,0BACE,gBxB5DF,2CwB0DF,YAII,aACA,aACA,YACA,cACA,mBACE,gBACF,mBACE,cACA,mCACE,YACF,oCxBbF,awBc4B,QAEhC,SACE,sBACA,WACA,UvB9HO,KuB+HP,kBACA,mBAOM,gLACE,MnB1KH,KmB2KD,4LACE,UvBzIC,OuB0IH,gMACE,UvB7IC,QuB8IH,4LACE,UvBhJC,OuBiJL,6DACE,MvB3KS,QuB4KT,OtBjLW,MsBkLX,oBACA,kBACA,MACA,MtBrLW,MsBsLX,UAEF,sEAEE,atB1LW,MsB2Lb,sCACE,OAEF,wEAEE,ctBhMW,MsBiMb,wCACE,QAEF,2BAEE,6BxBnDF,MwBoDgB,OACd,WACA,UACF,mCACE,UvB1KG,OuB2KL,oCACE,UvB9KG,QuB+KL,mCACE,UvBjLG,OwB1BT,YAGE,UxByBO,KwBxBP,mBACA,cACE,mBACA,MxBMW,QwBLX,aACA,uBACA,gBACA,oBACE,MxBfS,QwBgBb,eACE,mBACA,aACA,6BzBuHA,ayBtH2B,EAEzB,2BACE,MxBvBO,QwBwBP,eACA,oBACJ,0BACE,MxBxBS,QwByBT,YACJ,8BAEE,uBACA,aACA,eACA,2BAEA,8BzBsGA,ayBrG0B,KAC1B,6BzBoGA,YyBnG0B,KAG1B,sDAEE,uBAEF,gDAEE,yBAEJ,qBACE,UxBnBK,OwBoBP,sBACE,UxBvBK,QwBwBP,qBACE,UxB1BK,OwB6BL,8CACE,YAEF,+CACE,YAEF,4CACE,YAEF,iDACE,YCvDN,MACE,iBzBLa,KyBMb,WAnBY,qEAoBZ,MrBnBK,KqBoBL,eACA,kBAEF,aACE,iBAvB6B,YAwB7B,oBACA,WAtBmB,iCAuBnB,aAEF,mBACE,mBACA,MzB5Ba,QyB6Bb,aACA,YACA,YzBOY,IyBNZ,QAhCoB,YAiCpB,+BACE,uBAEJ,kBACE,mBACA,eACA,aACA,uBACA,QAzCoB,YA2CtB,YACE,cACA,kBAEF,cACE,iBA5C8B,YA6C9B,QA5CqB,OA8CvB,aACE,iBA7C6B,YA8C7B,WA7CuB,kBA8CvB,oBACA,aAEF,kBACE,mBACA,aACA,aACA,YACA,cACA,uBACA,QAvDoB,OAwDpB,mC1ByEE,a0BlIqB,kBA+DvB,8BACE,czB9BY,O0B7BhB,UACE,oBACA,kBACA,mBAGE,+EACE,cAEF,kCACE,UACA,QAEF,+BACE,YACA,eA9BoB,IA+BpB,oBACA,SAEN,eACE,a3BiHE,K2BhHY,EACd,UAzCwB,MA0CxB,YAtCwB,IAuCxB,kBACA,SACA,QApCmB,GAsCrB,kBACE,iB1BjCa,K0BkCb,c1BoBO,I0BnBP,WA1CwB,qEA2CxB,eA9CgC,MA+ChC,YA9C6B,MAgD/B,eACE,MtBpDK,KsBqDL,cACA,kBACA,gBACA,qBACA,kBAEF,qC3BkFI,c2BhFuB,KACzB,mBACA,mBACA,WACA,iDACE,iB1BxDW,Q0ByDX,M1BpEW,Q0BqEb,yDACE,iB1BlDW,Q0BmDX,WAEJ,kBACE,iB1BjEc,Q0BkEd,YACA,cACA,WACA,eC9EF,OAEE,mBACA,8BACA,YACE,c3B8DK,I2B7DP,WACE,qBACA,mBAEF,iBACE,aACA,2DAEE,aACF,0CACE,aAEA,8CACE,gB5B2HJ,a4BhJiB,OAuBf,6CACE,Y5B6DN,2C4BnFF,OAyBI,aAEE,mCACE,aAER,YACE,mBACA,aACA,gBACA,YACA,cACA,uBACA,yCAEE,gB5BwCF,qC4BrCE,6BACE,cA7Ce,QA+CrB,yBAEE,gBACA,YACA,cAGE,yEACE,Y5B8BJ,2C4B3BI,mF5BsFF,a4BhJiB,QA6DrB,YACE,mBACA,2B5BkBA,qC4BfE,yBACE,mB5BkBJ,2C4BxBF,YAQI,cAEJ,aACE,mBACA,yB5BYA,2C4BdF,aAKI,cCxEJ,OACE,uBACA,aACA,mBACA,iCACE,qBACF,cACE,0CACA,aACA,mBACA,gFAEE,oBACF,qBACE,kBACA,4BACE,iBACN,cACE,0CACA,WAtBY,KAuBZ,YAvBY,KA0BZ,uBACE,WA1BgB,OA2BhB,YA3BgB,OA6BtB,yBAEE,gBACA,YACA,cAEF,Y7B2GI,a6B/IY,KAuChB,a7BwGI,Y6B/IY,KA0ChB,eACE,gBACA,YACA,cACA,mB7BkCA,qC6B/BA,eACE,iBCjCJ,MACE,U7BkBO,K6BhBP,eACE,U7BgBK,O6BfP,gBACE,U7BYK,Q6BXP,eACE,U7BSK,O6BPT,WACE,YArBsB,KAsBtB,aACE,c7BqCW,I6BpCX,MzB7BG,KyB8BH,cACA,QAzBqB,WA0BrB,mBACE,iB7BvBS,Q6BwBT,M7B/BS,Q6BiCX,uBACE,iB7BlBS,Q6BmBT,MfgCe,Ke9BjB,iB9BqGA,Y8BzIoB,kBAsClB,OAnCoB,M9BsItB,a8BrI4B,MAqChC,YACE,M7BzCa,Q6B0Cb,UApCqB,MAqCrB,eApC0B,KAqC1B,yBACA,8BACE,WAtCiB,IAuCnB,6BACE,cAxCiB,ICKrB,SAEE,iB9BVa,Q8BWb,c9B6CO,I8B5CP,U9BYO,K8BXP,gBACE,mBACF,sDACE,mBACA,0BAEF,kBACE,U9BKK,O8BJP,mBACE,U9BCK,0B8BCL,U9BFK,O8BuBL,kBACE,iBAHc,KAId,kCACE,iBArBI,KAsBJ,MArBW,QAsBb,gCACE,aAxBI,KAkBR,kBACE,iBAHc,QAId,kCACE,iBArBI,QAsBJ,MArBW,KAsBb,gCACE,aAxBI,QAkBR,kBACE,iBAHc,QAId,kCACE,iBArBI,QAsBJ,MArBW,eAsBb,gCACE,aAxBI,QAkBR,iBACE,iBAHc,QAId,iCACE,iBArBI,QAsBJ,MArBW,KAsBb,+BACE,aAxBI,QAkBR,oBACE,iBAbc,QAcd,oCACE,iBArBI,QAsBJ,MArBW,KAsBb,kCACE,aAxBI,QAyBJ,MAjBa,QAUjB,iBACE,iBAbc,QAcd,iCACE,iBArBI,QAsBJ,MArBW,KAsBb,+BACE,aAxBI,QAyBJ,MAjBa,QAUjB,iBACE,iBAbc,QAcd,iCACE,iBArBI,QAsBJ,MArBW,KAsBb,+BACE,aAxBI,QAyBJ,MAjBa,QAUjB,oBACE,iBAbc,QAcd,oCACE,iBArBI,QAsBJ,MArBW,KAsBb,kCACE,aAxBI,QAyBJ,MAjBa,QAUjB,oBACE,iBAbc,QAcd,oCACE,iBArBI,QAsBJ,MArBW,eAsBb,kCACE,aAxBI,QAyBJ,MAjBa,QAUjB,mBACE,iBAbc,QAcd,mCACE,iBArBI,QAsBJ,MArBW,KAsBb,iCACE,aAxBI,QAyBJ,MAjBa,QAmBrB,gBACE,mBACA,iB1BlEK,K0BmEL,0BACA,MhBbY,KgBcZ,aACA,Y9B7BY,I8B8BZ,8BACA,iBACA,QAtEuB,UAuEvB,kBACA,wBACE,YACA,c/BgEA,Y+B/DwB,MAC1B,8BACE,aAjE+B,EAkE/B,yBACA,0BAEJ,cACE,a9B9Ea,Q8B+Eb,c9BpBO,I8BqBP,mBACA,aAjF0B,UAkF1B,M1BzFK,K0B0FL,QAjFqB,aAkFrB,qCAEE,iB9BjFW,K8BkFb,uBACE,iBAlFqC,YCczC,OAEE,mBACA,aACA,sBACA,uBACA,gBACA,eACA,QAtCQ,GAwCR,iBACE,aAEJ,kBAEE,iBA3CkC,mBA6CpC,2BAEE,cACA,+BACA,cACA,kBACA,WhCgCA,2CgCtCF,2BASI,cACA,8BACA,MAtDkB,OAwDtB,aAEE,gBACA,OAtDuB,KAuDvB,ehCwFE,MgC9IgB,KAwDlB,IAvDgB,KAwDhB,MA1DuB,KA4DzB,YACE,aACA,sBACA,8BACA,gBACA,uBAEF,kCAEE,mBACA,iB/BlEa,Q+BmEb,aACA,cACA,2BACA,QAlEwB,KAmExB,kBAEF,iBACE,cAvE8B,kBAwE9B,uB/BlBa,I+BmBb,wB/BnBa,I+BqBf,kBACE,M/BtFa,Q+BuFb,YACA,cACA,U/B5DO,O+B6DP,YA3E6B,EA6E/B,iBACE,0B/B7Ba,I+B8Bb,2B/B9Ba,I+B+Bb,WA5E2B,kBA8EzB,0ChCyCA,agCxC0B,KAE9B,iBhC5CE,iCgC8CA,iB/B7Fa,K+B8Fb,YACA,cACA,cACA,QApFwB,KC0B1B,QACE,iBhCxCa,KgCyCb,WArDc,QAsDd,kBACA,QApDS,GAwDP,iBACE,iBAHM,KAIN,MAHa,QAKX,wFAEE,MAPS,QAUT,uTAGE,yBACA,MAdO,QAgBT,mDACE,aAjBO,QAkBb,gCACE,MAnBW,QjCYjB,sCiCWQ,4KAEE,MAzBO,QA4BP,kmBAGE,yBACA,MAhCK,QAkCP,oGACE,aAnCK,QAoCX,8LAGE,yBACA,MAxCS,QA2CP,0DACE,iBA7CF,KA8CE,MA7CK,SACf,iBACE,iBAHM,QAIN,MAHa,KAKX,wFAEE,MAPS,KAUT,uTAGE,sBACA,MAdO,KAgBT,mDACE,aAjBO,KAkBb,gCACE,MAnBW,KjCYjB,sCiCWQ,4KAEE,MAzBO,KA4BP,kmBAGE,sBACA,MAhCK,KAkCP,oGACE,aAnCK,KAoCX,8LAGE,sBACA,MAxCS,KA2CP,0DACE,iBA7CF,QA8CE,MA7CK,MACf,iBACE,iBAHM,QAIN,MAHa,eAKX,wFAEE,MAPS,eAUT,uTAGE,yBACA,MAdO,eAgBT,mDACE,aAjBO,eAkBb,gCACE,MAnBW,ejCYjB,sCiCWQ,4KAEE,MAzBO,eA4BP,kmBAGE,yBACA,MAhCK,eAkCP,oGACE,aAnCK,eAoCX,8LAGE,yBACA,MAxCS,eA2CP,0DACE,iBA7CF,QA8CE,MA7CK,gBACf,gBACE,iBAHM,QAIN,MAHa,KAKX,sFAEE,MAPS,KAUT,iTAGE,yBACA,MAdO,KAgBT,kDACE,aAjBO,KAkBb,+BACE,MAnBW,KjCYjB,sCiCWQ,wKAEE,MAzBO,KA4BP,slBAGE,yBACA,MAhCK,KAkCP,kGACE,aAnCK,KAoCX,2LAGE,yBACA,MAxCS,KA2CP,yDACE,iBA7CF,QA8CE,MA7CK,MACf,mBACE,iBAHM,QAIN,MAHa,KAKX,4FAEE,MAPS,KAUT,mUAGE,yBACA,MAdO,KAgBT,qDACE,aAjBO,KAkBb,kCACE,MAnBW,KjCYjB,sCiCWQ,oLAEE,MAzBO,KA4BP,0nBAGE,yBACA,MAhCK,KAkCP,wGACE,aAnCK,KAoCX,oMAGE,yBACA,MAxCS,KA2CP,4DACE,iBA7CF,QA8CE,MA7CK,MACf,gBACE,iBAHM,QAIN,MAHa,KAKX,sFAEE,MAPS,KAUT,iTAGE,yBACA,MAdO,KAgBT,kDACE,aAjBO,KAkBb,+BACE,MAnBW,KjCYjB,sCiCWQ,wKAEE,MAzBO,KA4BP,slBAGE,yBACA,MAhCK,KAkCP,kGACE,aAnCK,KAoCX,2LAGE,yBACA,MAxCS,KA2CP,yDACE,iBA7CF,QA8CE,MA7CK,MACf,gBACE,iBAHM,QAIN,MAHa,KAKX,sFAEE,MAPS,KAUT,iTAGE,yBACA,MAdO,KAgBT,kDACE,aAjBO,KAkBb,+BACE,MAnBW,KjCYjB,sCiCWQ,wKAEE,MAzBO,KA4BP,slBAGE,yBACA,MAhCK,KAkCP,kGACE,aAnCK,KAoCX,2LAGE,yBACA,MAxCS,KA2CP,yDACE,iBA7CF,QA8CE,MA7CK,MACf,mBACE,iBAHM,QAIN,MAHa,KAKX,4FAEE,MAPS,KAUT,mUAGE,yBACA,MAdO,KAgBT,qDACE,aAjBO,KAkBb,kCACE,MAnBW,KjCYjB,sCiCWQ,oLAEE,MAzBO,KA4BP,0nBAGE,yBACA,MAhCK,KAkCP,wGACE,aAnCK,KAoCX,oMAGE,yBACA,MAxCS,KA2CP,4DACE,iBA7CF,QA8CE,MA7CK,MACf,mBACE,iBAHM,QAIN,MAHa,eAKX,4FAEE,MAPS,eAUT,mUAGE,yBACA,MAdO,eAgBT,qDACE,aAjBO,eAkBb,kCACE,MAnBW,ejCYjB,sCiCWQ,oLAEE,MAzBO,eA4BP,0nBAGE,yBACA,MAhCK,eAkCP,wGACE,aAnCK,eAoCX,oMAGE,yBACA,MAxCS,eA2CP,4DACE,iBA7CF,QA8CE,MA7CK,gBACf,kBACE,iBAHM,QAIN,MAHa,KAKX,0FAEE,MAPS,KAUT,6TAGE,yBACA,MAdO,KAgBT,oDACE,aAjBO,KAkBb,iCACE,MAnBW,KjCYjB,sCiCWQ,gLAEE,MAzBO,KA4BP,8mBAGE,yBACA,MAhCK,KAkCP,sGACE,aAnCK,KAoCX,iMAGE,yBACA,MAxCS,KA2CP,2DACE,iBA7CF,QA8CE,MA7CK,MA8CjB,mBACE,oBACA,aACA,WA3GY,QA4GZ,WACF,mBACE,6BACF,6CAjEA,OACA,eACA,QACA,QA7Ce,GA8Gf,wBACE,SACA,mCACE,8BACJ,qBACE,MAIF,oDACE,YA5HY,QA6Hd,0DACE,eA9HY,QAgIhB,2BAEE,oBACA,aACA,cACA,WArIc,QAyIZ,oEAEE,6BAEN,ajClFE,iCiCoFA,gBACA,gBACA,kBAEF,eACE,M5BpJK,KLwBL,eACA,cACA,OiC1Bc,QjC2Bd,kBACA,MiC5Bc,QjC6IZ,YiCSsB,KjCzHxB,oBACE,8BACA,cACA,WACA,qBACA,kBACA,wBACA,oBCiCI,KDhCJ,uDACA,2BC0BK,SDzBL,WACA,iCACE,oBACF,iCACE,oBACF,iCACE,oBACJ,qBACE,iCAIE,2CACE,wCACF,2CACE,UACF,2CACE,0CiCgGR,aACE,aAEF,0BAEE,M5B7JK,K4B8JL,cACA,gBACA,qBACA,kBAEE,4DACE,qBACA,sBAEN,2BAEE,eACA,kLAIE,iBhCnKW,QgCoKX,MhC5JW,QgC8Jf,aACE,YACA,cACA,iBACE,WA1KyB,QA2K3B,0BACE,UACF,yBACE,YACA,cACF,oBACE,oCACA,WA7LY,QA8LZ,kCACA,oDAEE,iBAlL8B,YAmL9B,oBhC/KS,QgCgLX,8BACE,iBAlL+B,YAmL/B,oBhClLS,QgCmLT,oBAlLkC,MAmLlC,oBAlLkC,IAmLlC,MhCrLS,QgCsLT,kCAEN,gBACE,YACA,cAEF,gCjClEI,ciCmEuB,MACzB,uCAEE,ahChMW,QgCiMX,oBjC/DA,MiCgEc,QAElB,iBACE,kBACA,qBACA,kBACA,8BACE,oBACA,qBAEJ,gBACE,iBhCtNa,QgCuNb,YACA,aACA,OA5LsB,IA6LtB,ejC1JA,sCiC6JA,mBACE,cAGA,qDACE,mBACA,aAEF,oBACE,aACJ,aACE,iBhCtOW,KgCuOX,wCACA,gBACA,uBACE,cAGF,yDA3MF,OACA,eACA,QACA,QA7Ce,GAwPb,8BACE,SACA,yCACE,wCACJ,2BACE,MAGA,0EjCzMJ,iCiC2MM,iCACA,cAGJ,gEACE,YA3QU,QA4QZ,sEACE,eA7QU,SjCsEd,sCiC0MA,+CAIE,oBACA,aACF,QACE,WAvRY,QAwRZ,kBACE,kBACA,8DAEE,mBACF,+DAEE,chC7NC,IgCiOD,uQAGE,wCAMA,kUACE,wCAGF,wHAEE,iBhCxSG,QgCySH,MhCpTG,QgCqTL,gEACE,iBhC3SG,QgC4SH,MhCnSG,QgCoSb,eACE,aACF,0BAEE,mBACA,aAEA,0BACE,oBAEA,iDACE,oDACF,8CACE,cA5SqB,kBA6SrB,0BACA,gBACA,YACA,wCACA,SAKF,kMACE,cACA,gfAEE,UACA,oBACA,wBACR,aACE,YACA,cACF,cACE,2BjC5MA,aiC6MwB,KAC1B,YACE,yBjC/MA,YiCgNwB,KAC1B,iBACE,iBhCnVW,KgCoVX,0BhC7RW,IgC8RX,2BhC9RW,IgC+RX,WA1UyB,kBA2UzB,uCACA,aACA,kBjChNA,KiCiNc,EACd,eACA,kBACA,SACA,QA9UgB,GA+UhB,8BACE,qBACA,mBACF,+BjCjOA,ciCkO2B,KACzB,0EAEE,iBhCxWO,QgCyWP,MhCpXO,QgCqXT,yCACE,iBhC3WO,QgC4WP,MhCnWO,QgCoWX,6DAEE,chCtTS,IgCuTT,gBACA,WA5VyB,wDA6VzB,cACA,UACA,oBACA,wBACA,2BACA,oBhC5TE,KgC6TF,sCACF,0BACE,UACA,QACJ,gBACE,cAGA,kEjC7PA,YiC8P0B,SAC1B,gEjC/PA,aiCgQ0B,SAG1B,6DAlWF,OACA,eACA,QACA,QA7Ce,GA+Yb,gCACE,SACA,2CACE,wCACJ,6BACE,MAGF,oEACE,YA5ZU,QA6ZZ,0EACE,eA9ZU,QA+ZZ,kEACE,oBACF,wEACE,uBAIF,+CACE,MhCxaS,QgCyaX,+FACE,iBA/ZgC,YAoahC,2IACE,iBhCpaO,SgCyab,gCACE,iCCzZJ,YAEE,UjCIO,KiCHP,OAhCkB,SAkClB,qBACE,UjCCK,6BiCCL,UjCHK,QiCIP,qBACE,UjCNK,OiCQL,oFAEE,iBACA,kBACA,cjCwBW,SiCvBb,wCACE,cjCsBW,SiCpBjB,6BAEE,mBACA,aACA,uBACA,kBAEF,4EAME,UA3D0B,IA4D1B,uBACA,OA5DuB,OA6DvB,aA5D6B,KA6D7B,cA5D8B,KA6D9B,kBAEF,uDAGE,ajChEa,QiCiEb,MjCrEa,QiCsEb,UhCvEe,MgCwEf,yEACE,ajCrEW,QiCsEX,MjCzEW,QiC0Eb,yEACE,ajC3DW,QiC4Db,4EACE,WAtDsB,kCAuDxB,qFACE,iBjC3EW,QiC4EX,ajC5EW,QiC6EX,gBACA,MjChFW,QiCiFX,WAEJ,sCAEE,mBACA,oBACA,mBAGA,4BACE,iBjC7EW,QiC8EX,ajC9EW,QiC+EX,MnB5BiB,KmB8BrB,qBACE,MjC/Fa,QiCgGb,oBAEF,iBACE,elC3BA,qCkC8BA,YACE,eACF,sCAEE,YACA,cAEA,oBACE,YACA,elCnCJ,2CkCsCA,iBACE,YACA,cACA,2BACA,QACF,qBACE,QACF,iBACE,QACF,YACE,8BAEE,6CACE,QACF,yCACE,uBACA,QACF,yCACE,QAEF,0CACE,QACF,sCACE,QACF,sCACE,yBACA,SCvHR,OACE,clCuCa,IkCtCb,WA7Ba,qEA8Bb,UlCIO,KkCHP,wBACE,clCaY,OkCPV,+BACE,iBAJI,KAKJ,MAJW,QAKb,wCACE,oBAPI,KAQN,mDACE,MATI,KAGN,+BACE,iBAJI,QAKJ,MAJW,KAKb,wCACE,oBAPI,QAQN,mDACE,MATI,QAGN,+BACE,iBAJI,QAKJ,MAJW,eAKb,wCACE,oBAPI,QAQN,mDACE,MATI,QAGN,8BACE,iBAJI,QAKJ,MAJW,KAKb,uCACE,oBAPI,QAQN,kDACE,MATI,QAGN,iCACE,iBAJI,QAKJ,MAJW,KAKb,0CACE,oBAPI,QAQN,qDACE,MATI,QAGN,8BACE,iBAJI,QAKJ,MAJW,KAKb,uCACE,oBAPI,QAQN,kDACE,MATI,QAGN,8BACE,iBAJI,QAKJ,MAJW,KAKb,uCACE,oBAPI,QAQN,kDACE,MATI,QAGN,iCACE,iBAJI,QAKJ,MAJW,KAKb,0CACE,oBAPI,QAQN,qDACE,MATI,QAGN,iCACE,iBAJI,QAKJ,MAJW,eAKb,0CACE,oBAPI,QAQN,qDACE,MATI,QAGN,gCACE,iBAJI,QAKJ,MAJW,KAKb,yCACE,oBAPI,QAQN,oDACE,MATI,QAaV,2DACE,cAnDgB,kBAqDpB,eACE,iBlC5Cc,QkC6Cd,0BACA,MlCnDa,QkCoDb,UAhDmB,OAiDnB,YlCfY,IkCgBZ,YArD0B,KAsD1B,QArDsB,UAuDxB,YACE,qBACA,aACA,UArDqB,OAsDrB,uBACA,cACE,cAvDsB,kBAwDtB,mBACA,aAEA,wBACE,oBlCnES,QkCoET,MlCrES,QkCwEb,cACE,M9B5EG,K8B6EH,oBACE,MlC3DS,QkC6Df,aACE,mBACA,MlC/Ea,QkCgFb,aACA,2BACA,mBACA,kCnCuDE,amCtDwB,MAC1B,sBACE,YACA,cACA,WACF,wBACE,eACF,uBACE,kBlC5EW,QkC6EX,MlC7FW,QkC8FX,mCACE,MlC/ES,QkCgFb,wBACE,0BlCjCW,IkCkCX,2BlClCW,IkCoCf,gCAEE,eACA,4CACE,iBlCjGW,QkCmGf,YnC9FE,qBACA,UmC8FI,KnC7FJ,OmC6FU,InC5FV,YmC4FU,InC3FV,kBACA,mBACA,MmCyFU,IACV,MlC1Ga,QDwIX,amC7BsB,MACxB,gBACE,kBACA,oBC1FJ,MpCkCE,iCoC9BA,oBACA,aACA,UnCGO,KmCFP,8BACA,gBACA,gBACA,mBACA,QACE,mBACA,oBnC/BW,QmCgCX,oBAzCuB,MA0CvB,oBAzCuB,IA0CvB,M/BzCG,K+B0CH,aACA,uBACA,mBACA,QAxCgB,SAyChB,mBACA,cACE,oBnC7CS,QmC8CT,MnC9CS,QmC+Cb,SACE,cAEE,qBACE,oBnCnCO,QmCoCP,MnCpCO,QmCqCb,SACE,mBACA,oBnCnDW,QmCoDX,oBA7DuB,MA8DvB,oBA7DuB,IA8DvB,aACA,YACA,cACA,2BACA,iBACE,oBACF,mBACE,UACA,uBACA,mBACA,oBACF,kBACE,yBACA,mBAEF,wBpCiEA,aoChE0B,KAC1B,uBpC+DA,YoC9D0B,KAG1B,qBACE,uBAEF,kBACE,yBAGF,iBACE,6BAEE,0BAGF,uBACE,iBnCtFO,QmCuFP,oBnC1FO,QmC6FP,8BACE,iBnCzFK,KmC0FL,anC/FK,QmCgGL,2CAEN,sBACE,YACA,cAEF,kBACE,anCvGS,QmCwGT,aA/F0B,MAgG1B,aA/F0B,IAgG1B,gBACA,kBACA,wBACE,iBnC1GO,QmC2GP,anC/GO,QmCgHP,UAEF,sBpCqBF,YoCpB4B,KAC1B,iCAEI,uBnC1DD,ImC2DC,0BnC3DD,ImC+DH,gCAEI,wBnCjED,ImCkEC,2BnClED,ImCuED,+BACE,iBnCvHK,QmCwHL,anCxHK,QmCyHL,MrBtEW,KqBuEX,UACN,mBACE,mBAGE,mDAEI,0BnChFK,SmCiFL,uBnCjFK,SmCkFL,oBAKJ,kDAEI,2BnCzFK,SmC0FL,wBnC1FK,SmC2FL,qBAMV,eACE,UnCnIK,OmCoIP,gBACE,UnCvIK,QmCwIP,eACE,UnC1IK,OoCjCT,QACE,cACA,aACA,YACA,cACA,QAPW,OAQX,qCACE,UACF,mCACE,UACA,WACF,6CACE,UACA,UACF,yCACE,UACA,eACF,mCACE,UACA,UACF,wCACE,UACA,eACF,0CACE,UACA,UACF,wCACE,UACA,UACF,yCACE,UACA,UACF,2CACE,UACA,UACF,0CACE,UACA,UACF,oDACE,gBACF,gDACE,qBACF,0CACE,gBACF,+CACE,qBACF,iDACE,gBACF,+CACE,gBACF,gDACE,gBACF,kDACE,gBACF,iDACE,gBAEA,gCACE,UACA,SACF,uCACE,eAJF,gCACE,UACA,oBACF,uCACE,0BAJF,gCACE,UACA,qBACF,uCACE,2BAJF,gCACE,UACA,UACF,uCACE,gBAJF,gCACE,UACA,qBACF,uCACE,2BAJF,gCACE,UACA,qBACF,uCACE,2BAJF,gCACE,UACA,UACF,uCACE,gBAJF,gCACE,UACA,qBACF,uCACE,2BAJF,gCACE,UACA,qBACF,uCACE,2BAJF,gCACE,UACA,UACF,uCACE,gBAJF,iCACE,UACA,qBACF,wCACE,2BAJF,iCACE,UACA,qBACF,wCACE,2BAJF,iCACE,UACA,WACF,wCACE,iBrCkBJ,qCqChBE,yBACE,UACF,uBACE,UACA,WACF,iCACE,UACA,UACF,6BACE,UACA,eACF,uBACE,UACA,UACF,4BACE,UACA,eACF,8BACE,UACA,UACF,4BACE,UACA,UACF,6BACE,UACA,UACF,+BACE,UACA,UACF,8BACE,UACA,UACF,wCACE,gBACF,oCACE,qBACF,8BACE,gBACF,mCACE,qBACF,qCACE,gBACF,mCACE,gBACF,oCACE,gBACF,sCACE,gBACF,qCACE,gBAEA,oBACE,UACA,SACF,2BACE,eAJF,oBACE,UACA,oBACF,2BACE,0BAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,UACF,2BACE,gBAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,UACF,2BACE,gBAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,UACF,2BACE,gBAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,WACF,4BACE,kBrCnCN,2CqCqCE,2CAEE,UACF,uCAEE,UACA,WACF,2DAEE,UACA,UACF,mDAEE,UACA,eACF,uCAEE,UACA,UACF,iDAEE,UACA,eACF,qDAEE,UACA,UACF,iDAEE,UACA,UACF,mDAEE,UACA,UACF,uDAEE,UACA,UACF,qDAEE,UACA,UACF,yEAEE,gBACF,iEAEE,qBACF,qDAEE,gBACF,+DAEE,qBACF,mEAEE,gBACF,+DAEE,gBACF,iEAEE,gBACF,qEAEE,gBACF,mEAEE,gBAEA,iCAEE,UACA,SACF,+CAEE,eANF,iCAEE,UACA,oBACF,+CAEE,0BANF,iCAEE,UACA,qBACF,+CAEE,2BANF,iCAEE,UACA,UACF,+CAEE,gBANF,iCAEE,UACA,qBACF,+CAEE,2BANF,iCAEE,UACA,qBACF,+CAEE,2BANF,iCAEE,UACA,UACF,+CAEE,gBANF,iCAEE,UACA,qBACF,+CAEE,2BANF,iCAEE,UACA,qBACF,+CAEE,2BANF,iCAEE,UACA,UACF,+CAEE,gBANF,mCAEE,UACA,qBACF,iDAEE,2BANF,mCAEE,UACA,qBACF,iDAEE,2BANF,mCAEE,UACA,WACF,iDAEE,kBrC1GN,sCqC4GE,wBACE,UACF,sBACE,UACA,WACF,gCACE,UACA,UACF,4BACE,UACA,eACF,sBACE,UACA,UACF,2BACE,UACA,eACF,6BACE,UACA,UACF,2BACE,UACA,UACF,4BACE,UACA,UACF,8BACE,UACA,UACF,6BACE,UACA,UACF,uCACE,gBACF,mCACE,qBACF,6BACE,gBACF,kCACE,qBACF,oCACE,gBACF,kCACE,gBACF,mCACE,gBACF,qCACE,gBACF,oCACE,gBAEA,mBACE,UACA,SACF,0BACE,eAJF,mBACE,UACA,oBACF,0BACE,0BAJF,mBACE,UACA,qBACF,0BACE,2BAJF,mBACE,UACA,UACF,0BACE,gBAJF,mBACE,UACA,qBACF,0BACE,2BAJF,mBACE,UACA,qBACF,0BACE,2BAJF,mBACE,UACA,UACF,0BACE,gBAJF,mBACE,UACA,qBACF,0BACE,2BAJF,mBACE,UACA,qBACF,0BACE,2BAJF,mBACE,UACA,UACF,0BACE,gBAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,WACF,2BACE,kBrC/JN,sCqCiKE,0BACE,UACF,wBACE,UACA,WACF,kCACE,UACA,UACF,8BACE,UACA,eACF,wBACE,UACA,UACF,6BACE,UACA,eACF,+BACE,UACA,UACF,6BACE,UACA,UACF,8BACE,UACA,UACF,gCACE,UACA,UACF,+BACE,UACA,UACF,yCACE,gBACF,qCACE,qBACF,+BACE,gBACF,oCACE,qBACF,sCACE,gBACF,oCACE,gBACF,qCACE,gBACF,uCACE,gBACF,sCACE,gBAEA,qBACE,UACA,SACF,4BACE,eAJF,qBACE,UACA,oBACF,4BACE,0BAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,UACF,4BACE,gBAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,UACF,4BACE,gBAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,UACF,4BACE,gBAJF,sBACE,UACA,qBACF,6BACE,2BAJF,sBACE,UACA,qBACF,6BACE,2BAJF,sBACE,UACA,WACF,6BACE,kBrCzMJ,sCqC2MA,6BACE,UACF,2BACE,UACA,WACF,qCACE,UACA,UACF,iCACE,UACA,eACF,2BACE,UACA,UACF,gCACE,UACA,eACF,kCACE,UACA,UACF,gCACE,UACA,UACF,iCACE,UACA,UACF,mCACE,UACA,UACF,kCACE,UACA,UACF,4CACE,gBACF,wCACE,qBACF,kCACE,gBACF,uCACE,qBACF,yCACE,gBACF,uCACE,gBACF,wCACE,gBACF,0CACE,gBACF,yCACE,gBAEA,wBACE,UACA,SACF,+BACE,eAJF,wBACE,UACA,oBACF,+BACE,0BAJF,wBACE,UACA,qBACF,+BACE,2BAJF,wBACE,UACA,UACF,+BACE,gBAJF,wBACE,UACA,qBACF,+BACE,2BAJF,wBACE,UACA,qBACF,+BACE,2BAJF,wBACE,UACA,UACF,+BACE,gBAJF,wBACE,UACA,qBACF,+BACE,2BAJF,wBACE,UACA,qBACF,+BACE,2BAJF,wBACE,UACA,UACF,+BACE,gBAJF,yBACE,UACA,qBACF,gCACE,2BAJF,yBACE,UACA,qBACF,gCACE,2BAJF,yBACE,UACA,WACF,gCACE,kBrCnPJ,sCqCqPA,yBACE,UACF,uBACE,UACA,WACF,iCACE,UACA,UACF,6BACE,UACA,eACF,uBACE,UACA,UACF,4BACE,UACA,eACF,8BACE,UACA,UACF,4BACE,UACA,UACF,6BACE,UACA,UACF,+BACE,UACA,UACF,8BACE,UACA,UACF,wCACE,gBACF,oCACE,qBACF,8BACE,gBACF,mCACE,qBACF,qCACE,gBACF,mCACE,gBACF,oCACE,gBACF,sCACE,gBACF,qCACE,gBAEA,oBACE,UACA,SACF,2BACE,eAJF,oBACE,UACA,oBACF,2BACE,0BAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,UACF,2BACE,gBAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,UACF,2BACE,gBAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,qBACF,2BACE,2BAJF,oBACE,UACA,UACF,2BACE,gBAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,qBACF,4BACE,2BAJF,qBACE,UACA,WACF,4BACE,kBAER,SACE,qBACA,sBACA,oBACA,oBACE,uBACF,0BACE,qCAEF,qBACE,uBACF,oBACE,cACA,eACA,aACA,4BACE,SACA,qBACF,qCACE,qBACF,+BACE,gBACJ,mBACE,aACF,sBACE,eACF,sBACE,mBrCnXF,2CqCsXE,0BACE,crC3WJ,sCqC8WE,oBACE,cAGJ,qBACE,qBACA,wCACA,yCACA,6BACE,8BACA,+BAEA,0BACE,kBrC3YN,qCqC6YM,iCACE,mBrC1YR,2CqC4YM,iCACE,mBrCzYR,4DqC2YM,sCACE,mBrCxYR,sCqC0YM,gCACE,mBrCvYR,sCqCyYM,kCACE,mBrCrYN,6DqCuYI,uCACE,mBrC9XN,sCqCgYI,qCACE,mBrC5XN,6DqC8XI,0CACE,mBrCrXN,sCqCuXI,iCACE,mBA5BJ,0BACE,qBrC3YN,qCqC6YM,iCACE,sBrC1YR,2CqC4YM,iCACE,sBrCzYR,4DqC2YM,sCACE,sBrCxYR,sCqC0YM,gCACE,sBrCvYR,sCqCyYM,kCACE,sBrCrYN,6DqCuYI,uCACE,sBrC9XN,sCqCgYI,qCACE,sBrC5XN,6DqC8XI,0CACE,sBrCrXN,sCqCuXI,iCACE,sBA5BJ,0BACE,oBrC3YN,qCqC6YM,iCACE,qBrC1YR,2CqC4YM,iCACE,qBrCzYR,4DqC2YM,sCACE,qBrCxYR,sCqC0YM,gCACE,qBrCvYR,sCqCyYM,kCACE,qBrCrYN,6DqCuYI,uCACE,qBrC9XN,sCqCgYI,qCACE,qBrC5XN,6DqC8XI,0CACE,qBrCrXN,sCqCuXI,iCACE,qBA5BJ,0BACE,qBrC3YN,qCqC6YM,iCACE,sBrC1YR,2CqC4YM,iCACE,sBrCzYR,4DqC2YM,sCACE,sBrCxYR,sCqC0YM,gCACE,sBrCvYR,sCqCyYM,kCACE,sBrCrYN,6DqCuYI,uCACE,sBrC9XN,sCqCgYI,qCACE,sBrC5XN,6DqC8XI,0CACE,sBrCrXN,sCqCuXI,iCACE,sBA5BJ,0BACE,kBrC3YN,qCqC6YM,iCACE,mBrC1YR,2CqC4YM,iCACE,mBrCzYR,4DqC2YM,sCACE,mBrCxYR,sCqC0YM,gCACE,mBrCvYR,sCqCyYM,kCACE,mBrCrYN,6DqCuYI,uCACE,mBrC9XN,sCqCgYI,qCACE,mBrC5XN,6DqC8XI,0CACE,mBrCrXN,sCqCuXI,iCACE,mBA5BJ,0BACE,qBrC3YN,qCqC6YM,iCACE,sBrC1YR,2CqC4YM,iCACE,sBrCzYR,4DqC2YM,sCACE,sBrCxYR,sCqC0YM,gCACE,sBrCvYR,sCqCyYM,kCACE,sBrCrYN,6DqCuYI,uCACE,sBrC9XN,sCqCgYI,qCACE,sBrC5XN,6DqC8XI,0CACE,sBrCrXN,sCqCuXI,iCACE,sBA5BJ,0BACE,oBrC3YN,qCqC6YM,iCACE,qBrC1YR,2CqC4YM,iCACE,qBrCzYR,4DqC2YM,sCACE,qBrCxYR,sCqC0YM,gCACE,qBrCvYR,sCqCyYM,kCACE,qBrCrYN,6DqCuYI,uCACE,qBrC9XN,sCqCgYI,qCACE,qBrC5XN,6DqC8XI,0CACE,qBrCrXN,sCqCuXI,iCACE,qBA5BJ,0BACE,qBrC3YN,qCqC6YM,iCACE,sBrC1YR,2CqC4YM,iCACE,sBrCzYR,4DqC2YM,sCACE,sBrCxYR,sCqC0YM,gCACE,sBrCvYR,sCqCyYM,kCACE,sBrCrYN,6DqCuYI,uCACE,sBrC9XN,sCqCgYI,qCACE,sBrC5XN,6DqC8XI,0CACE,sBrCrXN,sCqCuXI,iCACE,sBA5BJ,0BACE,kBrC3YN,qCqC6YM,iCACE,mBrC1YR,2CqC4YM,iCACE,mBrCzYR,4DqC2YM,sCACE,mBrCxYR,sCqC0YM,gCACE,mBrCvYR,sCqCyYM,kCACE,mBrCrYN,6DqCuYI,uCACE,mBrC9XN,sCqCgYI,qCACE,mBrC5XN,6DqC8XI,0CACE,mBrCrXN,sCqCuXI,iCACE,mBCrfV,MACE,oBACA,cACA,aACA,YACA,cACA,uBAEA,kBACE,qBACA,sBACA,oBACA,6BACE,uBACF,mCACE,cAjBS,OAkBb,eACE,oBACF,gBACE,QArBW,OAsBb,kBACE,sBACA,kDACE,gCtC4DJ,2CsCzDE,qBACE,aAEA,WACE,UACA,oBAFF,WACE,UACA,qBAFF,WACE,UACA,UAFF,WACE,UACA,qBAFF,WACE,UACA,qBAFF,WACE,UACA,UAFF,WACE,UACA,qBAFF,WACE,UACA,qBAFF,WACE,UACA,UAFF,YACE,UACA,qBAFF,YACE,UACA,qBAFF,YACE,UACA,YC/BN,gBACE,sBAEA,8CAEE,yBACJ,sBACE,iCAPF,gBACE,yBAEA,8CAEE,sBACJ,sBACE,oCAPF,gBACE,yBAEA,8CAEE,yBACJ,sBACE,oCAPF,eACE,yBAEA,4CAEE,yBACJ,qBACE,oCAPF,kBACE,yBAEA,kDAEE,yBACJ,wBACE,oCAKA,wBACE,yBAEA,8DAEE,yBACJ,8BACE,oCAEF,uBACE,yBAEA,4DAEE,yBACJ,6BACE,oCA5BJ,eACE,yBAEA,4CAEE,yBACJ,qBACE,oCAKA,qBACE,yBAEA,wDAEE,yBACJ,2BACE,oCAEF,oBACE,yBAEA,sDAEE,yBACJ,0BACE,oCA5BJ,eACE,yBAEA,4CAEE,yBACJ,qBACE,oCAKA,qBACE,yBAEA,wDAEE,yBACJ,2BACE,oCAEF,oBACE,yBAEA,sDAEE,yBACJ,0BACE,oCA5BJ,kBACE,yBAEA,kDAEE,yBACJ,wBACE,oCAKA,wBACE,yBAEA,8DAEE,yBACJ,8BACE,oCAEF,uBACE,yBAEA,4DAEE,yBACJ,6BACE,oCA5BJ,kBACE,yBAEA,kDAEE,yBACJ,wBACE,oCAKA,wBACE,yBAEA,8DAEE,yBACJ,8BACE,oCAEF,uBACE,yBAEA,4DAEE,yBACJ,6BACE,oCA5BJ,iBACE,yBAEA,gDAEE,yBACJ,uBACE,oCAKA,uBACE,yBAEA,4DAEE,yBACJ,6BACE,oCAEF,sBACE,yBAEA,0DAEE,yBACJ,4BACE,oCAGJ,oBACE,yBACF,0BACE,oCAHF,oBACE,yBACF,0BACE,oCAHF,sBACE,yBACF,4BACE,oCAHF,oBACE,yBACF,0BACE,oCAHF,eACE,yBACF,qBACE,oCAHF,qBACE,yBACF,2BACE,oCAHF,uBACE,yBACF,6BACE,oCAHF,oBACE,yBACF,0BACE,oCAHF,oBACE,yBACF,0BACE,oCvCjCF,oBACE,WACA,YACA,cwCHJ,gBACE,sBAEF,iBACE,uBCPF,eACE,2BAEF,eACE,2BCJF,YACE,2BCEF,aACE,6BCJF,eACE,oBAEF,gBACE,qBAYI,MACE,wBADF,MACE,0BADF,MACE,2BADF,MACE,yBAGF,MACE,yBACA,0BAGF,MACE,wBACA,2BAXF,MACE,6BADF,MACE,+BADF,MACE,gCADF,MACE,8BAGF,MACE,8BACA,+BAGF,MACE,6BACA,gCAXF,MACE,4BADF,MACE,8BADF,MACE,+BADF,MACE,6BAGF,MACE,6BACA,8BAGF,MACE,4BACA,+BAXF,MACE,6BADF,MACE,+BADF,MACE,gCADF,MACE,8BAGF,MACE,8BACA,+BAGF,MACE,6BACA,gCAXF,MACE,2BADF,MACE,6BADF,MACE,8BADF,MACE,4BAGF,MACE,4BACA,6BAGF,MACE,2BACA,8BAXF,MACE,6BADF,MACE,+BADF,MACE,gCADF,MACE,8BAGF,MACE,8BACA,+BAGF,MACE,6BACA,gCAXF,MACE,2BADF,MACE,6BADF,MACE,8BADF,MACE,4BAGF,MACE,4BACA,6BAGF,MACE,2BACA,8BAXF,MACE,yBADF,MACE,2BADF,MACE,4BADF,MACE,0BAGF,MACE,0BACA,2BAGF,MACE,yBACA,4BAXF,MACE,8BADF,MACE,gCADF,MACE,iCADF,MACE,+BAGF,MACE,+BACA,gCAGF,MACE,8BACA,iCAXF,MACE,6BADF,MACE,+BADF,MACE,gCADF,MACE,8BAGF,MACE,8BACA,+BAGF,MACE,6BACA,gCAXF,MACE,8BADF,MACE,gCADF,MACE,iCADF,MACE,+BAGF,MACE,+BACA,gCAGF,MACE,8BACA,iCAXF,MACE,4BADF,MACE,8BADF,MACE,+BADF,MACE,6BAGF,MACE,6BACA,8BAGF,MACE,4BACA,+BAXF,MACE,8BADF,MACE,gCADF,MACE,iCADF,MACE,+BAGF,MACE,+BACA,gCAGF,MACE,8BACA,iCAXF,MACE,4BADF,MACE,8BADF,MACE,+BADF,MACE,6BAGF,MACE,6BACA,8BAGF,MACE,4BACA,+BCxBJ,WACE,0BADF,WACE,4BADF,WACE,0BADF,WACE,4BADF,WACE,6BADF,WACE,0BADF,WACE,4B7C6EJ,qC6C9EE,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6B7CiFJ,2C6ClFE,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6B7CyFJ,sC6C1FE,iBACE,0BADF,iBACE,4BADF,iBACE,0BADF,iBACE,4BADF,iBACE,6BADF,iBACE,0BADF,iBACE,6B7C6FJ,sC6C9FE,mBACE,0BADF,mBACE,4BADF,mBACE,0BADF,mBACE,4BADF,mBACE,6BADF,mBACE,0BADF,mBACE,6B7C4GF,sC6C7GA,sBACE,0BADF,sBACE,4BADF,sBACE,0BADF,sBACE,4BADF,sBACE,6BADF,sBACE,0BADF,sBACE,6B7C2HF,sC6C5HA,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6BAyBJ,mBACE,6BADF,oBACE,8BADF,eACE,2BADF,gBACE,4B7CmDF,qC6C/CE,0BACE,8B7CkDJ,2C6ChDE,0BACE,8B7CmDJ,4D6CjDE,+BACE,8B7CoDJ,sC6ClDE,yBACE,8B7CqDJ,sC6CnDE,2BACE,8B7CuDF,6D6CrDA,gCACE,8B7C8DF,sC6C5DA,8BACE,8B7CgEF,6D6C9DA,mCACE,8B7CuEF,sC6CrEA,0BACE,8B7CsBJ,qC6C/CE,2BACE,+B7CkDJ,2C6ChDE,2BACE,+B7CmDJ,4D6CjDE,gCACE,+B7CoDJ,sC6ClDE,0BACE,+B7CqDJ,sC6CnDE,4BACE,+B7CuDF,6D6CrDA,iCACE,+B7C8DF,sC6C5DA,+BACE,+B7CgEF,6D6C9DA,oCACE,+B7CuEF,sC6CrEA,2BACE,+B7CsBJ,qC6C/CE,sBACE,4B7CkDJ,2C6ChDE,sBACE,4B7CmDJ,4D6CjDE,2BACE,4B7CoDJ,sC6ClDE,qBACE,4B7CqDJ,sC6CnDE,uBACE,4B7CuDF,6D6CrDA,4BACE,4B7C8DF,sC6C5DA,0BACE,4B7CgEF,6D6C9DA,+BACE,4B7CuEF,sC6CrEA,sBACE,4B7CsBJ,qC6C/CE,uBACE,6B7CkDJ,2C6ChDE,uBACE,6B7CmDJ,4D6CjDE,4BACE,6B7CoDJ,sC6ClDE,sBACE,6B7CqDJ,sC6CnDE,wBACE,6B7CuDF,6D6CrDA,6BACE,6B7C8DF,sC6C5DA,2BACE,6B7CgEF,6D6C9DA,gCACE,6B7CuEF,sC6CrEA,uBACE,6BAEN,gBACE,qCAEF,cACE,oCAEF,cACE,oCAEF,WACE,6BAEF,uBACE,2BACF,wBACE,2BACF,wBACE,2BACF,0BACE,2BACF,sBACE,2BAEF,mBACE,mLAEF,qBACE,mLAEF,sBACE,mLAEF,qBACE,iCAEF,gBACE,iCC5FA,UACE,yB9C2EF,qC8CzEE,iBACE,0B9C4EJ,2C8C1EE,iBACE,0B9C6EJ,4D8C3EE,sBACE,0B9C8EJ,sC8C5EE,gBACE,0B9C+EJ,sC8C7EE,kBACE,0B9CiFF,6D8C/EA,uBACE,0B9CwFF,sC8CtFA,qBACE,0B9C0FF,6D8CxFA,0BACE,0B9CiGF,sC8C/FA,iBACE,0BA5BJ,SACE,wB9C2EF,qC8CzEE,gBACE,yB9C4EJ,2C8C1EE,gBACE,yB9C6EJ,4D8C3EE,qBACE,yB9C8EJ,sC8C5EE,eACE,yB9C+EJ,sC8C7EE,iBACE,yB9CiFF,6D8C/EA,sBACE,yB9CwFF,sC8CtFA,oBACE,yB9C0FF,6D8CxFA,yBACE,yB9CiGF,sC8C/FA,gBACE,yBA5BJ,WACE,0B9C2EF,qC8CzEE,kBACE,2B9C4EJ,2C8C1EE,kBACE,2B9C6EJ,4D8C3EE,uBACE,2B9C8EJ,sC8C5EE,iBACE,2B9C+EJ,sC8C7EE,mBACE,2B9CiFF,6D8C/EA,wBACE,2B9CwFF,sC8CtFA,sBACE,2B9C0FF,6D8CxFA,2BACE,2B9CiGF,sC8C/FA,kBACE,2BA5BJ,iBACE,gC9C2EF,qC8CzEE,wBACE,iC9C4EJ,2C8C1EE,wBACE,iC9C6EJ,4D8C3EE,6BACE,iC9C8EJ,sC8C5EE,uBACE,iC9C+EJ,sC8C7EE,yBACE,iC9CiFF,6D8C/EA,8BACE,iC9CwFF,sC8CtFA,4BACE,iC9C0FF,6D8CxFA,iCACE,iC9CiGF,sC8C/FA,wBACE,iCA5BJ,gBACE,+B9C2EF,qC8CzEE,uBACE,gC9C4EJ,2C8C1EE,uBACE,gC9C6EJ,4D8C3EE,4BACE,gC9C8EJ,sC8C5EE,sBACE,gC9C+EJ,sC8C7EE,wBACE,gC9CiFF,6D8C/EA,6BACE,gC9CwFF,sC8CtFA,2BACE,gC9C0FF,6D8CxFA,gCACE,gC9CiGF,sC8C/FA,uBACE,gCAEN,WACE,wBAEF,YACE,uBACA,iCACA,wBACA,2BACA,qBACA,6BACA,8BACA,uB9CmCA,qC8ChCA,kBACE,yB9CmCF,2C8ChCA,kBACE,yB9CmCF,4D8ChCA,uBACE,yB9CmCF,sC8ChCA,iBACE,yB9CmCF,sC8ChCA,mBACE,yB9CoCA,6D8CjCF,wBACE,yB9C0CA,sC8CvCF,sBACE,yB9C2CA,6D8CxCF,2BACE,yB9CiDA,sC8C9CF,kBACE,yBAEJ,cACE,6B9CJA,qC8COA,qBACE,8B9CJF,2C8COA,qBACE,8B9CJF,4D8COA,0BACE,8B9CJF,sC8COA,oBACE,8B9CJF,sC8COA,sBACE,8B9CHA,6D8CMF,2BACE,8B9CGA,+D8CCA,8B9CIA,6D8CDF,8BACE,8B9CUA,sC8CPF,qBACE,8BCnHJ,MACE,oBACA,aACA,sBACA,8BACA,cACE,gBAEA,eACE,mBAKF,eACE,iBAHM,KAIN,MAHa,QAIb,mHAEE,cACF,sBACE,MARW,QASb,yBACE,wBACA,wEAEE,MAbS,Q/C0EjB,sC+C5DI,4BAEI,iBAjBE,MAkBN,wDAEE,wBAGA,kJAEE,yBACA,MAzBS,QA2BX,uBACE,MA5BS,QA6BT,WACA,6BACE,UAEF,oCACE,UAGF,iEACE,MAtCO,QAuCP,6EACE,mCAEF,kMAEE,iBA5CK,QA6CL,aA7CK,QA8CL,MA/CF,KAkDJ,uBAGE,4E/CUR,qC+CRU,oCACE,6EAtDV,eACE,iBAHM,QAIN,MAHa,KAIb,mHAEE,cACF,sBACE,MARW,KASb,yBACE,2BACA,wEAEE,MAbS,K/C0EjB,sC+C5DI,4BAEI,iBAjBE,SAkBN,wDAEE,2BAGA,kJAEE,sBACA,MAzBS,KA2BX,uBACE,MA5BS,KA6BT,WACA,6BACE,UAEF,oCACE,UAGF,iEACE,MAtCO,KAuCP,6EACE,mCAEF,kMAEE,iBA5CK,KA6CL,aA7CK,KA8CL,MA/CF,QAkDJ,uBAGE,8E/CUR,qC+CRU,oCACE,+EAtDV,eACE,iBAHM,QAIN,MAHa,eAIb,mHAEE,cACF,sBACE,MARW,eASb,yBACE,qBACA,wEAEE,MAbS,e/C0EjB,sC+C5DI,4BAEI,iBAjBE,SAkBN,wDAEE,qBAGA,kJAEE,yBACA,MAzBS,eA2BX,uBACE,MA5BS,eA6BT,WACA,6BACE,UAEF,oCACE,UAGF,iEACE,MAtCO,eAuCP,6EACE,mCAEF,kMAEE,iBA5CK,eA6CL,aA7CK,eA8CL,MA/CF,QAkDJ,uBAGE,iF/CUR,qC+CRU,oCACE,kFAtDV,cACE,iBAHM,QAIN,MAHa,KAIb,iHAEE,cACF,qBACE,MARW,KASb,wBACE,2BACA,sEAEE,MAbS,K/C0EjB,sC+C5DI,2BAEI,iBAjBE,SAkBN,sDAEE,2BAGA,8IAEE,yBACA,MAzBS,KA2BX,sBACE,MA5BS,KA6BT,WACA,4BACE,UAEF,mCACE,UAGF,+DACE,MAtCO,KAuCP,2EACE,mCAEF,8LAEE,iBA5CK,KA6CL,aA7CK,KA8CL,MA/CF,QAkDJ,sBAGE,gF/CUR,qC+CRU,mCACE,iFAtDV,iBACE,iBAHM,QAIN,MAHa,KAIb,uHAEE,cACF,wBACE,MARW,KASb,2BACE,2BACA,4EAEE,MAbS,K/C0EjB,sC+C5DI,8BAEI,iBAjBE,SAkBN,4DAEE,2BAGA,0JAEE,yBACA,MAzBS,KA2BX,yBACE,MA5BS,KA6BT,WACA,+BACE,UAEF,sCACE,UAGF,qEACE,MAtCO,KAuCP,iFACE,mCAEF,0MAEE,iBA5CK,KA6CL,aA7CK,KA8CL,MA/CF,QAkDJ,yBAGE,gF/CUR,qC+CRU,sCACE,iFAtDV,cACE,iBAHM,QAIN,MAHa,KAIb,iHAEE,cACF,qBACE,MARW,KASb,wBACE,2BACA,sEAEE,MAbS,K/C0EjB,sC+C5DI,2BAEI,iBAjBE,SAkBN,sDAEE,2BAGA,8IAEE,yBACA,MAzBS,KA2BX,sBACE,MA5BS,KA6BT,WACA,4BACE,UAEF,mCACE,UAGF,+DACE,MAtCO,KAuCP,2EACE,mCAEF,8LAEE,iBA5CK,KA6CL,aA7CK,KA8CL,MA/CF,QAkDJ,sBAGE,gF/CUR,qC+CRU,mCACE,iFAtDV,cACE,iBAHM,QAIN,MAHa,KAIb,iHAEE,cACF,qBACE,MARW,KASb,wBACE,2BACA,sEAEE,MAbS,K/C0EjB,sC+C5DI,2BAEI,iBAjBE,SAkBN,sDAEE,2BAGA,8IAEE,yBACA,MAzBS,KA2BX,sBACE,MA5BS,KA6BT,WACA,4BACE,UAEF,mCACE,UAGF,+DACE,MAtCO,KAuCP,2EACE,mCAEF,8LAEE,iBA5CK,KA6CL,aA7CK,KA8CL,MA/CF,QAkDJ,sBAGE,gF/CUR,qC+CRU,mCACE,iFAtDV,iBACE,iBAHM,QAIN,MAHa,KAIb,uHAEE,cACF,wBACE,MARW,KASb,2BACE,2BACA,4EAEE,MAbS,K/C0EjB,sC+C5DI,8BAEI,iBAjBE,SAkBN,4DAEE,2BAGA,0JAEE,yBACA,MAzBS,KA2BX,yBACE,MA5BS,KA6BT,WACA,+BACE,UAEF,sCACE,UAGF,qEACE,MAtCO,KAuCP,iFACE,mCAEF,0MAEE,iBA5CK,KA6CL,aA7CK,KA8CL,MA/CF,QAkDJ,yBAGE,gF/CUR,qC+CRU,sCACE,iFAtDV,iBACE,iBAHM,QAIN,MAHa,eAIb,uHAEE,cACF,wBACE,MARW,eASb,2BACE,qBACA,4EAEE,MAbS,e/C0EjB,sC+C5DI,8BAEI,iBAjBE,SAkBN,4DAEE,qBAGA,0JAEE,yBACA,MAzBS,eA2BX,yBACE,MA5BS,eA6BT,WACA,+BACE,UAEF,sCACE,UAGF,qEACE,MAtCO,eAuCP,iFACE,mCAEF,0MAEE,iBA5CK,eA6CL,aA7CK,eA8CL,MA/CF,QAkDJ,yBAGE,gF/CUR,qC+CRU,sCACE,iFAtDV,gBACE,iBAHM,QAIN,MAHa,KAIb,qHAEE,cACF,uBACE,MARW,KASb,0BACE,2BACA,0EAEE,MAbS,K/C0EjB,sC+C5DI,6BAEI,iBAjBE,SAkBN,0DAEE,2BAGA,sJAEE,yBACA,MAzBS,KA2BX,wBACE,MA5BS,KA6BT,WACA,8BACE,UAEF,qCACE,UAGF,mEACE,MAtCO,KAuCP,+EACE,mCAEF,sMAEE,iBA5CK,KA6CL,aA7CK,KA8CL,MA/CF,QAkDJ,wBAGE,gF/CUR,qC+CRU,qCACE,iFAGV,0BACE,QA7EoB,O/CoFxB,2C+CJI,2BACE,QAhFmB,a/CmFzB,qE+CCM,QAnFkB,cAuFtB,yGACE,mBACA,aACA,0IACE,YACA,cACN,oBACE,gBACF,oBACE,iBAIJ,YAEE,gBACA,kBACE,SACA,gBACA,eACA,kBACA,QACA,qCAEF,2BACE,W/ClCF,qC+CsBF,YAeI,cAEJ,cACE,kB/CxCA,qC+C2CE,sBACE,aACA,uCACE,sB/C1CN,2C+CmCF,cASI,aACA,uBACA,uC/CaA,a+CZ0B,QAI9B,sBAEE,YACA,cAEF,WACE,YACA,cACA,QAhJkB,YCIpB,SACE,QALgB,YhDiGhB,sCgDxFE,mBACE,QATmB,YAUrB,kBACE,QAVkB,cCExB,QACE,iBhDSa,QgDRb,QAJe,iB5CQjB,UACE,WAHiB,QAIjB,eAGF,EACE,qBACA,cAKF,YACE,iBAhBc,QAiBd,cACA,gBAEA,2BACE,aLqDF,qCK3DF,YAUI,aACA,eAEA,2BACE,aACA,2BACA,8BAGF,oBACE,aAGF,0BACE,eAMJ,iDACE,8BAGF,wBACE,2BAGF,kBACE,gBACA,cAMJ,WACE,0BACA,iBAIA,0BACE,gBACA,UACA,SAEA,6BACE,WACA,gBACA,UAEA,+BACE,cAIJ,qCACE,mBAKN,mBACE,cACA,YACA,YACA,iBACA,SAGF,qBACE,mBAGF,yDACE,gBAOF,mBACI,iBAHW,QAIX,WAEA,yBACE,iBAjHU,QAkHV,WAIN,mBACE,iBAba,QAcX,WAEA,yBACE,iBA3HU,QA4HV,WAMN,cACE,eACA,gBACA,kBAKF,iBACE,iBA3Ic,QA6Id,mCACE","file":"bds.css"} \ No newline at end of file diff --git a/bds/static/bds/images/logo.svg b/bds/static/bds/images/logo.svg deleted file mode 100644 index 15292488..00000000 --- a/bds/static/bds/images/logo.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/bds/static/bds/images/logo_square.svg b/bds/static/bds/images/logo_square.svg deleted file mode 100644 index 25c1aefd..00000000 --- a/bds/static/bds/images/logo_square.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - diff --git a/bds/static/bds/images/logout.svg b/bds/static/bds/images/logout.svg deleted file mode 100644 index 12489bbd..00000000 --- a/bds/static/bds/images/logout.svg +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - -image/svg+xmlOpenclipart diff --git a/bds/static/bds/js/bds.js b/bds/static/bds/js/bds.js deleted file mode 100644 index 3c2fb49b..00000000 --- a/bds/static/bds/js/bds.js +++ /dev/null @@ -1,7 +0,0 @@ -$(function () { - // Close notifications when delete button is pressed - $(".notification .delete").on("click", function () { - $(this).parent().remove(); - }); - -}); \ No newline at end of file diff --git a/bds/static/src/sass/bds.scss b/bds/static/src/sass/bds.scss deleted file mode 100644 index dad677a8..00000000 --- a/bds/static/src/sass/bds.scss +++ /dev/null @@ -1,152 +0,0 @@ -// Compilation command : -// sass -I shared/static/src/ --watch bds/static/src/sass/bds.scss bds/static/bds/css/bds.css --style compressed - -$text: black; - -@import "bulma/bulma.sass"; - -$primary_color: #3e2263; -$background_color: #ddcecc; - -html, body { - background: $background_color; - font-size: 18px; -} - -a { - text-decoration: none; - color: #a82305; -} - -/* header */ - -#search-bar { - background-color: $primary_color; - padding: 0 1em; - margin-bottom: 0; - - #logout-mobile { - display: none; - } - - @include mobile { - display: flex; - flex-wrap: wrap; - - #logout-mobile { - display: flex; - flex-direction: row-reverse; - justify-content: space-between; - } - - #logout { - display: none; - } - - #search-input { - flex: 0 1 100%; - } - } - - // Workaround : `justify-content : ` pas encore supporté - // Voir https://developer.mozilla.org/en-US/docs/Web/CSS/justify-content - & :first-child, & :last-child { - justify-content: space-between; - } - - & :last-child { - flex-direction: row-reverse; - } - - input { - border-radius: 0; - margin: 10px 0; - } -} - -/* Autocomplétion du BDS */ - -.highlight { - text-decoration: underline; - font-weight: bold; -} - -.yourlabs-autocomplete { - ul { - list-style: none; - padding: 0; - margin: 0; - - li { - height: 2em; - line-height: 2em; - padding: 0; - - a { - color: inherit; - } - } - - li.hilight { - background: #e8554e; - } - } -} - -.autocomplete-item { - display: block; - width: 480px; - height: 100%; - padding: 2px 10px; - margin: 0; -} - -.autocomplete-header { - background: #b497e1; -} - -.autocomplete-value, .autocomplete-new, .autocomplete-more { - background: white; -} - -/* --- Forms --- */ - -$button_color: lighten($primary_color, 10); - -input[type="submit"] { - background-color: $button_color; - color: findColorInvert($button_color); - - &:hover { - background-color: $primary_color; - color: findColorInvert($primary_color); - } -} - -.button.is-primary { - background-color: $button_color; - color: findColorInvert($button_color); - - &:hover { - background-color: $primary_color; - color: findColorInvert($primary_color); - } -} - -/* --- Message styling --- */ - -.notification { - padding: 0.5em 0; - font-size: 1.2em; - text-align: center; -} - -/* --- Modals --- */ - -.modal-card-head { - background-color: $primary_color; - - .modal-card-title { - color: findColorInvert($primary_color); - } -} diff --git a/bds/templates/bds/base.html b/bds/templates/bds/base.html deleted file mode 100644 index 74759e88..00000000 --- a/bds/templates/bds/base.html +++ /dev/null @@ -1,50 +0,0 @@ -{% load static %} -{% load bulma_utils %} - - - - - {{ site.name }} - - - - - {# CSS #} - - - - {# Javascript #} - - - - - {% block extra_head %}{% endblock extra_head %} - - - {% include "bds/nav.html" %} - {% block layout %} -
-
-
- - {% if messages %} - {% for message in messages %} -
- {% if 'safe' in message.tags %} - {{ message|safe }} - {% else %} - {{ message }} - {% endif %} - -
- {% endfor %} - {% endif %} - - {% block content %} - {% endblock content %} -
-
-
- {% endblock layout %} - - diff --git a/bds/templates/bds/expired_members.html b/bds/templates/bds/expired_members.html deleted file mode 100644 index c5b53dbc..00000000 --- a/bds/templates/bds/expired_members.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "bds/base.html" %} - - -{% block content %} - -

Liste des adhésions expirées

- -{% if object_list %} -
-
    - {% for p in object_list %} -
  • {{ p.user.first_name }} {{ p.user.last_name }} ({{ p.user.username }}), {{ p.get_cotisation_period_display }}
  • - {% endfor %} -
-
- - -{% endif %} - -{% endblock %} diff --git a/bds/templates/bds/forms/checkbox.html b/bds/templates/bds/forms/checkbox.html deleted file mode 100644 index 109ba75c..00000000 --- a/bds/templates/bds/forms/checkbox.html +++ /dev/null @@ -1,16 +0,0 @@ -
- {% if field.auto_id %} - - {% endif %} - {% for error in field.errors %} - {{ error }} - {% endfor %} - - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/forms/field.html b/bds/templates/bds/forms/field.html deleted file mode 100644 index 132d6520..00000000 --- a/bds/templates/bds/forms/field.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load bulma_utils %} - -
- {% if field|is_checkbox %} - - {% include "bds/forms/checkbox.html" with field=field %} - - {% elif field|is_radio %} - - {% include "bds/forms/radio.html" with field=field %} - - {% elif field|is_input %} - - {% include "bds/forms/input.html" with field=field %} - - {% elif field|is_textarea %} - - {% include "bds/forms/textarea.html" with field=field %} - - {% elif field|is_select %} - - {% include "bds/forms/select.html" with field=field %} - - {% elif field|is_file %} - - {% include "bds/forms/file.html" with field=field %} - - {% else %} - - {% include "bds/forms/other.html" with field=field %} - - {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/forms/file.html b/bds/templates/bds/forms/file.html deleted file mode 100644 index 65c249ef..00000000 --- a/bds/templates/bds/forms/file.html +++ /dev/null @@ -1,31 +0,0 @@ -{% load bulma_utils %} -{% load i18n %} - - - -
- - - - {% for error in field.errors %} - {{ error }} - {% endfor %} - - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/forms/form.html b/bds/templates/bds/forms/form.html deleted file mode 100644 index fdc9dc00..00000000 --- a/bds/templates/bds/forms/form.html +++ /dev/null @@ -1,22 +0,0 @@ -{% if errors %} - {% if form.non_field_errors %} -
-
- -
-
- {% for non_field_error in form.non_field_errors %} - {{ non_field_error }} - {% endfor %} -
-
- {% endif %} -{% endif %} - -{% for field in form.hidden_fields %} - {{ field }} -{% endfor %} - -{% for field in form.visible_fields %} - {% include 'bds/forms/field.html' %} -{% endfor %} \ No newline at end of file diff --git a/bds/templates/bds/forms/input.html b/bds/templates/bds/forms/input.html deleted file mode 100644 index 51c6f252..00000000 --- a/bds/templates/bds/forms/input.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load bulma_utils %} - - - -
- {{ field|bulmafy:'input' }} - - {% for error in field.errors %} - {{ error }} - {% endfor %} - - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/forms/other.html b/bds/templates/bds/forms/other.html deleted file mode 100644 index 4e1cdc8e..00000000 --- a/bds/templates/bds/forms/other.html +++ /dev/null @@ -1,19 +0,0 @@ -{% if field.auto_id %} - -{% endif %} - -
- {{ field }} - - {% for error in field.errors %} - {{ error }} - {% endfor %} - - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/forms/radio.html b/bds/templates/bds/forms/radio.html deleted file mode 100644 index d2e3b141..00000000 --- a/bds/templates/bds/forms/radio.html +++ /dev/null @@ -1,24 +0,0 @@ -{% if field.auto_id %} - -{% endif %} - -
- {% for choice in field %} - - {% endfor %} - - {% for error in field.errors %} - {{ error }} - {% endfor %} - - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/forms/select.html b/bds/templates/bds/forms/select.html deleted file mode 100644 index 2a064311..00000000 --- a/bds/templates/bds/forms/select.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load bulma_utils %} - - - -
- - {{ field }} - - - {% for error in field.errors %} - {{ error }} - {% endfor %} - - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/forms/textarea.html b/bds/templates/bds/forms/textarea.html deleted file mode 100644 index c5bc50f4..00000000 --- a/bds/templates/bds/forms/textarea.html +++ /dev/null @@ -1,19 +0,0 @@ -{% load bulma_utils %} - - - -
- {{ field|bulmafy:'textarea' }} - - {% for error in field.errors %} - {{ error }} - {% endfor %} - - {% if field.help_text %} -

- {{ field.help_text|safe }} -

- {% endif %} -
\ No newline at end of file diff --git a/bds/templates/bds/home.html b/bds/templates/bds/home.html deleted file mode 100644 index aedbaa25..00000000 --- a/bds/templates/bds/home.html +++ /dev/null @@ -1,62 +0,0 @@ -{% extends "bds/base.html" %} -{% load bulma_utils %} - -{% block layout %} -
-
-
-
-
-

{{ member_count }}

- adhérent·e·s -
-
-
-
-
-
- - {% if messages %} - {% for message in messages %} -
- {% if 'safe' in message.tags %} - {{ message|safe }} - {% else %} - {{ message }} - {% endif %} - -
- {% endfor %} - {% endif %} -
- Bienvenue sur GestioBDS ! - -
-
- - Télécharger la liste des membres (CSV) - - Liste des adhésions expirées ({{ nb_expired }}) - -
-
- - Le site est encore en développement. -
- Suivez notre avancement sur - - cette milestone sur le gitlab de l'ENS. -
- Faites vos remarques par mail à - klub-dev@ens.fr - ou en ouvrant une - - issue. -
-
-
-
-{% endblock layout %} - - - diff --git a/bds/templates/bds/nav.html b/bds/templates/bds/nav.html deleted file mode 100644 index ff167189..00000000 --- a/bds/templates/bds/nav.html +++ /dev/null @@ -1,57 +0,0 @@ -{% load i18n %} -{% load static %} - - - - diff --git a/bds/templates/bds/search_results.html b/bds/templates/bds/search_results.html deleted file mode 100644 index fec629df..00000000 --- a/bds/templates/bds/search_results.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "shared/search_results.html" %} -{% load i18n %} - -{% block extra_section %} -
  • - {% if not results %} - - {% trans "Aucune correspondance trouvée" %} - - {% else %} - - {% trans "Pas dans la liste ?" %} - - {% endif %} -
  • -
  • - - {% trans "Créer un compte" %} - -
  • -{% endblock %} diff --git a/bds/templates/bds/user_create.html b/bds/templates/bds/user_create.html deleted file mode 100644 index a36874cb..00000000 --- a/bds/templates/bds/user_create.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "bds/base.html" %} -{% load i18n %} - - -{% block content %} - -{% for form in forms.values %} - {% for error in form.non_field_errors %} -
    - {{ error }} -
    - {% endfor %} -{% endfor %} - -

    {% trans "Création d'un profil" %}

    - -
    -
    - {% csrf_token %} - - {% for form in forms.values %} - {% include "bds/forms/form.html" with form=form errors=False %} - {% endfor %} - -
    -

    - -

    -
    -
    -
    - -{% endblock %} diff --git a/bds/templates/bds/user_update.html b/bds/templates/bds/user_update.html deleted file mode 100644 index 1f54abe2..00000000 --- a/bds/templates/bds/user_update.html +++ /dev/null @@ -1,104 +0,0 @@ -{% extends "bds/base.html" %} -{% load i18n %} - - -{% block content %} - -{% for form in forms.values %} -{% for error in form.non_field_errors %} -
    - {{ error }} -
    -{% endfor %} -{% endfor %} - -

    {% trans "Modification du profil " %}{{ view.user.username }}

    - -
    -
    - {% csrf_token %} - - {% for form in forms.values %} - {% include "bds/forms/form.html" with form=form errors=False %} - {% endfor %} -
    - -
    - {% csrf_token %} -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -
    - - - - - - -{% endblock %} diff --git a/bds/templatetags/bulma_utils.py b/bds/templatetags/bulma_utils.py deleted file mode 100644 index e0e15e2f..00000000 --- a/bds/templatetags/bulma_utils.py +++ /dev/null @@ -1,74 +0,0 @@ -from django import forms, template - -register = template.Library() - - -@register.filter -def widget_type(field): - return field.field.widget - - -@register.filter -def is_select(field): - return isinstance(field.field.widget, forms.Select) - - -@register.filter -def is_multiple_select(field): - return isinstance(field.field.widget, forms.SelectMultiple) - - -@register.filter -def is_textarea(field): - return isinstance(field.field.widget, forms.Textarea) - - -@register.filter -def is_input(field): - return isinstance( - field.field.widget, - ( - forms.TextInput, - forms.NumberInput, - forms.EmailInput, - forms.PasswordInput, - forms.URLInput, - ), - ) - - -@register.filter -def is_checkbox(field): - return isinstance(field.field.widget, forms.CheckboxInput) - - -@register.filter -def is_multiple_checkbox(field): - return isinstance(field.field.widget, forms.CheckboxSelectMultiple) - - -@register.filter -def is_radio(field): - return isinstance(field.field.widget, forms.RadioSelect) - - -@register.filter -def is_file(field): - return isinstance(field.field.widget, forms.FileInput) - - -@register.filter -def bulmafy(field, css_class): - if len(field.errors) > 0: - css_class += " is-danger" - field_classes = field.field.widget.attrs.get("class", "") - field_classes += f" {css_class}" - return field.as_widget(attrs={"class": field_classes}) - - -@register.filter -def bulma_message_tag(tag): - if tag == "error": - return "danger" - - return tag diff --git a/bds/tests/test_views.py b/bds/tests/test_views.py deleted file mode 100644 index 332db8d7..00000000 --- a/bds/tests/test_views.py +++ /dev/null @@ -1,76 +0,0 @@ -from unittest import mock - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Permission -from django.test import Client, TestCase -from django.urls import reverse, reverse_lazy - -User = get_user_model() - - -def give_bds_buro_permissions(user: User) -> None: - perm = Permission.objects.get(content_type__app_label="bds", codename="is_team") - user.user_permissions.add(perm) - - -def login_url(next=None): - login_url = reverse_lazy(settings.LOGIN_URL) - if next is None: - return login_url - else: - return "{}?next={}".format(login_url, next) - - -class TestHomeView(TestCase): - @mock.patch("gestioncof.signals.messages") - def test_get(self, mock_messages): - user = User.objects.create_user(username="random_user") - give_bds_buro_permissions(user) - self.client.force_login( - user, backend="django.contrib.auth.backends.ModelBackend" - ) - resp = self.client.get(reverse("bds:home")) - self.assertEqual(resp.status_code, 200) - - -class TestRegistrationView(TestCase): - @mock.patch("gestioncof.signals.messages") - def test_get_autocomplete(self, mock_messages): - user = User.objects.create_user(username="toto") - url = reverse("bds:autocomplete") + "?q=foo" - client = Client() - - # Anonymous GET - resp = client.get(url) - self.assertRedirects(resp, login_url(next=url)) - - # Logged-in but unprivileged GET - client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") - resp = client.get(url) - self.assertEqual(resp.status_code, 403) - - # Burô user GET - give_bds_buro_permissions(user) - resp = client.get(url) - self.assertEqual(resp.status_code, 200) - - @mock.patch("gestioncof.signals.messages") - def test_get(self, mock_messages): - user = User.objects.create_user(username="toto") - url = reverse("bds:user.update", args=(user.id,)) - client = Client() - - # Anonymous GET - resp = client.get(url) - self.assertRedirects(resp, login_url(next=url)) - - # Logged-in but unprivileged GET - client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") - resp = client.get(url) - self.assertEqual(resp.status_code, 403) - - # Burô user GET - give_bds_buro_permissions(user) - resp = client.get(url) - self.assertEqual(resp.status_code, 200) diff --git a/bds/urls.py b/bds/urls.py deleted file mode 100644 index 68d780df..00000000 --- a/bds/urls.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.urls import path - -from bds import views -from shared.views import SympaListView - -app_name = "bds" -urlpatterns = [ - path("", views.Home.as_view(), name="home"), - path("autocomplete", views.BDSAutocompleteView.as_view(), name="autocomplete"), - path("user/update/", views.UserUpdateView.as_view(), name="user.update"), - path("user/create/", views.UserCreateView.as_view(), name="user.create"), - path( - "user/create/", - views.UserCreateView.as_view(), - name="user.create.fromclipper", - ), - path("user/delete/", views.UserDeleteView.as_view(), name="user.delete"), - path("members", views.export_members, name="export.members"), - path( - "members/expired", - views.ResetMembershipListView.as_view(), - name="members.expired", - ), - path("members/reset", views.ResetMembershipView.as_view(), name="members.reset"), - # Sympa export view - path( - "sympa/members/", - SympaListView.as_view(filters={"bds__is_member": True}), - name="export.sympa", - ), -] diff --git a/bds/views.py b/bds/views.py index 5fc4f316..60f00ef0 100644 --- a/bds/views.py +++ b/bds/views.py @@ -1,184 +1 @@ -import csv - -from django.contrib import messages -from django.contrib.auth import get_user_model -from django.contrib.auth.decorators import permission_required -from django.contrib.auth.models import Permission -from django.http import HttpResponse -from django.shortcuts import get_object_or_404 -from django.urls import reverse, reverse_lazy -from django.utils.translation import gettext_lazy as _ -from django.views.generic import DeleteView, ListView, RedirectView, TemplateView - -from bds.autocomplete import bds_search -from bds.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm -from bds.mixins import MultipleFormView, StaffRequiredMixin -from bds.models import BDSProfile -from shared.views import AutocompleteView - -User = get_user_model() - - -class BDSAutocompleteView(StaffRequiredMixin, AutocompleteView): - template_name = "bds/search_results.html" - search_composer = bds_search - - -class Home(StaffRequiredMixin, TemplateView): - template_name = "bds/home.html" - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["member_count"] = BDSProfile.objects.filter(is_member=True).count() - context["nb_expired"] = BDSProfile.expired_members().count() - return context - - -class UserUpdateView(StaffRequiredMixin, MultipleFormView): - template_name = "bds/user_update.html" - - form_classes = { - "user": UserForm, - "profile": ProfileForm, - } - - def get_user_initial(self): - return {"is_buro": self.get_user_instance().has_perm("bds.is_team")} - - def dispatch(self, request, *args, **kwargs): - self.user = get_object_or_404(User, pk=self.kwargs["pk"]) - return super().dispatch(request, *args, **kwargs) - - def get_user_instance(self): - return self.user - - def get_profile_instance(self): - return getattr(self.user, "bds", None) - - def get_success_url(self): - return reverse("bds:user.update", args=(self.user.pk,)) - - def form_valid(self, forms): - user = forms["user"].save() - profile = forms["profile"].save(commit=False) - perm = Permission.objects.get(content_type__app_label="bds", codename="is_team") - if forms["user"].cleaned_data["is_buro"]: - user.user_permissions.add(perm) - else: - user.user_permissions.remove(perm) - profile.user = user - profile.save() - messages.success(self.request, _("Profil mis à jour avec succès !")) - - return super().form_valid(forms) - - def form_invalid(self, forms): - messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous")) - return super().form_invalid(forms) - - -class UserCreateView(StaffRequiredMixin, MultipleFormView): - template_name = "bds/user_create.html" - - def get_form_classes(self): - profile_class = ProfileForm - - if "clipper" in self.kwargs: - user_class = UserFromClipperForm - else: - user_class = UserFromScratchForm - - return {"user": user_class, "profile": profile_class} - - def get_user_initial(self): - if "clipper" in self.kwargs: - clipper = self.kwargs["clipper"] - email = self.request.GET.get("mail", "{}@clipper.ens.fr".format(clipper)) - fullname = self.request.GET.get("fullname", None) - - if fullname: - # Heuristique : le premier mot est le prénom - first_name = fullname.split()[0] - last_name = " ".join(fullname.split()[1:]) - else: - first_name = "" - last_name = "" - - return { - "username": clipper, - "email": email, - "first_name": first_name, - "last_name": last_name, - } - else: - return {} - - def get_success_url(self): - return reverse("bds:user.update", args=(self.user.pk,)) - - def form_valid(self, forms): - # On redéfinit self.user pour get_success_url - self.user = forms["user"].save() - profile = forms["profile"].save(commit=False) - profile.user = self.user - profile.save() - messages.success(self.request, _("Profil créé avec succès !")) - - return super().form_valid(forms) - - def form_invalid(self, forms): - messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous")) - return super().form_invalid(forms) - - -class UserDeleteView(StaffRequiredMixin, DeleteView): - model = User - success_url = reverse_lazy("bds:home") - success_message = "Profil supprimé avec succès !" - - def delete(self, request, *args, **kwargs): - # SuccessMessageMixin does not work with DeleteView, see : - # https://code.djangoproject.com/ticket/21926 - messages.success(request, self.success_message) - - return super().delete(request, *args, **kwargs) - - -class ResetMembershipListView(StaffRequiredMixin, ListView): - model = BDSProfile - template_name = "bds/expired_members.html" - - def get_queryset(self): - return BDSProfile.expired_members() - - -class ResetMembershipView(StaffRequiredMixin, RedirectView): - url = reverse_lazy("bds:members.expired") - - def get(self, request, *args, **kwargs): - qs = BDSProfile.expired_members() - nb = qs.count() - - qs.update(cotisation_period="NO", is_member=False, mails_bds=False) - - messages.success(request, f"{nb} adhésions réinitialisées") - - return super().get(request, *args, **kwargs) - - -@permission_required("bds.is_team") -def export_members(request): - response = HttpResponse(content_type="text/csv") - response["Content-Disposition"] = "attachment; filename=membres_bds.csv" - - writer = csv.writer(response) - for profile in BDSProfile.objects.filter(is_member=True).all(): - user = profile.user - bits = [ - user.username, - user.get_full_name(), - user.email, - ] - writer.writerow([str(bit) for bit in bits]) - - return response +# Create your views here. diff --git a/clubs/migrations/0001_initial.py b/clubs/migrations/0001_initial.py index 8b9d60bc..689b5d33 100644 --- a/clubs/migrations/0001_initial.py +++ b/clubs/migrations/0001_initial.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] diff --git a/bds/tests/__init__.py b/cof/__init__.py similarity index 100% rename from bds/tests/__init__.py rename to cof/__init__.py diff --git a/gestioasso/apps.py b/cof/apps.py similarity index 100% rename from gestioasso/apps.py rename to cof/apps.py diff --git a/cof/asgi.py b/cof/asgi.py new file mode 100644 index 00000000..ab4ce291 --- /dev/null +++ b/cof/asgi.py @@ -0,0 +1,8 @@ +import os + +from channels.asgi import get_channel_layer + +if "DJANGO_SETTINGS_MODULE" not in os.environ: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings") + +channel_layer = get_channel_layer() diff --git a/gestioasso/__init__.py b/cof/locale/__init__.py similarity index 100% rename from gestioasso/__init__.py rename to cof/locale/__init__.py diff --git a/gestioasso/locale/__init__.py b/cof/locale/en/__init__.py similarity index 100% rename from gestioasso/locale/__init__.py rename to cof/locale/en/__init__.py diff --git a/gestioasso/locale/en/formats.py b/cof/locale/en/formats.py similarity index 100% rename from gestioasso/locale/en/formats.py rename to cof/locale/en/formats.py diff --git a/gestioasso/locale/en/__init__.py b/cof/locale/fr/__init__.py similarity index 100% rename from gestioasso/locale/en/__init__.py rename to cof/locale/fr/__init__.py diff --git a/gestioasso/locale/fr/formats.py b/cof/locale/fr/formats.py similarity index 100% rename from gestioasso/locale/fr/formats.py rename to cof/locale/fr/formats.py diff --git a/cof/routing.py b/cof/routing.py new file mode 100644 index 00000000..3c2e5718 --- /dev/null +++ b/cof/routing.py @@ -0,0 +1,3 @@ +from channels.routing import include + +routing = [include("kfet.routing.routing", path=r"^/ws/k-fet")] diff --git a/gestioasso/settings/.gitignore b/cof/settings/.gitignore similarity index 100% rename from gestioasso/settings/.gitignore rename to cof/settings/.gitignore diff --git a/gestioasso/locale/fr/__init__.py b/cof/settings/__init__.py similarity index 100% rename from gestioasso/locale/fr/__init__.py rename to cof/settings/__init__.py diff --git a/cof/settings/common.py b/cof/settings/common.py new file mode 100644 index 00000000..dd80a7b3 --- /dev/null +++ b/cof/settings/common.py @@ -0,0 +1,269 @@ +""" +Django common settings for cof project. + +Everything which is supposed to be identical between the production server and +the local development server should be here. +""" + +import os +import sys + +try: + from . import secret +except ImportError: + raise ImportError( + "The secret.py file is missing.\n" + "For a development environment, simply copy secret_example.py" + ) + + +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") +EMAIL_HOST = import_secret("EMAIL_HOST") + +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") + +KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") +LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL") + + +BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +TESTING = sys.argv[1] == "test" + + +# Application definition +INSTALLED_APPS = [ + "shared", + "gestioncof", + # Must be before 'django.contrib.admin'. + # https://django-autocomplete-light.readthedocs.io/en/master/install.html + "dal", + "dal_select2", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "django.contrib.messages", + "cof.apps.IgnoreSrcStaticFilesConfig", + # Must be before django admin + # https://github.com/infoportugal/wagtail-modeltranslation/issues/193 + "wagtail_modeltranslation", + "wagtail_modeltranslation.makemigrations", + "wagtail_modeltranslation.migrate", + "modeltranslation", + "django.contrib.admin", + "django.contrib.admindocs", + "bda", + "petitscours", + "journaldecaisse.apps.JournaldecaisseConfig", + "captcha", + "django_cas_ng", + "bootstrapform", + "kfet", + "kfet.open", + "channels", + "widget_tweaks", + "custommail", + "djconfig", + "wagtail.contrib.forms", + "wagtail.contrib.redirects", + "wagtail.embeds", + "wagtail.sites", + "wagtail.users", + "wagtail.snippets", + "wagtail.documents", + "wagtail.images", + "wagtail.search", + "wagtail.admin", + "wagtail.core", + "wagtail.contrib.modeladmin", + "wagtail.contrib.routable_page", + "wagtailmenus", + "modelcluster", + "taggit", + "kfet.auth", + "kfet.cms", + "gestioncof.cms", + "django_js_reverse", +] + + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + "django.middleware.security.SecurityMiddleware", + "djconfig.middleware.DjConfigMiddleware", + "wagtail.core.middleware.SiteMiddleware", + "wagtail.contrib.redirects.middleware.RedirectMiddleware", + "django.middleware.locale.LocaleMiddleware", +] + +ROOT_URLCONF = "cof.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + "django.template.context_processors.i18n", + "django.template.context_processors.media", + "django.template.context_processors.static", + "wagtailmenus.context_processors.wagtailmenus", + "djconfig.context_processors.config", + "gestioncof.shared.context_processor", + "kfet.auth.context_processors.temporary_auth", + "kfet.context_processors.config", + ] + }, + } +] + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql_psycopg2", + "NAME": DBNAME, + "USER": DBUSER, + "PASSWORD": DBPASSWD, + "HOST": os.environ.get("DBHOST", "localhost"), + } +} + + +# Internationalization +# https://docs.djangoproject.com/en/1.8/topics/i18n/ + +LANGUAGE_CODE = "fr-fr" + +TIME_ZONE = "Europe/Paris" + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + +LANGUAGES = (("fr", "Français"), ("en", "English")) + +# Various additional settings +SITE_ID = 1 + +GRAPPELLI_ADMIN_HEADLINE = "GestioCOF" +GRAPPELLI_ADMIN_TITLE = 'GestioCOF' + +MAIL_DATA = { + "petits_cours": { + "FROM": "Le COF ", + "BCC": "archivescof@gmail.com", + "REPLYTO": "cof@ens.fr", + }, + "rappels": {"FROM": "Le BdA ", "REPLYTO": "Le BdA "}, + "revente": { + "FROM": "BdA-Revente ", + "REPLYTO": "BdA-Revente ", + }, +} + +LOGIN_URL = "cof-login" +LOGIN_REDIRECT_URL = "home" + +CAS_SERVER_URL = "https://cas.eleves.ens.fr/" +CAS_VERSION = "2" +CAS_LOGIN_MSG = None +CAS_IGNORE_REFERER = True +CAS_REDIRECT_URL = "/" +CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" + +AUTHENTICATION_BACKENDS = ( + "django.contrib.auth.backends.ModelBackend", + "gestioncof.shared.COFCASBackend", + "kfet.auth.backends.GenericBackend", +) + + +# reCAPTCHA settings +# https://github.com/praekelt/django-recaptcha +# +# Default settings authorize reCAPTCHA usage for local developement. +# Public and private keys are appended in the 'prod' module settings. + +NOCAPTCHA = True +RECAPTCHA_USE_SSL = True + +CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") + +# 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 + +CHANNEL_LAYERS = { + "default": { + "BACKEND": "asgi_redis.RedisChannelLayer", + "CONFIG": { + "hosts": [ + ( + "redis://:{passwd}@{host}:{port}/{db}".format( + passwd=REDIS_PASSWD, + host=REDIS_HOST, + port=REDIS_PORT, + db=REDIS_DB, + ) + ) + ] + }, + "ROUTING": "cof.routing.routing", + } +} + +FORMAT_MODULE_PATH = "cof.locale" + +# Wagtail settings + +WAGTAIL_SITE_NAME = "GestioCOF" +WAGTAIL_ENABLE_UPDATE_CHECK = False +TAGGIT_CASE_INSENSITIVE = True + +# Django-js-reverse settings +JS_REVERSE_JS_VAR_NAME = "django_urls" +# Quand on aura namespace les urls... +# JS_REVERSE_INCLUDE_ONLY_NAMESPACES = ['k-fet'] diff --git a/cof/settings/dev.py b/cof/settings/dev.py new file mode 100644 index 00000000..d287eab8 --- /dev/null +++ b/cof/settings/dev.py @@ -0,0 +1,55 @@ +""" +Django development settings for the cof project. +The settings that are not listed here are imported from .common +""" + +import os + +from .common import * # NOQA +from .common import INSTALLED_APPS, MIDDLEWARE, TESTING + +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" + +DEBUG = True + +if TESTING: + PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] + +# As long as these apps are not ready for production, they are only available +# in development mode +INSTALLED_APPS += ["events", "bds", "clubs"] + + +# --- +# Apache static/media config +# --- + +STATIC_URL = "/static/" +STATIC_ROOT = "/srv/gestiocof/static/" + +MEDIA_ROOT = "/srv/gestiocof/media/" +MEDIA_URL = "/media/" + + +# --- +# Debug tool bar +# --- + + +def show_toolbar(request): + """ + On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar + car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la + machine physique n'est pas forcément connue, et peut difficilement être + mise dans les INTERNAL_IPS. + """ + env_no_ddt = bool(os.environ.get("DJANGO_NO_DDT", None)) + return DEBUG and not env_no_ddt and not request.path.startswith("/admin/") + + +if not TESTING: + INSTALLED_APPS += ["debug_toolbar"] + + MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE + + DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} diff --git a/cof/settings/local.py b/cof/settings/local.py new file mode 100644 index 00000000..06cdf4a0 --- /dev/null +++ b/cof/settings/local.py @@ -0,0 +1,31 @@ +""" +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 diff --git a/cof/settings/prod.py b/cof/settings/prod.py new file mode 100644 index 00000000..748abe73 --- /dev/null +++ b/cof/settings/prod.py @@ -0,0 +1,28 @@ +""" +Django development settings for the cof project. +The settings that are not listed here are imported from .common +""" + +import os + +from .common import * # NOQA +from .common import BASE_DIR, INSTALLED_APPS, TESTING, import_secret + +DEBUG = False + +ALLOWED_HOSTS = ["cof.ens.fr", "www.cof.ens.fr", "dev.cof.ens.fr"] + +if TESTING: + INSTALLED_APPS += ["events", "clubs"] + +STATIC_ROOT = os.path.join( + os.path.dirname(os.path.dirname(BASE_DIR)), "public", "gestion", "static" +) + +STATIC_URL = "/gestion/static/" +MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") +MEDIA_URL = "/gestion/media/" + + +RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") +RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") diff --git a/gestioasso/settings/secret_example.py b/cof/settings/secret_example.py similarity index 69% rename from gestioasso/settings/secret_example.py rename to cof/settings/secret_example.py index b93aeb4f..7921d467 100644 --- a/gestioasso/settings/secret_example.py +++ b/cof/settings/secret_example.py @@ -1,7 +1,3 @@ -""" -Secrets à re-définir en production. -""" - SECRET_KEY = "q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah" ADMINS = None SERVER_EMAIL = "root@vagrant" @@ -16,8 +12,8 @@ REDIS_PORT = 6379 REDIS_DB = 0 REDIS_HOST = "127.0.0.1" -HCAPTCHA_SITEKEY = "10000000-ffff-ffff-ffff-000000000001" -HCAPTCHA_SECRET = "0x0000000000000000000000000000000000000000" +RECAPTCHA_PUBLIC_KEY = "DUMMY" +RECAPTCHA_PRIVATE_KEY = "DUMMY" EMAIL_HOST = None diff --git a/cof/urls.py b/cof/urls.py new file mode 100644 index 00000000..9b24f932 --- /dev/null +++ b/cof/urls.py @@ -0,0 +1,153 @@ +""" +Fichier principal de configuration des urls du projet GestioCOF +""" + +from django.conf import settings +from django.conf.urls.i18n import i18n_patterns +from django.conf.urls.static import static +from django.contrib import admin +from django.contrib.auth import views as django_auth_views +from django.urls import include, path +from django.views.decorators.cache import cache_page +from django.views.generic.base import TemplateView +from django_cas_ng import views as django_cas_views +from django_js_reverse.views import urls_js +from wagtail.admin import urls as wagtailadmin_urls +from wagtail.core import urls as wagtail_urls +from wagtail.documents import urls as wagtaildocs_urls + + + +from gestioncof import csv_views, views as gestioncof_views +from gestioncof.autocomplete import autocomplete +from gestioncof.urls import ( + calendar_patterns, + clubs_patterns, + events_patterns, + export_patterns, + surveys_patterns, +) + +admin.autodiscover() + +urlpatterns = [ + # Page d'accueil + path("", gestioncof_views.HomeView.as_view(), name="home"), + # Le BdA + path("bda/", include("bda.urls")), + # Les exports + path("export/", include(export_patterns)), + # Les petits cours + path("petitcours/", include("petitscours.urls")), + # Les sondages + path("survey/", include(surveys_patterns)), + # Evenements + path("event/", include(events_patterns)), + # Calendrier + path("calendar/", include(calendar_patterns)), + #Journal de Caisse + path('journaldecaisse/', include('journaldecaisse.urls', namespace="journaldecaisse")), + # Clubs + path("clubs/", include(clubs_patterns)), + # Authentification + path( + "cof/denied", + TemplateView.as_view(template_name="cof-denied.html"), + name="cof-denied", + ), + path("cas/login", django_cas_views.LoginView.as_view(), name="cas_login_view"), + path("cas/logout", django_cas_views.LogoutView.as_view()), + path( + "outsider/login", gestioncof_views.LoginExtView.as_view(), name="ext_login_view" + ), + path( + "outsider/logout", django_auth_views.LogoutView.as_view(), {"next_page": "home"} + ), + path("login", gestioncof_views.login, name="cof-login"), + path("logout", gestioncof_views.logout, name="cof-logout"), + # Infos persos + path("profile", gestioncof_views.profile, name="profile"), + path( + "outsider/password-change", + django_auth_views.PasswordChangeView.as_view(), + name="password_change", + ), + path( + "outsider/password-change-done", + django_auth_views.PasswordChangeDoneView.as_view(), + name="password_change_done", + ), + # Inscription d'un nouveau membre + path("registration", gestioncof_views.registration, name="registration"), + path( + "registration/clipper//", + gestioncof_views.registration_form2, + name="clipper-registration", + ), + path( + "registration/user/", + gestioncof_views.registration_form2, + name="user-registration", + ), + path( + "registration/empty", + gestioncof_views.registration_form2, + name="empty-registration", + ), + # Autocompletion + path( + "autocomplete/registration", autocomplete, name="cof.registration.autocomplete" + ), + path( + "user/autocomplete", + gestioncof_views.user_autocomplete, + name="cof-user-autocomplete", + ), + # Interface admin + path("admin/logout/", gestioncof_views.logout), + path("admin/doc/", include("django.contrib.admindocs.urls")), + path( + "admin///csv/", + csv_views.admin_list_export, + {"fields": ["username"]}, + ), + path("admin/", admin.site.urls), + # Liens utiles du COF et du BdA + path("utile_cof", gestioncof_views.utile_cof, name="utile_cof"), + path("utile_bda", gestioncof_views.utile_bda, name="utile_bda"), + path("utile_bda/bda_diff", gestioncof_views.liste_bdadiff, name="ml_diffbda"), + path("utile_cof/diff_cof", gestioncof_views.liste_diffcof, name="ml_diffcof"), + path( + "utile_bda/bda_revente", + gestioncof_views.liste_bdarevente, + name="ml_bda_revente", + ), + path("k-fet/", include("kfet.urls")), + path("cms/", include(wagtailadmin_urls)), + path("documents/", include(wagtaildocs_urls)), + # djconfig + path("config", gestioncof_views.ConfigUpdate.as_view(), name="config.edit"), + # js-reverse + path("jsreverse/", urls_js, name="js_reverse"), +] + +if "events" in settings.INSTALLED_APPS: + # The new event application is still in development + # → for now it is namespaced below events_v2 + # → when the old events system is out, move this above in the others apps + urlpatterns += [path("event_v2/", include("events.urls"))] + +if "debug_toolbar" in settings.INSTALLED_APPS: + import debug_toolbar + + urlpatterns += [path("__debug__/", include(debug_toolbar.urls))] + +if settings.DEBUG: + # Si on est en production, MEDIA_ROOT est servi par Apache. + # Il faut dire à Django de servir MEDIA_ROOT lui-même en développement. + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + +# Wagtail for uncatched +urlpatterns += i18n_patterns( + path("", include(wagtail_urls)), prefix_default_language=False +) diff --git a/events/migrations/0001_event.py b/events/migrations/0001_event.py index 4b70d087..dc3a6bce 100644 --- a/events/migrations/0001_event.py +++ b/events/migrations/0001_event.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + initial = True dependencies = [] diff --git a/events/migrations/0002_event_subscribers.py b/events/migrations/0002_event_subscribers.py index b8980a78..7c0c35f7 100644 --- a/events/migrations/0002_event_subscribers.py +++ b/events/migrations/0002_event_subscribers.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("events", "0001_event"), diff --git a/events/migrations/0003_options_and_extra_fields.py b/events/migrations/0003_options_and_extra_fields.py deleted file mode 100644 index 0e993e7b..00000000 --- a/events/migrations/0003_options_and_extra_fields.py +++ /dev/null @@ -1,198 +0,0 @@ -# Generated by Django 2.2.8 on 2019-12-22 14:54 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("events", "0002_event_subscribers"), - ] - - operations = [ - migrations.CreateModel( - name="ExtraField", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField( - max_length=200, verbose_name="champ d'événement supplémentaire" - ), - ), - ( - "field_type", - models.CharField( - choices=[ - ("shorttext", "texte court (une ligne)"), - ("longtext", "texte long (plusieurs lignes)"), - ], - max_length=9, - verbose_name="type de champ", - ), - ), - ], - ), - migrations.CreateModel( - name="Option", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "name", - models.CharField(max_length=200, verbose_name="option d'événement"), - ), - ( - "multi_choices", - models.BooleanField(default=False, verbose_name="choix multiples"), - ), - ], - options={ - "verbose_name": "option d'événement", - "verbose_name_plural": "options d'événement", - }, - ), - migrations.CreateModel( - name="OptionChoice", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("choice", models.CharField(max_length=200, verbose_name="choix")), - ( - "option", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="choices", - to="events.Option", - ), - ), - ], - options={ - "verbose_name": "choix d'option d'événement", - "verbose_name_plural": "choix d'option d'événement", - }, - ), - migrations.CreateModel( - name="Registration", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ], - options={ - "verbose_name": "inscription à un événement", - "verbose_name_plural": "inscriptions à un événement", - }, - ), - migrations.RemoveField(model_name="event", name="subscribers"), - migrations.AddField( - model_name="event", - name="subscribers", - field=models.ManyToManyField( - through="events.Registration", - to=settings.AUTH_USER_MODEL, - verbose_name="inscrit⋅e⋅s", - ), - ), - migrations.AddField( - model_name="registration", - name="event", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="events.Event" - ), - ), - migrations.AddField( - model_name="registration", - name="options_choices", - field=models.ManyToManyField(to="events.OptionChoice"), - ), - migrations.AddField( - model_name="registration", - name="user", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL - ), - ), - migrations.AddField( - model_name="option", - name="event", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="options", - to="events.Event", - ), - ), - migrations.CreateModel( - name="ExtraFieldContent", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ("content", models.TextField(verbose_name="contenu du champ")), - ( - "field", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="events.ExtraField", - ), - ), - ( - "registration", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="extra_info", - to="events.Registration", - ), - ), - ], - options={ - "verbose_name": "contenu d'un champ événement supplémentaire", - "verbose_name_plural": "contenus d'un champ événement supplémentaire", - }, - ), - migrations.AddField( - model_name="extrafield", - name="event", - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name="extra_fields", - to="events.Event", - ), - ), - ] diff --git a/events/migrations/0004_unique_constraints.py b/events/migrations/0004_unique_constraints.py deleted file mode 100644 index 4e73a8a8..00000000 --- a/events/migrations/0004_unique_constraints.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 2.2.12 on 2020-05-20 15:41 - -from django.conf import settings -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("events", "0003_options_and_extra_fields"), - ] - - operations = [ - migrations.AlterUniqueTogether( - name="extrafield", - unique_together={("event", "name")}, - ), - migrations.AlterUniqueTogether( - name="extrafieldcontent", - unique_together={("field", "registration")}, - ), - migrations.AlterUniqueTogether( - name="option", - unique_together={("event", "name")}, - ), - migrations.AlterUniqueTogether( - name="optionchoice", - unique_together={("option", "choice")}, - ), - migrations.AlterUniqueTogether( - name="registration", - unique_together={("event", "user")}, - ), - ] diff --git a/events/migrations/0005_auto_20220630_1239.py b/events/migrations/0005_auto_20220630_1239.py deleted file mode 100644 index d2624da2..00000000 --- a/events/migrations/0005_auto_20220630_1239.py +++ /dev/null @@ -1,63 +0,0 @@ -# Generated by Django 3.2.13 on 2022-06-30 10:39 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("events", "0004_unique_constraints"), - ] - - operations = [ - migrations.AlterUniqueTogether( - name="extrafield", - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name="extrafieldcontent", - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name="option", - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name="optionchoice", - unique_together=set(), - ), - migrations.AlterUniqueTogether( - name="registration", - unique_together=set(), - ), - migrations.AddConstraint( - model_name="extrafield", - constraint=models.UniqueConstraint( - fields=("event", "name"), name="unique_extra_field" - ), - ), - migrations.AddConstraint( - model_name="extrafieldcontent", - constraint=models.UniqueConstraint( - fields=("field", "registration"), name="unique_extra_field_content" - ), - ), - migrations.AddConstraint( - model_name="option", - constraint=models.UniqueConstraint( - fields=("event", "name"), name="unique_event_option" - ), - ), - migrations.AddConstraint( - model_name="optionchoice", - constraint=models.UniqueConstraint( - fields=("option", "choice"), name="unique_option_choice" - ), - ), - migrations.AddConstraint( - model_name="registration", - constraint=models.UniqueConstraint( - fields=("event", "user"), name="unique_registration" - ), - ), - ] diff --git a/events/models.py b/events/models.py index a421e8a3..b2876301 100644 --- a/events/models.py +++ b/events/models.py @@ -1,35 +1,4 @@ -""" -Event framework for GestioCOF and GestioBDS. - -The events implemented in this module provide two types of customisations to event -creators (the COF and BDS staff): options and extra (text) fields. - -Options -------- - -An option is an extra field in the registration form with a predefined list of available -choices. Any number of options can be added to an event. - -For instance, a typical use-case is events where meals are served to participants -with different possible menus, say: vegeterian / vegan / without pork / etc. This -example can be implemented with an `Option(name="menu")` and an `OptionChoice` for each -available menu. - -In this example, the choice was exclusive: participants can only chose one menu. For -situations, where multiple choices can be made at the same time, use the `multi_choices` -flag. - -Extra fields ------------- - -Extra fields can also be added to the registration form that can receive arbitrary text. -Typically, this can be a "remark" field (prefer the LONGTEXT option in this case) or -small form entries such as "phone number" or "emergency contact" (prefer the SHORTTEXT -option in this case). -""" - from django.contrib.auth import get_user_model -from django.core.exceptions import ValidationError from django.db import models from django.utils.translation import gettext_lazy as _ @@ -47,9 +16,7 @@ class Event(models.Model): ) registration_open = models.BooleanField(_("inscriptions ouvertes"), default=True) old = models.BooleanField(_("archiver (événement fini)"), default=False) - subscribers = models.ManyToManyField( - User, through="Registration", verbose_name=_("inscrit⋅e⋅s") - ) + subscribers = models.ManyToManyField(User, verbose_name=_("inscrit⋅e⋅s")) class Meta: verbose_name = _("événement") @@ -59,131 +26,8 @@ class Event(models.Model): return self.title -class Option(models.Model): - """Extra form fields with a limited set of available choices. +# TODO: gérer les options (EventOption & EventOptionChoice de gestioncof) +# par exemple: "option végé au Mega (oui / non)" - The available choices are given by `OptionChoice`s (see below). A typical use-case - is events where the participants have the choice between different menus (e.g. - vegan / vegetarian / without-pork / etc). - """ - - event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="options") - name = models.CharField(_("option d'événement"), max_length=200) - multi_choices = models.BooleanField(_("choix multiples"), default=False) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["event", "name"], name="unique_event_option" - ) - ] - verbose_name = _("option d'événement") - verbose_name_plural = _("options d'événement") - - def __str__(self): - return self.name - - -class OptionChoice(models.Model): - """A possible choice for an event option.""" - - option = models.ForeignKey(Option, on_delete=models.CASCADE, related_name="choices") - choice = models.CharField(_("choix"), max_length=200) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["option", "choice"], name="unique_option_choice" - ) - ] - verbose_name = _("choix d'option d'événement") - verbose_name_plural = _("choix d'option d'événement") - - def __str__(self): - return self.choice - - -class ExtraField(models.Model): - """Extra event field receiving arbitrary text. - - Extra text field that can be added by event creators to the event registration form. - Typical examples are "remarks" fields (of type LONGTEXT) or more specific fields - such as "emergency contact" (of type SHORTTEXT probably?). - """ - - LONGTEXT = "longtext" - SHORTTEXT = "shorttext" - - FIELD_TYPE = [ - (SHORTTEXT, _("texte court (une ligne)")), - (LONGTEXT, _("texte long (plusieurs lignes)")), - ] - - event = models.ForeignKey( - Event, on_delete=models.CASCADE, related_name="extra_fields" - ) - name = models.CharField(_("champ d'événement supplémentaire"), max_length=200) - field_type = models.CharField(_("type de champ"), max_length=9, choices=FIELD_TYPE) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=["event", "name"], name="unique_extra_field") - ] - - -class ExtraFieldContent(models.Model): - """Value entered in an extra field.""" - - field = models.ForeignKey(ExtraField, on_delete=models.CASCADE) - registration = models.ForeignKey( - "Registration", on_delete=models.CASCADE, related_name="extra_info" - ) - content = models.TextField(_("contenu du champ")) - - def clean(self): - if self.registration.event != self.field.event: - raise ValidationError( - _("Inscription et champ texte incohérents pour ce commentaire") - ) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["field", "registration"], name="unique_extra_field_content" - ) - ] - verbose_name = _("contenu d'un champ événement supplémentaire") - verbose_name_plural = _("contenus d'un champ événement supplémentaire") - - def __str__(self): - max_length = 50 - if len(self.content) > max_length: - return self.content[: max_length - 1] + "…" - else: - return self.content - - -class Registration(models.Model): - """A user registration to an event.""" - - event = models.ForeignKey(Event, on_delete=models.CASCADE) - user = models.ForeignKey(User, on_delete=models.CASCADE) - options_choices = models.ManyToManyField(OptionChoice) - - def clean(self): - if not all((ch.option.event == self.event for ch in self.options_choices)): - raise ValidationError( - _("Choix d'options incohérents avec l'événement pour cette inscription") - ) - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=["event", "user"], name="unique_registration" - ) - ] - verbose_name = _("inscription à un événement") - verbose_name_plural = _("inscriptions à un événement") - - def __str__(self): - return "inscription de {} à {}".format(self.user, self.event) +# TODO: gérer les champs commentaires (EventCommentField & EventCommentChoice) +# par exemple: "champ "allergies / régime particulier" au Mega diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 611f1871..5dc01fbb 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -1,20 +1,11 @@ from unittest import mock -from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.test import Client, TestCase from django.urls import reverse -from events.models import ( - Event, - ExtraField, - ExtraFieldContent, - Option, - OptionChoice, - Registration, -) -from shared.tests.mixins import CSVResponseMixin +from events.models import Event User = get_user_model() @@ -25,15 +16,14 @@ def make_user(name): def make_staff_user(name): view_event_perm = Permission.objects.get( - codename="view_event", - content_type__app_label="events", + codename="view_event", content_type__app_label="events", ) user = make_user(name) user.user_permissions.add(view_event_perm) return user -class MessagePatch: +class CSVExportTest(TestCase): def setUp(self): # Signals handlers on login/logout send messages. # Due to the way the Django' test Client performs login, this raise an @@ -42,120 +32,28 @@ class MessagePatch: patcher_messages.start() self.addCleanup(patcher_messages.stop) - -class CSVExportAccessTest(MessagePatch, TestCase): - def setUp(self): - super().setUp() - self.staff = make_staff_user("staff") self.u1 = make_user("toto") + self.u2 = make_user("titi") self.event = Event.objects.create(title="test_event", location="somewhere") + self.event.subscribers.set([self.u1, self.u2]) self.url = reverse("events:csv-participants", args=[self.event.id]) def test_get(self): client = Client() - client.force_login( - self.staff, backend="django.contrib.auth.backends.ModelBackend" - ) + client.force_login(self.staff) r = client.get(self.url) self.assertEqual(r.status_code, 200) def test_anonymous(self): client = Client() r = client.get(self.url) - login_url = "{}?next={}".format(reverse(settings.LOGIN_URL), self.url) - self.assertRedirects(r, login_url, fetch_redirect_response=False) + self.assertRedirects( + r, "/login?next={}".format(self.url), fetch_redirect_response=False + ) def test_unauthorised(self): client = Client() - client.force_login(self.u1, backend="django.contrib.auth.backends.ModelBackend") + client.force_login(self.u1) r = client.get(self.url) self.assertEqual(r.status_code, 403) - - -class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): - def setUp(self): - super().setUp() - - self.event = Event.objects.create(title="test_event", location="somewhere") - self.url = reverse("events:csv-participants", args=[self.event.id]) - - self.u1 = User.objects.create_user( - username="toto_foo", first_name="toto", last_name="foo", email="toto@a.b" - ) - self.u2 = User.objects.create_user( - username="titi_bar", first_name="titi", last_name="bar", email="titi@a.b" - ) - self.staff = make_staff_user("staff") - self.client = Client() - self.client.force_login( - self.staff, backend="django.contrib.auth.backends.ModelBackend" - ) - - def test_simple_event(self): - self.event.subscribers.set([self.u1, self.u2]) - - response = self.client.get(self.url) - - self.assertCSVEqual( - response, - [ - { - "username": "toto_foo", - "prénom": "toto", - "nom de famille": "foo", - "email": "toto@a.b", - }, - { - "username": "titi_bar", - "prénom": "titi", - "nom de famille": "bar", - "email": "titi@a.b", - }, - ], - ) - - def test_complex_event(self): - registration = Registration.objects.create(event=self.event, user=self.u1) - # Set up some options - option1 = Option.objects.create( - event=self.event, name="abc", multi_choices=False - ) - option2 = Option.objects.create( - event=self.event, name="def", multi_choices=True - ) - OptionChoice.objects.bulk_create( - [ - OptionChoice(option=option1, choice="a"), - OptionChoice(option=option1, choice="b"), - OptionChoice(option=option1, choice="c"), - OptionChoice(option=option2, choice="d"), - OptionChoice(option=option2, choice="e"), - OptionChoice(option=option2, choice="f"), - ] - ) - registration.options_choices.set( - OptionChoice.objects.filter(choice__in=["d", "f"]) - ) - registration.options_choices.add(OptionChoice.objects.get(choice="a")) - # And an extra field - field = ExtraField.objects.create(event=self.event, name="remarks") - ExtraFieldContent.objects.create( - field=field, registration=registration, content="hello" - ) - - response = self.client.get(self.url) - self.assertCSVEqual( - response, - [ - { - "username": "toto_foo", - "prénom": "toto", - "nom de famille": "foo", - "email": "toto@a.b", - "abc": "a", - "def": "d & f", - "remarks": "hello", - } - ], - ) diff --git a/events/views.py b/events/views.py index b47ae76f..6f49cdb7 100644 --- a/events/views.py +++ b/events/views.py @@ -5,7 +5,7 @@ from django.http import HttpResponse from django.shortcuts import get_object_or_404 from django.utils.text import slugify -from events.models import Event, Registration +from events.models import Event @login_required @@ -13,43 +13,13 @@ from events.models import Event, Registration def participants_csv(request, event_id): event = get_object_or_404(Event, id=event_id) - # Create a CSV response filename = "{}-participants.csv".format(slugify(event.title)) response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = 'attachment; filename="{}"'.format(filename) + writer = csv.writer(response) - - # The first line of the file is a header - header = ["username", "email", "prénom", "nom de famille"] - options_names = list(event.options.values_list("name", flat=True).order_by("id")) - header += options_names - extra_fields = list( - event.extra_fields.values_list("name", flat=True).order_by("id") - ) - header += extra_fields - writer.writerow(header) - - # Next, one line by registered user - registrations = Registration.objects.filter(event=event) - for registration in registrations: - user = registration.user - row = [user.username, user.email, user.first_name, user.last_name] - - # Options - all_choices = registration.options_choices.values_list("choice", flat=True) - options_choices = [ - " & ".join(all_choices.filter(option__id=id).order_by("id")) - for id in event.options.values_list("id", flat=True).order_by("id") - ] - row += options_choices - # Extra info - extra_info = list( - registration.extra_info.values_list("content", flat=True).order_by( - "field__id" - ) - ) - row += extra_info - - writer.writerow(row) + writer.writerow(["username", "email", "prénom", "nom de famille"]) + for user in event.subscribers.all(): + writer.writerow([user.username, user.email, user.first_name, user.last_name]) return response diff --git a/gestioasso/asgi.py b/gestioasso/asgi.py deleted file mode 100644 index 728a3433..00000000 --- a/gestioasso/asgi.py +++ /dev/null @@ -1,15 +0,0 @@ -""" -ASGI entrypoint. Configures Django and then runs the application -defined in the ASGI_APPLICATION setting. -""" - -import os - -import django -from channels.routing import get_default_application - -if "DJANGO_SETTINGS_MODULE" not in os.environ: - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local") - -django.setup() -application = get_default_application() diff --git a/gestioasso/routing.py b/gestioasso/routing.py deleted file mode 100644 index 2b42648a..00000000 --- a/gestioasso/routing.py +++ /dev/null @@ -1,20 +0,0 @@ -from channels.auth import AuthMiddlewareStack -from channels.routing import ProtocolTypeRouter, URLRouter -from django.core.asgi import get_asgi_application -from django.urls import path - -from kfet.routing import KFRouter - -application = ProtocolTypeRouter( - { - # WebSocket chat handler - "websocket": AuthMiddlewareStack( - URLRouter( - [ - path("ws/k-fet", KFRouter), - ] - ) - ), - "http": get_asgi_application(), - } -) diff --git a/gestioasso/settings/bds_prod.py b/gestioasso/settings/bds_prod.py deleted file mode 100644 index 361ed7cb..00000000 --- a/gestioasso/settings/bds_prod.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -Settings de production de GestioBDS. - -Surcharge les settings définis dans common.py -""" - -from .common import * # NOQA -from .common import INSTALLED_APPS - -# --- -# BDS-only Django settings -# --- - -ALLOWED_HOSTS = ["bds.ens.fr", "www.bds.ens.fr", "dev.cof.ens.fr"] - -INSTALLED_APPS += ["bds", "events", "clubs", "authens"] - -STATIC_ROOT = "/srv/bds.ens.fr/public/gestion2/static" -STATIC_URL = "/gestion2/static/" -MEDIA_ROOT = "/srv/bds.ens.fr/gestion2/media" -MEDIA_URL = "/gestion2/media/" - - -# --- -# Auth-related stuff -# --- - -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "authens.backends.ENSCASBackend", - "authens.backends.OldCASBackend", -] - -AUTHENS_USE_OLDCAS = False - -LOGIN_URL = "authens:login" -LOGIN_REDIRECT_URL = "bds:home" -LOGOUT_REDIRECT_URL = "bds:home" diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py deleted file mode 100644 index 91bee648..00000000 --- a/gestioasso/settings/cof_prod.py +++ /dev/null @@ -1,233 +0,0 @@ -""" -Settings de production de GestioCOF. - -Surcharge les settings définis dans common.py -""" - -import os -from datetime import timedelta - -from django.utils import timezone - -from .common import * # NOQA -from .common import ( - AUTHENTICATION_BACKENDS, - BASE_DIR, - INSTALLED_APPS, - MIDDLEWARE, - TEMPLATES, - import_secret, -) - -# --- -# COF-specific secrets -# --- - -REDIS_PASSWD = import_secret("REDIS_PASSWD") -REDIS_DB = import_secret("REDIS_DB") -REDIS_HOST = import_secret("REDIS_HOST") -REDIS_PORT = import_secret("REDIS_PORT") - -HCAPTCHA_SITEKEY = import_secret("HCAPTCHA_SITEKEY") -HCAPTCHA_SECRET = import_secret("HCAPTCHA_SECRET") -KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") - -# --- -# COF-only Django settings -# --- - -ALLOWED_HOSTS = ["cof.ens.fr", "www.cof.ens.fr", "dev.cof.ens.fr"] - -INSTALLED_APPS = ( - [ - "gestioncof", - # Must be before django admin - # https://github.com/infoportugal/wagtail-modeltranslation/issues/193 - "wagtail_modeltranslation", - "wagtail_modeltranslation.makemigrations", - "wagtail_modeltranslation.migrate", - "modeltranslation", - ] - + INSTALLED_APPS - + [ - "bda", - "petitscours", - "hcaptcha", - "kfet", - "kfet.open", - "channels", - "djconfig", - "wagtail.contrib.forms", - "wagtail.contrib.redirects", - "wagtail.embeds", - "wagtail.sites", - "wagtail.users", - "wagtail.snippets", - "wagtail.documents", - "wagtail.images", - "wagtail.search", - "wagtail.admin", - "wagtail", - # "wagtail.contrib.modeladmin", - "wagtail.contrib.routable_page", - "wagtailmenus", - "modelcluster", - "taggit", - "kfet.auth", - "kfet.cms", - "gestioncof.cms", - "django_js_reverse", - ] -) - -MIDDLEWARE = ( - ["corsheaders.middleware.CorsMiddleware"] - + MIDDLEWARE - + [ - "djconfig.middleware.DjConfigMiddleware", - "wagtail.contrib.redirects.middleware.RedirectMiddleware", - ] -) - -TEMPLATES[0]["OPTIONS"]["context_processors"] += [ - "wagtailmenus.context_processors.wagtailmenus", - "djconfig.context_processors.config", - "gestioncof.shared.context_processor", - "kfet.auth.context_processors.temporary_auth", - "kfet.context_processors.config", -] - -STATIC_ROOT = os.path.join( - os.path.dirname(os.path.dirname(BASE_DIR)), "public", "gestion", "static" -) - -STATIC_URL = "/gestion/static/" -MEDIA_ROOT = os.path.join(os.path.dirname(BASE_DIR), "media") -MEDIA_URL = "/gestion/media/" - -CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.ens.fr") - - -ASGI_APPLICATION = "gestioasso.routing.application" - -# --- -# Auth-related stuff -# --- - -AUTHENTICATION_BACKENDS = ( - [ - # Must be in first - "kfet.auth.backends.BlockFrozenAccountBackend" - ] - + AUTHENTICATION_BACKENDS - + [ - "gestioncof.shared.COFCASBackend", - "kfet.auth.backends.GenericBackend", - ] -) -LOGIN_URL = "cof-login" -LOGIN_REDIRECT_URL = "home" - -# --- -# 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 -# --- - -CHANNEL_LAYERS = { - "default": { - "BACKEND": "shared.channels.ChannelLayer", - "CONFIG": { - "hosts": [ - ( - "redis://:{passwd}@{host}:{port}/{db}".format( - passwd=REDIS_PASSWD, - host=REDIS_HOST, - port=REDIS_PORT, - db=REDIS_DB, - ) - ) - ] - }, - } -} - -# --- -# reCAPTCHA settings -# https://github.com/praekelt/django-recaptcha -# -# Default settings authorize reCAPTCHA usage for local developement. -# Public and private keys are appended in the 'prod' module settings. -# --- - -NOCAPTCHA = True -RECAPTCHA_USE_SSL = True - - -# --- -# Wagtail settings -# --- - -WAGTAIL_SITE_NAME = "GestioCOF" -WAGTAIL_ENABLE_UPDATE_CHECK = False -TAGGIT_CASE_INSENSITIVE = True - - -# --- -# Django-js-reverse settings -# --- - -JS_REVERSE_JS_VAR_NAME = "django_urls" -# Quand on aura namespace les urls... -# JS_REVERSE_INCLUDE_ONLY_NAMESPACES = ['k-fet'] - - -# --- -# Mail config -# --- - -MAIL_DATA = { - "petits_cours": { - "FROM": "Le COF ", - "BCC": "archivescof@gmail.com", - "REPLYTO": "cof@ens.fr", - }, - "rappels": {"FROM": "Le BdA ", "REPLYTO": "Le BdA "}, - "kfet": { - "FROM": "La K-Fêt ", - "REPLYTO": "La K-Fêt ", - }, - "revente": { - "FROM": "BdA-Revente ", - "REPLYTO": "BdA-Revente ", - }, -} - -# --- -# kfet history limits -# --- - -# L'historique n'est accesible que d'aujourd'hui -# à aujourd'hui - KFET_HISTORY_DATE_LIMIT -KFET_HISTORY_DATE_LIMIT = timedelta(days=7) - -# Limite plus longue pour les chefs/trez -# (qui ont la permission kfet.access_old_history) -KFET_HISTORY_LONG_DATE_LIMIT = timedelta(days=30) - -# These accounts don't represent actual people and can be freely accessed -# Identification based on trigrammes -KFET_HISTORY_NO_DATE_LIMIT_TRIGRAMMES = ["LIQ", "#13"] -KFET_HISTORY_NO_DATE_LIMIT = timezone.datetime(1794, 10, 30) # AKA the distant past diff --git a/gestioasso/settings/common.py b/gestioasso/settings/common.py deleted file mode 100644 index 13f2e5b1..00000000 --- a/gestioasso/settings/common.py +++ /dev/null @@ -1,141 +0,0 @@ -""" -Settings par défaut et settings communs à GestioCOF et GestioBDS. -""" - -import os -import sys - -# --- -# Secrets -# --- - -try: - from . import secret -except ImportError: - raise ImportError( - "The secret.py file is missing.\n" - "For a development environment, simply copy secret_example.py" - ) - - -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") -EMAIL_HOST = import_secret("EMAIL_HOST") - -DBNAME = import_secret("DBNAME") -DBUSER = import_secret("DBUSER") -DBPASSWD = import_secret("DBPASSWD") - -LDAP_SERVER_URL = import_secret("LDAP_SERVER_URL") - - -# --- -# Default Django settings -# --- - -DEBUG = False # False by default feels safer -TESTING = len(sys.argv) > 1 and sys.argv[1] == "test" -BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) - -INSTALLED_APPS = [ - "shared", - # Must be before 'django.contrib.admin'. - # https://django-autocomplete-light.readthedocs.io/en/master/install.html - "dal", - "dal_select2", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.sites", - "django.contrib.messages", - "django.contrib.admin", - "django.contrib.admindocs", - "gestioasso.apps.IgnoreSrcStaticFilesConfig", - "django_cas_ng", - "bootstrapform", - "widget_tweaks", -] - -MIDDLEWARE = [ - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django.middleware.security.SecurityMiddleware", - "django.middleware.locale.LocaleMiddleware", -] - -ROOT_URLCONF = "gestioasso.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.template.context_processors.i18n", - "django.template.context_processors.media", - "django.template.context_processors.static", - ] - }, - } -] - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.postgresql", - "NAME": DBNAME, - "USER": DBUSER, - "PASSWORD": DBPASSWD, - "HOST": os.environ.get("DBHOST", "localhost"), - } -} - -SITE_ID = 1 - -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - -# --- -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ -# --- - -LANGUAGE_CODE = "fr-fr" -TIME_ZONE = "Europe/Paris" -USE_I18N = True -USE_L10N = True -USE_TZ = True -LANGUAGES = (("fr", "Français"), ("en", "English")) -FORMAT_MODULE_PATH = "gestioasso.locale" - - -# --- -# Auth-related stuff -# --- - -AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] - -CAS_SERVER_URL = "https://cas.eleves.ens.fr/" -CAS_VERSION = "2" -CAS_LOGIN_MSG = None -CAS_IGNORE_REFERER = True -CAS_REDIRECT_URL = "/" -CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" diff --git a/gestioasso/settings/dev.py b/gestioasso/settings/dev.py deleted file mode 100644 index cd254b7a..00000000 --- a/gestioasso/settings/dev.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Settings utilisés dans la VM Vagrant. -Active toutes les applications (de GestioCOF et de GestioBDS). - -Surcharge les settings définis dans common.py -""" - -import os - -from . import bds_prod -from .cof_prod import * # NOQA -from .cof_prod import INSTALLED_APPS, MIDDLEWARE, TESTING - -# --- -# Merge COF and BDS configs -# --- - -for app in bds_prod.INSTALLED_APPS: - if app not in INSTALLED_APPS: - INSTALLED_APPS.append(app) - -# --- -# Tweaks for debug/local development -# --- - -ALLOWED_HOSTS = [] - -DEBUG = True -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -if TESTING: - PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] - -STATIC_URL = "/static/" -STATIC_ROOT = "/srv/gestiocof/static" -MEDIA_URL = "/media/" -MEDIA_ROOT = "/srv/gestiocof/media" - - -# --- -# Debug tool bar -# --- - - -def show_toolbar(request): - """ - On active la debug-toolbar en mode développement local sauf : - - dans l'admin où ça ne sert pas à grand chose; - - si la variable d'environnement DJANGO_NO_DDT est à 1 → ça permet de la désactiver - sans modifier ce fichier en exécutant `export DJANGO_NO_DDT=1` dans le terminal - qui lance `./manage.py runserver`. - - Autre side effect de cette fonction : on ne fait pas la vérification de INTERNAL_IPS - que ferait la debug-toolbar par défaut, ce qui la fait fonctionner aussi à - l'intérieur de Vagrant (comportement non testé depuis un moment…) - """ - env_no_ddt = bool(os.environ.get("DJANGO_NO_DDT", None)) - return DEBUG and not env_no_ddt and not request.path.startswith("/admin/") - - -if not TESTING: - INSTALLED_APPS += ["debug_toolbar"] - MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE - DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} diff --git a/gestioasso/settings/local.py b/gestioasso/settings/local.py deleted file mode 100644 index c6a22e64..00000000 --- a/gestioasso/settings/local.py +++ /dev/null @@ -1,82 +0,0 @@ -""" -Settings utilisés lors d'un développement en local (dans un virtualenv). -Active toutes les applications (de GestioCOF et de GestioBDS). - -Surcharge les settings définis dans common.py -""" -import os - -from . import bds_prod -from .cof_prod import * # NOQA -from .cof_prod import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING - -# --- -# Merge COF and BDS configs -# --- - -for app in bds_prod.INSTALLED_APPS: - if app not in INSTALLED_APPS: - INSTALLED_APPS.append(app) - -# --- -# Tweaks for debug/local development -# --- - -ALLOWED_HOSTS = [] - -DEBUG = True -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - -SYMPA_PASSWORD = b"sympa" -SYMPA_USERNAME = b"sympa" - -if TESTING: - PASSWORD_HASHERS = ["django.contrib.auth.hashers.MD5PasswordHasher"] - -STATIC_URL = "/static/" -MEDIA_URL = "/media/" -MEDIA_ROOT = os.path.join(BASE_DIR, "media") - -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": "channels.layers.InMemoryChannelLayer", - } -} - - -# --- -# Debug tool bar -# --- - - -def show_toolbar(request): - """ - On active la debug-toolbar en mode développement local sauf : - - dans l'admin où ça ne sert pas à grand chose; - - si la variable d'environnement DJANGO_NO_DDT est à 1 → ça permet de la désactiver - sans modifier ce fichier en exécutant `export DJANGO_NO_DDT=1` dans le terminal - qui lance `./manage.py runserver`. - - Autre side effect de cette fonction : on ne fait pas la vérification de INTERNAL_IPS - que ferait la debug-toolbar par défaut, ce qui la fait fonctionner aussi à - l'intérieur de Vagrant (comportement non testé depuis un moment…) - """ - env_no_ddt = bool(os.environ.get("DJANGO_NO_DDT", None)) - return DEBUG and not env_no_ddt and not request.path.startswith("/admin/") - - -if not TESTING: - INSTALLED_APPS += ["debug_toolbar"] - MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE - DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} diff --git a/gestioasso/settings_bds.py b/gestioasso/settings_bds.py deleted file mode 100644 index e640b222..00000000 --- a/gestioasso/settings_bds.py +++ /dev/null @@ -1,197 +0,0 @@ -""" -Django settings for the gestioBDS project. -""" - -import os -from pathlib import Path - -from loadcredential import Credentials - -credentials = Credentials(env_prefix="GESTIOBDS_") - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# WARNING: keep the secret key used in production secret! -SECRET_KEY = credentials["SECRET_KEY"] - -# WARNING: don't run with debug turned on in production! -DEBUG = credentials.get_json("DEBUG", False) - -ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", []) -ADMINS = credentials.get_json("ADMINS", []) - -SERVER_EMAIL = credentials.get("SERVER_EMAIL") -EMAIL_HOST = credentials.get("EMAIL_HOST") - -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - -SYMPA_PASSWORD = credentials["SYMPA_PASSWORD"].encode() -SYMPA_USERNAME = credentials["SYMPA_USERNAME"].encode() - - -## -# Installed Apps configuration - -INSTALLED_APPS = [ - "shared", - # Must be before 'django.contrib.admin'. - # https://django-autocomplete-light.readthedocs.io/en/master/install.html - "dal", - "dal_select2", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.sites", - "django.contrib.messages", - "django.contrib.admin", - "django.contrib.admindocs", - "gestioasso.apps.IgnoreSrcStaticFilesConfig", - "django_cas_ng", - "bootstrapform", - "widget_tweaks", - "bds", - "events", - "clubs", - "authens", -] - - -## -# Middleware configuration - -MIDDLEWARE = [ - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django.middleware.security.SecurityMiddleware", - "django.middleware.locale.LocaleMiddleware", -] - - -## -# URL configuration - -ROOT_URLCONF = "gestioasso.urls" - - -## -# Templates configuration - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.template.context_processors.i18n", - "django.template.context_processors.media", - "django.template.context_processors.static", - ] - }, - } -] - - -## -# Database configuration - -DATABASES = credentials.get_json( - "DATABASES", - default={ - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": (BASE_DIR / "db.sqlite3"), - } - }, -) - -CACHES = credentials.get_json( - "CACHES", - default={ - "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - }, - }, -) - -CORS_ORIGIN_WHITELIST = credentials.get("CORS_ORIGIN_WHITELIST", []) - - -SITE_ID = 1 - - -### -# Staticfiles configuration - -STATIC_ROOT = credentials["STATIC_ROOT"] -STATIC_URL = "/static/" - -MEDIA_ROOT = credentials.get("MEDIA_ROOT", (BASE_DIR / "media")) -MEDIA_URL = "/media/" - - -## -# Authens and Authentication configuration - -AUTHENTICATION_BACKENDS = [ - "django.contrib.auth.backends.ModelBackend", - "authens.backends.ENSCASBackend", - "authens.backends.OldCASBackend", -] - -AUTHENS_USE_OLDCAS = False - -LOGIN_URL = "authens:login" -LOGIN_REDIRECT_URL = "bds:home" -LOGOUT_REDIRECT_URL = "bds:home" - - -# --- -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ -# --- - -LANGUAGE_CODE = "fr-fr" -TIME_ZONE = "Europe/Paris" -USE_I18N = True -USE_L10N = True -USE_TZ = True -LANGUAGES = (("fr", "Français"), ("en", "English")) -FORMAT_MODULE_PATH = "gestioasso.locale" - -## -# Development configuration - -if DEBUG: - EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - - def show_toolbar(request): - """ - On active la debug-toolbar en mode développement local sauf : - - dans l'admin où ça ne sert pas à grand chose; - - si la variable d'environnement DJANGO_NO_DDT est à 1 → ça permet de la désactiver - sans modifier ce fichier en exécutant `export DJANGO_NO_DDT=1` dans le terminal - qui lance `./manage.py runserver`. - - Autre side effect de cette fonction : on ne fait pas la vérification de INTERNAL_IPS - que ferait la debug-toolbar par défaut, ce qui la fait fonctionner aussi à - l'intérieur de Vagrant (comportement non testé depuis un moment…) - """ - - env_no_ddt = bool(os.environ.get("DJANGO_NO_DDT", None)) - return not (env_no_ddt or request.path.startswith("/admin/")) - - ## - # Django Debug Toolbar configuration - - DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} - INSTALLED_APPS += ["debug_toolbar"] - MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE diff --git a/gestioasso/settings_cof.py b/gestioasso/settings_cof.py deleted file mode 100644 index 057019a2..00000000 --- a/gestioasso/settings_cof.py +++ /dev/null @@ -1,324 +0,0 @@ -""" -Django settings for the gestioCOF project. -""" - -import os -from datetime import datetime, timedelta -from pathlib import Path - -from django.urls import reverse_lazy -from loadcredential import Credentials - -credentials = Credentials(env_prefix="GESTIOCOF_") - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - -# WARNING: keep the secret key used in production secret! -SECRET_KEY = credentials["SECRET_KEY"] - -# WARNING: don't run with debug turned on in production! -DEBUG = credentials.get_json("DEBUG", False) - -ALLOWED_HOSTS = credentials.get_json("ALLOWED_HOSTS", []) -ADMINS = credentials.get_json("ADMINS", []) - -SERVER_EMAIL = credentials.get("SERVER_EMAIL") -EMAIL_HOST = credentials.get("EMAIL_HOST") - -LDAP_SERVER_URL = credentials.get("LDAP_SERVER_URL") - -DEFAULT_AUTO_FIELD = "django.db.models.AutoField" - -SYMPA_PASSWORD = credentials["SYMPA_PASSWORD"].encode() -SYMPA_USERNAME = credentials["SYMPA_USERNAME"].encode() - - -## -# Installed Apps configuration - -INSTALLED_APPS = [ - "gestioncof", - # Must be before django admin - # https://github.com/infoportugal/wagtail-modeltranslation/issues/193 - "wagtail_modeltranslation", - "wagtail_modeltranslation.makemigrations", - "wagtail_modeltranslation.migrate", - "modeltranslation", - "shared", - # Must be before 'django.contrib.admin'. - # https://django-autocomplete-light.readthedocs.io/en/master/install.html - "dal", - "dal_select2", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.sites", - "django.contrib.messages", - "django.contrib.admin", - "django.contrib.admindocs", - "gestioasso.apps.IgnoreSrcStaticFilesConfig", - "django_cas_ng", - "bootstrapform", - "widget_tweaks", - "bda", - "petitscours", - "hcaptcha", - "kfet", - "kfet.open", - "channels", - "djconfig", - "wagtail.contrib.forms", - "wagtail.contrib.redirects", - "wagtail.embeds", - "wagtail.sites", - "wagtail.users", - "wagtail.snippets", - "wagtail.documents", - "wagtail.images", - "wagtail.search", - "wagtail.admin", - "wagtail", - # "wagtail.contrib.modeladmin", - "wagtail.contrib.routable_page", - "wagtailmenus", - "modelcluster", - "taggit", - "kfet.auth", - "kfet.cms", - "gestioncof.cms", - "django_js_reverse", -] - - -## -# Middleware configuration - -MIDDLEWARE = [ - "corsheaders.middleware.CorsMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django.middleware.security.SecurityMiddleware", - "django.middleware.locale.LocaleMiddleware", - "djconfig.middleware.DjConfigMiddleware", - "wagtail.contrib.redirects.middleware.RedirectMiddleware", -] - - -## -# URL configuration - -ROOT_URLCONF = "gestioasso.urls" - - -## -# Templates configuration - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "wagtailmenus.context_processors.wagtailmenus", - "djconfig.context_processors.config", - "gestioncof.shared.context_processor", - "kfet.auth.context_processors.temporary_auth", - "kfet.context_processors.config", - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - "django.template.context_processors.i18n", - "django.template.context_processors.media", - "django.template.context_processors.static", - ] - }, - } -] - - -## -# Wagtail configuration - -WAGTAIL_SITE_NAME = "GestioCOF" -WAGTAIL_ENABLE_UPDATE_CHECK = False -TAGGIT_CASE_INSENSITIVE = True - -## -# Django-js-reverse settings - -JS_REVERSE_JS_VAR_NAME = "django_urls" -# Quand on aura namespace les urls... -# JS_REVERSE_INCLUDE_ONLY_NAMESPACES = ['k-fet'] - -## -# K-Fêt history configuration - -# L'historique n'est accesible que d'aujourd'hui -# à aujourd'hui - KFET_HISTORY_DATE_LIMIT -KFET_HISTORY_DATE_LIMIT = timedelta(days=7) - -# Limite plus longue pour les chefs/trez -# (qui ont la permission kfet.access_old_history) -KFET_HISTORY_LONG_DATE_LIMIT = timedelta(days=30) - -# These accounts don't represent actual people and can be freely accessed -# Identification based on trigrammes -KFET_HISTORY_NO_DATE_LIMIT_TRIGRAMMES = ["LIQ", "#13"] -KFET_HISTORY_NO_DATE_LIMIT = datetime(1794, 10, 30) # AKA the distant past - - -## -# Database configuration - -DATABASES = credentials.get_json( - "DATABASES", - default={ - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": (BASE_DIR / "db.sqlite3"), - } - }, -) - -CACHES = credentials.get_json( - "CACHES", - default={ - "default": { - "BACKEND": "django.core.cache.backends.locmem.LocMemCache", - }, - }, -) - -CHANNEL_LAYERS = credentials.get_json( - "CHANNEL_LAYERS", - default={ - "default": { - "BACKEND": "channels.layers.InMemoryChannelLayer", - } - }, -) - -ASGI_APPLICATION = "gestioasso.routing.application" - -CORS_ALLOWED_ORIGINS = credentials.get("CORS_ALLOWED_ORIGINS", []) -CSRF_TRUSTED_ORIGINS = [f"https://{host}" for host in ALLOWED_HOSTS] - - -SITE_ID = 1 - - -### -# Staticfiles configuration - -STATIC_ROOT = credentials["STATIC_ROOT"] -STATIC_URL = "/static/" - -MEDIA_ROOT = credentials.get("MEDIA_ROOT", (BASE_DIR / "media")) -MEDIA_URL = "/media/" - - -## -# Authentication configuration - -AUTHENTICATION_BACKENDS = [ - "kfet.auth.backends.BlockFrozenAccountBackend", # Must be in first - "django.contrib.auth.backends.ModelBackend", - "gestioncof.shared.COFCASBackend", - "kfet.auth.backends.GenericBackend", -] - -LOGIN_URL = "cof-login" -LOGIN_REDIRECT_URL = reverse_lazy("home") - -# FIXME: Switch to authens -CAS_SERVER_URL = "https://cas.eleves.ens.fr/" -CAS_VERSION = "2" -CAS_LOGIN_MSG = None -CAS_IGNORE_REFERER = True -CAS_REDIRECT_URL = "/" -CAS_EMAIL_FORMAT = "%s@clipper.ens.fr" - - -## -# h-captcha configuration - -HCAPTCHA_SITEKEY = credentials["HCAPTCHA_SITEKEY"] -HCAPTCHA_SECRET = credentials["HCAPTCHA_SECRET"] - -## -# K-Fêt token for the openness indicator - -KFETOPEN_TOKEN = credentials["KFETOPEN_TOKEN"] - - -## -# Mail configuration - -MAIL_DATA = { - "petits_cours": { - "FROM": "Le COF ", - "BCC": "archivescof@gmail.com", - "REPLYTO": "cof@ens.fr", - }, - "rappels": { - "FROM": "Le BdA ", - "REPLYTO": "Le BdA ", - }, - "kfet": { - "FROM": "La K-Fêt ", - "REPLYTO": "La K-Fêt ", - }, - "revente": { - "FROM": "BdA-Revente ", - "REPLYTO": "BdA-Revente ", - }, -} - - -# --- -# Internationalization -# https://docs.djangoproject.com/en/1.8/topics/i18n/ -# --- - -LANGUAGE_CODE = "fr-fr" -TIME_ZONE = "Europe/Paris" -USE_I18N = True -USE_L10N = True -USE_TZ = True -LANGUAGES = (("fr", "Français"), ("en", "English")) -FORMAT_MODULE_PATH = "gestioasso.locale" - -## -# Development configuration - -if DEBUG: - EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" - - def show_toolbar(request): - """ - On active la debug-toolbar en mode développement local sauf : - - dans l'admin où ça ne sert pas à grand chose; - - si la variable d'environnement DJANGO_NO_DDT est à 1 → ça permet de la désactiver - sans modifier ce fichier en exécutant `export DJANGO_NO_DDT=1` dans le terminal - qui lance `./manage.py runserver`. - - Autre side effect de cette fonction : on ne fait pas la vérification de INTERNAL_IPS - que ferait la debug-toolbar par défaut, ce qui la fait fonctionner aussi à - l'intérieur de Vagrant (comportement non testé depuis un moment…) - """ - - env_no_ddt = bool(os.environ.get("DJANGO_NO_DDT", None)) - return not (env_no_ddt or request.path.startswith("/admin/")) - - ## - # Django Debug Toolbar configuration - - DEBUG_TOOLBAR_CONFIG = {"SHOW_TOOLBAR_CALLBACK": show_toolbar} - INSTALLED_APPS += ["debug_toolbar"] - MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + MIDDLEWARE diff --git a/gestioasso/urls.py b/gestioasso/urls.py deleted file mode 100644 index 753e947d..00000000 --- a/gestioasso/urls.py +++ /dev/null @@ -1,75 +0,0 @@ -""" -Fichier principal de configuration des urls du projet GestioCOF -""" - -from django.conf import settings -from django.conf.urls.i18n import i18n_patterns -from django.conf.urls.static import static -from django.contrib import admin -from django.urls import include, path -from django.views.generic.base import RedirectView - -bds_is_alone = ( - "bds" in settings.INSTALLED_APPS and "gestioncof" not in settings.INSTALLED_APPS -) - -admin.autodiscover() -urlpatterns = [ - # Website administration (independent from installed apps) - path("admin/doc/", include("django.contrib.admindocs.urls")), - path("admin/", admin.site.urls), -] - -if not bds_is_alone: - # Redirection / → /gestion, only useful for developpers. - urlpatterns.append(path("", RedirectView.as_view(url="gestion/"))) - -# App-specific urls - -app_dict = { - "bds": "" if bds_is_alone else "bds/", - "kfet": "k-fet/", - # Below = GestioCOF → goes under gestion/ - "gestioncof": "gestion/", - "bda": "gestion/bda/", - "petitscours": "gestion/petitcours/", - "events": "gestion/event_v2/", # the events module is still experimental ! - "authens": "gestion/authens/", -} -for app_name, url_prefix in app_dict.items(): - if app_name in settings.INSTALLED_APPS: - urlpatterns += [path(url_prefix, include("{}.urls".format(app_name)))] - - -if "django_js_reverse" in settings.INSTALLED_APPS: - from django_js_reverse.views import urls_js - - urlpatterns += [ - path("jsreverse/", urls_js, name="js_reverse"), - ] - -if "debug_toolbar" in settings.INSTALLED_APPS: - import debug_toolbar - - urlpatterns += [path("__debug__/", include(debug_toolbar.urls))] - -if settings.DEBUG: - # Si on est en production, MEDIA_ROOT est servi par Apache. - # Il faut dire à Django de servir MEDIA_ROOT lui-même en développement. - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - - -# Wagtail URLs (wagtail urls must be last, as catch-all) -if "wagtail" in settings.INSTALLED_APPS: - from wagtail import urls as wagtail_urls - from wagtail.admin import urls as wagtailadmin_urls - from wagtail.documents import urls as wagtaildocs_urls - - urlpatterns += [ - path("cms/", include(wagtailadmin_urls)), - path("documents/", include(wagtaildocs_urls)), - ] - urlpatterns += i18n_patterns( - path("", include(wagtail_urls)), prefix_default_language=False - ) diff --git a/gestioasso/wsgi.py b/gestioasso/wsgi.py deleted file mode 100644 index bdd9a64c..00000000 --- a/gestioasso/wsgi.py +++ /dev/null @@ -1,6 +0,0 @@ -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.bds_prod") -application = get_wsgi_application() diff --git a/gestioncof/__init__.py b/gestioncof/__init__.py index e69de29b..3bb260b9 100644 --- a/gestioncof/__init__.py +++ b/gestioncof/__init__.py @@ -0,0 +1 @@ +default_app_config = "gestioncof.apps.GestioncofConfig" diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 3576efda..768cff3b 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -6,7 +6,7 @@ from django.contrib.auth.models import Group, Permission, User from django.db.models import Q from django.urls import reverse from django.utils.safestring import mark_safe -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ from gestioncof.models import ( Club, @@ -100,6 +100,28 @@ class CofProfileInline(admin.StackedInline): inline_classes = ("collapse open",) +class FkeyLookup(object): + def __init__(self, fkeydecl, short_description=None, admin_order_field=None): + self.fk, fkattrs = fkeydecl.split("__", 1) + self.fkattrs = fkattrs.split("__") + + self.short_description = short_description or self.fkattrs[-1] + self.admin_order_field = admin_order_field or fkeydecl + + def __get__(self, obj, klass): + if obj is None: + """ + hack required to make Django validate (if obj is + None, then we're a class, and classes are callable + ) + """ + return self + item = getattr(obj, self.fk) + for attr in self.fkattrs: + item = getattr(item, attr) + return item + + def ProfileInfo(field, short_description, boolean=False): def getter(self): try: @@ -112,6 +134,7 @@ def ProfileInfo(field, short_description, boolean=False): return getter +User.profile_login_clipper = FkeyLookup("profile__login_clipper", "Login clipper") User.profile_phone = ProfileInfo("phone", "Téléphone") User.profile_occupation = ProfileInfo("occupation", "Occupation") User.profile_departement = ProfileInfo("departement", "Departement") @@ -130,43 +153,24 @@ class UserProfileAdmin(UserAdmin): is_buro.short_description = "Membre du Buro" is_buro.boolean = True - def is_chef(self, obj): - try: - return obj.profile.is_chef - except CofProfile.DoesNotExist: - return False - - is_chef.short_description = "Chef K-Fêt" - is_chef.boolean = True - def is_cof(self, obj): try: return obj.profile.is_cof except CofProfile.DoesNotExist: return False - is_cof.short_description = "Membre COF" + is_cof.short_description = "Membre du COF" is_cof.boolean = True - def is_kfet(self, obj): - try: - return obj.profile.is_kfet - except CofProfile.DoesNotExist: - return False - - is_kfet.short_description = "Membre K-Fêt" - is_kfet.boolean = True - list_display = UserAdmin.list_display + ( + "profile_login_clipper", "profile_phone", "profile_occupation", "profile_mailing_cof", "profile_mailing_bda", "profile_mailing_bda_revente", "is_cof", - "is_kfet", "is_buro", - "is_chef", ) list_display_links = ("username", "email", "first_name", "last_name") list_filter = UserAdmin.list_filter + ( diff --git a/gestioncof/apps.py b/gestioncof/apps.py index 0ac33f93..88e2fbfc 100644 --- a/gestioncof/apps.py +++ b/gestioncof/apps.py @@ -12,7 +12,6 @@ class GestioncofConfig(AppConfig): def register_config(self): import djconfig - from .forms import GestioncofConfigForm djconfig.register(GestioncofConfigForm) diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index 9570acb5..e27cdb92 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -1,58 +1,94 @@ -from django.contrib.auth import get_user_model +from django import shortcuts +from django.conf import settings +from django.contrib.auth.models import User from django.db.models import Q -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.http import Http404 -from shared import autocomplete +from gestioncof.decorators import buro_required +from gestioncof.models import CofProfile -User = get_user_model() +if getattr(settings, "LDAP_SERVER_URL", None): + from ldap3 import Connection +else: + # shared.tests.testcases.TestCaseMixin.mockLDAP needs + # Connection to be defined in order to mock it. + Connection = None -class COFMemberSearch(autocomplete.ModelSearch): - model = User - search_fields = ["username", "first_name", "last_name"] - verbose_name = _("Membres du COF") +class Clipper(object): + def __init__(self, clipper, fullname): + if fullname is None: + fullname = "" + assert isinstance(clipper, str) + assert isinstance(fullname, str) + self.clipper = clipper + self.fullname = fullname - def get_queryset_filter(self, *args, **kwargs): - qset_filter = super().get_queryset_filter(*args, **kwargs) - qset_filter &= Q(profile__is_cof=True) - return qset_filter + def __str__(self): + return "{} ({})".format(self.clipper, self.fullname) - def result_uuid(self, user): - return user.username - - def result_link(self, user): - return reverse("user-registration", args=(user.username,)) + def __eq__(self, other): + return self.clipper == other.clipper and self.fullname == other.fullname -class COFOthersSearch(autocomplete.ModelSearch): - model = User - search_fields = ["username", "first_name", "last_name"] - verbose_name = _("Non-membres du COF") +@buro_required +def autocomplete(request): + if "q" not in request.GET: + raise Http404 + q = request.GET["q"] + data = {"q": q} - def get_queryset_filter(self, *args, **kwargs): - qset_filter = super().get_queryset_filter(*args, **kwargs) - qset_filter &= Q(profile__is_cof=False) - return qset_filter + queries = {} + bits = q.split() - def result_uuid(self, user): - return user.username + # Fetching data from User and CofProfile tables + queries["members"] = CofProfile.objects.filter(is_cof=True) + queries["users"] = User.objects.filter(profile__is_cof=False) + for bit in bits: + queries["members"] = queries["members"].filter( + Q(user__first_name__icontains=bit) + | Q(user__last_name__icontains=bit) + | Q(user__username__icontains=bit) + | Q(login_clipper__icontains=bit) + ) + queries["users"] = queries["users"].filter( + Q(first_name__icontains=bit) + | Q(last_name__icontains=bit) + | Q(username__icontains=bit) + ) + queries["members"] = queries["members"].distinct() + queries["users"] = queries["users"].distinct() - def result_link(self, user): - return reverse("user-registration", args=(user.username,)) + # Clearing redundancies + usernames = set(queries["members"].values_list("login_clipper", flat="True")) | set( + queries["users"].values_list("profile__login_clipper", flat="True") + ) + # Fetching data from the SPI + if getattr(settings, "LDAP_SERVER_URL", None): + # Fetching + ldap_query = "(&{:s})".format( + "".join( + "(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=bit) + for bit in bits + if bit.isalnum() + ) + ) + if ldap_query != "(&)": + # If none of the bits were legal, we do not perform the query + entries = None + with Connection(settings.LDAP_SERVER_URL) as conn: + conn.search("dc=spi,dc=ens,dc=fr", ldap_query, attributes=["uid", "cn"]) + entries = conn.entries + # Clearing redundancies + queries["clippers"] = [ + Clipper(entry.uid.value, entry.cn.value) + for entry in entries + if entry.uid.value and entry.uid.value not in usernames + ] -class COFLDAPSearch(autocomplete.LDAPSearch): - def result_link(self, clipper): - return reverse("clipper-registration", args=(clipper.clipper, clipper.fullname)) + # Resulting data + data.update(queries) + data["options"] = sum(len(query) for query in queries) - -class COFAutocomplete(autocomplete.Compose): - search_units = [ - ("members", COFMemberSearch()), - ("others", COFOthersSearch()), - ("clippers", COFLDAPSearch()), - ] - - -cof_autocomplete = COFAutocomplete() + return shortcuts.render(request, "autocomplete_user.html", data) diff --git a/gestioncof/cms/__init__.py b/gestioncof/cms/__init__.py index e69de29b..043b644d 100644 --- a/gestioncof/cms/__init__.py +++ b/gestioncof/cms/__init__.py @@ -0,0 +1 @@ +default_app_config = "gestioncof.cms.apps.COFCMSAppConfig" diff --git a/gestioncof/cms/fixtures/examplesite0.json b/gestioncof/cms/fixtures/examplesite0.json deleted file mode 100644 index 816ebe82..00000000 --- a/gestioncof/cms/fixtures/examplesite0.json +++ /dev/null @@ -1,641 +0,0 @@ -[ -{ - "model": "wagtailcore.page", - "pk": 27, - "fields": { - "path": "000100010002", - "depth": 3, - "numchild": 3, - "title": "Site du COF", - "title_fr": "Site du COF", - "title_en": null, - "draft_title": "Site du COF", - "slug": "site", - "slug_fr": "site", - "slug_en": null, - "content_type": 89, - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/site/", - "url_path_fr": "/global/site/", - "url_path_en": "/global/site/", - "owner": 165, - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": false, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-02-04T20:54:14.724Z", - "last_published_at": "2019-02-04T20:54:14.724Z", - "latest_revision_created_at": null, - "live_revision": null - } -}, -{ - "model": "wagtailcore.page", - "pk": 28, - "fields": { - "path": "0001000100020001", - "depth": 4, - "numchild": 0, - "title": "Pr\u00e9sentation", - "title_fr": "Pr\u00e9sentation", - "title_en": "Presentation", - "draft_title": "Pr\u00e9sentation", - "slug": "pr\u00e9sentation", - "slug_fr": "pr\u00e9sentation", - "slug_en": null, - "content_type": 88, - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/site/pr\u00e9sentation/", - "url_path_fr": "/global/site/pr\u00e9sentation/", - "url_path_en": "/global/site/pr\u00e9sentation/", - "owner": 165, - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": true, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-02-04T20:55:06.574Z", - "last_published_at": "2019-02-04T21:42:00.461Z", - "latest_revision_created_at": null, - "live_revision": null - } -}, -{ - "model": "wagtailcore.page", - "pk": 29, - "fields": { - "path": "0001000100020002", - "depth": 4, - "numchild": 2, - "title": "Actualit\u00e9s", - "title_fr": "Actualit\u00e9s", - "title_en": "News", - "draft_title": "Actualit\u00e9s", - "slug": "actualit\u00e9s", - "slug_fr": "actualit\u00e9s", - "slug_en": "news", - "content_type": 92, - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/site/actualit\u00e9s/", - "url_path_fr": "/global/site/actualit\u00e9s/", - "url_path_en": "/global/site/news/", - "owner": 165, - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": true, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-02-04T20:58:47.657Z", - "last_published_at": "2019-02-04T21:43:55.575Z", - "latest_revision_created_at": null, - "live_revision": null - } -}, -{ - "model": "wagtailcore.page", - "pk": 30, - "fields": { - "path": "00010001000200020001", - "depth": 5, - "numchild": 0, - "title": "Grosse teuf en K-F\u00eat", - "title_fr": "Grosse teuf en K-F\u00eat", - "title_en": "Big feast in K-F\u00eat", - "draft_title": "Grosse teuf en K-F\u00eat", - "slug": "grosse-teuf-en-k-f\u00eat", - "slug_fr": "grosse-teuf-en-k-f\u00eat", - "slug_en": "big-feast-in-k-f\u00eat", - "content_type": 93, - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/site/actualit\u00e9s/grosse-teuf-en-k-f\u00eat/", - "url_path_fr": "/global/site/actualit\u00e9s/grosse-teuf-en-k-f\u00eat/", - "url_path_en": "/global/site/news/big-feast-in-k-f\u00eat/", - "owner": 165, - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": false, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-02-04T21:04:39.422Z", - "last_published_at": "2019-02-04T21:04:39.422Z", - "latest_revision_created_at": null, - "live_revision": null - } -}, -{ - "model": "wagtailcore.page", - "pk": 31, - "fields": { - "path": "00010001000200020002", - "depth": 5, - "numchild": 0, - "title": "Les 48h des Arts", - "title_fr": "Les 48h des Arts", - "title_en": null, - "draft_title": "Les 48h des Arts", - "slug": "les-48h-des-arts", - "slug_fr": "les-48h-des-arts", - "slug_en": null, - "content_type": 93, - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/site/actualit\u00e9s/les-48h-des-arts/", - "url_path_fr": "/global/site/actualit\u00e9s/les-48h-des-arts/", - "url_path_en": "/global/site/news/les-48h-des-arts/", - "owner": 165, - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": false, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-02-04T21:05:27.190Z", - "last_published_at": "2019-02-04T21:05:27.190Z", - "latest_revision_created_at": null, - "live_revision": null - } -}, -{ - "model": "wagtailcore.page", - "pk": 32, - "fields": { - "path": "0001000100020003", - "depth": 4, - "numchild": 1, - "title": "Clubs", - "title_fr": "Clubs", - "title_en": null, - "draft_title": "Clubs", - "slug": "clubs", - "slug_fr": "clubs", - "slug_en": null, - "content_type": 87, - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/site/clubs/", - "url_path_fr": "/global/site/clubs/", - "url_path_en": "/global/site/clubs/", - "owner": 165, - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": true, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-02-04T21:44:23.382Z", - "last_published_at": "2019-02-04T21:44:23.382Z", - "latest_revision_created_at": null, - "live_revision": null - } -}, -{ - "model": "wagtailcore.page", - "pk": 33, - "fields": { - "path": "00010001000200030001", - "depth": 5, - "numchild": 0, - "title": "Arts Plastiques", - "title_fr": "Arts Plastiques", - "title_en": null, - "draft_title": "Arts Plastiques", - "slug": "arts-plastiques", - "slug_fr": "arts-plastiques", - "slug_en": null, - "content_type": 90, - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/site/clubs/arts-plastiques/", - "url_path_fr": "/global/site/clubs/arts-plastiques/", - "url_path_en": "/global/site/clubs/arts-plastiques/", - "owner": 165, - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": false, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-02-04T21:48:58.013Z", - "last_published_at": "2019-02-04T21:48:58.013Z", - "latest_revision_created_at": null, - "live_revision": null - } -}, -{ - "model": "wagtailcore.collection", - "pk": 3, - "fields": { - "path": "00010002", - "depth": 2, - "numchild": 0, - "name": "COF" - } -}, - { - "model": "wagtailimages.image", - "pk": 33, - "fields": { - "collection": 3, - "title": "COF-17", - "file": "original_images/cof-768x576.jpg", - "width": 768, - "height": 576, - "created_at": "2018-01-22T18:49:25.647Z", - "uploaded_by_user": 165, - "focal_point_x": null, - "focal_point_y": null, - "focal_point_width": null, - "focal_point_height": null, - "file_size": 132330, - "file_hash": "" - } -}, -{ - "model": "wagtailimages.image", - "pk": 34, - "fields": { - "collection": 3, - "title": "Singin in the RENS", - "file": "original_images/singin.jpg", - "width": 682, - "height": 361, - "created_at": "2018-01-22T19:13:49.753Z", - "uploaded_by_user": 165, - "focal_point_x": null, - "focal_point_y": null, - "focal_point_width": null, - "focal_point_height": null, - "file_size": null, - "file_hash": "" - } -}, -{ - "model": "wagtailimages.image", - "pk": 35, - "fields": { - "collection": 3, - "title": "Retour du Bur\u00f4", - "file": "original_images/retour.jpg", - "width": 614, - "height": 211, - "created_at": "2018-01-22T19:16:25.375Z", - "uploaded_by_user": 165, - "focal_point_x": null, - "focal_point_y": null, - "focal_point_width": null, - "focal_point_height": null, - "file_size": null, - "file_hash": "" - } -}, -{ - "model": "wagtailimages.image", - "pk": 36, - "fields": { - "collection": 3, - "title": "elections 18", - "file": "original_images/elections.png", - "width": 850, - "height": 406, - "created_at": "2018-01-22T19:21:31.954Z", - "uploaded_by_user": 165, - "focal_point_x": null, - "focal_point_y": null, - "focal_point_width": null, - "focal_point_height": null, - "file_size": null, - "file_hash": "" - } -}, -{ - "model": "wagtailimages.image", - "pk": 37, - "fields": { - "collection": 3, - "title": "Arts Plastiques", - "file": "original_images/ArtsPla.png", - "width": 150, - "height": 150, - "created_at": "2018-01-22T20:11:56.461Z", - "uploaded_by_user": 165, - "focal_point_x": null, - "focal_point_y": null, - "focal_point_width": null, - "focal_point_height": null, - "file_size": null, - "file_hash": "" - } -}, -{ - "model": "wagtailimages.image", - "pk": 38, - "fields": { - "collection": 3, - "title": "MGEN", - "file": "original_images/MGEN.jpg", - "width": 300, - "height": 204, - "created_at": "2018-01-22T20:20:41.712Z", - "uploaded_by_user": 165, - "focal_point_x": null, - "focal_point_y": null, - "focal_point_width": null, - "focal_point_height": null, - "file_size": null, - "file_hash": "" - } -}, -{ - "model": "wagtailimages.image", - "pk": 39, - "fields": { - "collection": 3, - "title": "MAIF", - "file": "original_images/Logo-MAIF.gif", - "width": 300, - "height": 290, - "created_at": "2018-01-28T16:20:13.828Z", - "uploaded_by_user": 165, - "focal_point_x": null, - "focal_point_y": null, - "focal_point_width": null, - "focal_point_height": null, - "file_size": null, - "file_hash": "" - } -}, -{ - "model": "cofcms.cofrootpage", - "pk": 27, - "fields": { - "introduction": "

    Bienvenue sur le site du COF !

    ", - "introduction_fr": "

    Bienvenue sur le site du COF !

    ", - "introduction_en": "

    " - } -}, -{ - "model": "cofcms.cofpage", - "pk": 28, - "fields": { - "body": "[{\"value\": \"

    On est le COF on est tout gentil

    \", \"type\": \"paragraph\", \"id\": \"0b3a92bd-1e27-433b-842c-ab4f0a2750ad\"}]", - "body_fr": "[{\"value\": \"

    On est le COF on est tout gentil

    \", \"type\": \"paragraph\", \"id\": \"0b3a92bd-1e27-433b-842c-ab4f0a2750ad\"}]", - "body_en": "[]" - } -}, -{ - "model": "cofcms.cofactuindexpage", - "pk": 29, - "fields": {} -}, -{ - "model": "cofcms.cofactupage", - "pk": 30, - "fields": { - "chapo": "Grosse teuf en K-F\u00eat", - "chapo_fr": "Grosse teuf en K-F\u00eat", - "chapo_en": "Big typar in K-F\u00eat", - "body": "

    Viens boire en K-F\u00eat

    ", - "body_fr": "

    Viens boire en K-F\u00eat

    ", - "body_en": "

    ", - "image": 34, - "is_event": true, - "date_start": "2019-02-07T21:00:00Z", - "date_end": "2019-02-08T03:00:00Z", - "all_day": false - } -}, -{ - "model": "cofcms.cofactupage", - "pk": 31, - "fields": { - "chapo": "", - "chapo_fr": "", - "chapo_en": "", - "body": "

    C'est l'art

    ", - "body_fr": "

    C'est l'art

    ", - "body_en": "

    ", - "image": 37, - "is_event": true, - "date_start": "2019-03-16T21:05:00Z", - "date_end": "2019-03-24T21:05:00Z", - "all_day": true - } -}, -{ - "model": "cofcms.cofdirectorypage", - "pk": 32, - "fields": { - "introduction": "

    Ce sont les clubs

    ", - "introduction_fr": "

    Ce sont les clubs

    ", - "introduction_en": "

    ", - "alphabetique": true - } -}, -{ - "model": "cofcms.cofdirectoryentrypage", - "pk": 33, - "fields": { - "body": "

    Club Arts Plastiques

    ", - "body_fr": "

    Club Arts Plastiques

    ", - "body_en": "

    ", - "links": "[{\"value\": {\"texte\": \"Liste Mails\", \"email\": \"artsplastiques@ens.fr\"}, \"type\": \"contact\", \"id\": \"cf198b98-0b84-4f38-ac00-6d883cfd60a4\"}]", - "links_fr": "[{\"value\": {\"texte\": \"Liste Mails\", \"email\": \"artsplastiques@ens.fr\"}, \"type\": \"contact\", \"id\": \"cf198b98-0b84-4f38-ac00-6d883cfd60a4\"}]", - "links_en": "[]", - "image": 37 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 7, - "fields": { - "sort_order": 0, - "link_page": null, - "link_url": "https://www.cof.ens.fr/bda/", - "url_append": "", - "handle": "", - "link_text": "BdA", - "allow_subnav": false, - "menu": 2 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 8, - "fields": { - "sort_order": 1, - "link_page": null, - "link_url": "https://www.cof.ens.fr/bds/", - "url_append": "", - "handle": "", - "link_text": "BdS", - "allow_subnav": false, - "menu": 2 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 9, - "fields": { - "sort_order": 2, - "link_page": null, - "link_url": "https://www.cof.ens.fr/gestion", - "url_append": "", - "handle": "", - "link_text": "GestioCOF", - "allow_subnav": false, - "menu": 2 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 10, - "fields": { - "sort_order": 3, - "link_page": null, - "link_url": "https://www.cof.ens.fr/bocal", - "url_append": "", - "handle": "", - "link_text": "Le BOcal", - "allow_subnav": false, - "menu": 2 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 11, - "fields": { - "sort_order": 4, - "link_page": null, - "link_url": "https://photos.cof.ens.fr/", - "url_append": "", - "handle": "", - "link_text": "Serveur photos", - "allow_subnav": false, - "menu": 2 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 12, - "fields": { - "sort_order": 5, - "link_page": null, - "link_url": "https://www.eleves.ens.fr", - "url_append": "", - "handle": "", - "link_text": "Services \u00e9l\u00e8ves ENS", - "allow_subnav": false, - "menu": 2 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 20, - "fields": { - "sort_order": 0, - "link_page": 28, - "link_url": null, - "url_append": "", - "handle": "", - "link_text": "", - "allow_subnav": false, - "menu": 4 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 21, - "fields": { - "sort_order": 1, - "link_page": 29, - "link_url": null, - "url_append": "", - "handle": "", - "link_text": "", - "allow_subnav": false, - "menu": 4 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 22, - "fields": { - "sort_order": 2, - "link_page": 32, - "link_url": null, - "url_append": "", - "handle": "", - "link_text": "", - "allow_subnav": false, - "menu": 4 - } -}, -{ - "model": "wagtailmenus.flatmenu", - "pk": 2, - "fields": { - "site": 2, - "title": "COF - liens externes", - "handle": "cof-nav-ext", - "heading": "", - "max_levels": 1, - "use_specific": 1 - } -}, -{ - "model": "wagtailmenus.flatmenu", - "pk": 4, - "fields": { - "site": 2, - "title": "COF - liens internes", - "handle": "cof-nav-int", - "heading": "", - "max_levels": 1, - "use_specific": 1 - } -} -] diff --git a/gestioncof/cms/forms.py b/gestioncof/cms/forms.py deleted file mode 100644 index d2766bf0..00000000 --- a/gestioncof/cms/forms.py +++ /dev/null @@ -1,15 +0,0 @@ -import re - -from django import forms -from django.utils.translation import gettext as _ - - -class CaptchaForm(forms.Form): - answer = forms.CharField(label="Réponse", max_length=32) - - def clean_answer(self): - value = self.cleaned_data["answer"] - if not re.match(r"(les|the)? *ernests?", value.strip().lower()): - raise forms.ValidationError(_("Réponse incorrecte")) - - return value diff --git a/gestioncof/cms/locale/en/LC_MESSAGES/django.mo b/gestioncof/cms/locale/en/LC_MESSAGES/django.mo deleted file mode 100644 index 40b2f45f..00000000 Binary files a/gestioncof/cms/locale/en/LC_MESSAGES/django.mo and /dev/null differ diff --git a/gestioncof/cms/locale/en/LC_MESSAGES/django.po b/gestioncof/cms/locale/en/LC_MESSAGES/django.po deleted file mode 100644 index d586f73e..00000000 --- a/gestioncof/cms/locale/en/LC_MESSAGES/django.po +++ /dev/null @@ -1,118 +0,0 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. -# -msgid "" -msgstr "" -"Project-Id-Version: \n" -"Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-08-22 12:28+0200\n" -"PO-Revision-Date: 2020-08-22 12:29+0200\n" -"Last-Translator: \n" -"Language-Team: \n" -"Language: en\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" -"X-Generator: Poedit 2.0.6\n" - -#: templates/cofcms/base.html:7 -msgid "Association des élèves de l'ENS Ulm" -msgstr "Student Association of the École Normale Supérieure" - -#: templates/cofcms/calendar.html:9 -msgid "LMMJVSD" -msgstr "MTuWThFSaSu" - -#: templates/cofcms/calendar.html:17 -msgid "Le " -msgstr "On " - -#: templates/cofcms/cof_actu_index_page.html:10 -msgid "Calendrier" -msgstr "Calendar" - -#: templates/cofcms/cof_actu_index_page.html:25 -#: templates/cofcms/cof_actu_index_page.html:47 -msgid "Actualités plus récentes" -msgstr "Newer" - -#: templates/cofcms/cof_actu_index_page.html:28 -#: templates/cofcms/cof_actu_index_page.html:50 -msgid "Actualités plus anciennes" -msgstr "Older" - -#: templates/cofcms/cof_actu_index_page.html:41 -msgid "Lire plus" -msgstr "Read more" - -#: templates/cofcms/cof_actu_page.html:7 -msgid "A lieu" -msgstr "Happens" - -#: templates/cofcms/cof_directory_page.html:9 -msgid "Accès rapide" -msgstr "Quick access" - -#: templates/cofcms/cof_directory_page.html:39 -msgid "Afficher l'adresse mail" -msgstr "Show mail address" - -#: templates/cofcms/cof_root_page.html:11 -msgid "Agenda" -msgstr "Agenda" - -#: templates/cofcms/sympa.html:4 -msgid "Redirection vers" -msgstr "Redirecting to" - -#: templates/cofcms/sympa.html:18 -msgid "Comment s'appellent les poissons du bassin ?" -msgstr "How are called the fish in the school's pond?" - -#: templatetags/cofcms_tags.py:134 templatetags/cofcms_tags.py:163 -#, python-brace-format -msgid "le {datestart}" -msgstr "on {datestart}" - -#: templatetags/cofcms_tags.py:136 -#, python-brace-format -msgid "le {datestart} de {timestart} à {timeend}" -msgstr "on {datestart} from {timestart} to {timeend}" - -#: templatetags/cofcms_tags.py:147 -#, python-brace-format -msgid "du {datestart} au {dateend}{common}" -msgstr "from {datestart} to {dateend}{common}" - -#: templatetags/cofcms_tags.py:153 -#, python-brace-format -msgid "du {datestart}{common} à {timestart} au {dateend} à {timeend}" -msgstr "from {datestart}{common} {timestart} to {dateend} {timeend}" - -#: templatetags/cofcms_tags.py:165 -#, python-brace-format -msgid "le {datestart} à {timestart}" -msgstr "on {datestart} at {timestart}" - -#: views.py:16 -msgid "Réponse incorrecte" -msgstr "Invalid answer" - -#~ msgid "Listes mail" -#~ msgstr "Mailing lists" - -#~ msgid "" -#~ "\n" -#~ " Tous les abonnements aux listes de diffusion de mail à l'ENS se gèrent sur le serveur de mail SYMPA\n" -#~ "\n" -#~ " Pour y accéder : Merci de répondre à cette question anti-robots.\n" -#~ " " -#~ msgstr "" -#~ "\n" -#~ " All the mailing list subscriptions can be managed through the SYMPA mail server\n" -#~ "\n" -#~ " In order to access it, please answer this antispam question.\n" -#~ " " diff --git a/gestioncof/cms/migrations/0001_initial.py b/gestioncof/cms/migrations/0001_initial.py index ef024c74..6c6a801e 100644 --- a/gestioncof/cms/migrations/0001_initial.py +++ b/gestioncof/cms/migrations/0001_initial.py @@ -3,9 +3,9 @@ from __future__ import unicode_literals import django.db.models.deletion -import wagtail.blocks import wagtail.contrib.routable_page.models -import wagtail.fields +import wagtail.core.blocks +import wagtail.core.fields import wagtail.images.blocks from django.db import migrations, models @@ -13,6 +13,7 @@ import gestioncof.cms.models class Migration(migrations.Migration): + initial = True dependencies = [ @@ -72,14 +73,18 @@ class Migration(migrations.Migration): blank=True, null=True, verbose_name="Description rapide" ), ), - ("body", wagtail.fields.RichTextField(verbose_name="Contenu")), + ("body", wagtail.core.fields.RichTextField(verbose_name="Contenu")), ( "body_fr", - wagtail.fields.RichTextField(null=True, verbose_name="Contenu"), + wagtail.core.fields.RichTextField( + null=True, verbose_name="Contenu" + ), ), ( "body_en", - wagtail.fields.RichTextField(null=True, verbose_name="Contenu"), + wagtail.core.fields.RichTextField( + null=True, verbose_name="Contenu" + ), ), ( "is_event", @@ -134,40 +139,46 @@ class Migration(migrations.Migration): to="wagtailcore.Page", ), ), - ("body", wagtail.fields.RichTextField(verbose_name="Description")), + ("body", wagtail.core.fields.RichTextField(verbose_name="Description")), ( "body_fr", - wagtail.fields.RichTextField(null=True, verbose_name="Description"), + wagtail.core.fields.RichTextField( + null=True, verbose_name="Description" + ), ), ( "body_en", - wagtail.fields.RichTextField(null=True, verbose_name="Description"), + wagtail.core.fields.RichTextField( + null=True, verbose_name="Description" + ), ), ( "links", - wagtail.fields.StreamField( + wagtail.core.fields.StreamField( [ ( "lien", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "url", - wagtail.blocks.URLBlock(required=True), + wagtail.core.blocks.URLBlock(required=True), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), ( "contact", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "email", - wagtail.blocks.EmailBlock(required=True), + wagtail.core.blocks.EmailBlock( + required=True + ), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), @@ -176,29 +187,31 @@ class Migration(migrations.Migration): ), ( "links_fr", - wagtail.fields.StreamField( + wagtail.core.fields.StreamField( [ ( "lien", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "url", - wagtail.blocks.URLBlock(required=True), + wagtail.core.blocks.URLBlock(required=True), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), ( "contact", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "email", - wagtail.blocks.EmailBlock(required=True), + wagtail.core.blocks.EmailBlock( + required=True + ), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), @@ -208,29 +221,31 @@ class Migration(migrations.Migration): ), ( "links_en", - wagtail.fields.StreamField( + wagtail.core.fields.StreamField( [ ( "lien", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "url", - wagtail.blocks.URLBlock(required=True), + wagtail.core.blocks.URLBlock(required=True), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), ( "contact", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "email", - wagtail.blocks.EmailBlock(required=True), + wagtail.core.blocks.EmailBlock( + required=True + ), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), @@ -272,17 +287,17 @@ class Migration(migrations.Migration): ), ( "introduction", - wagtail.fields.RichTextField(verbose_name="Introduction"), + wagtail.core.fields.RichTextField(verbose_name="Introduction"), ), ( "introduction_fr", - wagtail.fields.RichTextField( + wagtail.core.fields.RichTextField( null=True, verbose_name="Introduction" ), ), ( "introduction_en", - wagtail.fields.RichTextField( + wagtail.core.fields.RichTextField( null=True, verbose_name="Introduction" ), ), @@ -315,27 +330,27 @@ class Migration(migrations.Migration): ), ( "body", - wagtail.fields.StreamField( + wagtail.core.fields.StreamField( [ ( "heading", - wagtail.blocks.CharBlock(classname="full title"), + wagtail.core.blocks.CharBlock(classname="full title"), ), - ("paragraph", wagtail.blocks.RichTextBlock()), + ("paragraph", wagtail.core.blocks.RichTextBlock()), ("image", wagtail.images.blocks.ImageChooserBlock()), ( "iframe", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "url", - wagtail.blocks.URLBlock( + wagtail.core.blocks.URLBlock( "Adresse de la page" ), ), ( "height", - wagtail.blocks.CharBlock( + wagtail.core.blocks.CharBlock( "Hauteur (en pixels)" ), ), @@ -347,27 +362,27 @@ class Migration(migrations.Migration): ), ( "body_fr", - wagtail.fields.StreamField( + wagtail.core.fields.StreamField( [ ( "heading", - wagtail.blocks.CharBlock(classname="full title"), + wagtail.core.blocks.CharBlock(classname="full title"), ), - ("paragraph", wagtail.blocks.RichTextBlock()), + ("paragraph", wagtail.core.blocks.RichTextBlock()), ("image", wagtail.images.blocks.ImageChooserBlock()), ( "iframe", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "url", - wagtail.blocks.URLBlock( + wagtail.core.blocks.URLBlock( "Adresse de la page" ), ), ( "height", - wagtail.blocks.CharBlock( + wagtail.core.blocks.CharBlock( "Hauteur (en pixels)" ), ), @@ -380,27 +395,27 @@ class Migration(migrations.Migration): ), ( "body_en", - wagtail.fields.StreamField( + wagtail.core.fields.StreamField( [ ( "heading", - wagtail.blocks.CharBlock(classname="full title"), + wagtail.core.blocks.CharBlock(classname="full title"), ), - ("paragraph", wagtail.blocks.RichTextBlock()), + ("paragraph", wagtail.core.blocks.RichTextBlock()), ("image", wagtail.images.blocks.ImageChooserBlock()), ( "iframe", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "url", - wagtail.blocks.URLBlock( + wagtail.core.blocks.URLBlock( "Adresse de la page" ), ), ( "height", - wagtail.blocks.CharBlock( + wagtail.core.blocks.CharBlock( "Hauteur (en pixels)" ), ), @@ -434,17 +449,17 @@ class Migration(migrations.Migration): ), ( "introduction", - wagtail.fields.RichTextField(verbose_name="Introduction"), + wagtail.core.fields.RichTextField(verbose_name="Introduction"), ), ( "introduction_fr", - wagtail.fields.RichTextField( + wagtail.core.fields.RichTextField( null=True, verbose_name="Introduction" ), ), ( "introduction_en", - wagtail.fields.RichTextField( + wagtail.core.fields.RichTextField( null=True, verbose_name="Introduction" ), ), diff --git a/gestioncof/cms/migrations/0002_auto_20190523_1521.py b/gestioncof/cms/migrations/0002_auto_20190523_1521.py index 1a0a87d4..35e50b8e 100644 --- a/gestioncof/cms/migrations/0002_auto_20190523_1521.py +++ b/gestioncof/cms/migrations/0002_auto_20190523_1521.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("cofcms", "0001_initial")] operations = [ diff --git a/gestioncof/cms/migrations/0003_directory_entry_optional_links.py b/gestioncof/cms/migrations/0003_directory_entry_optional_links.py index 22533193..10cf5c5e 100644 --- a/gestioncof/cms/migrations/0003_directory_entry_optional_links.py +++ b/gestioncof/cms/migrations/0003_directory_entry_optional_links.py @@ -1,11 +1,12 @@ # Generated by Django 2.2.8 on 2019-12-20 16:22 -import wagtail.blocks -import wagtail.fields +import wagtail.core.blocks +import wagtail.core.fields from django.db import migrations class Migration(migrations.Migration): + dependencies = [ ("cofcms", "0002_auto_20190523_1521"), ] @@ -14,26 +15,26 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="cofdirectoryentrypage", name="links", - field=wagtail.fields.StreamField( + field=wagtail.core.fields.StreamField( [ ( "lien", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), + ("url", wagtail.core.blocks.URLBlock(required=True)), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), ( "contact", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "email", - wagtail.blocks.EmailBlock(required=True), + wagtail.core.blocks.EmailBlock(required=True), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), @@ -44,26 +45,26 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="cofdirectoryentrypage", name="links_en", - field=wagtail.fields.StreamField( + field=wagtail.core.fields.StreamField( [ ( "lien", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), + ("url", wagtail.core.blocks.URLBlock(required=True)), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), ( "contact", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "email", - wagtail.blocks.EmailBlock(required=True), + wagtail.core.blocks.EmailBlock(required=True), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), @@ -75,26 +76,26 @@ class Migration(migrations.Migration): migrations.AlterField( model_name="cofdirectoryentrypage", name="links_fr", - field=wagtail.fields.StreamField( + field=wagtail.core.fields.StreamField( [ ( "lien", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), + ("url", wagtail.core.blocks.URLBlock(required=True)), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), ( "contact", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( [ ( "email", - wagtail.blocks.EmailBlock(required=True), + wagtail.core.blocks.EmailBlock(required=True), ), - ("texte", wagtail.blocks.CharBlock()), + ("texte", wagtail.core.blocks.CharBlock()), ] ), ), diff --git a/gestioncof/cms/migrations/0004_auto_20200829_2314.py b/gestioncof/cms/migrations/0004_auto_20200829_2314.py deleted file mode 100644 index eb660ad9..00000000 --- a/gestioncof/cms/migrations/0004_auto_20200829_2314.py +++ /dev/null @@ -1,133 +0,0 @@ -# Generated by Django 2.2.15 on 2020-08-29 21:14 - -import wagtail.blocks -import wagtail.fields -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("cofcms", "0003_directory_entry_optional_links"), - ] - - operations = [ - migrations.AlterField( - model_name="cofdirectoryentrypage", - name="links", - field=wagtail.fields.StreamField( - [ - ( - "lien", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "contact", - wagtail.blocks.StructBlock( - [ - ( - "email", - wagtail.blocks.EmailBlock(required=True), - ), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "info", - wagtail.blocks.StructBlock( - [ - ("nom", wagtail.blocks.CharBlock(required=False)), - ("texte", wagtail.blocks.CharBlock(required=True)), - ] - ), - ), - ], - blank=True, - ), - ), - migrations.AlterField( - model_name="cofdirectoryentrypage", - name="links_en", - field=wagtail.fields.StreamField( - [ - ( - "lien", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "contact", - wagtail.blocks.StructBlock( - [ - ( - "email", - wagtail.blocks.EmailBlock(required=True), - ), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "info", - wagtail.blocks.StructBlock( - [ - ("nom", wagtail.blocks.CharBlock(required=False)), - ("texte", wagtail.blocks.CharBlock(required=True)), - ] - ), - ), - ], - blank=True, - null=True, - ), - ), - migrations.AlterField( - model_name="cofdirectoryentrypage", - name="links_fr", - field=wagtail.fields.StreamField( - [ - ( - "lien", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "contact", - wagtail.blocks.StructBlock( - [ - ( - "email", - wagtail.blocks.EmailBlock(required=True), - ), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "info", - wagtail.blocks.StructBlock( - [ - ("nom", wagtail.blocks.CharBlock(required=False)), - ("texte", wagtail.blocks.CharBlock(required=True)), - ] - ), - ), - ], - blank=True, - null=True, - ), - ), - ] diff --git a/gestioncof/cms/migrations/0005_alter_cofdirectoryentrypage_links_and_more.py b/gestioncof/cms/migrations/0005_alter_cofdirectoryentrypage_links_and_more.py deleted file mode 100644 index 5b563942..00000000 --- a/gestioncof/cms/migrations/0005_alter_cofdirectoryentrypage_links_and_more.py +++ /dev/null @@ -1,203 +0,0 @@ -# Generated by Django 4.2.17 on 2024-12-19 12:27 - -import wagtail.blocks -import wagtail.fields -import wagtail.images.blocks -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("cofcms", "0004_auto_20200829_2314"), - ] - - operations = [ - migrations.AlterField( - model_name="cofdirectoryentrypage", - name="links", - field=wagtail.fields.StreamField( - [ - ( - "lien", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "contact", - wagtail.blocks.StructBlock( - [ - ("email", wagtail.blocks.EmailBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "info", - wagtail.blocks.StructBlock( - [ - ("nom", wagtail.blocks.CharBlock(required=False)), - ("texte", wagtail.blocks.CharBlock(required=True)), - ] - ), - ), - ], - blank=True, - use_json_field=True, - ), - ), - migrations.AlterField( - model_name="cofdirectoryentrypage", - name="links_en", - field=wagtail.fields.StreamField( - [ - ( - "lien", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "contact", - wagtail.blocks.StructBlock( - [ - ("email", wagtail.blocks.EmailBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "info", - wagtail.blocks.StructBlock( - [ - ("nom", wagtail.blocks.CharBlock(required=False)), - ("texte", wagtail.blocks.CharBlock(required=True)), - ] - ), - ), - ], - blank=True, - null=True, - use_json_field=True, - ), - ), - migrations.AlterField( - model_name="cofdirectoryentrypage", - name="links_fr", - field=wagtail.fields.StreamField( - [ - ( - "lien", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "contact", - wagtail.blocks.StructBlock( - [ - ("email", wagtail.blocks.EmailBlock(required=True)), - ("texte", wagtail.blocks.CharBlock()), - ] - ), - ), - ( - "info", - wagtail.blocks.StructBlock( - [ - ("nom", wagtail.blocks.CharBlock(required=False)), - ("texte", wagtail.blocks.CharBlock(required=True)), - ] - ), - ), - ], - blank=True, - null=True, - use_json_field=True, - ), - ), - migrations.AlterField( - model_name="cofpage", - name="body", - field=wagtail.fields.StreamField( - [ - ("heading", wagtail.blocks.CharBlock(form_classname="full title")), - ("paragraph", wagtail.blocks.RichTextBlock()), - ("image", wagtail.images.blocks.ImageChooserBlock()), - ( - "iframe", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock("Adresse de la page")), - ( - "height", - wagtail.blocks.CharBlock("Hauteur (en pixels)"), - ), - ] - ), - ), - ], - use_json_field=True, - ), - ), - migrations.AlterField( - model_name="cofpage", - name="body_en", - field=wagtail.fields.StreamField( - [ - ("heading", wagtail.blocks.CharBlock(form_classname="full title")), - ("paragraph", wagtail.blocks.RichTextBlock()), - ("image", wagtail.images.blocks.ImageChooserBlock()), - ( - "iframe", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock("Adresse de la page")), - ( - "height", - wagtail.blocks.CharBlock("Hauteur (en pixels)"), - ), - ] - ), - ), - ], - null=True, - use_json_field=True, - ), - ), - migrations.AlterField( - model_name="cofpage", - name="body_fr", - field=wagtail.fields.StreamField( - [ - ("heading", wagtail.blocks.CharBlock(form_classname="full title")), - ("paragraph", wagtail.blocks.RichTextBlock()), - ("image", wagtail.images.blocks.ImageChooserBlock()), - ( - "iframe", - wagtail.blocks.StructBlock( - [ - ("url", wagtail.blocks.URLBlock("Adresse de la page")), - ( - "height", - wagtail.blocks.CharBlock("Hauteur (en pixels)"), - ), - ] - ), - ), - ], - null=True, - use_json_field=True, - ), - ), - ] diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index 6e9e8715..3bb757b7 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -1,11 +1,12 @@ from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from django.db import models -from wagtail import blocks -from wagtail.admin.panels import FieldPanel +from wagtail.admin.edit_handlers import FieldPanel, StreamFieldPanel from wagtail.contrib.routable_page.models import RoutablePageMixin, route -from wagtail.fields import RichTextField, StreamField +from wagtail.core import blocks +from wagtail.core.fields import RichTextField, StreamField +from wagtail.core.models import Page from wagtail.images.blocks import ImageChooserBlock -from wagtail.models import Page +from wagtail.images.edit_handlers import ImageChooserPanel # Page pouvant afficher des actualités @@ -30,10 +31,6 @@ class COFRootPage(RoutablePageMixin, Page, COFActuIndexMixin): verbose_name = "Racine site du COF" verbose_name_plural = "Racines site du COF" - @property - def actus(self): - return super().actus[:4] - # Mini calendrier @route(r"^calendar/(\d+)/(\d+)/$") def calendar(self, request, year, month): @@ -41,13 +38,6 @@ class COFRootPage(RoutablePageMixin, Page, COFActuIndexMixin): return raw_calendar_view(request, int(year), int(month)) - # Captcha Mailing-listes - @route(r"^sympa/captcha/$") - def sympa_captcha(self, request): - from .views import sympa_captcha_form_view - - return sympa_captcha_form_view(request) - # Block iframe class IFrameBlock(blocks.StructBlock): @@ -68,11 +58,10 @@ class COFPage(Page): ("paragraph", blocks.RichTextBlock()), ("image", ImageChooserBlock()), ("iframe", IFrameBlock()), - ], - use_json_field=True, + ] ) - content_panels = Page.content_panels + [FieldPanel("body")] + content_panels = Page.content_panels + [StreamFieldPanel("body")] subpage_types = ["COFDirectoryPage", "COFPage"] parent_page_types = ["COFPage", "COFRootPage"] @@ -127,7 +116,7 @@ class COFActuPage(RoutablePageMixin, Page): all_day = models.BooleanField("Toute la journée", default=False, blank=True) content_panels = Page.content_panels + [ - FieldPanel("image"), + ImageChooserPanel("image"), FieldPanel("chapo"), FieldPanel("body", classname="full"), FieldPanel("is_event"), @@ -193,18 +182,8 @@ class COFDirectoryEntryPage(Page): ] ), ), - ( - "info", - blocks.StructBlock( - [ - ("nom", blocks.CharBlock(required=False)), - ("texte", blocks.CharBlock(required=True)), - ] - ), - ), ], blank=True, - use_json_field=True, ) image = models.ForeignKey( @@ -217,9 +196,9 @@ class COFDirectoryEntryPage(Page): ) content_panels = Page.content_panels + [ - FieldPanel("image"), + ImageChooserPanel("image"), FieldPanel("body", classname="full"), - FieldPanel("links"), + StreamFieldPanel("links"), ] subpage_types = [] diff --git a/gestioncof/cms/static/cofcms/css/ie.css b/gestioncof/cms/static/cofcms/css/ie.css new file mode 100644 index 00000000..5cd5b6c5 --- /dev/null +++ b/gestioncof/cms/static/cofcms/css/ie.css @@ -0,0 +1,5 @@ +/* Welcome to Compass. Use this file to write IE specific override styles. + * Import this file using the following HTML or equivalent: + * */ diff --git a/gestioncof/cms/static/cofcms/css/print.css b/gestioncof/cms/static/cofcms/css/print.css new file mode 100644 index 00000000..b0e9e456 --- /dev/null +++ b/gestioncof/cms/static/cofcms/css/print.css @@ -0,0 +1,3 @@ +/* Welcome to Compass. Use this file to define print styles. + * Import this file using the following HTML or equivalent: + * */ diff --git a/gestioncof/cms/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css index bbd90344..4cab72c5 100644 --- a/gestioncof/cms/static/cofcms/css/screen.css +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -2,7 +2,8 @@ * In this file you should write your main styles. (or centralize your imports) * Import this file using the following HTML or equivalent: * */ -/* line 5, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +@import url("https://fonts.googleapis.com/css?family=Carter+One|Source+Sans+Pro:300,300i,700"); +/* line 5, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, @@ -24,158 +25,141 @@ time, mark, audio, video { vertical-align: baseline; } -/* line 22, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 22, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html { line-height: 1; } -/* line 24, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 24, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ ol, ul { list-style: none; } -/* line 26, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 26, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ table { border-collapse: collapse; border-spacing: 0; } -/* line 28, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 28, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; } -/* line 30, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 30, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q, blockquote { quotes: none; } -/* line 103, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 103, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q:before, q:after, blockquote:before, blockquote:after { content: ""; content: none; } -/* line 32, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 32, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ a img { border: none; } -/* line 116, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 116, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } -/* line 10, ../sass/screen.scss */ +/* line 12, ../sass/screen.scss */ *, *:after, *:before { box-sizing: border-box; } -/* line 14, ../sass/screen.scss */ +/* line 16, ../sass/screen.scss */ body { background: #fefefe; font: 17px "Source Sans Pro", "sans-serif"; - color: #000; } -/* line 20, ../sass/screen.scss */ +/* line 21, ../sass/screen.scss */ header { background: #5B0012; } -/* line 24, ../sass/screen.scss */ +/* line 25, ../sass/screen.scss */ h1, h2 { font-family: "Carter One", "serif"; -} - -/* line 28, ../sass/screen.scss */ -h1 { - font-size: 2.3em; color: #90001C; } -/* line 33, ../sass/screen.scss */ -h2 { - font-size: 1.6em; - color: #b01432; +/* line 30, ../sass/screen.scss */ +h1 { + font-size: 2.3em; } -/* line 39, ../sass/screen.scss */ +/* line 34, ../sass/screen.scss */ +h2 { + font-size: 1.6em; +} + +/* line 38, ../sass/screen.scss */ a { color: #CC9500; text-decoration: none; font-weight: bold; - padding: 0 2px; - margin: 0 -2px; -} -/* line 45, ../sass/screen.scss */ -a:hover { - text-decoration: underline; } -/* line 50, ../sass/screen.scss */ +/* line 44, ../sass/screen.scss */ h2 a { font-weight: inherit; color: inherit; } -/* line 56, ../sass/screen.scss */ +/* line 50, ../sass/screen.scss */ header a { color: #fefefe; } -/* line 58, ../sass/screen.scss */ -header a:hover { - text-decoration: none; -} -/* line 62, ../sass/screen.scss */ +/* line 53, ../sass/screen.scss */ header section { display: flex; width: 100%; justify-content: space-between; align-items: stretch; } -/* line 68, ../sass/screen.scss */ +/* line 59, ../sass/screen.scss */ header section.bottom-menu { justify-content: space-around; text-align: center; background: #90001C; } -/* line 74, ../sass/screen.scss */ +/* line 65, ../sass/screen.scss */ header h1 { padding: 0 15px; } -/* line 77, ../sass/screen.scss */ -header nav { - display: inline-flex; -} -/* line 79, ../sass/screen.scss */ +/* line 69, ../sass/screen.scss */ header nav ul { display: inline-flex; - flex-wrap: wrap; } -/* line 82, ../sass/screen.scss */ +/* line 71, ../sass/screen.scss */ header nav ul li { display: inline-block; } -/* line 84, ../sass/screen.scss */ +/* line 73, ../sass/screen.scss */ header nav ul li > * { display: block; padding: 10px 15px; font-weight: bold; } -/* line 89, ../sass/screen.scss */ +/* line 78, ../sass/screen.scss */ header nav ul li > *:hover { background: #280008; } -/* line 95, ../sass/screen.scss */ +/* line 84, ../sass/screen.scss */ header nav .lang-select { display: inline-block; height: 100%; vertical-align: top; position: relative; } -/* line 101, ../sass/screen.scss */ +/* line 90, ../sass/screen.scss */ header nav .lang-select:before { content: ""; color: #fff; @@ -187,12 +171,12 @@ header nav .lang-select:before { margin: 10px 0; padding-left: 10px; } -/* line 113, ../sass/screen.scss */ +/* line 102, ../sass/screen.scss */ header nav .lang-select a { padding: 10px 20px; display: block; } -/* line 117, ../sass/screen.scss */ +/* line 106, ../sass/screen.scss */ header nav .lang-select a img { display: block; width: auto; @@ -200,34 +184,34 @@ header nav .lang-select a img { vertical-align: middle; } -/* line 128, ../sass/screen.scss */ +/* line 117, ../sass/screen.scss */ article { line-height: 1.4; } -/* line 130, ../sass/screen.scss */ +/* line 119, ../sass/screen.scss */ article p, article ul { margin: 0.4em 0; } -/* line 133, ../sass/screen.scss */ +/* line 122, ../sass/screen.scss */ article ul { padding-left: 20px; } -/* line 135, ../sass/screen.scss */ +/* line 124, ../sass/screen.scss */ article ul li { list-style: outside; } -/* line 139, ../sass/screen.scss */ +/* line 128, ../sass/screen.scss */ article:last-child { margin-bottom: 30px; } -/* line 144, ../sass/screen.scss */ +/* line 133, ../sass/screen.scss */ .container { max-width: 1000px; margin: 0 auto; position: relative; } -/* line 149, ../sass/screen.scss */ +/* line 138, ../sass/screen.scss */ .container .aside-wrap { position: absolute; top: 30px; @@ -235,7 +219,7 @@ article:last-child { width: 25%; left: 6px; } -/* line 156, ../sass/screen.scss */ +/* line 145, ../sass/screen.scss */ .container .aside-wrap .aside { color: #222; position: fixed; @@ -244,180 +228,114 @@ article:last-child { width: 100%; background: #FFC500; padding: 15px; - box-shadow: -3px 3px 1px rgba(34, 6, 12, 0.3); + box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.3); } -/* line 166, ../sass/screen.scss */ +/* line 155, ../sass/screen.scss */ .container .aside-wrap .aside h2 { - color: #000; + color: #fff; } -/* line 170, ../sass/screen.scss */ +/* line 159, ../sass/screen.scss */ .container .aside-wrap .aside .calendar { margin: 0 auto; display: block; } -/* line 174, ../sass/screen.scss */ -.container .aside-wrap .aside .calendar:last-child { - margin-bottom: 40px; -} -/* line 179, ../sass/screen.scss */ +/* line 164, ../sass/screen.scss */ .container .aside-wrap .aside a { - color: #000; - text-decoration: none; - background-image: linear-gradient(to top, rgba(255, 255, 255, 0.7) 30%, rgba(255, 255, 255, 0) 30%); + color: #997000; } -/* line 183, ../sass/screen.scss */ -.container .aside-wrap .aside a:hover { - text-decoration: underline; -} -/* line 188, ../sass/screen.scss */ -.container .aside-wrap .aside .aside-content { - max-height: 70vh; - max-height: calc(80vh - 150px); - overflow-y: auto; - overflow-x: hidden; -} -/* line 196, ../sass/screen.scss */ -.container .aside-wrap .aside ul.directory li { - list-style: "*" inside; - padding-left: 10px; - text-indent: -10px; - margin-bottom: 5px; -} -/* line 206, ../sass/screen.scss */ +/* line 170, ../sass/screen.scss */ .container .content { max-width: 900px; margin-left: auto; margin-right: 6px; } -/* line 211, ../sass/screen.scss */ -.container .content h3 { - font-weight: bold; - font-size: 1.2em; -} -/* line 216, ../sass/screen.scss */ -.container .content h4 { - font-weight: bold; - font-style: italic; -} -/* line 221, ../sass/screen.scss */ -.container .content b, .container .content strong { - font-weight: bold; -} -/* line 225, ../sass/screen.scss */ -.container .content i { - font-style: italic; -} -/* line 229, ../sass/screen.scss */ -.container .content .intro, .container .content article.paragraph, .container .content article.entry { - line-height: 1.5; -} -/* line 232, ../sass/screen.scss */ -.container .content .intro a, .container .content article.paragraph a, .container .content article.entry a { - color: #000; - background-image: linear-gradient(to top, rgba(255, 197, 0, 0.8) 30%, rgba(255, 197, 0, 0) 30%); - text-decoration: none; -} -/* line 238, ../sass/screen.scss */ -.container .content .intro ul, .container .content .intro ol, .container .content article.paragraph ul, .container .content article.paragraph ol, .container .content article.entry ul, .container .content article.entry ol { - padding-left: 1em; -} -/* line 244, ../sass/screen.scss */ -.container .content .intro ul li, .container .content article.paragraph ul li, .container .content article.entry ul li { - list-style: disc; -} -/* line 247, ../sass/screen.scss */ -.container .content .intro ol li, .container .content article.paragraph ol li, .container .content article.entry ol li { - list-style: arabic; -} -/* line 252, ../sass/screen.scss */ +/* line 175, ../sass/screen.scss */ .container .content .intro { border-bottom: 3px solid #7f7f7f; margin: 20px 0; margin-top: 5px; padding: 15px 5px; } -/* line 261, ../sass/screen.scss */ +/* line 184, ../sass/screen.scss */ .container .content section article { background: #fff; padding: 20px 30px; - box-shadow: -3px 3px 1px rgba(34, 6, 12, 0.3); - border: 1px solid rgba(34, 6, 12, 0.1); + box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.3); + border: 1px solid rgba(153, 118, 0, 0.1); border-radius: 2px; } -/* line 269, ../sass/screen.scss */ +/* line 190, ../sass/screen.scss */ +.container .content section article a { + color: #CC9500; +} +/* line 195, ../sass/screen.scss */ .container .content section article + h2 { margin-top: 15px; } -/* line 273, ../sass/screen.scss */ +/* line 199, ../sass/screen.scss */ .container .content section article + article { margin-top: 25px; } -/* line 277, ../sass/screen.scss */ +/* line 203, ../sass/screen.scss */ .container .content section .image { margin: 15px 0; text-align: center; padding: 20px; } -/* line 282, ../sass/screen.scss */ +/* line 208, ../sass/screen.scss */ .container .content section .image img { max-width: 100%; height: auto; - box-shadow: -7px 7px 1px rgba(34, 6, 12, 0.2); + box-shadow: -7px 7px 1px rgba(153, 118, 0, 0.2); } -/* line 290, ../sass/screen.scss */ +/* line 216, ../sass/screen.scss */ .container .content section.directory article.entry { - width: 90%; + width: 80%; max-width: 600px; max-height: 100%; position: relative; margin-left: 6%; } -/* line 297, ../sass/screen.scss */ +/* line 223, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image { display: block; float: right; width: 150px; background: #fff; - box-shadow: -4px 4px 1px rgba(34, 6, 12, 0.2); - border-right: 1px solid rgba(34, 6, 12, 0.2); - border-top: 1px solid rgba(34, 6, 12, 0.2); + box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.2); + border-right: 1px solid rgba(153, 118, 0, 0.2); + border-top: 1px solid rgba(153, 118, 0, 0.2); padding: 1px; overflow: hidden; margin-left: 10px; margin-bottom: 10px; transform: translateX(10px); - line-height: 0; } -/* line 312, ../sass/screen.scss */ +/* line 237, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image img { width: auto; height: auto; max-width: 100%; max-height: 100%; } -/* line 320, ../sass/screen.scss */ +/* line 245, ../sass/screen.scss */ .container .content section.directory article.entry ul.links { margin-top: 10px; border-top: 1px solid #90001C; padding-top: 10px; - font-weight: bold; } -/* line 326, ../sass/screen.scss */ -.container .content section.directory article.entry ul.links .label { - font-weight: normal; -} -/* line 333, ../sass/screen.scss */ +/* line 253, ../sass/screen.scss */ .container .content section.actuhome { display: flex; flex-wrap: wrap; justify-content: space-around; align-items: top; } -/* line 339, ../sass/screen.scss */ +/* line 259, ../sass/screen.scss */ .container .content section.actuhome article + article { margin: 0; } -/* line 343, ../sass/screen.scss */ +/* line 263, ../sass/screen.scss */ .container .content section.actuhome article.actu { position: relative; background: none; @@ -427,12 +345,12 @@ article:last-child { min-width: 300px; flex: 1; } -/* line 352, ../sass/screen.scss */ +/* line 272, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header { position: relative; - box-shadow: -4px 5px 1px rgba(34, 6, 12, 0.3); - border-right: 1px solid rgba(34, 6, 12, 0.2); - border-top: 1px solid rgba(34, 6, 12, 0.2); + box-shadow: -4px 5px 1px rgba(153, 118, 0, 0.3); + border-right: 1px solid rgba(153, 118, 0, 0.2); + border-top: 1px solid rgba(153, 118, 0, 0.2); min-height: 180px; padding: 0; margin: 0; @@ -441,41 +359,41 @@ article:last-child { background-position: center center; background-repeat: no-repeat; } -/* line 365, ../sass/screen.scss */ +/* line 285, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 { position: absolute; width: 100%; bottom: 0; left: 0; padding: 5px; - text-shadow: 0 0 5px rgba(34, 6, 12, 0.8); + text-shadow: 0 0 5px rgba(153, 118, 0, 0.8); background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); } -/* line 373, ../sass/screen.scss */ +/* line 293, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 a { color: #fff; } -/* line 379, ../sass/screen.scss */ +/* line 299, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc { background: white; - box-shadow: -2px 2px 1px rgba(34, 6, 12, 0.2); - border: 1px solid rgba(34, 6, 12, 0.2); + box-shadow: -2px 2px 1px rgba(153, 118, 0, 0.2); + border: 1px solid rgba(153, 118, 0, 0.2); border-radius: 2px; margin: 0 10px; padding: 15px; padding-top: 5px; } -/* line 388, ../sass/screen.scss */ +/* line 308, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-minical { display: block; } -/* line 391, ../sass/screen.scss */ +/* line 311, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-dates { display: block; text-align: right; font-size: 0.9em; } -/* line 398, ../sass/screen.scss */ +/* line 318, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-overlay { display: block; background: none; @@ -487,100 +405,98 @@ article:last-child { z-index: 5; opacity: 0; } -/* line 414, ../sass/screen.scss */ +/* line 334, ../sass/screen.scss */ .container .content section.actulist article.actu { display: flex; width: 100%; padding: 0; } -/* line 419, ../sass/screen.scss */ +/* line 339, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-image { width: 30%; max-width: 200px; background-size: cover; background-position: center center; } -/* line 425, ../sass/screen.scss */ +/* line 345, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos { padding: 15px; flex: 1; } -/* line 429, ../sass/screen.scss */ +/* line 349, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos .actu-dates { font-weight: bold; font-size: 0.9em; } -/* line 439, ../sass/screen.scss */ +/* line 359, ../sass/screen.scss */ .container .aside-wrap + .content { max-width: 70%; } -/* line 444, ../sass/screen.scss */ +/* line 364, ../sass/screen.scss */ .calendar { color: rgba(0, 0, 0, 0.8); width: 200px; } -/* line 448, ../sass/screen.scss */ +/* line 368, ../sass/screen.scss */ .calendar td, .calendar th { text-align: center; vertical-align: middle; border: 2px solid transparent; padding: 1px; } -/* line 455, ../sass/screen.scss */ +/* line 375, ../sass/screen.scss */ .calendar th { font-weight: bold; } -/* line 459, ../sass/screen.scss */ +/* line 379, ../sass/screen.scss */ .calendar td { font-size: 0.8em; width: 28px; height: 28px; } -/* line 464, ../sass/screen.scss */ +/* line 384, ../sass/screen.scss */ .calendar td.out { opacity: 0.3; } -/* line 467, ../sass/screen.scss */ +/* line 387, ../sass/screen.scss */ .calendar td.today { border-bottom-color: #000; } -/* line 470, ../sass/screen.scss */ +/* line 390, ../sass/screen.scss */ .calendar td:nth-child(7), .calendar td:nth-child(6) { background: rgba(0, 0, 0, 0.2); } -/* line 473, ../sass/screen.scss */ +/* line 393, ../sass/screen.scss */ .calendar td.hasevent { position: relative; font-weight: bold; color: #90001C; font-size: 1em; } -/* line 479, ../sass/screen.scss */ +/* line 399, ../sass/screen.scss */ .calendar td.hasevent > a { padding: 3px; color: #90001C !important; - background: none !important; } -/* line 485, ../sass/screen.scss */ +/* line 404, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events { - font-size: 0.9em; text-align: left; display: none; position: absolute; z-index: 2; background: #fff; - width: 100px; + width: 150px; left: -30px; margin-top: 10px; padding: 5px; background-color: #90001C; } -/* line 498, ../sass/screen.scss */ +/* line 417, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events .datename { display: none; } -/* line 501, ../sass/screen.scss */ +/* line 420, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events:before { top: -12px; left: 38px; @@ -589,57 +505,33 @@ article:last-child { border: 6px solid transparent; border-bottom-color: #90001C; } -/* line 509, ../sass/screen.scss */ +/* line 428, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events a { color: #fff; - background: none !important; } -/* line 515, ../sass/screen.scss */ +/* line 433, ../sass/screen.scss */ .calendar td.hasevent > a:hover { background-color: #90001C; color: #fff !important; } -/* line 519, ../sass/screen.scss */ +/* line 437, ../sass/screen.scss */ .calendar td.hasevent > a:hover + ul.cal-events { display: block; } -/* line 527, ../sass/screen.scss */ -.calendar tr.head th { - position: relative; -} -/* line 529, ../sass/screen.scss */ -.calendar tr.head th a { - position: absolute; - display: block; - width: 150%; - padding: 5px; - top: -5px; - background: none !important; -} -/* line 537, ../sass/screen.scss */ -.calendar tr.head th:first-child a { - left: 0; - text-align: left; -} -/* line 541, ../sass/screen.scss */ -.calendar tr.head th:last-child a { - text-align: right; - right: 0; -} -/* line 549, ../sass/screen.scss */ +/* line 445, ../sass/screen.scss */ #calendar-wrap .details { border-top: 1px solid #90001C; margin-top: 15px; padding-top: 10px; } -/* line 554, ../sass/screen.scss */ +/* line 450, ../sass/screen.scss */ #calendar-wrap .details li.datename { font-weight: bold; font-size: 1.1em; margin-bottom: 5px; } -/* line 555, ../sass/screen.scss */ +/* line 451, ../sass/screen.scss */ #calendar-wrap .details li.datename:after { content: " :"; } @@ -774,8 +666,4 @@ header .minimenu { display: block; padding: 15px; } - /* line 122, ../sass/_responsive.scss */ - .container .aside-wrap .aside .aside-content { - max-height: calc(100vh - 110px); - } } diff --git a/gestioncof/cms/static/cofcms/js/script.js b/gestioncof/cms/static/cofcms/js/script.js index 0c520e9f..c7693a6d 100644 --- a/gestioncof/cms/static/cofcms/js/script.js +++ b/gestioncof/cms/static/cofcms/js/script.js @@ -2,10 +2,10 @@ $(function() { $(".facteur").on("click", function(){ var $this = $(this); var sticker = $this.attr('data-mref') - .replace(/pont/g, '.') - .replace(/arbre/g, '@') + .replace('pont', '.') + .replace('arbre', '@') .replace(/(.)-/g, '$1'); - + var boite = $("", {href:"ma"+"il"+"to:"+sticker}).text(sticker); $(this).before(boite) .remove(); diff --git a/gestioncof/cms/static/cofcms/sass/_colors.scss b/gestioncof/cms/static/cofcms/sass/_colors.scss index e07429f8..2d295b98 100644 --- a/gestioncof/cms/static/cofcms/sass/_colors.scss +++ b/gestioncof/cms/static/cofcms/sass/_colors.scss @@ -5,7 +5,7 @@ $aside: #FFC500; $titre: $sousbandeau; $lien: #CC9500; $headerlien: $fond; -$ombres: darken(desaturate($bandeau, 30%), 10%); +$ombres: darken($aside, 20%); $bodyfont: "Source Sans Pro", "sans-serif"; $headfont: "Carter One", "serif"; diff --git a/gestioncof/cms/static/cofcms/sass/_responsive.scss b/gestioncof/cms/static/cofcms/sass/_responsive.scss index 7cf4cb29..28216a98 100644 --- a/gestioncof/cms/static/cofcms/sass/_responsive.scss +++ b/gestioncof/cms/static/cofcms/sass/_responsive.scss @@ -118,10 +118,6 @@ header .minimenu { } } } - - .aside-content { - max-height: calc(100vh - 110px); - } } } } diff --git a/gestioncof/cms/static/cofcms/sass/screen.scss b/gestioncof/cms/static/cofcms/sass/screen.scss index 2d7c1ad4..43ad8216 100644 --- a/gestioncof/cms/static/cofcms/sass/screen.scss +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -3,6 +3,8 @@ * Import this file using the following HTML or equivalent: * */ +@import url('https://fonts.googleapis.com/css?family=Carter+One|Source+Sans+Pro:300,300i,700'); + @import "compass/reset"; @import "_colors"; @@ -14,7 +16,6 @@ body { background: $fond; font: 17px $bodyfont; - color: #000; } header { @@ -23,28 +24,21 @@ header { h1, h2 { font-family: $headfont; + color: $titre; } h1 { font-size: 2.3em; - color: $titre; } h2 { font-size: 1.6em; - color: desaturate(lighten($titre, 10%), 20%); } - a { color: $lien; text-decoration: none; font-weight: bold; - padding: 0 2px; - margin: 0 -2px; - &:hover { - text-decoration: underline; - } } h2 a { @@ -55,9 +49,6 @@ h2 a { header { a { color: $headerlien; - &:hover { - text-decoration: none; - } } section { display: flex; @@ -75,10 +66,8 @@ header { padding: 0 15px; } nav { - display: inline-flex; ul { display: inline-flex; - flex-wrap: wrap; li { display: inline-block; & > * { @@ -161,44 +150,19 @@ article { width: 100%; background: $aside; padding: 15px; - box-shadow: -3px 3px 1px rgba($ombres, 0.3); + box-shadow: -4px 4px 1px rgba($ombres, 0.3); h2 { - color: #000; + color: #fff; } .calendar { margin: 0 auto; display: block; - - &:last-child { - margin-bottom: 40px; - } } a { - color: #000; - text-decoration: none; - background-image: linear-gradient(to top, rgba(#fff, 0.7) 30%, rgba(#fff, 0) 30%); - &:hover { - text-decoration: underline; - } - } - - .aside-content { - max-height: 70vh; - max-height: calc(80vh - 150px); - overflow-y: auto; - overflow-x: hidden; - } - - ul.directory { - li { - list-style: "*" inside; - padding-left: 10px; - text-indent: -10px; - margin-bottom: 5px; - } + color: darken($lien, 10%); } } } @@ -208,47 +172,6 @@ article { margin-left: auto; margin-right: 6px; - h3 { - font-weight: bold; - font-size: 1.2em; - } - - h4 { - font-weight: bold; - font-style: italic; - } - - b, strong { - font-weight: bold; - } - - i { - font-style: italic; - } - - .intro, article.paragraph, article.entry { - line-height: 1.5; - - a { - color: #000; - background-image: linear-gradient(to top, rgba($aside, 0.8) 30%, rgba($aside, 0) 30%); - text-decoration: none; - } - - ul, ol { - padding-left: 1em; - li { - - } - } - ul li { - list-style: disc; - } - ol li { - list-style: arabic; - } - } - .intro { border-bottom: 3px solid darken($fond, 50%); margin: 20px 0; @@ -261,9 +184,12 @@ article { article { background: #fff; padding: 20px 30px;; - box-shadow: -3px 3px 1px rgba($ombres, 0.3); + box-shadow: -4px 4px 1px rgba($ombres, 0.3); border: 1px solid rgba($ombres, 0.1); border-radius: 2px; + a { + color: $lien; + } } article + h2 { @@ -288,7 +214,7 @@ article { &.directory { article.entry { - width: 90%; + width: 80%; max-width: 600px; max-height: 100%; position: relative; @@ -307,7 +233,6 @@ article { margin-left: 10px; margin-bottom: 10px; transform: translateX(10px); - line-height: 0; img { width: auto; @@ -321,11 +246,6 @@ article { margin-top: 10px; border-top: 1px solid $sousbandeau; padding-top: 10px; - font-weight: bold; - - .label { - font-weight: normal; - } } } } @@ -479,17 +399,16 @@ article { & > a { padding: 3px; color: $sousbandeau !important; - background: none !important; } ul.cal-events { - font-size: 0.9em; + text-align: left; display: none; position: absolute; z-index: 2; background: #fff; - width: 100px; + width: 150px; left: -30px; margin-top: 10px; padding: 5px; @@ -508,7 +427,6 @@ article { } a { color: #fff; - background: none !important; } } @@ -522,28 +440,6 @@ article { } } } - - tr.head { - th { - position: relative; - a { - position: absolute; - display: block; - width: 150%; - padding: 5px; - top: -5px; - background: none !important; - } - &:first-child a { - left: 0; - text-align: left; - } - &:last-child a { - text-align: right; - right: 0; - } - } - } } #calendar-wrap .details { diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index c420115f..c11a2761 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -4,20 +4,9 @@ - {% block title %} - {% if page.seo_title %} - {{ page.seo_title }} | - {% elif page.title %} - {{ page.title }} | - {% endif %} - {% trans "Association des élèves de l'ENS Ulm" %} - {% endblock %} - - + {% block title %}Association des élèves de l'ENS Ulm{% endblock %} {% block extra_head %}{% endblock %} - - @@ -32,27 +21,27 @@
    {% block superaside %}{% endblock %} - +
    {% block content %}{% endblock %}
    diff --git a/gestioncof/cms/templates/cofcms/base_nav.html b/gestioncof/cms/templates/cofcms/base_nav.html index f8b3fa65..b7ce4c66 100644 --- a/gestioncof/cms/templates/cofcms/base_nav.html +++ b/gestioncof/cms/templates/cofcms/base_nav.html @@ -4,11 +4,7 @@
  • {% if item.link_page %} - {% if item.link_page.seo_title %} - {{ item.link_page.seo_title }} - {% else %} - {{ item.link_page.title }} - {% endif %} + {{ item.link_page.title }} {% else %} diff --git a/gestioncof/cms/templates/cofcms/calendar.html b/gestioncof/cms/templates/cofcms/calendar.html index c06a23c7..a6a9d87e 100644 --- a/gestioncof/cms/templates/cofcms/calendar.html +++ b/gestioncof/cms/templates/cofcms/calendar.html @@ -1,7 +1,7 @@ {% load wagtailcore_tags wagtailroutablepage_tags static i18n %} - + diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index a1ea29ed..a6a909db 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -3,11 +3,11 @@ {% block extra_head %} {{ block.super }} - + {% endblock %} -{% block aside_title %}{% trans "Calendrier" %}{% endblock %} +{% block aside_title %}Calendrier{% endblock %} {% block aside %}
    {% calendar %} @@ -17,17 +17,17 @@ {% block content %}

    {{ page.title }}

    -
    {{ page.introduction|richtext }}
    +
    {{ page.introduction|safe }}
    {% if actus.has_previous %} - {% trans "Actualités plus récentes" %} - {% endif %} + Actualités plus récentes + {% endif %} {% if actus.has_next %} - {% trans "Actualités plus anciennes" %} - {% endif %} - + Actualités plus anciennes + {% endif %} + {% for actu in page.actus %}
    @@ -36,18 +36,18 @@ {% if actu.is_event %}

    {{ actu|dates|capfirst }}
    {{ actu.chapo }}

    {% else %} - {{ actu.body|richtext|truncatewords_html:15 }} + {{ actu.body|safe|truncatewords_html:15 }} {% endif %} - {% trans "Lire plus" %} > + Lire plus >
    {% endfor %} {% if actus.has_previous %} - {% trans "Actualités plus récentes" %} - {% endif %} + Actualités plus récentes + {% endif %} {% if actus.has_next %} - {% trans "Actualités plus anciennes" %} - {% endif %} + Actualités plus anciennes + {% endif %} {% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_actu_page.html b/gestioncof/cms/templates/cofcms/cof_actu_page.html index 5cd88134..b531aedc 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_page.html @@ -1,17 +1,17 @@ {% extends "cofcms/base.html" %} -{% load wagtailcore_tags wagtailimages_tags cofcms_tags i18n %} +{% load wagtailimages_tags cofcms_tags i18n %} {% block content %}

    {{ page.title }}

    -

    {% trans "A lieu" %} {{ page|dates }}

    +

    A lieu {{ page|dates }}

    {{ page.chapo }}

    {% image page.image width-700 %}
    - {{ page.body|richtext }} + {{ page.body|safe }}
    {% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_directory_page.html b/gestioncof/cms/templates/cofcms/cof_directory_page.html index da0fa3ce..6ac5491b 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -1,14 +1,14 @@ {% extends "cofcms/base_aside.html" %} -{% load wagtailcore_tags wagtailimages_tags cofcms_tags static i18n %} +{% load wagtailimages_tags cofcms_tags static %} {% block extra_head %} {{ block.super }} - + {% endblock %} -{% block aside_title %}{% trans "Accès rapide" %}{% endblock %} +{% block aside_title %}Accès rapide{% endblock %} {% block aside %} -
      +
        {% for entry in page.entries %}
      • {{ entry.title }}
      • {% endfor %} @@ -18,7 +18,7 @@ {% block content %}

        {{ page.title }}

        -
        {{ page.introduction|richtext }}
        +
        {{ page.introduction|safe }}
        @@ -28,17 +28,15 @@
        {% image entry.image width-150 class="entry-img" %}
        {% endif %}

        {{ entry.title }}

        -
        {{ entry.body|richtext }}
        +
        {{ entry.body|safe }}
        {% if entry.links %} + +

        Kaisse

        +
        + + +

        Gestion tirages BdA

        {% if active_tirages %} @@ -132,7 +132,6 @@
        diff --git a/gestioncof/templates/gestioncof/mails/welcome.txt b/gestioncof/templates/gestioncof/mails/welcome.txt deleted file mode 100644 index 61244bb2..00000000 --- a/gestioncof/templates/gestioncof/mails/welcome.txt +++ /dev/null @@ -1,11 +0,0 @@ -Bonjour {{ member.first_name }} et bienvenue au COF ! - -Tu trouveras plein de trucs cool sur le site du COF : https://cof.ens.fr/ et notre compte instagram : https://www.instagram.com/cof_ulm -Et n'oublie pas d'aller découvrir GestioCOF, la plateforme de gestion du COF ! -Si tu as des questions, tu peux nous envoyer un mail à cof@ens.fr (on aime le spam), ou passer nous voir au Burô près de la Courô les lundi, mardi, jeudi et vendredi de 12h à 14h et du lundi au jeudi de 18h30 à 19h30. - -Retrouvez tout les évènements organisés par le COF et ses clubs ici : https://calendrier.dgnum.eu/. - -Amicalement, - -Ton COF qui t'aime. diff --git a/gestioncof/templates/gestioncof/registration_kf_form.html b/gestioncof/templates/gestioncof/registration_kf_form.html deleted file mode 100644 index 2b0711f6..00000000 --- a/gestioncof/templates/gestioncof/registration_kf_form.html +++ /dev/null @@ -1,21 +0,0 @@ -{% load bootstrap %} - - {% if login_clipper %} -

        Inscription associée au compte clipper {{ login_clipper }}

        - {% elif member %} -

        Inscription du compte GestioCOF existant {{ member.username }}

        - {% else %} -

        Inscription d'un nouveau compte (extérieur ?)

        - {% endif %} - - {% csrf_token %} -
    < {{ this_month|date:"F Y" }} >
    - {{ user_form | bootstrap }} - {{ profile_form | bootstrap }} -
    -
    - {% if login_clipper or member %} - - {% endif %} - - diff --git a/gestioncof/templates/gestioncof/registration_kf_post.html b/gestioncof/templates/gestioncof/registration_kf_post.html deleted file mode 100644 index b5690d70..00000000 --- a/gestioncof/templates/gestioncof/registration_kf_post.html +++ /dev/null @@ -1,8 +0,0 @@ -{% extends "base_title.html" %} - -{% block realcontent %} -

    Inscription d'un nouveau membre

    -
    - {% include "gestioncof/registration_kf_form.html" %} -
    -{% endblock %} diff --git a/gestioncof/templates/gestioncof/reset_comptes.html b/gestioncof/templates/gestioncof/reset_comptes.html deleted file mode 100644 index 55d54376..00000000 --- a/gestioncof/templates/gestioncof/reset_comptes.html +++ /dev/null @@ -1,14 +0,0 @@ -{% extends "base_title.html" %} - -{% block realcontent %} -

    Remise à zéro des membres COF

    - {% if is_done%} -

    {{nb_adherents}} compte{{ nb_adherents|pluralize }} désinscrit{{ nb_adherents|pluralize }} du COF.

    - {% else%} -
    ATTENTION : Cette action est irréversible.
    -

    Voulez-vous vraiment remettre à zéro le statut COF de tous les membres actuels ?

    -
    - {% csrf_token %} -
    - {% endif %} -{% endblock %} \ No newline at end of file diff --git a/gestioncof/templates/gestioncof/search_results.html b/gestioncof/templates/gestioncof/search_results.html deleted file mode 100644 index fdcd36ce..00000000 --- a/gestioncof/templates/gestioncof/search_results.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "shared/search_results.html" %} -{% load i18n %} - -{% block extra_section %} -
  • - {% if not results %} - - {% trans "Aucune correspondance trouvée" %} - - {% else %} - - {% trans "Pas dans la liste ?" %} - - {% endif %} -
  • -
  • - - {% trans "Créer un compte" %} - -
  • -{% endblock %} diff --git a/gestioncof/templates/gestioncof/self_registration.html b/gestioncof/templates/gestioncof/self_registration.html deleted file mode 100644 index c60dce4f..00000000 --- a/gestioncof/templates/gestioncof/self_registration.html +++ /dev/null @@ -1,28 +0,0 @@ -{% extends "base_title.html" %} -{% load static %} - -{% block page_size %}col-sm-8{% endblock %} - -{% load bootstrap %} - -{% block realcontent %} - - {% if member %} -

    Inscription K-Fêt du compte GestioCOF existant {{ member.username }}

    - {% else %} -

    Inscription K-Fêt d'un nouveau compte (extérieur ?)

    - {% endif %} -
    - {% csrf_token %} - - {{ user_form | bootstrap }} - {{ profile_form | bootstrap }} - {{ agreement_form | bootstrap }} -
    -
    - {% if login_clipper or member %} - - {% endif %} - -
    -{% endblock %} diff --git a/gestioncof/templates/gestioncof/utile_cof.html b/gestioncof/templates/gestioncof/utile_cof.html index 71a3b865..637055c5 100644 --- a/gestioncof/templates/gestioncof/utile_cof.html +++ b/gestioncof/templates/gestioncof/utile_cof.html @@ -4,21 +4,21 @@ {% endblock %} {% block realcontent %} -

    Liens utiles du COF

    -

    COF

    - +

    Liens utiles du COF

    +

    COF

    + -

    Mega

    - +

    Mega

    + -

    Note : pour ouvrir les fichiers .csv avec Excel, il faut - passer par Fichier > Importer et sélectionner la - virgule comme séparateur.

    +

    Note : pour ouvrir les fichiers .csv avec Excel, il faut + passer par Fichier > Importer et sélectionner la + virgule comme séparateur.

    {% endblock %} diff --git a/gestioncof/templates/kfet-denied.html b/gestioncof/templates/kfet-denied.html deleted file mode 100644 index 6d18dbb5..00000000 --- a/gestioncof/templates/kfet-denied.html +++ /dev/null @@ -1,5 +0,0 @@ -{% extends "base_title.html" %} - -{% block realcontent %} -

    Section réservée aux membres K-Fêt -- merci de vous inscrire au COF ou de passer au COF/nous envoyer un mail si vous êtes déjà membre :)

    -{% endblock %} diff --git a/gestioncof/templates/registration.html b/gestioncof/templates/registration.html index 9807afde..2ef997e1 100644 --- a/gestioncof/templates/registration.html +++ b/gestioncof/templates/registration.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block page_size %}col-sm-8{% endblock %} diff --git a/gestioncof/templates/tristate_js.html b/gestioncof/templates/tristate_js.html index 6b5312a8..af906ebe 100644 --- a/gestioncof/templates/tristate_js.html +++ b/gestioncof/templates/tristate_js.html @@ -1,4 +1,4 @@ -{% load static %} +{% load staticfiles %} + +{% endblock %} + diff --git a/journaldecaisse/tests.py b/journaldecaisse/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/journaldecaisse/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/journaldecaisse/urls.py b/journaldecaisse/urls.py new file mode 100644 index 00000000..1afa9e7b --- /dev/null +++ b/journaldecaisse/urls.py @@ -0,0 +1,15 @@ +from django.urls import path +from django.conf.urls import url + +from . import views + +app_name = 'journaldecaisse' +urlpatterns = [ + path('', views.index, name='index'), + path('vente.html', views.vente, name='vente'), + url(r'^ajax/iterate_options/$', views.iterate_options, name='iterate_options'), + url(r'^ajax/submit_entry/$', views.submit_entry, name='submit_entry'), + +] + + diff --git a/journaldecaisse/views.py b/journaldecaisse/views.py new file mode 100644 index 00000000..3240f12a --- /dev/null +++ b/journaldecaisse/views.py @@ -0,0 +1,75 @@ +from django.http import HttpResponse +from .models import JournalEntry, Produit, ProduitStock +from django.shortcuts import render +from django.http import JsonResponse +import datetime + +global_filter = None + +def index(request): + entry_list = JournalEntry.objects.order_by('entry_date') + context = {'entry_list': entry_list} + return render(request, 'journaldecaisse/index.html', context) + +def vente(request): + print(request.user.first_name) + global global_filter + product_list = Produit.objects.all() + global_filter = product_list + product_list = product_list.values_list() + products = [] + for elem in product_list: + products.append(elem[1]) + products = list(set(products)) + + + context = {"product_list" : products} + return render(request, 'journaldecaisse/vente.html', context) + + + + +def iterate_options(request): + + global global_filter + elem_selected = None + elem_price = None + elem_stock = None + options_selected = request.GET.get('option', None) + options_selected = options_selected.split(":") + field = str(Produit._meta.get_fields()[int(options_selected[2])+3]).split(".")[-1] + elems = global_filter.filter(**{options_selected[0]: options_selected[1]}) + global_filter = elems + elems = elems.values_list() + options = [] + for elem in elems: + options.append(elem[int(options_selected[2])+2]) + options = list(set(options)) + if global_filter.count() == 1: + elem_selected = str(global_filter.first()) + elem_price = global_filter.first().price + elem_stock = ProduitStock.objects.get(ProduitTaille=global_filter.first()).Amount + + + + + data = { + 'options': options, + 'options_int': int(options_selected[2])+2, + 'field': field, + 'elem_selected': elem_selected, + 'price': elem_price, + 'stock': elem_stock, + 'name': request.user.first_name + } + return JsonResponse(data) + + + +def submit_entry(request): + product = global_filter.first() + buyer = request.GET.get('buyer', None) + payment = request.GET.get('payment', None) + author = request.user.first_name + JournalEntry.objects.create(entry_date=datetime.datetime.now(), entry_text=str(product) + " " + buyer, cofeux_id=author, entry_amount=product.price, payment_type=payment) + return JsonResponse({"success":"True"}) diff --git a/kfet/__init__.py b/kfet/__init__.py index 4937ccfa..47a6b0b8 100644 --- a/kfet/__init__.py +++ b/kfet/__init__.py @@ -1,2 +1,3 @@ +default_app_config = "kfet.apps.KFetConfig" KFET_DELETED_TRIGRAMME = "☠☠☠" KFET_DELETED_USERNAME = "kfet_deleted_user" diff --git a/kfet/apps.py b/kfet/apps.py index 2843fd67..f3c7b07b 100644 --- a/kfet/apps.py +++ b/kfet/apps.py @@ -10,7 +10,6 @@ class KFetConfig(AppConfig): def register_config(self): import djconfig - from kfet.forms import KFetConfigForm djconfig.register(KFetConfigForm) diff --git a/kfet/auth/__init__.py b/kfet/auth/__init__.py index 2b30780e..ef2486a7 100644 --- a/kfet/auth/__init__.py +++ b/kfet/auth/__init__.py @@ -1,2 +1,4 @@ +default_app_config = "kfet.auth.apps.KFetAuthConfig" + KFET_GENERIC_USERNAME = "kfet_genericteam" KFET_GENERIC_TRIGRAMME = "GNR" diff --git a/kfet/auth/apps.py b/kfet/auth/apps.py index 49e19a26..5b4fe7fd 100644 --- a/kfet/auth/apps.py +++ b/kfet/auth/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig from django.db.models.signals import post_migrate -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ class KFetAuthConfig(AppConfig): diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index 0f7789a1..55e18458 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -1,5 +1,4 @@ from django.contrib.auth import get_user_model -from django.core.exceptions import PermissionDenied from kfet.models import Account, GenericTeamToken @@ -38,36 +37,3 @@ class GenericBackend(BaseKFetBackend): team_token.delete() return get_kfet_generic_user() - - -class BlockFrozenAccountBackend: - def authenticate(self, request, **kwargs): - return None - - def get_user(self, user_id): - return None - - def has_perm(self, user_obj, perm, obj=None): - app_label, _ = perm.split(".") - if app_label == "kfet": - if ( - hasattr(user_obj, "profile") - and hasattr(user_obj.profile, "account_kfet") - and user_obj.profile.account_kfet.is_frozen - ): - raise PermissionDenied - - # Dans le cas général, on se réfère aux autres backends - return False - - def has_module_perms(self, user_obj, app_label): - if app_label == "kfet": - if ( - hasattr(user_obj, "profile") - and hasattr(user_obj.profile, "account_kfet") - and user_obj.profile.account_kfet.is_frozen - ): - raise PermissionDenied - - # Dans le cas général, on se réfère aux autres backends - return False diff --git a/kfet/auth/fields.py b/kfet/auth/fields.py index 4c842b85..a5544787 100644 --- a/kfet/auth/fields.py +++ b/kfet/auth/fields.py @@ -1,24 +1,17 @@ from django import forms -from django.contrib.auth.models import Group - -from .models import KFetPermission +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.forms import widgets class KFetPermissionsField(forms.ModelMultipleChoiceField): def __init__(self, *args, **kwargs): - kwargs.setdefault("queryset", KFetPermission.objects.all()) - kwargs.setdefault("widget", forms.CheckboxSelectMultiple) - super().__init__(*args, **kwargs) - - def label_from_instance(self, obj): - return obj.name - - -class KFetGroupsField(forms.ModelMultipleChoiceField): - def __init__(self, *args, **kwargs): - kwargs.setdefault("queryset", Group.objects.filter(kfetgroup__isnull=False)) - kwargs.setdefault("widget", forms.SelectMultiple) - super().__init__(*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 diff --git a/kfet/auth/forms.py b/kfet/auth/forms.py index 74823364..b1628af0 100644 --- a/kfet/auth/forms.py +++ b/kfet/auth/forms.py @@ -1,29 +1,48 @@ -from django.contrib.auth.models import User -from django.utils.translation import gettext_lazy as _ +from django import forms +from django.contrib.auth.models import Group, User -from shared.forms import ProtectedModelForm - -from .fields import KFetGroupsField, KFetPermissionsField -from .models import KFetGroup +from .fields import KFetPermissionsField -class GroupForm(ProtectedModelForm): +class GroupForm(forms.ModelForm): permissions = KFetPermissionsField() - protected_fields = ["permissions"] + def clean_name(self): + name = self.cleaned_data["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 + # ) + if self.instance.pk is None: + return kfet_perms + 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: - model = KFetGroup + model = Group fields = ["name", "permissions"] -class UserGroupForm(ProtectedModelForm): - groups = KFetGroupsField( - label=_("Statut équipe"), +class UserGroupForm(forms.ModelForm): + groups = forms.ModelMultipleChoiceField( + Group.objects.filter(name__icontains="K-Fêt"), + label="Statut équipe", required=False, ) - protected_fields = ["groups"] + def clean_groups(self): + kfet_groups = self.cleaned_data.get("groups") + if self.instance.pk is None: + return kfet_groups + other_groups = self.instance.groups.exclude(name__icontains="K-Fêt") + return list(kfet_groups) + list(other_groups) class Meta: model = User diff --git a/kfet/auth/migrations/0001_initial.py b/kfet/auth/migrations/0001_initial.py index 9919c46b..caf5d786 100644 --- a/kfet/auth/migrations/0001_initial.py +++ b/kfet/auth/migrations/0001_initial.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ ("auth", "0006_require_contenttypes_0002"), # Following dependency allows using Account model to set up the kfet diff --git a/kfet/auth/migrations/0002_kfetgroup_kfetpermission.py b/kfet/auth/migrations/0002_kfetgroup_kfetpermission.py deleted file mode 100644 index 7e87b2d6..00000000 --- a/kfet/auth/migrations/0002_kfetgroup_kfetpermission.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 2.2.8 on 2020-01-08 21:03 - -import django.contrib.auth.models -import django.db.models.deletion -import django.db.models.manager -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("auth", "0011_update_proxy_permissions"), - ("kfetauth", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="KFetGroup", - fields=[ - ( - "group_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="auth.Group", - ), - ), - ], - options={ - "verbose_name": "Groupe K-Fêt", - "verbose_name_plural": "Groupes K-Fêt", - }, - bases=("auth.group",), - managers=[("objects", django.contrib.auth.models.GroupManager())], - ), - migrations.CreateModel( - name="KFetPermission", - fields=[], - options={ - "verbose_name": "Permission K-Fêt", - "verbose_name_plural": "Permissions K-Fêt", - "proxy": True, - "indexes": [], - "constraints": [], - }, - bases=("auth.permission",), - managers=[ - ("kfet", django.db.models.manager.Manager()), - ("objects", django.contrib.auth.models.PermissionManager()), - ], - ), - ] diff --git a/kfet/auth/migrations/0003_existing_groups.py b/kfet/auth/migrations/0003_existing_groups.py deleted file mode 100644 index 43fd221c..00000000 --- a/kfet/auth/migrations/0003_existing_groups.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 2.2.8 on 2020-01-08 21:04 - -from django.db import migrations - - -def existing_groups(apps, schema_editor): - Group = apps.get_model("auth", "Group") - KFetGroup = apps.get_model("kfetauth", "KFetGroup") - - for group in Group.objects.filter(name__icontains="K-Fêt"): - kf_group = KFetGroup(group_ptr=group, pk=group.pk, name=group.name) - kf_group.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("kfetauth", "0002_kfetgroup_kfetpermission"), - ] - - operations = [migrations.RunPython(existing_groups)] diff --git a/kfet/auth/models.py b/kfet/auth/models.py index 70e2de51..73a70c22 100644 --- a/kfet/auth/models.py +++ b/kfet/auth/models.py @@ -1,9 +1,5 @@ -from django.contrib.auth.models import Group, Permission from django.db import models from django.utils.crypto import get_random_string -from django.utils.translation import gettext_lazy as _ - -KFET_APP_LABELS = ["kfet", "kfetauth"] class GenericTeamTokenManager(models.Manager): @@ -18,28 +14,3 @@ class GenericTeamToken(models.Model): token = models.CharField(max_length=50, unique=True) objects = GenericTeamTokenManager() - - -class KFetPermissionManager(models.Manager): - def get_queryset(self): - return ( - super().get_queryset().filter(content_type__app_label__in=KFET_APP_LABELS) - ) - - -class KFetPermission(Permission): - objects = KFetPermissionManager() - - class Meta: - proxy = True - verbose_name = _("Permission K-Fêt") - verbose_name_plural = _("Permissions K-Fêt") - - -class KFetGroup(Group): - # On fait un héritage complet pour - # mieux distinguer les groupes K-Fêt via l'ORM (i.e. faire `KFetGroup.objects.all`) - - class Meta: - verbose_name = _("Groupe K-Fêt") - verbose_name_plural = _("Groupes K-Fêt") diff --git a/kfet/auth/signals.py b/kfet/auth/signals.py index b1e3fc57..3f691068 100644 --- a/kfet/auth/signals.py +++ b/kfet/auth/signals.py @@ -3,7 +3,7 @@ from django.contrib.auth.signals import user_logged_in from django.dispatch import receiver from django.urls import reverse from django.utils.safestring import mark_safe -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ugettext as _ from .utils import get_kfet_generic_user diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index ade0d11a..9006612c 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -10,7 +10,7 @@ from kfet.models import Account from . import KFET_GENERIC_TRIGRAMME, KFET_GENERIC_USERNAME from .backends import AccountBackend, GenericBackend -from .models import GenericTeamToken, KFetGroup +from .models import GenericTeamToken from .utils import get_kfet_generic_user from .views import GenericLoginView @@ -27,8 +27,11 @@ class UserGroupFormTests(TestCase): self.user = User.objects.create(username="foo", password="foo") # create some K-Fêt groups + prefix_name = "K-Fêt " names = ["Group 1", "Group 2", "Group 3"] - self.kfet_groups = [KFetGroup.objects.create(name=name) for name in names] + self.kfet_groups = [ + Group.objects.create(name=prefix_name + name) for name in names + ] # create a non-K-Fêt group self.other_group = Group.objects.create(name="Other group") @@ -38,10 +41,7 @@ class UserGroupFormTests(TestCase): form = UserGroupForm(instance=self.user) groups_field = form.fields["groups"] self.assertQuerysetEqual( - groups_field.queryset, - [repr(g.group_ptr) for g in self.kfet_groups], - transform=repr, - ordered=False, + groups_field.queryset, [repr(g) for g in self.kfet_groups], ordered=False ) def test_keep_others(self): @@ -59,8 +59,7 @@ class UserGroupFormTests(TestCase): form.save() self.assertQuerysetEqual( user.groups.all(), - [self.other_group.pk] + [group.pk for group in self.kfet_groups], - transform=lambda group: group.pk, + [repr(g) for g in [self.other_group] + self.kfet_groups], ordered=False, ) @@ -169,7 +168,7 @@ class GenericLoginViewTests(TestCase): r = self.client.post(self.url) self.assertRedirects( - r, "/gestion/logout?next={}".format(self.url), fetch_redirect_response=False + r, "/logout?next={}".format(self.url), fetch_redirect_response=False ) def test_notoken_not_team(self): @@ -181,13 +180,13 @@ class GenericLoginViewTests(TestCase): # With GET. r = self.client.get(self.url) self.assertRedirects( - r, "/gestion/login?next={}".format(self.url), fetch_redirect_response=False + r, "/login?next={}".format(self.url), fetch_redirect_response=False ) # Also with POST. r = self.client.post(self.url) self.assertRedirects( - r, "/gestion/login?next={}".format(self.url), fetch_redirect_response=False + r, "/login?next={}".format(self.url), fetch_redirect_response=False ) def _set_signed_cookie(self, client, key, value): @@ -285,11 +284,7 @@ class TemporaryAuthTests(TestCase): self.perm = Permission.objects.get( content_type__app_label="kfet", codename="is_team" ) - self.perm2 = Permission.objects.get( - content_type__app_label="kfet", codename="can_force_close" - ) - self.user1.user_permissions.add(self.perm) - self.user2.user_permissions.add(self.perm, self.perm2) + self.user2.user_permissions.add(self.perm) def test_context_processor(self): """ @@ -300,7 +295,7 @@ class TemporaryAuthTests(TestCase): r = self.client.post("/k-fet/accounts/000/edit", HTTP_KFETPASSWORD="kfet_user2") self.assertEqual(r.context["user"], self.user1) - self.assertNotIn("kfet.can_force_close", r.context["perms"]) + self.assertNotIn("kfet.is_team", r.context["perms"]) def test_auth_not_persistent(self): """ diff --git a/kfet/auth/views.py b/kfet/auth/views.py index 15519caa..5dd048e9 100644 --- a/kfet/auth/views.py +++ b/kfet/auth/views.py @@ -1,7 +1,7 @@ from django.contrib import messages from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import permission_required -from django.contrib.auth.models import User +from django.contrib.auth.models import Group, User from django.contrib.auth.views import redirect_to_login from django.contrib.messages.views import SuccessMessageMixin from django.db.models import Prefetch @@ -9,13 +9,13 @@ from django.http import QueryDict from django.shortcuts import redirect, render from django.urls import reverse, reverse_lazy from django.utils.decorators import method_decorator -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ from django.views.decorators.http import require_http_methods from django.views.generic import View from django.views.generic.edit import CreateView, UpdateView from .forms import GroupForm -from .models import GenericTeamToken, KFetGroup +from .models import GenericTeamToken class GenericLoginView(View): @@ -113,20 +113,23 @@ def account_group(request): user_pre = Prefetch( "user_set", queryset=User.objects.select_related("profile__account_kfet") ) - groups = KFetGroup.objects.prefetch_related("permissions", user_pre) + groups = Group.objects.filter(name__icontains="K-Fêt").prefetch_related( + "permissions", user_pre + ) return render(request, "kfet/account_group.html", {"groups": groups}) -class AccountGroupFormMixin(SuccessMessageMixin): - model = KFetGroup +class AccountGroupCreate(SuccessMessageMixin, CreateView): + model = Group template_name = "kfet/account_group_form.html" form_class = GroupForm + success_message = "Nouveau groupe : %(name)s" success_url = reverse_lazy("kfet.account.group") -class AccountGroupCreate(AccountGroupFormMixin, CreateView): - success_message = "Nouveau groupe : %(name)s" - - -class AccountGroupUpdate(AccountGroupFormMixin, UpdateView): +class AccountGroupUpdate(SuccessMessageMixin, UpdateView): + queryset = Group.objects.filter(name__icontains="K-Fêt") + template_name = "kfet/account_group_form.html" + form_class = GroupForm success_message = "Groupe modifié : %(name)s" + success_url = reverse_lazy("kfet.account.group") diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index 326c2796..5b23bb1e 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -1,90 +1,134 @@ -from django.contrib.auth import get_user_model +from django.conf import settings from django.db.models import Q -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.http import Http404 +from django.shortcuts import render -from shared import autocomplete +from gestioncof.models import User +from kfet.decorators import teamkfet_required +from kfet.models import Account -User = get_user_model() +if getattr(settings, "LDAP_SERVER_URL", None): + from ldap3 import Connection +else: + # shared.tests.testcases.TestCaseMixin.mockLDAP needs + # Connection to be defined in order to mock it. + Connection = None -class KfetAccountSearch(autocomplete.ModelSearch): - model = User - search_fields = [ - "username", - "first_name", - "last_name", - "profile__account_kfet__trigramme", - ] - verbose_name = _("Comptes existants") - - def get_queryset_filter(self, *args, **kwargs): - qset_filter = super().get_queryset_filter(*args, **kwargs) - qset_filter &= Q(profile__account_kfet__isnull=False) - return qset_filter - - def result_verbose_name(self, user): - return "{} ({})".format(user, user.profile.account_kfet.trigramme) - - def result_uuid(self, user): - return user.username +class Clipper(object): + def __init__(self, clipper, fullname): + if fullname is None: + fullname = "" + assert isinstance(clipper, str) + assert isinstance(fullname, str) + self.clipper = clipper + self.fullname = fullname -class COFMemberSearch(autocomplete.ModelSearch): - model = User - search_fields = ["username", "first_name", "last_name"] - verbose_name = _("Membres du COF") +@teamkfet_required +def account_create(request): + if "q" not in request.GET: + raise Http404 + q = request.GET.get("q") - def get_queryset_filter(self, *args, **kwargs): - qset_filter = super().get_queryset_filter(*args, **kwargs) - qset_filter &= Q(profile__account_kfet__isnull=True) & Q(profile__is_cof=True) - return qset_filter + if len(q) == 0: + return render(request, "kfet/account_create_autocomplete.html") - def result_uuid(self, user): - return user.username + data = {"q": q} - def result_link(self, user): - return reverse("kfet.account.create.fromuser", args=(user.username,)) + queries = {} + search_words = q.split() + # Fetching data from User, CofProfile and Account tables + queries["kfet"] = Account.objects + queries["users_cof"] = User.objects.filter(profile__is_cof=True) + queries["users_notcof"] = User.objects.filter(profile__is_cof=False) -class OthersSearch(autocomplete.ModelSearch): - model = User - search_fields = ["username", "first_name", "last_name"] - verbose_name = _("Non-membres du COF") - - def get_queryset_filter(self, *args, **kwargs): - qset_filter = super().get_queryset_filter(*args, **kwargs) - qset_filter &= Q(profile__account_kfet__isnull=True) & Q(profile__is_cof=False) - return qset_filter - - def result_uuid(self, user): - return user.username - - def result_link(self, user): - return reverse("kfet.account.create.fromuser", args=(user.username,)) - - -class KfetLDAPSearch(autocomplete.LDAPSearch): - def result_link(self, clipper): - return reverse( - "kfet.account.create.fromclipper", args=(clipper.clipper, clipper.fullname) + for word in search_words: + queries["kfet"] = queries["kfet"].filter( + Q(cofprofile__user__username__icontains=word) + | Q(cofprofile__user__first_name__icontains=word) + | Q(cofprofile__user__last_name__icontains=word) + ) + queries["users_cof"] = queries["users_cof"].filter( + Q(username__icontains=word) + | Q(first_name__icontains=word) + | Q(last_name__icontains=word) + ) + queries["users_notcof"] = queries["users_notcof"].filter( + Q(username__icontains=word) + | Q(first_name__icontains=word) + | Q(last_name__icontains=word) ) - -class KfetAutocomplete(autocomplete.Compose): - search_units = [ - ("kfet", KfetAccountSearch()), - ("users_cof", COFMemberSearch()), - ("users_notcof", OthersSearch()), - ("clippers", KfetLDAPSearch()), + # Clearing redundancies + queries["kfet"] = queries["kfet"].distinct() + usernames = set( + queries["kfet"].values_list("cofprofile__user__username", flat=True) + ) + queries["kfet"] = [ + (account, account.cofprofile.user) for account in queries["kfet"] ] + queries["users_cof"] = ( + queries["users_cof"].exclude(username__in=usernames).distinct() + ) + queries["users_notcof"] = ( + queries["users_notcof"].exclude(username__in=usernames).distinct() + ) + usernames |= set(queries["users_cof"].values_list("username", flat=True)) + usernames |= set(queries["users_notcof"].values_list("username", flat=True)) -kfet_autocomplete = KfetAutocomplete() + # Fetching data from the SPI + if getattr(settings, "LDAP_SERVER_URL", None): + # Fetching + ldap_query = "(&{:s})".format( + "".join( + "(|(cn=*{bit:s}*)(uid=*{bit:s}*))".format(bit=word) + for word in search_words + if word.isalnum() + ) + ) + if ldap_query != "(&)": + # If none of the bits were legal, we do not perform the query + entries = None + with Connection(settings.LDAP_SERVER_URL) as conn: + conn.search("dc=spi,dc=ens,dc=fr", ldap_query, attributes=["uid", "cn"]) + entries = conn.entries + # Clearing redundancies + queries["clippers"] = [ + Clipper(entry.uid.value, entry.cn.value) + for entry in entries + if entry.uid.value and entry.uid.value not in usernames + ] + + # Resulting data + data.update(queries) + data["options"] = sum([len(query) for query in queries]) + + return render(request, "kfet/account_create_autocomplete.html", data) -class KfetAccountOnlyAutocomplete(autocomplete.Compose): - search_units = [("kfet", KfetAccountSearch())] +@teamkfet_required +def account_search(request): + if "q" not in request.GET: + raise Http404 + q = request.GET.get("q") + words = q.split() + data = {"q": q} -kfet_account_only_autocomplete = KfetAccountOnlyAutocomplete() + for word in words: + query = Account.objects.filter( + Q(cofprofile__user__username__icontains=word) + | Q(cofprofile__user__first_name__icontains=word) + | Q(cofprofile__user__last_name__icontains=word) + ).distinct() + + query = [ + (account.trigramme, account.cofprofile.user.get_full_name()) + for account in query + ] + + data["accounts"] = query + return render(request, "kfet/account_search_autocomplete.html", data) diff --git a/kfet/cms/__init__.py b/kfet/cms/__init__.py index e69de29b..f6aabddc 100644 --- a/kfet/cms/__init__.py +++ b/kfet/cms/__init__.py @@ -0,0 +1 @@ +default_app_config = "kfet.cms.apps.KFetCMSAppConfig" diff --git a/kfet/cms/fixtures/kfet_wagtail_02_19.json b/kfet/cms/fixtures/kfet_wagtail_02_19.json index 5364c4a5..72b2a06d 100644 --- a/kfet/cms/fixtures/kfet_wagtail_02_19.json +++ b/kfet/cms/fixtures/kfet_wagtail_02_19.json @@ -356,7 +356,7 @@ "model": "wagtailcore.page", "pk": 10, "fields": { - "path": "0001000100010007", + "path": "0001000100010006", "depth": 4, "numchild": 0, "title": "Mentions l\u00e9gales", @@ -395,92 +395,6 @@ "live_revision": null } }, -{ - "model": "wagtailcore.page", - "pk": 12, - "fields": { - "path": "0001000100010006", - "depth": 4, - "numchild": 0, - "title": "Le calendrier", - "title_fr": "Le calendrier", - "title_en": "Le calendrier", - "draft_title": "Le calendrier", - "slug": "le-calendrier", - "slug_fr": "le-calendrier", - "slug_en": "le-calendrier", - "content_type": [ - "kfetcms", - "kfetpage" - ], - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/k-fet/le-calendrier/", - "url_path_fr": "/global/k-fet/le-calendrier/", - "url_path_en": null, - "owner": [ - "kfet_genericteam" - ], - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": true, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2019-08-01T09:00:04.113Z", - "last_published_at": null, - "latest_revision_created_at": null, - "live_revision": null - } -}, - { - "model": "wagtailcore.page", - "pk": 146, - "fields": { - "path": "0001000100010008", - "depth": 4, - "numchild": 0, - "title": "K-F\u00eat Overheard", - "title_fr": "K-F\u00eat Overheard", - "title_en": null, - "draft_title": "K-F\u00eat Overheard", - "slug": "k-fet-overheard", - "slug_fr": "k-fet-overheard", - "slug_en": null, - "content_type": [ - "kfetcms", - "kfetpage" - ], - "live": true, - "has_unpublished_changes": false, - "url_path": "/global/k-fet/k-fet-overheard/", - "url_path_fr": "/global/k-fet/k-fet-overheard/", - "url_path_en": null, - "owner": [ - "kfet_genericteam" - ], - "seo_title": "", - "seo_title_fr": null, - "seo_title_en": null, - "show_in_menus": true, - "search_description": "", - "search_description_fr": "", - "search_description_en": "", - "go_live_at": null, - "expire_at": null, - "expired": false, - "locked": false, - "first_published_at": "2022-08-21T16:13:39.664Z", - "last_published_at": null, - "latest_revision_created_at": null, - "live_revision": null - } -}, { "model": "wagtailcore.collection", "pk": 1, @@ -801,521 +715,11 @@ "photo": 32 } }, -{ - "model": "kfetcms.memberteam", - "pk": 31, - "fields": { - "first_name": "Nicolas", - "last_name": "Dubouis", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 32, - "fields": { - "first_name": "Krojetan", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 33, - "fields": { - "first_name": "Cl\u00e9mence", - "last_name": "Topart", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 34, - "fields": { - "first_name": "Camille", - "last_name": "Gobert", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 35, - "fields": { - "first_name": "Paul-Nicolas", - "last_name": "Madelaine", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 36, - "fields": { - "first_name": "Michele", - "last_name": "Orr\u00f9", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 37, - "fields": { - "first_name": "Auriane", - "last_name": "Meilland", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 39, - "fields": { - "first_name": "Robin", - "last_name": "Durand", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 40, - "fields": { - "first_name": "Victor", - "last_name": "Barizien", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 41, - "fields": { - "first_name": "Chems", - "last_name": "Amari", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 42, - "fields": { - "first_name": "S\u00e9l\u00e8ne", - "last_name": "Forget", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 43, - "fields": { - "first_name": "Th\u00e9ophile", - "last_name": "Wallez", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 44, - "fields": { - "first_name": "Ludovic", - "last_name": "Stephan", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 45, - "fields": { - "first_name": "Maxime", - "last_name": "Ligonni\u00e8re", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 46, - "fields": { - "first_name": "Sarah", - "last_name": "Duclos--Ivetich", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 47, - "fields": { - "first_name": "Pablo", - "last_name": "Bustillo-Vazquez", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 48, - "fields": { - "first_name": "Matthias", - "last_name": "Trannoy", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 49, - "fields": { - "first_name": "Ivan", - "last_name": "Detout", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 51, - "fields": { - "first_name": "Dylan", - "last_name": "Jaoui", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 52, - "fields": { - "first_name": "Benja", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 53, - "fields": { - "first_name": "RNB", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 54, - "fields": { - "first_name": "Bryan", - "last_name": "Raimbault", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 55, - "fields": { - "first_name": "Etienne", - "last_name": "Lempereur", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 56, - "fields": { - "first_name": "Pierre", - "last_name": "Moller", - "nick_name": "Mollet", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 57, - "fields": { - "first_name": "Antoine", - "last_name": "Demont", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 58, - "fields": { - "first_name": "Antoine", - "last_name": "Le Calvez", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 59, - "fields": { - "first_name": "Laure", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 60, - "fields": { - "first_name": "Antonin", - "last_name": "Reitz", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 61, - "fields": { - "first_name": "Luj", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 62, - "fields": { - "first_name": "Paul", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 63, - "fields": { - "first_name": "R\u00e9my", - "last_name": "Le Goff", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 64, - "fields": { - "first_name": "Louise", - "last_name": "Harari", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 65, - "fields": { - "first_name": "Matchilde", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 66, - "fields": { - "first_name": "Eno", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 67, - "fields": { - "first_name": "Maud", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 68, - "fields": { - "first_name": "Rachel", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 69, - "fields": { - "first_name": "Martin", - "last_name": "Flament", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 70, - "fields": { - "first_name": "Paco", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 71, - "fields": { - "first_name": "Naama", - "last_name": "Drahy", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 72, - "fields": { - "first_name": "Lyssu", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 73, - "fields": { - "first_name": "Mathilde", - "last_name": "Poq", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 74, - "fields": { - "first_name": "Alexandre", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 75, - "fields": { - "first_name": "Chiara", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 76, - "fields": { - "first_name": "Valentin", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 77, - "fields": { - "first_name": "Marine", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 78, - "fields": { - "first_name": "Dylan", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 79, - "fields": { - "first_name": "Emma", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 80, - "fields": { - "first_name": "Daniel", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 81, - "fields": { - "first_name": "Thomas", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 82, - "fields": { - "first_name": "Clara", - "last_name": "", - "nick_name": "", - "photo": null - } -}, -{ - "model": "kfetcms.memberteam", - "pk": 83, - "fields": { - "first_name": "No\u00e9", - "last_name": "", - "nick_name": "", - "photo": null - } -}, { "model": "kfetcms.kfetpage", "pk": 3, "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

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

    Ouverte le plus souvent possible, la K-F\\u00eat (et pas kfet, KF\\u00eat, K-Fet ou kf\\u00eat) est le foyer associatif du COF. Elle est situ\\u00e9e dans les locaux de l'\\u00c9cole, au pied de l'escalier C (plan).

    En journ\\u00e9e, tu peux y passer pour prendre un caf\\u00e9, un th\\u00e9, un jus de fruit ou encore de quoi grignoter (le choix est vaste, promis !), tout en faisant une sieste, en travaillant avec un s\\u00e9rieux l\\u00e9gendaire, en jouant au flipper, ou au baby-foot... :) N\\u2019h\\u00e9site pas simplement \\u00e0 venir t\\u2019y d\\u00e9tendre, discuter, lire un livre ou m\\u00eame, pour les irr\\u00e9ductibles, jouer \\u00e0 la coinche. Que ce soit pour travailler, regarder le dernier \\u00e9pisode de Drag Race France ou t\\u00e9l\\u00e9charger Lego Marvel Super Heroes 2, toutes les options te sont ouvertes, et tu peux d\\u2019ailleurs profiter de l\\u2019excellente connexion Wi-Fi qui est install\\u00e9e en K-F\\u00eat !

    Mais c\\u2019est apr\\u00e8s 18h que l\\u2019ambiance se transforme, imperceptiblement. Une effervescence commence \\u00e0 animer certain\\u00b7e\\u00b7s quem\\u00b7quam\\u00b7quid\\u00b7dams (avis aux non-latinistes :\\u202fen latin, le genre est port\\u00e9 par la premi\\u00e8re partie du mot, expliquant cette inclusivation) : iels sortent des canap\\u00e9s, allument les tireuses, transportent des caisses charg\\u00e9es de d\\u00e9licieux breuvages, le Boum installe ses platines et PLS sa r\\u00e9gie. \\u00c0 22h, c\\u2019est le coup de feu : la soir\\u00e9e commence ! Elle sera, intrins\\u00e8quement inoubliable, et peut-\\u00eatre m\\u00eame immortalis\\u00e9e si le club Reflex (le club photo) est dans le coin ! Enfin, \\u00e0 3h, l\\u2019\\u00e9quipe K-F\\u00eat sort balais, serpill\\u00e8res et l\\u2019aspirateur \\u00e0 eau, puis toutes les \\u00e2mes restantes \\u0153uvrent \\u00e0 nettoyer la K-F\\u00eat, pr\\u00eate pour le lendemain.

    \", \"id\": \"3a051266-82d0-4b60-85fd-f7f8f5611b1d\"}, {\"type\": \"rich\", \"value\": \"

    Mais on n'y fait que boire, manger et chiller ?

    Que nenni, \\u00f4 jeune et innocent\\u00b7e conscrit\\u00b7e ! Tout au long de l\\u2019ann\\u00e9e, l\\u2019\\u00e9quipe K-F\\u00eat tente de proposer propose des activit\\u00e9s et \\u00e9v\\u00e9nements pour rompre avec la routine. Entre karaok\\u00e9s, fresques collaboratives et d\\u00e9gustations, c\\u2019est pendant ces \\u00e9v\\u00e9nements que la K-F\\u00eat d\\u00e9fend au mieux son qualificatif de foyer associatif. Nous essayons d\\u2019en proposer pour tous les go\\u00fbts, mais n\\u2019h\\u00e9site pas \\u00e0 sugg\\u00e9rer, voire organiser un \\u00e9v\\u00e9nement qui t\\u2019enthousiasme !

    \", \"id\": \"324b16f7-7b65-4e7f-9e00-25fb773446cc\"}, {\"type\": \"rich\", \"value\": \"

    Mais qui est donc cette \\u00e9quipe K-F\\u00eat dont on me parle tant ?

    Le groupe d'\\u00e9tudiant\\u00b7e\\u00b7s motiv\\u00e9\\u22c5e\\u00b7s qui s'occupent de la K-F\\u00eat s'appelle, en toute logique, l'\\u00e9quipe K-F\\u00eat. Elle est men\\u00e9e par trois chef\\u00b7fe\\u00b7s charismatiques et bien-aim\\u00e9\\u22c5e\\u00b7s, second\\u00e9\\u00b7e\\u00b7s par les wo\\u00b7men et accompagn\\u00e9\\u22c5e\\u00b7s des K-F\\u00eat boy\\u00b7girl\\u00b7s. Les candidatures pour rejoindre l\\u2019\\u00e9quipe sont ouvertes \\u00e0 toustes, et les chef\\u00b7fe\\u00b7s sont \\u00e9lu\\u00b7e\\u00b7s tous les ans par les ulmites. N'h\\u00e9site pas \\u00e0 d\\u00e9poser ton nom dans l'urne en bois pr\\u00e8s du bar pour rejoindre cette merveilleuse \\u00e9quipe, pleine de talent, de pers\\u00e9v\\u00e9rance et de charme.

    Enfin, pour assurer une pr\\u00e9sence chaleureuse et rassurante \\u00e0 tout instant, tu peux compter sur les diff\\u00e9rentes mascottes K-F\\u00eat, K-Dy, Jacques-a-dit Caddie et Gaston. Bien qu\\u2019iel nous ait quitt\\u00e9\\u00b7e\\u00b7s, l\\u2019\\u00e2me de Sh\\u00e9h\\u00e9razade continue \\u00e0 veiller sur nous.

    \", \"id\": \"b341ba84-0622-4562-82d6-af22fd336316\"}, {\"type\": \"rich\", \"value\": \"

    Quand puis-je venir en K-F\\u00eat ?

    Le local de la K-F\\u00eat n'est ouvert que si un\\u22c5e K-F\\u00eat wo\\u22c5man est pr\\u00e9sent\\u22c5e, \\u00e0 savoir la plupart du temps entre la fin de matin\\u00e9e et jusqu\\u2019en soir\\u00e9e, mais pas apr\\u00e8s 1h du matin (sauf soir\\u00e9e organis\\u00e9e). Le service d'alcool d\\u00e9bute quant \\u00e0 lui \\u00e0 18h. Tu peux donc tenter ta chance un peu n\\u2019importe quand pour venir profiter des merveilleux canap\\u00e9s du local. Si toutefois tu n\\u2019as pas envie de descendre de ta thurne pour rien, un indicateur, en rouge, orange ou vert en haut \\u00e0 gauche de cette page, t\\u2019indique si la K-F\\u00eat est actuellement ouverte !

    \", \"id\": \"db0f9625-abfa-4c6f-9e9d-c3d620d40d87\"}, {\"type\": \"rich\", \"value\": \"

    Et je peux y faire ce que je veux ?

    En K-F\\u00eat, comme partout, tu peux faire ce que tu veux tant que tout le monde est consentant\\u00b7e et que tu respectes la Charte K-F\\u00eat, et tu y es \\u00e9videmment la\\u00b7e bienvenu\\u00b7e. Nous n\\u2019allons pas t\\u2019emp\\u00eacher de faire la f\\u00eate ou de prendre ton 26e pampryl tomate de la journ\\u00e9e, mais nous attendons de toi que tu respectes le lieu et l\\u2019\\u00e9quipe. Le lieu, en le laissant dans l\\u2019\\u00e9tat dans lequel tu aurais aim\\u00e9 le trouver. L\\u2019\\u00e9quipe, en \\u00e9tant responsable pour rester g\\u00e9rable en fin de soir\\u00e9e. Souviens toi que l\\u2019\\u00e9quipe est b\\u00e9n\\u00e9vole et donne de son temps pour que ton passage en K-F\\u00eat soit agr\\u00e9able, en essayant de maintenir le lieu propre et en s\\u2019occupant de toi au besoin. En cas d\\u2019interrogation sur ce qu\\u2019il est possible de faire ou non en K-F\\u00eat, n\\u2019h\\u00e9site pas \\u00e0 jeter un \\u0153il sur le mode d'emploi de la K-F\\u00eat, ou \\u00e0 demander \\u00e0 un\\u00b7e membre de l\\u2019\\u00e9quipe !

    \", \"id\": \"b9c635bd-2f5d-4954-a2f2-59e9011ddece\"}, {\"type\": \"rich\", \"value\": \"

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

    Bien s\\u00fbr ! Pour cela commence par lire ce petit guide, histoire de savoir dans quoi tu t'engages, v\\u00e9rifie le calendrier (URL) puis contacte tes wo\\u00b7men ador\\u00e9\\u22c5e\\u00b7s pour v\\u00e9rifier que la date de ta soir\\u00e9e ne soit pas d\\u00e9j\\u00e0 prise par un autre \\u00e9v\\u00e9nement et pour obtenir leur accord.

    \", \"id\": \"6a2783ac-daf8-41e0-99d9-628549d2fe62\"}, {\"type\": \"rich\", \"value\": \"

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

    Commence d\\u00e9j\\u00e0 par jeter un oeil au mode d'emploi de la K-F\\u00eat. Si la r\\u00e9ponse \\u00e0 tes interrogations ne s'y trouve pas, rien n'est perdu ! Tu peux en effet contacter l\\u2019\\u00e9quipe \\u00e0 l\\u2019adresse suivante : k-fet (at) ens (point) psl (point) eu, ou les wo\\u00b7men \\u00e0 celle-ci, mod\\u00e9r\\u00e9e par les chef\\u00b7fe\\u00b7s : chefs-k-fet (at) ens (point) psl (point) eu. Mais le plus simple reste peut-\\u00eatre de passer en K-F\\u00eat, il y aura forc\\u00e9ment un\\u00b7e wo\\u22c5man qui saura r\\u00e9pondre \\u00e0 ta question, ou au moins t\\u2019indiquer vers qui te tourner.

    \", \"id\": \"3e78b093-fb16-42c9-b0ac-22122c8b2f4a\"}]", + "content": "[{\"id\": \"2e0c0ac1-68d7-4c86-809b-ba744173091b\", \"type\": \"rich\", \"value\": \"

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

    \\n\\n

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

    \"}, {\"id\": \"f583a378-54ee-44b2-ae28-01abf9d1c90c\", \"type\": \"rich\", \"value\": \"

    Mais on n'y fait que boire et manger ?

    \\n\\n

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

    \"}, {\"id\": \"b10fc172-f46c-4244-9f2b-648252a55fae\", \"type\": \"rich\", \"value\": \"

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

    \\n\\n

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

    \"}, {\"id\": \"f529e95c-3d36-4cef-b973-9a8768a8f36c\", \"type\": \"rich\", \"value\": \"

    Et je peux y faire ce que je veux ?

    \\n\\n

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

    \"}, {\"id\": \"d6b5d2cd-20dc-414c-8485-33aa263de948\", \"type\": \"rich\", \"value\": \"

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

    \\n\\n

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

    \"}, {\"id\": \"3fe173eb-fc5f-41ca-8cc0-3dc0263e28a1\", \"type\": \"rich\", \"value\": \"

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

    \\n\\n

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

    \"}]", "no_header": false, "layout": "kfet/base_col_mult.html", "main_size": "", @@ -1326,7 +730,7 @@ "model": "kfetcms.kfetpage", "pk": 4, "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

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

    \", \"id\": \"04b47a9c-ca7f-47d5-a406-21035aa4b01e\"}, {\"type\": \"rich\", \"value\": \"

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

    La K-F\\u00eat n'a rien de magique, il n'y a pas de petit\\u00b7e\\u00b7s elfes qui font le m\\u00e9nage, pas plus qu\\u2019une for\\u00eat enchant\\u00e9e dans laquelle on part \\u00e0 la cueillette aux kinder bueno. La K-F\\u00eat c'est avant tout une \\u00e9quipe qui sacrifie une partie (parfois (trop) importante) de son temps libre pour que tout se passe pour le mieux.

    \", \"id\": \"3cfccab0-738f-4759-8c37-5ab8c1dfbf3f\"}, {\"type\": \"rich\", \"value\": \"

    Que puis-je faire pour vous aider un peu ?

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

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

    Si tu es motiv\\u00e9\\u00b7e pour en faire plus, parles-en \\u00e0 l'\\u00e9quipe ! Il y a toujours quelque chose \\u00e0 faire pour s'occuper de l'endroit !

    \", \"id\": \"13a83f1d-0231-4ca9-97e6-49ed023fd95b\"}, {\"type\": \"rich\", \"value\": \"

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

    En bon \\u00e9tat souverain et ind\\u00e9pendant, la K-F\\u00eat a sa propre monnaie : l'unit\\u00e9 K-F\\u00eat (UKF). Elle vaut 10 centimes d'euro. La K-F\\u00eat ne battant pas monnaie, les UKF que tu poss\\u00e8des sont not\\u00e9es sur ton compte, identifi\\u00e9 par un trigramme (une suite de trois caract\\u00e8res) et que tu peux recharger en liquide ou par ch\\u00e8que. Note que si tu y tiens vraiment, tu peux payer en liquide, mais poss\\u00e9der un compte est bien plus pratique. Enfin, quand tu quittes l'ENS, pense \\u00e0 demander la suppression du trigramme pour le remettre en circulation et permettre sa r\\u00e9attribution \\u00e0 un\\u00b7e nouvelleau, car il reste sans \\u00e7a inchang\\u00e9 \\u00e0 jamais.

    \", \"id\": \"92074454-dee7-4334-b2b9-52b2c77be018\"}, {\"type\": \"rich\", \"value\": \"

    Comment commander \\u00e0 boire ou \\u00e0 manger ?

    Pour commander \\u00e0 boire ou \\u00e0 manger, il suffit de demander \\u00e0 un membre de l'\\u00e9quipe K-F\\u00eat. L'\\u00e9quipe est constitu\\u00e9e de volontaires b\\u00e9n\\u00e9voles qui prennent de leur temps pour servir, donc un s'il vous pla\\u00eet et un merci est toujours bienvenu. Si personne n\\u2019est derri\\u00e8re le bar, n\\u2019h\\u00e9site pas \\u00e0 demander aux personnes qui sont sur les canap\\u00e9s alentour, ce sont probablement des K-F\\u00eateux\\u00b7ses qui chillent plus ou moins \\u00e0 c\\u00f4t\\u00e9 du bar. Dans tous les cas, et m\\u00eame si tu ne trouves personne, ne passe pas derri\\u00e8re le bar s\\u2019il te pla\\u00eet !

    \", \"id\": \"22000a07-6353-4fb6-b08a-5623d207d94f\"}, {\"type\": \"rich\", \"value\": \"

    Puis-je fumer en K-F\\u00eat ?

    Comme dans tout endroit public clos, il est interdit de fumer ou de vapoter en K-F\\u00eat. Nous te serons donc reconnaissant d\\u2019aller fumer dehors, il y a m\\u00eame des cendriers juste pour toi ! Tu remarqueras cependant que les chambres de l'internat se trouvent juste au dessus de toi, t\\u00e2che donc de ne pas faire trop de bruit.

    \", \"id\": \"b34274c9-859a-4b79-95b3-d9179fb73b13\"}, {\"type\": \"rich\", \"value\": \"

    Et amener ma propre bouteille ?

    D\\u00e9j\\u00e0, c'est apporter. Ensuite la K-F\\u00eat est un lieu de convivialit\\u00e9 o\\u00f9 les produits te sont majoritairement vendus \\u00e0 prix co\\u00fbtant. Franchement, ce ne serait pas fair-play de te la jouer solo, et il t\\u2019est de toutes fa\\u00e7ons demand\\u00e9 de ne pas le faire dans la charte K-F\\u00eat que tu as accept\\u00e9 en entrant dans ce lieu, jeune mortel\\u00b7le !

    \", \"id\": \"33b0e257-4cab-4442-9713-1a0a08685003\"}, {\"type\": \"rich\", \"value\": \"

    Je peux passer ma musique ?

    La musique qui passe en K-F\\u00eat est diffus\\u00e9e gr\\u00e2ce au compte Spotify de la K-F\\u00eat, depuis l\\u2019ordinateur pr\\u00e9sent en arri\\u00e8re, et ce sont les K-F\\u00eateux\\u00b7se\\u00b7s qui choisissent la musique qu\\u2019iels passent. Cependant, m\\u00eame si rien ne d\\u00e9tr\\u00f4nera ABBA, nous restons ouvert\\u00b7e\\u00b7s et curieux\\u00b7ses de ce que tu pourras nous faire d\\u00e9couvrir, donc n\\u2019h\\u00e9site pas \\u00e0 nous demander de passer les titres qui te plaisent !

    Le plus simple pour toi (et pour nous) reste s\\u00fbrement de pr\\u00e9voir des playlists sur Spotify \\u00e0 l'avance et de nous les proposer. Toutefois, l\\u2019\\u00e9quipe se r\\u00e9serve le droit de ne pas acc\\u00e9der \\u00e0 ta demande, si un trop grand nombre de personnes nous demande de passer de la musique.

    \", \"id\": \"93f44d53-eb10-40ed-ab77-5b74e8a160ab\"}, {\"type\": \"rich\", \"value\": \"

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

    Tout adh\\u00e9rent\\u00b7e du COF peut organiser une soir\\u00e9e en K-F\\u00eat, \\u00e0 la condition qu'elle soit publique. Apr\\u00e8s avoir demand\\u00e9 l\\u2019accord de l\\u2019\\u00e9quipe K-F\\u00eat, pour s\\u2019assurer qu\\u2019il y ait un\\u00b7e wo\\u00b7men disponible pour ouvrir, il te faut l\\u2019aval du bur\\u00f4 du COF, qui peut souhaiter que des COFeux\\u2011ses soient pr\\u00e9sent\\u2011e\\u00b7s, et qui doivent s\\u2019assurer que l\\u2019administration accepte. Tu peux ensuite \\u00e9ventuellement solliciter le Boum pour qu\\u2019iels s\\u2019occupent de la musique et PLS pour qu\\u2019iels t\\u2019allument les lumi\\u00e8res. Tout t\\u2019est r\\u00e9sum\\u00e9 ici, et n\\u2019oublie pas de faire parler de ton \\u00e9v\\u00e9nement, si tu l\\u2019as d\\u00e9clar\\u00e9 \\u00e0 l\\u2019administration, en collant des affiches, en envoyant tes invitations par pigeon voyageur ou en m\\u00e9diatisant ta soir\\u00e9e sur les r\\u00e9seaux sociaux.

    \", \"id\": \"b701bf44-8010-4a97-8927-0168feaf4ce4\"}, {\"type\": \"rich\", \"value\": \"

    Quel comportement je dois adopter en K-F\\u00eat ?

    Tu dois, en tout temps et comme partout, adopter un comportement respectueux de ton entourage. Pour \\u00eatre s\\u00fbr\\u00b7e de ce que tu peux faire ou non, consulte la Charte K-F\\u00eat ou demande \\u00e0 un\\u00b7e K-F\\u00eateux\\u00b7se.

    \", \"id\": \"3f93cd9d-b16b-4f89-ab02-48535e087f0a\"}, {\"type\": \"rich\", \"value\": \"

    D'autres remarques ?

    Une myriade, en voici quelques unes :

    • Ce n'est pas caf\\u00e8t, ni kfet, ni caf\\u00e9t\\u00e9ria, c'est K-F\\u00eat, avec les majuscules, le tiret et l\\u2019accent.
    • On dit "en K-F\\u00eat".
    • Ne passe pas derri\\u00e8re le bar, s'il te pla\\u00eet. S'il n'y a personne pour servir c'est que les K-F\\u00eateux\\u00b7ses sont soit occup\\u00e9\\u00b7e\\u00b7s quelque chose d'important en arri\\u00e8re-K-F\\u00eat, soit pos\\u00e9\\u00b7e\\u00b7s dans le canap\\u00e9 \\u00e0 c\\u00f4t\\u00e9 du bar, soit en train de jouer au baby ou \\u00e0 l'Amazon. Demande-leur, ou prends ton mal en patience, et sache qu'on essaie d'\\u00eatre le plus disponibles possible.
    • Les K-F\\u00eateux\\u00b7ses passent beaucoup de temps \\u00e0 ranger l'endroit. Donc jette tes d\\u00e9chets et pr\\u00e9viens nous si jamais tu casses une bouteille ou la renverses, merci d\\u2019avance !

    F\\u00e9licitations d'avoir lu jusque l\\u00e0\\u202f!

    \", \"id\": \"013cb5c3-f241-4da6-9318-ebb2767e2aea\"}]", + "content": "[{\"id\": \"2d5bf7d6-8943-4de1-b4bf-2471b0b83fe5\", \"type\": \"rich\", \"value\": \"

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

    \"}, {\"id\": \"cc88d98a-455c-4135-9cf1-cb95204f7bc9\", \"type\": \"rich\", \"value\": \"

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

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

    \"}, {\"id\": \"0d801331-ea10-4af4-a37b-73dc4e0ccf0d\", \"type\": \"rich\", \"value\": \"

    Que puis-je faire pour vous aider un peu ?

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

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

    \"}, {\"id\": \"5bc33436-d3bc-4e86-85aa-5bbaf8605b04\", \"type\": \"rich\", \"value\": \"

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

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

    \"}, {\"id\": \"36c30321-faf8-4e03-9d76-dee83d2b71b5\", \"type\": \"rich\", \"value\": \"

    Comment commander \\u00e0 boire ou \\u00e0 manger ?

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

    \"}, {\"id\": \"0897b4c2-aacf-4590-b954-7342519eb860\", \"type\": \"rich\", \"value\": \"

    Puis-je fumer en K-F\\u00eat ?

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

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

    \"}, {\"id\": \"13400963-d70a-4cca-85fb-9d4fb3488775\", \"type\": \"rich\", \"value\": \"

    Et amener ma propre bouteille ?

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

    \"}, {\"id\": \"f12919e2-1e09-4976-a077-74d17248621a\", \"type\": \"rich\", \"value\": \"

    Je peux passer ma musique ?

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

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

    \"}, {\"id\": \"18786055-718b-4ca0-8e7c-350d873c3575\", \"type\": \"rich\", \"value\": \"

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

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

    \"}, {\"id\": \"e2b7d070-edfd-484c-8524-d3af83562a9b\", \"type\": \"rich\", \"value\": \"

    D'autres remarques ?

    Des tonnes, en voici quelques unes :

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

    Les chef\\u22c5fe\\u00b7s

    Les chef\\u00b7fe\\u00b7s K-F\\u00eat, \\u00e9lu\\u00b7e\\u00b7s en m\\u00eame temps que les membres du COF, en tant que membres \\u00e0 part enti\\u00e8re du bur\\u00f4, sont les \\u00eatres de lumi\\u00e8re qui g\\u00e8rent le fonctionnement de la K-F\\u00eat, sur diff\\u00e9rents volets tels que l\\u2019organisation de la vie courante avec l\\u2019\\u00e9quipe, la gestion de la tr\\u00e9sorerie, ou les relations avec l\\u2019administration. Travaillant de mani\\u00e8re rapproch\\u00e9e avec les wo\\u00b7men, les chef\\u00b7fe\\u00b7s assurent la r\\u00e9partition des t\\u00e2ches au sein de l\\u2019\\u00e9quipe.

    \", \"id\": \"496d46a1-e694-4152-8079-35ba7d177c13\"}, {\"type\": \"group_team\", \"value\": {\"show_only\": null, \"members\": [68, 70, 75]}, \"id\": \"20697be5-f2cf-4d45-a504-754921f39d0b\"}], \"id\": \"b821b3e5-5a06-4e79-8e5e-ba6fc33a0ce4\"}, {\"type\": \"group\", \"value\": [{\"type\": \"rich\", \"value\": \"

    Les K-F\\u00eat wo\\u00b7men

    Les K-F\\u00eat wo\\u22c5men poss\\u00e8dent les cl\\u00e9s de la K-F\\u00eat et peuvent donc d\\u00e9cider ou non d'ouvrir la K-F\\u00eat. Ce sont des K-F\\u00eat boy\\u00b7girls qui ont d\\u00e9cid\\u00e9 de s'investir plus, qui doivent aussi g\\u00e9rer les livraisons et les rangements, et peuvent \\u00eatre responsables du dialogue avec un des fournisseur\\u00b7euse\\u00b7s de la K-F\\u00eat. Iels sont d\\u2019une aide pr\\u00e9cieuse pour les chef\\u00b7fe\\u00b7s, car iels assurent les diff\\u00e9rentes t\\u00e2ches n\\u00e9cessaires \\u00e0 la vie de la K-F\\u00eat, en s\\u2019assurant de l\\u2019approvisionnement en Alain Millat mangue ou en s\\u2019occupant l\\u2019organisation d\\u2019une soir\\u00e9e du c\\u00f4t\\u00e9 de la K-F\\u00eat.

    \", \"id\": \"98509830-e724-49d8-bd7c-f90b256ed255\"}, {\"type\": \"group_team\", \"value\": {\"show_only\": null, \"members\": [52, 76, 77, 80, 82, 81, 71, 83]}, \"id\": \"4f2aa16b-2f4e-458c-a6cc-8613941aea59\"}], \"id\": \"d5abbe4b-0330-4615-b59c-4576250040d2\"}, {\"type\": \"rich\", \"value\": \"

    Les K-F\\u00eateux\\u00b7ses

    Les K-F\\u00eateux\\u00b7ses, ou K-F\\u00eat boy\\u00b7girls, sont les acteurs n\\u00e9cessaires \\u00e0 la vie de la K-F\\u00eat. Ayant une voix de m\\u00eame valeur que tout autre personne de l\\u2019\\u00e9quipe, et participant au rangement de la K-F\\u00eat ou au service, iels peuvent passer derri\\u00e8re le bar tout en conservant leur int\\u00e9grit\\u00e9 physique, ce qui constitue s\\u00fbrement la mani\\u00e8re la plus simple de les reconna\\u00eetre.

    \", \"id\": \"dde1ca20-7d9f-42b7-83fb-c9c12314ce81\"}, {\"type\": \"group\", \"value\": [{\"type\": \"rich\", \"value\": \"

    Les ancien\\u00b7ne\\u00b7s chef\\u00b7fe\\u00b7s K-F\\u00eat

    C'est gr\\u00e2ce aux ancien\\u00b7ne\\u00b7s chef\\u00b7fe\\u00b7s que la K-F\\u00eat tourne encore, gr\\u00e2ce \\u00e0 elleux qu'elle a bien tourn\\u00e9, et puis, de pr\\u00e8s comme de loin, iels veillent encore sur nous. Ce sont les diff\\u00e9rentes facettes de la K-F\\u00eat historique, bien que d'un certain point de vue, iels se ressemblent toustes : les chef\\u22c5fe\\u22c5s K-F\\u00eat sont une dynastie, ils n'ont pas \\u00e9t\\u00e9 chef\\u22c5fe\\u22c5s apr\\u00e8s avoir prouv\\u00e9 quoi que ce soit, iels l'ont \\u00e9t\\u00e9 parce que ce r\\u00f4le leur revenait de droit. On na\\u00eet chef\\u22c5fe K-F\\u00eat, on ne le devient pas. Et on le reste toujours, dans l'\\u00e2me.

    \", \"id\": \"43c90917-5eb1-4a8c-82a9-c6dc59f60e24\"}, {\"type\": \"group_team\", \"value\": {\"show_only\": 12, \"members\": [63, 61, 64, 65, 59, 36, 58, 54, 55, 49, 52, 1, 9, 51, 41, 40, 32, 11, 2, 13, 31, 14, 15, 16, 17, 18, 19, 20, 22, 21, 23, 24, 26, 25, 27, 28, 29, 30]}, \"id\": \"91d3b9d7-becf-4e53-8a39-521abf56f420\"}], \"id\": \"fb33a704-52a3-4bce-a258-4ba3485b65bc\"}, {\"type\": \"rich\", \"value\": \"

    Comment devient-on K-F\\u00eateu\\u22c5se\\u22c5x ?

    Grande question que tout le monde se pose un jour ou l'autre. Pour faire partie de l'\\u00e9quipe, tu dois montrer que tu est int\\u00e9ress\\u00e9\\u00b7e en mettant ton nom dans la bo\\u00eete aux lettres pr\\u00e9vue \\u00e0 cet effet. \\u00c0 la r\\u00e9union suivante (il y en a tous les mois), l'\\u00e9quipe ouvre la bo\\u00eete et attribue al\\u00e9atoirement un bin\\u00f4me de p\\u00b7m\\u00b7arrain\\u00b7e\\u00b7s \\u00e0 toutes les personnes qui se sont pr\\u00e9sent\\u00e9es (apr\\u00e8s v\\u00e9rification en discutant avec elleux qu'iels ont bien mis leur nom dans la bo\\u00eete). Pendant un mois, tu seras en p\\u00e9riode d'apprentissage, avec tes p\\u00b7m\\u00b7arrain\\u00b7ne\\u00b7s : tu n'as pas le droit de servir les gens en K-F\\u00eat, mais tu peux participer \\u00e0 la vie de tous les jours de la K-F\\u00eat (rangements, livraison, ...) et passer derri\\u00e8re le bar quand l\\u2019un\\u00b7e de tes p\\u00b7m\\u00b7arrain\\u00b7ne\\u00b7s est avec toi. \\u00c0 la r\\u00e9union d'apr\\u00e8s, et suite \\u00e0 une discussion avec toi pour voir si tu es toujours motiv\\u00e9\\u00b7e, l'\\u00e9quipe v\\u00e9rifie que tout s'est bien pass\\u00e9 avec tes p\\u00b7m\\u00b7arrain\\u00b7ne\\u00b7s, et si c'est bon, tu peux faire partie de l'\\u00e9quipe !

    Les raisons de refus sont les suivantes, et te seront communiqu\\u00e9es en cas de refus (ce qu'on n'esp\\u00e8re pas ! ) :

    • d\\u00e9motivation, si tu n'as pas particip\\u00e9 \\u00e0 la vie de la K-F\\u00eat pendant ton apprentissage
    • non correspondance aux valeurs de la K-F\\u00eat, si pendant ton apprentissage tu as fait preuve de comportements inad\\u00e9quats qui seraient incompatibles avec le service d'alcool ou la tenue de soir\\u00e9e. \\u00c0 ce sujet, tu peux lire la Charte K-F\\u00eat qui r\\u00e9sume les valeurs de la K-F\\u00eat sur ce point.

    Comment devient-on wo\\u00b7men ?

    Les wo\\u00b7men sont d\\u00e9sign\\u00e9\\u00b7e\\u00b7s parmi les K-F\\u00eat boys et girls. Les wo\\u00b7men actuel\\u00b7le\\u00b7s peuvent proposer des girls boys au poste de wo\\u00b7men, apr\\u00e8s en avoir discut\\u00e9 avec elleux. Leur nomination est ensuite vot\\u00e9e en r\\u00e9union K-F\\u00eat.

    \", \"id\": \"5649f495-ebec-456f-bac7-cd4705951e9f\"}]", + "content": "[{\"id\": \"6d2ab3e2-9896-45d6-b595-feb0567b5510\", \"type\": \"group\", \"value\": [{\"id\": \"3c3b20e4-b9f3-4dc7-a4af-5583eff06a42\", \"type\": \"rich\", \"value\": \"

    Les ancien-ne-s Chef-fe-s K-F\\u00eat

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

    \"}, {\"id\": \"bf9b257b-78a7-46f9-aa73-9753e8a2adc4\", \"type\": \"group_team\", \"value\": {\"members\": [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30], \"show_only\": 12}}]}, {\"id\": \"5a3a0c4b-a1f5-4b87-a591-c5fa12b7175a\", \"type\": \"group\", \"value\": [{\"id\": \"a28619e6-e0ff-4f4d-8e6b-03b2f2e8dbac\", \"type\": \"rich\", \"value\": \"

    Le chef

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

    \"}, {\"id\": \"732b5bea-4662-41c1-928e-85d1f59cc109\", \"type\": \"group_team\", \"value\": {\"members\": [1, 2], \"show_only\": null}}]}, {\"id\": \"8ad9c453-9c70-430c-8093-cabb4f06cd3d\", \"type\": \"group\", \"value\": [{\"id\": \"506daca4-8363-4e31-a769-1521a7d6276a\", \"type\": \"rich\", \"value\": \"

    Les K-F\\u00eat Wo-Men

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

    \"}, {\"id\": \"519cc1a0-6544-46ac-8ed7-51fbb9712df9\", \"type\": \"group_team\", \"value\": {\"members\": [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], \"show_only\": null}}]}, {\"id\": \"4d53b23e-5922-46b5-aade-4f3572cbeb6d\", \"type\": \"group\", \"value\": [{\"id\": \"573c443d-23ed-40bb-87ce-996a5c71a0e4\", \"type\": \"rich\", \"value\": \"

    Les Vieux-illes

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

    \"}]}, {\"id\": \"bbf438ac-4692-4add-9950-2d89c265775e\", \"type\": \"rich\", \"value\": \"

    Les K-F\\u00eat boys et girls

    \\n\\n

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

    \"}, {\"id\": \"6969df29-a2a1-42b3-93d9-b7bf8624d5e7\", \"type\": \"rich\", \"value\": \"

    Comment devient-on K-F\\u00eat people ?

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

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

    \"}]", "no_header": false, "layout": "kfet/base_col_mult.html", "main_size": "", @@ -1348,7 +752,7 @@ "model": "kfetcms.kfetpage", "pk": 6, "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

    Les prix donn\\u00e9s sont en UKF (1\\u20ac = 10 UKF, pour simplifier). Si tu n'as pas compris, va voir par ici.

    \", \"id\": \"8f832402-8b30-4f0e-9f35-e9bab8f96dcd\"}, {\"type\": \"carte\", \"value\": null, \"id\": \"97c37d0d-f7d9-45e7-966d-508bac292784\"}]", + "content": "[{\"id\": \"a2c6afa3-efda-4a1f-ac68-623d6448f642\", \"type\": \"rich\", \"value\": \"

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

    \"}, {\"id\": \"6e5ca2df-521b-423a-8b25-ab7367c5ad47\", \"type\": \"carte\", \"value\": null}]", "no_header": false, "layout": "kfet/base_col_mult.html", "main_size": "", @@ -1359,7 +763,7 @@ "model": "kfetcms.kfetpage", "pk": 7, "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

    La K-F\\u00eat, le COF ou les organisateurices de Kal\\u00f4s s\\u2019occupent d\\u2019organiser en g\\u00e9n\\u00e9ral une soir\\u00e9e par semaine. Toutefois, il est compl\\u00e8tement possible d\\u2019organiser ta propre soir\\u00e9e en K-F\\u00eat, que ce soit pour ton anniversaire, pour f\\u00eater la fin des partiels ou pour une soir\\u00e9e \\u00e0 th\\u00e8me (on a h\\u00e2te de d\\u00e9couvrir ces remix techno-jazz de musique hongroise, ou les looks de tes invit\\u00e9\\u00b7e\\u00b7s pour ton th\\u00e8me \\u00ab\\u202fClash of the patterns\\u202f\\u00bb).

    \", \"id\": \"ddf307ae-e519-41ea-98d3-442261ecf9ec\"}, {\"type\": \"rich\", \"value\": \"

    Les soir\\u00e9es COF

    Le COF organise r\\u00e9guli\\u00e8rement des soir\\u00e9es. De ce fait, ils r\\u00e9servent les cr\\u00e9neaux assez en avance. Elles ont souvent lieu le mardi ou le vendredi, et sont souvent \\u00e0 th\\u00e8me. Durant de 22h \\u00e0 3h, elles restent ind\\u00e9niablement inoubliables.

    \", \"id\": \"d3c67c34-a947-4e1e-805a-73995ff4274b\"}, {\"type\": \"rich\", \"value\": \"

    Les PCPPC

    Probablement les meilleures soir\\u00e9es de l'ann\\u00e9e, les PCPPC sont des soir\\u00e9es organis\\u00e9es une \\u00e0 deux fois par an par l'\\u00e9quipe K-F\\u00eat.

    Contractuellement, nous ne pouvons malheureusement pas te donner plus d'indications sur cet embl\\u00e8me de la vie normalienne.

    Parcimonieusement, tu d\\u00e9couvriras au fil de l'ann\\u00e9e des indices qui te m\\u00e8neront sur la voie sacr\\u00e9e de la PCPPC.

    Pour que tu puisses d\\u00e9couvrir notre univers, viens assister \\u00e0 l\\u2019une de ces soir\\u00e9es mystiques que nous organisons.

    Courageusement, tu attendras donc ces nuits endiabl\\u00e9es dont tout t\\u2019est encore tenu secret.

    \", \"id\": \"c5b63f02-54b0-4ef1-8bf0-2681c02fc4ce\"}, {\"type\": \"rich\", \"value\": \"

    Les Kal\\u00f4s

    Avec sa pl\\u00e9thore de d\\u00e9partements et de clubs, l\\u2019ENS pr\\u00e9sente nombre de sensibilit\\u00e9s et cultures diff\\u00e9rentes, puisant leur richesses dans les \\u00e9changes qu\\u2019ils peuvent entretenir. Dans la dynamique de perp\\u00e9tuer les relations entre ulmites de tous horizons sont r\\u00e9guli\\u00e8rement organis\\u00e9es des Kal\\u00f4s. Les Kal\\u00f4s sont des soir\\u00e9es organis\\u00e9es par un groupe de personnes partageant un d\\u00e9nominateur commun, qui peut \\u00eatre par exemple l\\u2019appartenance \\u00e0 un m\\u00eame d\\u00e9partement ou \\u00e0 un m\\u00eame club. Ces personnes pr\\u00e9parent donc un budget, organisent des ventes \\u00e0 prix libre pour r\\u00e9colter de l\\u2019argent, d\\u00e9nichent des recettes de cocktails originaux, mettent au point la d\\u00e9coration et se d\\u00e9m\\u00e8nent plusieurs semaines en avance pour faire rayonner l\\u2019esprit de leur groupe dans toute l\\u2019\\u00c9cole, au cours de leur Kal\\u00f4 o\\u00f9 seront vendus leurs cocktails, mais o\\u00f9 sont surtout souvent dress\\u00e9s des buffets !

    \", \"id\": \"a0707cc9-bbf5-4634-bad3-fc5317e53a3a\"}, {\"type\": \"rich\", \"value\": \"


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

    \", \"id\": \"7331cd8a-6e12-4002-808a-75a2e7fd3f80\"}, {\"type\": \"rich\", \"value\": \"

    Quand puis-je organiser une soir\\u00e9e ?

    Tu peux organiser une soir\\u00e9e le jour que tu souhaites, \\u00e0 condition que la date ne soit pas d\\u00e9j\\u00e0 prise par quelqu'un d'autre et qu\\u2019un\\u00b7e wo\\u00b7man soit disponible pour t\\u2019ouvrir le local ce soir l\\u00e0. Tu peux v\\u00e9rifier si la date qui t'int\\u00e9resse est d\\u00e9j\\u00e0 r\\u00e9serv\\u00e9e en regardant le calendrier K-F\\u00eat. Sache par contre que la K-F\\u00eat ne te sera pas enti\\u00e8rement d\\u00e9di\\u00e9e et que les utilisateur\\u00b7rice\\u00b7s habituel\\u00b7le\\u00b7s continueront de la fr\\u00e9quenter (et risquent fortement de squatter ta soir\\u00e9e). Par ailleurs, le COF peut estimer n\\u00e9cessaire la pr\\u00e9sence de respos sobres du bur\\u00f4, et conditionner l\\u2019\\u00e9v\\u00e9nement \\u00e0 leur disponibilit\\u00e9.

    \", \"id\": \"013f341a-c5f0-43bc-b602-de2f68411b39\"}, {\"type\": \"rich\", \"value\": \"

    Quelles d\\u00e9marches dois-je effectuer ?

    D\\u00e9j\\u00e0 pr\\u00e9venir poliment l'\\u00e9quipe K-F\\u00eat, et surtout les chef\\u00b7fe\\u00b7s pour v\\u00e9rifier que la date est encore libre, et qu'il y aura au moins quelqu'un pour t'ouvrir la K-F\\u00eat. Ensuite, si ta soir\\u00e9e n'est pas une simple bouffe qui finit avant minuit, il faut pr\\u00e9venir les vigiles via l'administration au moyen d'une d\\u00e9claration de soir\\u00e9e qui se trouve sur la section du p\\u00f4le Pr\\u00e9vention et S\\u00e9curit\\u00e9 sur l'intranet, ou via l\\u2019hyperlien suivant : D\\u00e9claration de soir\\u00e9e. Pense \\u00e0 le faire au moins deux semaines avant ta soir\\u00e9e, car elle doit \\u00eatre transmise \\u00e0 la K-F\\u00eat pour v\\u00e9rification, au COF pour signature, et \\u00e0 l\\u2019administration.

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

    \", \"id\": \"246a5d25-8bb9-4c60-92c4-812cf7ee64e3\"}, {\"type\": \"rich\", \"value\": \"

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

    Normalement, lae wo\\u00b7man responsable de ta soir\\u00e9e aura pris contact avec toi. En g\\u00e9n\\u00e9ral, il faut que tu pr\\u00e9voies de venir deux heures avant l\\u2019\\u00e9v\\u00e9nement en K-F\\u00eat accompagn\\u00e9 de quelques acolytes, pour d\\u00e9barrasser les canap\\u00e9s, les tables, et faire un brin de m\\u00e9nage. Lae wo\\u00b7man pourra t\\u2019ouvrir la K-F\\u00eat et te mettre \\u00e0 disposition tout le mat\\u00e9riel n\\u00e9cessaire pour la faire briller. Fais par contre attention aux bouteilles de bi\\u00e8re qui sont consign\\u00e9es, s'il n'y a personne pour les ranger contente-toi de les mettre sur le bar, quelqu'un s'en chargera plus tard.

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

    \", \"id\": \"99068de1-68cd-476c-b21f-70e848b042bf\"}, {\"type\": \"rich\", \"value\": \"

    Je pourrai passer ma musique ?

    Si le BOUM est pr\\u00e9sent, il faut voir avec elleux : boum (at) ens (point) psl (point) eu.

    Sinon, pr\\u00e9pare ta musique sur un lecteur mp3 ou une playlist Spotify. Lors de la soir\\u00e9e, demande \\u00e0 un\\u00b7e K-F\\u00eat wo\\u00b7man de passer ce que tu as pr\\u00e9par\\u00e9.

    \", \"id\": \"cb69b1a8-e880-4655-b94f-44b09a9c7d30\"}, {\"type\": \"rich\", \"value\": \"

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

    Tu peux apporter toute la nourriture que tu souhaites ; pr\\u00e9vois assez large, il y a beaucoup de K-F\\u00eat people \\u00e0 nourrir. Pour ce qui est de la boisson, il faut te limiter aux boissons de cat\\u00e9gorie 3, c'est \\u00e0 dire bi\\u00e8res, vins et boissons \\u00e0 base de vin, champagne et bien s\\u00fbr les boissons sans alcool. Tu trouveras les explications sur ce site (dans la rubrique licence de 3e cat\\u00e9gorie, dite \\u00ab licence restreinte \\u00bb). N'h\\u00e9site pas \\u00e0 demander \\u00e0 un K-F\\u00eat wo\\u00b7man si tu as des questions l\\u00e0-dessus.

    \", \"id\": \"e390b61d-e522-4b6e-ba56-e9a72402604a\"}, {\"type\": \"rich\", \"value\": \"

    Et pendant la soir\\u00e9e ?

    Ce soir c'est ton soir, il est donc bien s\\u00fbr \\u00e9vident que tu dois rester pr\\u00e9sent\\u00b7e et joignable du d\\u00e9but \\u00e0 la fin de la soir\\u00e9e. Ce doit aussi \\u00eatre le cas de tes "respos sobres". Vous ne serez pas trop de deux ou trois pour r\\u00e9gler les probl\\u00e8mes qui pourraient survenir, tes potes bourr\\u00e9\\u00b7e\\u00b7s, tes potes qui fument, tes potes qui font du bordel dans la cage d'escalier, etc... Tu restes responsable des \\u00e9v\\u00e9nements ayant lieu lors de ta soir\\u00e9e, donc assure toi que tes potes suivent les r\\u00e8gles d\\u2019usage en K-F\\u00eat.

    \", \"id\": \"ca59c12c-b4df-47c9-b6d3-97797ce99d00\"}, {\"type\": \"rich\", \"value\": \"

    Apr\\u00e8s c'est bon ?

    Eh non, pas encore, apr\\u00e8s ta soir\\u00e9e il te faudra encore ranger, faire le m\\u00e9nage et r\\u00e9installer les meubles que tu auras pu descendre. Tu peux solliciter lae wo\\u00b7men responsable de ta soir\\u00e9e pour qu\\u2019iel te fournisse tout le mat\\u00e9riel n\\u00e9cessaire. N\\u2019oublie pas les \\u00e9cocups, si tu en as utilis\\u00e9, et de vider les poubelles. Et \\u00e9videmment, il ne te restera plus qu\\u2019\\u00e0 commencer \\u00e0 pr\\u00e9parer la prochaine soir\\u00e9e que tu organiseras en K-F\\u00eat !

    \", \"id\": \"aa3cdbb5-0241-4e89-9746-cc857e785ed1\"}, {\"type\": \"rich\", \"value\": \"

    Une derni\\u00e8re remarque ?

    Oui, au cours de la soir\\u00e9e l'\\u00e9quipe K-F\\u00eat pourrait te demander des choses (de baisser le son, de virer telle ou telle personne...). C'est en g\\u00e9n\\u00e9ral pour essayer de pr\\u00e9server l'endroit/garder une ambiance accueillante pour tou\\u00b7te\\u00b7s, donc nous te demandons de rester \\u00e0 l\\u2019\\u00e9coute, en particulier de lae wo\\u00b7man responsable. De plus, s\\u2019iel ne t\\u2019en a pas donn\\u00e9 l\\u2019autorisation, on aimerait que tu ne passes pas derri\\u00e8re le bar.

    \", \"id\": \"f6aac7b8-9cb4-4e5c-b9e0-0796de5ad64d\"}, {\"type\": \"rich\", \"value\": \"

    Vous pourriez me faire un r\\u00e9sum\\u00e9 ?

    Organiser ta soir\\u00e9e c'est facile :

    • Envoie un mail \\u00e0 la K-F\\u00eat pour demander l'autorisation : chefs-k-fet (at) ens (point) psl (point) eu ;
    • Lorsque c'est bon, remplis la D\\u00e9claration de soir\\u00e9e, envoie la aux adresses mentionn\\u00e9es et met chefs-k-fet (at) ens (point) psl (point) eu en copie ;
    • Pour la musique, l'alcool, les ecocups, contacte la K-F\\u00eat ;
    • Le jour de la soir\\u00e9e, viens faire le m\\u00e9nage et donne une liste des ext\\u00e9rieur\\u00b7e\\u00b7s \\u00e0 la loge ;
    • Pendant la soir\\u00e9e, surveille tes invit\\u00e9s (pas trop de bruit \\u00e0 l'ext\\u00e9rieur de la K-F\\u00eat, pas de gens trop bourr\\u00e9s qui font des b\\u00eatises avec les alarmes ou les sorties de secours...) ;
    • Apr\\u00e8s la soir\\u00e9e, il ne te reste qu\\u2019\\u00e0 faire le m\\u00e9nage et remettre le lieu en \\u00e9tat.

    Voila, facile, non ?

    \", \"id\": \"a9f5bf3b-a95d-4c5a-8c5b-9d71eb314a1d\"}]", + "content": "[{\"id\": \"47826c4a-75c3-48f3-a7ba-ebf42d281e66\", \"type\": \"rich\", \"value\": \"

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

    \"}, {\"id\": \"4cb0d7d8-ebe6-403c-b578-b2c883f393dc\", \"type\": \"rich\", \"value\": \"

    Quand puis-je organiser une soir\\u00e9e ?

    \\n\\n

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

    \"}, {\"id\": \"57cfcef2-0cf7-42b7-8d4f-cb9bad018544\", \"type\": \"rich\", \"value\": \"

    Quelles d\\u00e9marches dois-je effectuer ?

    \\n

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

    \\n

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

    \"}, {\"id\": \"4afd4bbe-1dc7-4ac5-b5db-85c290f04ff8\", \"type\": \"rich\", \"value\": \"

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

    \\n

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

    \\n

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

    \"}, {\"id\": \"f38c629e-da20-40c8-aa05-2c97bcee8e98\", \"type\": \"rich\", \"value\": \"

    Je pourrai passer ma musique ?

    \\n\\n

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

    \"}, {\"id\": \"b76abb80-f090-47c0-9fe2-1df159269166\", \"type\": \"rich\", \"value\": \"

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

    \\n

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

    \"}, {\"id\": \"0a450952-7c26-4328-a8c6-79722a8434b9\", \"type\": \"rich\", \"value\": \"

    Et pendant la soir\\u00e9e ?

    \\n

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

    \"}, {\"id\": \"4e24f050-0c09-4c45-8c24-0020ce362120\", \"type\": \"rich\", \"value\": \"

    Apr\\u00e8s c'est bon ?

    \\n

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

    \"}, {\"id\": \"79083e66-9ec1-45ac-999d-d7d08ab2cbd4\", \"type\": \"rich\", \"value\": \"

    Une derni\\u00e8re remarque ?

    \\n\\n

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

    \"}, {\"id\": \"1d8593ff-3e01-451b-945b-f12d917d28ae\", \"type\": \"rich\", \"value\": \"

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

    \\n

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

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

    \\n Voila, facile, non ?\\n

    \"}]", "no_header": false, "layout": "kfet/base_col_mult.html", "main_size": "", @@ -1370,7 +774,7 @@ "model": "kfetcms.kfetpage", "pk": 8, "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

    Le flipper

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

    La borne d'arcade

    Cr\\u00e9\\u00e9e de toute pi\\u00e8ce par HackENS, elle permet de jouer aux plus grands jeux d'arcades en K-F\\u00eat. Une partie de PacMan, de Street-Fighter ou encore de Eggnog, la borne d'arcade est l\\u00e0. Et vous pouvez m\\u00eame y jouer \\u00e0 deux ou plus pour un petit tournoi.

    Le baby-foot

    Pr\\u00e9sent dans la cage de l\\u2019escalier C, le baby-foot est ind\\u00e9niablement pl\\u00e9biscit\\u00e9 par les usager\\u00b7\\u00e8re\\u00b7s de la K-F\\u00eat. L\\u2019\\u00e9quipe ne peut malheureusement pas pr\\u00eater de balles, et il faut donc penser \\u00e0 rapporter les siennes pour jouer !

    La d\\u00e9funte fun machine

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

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

    \", \"id\": \"c7cd68cf-21ac-4400-b8bc-fecdd471b1c3\"}]", + "content": "[{\"id\": \"cbb9703d-20f5-45bd-ad6f-9386b966a1b1\", \"type\": \"rich\", \"value\": \"

    Et le baby-foot

    \"}, {\"id\": \"aa3c7219-295a-4ec1-94d7-35abdc53ef46\", \"type\": \"rich\", \"value\": \"

    LE flipper

    \\n\\n\\n\\n

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

    \"}, {\"id\": \"1f0caadf-2d5c-4d06-99d7-910690dbfcba\", \"type\": \"rich\", \"value\": \"

    Le baby-foot

    \"}, {\"id\": \"6ded3ee7-975f-4531-a0f4-310df4851fcd\", \"type\": \"rich\", \"value\": \"

    La d\\u00e9funte fun machine

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

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

    \"}]", "no_header": false, "layout": "kfet/base_col_mult.html", "main_size": "", @@ -1381,34 +785,12 @@ "model": "kfetcms.kfetpage", "pk": 10, "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

    Responsable de la publication

    • Il s'agit de la pr\\u00e9sidence du COF :
      Association des \\u00c9l\\u00e8ves de l'\\u00c9cole Normale Sup\\u00e9rieure

      45 rue d'Ulm

      75005 Paris
    \", \"id\": \"4c7e2534-c943-4172-8cec-2f7b91074ec4\"}, {\"type\": \"rich\", \"value\": \"

    Informations prestataires

    • L'h\\u00e9bergement est fourni \\u00e0 titre gracieux par le SPI:
      \\u00c9cole Normale Sup\\u00e9rieure
      Service de prestations Informatiques
      45 rue d'Ulm
      75005 Paris
    • Le d\\u00e9veloppement est assur\\u00e9 par COF-Geek.
    \", \"id\": \"ecefc786-f529-4731-a8bc-05f266cc9c8b\"}]", + "content": "[{\"id\": \"15b49ad7-e257-4284-809d-10ece31e322d\", \"type\": \"rich\", \"value\": \"

    Responsable de la publication

    • Il s'agit de la pr\\u00e9sidence du COF :
      Association des \\u00c9l\\u00e8ves de l'\\u00c9cole Normale Sup\\u00e9rieure

      45 rue d'Ulm

      75005 Paris
    \"}, {\"id\": \"3ad8b024-9dfb-45e8-94e5-429840eeaedd\", \"type\": \"rich\", \"value\": \"

    Informations prestataires

    • L'h\\u00e9bergement est fourni \\u00e0 titre gracieux par le CRI:
      \\u00c9cole Normale Sup\\u00e9rieure
      Centre de Ressources Informatiques
      45 rue d'Ulm
      75005 Paris
    • Le d\\u00e9veloppement est assur\\u00e9 par COF-Geek.
    \"}]", "no_header": false, "layout": "kfet/base_col_mult.html", "main_size": "", "col_count": "" } -}, - { - "model": "kfetcms.kfetpage", - "pk": 12, - "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

    Gr\\u00e2ce aux merveilleux\\u00b7ses membres du Club r\\u00e9seau, la K-F\\u00eat est ravie de vous pr\\u00e9senter le calendrier de la vie \\u00e9tudiante ! Mis \\u00e0 jour automatiquement, il r\\u00e9pertorie les \\u00e9v\\u00e9nements ayant lieu en K-F\\u00eat, et donne des pr\\u00e9cisions sur lae wo\\u00b7man responsable. Il permet aussi de savoir si les \\u00e9v\\u00e9nements ont \\u00e9t\\u00e9 confirm\\u00e9s ou non par l\\u2019\\u00e9quipe. Bien s\\u00fbr ce n'est pas parce qu'il n'y a rien de particulier pr\\u00e9vu aujourd'hui que la K-F\\u00eat est ferm\\u00e9e\\u202f!

    Tu peux le trouver \\u00e0 l\\u2019adresse suivante : https://www.eleves.ens.fr/calendrier/?v=dayGridMonth&c=K-F%C3%AAt

    C'est ici que tu peux voir si tu peux r\\u00e9server la K-F\\u00eat pour une apr\\u00e8s-midi ou une soir\\u00e9e : si une date qui te pla\\u00eet est disponible n'h\\u00e9site pas \\u00e0 envoyer un mail \\u00e0 k-fet (at) ens (point) psl (point) eu.

    \", \"id\": \"4b000610-f21b-4a99-a4a1-847a329a18d6\"}]", - "no_header": false, - "layout": "kfet/base_col_1.html", - "main_size": "", - "col_count": "2" - } -}, -{ - "model": "kfetcms.kfetpage", - "pk": 146, - "fields": { - "content": "[{\"type\": \"rich\", \"value\": \"

    Ci-apr\\u00e8s, les miscellan\\u00e9es de sentences bien senties entendues en K-F\\u00eat ou ailleurs. N\\u2019h\\u00e9site pas \\u00e0 y apporter ta propre contribution en contactant tes chef\\u00b7fe\\u00b7s pr\\u00e9f\\u00e9r\\u00e9\\u00b7e\\u00b7s, ou en d\\u00e9posant tes participations dans l\\u2019urne utilis\\u00e9e pour les candidatures K-F\\u00eat.

    • \\u00ab C\\u2019\\u00e9tait mieux avant \\u00bb
    • \\u00ab On est pas de droite, on est bobos : c'est hyper subtil en fait ! \\u00bb
    • \\u00ab Moins de sel, plus d\\u2019\\u00e9nergie \\u00bb
    • Pendant un rangement de soir\\u00e9e : \\u00ab Je propose d'organiser un tuto pot de fleur, pour expliquer que \\u00e7a n'est pas une poubelle. \\u00bb
    • \\u00c0 une cheffe K-F\\u00eat : \\u00ab Toi, toi tu m'insupportes. \\u00bb
    • Au milieu d'une conversation sur les Talas : \\u00ab En vrai, je commence \\u00e0 croire au destin, vu qu'il m'arrive des trucs de fou dans ma vie. \\u00bb
    • Durant une conversation sur le merch K-F\\u00eat : \\u00ab On n'a qu'\\u00e0 faire une brosse \\u00e0 chiottes K-F\\u00eat tant qu'on y est, comme \\u00e7a les gens pourront c**** sur la K-F\\u00eat jusqu'au bout. \\u00bb
    • \\u00ab - Mais on est vraiment des capitalistes de m**** ! \\u00bb \\u00ab - C'est de sa faute aussi, il nous cherche. \\u00bb
    \", \"id\": \"7d0f8099-bcc0-4591-b44d-5095e57a445c\"}]", - "no_header": false, - "layout": "kfet/base_col_1.html", - "main_size": "", - "col_count": "" - } }, { "model": "wagtailimages.image", @@ -2208,34 +1590,6 @@ "menu": 1 } }, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 7, - "fields": { - "sort_order": 6, - "link_page": 12, - "link_url": null, - "url_append": "", - "handle": "", - "link_text": "", - "allow_subnav": true, - "menu": 1 - } -}, -{ - "model": "wagtailmenus.flatmenuitem", - "pk": 24, - "fields": { - "sort_order": 7, - "link_page": 146, - "link_url": null, - "url_append": "", - "handle": "", - "link_text": "", - "allow_subnav": true, - "menu": 1 - } -}, { "model": "wagtailmenus.mainmenu", "pk": 1, diff --git a/kfet/cms/hooks.py b/kfet/cms/hooks.py index df400e0f..55551ec6 100644 --- a/kfet/cms/hooks.py +++ b/kfet/cms/hooks.py @@ -1,6 +1,6 @@ -from django.templatetags.static import static +from django.contrib.staticfiles.templatetags.staticfiles import static from django.utils.html import format_html -from wagtail import hooks +from wagtail.core import hooks @hooks.register("insert_editor_css") diff --git a/kfet/cms/management/commands/kfet_loadwagtail.py b/kfet/cms/management/commands/kfet_loadwagtail.py index fd04e8e3..2448a595 100644 --- a/kfet/cms/management/commands/kfet_loadwagtail.py +++ b/kfet/cms/management/commands/kfet_loadwagtail.py @@ -1,7 +1,7 @@ from django.contrib.auth.models import Group from django.core.management import call_command from django.core.management.base import BaseCommand -from wagtail.models import Page, Site +from wagtail.core.models import Page, Site class Command(BaseCommand): @@ -11,6 +11,7 @@ class Command(BaseCommand): parser.add_argument("--file", default="kfet_wagtail_02_19") 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 diff --git a/kfet/cms/migrations/0001_initial.py b/kfet/cms/migrations/0001_initial.py index b19f0db5..992f6052 100644 --- a/kfet/cms/migrations/0001_initial.py +++ b/kfet/cms/migrations/0001_initial.py @@ -2,8 +2,8 @@ from __future__ import unicode_literals import django.db.models.deletion -import wagtail.blocks -import wagtail.fields +import wagtail.core.blocks +import wagtail.core.fields import wagtail.snippets.blocks from django.db import migrations, models @@ -11,6 +11,7 @@ import kfet.cms.models class Migration(migrations.Migration): + dependencies = [ ("wagtailcore", "0033_remove_golive_expiry_help_text"), ("wagtailimages", "0019_delete_filter"), @@ -41,20 +42,20 @@ class Migration(migrations.Migration): ), ( "content", - wagtail.fields.StreamField( + wagtail.core.fields.StreamField( ( ( "rich", - wagtail.blocks.RichTextBlock(label="Éditeur"), + wagtail.core.blocks.RichTextBlock(label="Éditeur"), ), ("carte", kfet.cms.models.MenuBlock()), ( "group_team", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( ( ( "show_only", - wagtail.blocks.IntegerBlock( + wagtail.core.blocks.IntegerBlock( help_text="Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.", # noqa required=False, label="Montrer seulement", @@ -62,7 +63,7 @@ class Migration(migrations.Migration): ), ( "members", - wagtail.blocks.ListBlock( + wagtail.core.blocks.ListBlock( wagtail.snippets.blocks.SnippetChooserBlock( # noqa kfet.cms.models.MemberTeam ), @@ -75,22 +76,22 @@ class Migration(migrations.Migration): ), ( "group", - wagtail.blocks.StreamBlock( + wagtail.core.blocks.StreamBlock( ( ( "rich", - wagtail.blocks.RichTextBlock( + wagtail.core.blocks.RichTextBlock( label="Éditeur" ), ), ("carte", kfet.cms.models.MenuBlock()), ( "group_team", - wagtail.blocks.StructBlock( + wagtail.core.blocks.StructBlock( ( ( "show_only", - wagtail.blocks.IntegerBlock( # noqa + wagtail.core.blocks.IntegerBlock( # noqa help_text="Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.", # noqa required=False, label="Montrer seulement", @@ -98,7 +99,7 @@ class Migration(migrations.Migration): ), ( "members", - wagtail.blocks.ListBlock( + wagtail.core.blocks.ListBlock( wagtail.snippets.blocks.SnippetChooserBlock( # noqa kfet.cms.models.MemberTeam # noqa ), diff --git a/kfet/cms/migrations/0002_alter_kfetpage_colcount.py b/kfet/cms/migrations/0002_alter_kfetpage_colcount.py index 4df56993..4eace262 100644 --- a/kfet/cms/migrations/0002_alter_kfetpage_colcount.py +++ b/kfet/cms/migrations/0002_alter_kfetpage_colcount.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfetcms", "0001_initial")] operations = [ diff --git a/kfet/cms/migrations/0003_alter_kfetpage_content.py b/kfet/cms/migrations/0003_alter_kfetpage_content.py deleted file mode 100644 index 7e41eab4..00000000 --- a/kfet/cms/migrations/0003_alter_kfetpage_content.py +++ /dev/null @@ -1,90 +0,0 @@ -# Generated by Django 4.2.17 on 2024-12-19 12:27 - -import wagtail.blocks -import wagtail.fields -import wagtail.snippets.blocks -from django.db import migrations - -import kfet.cms.models - - -class Migration(migrations.Migration): - - dependencies = [ - ("kfetcms", "0002_alter_kfetpage_colcount"), - ] - - operations = [ - migrations.AlterField( - model_name="kfetpage", - name="content", - field=wagtail.fields.StreamField( - [ - ("rich", wagtail.blocks.RichTextBlock(label="Éditeur")), - ("carte", kfet.cms.models.MenuBlock()), - ( - "group_team", - wagtail.blocks.StructBlock( - [ - ( - "show_only", - wagtail.blocks.IntegerBlock( - help_text="Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.", - label="Montrer seulement", - required=False, - ), - ), - ( - "members", - wagtail.blocks.ListBlock( - wagtail.snippets.blocks.SnippetChooserBlock( - kfet.cms.models.MemberTeam - ), - form_classname="team-group", - label="K-Fêt-eux-ses", - ), - ), - ] - ), - ), - ( - "group", - wagtail.blocks.StreamBlock( - [ - ("rich", wagtail.blocks.RichTextBlock(label="Éditeur")), - ("carte", kfet.cms.models.MenuBlock()), - ( - "group_team", - wagtail.blocks.StructBlock( - [ - ( - "show_only", - wagtail.blocks.IntegerBlock( - help_text="Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.", - label="Montrer seulement", - required=False, - ), - ), - ( - "members", - wagtail.blocks.ListBlock( - wagtail.snippets.blocks.SnippetChooserBlock( - kfet.cms.models.MemberTeam - ), - form_classname="team-group", - label="K-Fêt-eux-ses", - ), - ), - ] - ), - ), - ], - label="Contenu groupé", - ), - ), - ], - use_json_field=True, - verbose_name="Contenu", - ), - ), - ] diff --git a/kfet/cms/models.py b/kfet/cms/models.py index 6ba36940..b9061b21 100644 --- a/kfet/cms/models.py +++ b/kfet/cms/models.py @@ -1,9 +1,15 @@ from django.db import models -from django.utils.translation import gettext_lazy as _ -from wagtail import blocks -from wagtail.admin.panels import FieldPanel, FieldRowPanel, MultiFieldPanel -from wagtail.fields import StreamField -from wagtail.models import Page +from django.utils.translation import ugettext_lazy as _ +from wagtail.admin.edit_handlers import ( + FieldPanel, + FieldRowPanel, + MultiFieldPanel, + StreamFieldPanel, +) +from wagtail.core import blocks +from wagtail.core.fields import StreamField +from wagtail.core.models import Page +from wagtail.images.edit_handlers import ImageChooserPanel from wagtail.snippets.blocks import SnippetChooserBlock from wagtail.snippets.models import register_snippet @@ -37,7 +43,7 @@ class MemberTeam(models.Model): FieldPanel("first_name"), FieldPanel("last_name"), FieldPanel("nick_name"), - FieldPanel("photo"), + ImageChooserPanel("photo"), ] def __str__(self): @@ -91,9 +97,8 @@ class KFetStreamBlock(ChoicesStreamBlock): class KFetPage(Page): - content = StreamField( - KFetStreamBlock, verbose_name=_("Contenu"), use_json_field=True - ) + + content = StreamField(KFetStreamBlock, verbose_name=_("Contenu")) # Layout fields @@ -131,7 +136,7 @@ class KFetPage(Page): # Panels - content_panels = Page.content_panels + [FieldPanel("content")] + content_panels = Page.content_panels + [StreamFieldPanel("content")] layout_panel = [ FieldPanel("no_header"), diff --git a/kfet/config.py b/kfet/config.py index 7280afc6..a8f7f0eb 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -1,5 +1,4 @@ import djconfig -from asgiref.sync import sync_to_async from django.core.exceptions import ValidationError from django.db import models @@ -24,7 +23,7 @@ class KFetConfig(object): # Note it should be called only once across requests, if you use # kfet_config instance below. if not self._conf_init: - sync_to_async(djconfig.reload_maybe)() + djconfig.reload_maybe() self._conf_init = True def __getattr__(self, key): diff --git a/kfet/consumers.py b/kfet/consumers.py index 1765836c..2655c86b 100644 --- a/kfet/consumers.py +++ b/kfet/consumers.py @@ -1,18 +1,6 @@ -from asgiref.sync import async_to_sync -from channels.layers import get_channel_layer - from .utils import DjangoJsonWebsocketConsumer, PermConsumerMixin class KPsul(PermConsumerMixin, DjangoJsonWebsocketConsumer): groups = ["kfet.kpsul"] perms_connect = ["kfet.is_team"] - - async def kpsul(self, event): - await self.send_json(event) - - @classmethod - @async_to_sync - async def group_send(cls, group, data): - channel_layer = get_channel_layer() - await channel_layer.group_send(group, data) diff --git a/kfet/forms.py b/kfet/forms.py index 2d0808ef..b6fad26f 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -1,19 +1,18 @@ -from datetime import date, timedelta +from datetime import timedelta from decimal import Decimal from django import forms -from django.conf import settings from django.contrib.auth.models import User from django.core import validators from django.core.exceptions import ValidationError from django.forms import modelformset_factory from django.utils import timezone -from django.utils.translation import gettext_lazy as _ from djconfig.forms import ConfigForm from gestioncof.models import CofProfile from kfet.models import ( Account, + AccountNegative, Article, ArticleCategory, Checkout, @@ -24,10 +23,7 @@ from kfet.models import ( Transfer, TransferGroup, ) -from kfet.statistic import SCALE_CLASS_CHOICES -from . import KFET_DELETED_TRIGRAMME -from .auth import KFET_GENERIC_TRIGRAMME from .auth.forms import UserGroupForm # noqa # ----- @@ -45,69 +41,12 @@ class DateTimeWidget(forms.DateTimeInput): js = ("kfet/vendor/bootstrap/bootstrap-datetimepicker.min.js",) -class ContactForm(forms.Form): - from_email = forms.EmailField( - label="Adresse mail", - help_text="Si aucune adresse mail n'est renseignée, la soumission sera anonyme.", - required=False, - ) - subject = forms.CharField(label="Objet", required=True) - message = forms.CharField(widget=forms.Textarea, required=True) - - def clean_from_email(self): - return self.cleaned_data["from_email"] or "Anonyme " - - -class DemandeSoireeForm(forms.Form): - HORAIRE_CHOICES = map(lambda s: (s, s), ("22h", "23h", "00h", "01h", "02h", "03h")) - SERVICE_CHOICES = ( - ("K-Fêt", "K-Fêt standard (L'équipe K-Fêt fait le service normal au bar)"), - ("Kalô", "Type Kalô (Vous ramenez vos propres boissons et servez vous-mêmes)"), - ) - - nom = forms.CharField() - from_email = forms.EmailField(label="Adresse mail de contact") - - contact_boum = forms.BooleanField(label="Contacter le Boum", required=False) - contact_pls = forms.BooleanField(label="Contacter PLS", required=False) - - theme = forms.CharField(label="Thème de la soirée") - horaire_fin = forms.ChoiceField(label="Horaire de fin", choices=HORAIRE_CHOICES) - service = forms.ChoiceField(label="Mode de service", choices=SERVICE_CHOICES) - date = forms.CharField(label="Date souhaitée") - - respo1 = forms.CharField(label="Nom de la personne respo n°1") - respo2 = forms.CharField(label="Nom de la personne respo n°2") - respo3 = forms.CharField(label="Nom de la personne respo n°3") - respo4 = forms.CharField(label="Nom de la personne respo n°4") - - remarques = forms.CharField( - label="Remarques supplémentaires", widget=forms.Textarea - ) - - # ----- # Account forms # ----- -def default_promo(): - now = date.today() - return now.month <= 7 and now.year - 1 or now.year - - -def get_promo_choices(): - return [("", "Sans promo")] + [(r, r) for r in range(1980, date.today().year + 1)] - - class AccountForm(forms.ModelForm): - promo = forms.TypedChoiceField( - choices=get_promo_choices, - coerce=int, - empty_value=None, - initial=default_promo, - required=False, - ) # Surcharge pour passer data à Account.save() def save(self, data={}, *args, **kwargs): @@ -117,12 +56,8 @@ class AccountForm(forms.ModelForm): class Meta: model = Account - fields = ["trigramme", "promo", "nickname"] - widgets = { - "trigramme": forms.TextInput( - attrs={"autocomplete": "off", "class": "trigramme_field"} - ) - } + fields = ["trigramme", "promo", "nickname", "is_frozen"] + widgets = {"trigramme": forms.TextInput(attrs={"autocomplete": "off"})} class AccountBalanceForm(forms.ModelForm): @@ -145,45 +80,31 @@ class AccountNoTriForm(AccountForm): exclude = ["trigramme"] +class AccountRestrictForm(AccountForm): + class Meta(AccountForm.Meta): + fields = ["is_frozen"] + + class AccountPwdForm(forms.Form): pwd1 = forms.CharField( label="Mot de passe K-Fêt", required=False, help_text="Le mot de passe doit contenir au moins huit caractères", - widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), - min_length=8, + widget=forms.PasswordInput, ) pwd2 = forms.CharField( - label="Confirmer le mot de passe", - required=False, - widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), + label="Confirmer le mot de passe", required=False, widget=forms.PasswordInput ) - def __init__(self, *args, account=None, **kwargs): - super().__init__(*args, **kwargs) - self.account = account - def clean(self): pwd1 = self.cleaned_data.get("pwd1", "") pwd2 = self.cleaned_data.get("pwd2", "") + if len(pwd1) < 8: + raise ValidationError("Mot de passe trop court") if pwd1 != pwd2: - self.add_error("pwd2", "Les mots de passe doivent être identiques !") + raise ValidationError("Les mots de passes sont différents") super().clean() - def save(self, commit=True): - password = self.cleaned_data["pwd1"] - self.account.change_pwd(password) - if commit: - self.account.save() - - return self.account - - -class AccountFrozenForm(forms.ModelForm): - class Meta: - model = Account - fields = ["is_frozen"] - class CofForm(forms.ModelForm): def clean_is_cof(self): @@ -195,13 +116,7 @@ class CofForm(forms.ModelForm): class Meta: model = CofProfile - fields = ["login_clipper", "is_cof", "is_kfet", "departement"] - - -class CofKFForm(forms.ModelForm): - class Meta: - model = CofProfile - fields = ["is_kfet"] + fields = ["login_clipper", "is_cof", "departement"] class UserForm(forms.ModelForm): @@ -224,6 +139,18 @@ class UserInfoForm(UserForm): fields = ["first_name", "last_name"] +class AccountNegativeForm(forms.ModelForm): + class Meta: + model = AccountNegative + fields = [ + "authz_overdraft_amount", + "authz_overdraft_until", + "balance_offset", + "comment", + ] + widgets = {"authz_overdraft_until": DateTimeWidget()} + + # ----- # Checkout forms # ----- @@ -356,7 +283,6 @@ class ArticleForm(forms.ModelForm): fields = [ "name", "is_sold", - "no_exte", "hidden", "price", "stock", @@ -371,7 +297,6 @@ class ArticleRestrictForm(ArticleForm): fields = [ "name", "is_sold", - "no_exte", "hidden", "price", "category", @@ -399,10 +324,7 @@ class KPsulOperationGroupForm(forms.ModelForm): widget=forms.HiddenInput(), ) on_acc = forms.ModelChoiceField( - queryset=Account.objects.exclude( - trigramme__in=[KFET_DELETED_TRIGRAMME, KFET_GENERIC_TRIGRAMME] - ), - widget=forms.HiddenInput(), + queryset=Account.objects.exclude(trigramme="GNR"), widget=forms.HiddenInput() ) class Meta: @@ -416,11 +338,7 @@ class KPsulAccountForm(forms.ModelForm): fields = ["trigramme"] widgets = { "trigramme": forms.TextInput( - attrs={ - "autocomplete": "off", - "spellcheck": "false", - "class": "trigramme_field", - } + attrs={"autocomplete": "off", "spellcheck": "false"} ) } @@ -518,6 +436,7 @@ class AddcostForm(forms.Form): class KFetConfigForm(ConfigForm): + kfet_reduction_cof = forms.DecimalField( label="Réduction COF", initial=Decimal("20"), @@ -558,31 +477,10 @@ class KFetConfigForm(ConfigForm): class FilterHistoryForm(forms.Form): - start = forms.DateTimeField( - label=_("De"), - widget=DateTimeWidget, - required=False, - help_text="Limité à {} jours ({} pour les chefs/trez)".format( - settings.KFET_HISTORY_DATE_LIMIT.days, - settings.KFET_HISTORY_LONG_DATE_LIMIT.days, - ), - ) - end = forms.DateTimeField(label=_("À"), widget=DateTimeWidget, required=False) - checkout = forms.ModelChoiceField( - label=_("Caisse"), - queryset=Checkout.objects.all(), - required=False, - empty_label=_("Toutes les caisses"), - ) - account = forms.ModelChoiceField( - label=_("Compte"), - queryset=Account.objects.all(), - required=False, - empty_label=_("Tous les comptes"), - ) - - transfers_only = forms.BooleanField(widget=forms.HiddenInput, required=False) - opes_only = forms.BooleanField(widget=forms.HiddenInput, required=False) + checkouts = forms.ModelMultipleChoiceField(queryset=Checkout.objects.all()) + accounts = forms.ModelMultipleChoiceField(queryset=Account.objects.all()) + from_date = forms.DateTimeField(widget=DateTimeWidget) + to_date = forms.DateTimeField(widget=DateTimeWidget) # ----- @@ -609,7 +507,7 @@ class TransferForm(forms.ModelForm): def clean_amount(self): amount = self.cleaned_data["amount"] if amount <= 0: - raise forms.ValidationError("Le montant d'un transfert doit être positif") + raise forms.ValidationError("Montant invalide") return amount class Meta: @@ -640,7 +538,6 @@ class InventoryArticleForm(forms.Form): self.category = kwargs["initial"]["category"] self.category_name = kwargs["initial"]["category__name"] self.box_capacity = kwargs["initial"]["box_capacity"] - self.is_sold = kwargs["initial"]["is_sold"] # ----- @@ -666,8 +563,7 @@ class OrderArticleForm(forms.Form): self.v_moy = kwargs["initial"]["v_moy"] self.v_et = kwargs["initial"]["v_et"] self.v_prev = kwargs["initial"]["v_prev"] - self.c_rec_1w = kwargs["initial"]["c_rec_1w"] - self.is_sold = kwargs["initial"]["is_sold"] + self.c_rec = kwargs["initial"]["c_rec"] class OrderArticleToInventoryForm(forms.Form): @@ -686,28 +582,3 @@ class OrderArticleToInventoryForm(forms.Form): self.category = kwargs["initial"]["category"] self.category_name = kwargs["initial"]["category__name"] self.quantity_ordered = kwargs["initial"]["quantity_ordered"] - - -# ---- -# Formulaires pour les statistiques K-Fêt -# ---- - - -class StatScaleForm(forms.Form): - """Formulaire pour nettoyer les paramètres envoyés aux - vues de statistiques K-Fêt. Non destiné à être affiché. - """ - - name = forms.ChoiceField(choices=SCALE_CLASS_CHOICES) - begin = forms.DateTimeField(required=False) - end = forms.DateTimeField(required=False) - n_steps = forms.IntegerField(required=False) - last = forms.BooleanField(required=False) - - -class AccountStatForm(forms.Form): - """Idem, mais pour la balance d'un compte""" - - begin_date = forms.DateTimeField(required=False) - end_date = forms.DateTimeField(required=False) - last_days = forms.IntegerField(required=False) diff --git a/kfet/management/commands/createopes.py b/kfet/management/commands/createopes.py index 3f8f7cf4..03458ea0 100644 --- a/kfet/management/commands/createopes.py +++ b/kfet/management/commands/createopes.py @@ -41,6 +41,7 @@ class Command(BaseCommand): ) def handle(self, *args, **options): + self.stdout.write("Génération d'opérations") # Output log vars @@ -73,6 +74,7 @@ class Command(BaseCommand): opegroup_list = [] for i in range(num_ops): + # Randomly pick account if random.random() > 0.25: account = random.choice(accounts) @@ -172,6 +174,7 @@ class Command(BaseCommand): at_list = [] for i in range(num_transfers): + # Randomly pick time at = now - timedelta(seconds=random.randint(0, time)) diff --git a/kfet/management/commands/loadkfetdevdata.py b/kfet/management/commands/loadkfetdevdata.py index 0f0c15fe..43154d6e 100644 --- a/kfet/management/commands/loadkfetdevdata.py +++ b/kfet/management/commands/loadkfetdevdata.py @@ -6,13 +6,12 @@ import os import random from datetime import timedelta -from django.contrib.auth.models import User +from django.contrib.auth.models import Group, Permission, User from django.core.management import call_command from django.utils import timezone from gestioncof.management.base import MyBaseCommand from gestioncof.models import CofProfile -from kfet.auth.models import KFetGroup, KFetPermission from kfet.models import ( Account, Article, @@ -34,17 +33,21 @@ class Command(MyBaseCommand): # Groupes # --- - group_chef, _ = KFetGroup.objects.get_or_create(name="K-Fêt César") - group_boy, _ = KFetGroup.objects.get_or_create(name="K-Fêt Légionnaire") + Group.objects.filter(name__icontains="K-Fêt").delete() - # Give relevant permissions to both groups - chef_perms = KFetPermission.objects.all() - group_chef.permissions.add(*chef_perms) + group_chef = Group(name="K-Fêt César") + group_boy = Group(name="K-Fêt Légionnaire") - boy_perms = KFetPermission.objects.filter( - codename__in=["is_team", "perform_deposit", "add_account", "add_transfer"] + group_chef.save() + group_boy.save() + + permissions_chef = Permission.objects.filter(content_type__app_label="kfet",) + permissions_boy = Permission.objects.filter( + content_type__app_label="kfet", codename__in=["is_team", "perform_deposit"] ) - group_boy.permissions.add(*boy_perms) + + group_chef.permissions.add(*permissions_chef) + group_boy.permissions.add(*permissions_boy) # --- # Comptes @@ -52,18 +55,16 @@ class Command(MyBaseCommand): self.stdout.write("Création des comptes K-Fêt") - cof_profiles = CofProfile.objects.order_by("user__username") - - gaulois = cof_profiles.filter(user__last_name="Gaulois") + gaulois = CofProfile.objects.filter(user__last_name="Gaulois") gaulois_trigramme = map("{:03d}".format, range(50)) - romains = cof_profiles.filter(user__last_name="Romain") + romains = CofProfile.objects.filter(user__last_name="Romain") romains_trigramme = map(lambda x: str(100 + x), range(99)) created_accounts = 0 team_accounts = 0 - for profile, trigramme in zip(gaulois, gaulois_trigramme): + for (profile, trigramme) in zip(gaulois, gaulois_trigramme): account, created = Account.objects.get_or_create( trigramme=trigramme, cofprofile=profile, @@ -74,7 +75,7 @@ class Command(MyBaseCommand): if profile.user.first_name == "Abraracourcix": profile.user.groups.add(group_chef) - for profile, trigramme in zip(romains, romains_trigramme): + for (profile, trigramme) in zip(romains, romains_trigramme): account, created = Account.objects.get_or_create( trigramme=trigramme, cofprofile=profile, diff --git a/kfet/management/commands/sendrappelsnegatifs.py b/kfet/management/commands/sendrappelsnegatifs.py deleted file mode 100644 index 981e2c44..00000000 --- a/kfet/management/commands/sendrappelsnegatifs.py +++ /dev/null @@ -1,57 +0,0 @@ -""" -Gestion en ligne de commande des mails de rappel K-Fet. -""" - -import smtplib -from datetime import timedelta - -from django.core.management.base import BaseCommand -from django.utils import timezone - -from kfet.models import AccountNegative - - -def send_mail(neg: AccountNegative, stdout) -> None: - try: - neg.send_rappel() - stdout.write(f"Mail de rappel pour {neg.account} envoyé avec succès.") - except smtplib.SMTPException: - stdout.write(f"Erreur lors de l'envoi du mail de rappel pour {neg.account}.") - - -class Command(BaseCommand): - help = ( - "Envoie un mail de rappel aux personnes en négatif.\n" - "Envoie un mail au bout de 24h, puis un mail par semaine." - ) - leave_locale_alone = True - - def handle(self, *args, **options): - now = timezone.now() - - # Le premier mail est envoyé après 24h de négatif, puis toutes les semaines - first_delay = timedelta(days=1) - periodic_delay = timedelta(weeks=1) - - # On n'envoie des mails qu'aux comptes qui ont un négatif vraiment actif - # et dont la balance est négative - # On ignore les comptes gelés qui signinfient une adresse mail plus valide - account_negatives = AccountNegative.objects.filter( - account__balance__lt=0, account__is_frozen=False - ).exclude(end__lte=now) - - accounts_first_mail = account_negatives.filter( - start__lt=now - first_delay, last_rappel__isnull=True - ) - accounts_periodic_mail = account_negatives.filter( - last_rappel__lt=now - periodic_delay - ) - - for neg in accounts_first_mail: - send_mail(neg, self.stdout) - - for neg in accounts_periodic_mail: - send_mail(neg, self.stdout) - - if not (accounts_first_mail.exists() or accounts_periodic_mail.exists()): - self.stdout.write("Aucun mail à envoyer.") diff --git a/kfet/migrations/0001_initial.py b/kfet/migrations/0001_initial.py index 403a1c22..8dbad4a8 100644 --- a/kfet/migrations/0001_initial.py +++ b/kfet/migrations/0001_initial.py @@ -9,6 +9,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("gestioncof", "0007_alter_club")] operations = [ diff --git a/kfet/migrations/0002_auto_20160802_2139.py b/kfet/migrations/0002_auto_20160802_2139.py index 3f91c880..39ccbbe5 100644 --- a/kfet/migrations/0002_auto_20160802_2139.py +++ b/kfet/migrations/0002_auto_20160802_2139.py @@ -7,6 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0001_initial")] operations = [ diff --git a/kfet/migrations/0003_auto_20160802_2142.py b/kfet/migrations/0003_auto_20160802_2142.py index 57ca930c..c3bfda52 100644 --- a/kfet/migrations/0003_auto_20160802_2142.py +++ b/kfet/migrations/0003_auto_20160802_2142.py @@ -7,6 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0002_auto_20160802_2139")] operations = [ diff --git a/kfet/migrations/0004_auto_20160802_2144.py b/kfet/migrations/0004_auto_20160802_2144.py index 0690bb22..48646cef 100644 --- a/kfet/migrations/0004_auto_20160802_2144.py +++ b/kfet/migrations/0004_auto_20160802_2144.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0003_auto_20160802_2142")] operations = [ diff --git a/kfet/migrations/0005_auto_20160802_2154.py b/kfet/migrations/0005_auto_20160802_2154.py index 27b3450d..49c96b1c 100644 --- a/kfet/migrations/0005_auto_20160802_2154.py +++ b/kfet/migrations/0005_auto_20160802_2154.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0004_auto_20160802_2144")] operations = [ diff --git a/kfet/migrations/0006_auto_20160804_0600.py b/kfet/migrations/0006_auto_20160804_0600.py index e95ad017..524e1352 100644 --- a/kfet/migrations/0006_auto_20160804_0600.py +++ b/kfet/migrations/0006_auto_20160804_0600.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0005_auto_20160802_2154")] operations = [ diff --git a/kfet/migrations/0007_auto_20160804_0641.py b/kfet/migrations/0007_auto_20160804_0641.py index d9326cf2..fb870a71 100644 --- a/kfet/migrations/0007_auto_20160804_0641.py +++ b/kfet/migrations/0007_auto_20160804_0641.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0006_auto_20160804_0600")] operations = [ diff --git a/kfet/migrations/0008_auto_20160804_1736.py b/kfet/migrations/0008_auto_20160804_1736.py index 4953a50e..930562b0 100644 --- a/kfet/migrations/0008_auto_20160804_1736.py +++ b/kfet/migrations/0008_auto_20160804_1736.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0007_auto_20160804_0641")] operations = [ diff --git a/kfet/migrations/0009_auto_20160805_0720.py b/kfet/migrations/0009_auto_20160805_0720.py index 3fef355f..90a19749 100644 --- a/kfet/migrations/0009_auto_20160805_0720.py +++ b/kfet/migrations/0009_auto_20160805_0720.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0008_auto_20160804_1736")] operations = [ diff --git a/kfet/migrations/0010_auto_20160806_2343.py b/kfet/migrations/0010_auto_20160806_2343.py index 14ea35dc..84267a6d 100644 --- a/kfet/migrations/0010_auto_20160806_2343.py +++ b/kfet/migrations/0010_auto_20160806_2343.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0009_auto_20160805_0720")] operations = [ diff --git a/kfet/migrations/0011_auto_20160807_1720.py b/kfet/migrations/0011_auto_20160807_1720.py index 806036bc..53064235 100644 --- a/kfet/migrations/0011_auto_20160807_1720.py +++ b/kfet/migrations/0011_auto_20160807_1720.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0010_auto_20160806_2343")] operations = [ diff --git a/kfet/migrations/0012_settings.py b/kfet/migrations/0012_settings.py index 08fcd9d9..1bae911d 100644 --- a/kfet/migrations/0012_settings.py +++ b/kfet/migrations/0012_settings.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0011_auto_20160807_1720")] operations = [ diff --git a/kfet/migrations/0013_auto_20160807_1840.py b/kfet/migrations/0013_auto_20160807_1840.py index 3eac5f07..9a9ac3b1 100644 --- a/kfet/migrations/0013_auto_20160807_1840.py +++ b/kfet/migrations/0013_auto_20160807_1840.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0012_settings")] operations = [ diff --git a/kfet/migrations/0014_auto_20160807_2314.py b/kfet/migrations/0014_auto_20160807_2314.py index b6cc029e..ecaee428 100644 --- a/kfet/migrations/0014_auto_20160807_2314.py +++ b/kfet/migrations/0014_auto_20160807_2314.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0013_auto_20160807_1840")] operations = [ diff --git a/kfet/migrations/0015_auto_20160807_2324.py b/kfet/migrations/0015_auto_20160807_2324.py index 29e130f6..fa2af882 100644 --- a/kfet/migrations/0015_auto_20160807_2324.py +++ b/kfet/migrations/0015_auto_20160807_2324.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0014_auto_20160807_2314")] operations = [ diff --git a/kfet/migrations/0016_settings_value_account.py b/kfet/migrations/0016_settings_value_account.py index 6abc615e..e10eb682 100644 --- a/kfet/migrations/0016_settings_value_account.py +++ b/kfet/migrations/0016_settings_value_account.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0015_auto_20160807_2324")] operations = [ diff --git a/kfet/migrations/0017_auto_20160808_0234.py b/kfet/migrations/0017_auto_20160808_0234.py index fb177e4a..795078a9 100644 --- a/kfet/migrations/0017_auto_20160808_0234.py +++ b/kfet/migrations/0017_auto_20160808_0234.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0016_settings_value_account")] operations = [ diff --git a/kfet/migrations/0018_auto_20160808_0341.py b/kfet/migrations/0018_auto_20160808_0341.py index 26d69745..3b29b716 100644 --- a/kfet/migrations/0018_auto_20160808_0341.py +++ b/kfet/migrations/0018_auto_20160808_0341.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0017_auto_20160808_0234")] operations = [ diff --git a/kfet/migrations/0019_auto_20160808_0343.py b/kfet/migrations/0019_auto_20160808_0343.py index 14444901..f500032a 100644 --- a/kfet/migrations/0019_auto_20160808_0343.py +++ b/kfet/migrations/0019_auto_20160808_0343.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0018_auto_20160808_0341")] operations = [ diff --git a/kfet/migrations/0020_auto_20160808_0450.py b/kfet/migrations/0020_auto_20160808_0450.py index 132d22d7..d3424bac 100644 --- a/kfet/migrations/0020_auto_20160808_0450.py +++ b/kfet/migrations/0020_auto_20160808_0450.py @@ -7,6 +7,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0019_auto_20160808_0343")] operations = [ diff --git a/kfet/migrations/0021_auto_20160808_0506.py b/kfet/migrations/0021_auto_20160808_0506.py index 75ceb569..2ef48232 100644 --- a/kfet/migrations/0021_auto_20160808_0506.py +++ b/kfet/migrations/0021_auto_20160808_0506.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0020_auto_20160808_0450")] operations = [ diff --git a/kfet/migrations/0022_auto_20160808_0512.py b/kfet/migrations/0022_auto_20160808_0512.py index 76894913..3701e856 100644 --- a/kfet/migrations/0022_auto_20160808_0512.py +++ b/kfet/migrations/0022_auto_20160808_0512.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0021_auto_20160808_0506")] operations = [ diff --git a/kfet/migrations/0023_auto_20160808_0535.py b/kfet/migrations/0023_auto_20160808_0535.py index 30f9ee63..03a8d1c3 100644 --- a/kfet/migrations/0023_auto_20160808_0535.py +++ b/kfet/migrations/0023_auto_20160808_0535.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0022_auto_20160808_0512")] operations = [ diff --git a/kfet/migrations/0024_settings_value_duration.py b/kfet/migrations/0024_settings_value_duration.py index 618c0649..56b22812 100644 --- a/kfet/migrations/0024_settings_value_duration.py +++ b/kfet/migrations/0024_settings_value_duration.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0023_auto_20160808_0535")] operations = [ diff --git a/kfet/migrations/0025_auto_20160809_0750.py b/kfet/migrations/0025_auto_20160809_0750.py index 317cac64..51f3b5a3 100644 --- a/kfet/migrations/0025_auto_20160809_0750.py +++ b/kfet/migrations/0025_auto_20160809_0750.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0024_settings_value_duration")] operations = [ diff --git a/kfet/migrations/0026_auto_20160809_0810.py b/kfet/migrations/0026_auto_20160809_0810.py index c67518ab..942eaf26 100644 --- a/kfet/migrations/0026_auto_20160809_0810.py +++ b/kfet/migrations/0026_auto_20160809_0810.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0025_auto_20160809_0750")] operations = [ diff --git a/kfet/migrations/0027_auto_20160811_0648.py b/kfet/migrations/0027_auto_20160811_0648.py index dc560d30..a0084e8a 100644 --- a/kfet/migrations/0027_auto_20160811_0648.py +++ b/kfet/migrations/0027_auto_20160811_0648.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0026_auto_20160809_0810")] operations = [ diff --git a/kfet/migrations/0028_auto_20160820_0146.py b/kfet/migrations/0028_auto_20160820_0146.py index 865aca29..a1b046cf 100644 --- a/kfet/migrations/0028_auto_20160820_0146.py +++ b/kfet/migrations/0028_auto_20160820_0146.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0027_auto_20160811_0648")] operations = [ diff --git a/kfet/migrations/0029_genericteamtoken.py b/kfet/migrations/0029_genericteamtoken.py index 976ff4d9..c5a81f05 100644 --- a/kfet/migrations/0029_genericteamtoken.py +++ b/kfet/migrations/0029_genericteamtoken.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0028_auto_20160820_0146")] operations = [ diff --git a/kfet/migrations/0030_auto_20160821_0029.py b/kfet/migrations/0030_auto_20160821_0029.py index b811ce3e..1858522d 100644 --- a/kfet/migrations/0030_auto_20160821_0029.py +++ b/kfet/migrations/0030_auto_20160821_0029.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0029_genericteamtoken")] operations = [ diff --git a/kfet/migrations/0031_auto_20160822_0523.py b/kfet/migrations/0031_auto_20160822_0523.py index 5de048b6..23aec1be 100644 --- a/kfet/migrations/0031_auto_20160822_0523.py +++ b/kfet/migrations/0031_auto_20160822_0523.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0030_auto_20160821_0029")] operations = [ diff --git a/kfet/migrations/0032_auto_20160822_2350.py b/kfet/migrations/0032_auto_20160822_2350.py index 6231a321..7cba0da3 100644 --- a/kfet/migrations/0032_auto_20160822_2350.py +++ b/kfet/migrations/0032_auto_20160822_2350.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0031_auto_20160822_0523")] operations = [ diff --git a/kfet/migrations/0033_checkoutstatement_not_count.py b/kfet/migrations/0033_checkoutstatement_not_count.py index f2b2250c..dd445406 100644 --- a/kfet/migrations/0033_checkoutstatement_not_count.py +++ b/kfet/migrations/0033_checkoutstatement_not_count.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0032_auto_20160822_2350")] operations = [ diff --git a/kfet/migrations/0034_auto_20160823_0206.py b/kfet/migrations/0034_auto_20160823_0206.py index 20ee92b0..1b28e289 100644 --- a/kfet/migrations/0034_auto_20160823_0206.py +++ b/kfet/migrations/0034_auto_20160823_0206.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0033_checkoutstatement_not_count")] operations = [ diff --git a/kfet/migrations/0035_auto_20160823_1505.py b/kfet/migrations/0035_auto_20160823_1505.py index f320c453..e2a98ca7 100644 --- a/kfet/migrations/0035_auto_20160823_1505.py +++ b/kfet/migrations/0035_auto_20160823_1505.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0034_auto_20160823_0206")] operations = [ diff --git a/kfet/migrations/0036_auto_20160823_1910.py b/kfet/migrations/0036_auto_20160823_1910.py index 9f81b2ba..09726e37 100644 --- a/kfet/migrations/0036_auto_20160823_1910.py +++ b/kfet/migrations/0036_auto_20160823_1910.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0035_auto_20160823_1505")] operations = [ diff --git a/kfet/migrations/0037_auto_20160826_2333.py b/kfet/migrations/0037_auto_20160826_2333.py index 3e99c36c..6ebc921d 100644 --- a/kfet/migrations/0037_auto_20160826_2333.py +++ b/kfet/migrations/0037_auto_20160826_2333.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0036_auto_20160823_1910")] operations = [ diff --git a/kfet/migrations/0038_auto_20160828_0402.py b/kfet/migrations/0038_auto_20160828_0402.py index 599a3205..be8d847e 100644 --- a/kfet/migrations/0038_auto_20160828_0402.py +++ b/kfet/migrations/0038_auto_20160828_0402.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0037_auto_20160826_2333")] operations = [ diff --git a/kfet/migrations/0039_auto_20160828_0430.py b/kfet/migrations/0039_auto_20160828_0430.py index 0b58cee4..2611d578 100644 --- a/kfet/migrations/0039_auto_20160828_0430.py +++ b/kfet/migrations/0039_auto_20160828_0430.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0038_auto_20160828_0402")] operations = [ diff --git a/kfet/migrations/0040_auto_20160829_2035.py b/kfet/migrations/0040_auto_20160829_2035.py index 15f972f4..16bd2b36 100644 --- a/kfet/migrations/0040_auto_20160829_2035.py +++ b/kfet/migrations/0040_auto_20160829_2035.py @@ -4,9 +4,11 @@ from __future__ import unicode_literals import datetime from django.db import migrations, models +from django.utils.timezone import utc class Migration(migrations.Migration): + dependencies = [("kfet", "0039_auto_20160828_0430")] operations = [ @@ -16,9 +18,7 @@ class Migration(migrations.Migration): name="at", field=models.DateTimeField( auto_now_add=True, - default=datetime.datetime( - 2016, 8, 29, 18, 35, 3, 419033, tzinfo=datetime.timezone.utc - ), + default=datetime.datetime(2016, 8, 29, 18, 35, 3, 419033, tzinfo=utc), ), preserve_default=False, ), diff --git a/kfet/migrations/0041_auto_20160830_1502.py b/kfet/migrations/0041_auto_20160830_1502.py index eab76e81..488d33ff 100644 --- a/kfet/migrations/0041_auto_20160830_1502.py +++ b/kfet/migrations/0041_auto_20160830_1502.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0040_auto_20160829_2035")] operations = [ diff --git a/kfet/migrations/0042_auto_20160831_0126.py b/kfet/migrations/0042_auto_20160831_0126.py index 297a41f6..70adbad5 100644 --- a/kfet/migrations/0042_auto_20160831_0126.py +++ b/kfet/migrations/0042_auto_20160831_0126.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0041_auto_20160830_1502")] operations = [ diff --git a/kfet/migrations/0043_auto_20160901_0046.py b/kfet/migrations/0043_auto_20160901_0046.py index f0a08184..b5132335 100644 --- a/kfet/migrations/0043_auto_20160901_0046.py +++ b/kfet/migrations/0043_auto_20160901_0046.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0042_auto_20160831_0126")] operations = [ diff --git a/kfet/migrations/0044_auto_20160901_1614.py b/kfet/migrations/0044_auto_20160901_1614.py index 69f3dc21..f81ae828 100644 --- a/kfet/migrations/0044_auto_20160901_1614.py +++ b/kfet/migrations/0044_auto_20160901_1614.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0043_auto_20160901_0046")] operations = [ diff --git a/kfet/migrations/0045_auto_20160905_0705.py b/kfet/migrations/0045_auto_20160905_0705.py index 7a17cbb9..0f98c56a 100644 --- a/kfet/migrations/0045_auto_20160905_0705.py +++ b/kfet/migrations/0045_auto_20160905_0705.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0044_auto_20160901_1614")] operations = [ diff --git a/kfet/migrations/0046_account_created_at.py b/kfet/migrations/0046_account_created_at.py index d62c6bc2..a0274432 100644 --- a/kfet/migrations/0046_account_created_at.py +++ b/kfet/migrations/0046_account_created_at.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0045_auto_20160905_0705")] operations = [ diff --git a/kfet/migrations/0047_auto_20170104_1528.py b/kfet/migrations/0047_auto_20170104_1528.py index 11a689e1..d391e1f4 100644 --- a/kfet/migrations/0047_auto_20170104_1528.py +++ b/kfet/migrations/0047_auto_20170104_1528.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0046_account_created_at")] operations = [ diff --git a/kfet/migrations/0048_article_hidden.py b/kfet/migrations/0048_article_hidden.py index b85b54bf..d4d89022 100644 --- a/kfet/migrations/0048_article_hidden.py +++ b/kfet/migrations/0048_article_hidden.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0047_auto_20170104_1528")] operations = [ diff --git a/kfet/migrations/0048_default_datetime.py b/kfet/migrations/0048_default_datetime.py index 0257e2eb..d5408c59 100644 --- a/kfet/migrations/0048_default_datetime.py +++ b/kfet/migrations/0048_default_datetime.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0047_auto_20170104_1528")] operations = [ diff --git a/kfet/migrations/0049_merge.py b/kfet/migrations/0049_merge.py index da38d232..e9bcb47a 100644 --- a/kfet/migrations/0049_merge.py +++ b/kfet/migrations/0049_merge.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0048_article_hidden"), ("kfet", "0048_default_datetime")] operations = [] diff --git a/kfet/migrations/0050_remove_checkout.py b/kfet/migrations/0050_remove_checkout.py index 871f864b..8cfd370a 100644 --- a/kfet/migrations/0050_remove_checkout.py +++ b/kfet/migrations/0050_remove_checkout.py @@ -19,6 +19,7 @@ def revert_operation_types(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("kfet", "0049_merge")] operations = [ diff --git a/kfet/migrations/0051_verbose_names.py b/kfet/migrations/0051_verbose_names.py index db1abe11..9892af71 100644 --- a/kfet/migrations/0051_verbose_names.py +++ b/kfet/migrations/0051_verbose_names.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0050_remove_checkout")] operations = [ diff --git a/kfet/migrations/0052_category_addcost.py b/kfet/migrations/0052_category_addcost.py index 39f0e6eb..8a8da85f 100644 --- a/kfet/migrations/0052_category_addcost.py +++ b/kfet/migrations/0052_category_addcost.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0051_verbose_names")] operations = [ diff --git a/kfet/migrations/0053_created_at.py b/kfet/migrations/0053_created_at.py index c0e8e7c2..6da14568 100644 --- a/kfet/migrations/0053_created_at.py +++ b/kfet/migrations/0053_created_at.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0052_category_addcost")] operations = [ diff --git a/kfet/migrations/0054_delete_settings.py b/kfet/migrations/0054_delete_settings.py index c3d6ece5..7294c8bf 100644 --- a/kfet/migrations/0054_delete_settings.py +++ b/kfet/migrations/0054_delete_settings.py @@ -40,6 +40,7 @@ def adapt_settings(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("kfet", "0053_created_at"), ("djconfig", "0001_initial")] operations = [ diff --git a/kfet/migrations/0054_update_promos.py b/kfet/migrations/0054_update_promos.py index 95ea78cd..0f86779b 100644 --- a/kfet/migrations/0054_update_promos.py +++ b/kfet/migrations/0054_update_promos.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0053_created_at")] operations = [ diff --git a/kfet/migrations/0055_move_permissions.py b/kfet/migrations/0055_move_permissions.py index a6d053a7..11cc9083 100644 --- a/kfet/migrations/0055_move_permissions.py +++ b/kfet/migrations/0055_move_permissions.py @@ -54,6 +54,7 @@ def forwards_perms(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [ ("kfet", "0054_delete_settings"), ("contenttypes", "0001_initial"), diff --git a/kfet/migrations/0056_change_account_meta.py b/kfet/migrations/0056_change_account_meta.py index 0224ae2b..27e51417 100644 --- a/kfet/migrations/0056_change_account_meta.py +++ b/kfet/migrations/0056_change_account_meta.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0055_move_permissions")] operations = [ diff --git a/kfet/migrations/0057_merge.py b/kfet/migrations/0057_merge.py index 98fa5b27..456bbeb0 100644 --- a/kfet/migrations/0057_merge.py +++ b/kfet/migrations/0057_merge.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ ("kfet", "0056_change_account_meta"), ("kfet", "0054_update_promos"), diff --git a/kfet/migrations/0058_delete_genericteamtoken.py b/kfet/migrations/0058_delete_genericteamtoken.py index e82fcd63..3b3216e9 100644 --- a/kfet/migrations/0058_delete_genericteamtoken.py +++ b/kfet/migrations/0058_delete_genericteamtoken.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0057_merge")] operations = [migrations.DeleteModel(name="GenericTeamToken")] diff --git a/kfet/migrations/0059_create_generic.py b/kfet/migrations/0059_create_generic.py index 1cac19a6..7408a300 100644 --- a/kfet/migrations/0059_create_generic.py +++ b/kfet/migrations/0059_create_generic.py @@ -30,6 +30,7 @@ def setup_kfet_generic_user(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("kfet", "0058_delete_genericteamtoken")] operations = [migrations.RunPython(setup_kfet_generic_user)] diff --git a/kfet/migrations/0060_amend_supplier.py b/kfet/migrations/0060_amend_supplier.py index 8694a0fc..0a56640d 100644 --- a/kfet/migrations/0060_amend_supplier.py +++ b/kfet/migrations/0060_amend_supplier.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0059_create_generic")] operations = [ diff --git a/kfet/migrations/0061_add_perms_config.py b/kfet/migrations/0061_add_perms_config.py index 2aa3fe7d..7d10da31 100644 --- a/kfet/migrations/0061_add_perms_config.py +++ b/kfet/migrations/0061_add_perms_config.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0060_amend_supplier")] operations = [ diff --git a/kfet/migrations/0063_promo.py b/kfet/migrations/0063_promo.py index 6b12195e..de04573a 100644 --- a/kfet/migrations/0063_promo.py +++ b/kfet/migrations/0063_promo.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0062_delete_globalpermissions")] operations = [ diff --git a/kfet/migrations/0064_promo_2018.py b/kfet/migrations/0064_promo_2018.py index 99c74687..7fe5e160 100644 --- a/kfet/migrations/0064_promo_2018.py +++ b/kfet/migrations/0064_promo_2018.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0063_promo")] operations = [ diff --git a/kfet/migrations/0065_choices_promo.py b/kfet/migrations/0065_choices_promo.py index d0fcf3c3..dd234c66 100644 --- a/kfet/migrations/0065_choices_promo.py +++ b/kfet/migrations/0065_choices_promo.py @@ -6,6 +6,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0064_promo_2018")] operations = [ diff --git a/kfet/migrations/0066_on_delete_actions.py b/kfet/migrations/0066_on_delete_actions.py index 6ca25529..e2d635e2 100644 --- a/kfet/migrations/0066_on_delete_actions.py +++ b/kfet/migrations/0066_on_delete_actions.py @@ -5,6 +5,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0065_choices_promo")] operations = [ diff --git a/kfet/migrations/0067_deleted_account.py b/kfet/migrations/0067_deleted_account.py index 8b5f9d83..155034f9 100644 --- a/kfet/migrations/0067_deleted_account.py +++ b/kfet/migrations/0067_deleted_account.py @@ -26,6 +26,7 @@ def setup_kfet_deleted_user(apps, schema_editor): class Migration(migrations.Migration): + dependencies = [("kfet", "0066_on_delete_actions")] operations = [migrations.RunPython(setup_kfet_deleted_user)] diff --git a/kfet/migrations/0068_on_delete_account.py b/kfet/migrations/0068_on_delete_account.py index a1cbcaf0..b8cfdb76 100644 --- a/kfet/migrations/0068_on_delete_account.py +++ b/kfet/migrations/0068_on_delete_account.py @@ -6,6 +6,7 @@ import kfet.models class Migration(migrations.Migration): + dependencies = [("kfet", "0067_deleted_account")] operations = [ diff --git a/kfet/migrations/0069_happy_new_year.py b/kfet/migrations/0069_happy_new_year.py index 2a06a9bd..bd380c47 100644 --- a/kfet/migrations/0069_happy_new_year.py +++ b/kfet/migrations/0069_happy_new_year.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [("kfet", "0068_on_delete_account")] operations = [ diff --git a/kfet/migrations/0070_articlecategory_has_reduction.py b/kfet/migrations/0070_articlecategory_has_reduction.py index 23814d52..c657dfdd 100644 --- a/kfet/migrations/0070_articlecategory_has_reduction.py +++ b/kfet/migrations/0070_articlecategory_has_reduction.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ ("kfet", "0069_happy_new_year"), ] diff --git a/kfet/migrations/0071_promo_2020.py b/kfet/migrations/0071_promo_2020.py index 4ff4af92..910c12cf 100644 --- a/kfet/migrations/0071_promo_2020.py +++ b/kfet/migrations/0071_promo_2020.py @@ -4,6 +4,7 @@ from django.db import migrations, models class Migration(migrations.Migration): + dependencies = [ ("kfet", "0070_articlecategory_has_reduction"), ] diff --git a/kfet/migrations/0072_auto_20200901_1526.py b/kfet/migrations/0072_auto_20200901_1526.py deleted file mode 100644 index 6dded31d..00000000 --- a/kfet/migrations/0072_auto_20200901_1526.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 2.2.12 on 2020-09-01 13:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0071_promo_2020"), - ] - - 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), - (2018, 2018), - (2019, 2019), - (2020, 2020), - ], - default=2020, - null=True, - ), - ), - ] diff --git a/kfet/migrations/0073_2021.py b/kfet/migrations/0073_2021.py deleted file mode 100644 index 9b3324c3..00000000 --- a/kfet/migrations/0073_2021.py +++ /dev/null @@ -1,65 +0,0 @@ -# Generated by Django 2.2.17 on 2021-01-06 20:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0072_auto_20200901_1526"), - ] - - 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), - (2018, 2018), - (2019, 2019), - (2020, 2020), - (2021, 2021), - ], - default=2020, - null=True, - ), - ), - ] diff --git a/kfet/migrations/0074_auto_20210219_1337.py b/kfet/migrations/0074_auto_20210219_1337.py deleted file mode 100644 index ea684d06..00000000 --- a/kfet/migrations/0074_auto_20210219_1337.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.2.17 on 2021-02-19 12:37 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0073_2021"), - ] - - 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"), - ("see_config", "Voir la configuration K-Fêt"), - ("change_config", "Modifier la configuration K-Fêt"), - ("access_old_history", "Peut accéder à l'historique plus ancien"), - ) - }, - ), - ] diff --git a/kfet/migrations/0075_remove_accountnegative_balance_offset.py b/kfet/migrations/0075_remove_accountnegative_balance_offset.py deleted file mode 100644 index 80122989..00000000 --- a/kfet/migrations/0075_remove_accountnegative_balance_offset.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2.17 on 2021-02-23 21:40 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0074_auto_20210219_1337"), - ] - - operations = [ - migrations.RemoveField( - model_name="accountnegative", - name="balance_offset", - ), - ] diff --git a/kfet/migrations/0076_unfreeze_accounts.py b/kfet/migrations/0076_unfreeze_accounts.py deleted file mode 100644 index d8ecc86b..00000000 --- a/kfet/migrations/0076_unfreeze_accounts.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 2.2.17 on 2021-02-23 22:51 - -from django.db import migrations - - -def unfreeze_accounts(apps, schema_editor): - Account = apps.get_model("kfet", "Account") - Account.objects.all().update(is_frozen=False) - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0075_remove_accountnegative_balance_offset"), - ] - - operations = [migrations.RunPython(unfreeze_accounts, migrations.RunPython.noop)] diff --git a/kfet/migrations/0077_delete_frozen_permission.py b/kfet/migrations/0077_delete_frozen_permission.py deleted file mode 100644 index c51426bb..00000000 --- a/kfet/migrations/0077_delete_frozen_permission.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.2.17 on 2021-02-23 23:22 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0076_unfreeze_accounts"), - ] - - operations = [ - migrations.AlterModelOptions( - name="operation", - options={ - "permissions": ( - ("perform_deposit", "Effectuer une charge"), - ( - "perform_negative_operations", - "Enregistrer des commandes en négatif", - ), - ("cancel_old_operations", "Annuler des commandes non récentes"), - ( - "perform_commented_operations", - "Enregistrer des commandes avec commentaires", - ), - ) - }, - ), - ] diff --git a/kfet/migrations/0078_negative_end.py b/kfet/migrations/0078_negative_end.py deleted file mode 100644 index d99c2a04..00000000 --- a/kfet/migrations/0078_negative_end.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.2.17 on 2021-02-28 01:00 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0077_delete_frozen_permission"), - ] - - operations = [ - migrations.RemoveField( - model_name="accountnegative", - name="authz_overdraft_amount", - ), - migrations.RemoveField( - model_name="accountnegative", - name="authz_overdraft_until", - ), - migrations.RemoveField( - model_name="accountnegative", - name="comment", - ), - migrations.AddField( - model_name="accountnegative", - name="end", - field=models.DateTimeField(blank=True, default=None, null=True), - ), - ] diff --git a/kfet/migrations/0079_auto_20210627_0022.py b/kfet/migrations/0079_auto_20210627_0022.py deleted file mode 100644 index 6e563650..00000000 --- a/kfet/migrations/0079_auto_20210627_0022.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.17 on 2021-06-26 22:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0078_negative_end"), - ] - - operations = [ - migrations.AlterField( - model_name="account", - name="promo", - field=models.IntegerField(null=True), - ), - ] diff --git a/kfet/migrations/0080_accountnegative_last_rappel.py b/kfet/migrations/0080_accountnegative_last_rappel.py deleted file mode 100644 index 61932937..00000000 --- a/kfet/migrations/0080_accountnegative_last_rappel.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.2.17 on 2021-02-23 21:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("kfet", "0079_auto_20210627_0022"), - ] - - operations = [ - migrations.AddField( - model_name="accountnegative", - name="last_rappel", - field=models.DateTimeField( - blank=True, null=True, verbose_name="Mail de rappel envoyé" - ), - ), - ] diff --git a/kfet/migrations/0081_article_no_exte.py b/kfet/migrations/0081_article_no_exte.py deleted file mode 100644 index 7cf515b4..00000000 --- a/kfet/migrations/0081_article_no_exte.py +++ /dev/null @@ -1,20 +0,0 @@ -# Generated by Django 4.2.16 on 2025-01-06 16:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("kfet", "0080_accountnegative_last_rappel"), - ] - - operations = [ - migrations.AddField( - model_name="article", - name="no_exte", - field=models.BooleanField( - default=False, verbose_name="Réservé au adhérents" - ), - ), - ] diff --git a/kfet/migrations/0082_alter_operation_options.py b/kfet/migrations/0082_alter_operation_options.py deleted file mode 100644 index 0af51ccb..00000000 --- a/kfet/migrations/0082_alter_operation_options.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 4.2.16 on 2025-01-18 10:01 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("kfet", "0081_article_no_exte"), - ] - - operations = [ - migrations.AlterModelOptions( - name="operation", - options={ - "permissions": ( - ("perform_deposit", "Effectuer une charge"), - ( - "perform_negative_operations", - "Enregistrer des commandes en négatif", - ), - ( - "perform_liq_reserved", - "Effectuer une opération réservé aux adhérents sur LIQ", - ), - ("cancel_old_operations", "Annuler des commandes non récentes"), - ( - "perform_commented_operations", - "Enregistrer des commandes avec commentaires", - ), - ) - }, - ), - ] diff --git a/kfet/migrations/0083_operationgroup_is_kfet.py b/kfet/migrations/0083_operationgroup_is_kfet.py deleted file mode 100644 index f344d829..00000000 --- a/kfet/migrations/0083_operationgroup_is_kfet.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 4.2.16 on 2025-03-18 10:25 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("kfet", "0082_alter_operation_options"), - ] - - operations = [ - migrations.AddField( - model_name="operationgroup", - name="is_kfet", - field=models.BooleanField(default=False), - ), - ] diff --git a/kfet/migrations/0084_alter_account_options.py b/kfet/migrations/0084_alter_account_options.py deleted file mode 100644 index 5174c000..00000000 --- a/kfet/migrations/0084_alter_account_options.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 4.2.16 on 2025-05-03 21:26 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ("kfet", "0083_operationgroup_is_kfet"), - ] - - operations = [ - migrations.AlterModelOptions( - name="account", - options={ - "permissions": ( - ("is_team", "Is part of the team"), - ("change_adh", "Gérer les adhésions K-Fêt"), - ("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"), - ("see_config", "Voir la configuration K-Fêt"), - ("change_config", "Modifier la configuration K-Fêt"), - ("access_old_history", "Peut accéder à l'historique plus ancien"), - ) - }, - ), - ] diff --git a/kfet/migrations/0085_alter_operation_options_alter_article_no_exte.py b/kfet/migrations/0085_alter_operation_options_alter_article_no_exte.py deleted file mode 100644 index f99320df..00000000 --- a/kfet/migrations/0085_alter_operation_options_alter_article_no_exte.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 4.2.16 on 2025-05-12 10:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("kfet", "0084_alter_account_options"), - ] - - operations = [ - migrations.AlterModelOptions( - name="operation", - options={ - "permissions": ( - ("perform_deposit", "Effectuer une charge"), - ( - "perform_negative_operations", - "Enregistrer des commandes en négatif", - ), - ( - "perform_liq_reserved", - "Effectuer une opération réservé aux adhérent⋅e⋅s sur LIQ", - ), - ("cancel_old_operations", "Annuler des commandes non récentes"), - ( - "perform_commented_operations", - "Enregistrer des commandes avec commentaires", - ), - ) - }, - ), - migrations.AlterField( - model_name="article", - name="no_exte", - field=models.BooleanField( - default=False, verbose_name="Réservé au adhérent⋅e⋅s" - ), - ), - ] diff --git a/kfet/models.py b/kfet/models.py index 8851d9f8..814f857a 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,16 +1,13 @@ import re +from datetime import date -from django.conf import settings from django.contrib.auth.models import User -from django.contrib.sites.models import Site -from django.core.mail import EmailMessage from django.core.validators import RegexValidator from django.db import models, transaction from django.db.models import F -from django.template import loader from django.urls import reverse from django.utils import timezone -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ from gestioncof.models import CofProfile from shared.utils import choices_length @@ -22,6 +19,11 @@ from .config import kfet_config from .utils import to_ukf +def default_promo(): + now = date.today() + return now.month <= 8 and now.year - 1 or now.year + + class AccountManager(models.Manager): """Manager for Account Model.""" @@ -64,7 +66,10 @@ class Account(models.Model): is_frozen = models.BooleanField("est gelé", default=False) created_at = models.DateTimeField(default=timezone.now) # Optional - promo = models.IntegerField(null=True) + PROMO_CHOICES = [(r, r) for r in range(1980, date.today().year + 1)] + promo = models.IntegerField( + choices=PROMO_CHOICES, blank=True, null=True, default=default_promo() + ) nickname = models.CharField("surnom(s)", max_length=255, blank=True, default="") password = models.CharField( max_length=255, unique=True, blank=True, null=True, default=None @@ -73,7 +78,6 @@ class Account(models.Model): class Meta: permissions = ( ("is_team", "Is part of the team"), - ("change_adh", "Gérer les adhésions K-Fêt"), ("manage_perms", "Gérer les permissions K-Fêt"), ("manage_addcosts", "Gérer les majorations"), ("edit_balance_account", "Modifier la balance d'un compte"), @@ -85,7 +89,6 @@ class Account(models.Model): ("can_force_close", "Fermer manuellement la K-Fêt"), ("see_config", "Voir la configuration K-Fêt"), ("change_config", "Modifier la configuration K-Fêt"), - ("access_old_history", "Peut accéder à l'historique plus ancien"), ) def __str__(self): @@ -120,15 +123,17 @@ class Account(models.Model): def is_cof(self): return self.cofprofile.is_cof - @property - def is_kfet(self): - return self.cofprofile.is_kfet - # Propriétés supplémentaires @property def balance_ukf(self): return to_ukf(self.balance, is_cof=self.is_cof) + @property + def real_balance(self): + if hasattr(self, "negative") and self.negative.balance_offset: + return self.balance - self.negative.balance_offset + return self.balance + @property def name(self): return self.user.get_full_name() @@ -145,15 +150,6 @@ class Account(models.Model): def readable(self): return self.trigramme not in [KFET_DELETED_TRIGRAMME, KFET_GENERIC_TRIGRAMME] - @property - def editable(self): - return self.trigramme not in [ - KFET_DELETED_TRIGRAMME, - KFET_GENERIC_TRIGRAMME, - "LIQ", - "#13", - ] - @property def is_team(self): return self.has_perm("kfet.is_team") @@ -170,23 +166,44 @@ class Account(models.Model): return data def perms_to_perform_operation(self, amount): + overdraft_duration_max = kfet_config.overdraft_duration + overdraft_amount_max = kfet_config.overdraft_amount perms = set() + stop_ope = False # Checking is cash account if self.is_cash: # Yes, so no perms and no stop return set(), False - if self.need_comment: perms.add("kfet.perform_commented_operations") - + # Checking is frozen account + if self.is_frozen: + perms.add("kfet.override_frozen_protection") new_balance = self.balance + amount - if new_balance < -kfet_config.overdraft_amount: - return set(), True - if new_balance < 0 and amount < 0: + # Retrieving overdraft amount limit + if ( + hasattr(self, "negative") + and self.negative.authz_overdraft_amount is not None + ): + overdraft_amount = -self.negative.authz_overdraft_amount + else: + overdraft_amount = -overdraft_amount_max + # Retrieving overdraft datetime limit + if ( + hasattr(self, "negative") + and self.negative.authz_overdraft_until is not None + ): + overdraft_until = self.negative.authz_overdraft_until + elif hasattr(self, "negative"): + overdraft_until = self.negative.start + overdraft_duration_max + else: + overdraft_until = timezone.now() + overdraft_duration_max + # Checking it doesn't break 1 rule + if new_balance < overdraft_amount or timezone.now() > overdraft_until: + stop_ope = True perms.add("kfet.perform_negative_operations") - - return perms, False + return perms, stop_ope # Surcharge Méthode save() avec gestions de User et CofProfile # Args: @@ -195,6 +212,7 @@ class Account(models.Model): # - Enregistre User, CofProfile à partir de "data" # - Enregistre Account def save(self, data={}, *args, **kwargs): + if self.pk and data: # Account update @@ -247,64 +265,30 @@ class Account(models.Model): self.password = hash_password(clear_password) def update_negative(self): - if self.balance < 0: - # On met à jour le début de négatif seulement si la fin du négatif précédent - # est "vieille" - if ( - hasattr(self, "negative") - and self.negative.end is not None - and timezone.now() > self.negative.end + kfet_config.cancel_duration - ): + if self.real_balance < 0: + if hasattr(self, "negative") and not self.negative.start: self.negative.start = timezone.now() - self.negative.end = None self.negative.save() elif not hasattr(self, "negative"): self.negative = AccountNegative.objects.create( account=self, start=timezone.now() ) elif hasattr(self, "negative"): - if self.negative.end is None: - self.negative.end = timezone.now() - self.negative.save() - elif timezone.now() > self.negative.end + kfet_config.cancel_duration: - # Idem: on supprime le négatif après une légère période - # Nécessaire pour se souvenir du négatif après une charge annulée - self.negative.delete() + # self.real_balance >= 0 + balance_offset = self.negative.balance_offset + if balance_offset: + ( + Account.objects.filter(pk=self.pk).update( + balance=F("balance") - balance_offset + ) + ) + self.refresh_from_db() + self.negative.delete() class UserHasAccount(Exception): def __init__(self, trigramme): self.trigramme = trigramme - def send_creation_email(self): - """ - Envoie un mail à la création du trigramme. - """ - mail_data = settings.MAIL_DATA["kfet"] - - email = EmailMessage( - subject="Création d'un trigramme", - body=loader.render_to_string( - "kfet/mails/creation_trigramme.txt", - context={ - "account": self, - "site": Site.objects.get_current(), - "url_read": reverse("kfet.account.read", args=(self.trigramme,)), - "url_update": reverse( - "kfet.account.update", args=(self.trigramme,) - ), - "url_delete": reverse( - "kfet.account.delete", args=(self.trigramme,) - ), - }, - ), - from_email=mail_data["FROM"], - to=[self.email], - reply_to=[mail_data["REPLYTO"]], - ) - - # On envoie le mail - email.send() - def get_deleted_account(): return Account.objects.get(trigramme=KFET_DELETED_TRIGRAMME) @@ -324,39 +308,34 @@ class AccountNegative(models.Model): Account, on_delete=models.CASCADE, related_name="negative" ) start = models.DateTimeField(blank=True, null=True, default=None) - end = models.DateTimeField(blank=True, null=True, default=None) - last_rappel = models.DateTimeField("Mail de rappel envoyé", blank=True, null=True) + balance_offset = models.DecimalField( + "décalage de balance", + help_text="Montant non compris dans l'autorisation de négatif", + max_digits=6, + decimal_places=2, + blank=True, + null=True, + default=None, + ) + authz_overdraft_amount = models.DecimalField( + "négatif autorisé", + max_digits=6, + decimal_places=2, + blank=True, + null=True, + default=None, + ) + authz_overdraft_until = models.DateTimeField( + "expiration du négatif", blank=True, null=True, default=None + ) + comment = models.CharField("commentaire", max_length=255, blank=True) class Meta: permissions = (("view_negs", "Voir la liste des négatifs"),) - def send_rappel(self): - """ - Envoie un mail de rappel signalant que la personne est en négatif. - """ - mail_data = settings.MAIL_DATA["kfet"] - - email = EmailMessage( - subject="Compte K-Psul négatif", - body=loader.render_to_string( - "kfet/mails/rappel.txt", - context={ - "account": self.account, - "neg_amount": -self.account.balance, - "start_date": self.start, - }, - ), - from_email=mail_data["FROM"], - to=[self.account.email], - reply_to=[mail_data["REPLYTO"]], - ) - - # On envoie le mail - email.send() - - # On enregistre le fait que l'envoi a bien eu lieu - self.last_rappel = timezone.now() - self.save() + @property + def until_default(self): + return self.start + kfet_config.overdraft_duration class CheckoutQuerySet(models.QuerySet): @@ -499,7 +478,6 @@ class ArticleCategory(models.Model): class Article(models.Model): name = models.CharField("nom", max_length=45) is_sold = models.BooleanField("en vente", default=True) - no_exte = models.BooleanField("Réservé au adhérent⋅e⋅s", default=False) hidden = models.BooleanField( "caché", default=False, @@ -688,7 +666,6 @@ class OperationGroup(models.Model): at = models.DateTimeField(default=timezone.now) amount = models.DecimalField(max_digits=6, decimal_places=2, default=0) is_cof = models.BooleanField(default=False) - is_kfet = models.BooleanField(default=False) # Optional comment = models.CharField(max_length=255, blank=True, default="") valid_by = models.ForeignKey( @@ -761,10 +738,7 @@ class Operation(models.Model): permissions = ( ("perform_deposit", "Effectuer une charge"), ("perform_negative_operations", "Enregistrer des commandes en négatif"), - ( - "perform_liq_reserved", - "Effectuer une opération réservé aux adhérent⋅e⋅s sur LIQ", - ), + ("override_frozen_protection", "Forcer le gel d'un compte"), ("cancel_old_operations", "Annuler des commandes non récentes"), ( "perform_commented_operations", diff --git a/kfet/open/consumers.py b/kfet/open/consumers.py index 4b21bef2..8b800c76 100644 --- a/kfet/open/consumers.py +++ b/kfet/open/consumers.py @@ -1,5 +1,3 @@ -from asgiref.sync import sync_to_async - from ..decorators import kfet_is_team from ..utils import DjangoJsonWebsocketConsumer, PermConsumerMixin from .open import kfet_open @@ -14,15 +12,13 @@ class OpenKfetConsumer(PermConsumerMixin, DjangoJsonWebsocketConsumer): """ - async def open_status(self, event): - await self.send_json(event) + 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"] - async def connect(self): + def connect(self, message, *args, **kwargs): """Send current status on connect.""" - await super().connect() - - group = "team" if await sync_to_async(kfet_is_team)(self.user) else "base" - - await self.channel_layer.group_add(f"kfet.open.{group}", self.channel_name) - - await self.send_json(kfet_open.export(self.user)) + super().connect(message, *args, **kwargs) + self.send(kfet_open.export(message.user)) diff --git a/kfet/open/open.py b/kfet/open/open.py index ec391de6..d0e0c901 100644 --- a/kfet/open/open.py +++ b/kfet/open/open.py @@ -1,6 +1,5 @@ from datetime import timedelta -from channels.layers import get_channel_layer from django.utils import timezone from ..decorators import kfet_is_team @@ -78,7 +77,7 @@ class OpenKfet(CachedMixin, object): """ status = self.status() - base = {"status": status, "type": "open.status"} + base = {"status": status} restrict = { "admin_status": self.admin_status(status), "force_close": self.force_close, @@ -96,14 +95,13 @@ class OpenKfet(CachedMixin, object): base, team = self._export() return team if kfet_is_team(user) else base - async def send_ws(self): + def send_ws(self): """Send internal state to websocket channels.""" + from .consumers import OpenKfetConsumer + base, team = self._export() - - channel_layer = get_channel_layer() - - await channel_layer.group_send("kfet.open.base", base) - await channel_layer.group_send("kfet.open.team", team) + OpenKfetConsumer.group_send("kfet.open.base", base) + OpenKfetConsumer.group_send("kfet.open.team", team) kfet_open = OpenKfet() diff --git a/kfet/open/routing.py b/kfet/open/routing.py index 9d205638..811ae56e 100644 --- a/kfet/open/routing.py +++ b/kfet/open/routing.py @@ -1,10 +1,5 @@ -from channels.routing import URLRouter -from django.urls import path +from channels.routing import route_class -from .consumers import OpenKfetConsumer +from . import consumers -OpenRouter = URLRouter( - [ - path("", OpenKfetConsumer.as_asgi()), - ] -) +routing = [route_class(consumers.OpenKfetConsumer)] diff --git a/kfet/open/static/kfetopen/kfet-open.js b/kfet/open/static/kfetopen/kfet-open.js index 9037e1fe..74f18d8a 100644 --- a/kfet/open/static/kfetopen/kfet-open.js +++ b/kfet/open/static/kfetopen/kfet-open.js @@ -2,23 +2,21 @@ var OpenWS = new KfetWebsocket({ relative_url: "open/" }); -var OpenKfet = function (force_close_url, admin) { - that = this; - $( function() { - that.force_close_url = force_close_url; - that.admin = admin; +var OpenKfet = function(force_close_url, admin) { + this.force_close_url = force_close_url; + this.admin = admin; - that.status = that.UNKNOWN; - that.dom = { - status_text: $('.kfetopen .status-text'), - force_close_btn: $('.kfetopen .force-close-btn'), - warning: $('.kfetopen .warning') - } + 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) ); - that.dom.force_close_btn.click(() => that.toggle_force_close()); - setInterval(() => that.refresh(), that.refresh_interval * 1000); - OpenWS.add_handler(data => that.refresh(data)); - }); }; OpenKfet.prototype = { @@ -51,13 +49,11 @@ OpenKfet.prototype = { deactivate: "Réouvrir la K-Fêt" }, - callbacks: [ ], - get is_recent() { return this.last_update && moment().diff(this.last_update, 'minute') <= this.time_unknown; }, - refresh: function (data) { + refresh: function(data) { if (data) { $.extend(this, data); this.last_update = moment(); @@ -67,15 +63,12 @@ OpenKfet.prototype = { this.refresh_dom(); }, - refresh_dom: function () { + refresh_dom: function() { let status = this.status; this.clear_class(); this.add_class(status); this.dom.status_text.html(this.status_text[status]); - for (callback of this.callbacks) { - callback(status); - } // admin specific if (this.admin) { @@ -90,35 +83,31 @@ OpenKfet.prototype = { } }, - toggle_force_close: function (password) { + toggle_force_close: function(password) { $.post({ - context: this, url: this.force_close_url, - data: { force_close: !this.force_close }, + data: {force_close: !this.force_close}, beforeSend: function ($xhr) { $xhr.setRequestHeader("X-CSRFToken", csrftoken); if (password !== undefined) $xhr.setRequestHeader("KFetPassword", password); } - }).fail(function ($xhr) { + }) + .fail(function($xhr) { switch ($xhr.status) { - case 403: - requestAuth({ 'errors': {} }, (password) => this.toggle_force_close(password)); - break; + case 403: + requestAuth({'errors': {}}, this.toggle_force_close); + break; } }); }, - clear_class: function () { + 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) { + add_class: function(status) { $(this.target).addClass(this.class_prefix + status); - }, - - add_callback: function (callback) { - this.callbacks.push(callback); } }; diff --git a/kfet/open/templates/kfetopen/indicator.html b/kfet/open/templates/kfetopen/indicator.html deleted file mode 100644 index 90ec0d85..00000000 --- a/kfet/open/templates/kfetopen/indicator.html +++ /dev/null @@ -1,89 +0,0 @@ -{% extends 'kfet/base.html' %} -{% load static %} - -{% block extra_head %} - - -{% endblock %} - -{% block title %}Indicateur{% endblock %} - -{% block header %}{% endblock %} -{% block help %}{% endblock %} - -{% block content %} - -
    -

    Non défini

    -

    Fermé

    -

    Ouvert

    -
    - -{% endblock %} diff --git a/kfet/open/templates/kfetopen/init.html b/kfet/open/templates/kfetopen/init.html index a8161d87..3834b32a 100644 --- a/kfet/open/templates/kfetopen/init.html +++ b/kfet/open/templates/kfetopen/init.html @@ -4,8 +4,10 @@ diff --git a/kfet/open/tests.py b/kfet/open/tests.py index 3eabcc02..0d527644 100644 --- a/kfet/open/tests.py +++ b/kfet/open/tests.py @@ -1,29 +1,24 @@ +import json import random from datetime import timedelta from unittest import mock -from asgiref.sync import async_to_sync -from channels.auth import AuthMiddlewareStack -from channels.consumer import get_channel_layer -from channels.testing import WebsocketCommunicator +from channels.channel import Group +from channels.test import ChannelTestCase, WSClient from django.contrib.auth.models import AnonymousUser, Permission, User -from django.test import Client, TestCase +from django.test import Client from django.utils import timezone from . import OpenKfet from .consumers import OpenKfetConsumer -def ws_communicator(cls, path: str, headers=[]): - return WebsocketCommunicator(AuthMiddlewareStack(cls.as_asgi()), path, headers) - - -class OpenKfetTest(TestCase): +class OpenKfetTest(ChannelTestCase): """OpenKfet object unit-tests suite.""" def setUp(self): self.kfet_open = OpenKfet( - cache_prefix="test_kfetopen_%s" % random.randrange(2**20) + cache_prefix="test_kfetopen_%s" % random.randrange(2 ** 20) ) self.addCleanup(self.kfet_open.clear_cache) @@ -84,7 +79,7 @@ class OpenKfetTest(TestCase): def test_export_user(self): """Export is limited for an anonymous user.""" export = self.kfet_open.export(AnonymousUser()) - self.assertSetEqual(set(["status", "type"]), set(export)) + self.assertSetEqual(set(["status"]), set(export)) def test_export_team(self): """Export all values for a team member.""" @@ -94,32 +89,24 @@ class OpenKfetTest(TestCase): ) user.user_permissions.add(is_team) export = self.kfet_open.export(user) - self.assertSetEqual( - set(["status", "admin_status", "force_close", "type"]), set(export) - ) + self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(export)) - async def test_send_ws(self): - channel_layer = get_channel_layer() - base_channel = await channel_layer.new_channel() - team_channel = await channel_layer.new_channel() + def test_send_ws(self): + Group("kfet.open.base").add("test.open.base") + Group("kfet.open.team").add("test.open.team") - await channel_layer.group_add("kfet.open.base", base_channel) - await channel_layer.group_add("kfet.open.team", team_channel) + self.kfet_open.send_ws() - await 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)) - base = await channel_layer.receive(base_channel) - - self.assertSetEqual(set(["status", "type"]), set(base)) - - team = await channel_layer.receive(team_channel) - - self.assertSetEqual( - set(["status", "admin_status", "force_close", "type"]), set(team) - ) + 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(TestCase): +class OpenKfetViewsTest(ChannelTestCase): """OpenKfet views unit-tests suite.""" def setUp(self): @@ -158,7 +145,7 @@ class OpenKfetViewsTest(TestCase): self.c_a.login(username="admin", password="admin") self.kfet_open = OpenKfet( - cache_prefix="test_kfetopen_%s" % random.randrange(2**20) + cache_prefix="test_kfetopen_%s" % random.randrange(2 ** 20) ) self.addCleanup(self.kfet_open.clear_cache) @@ -190,136 +177,117 @@ class OpenKfetViewsTest(TestCase): self.assertEqual(403, resp.status_code) -class OpenKfetConsumerTest(TestCase): +class OpenKfetConsumerTest(ChannelTestCase): """OpenKfet consumer unit-tests suite.""" - @classmethod - def setUpTestData(cls): + 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()) + + @mock.patch("gestioncof.signals.messages") + def test_team_user(self, mock_messages): + """Team user is added to kfet.open.team group.""" + # setup team user and its client t = User.objects.create_user("team", "", "team") is_team = Permission.objects.get( codename="is_team", content_type__app_label="kfet" ) t.user_permissions.add(is_team) + c = WSClient() + c.force_login(t) - cls.team_user = t - - async def test_standard_user(self): - """Lambda user is added to kfet.open.base group.""" - # setup anonymous client - c = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open") - - connected, _ = await c.connect() - - self.assertTrue(connected) + # connect + c.send_and_consume( + "websocket.connect", path="/ws/k-fet/open", fail_on_none=True + ) # initialization data is replied on connection - message = await c.receive_json_from() - self.assertIsNotNone(message) + self.assertIsNotNone(c.receive()) - # client belongs to the 'kfet.open' group... - channel_layer = get_channel_layer() + # client belongs to the 'kfet.open.admin' group... + OpenKfetConsumer.group_send("kfet.open.team", {"test": "plop"}) + self.assertEqual(c.receive(), {"test": "plop"}) - await channel_layer.group_send( - "kfet.open.base", {"test": "plop", "type": "open.status"} - ) - message = await c.receive_json_from() - - self.assertEqual(message, {"test": "plop"}) - - # ...but not to the 'kfet.open.admin' one - await channel_layer.group_send( - "kfet.open.team", {"test": "plop", "type": "open.status"} - ) - self.assertTrue(await c.receive_nothing()) - - async def test_team_user(self): - """Team user is added to kfet.open.team group.""" - - # On simule l'appartenance de l'user à la team kfet car l'utilisation de - # tests async avec postgres fait tout planter si on modifie la db dans un - # des sous tests. - with mock.patch("gestioncof.signals.messages"), mock.patch( - "kfet.open.consumers.kfet_is_team", return_value=True - ): - c = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open") - - connected, _ = await c.connect() - - channel_layer = get_channel_layer() - - self.assertTrue(connected) - - # initialization data is replied on connection - message = await c.receive_json_from() - self.assertIsNotNone(message) - - # client belongs to the 'kfet.open.team' group... - await channel_layer.group_send( - "kfet.open.team", {"test": "plop", "type": "open.status"} - ) - message = await c.receive_json_from() - - self.assertEqual(message, {"test": "plop"}) - - # ...but not to the 'kfet.open' one - await channel_layer.group_send( - "kfet.open.base", {"test": "plop", "type": "open.status"} - ) - self.assertTrue(await c.receive_nothing()) + # ... but not to the 'kfet.open' one + OpenKfetConsumer.group_send("kfet.open.base", {"test": "plop"}) + self.assertIsNone(c.receive()) -class OpenKfetScenarioTest(TestCase): +class OpenKfetScenarioTest(ChannelTestCase): """OpenKfet functionnal tests suite.""" - @classmethod - def setUpTestData(cls): - # root user - cls.r = User.objects.create_superuser("team", "", "team") + def setUp(self): + # Need this (and here) because of '.login' in setUp + patcher_messages = mock.patch("gestioncof.signals.messages") + patcher_messages.start() + self.addCleanup(patcher_messages.stop) # anonymous client (for views) - cls.c = Client() + self.c = Client() + # anonymous client (for websockets) + self.c_ws = WSClient() - # root client - cls.r_c = Client() - - with mock.patch("gestioncof.signals.messages"): - cls.r_c.login(username="team", password="team") - - def setUp(self): - # Create channels to listen to messages - channel_layer = get_channel_layer() - - self.channel = async_to_sync(channel_layer.new_channel)() - self.team_channel = async_to_sync(channel_layer.new_channel)() - - async_to_sync(channel_layer.group_add)("kfet.open.base", self.channel) - async_to_sync(channel_layer.group_add)("kfet.open.team", self.team_channel) - - self.receive_msg = lambda c: async_to_sync(channel_layer.receive)(c) + # 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) self.kfet_open = OpenKfet( - cache_prefix="test_kfetopen_%s" % random.randrange(2**20) + cache_prefix="test_kfetopen_%s" % random.randrange(2 ** 20) ) self.addCleanup(self.kfet_open.clear_cache) - async def ws_connect(self, ws_communicator): - c, _ = await ws_communicator.connect() + 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) - self.assertTrue(c) - return await ws_communicator.receive_json_from() + 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.receive_msg(self.channel) + msg = self.c_ws.receive(json=True) self.assertEqual(OpenKfet.OPENED, msg["status"]) # root user too - msg = self.receive_msg(self.team_channel) + msg = self.r_c_ws.receive(json=True) self.assertEqual(OpenKfet.OPENED, msg["status"]) self.assertEqual(OpenKfet.OPENED, msg["admin_status"]) @@ -327,11 +295,11 @@ class OpenKfetScenarioTest(TestCase): self.r_c.post("/k-fet/open/force_close", {"force_close": True}) # so anonymous user see it's closed - msg = self.receive_msg(self.channel) + msg = self.c_ws.receive(json=True) self.assertEqual(OpenKfet.CLOSED, msg["status"]) # root user too - msg = self.receive_msg(self.team_channel) + 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"]) @@ -342,42 +310,20 @@ class OpenKfetScenarioTest(TestCase): self.kfet_open.raw_open = True self.kfet_open.force_close = True - async_to_sync(OpenKfet().send_ws)() - - msg = self.receive_msg(self.channel) + msg = self.ws_connect(self.c_ws) self.assertEqual(OpenKfet.CLOSED, msg["status"]) - msg = self.receive_msg(self.team_channel) + 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.receive_msg(self.channel) + msg = self.c_ws.receive(json=True) self.assertEqual(OpenKfet.OPENED, msg["status"]) - msg = self.receive_msg(self.team_channel) + 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"]) - - async def test_scenario_3(self): - """Clients connect.""" - # anonymous client (for websockets) - self.c_ws = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open") - - # test for anonymous user - msg = await self.ws_connect(self.c_ws) - self.assertSetEqual(set(["status"]), set(msg)) - - # test for root user - with mock.patch( - "kfet.open.consumers.kfet_is_team", return_value=True - ), mock.patch("kfet.open.open.kfet_is_team", return_value=True): - self.r_c_ws = ws_communicator(OpenKfetConsumer, "/ws/k-fet/open") - - msg = await self.ws_connect(self.r_c_ws) - self.assertSetEqual( - set(["status", "admin_status", "force_close"]), set(msg) - ) diff --git a/kfet/open/urls.py b/kfet/open/urls.py index 28db8618..8510adc4 100644 --- a/kfet/open/urls.py +++ b/kfet/open/urls.py @@ -5,5 +5,4 @@ from . import views urlpatterns = [ path("raw_open", views.raw_open, name="kfet.open.edit_raw_open"), path("force_close", views.force_close, name="kfet.open.edit_force_close"), - path("", views.indicator, name="kfet.open.indicator"), ] diff --git a/kfet/open/views.py b/kfet/open/views.py index 1a044c68..49b91f4a 100644 --- a/kfet/open/views.py +++ b/kfet/open/views.py @@ -1,9 +1,7 @@ -from asgiref.sync import async_to_sync from django.conf import settings from django.contrib.auth.decorators import permission_required from django.core.exceptions import PermissionDenied from django.http import HttpResponse -from django.shortcuts import render from django.views.decorators.csrf import csrf_exempt from django.views.decorators.http import require_POST @@ -20,7 +18,7 @@ def raw_open(request): raise PermissionDenied raw_open = request.POST.get("raw_open") in TRUE_STR kfet_open.raw_open = raw_open - async_to_sync(kfet_open.send_ws)() + kfet_open.send_ws() return HttpResponse() @@ -29,12 +27,5 @@ def raw_open(request): def force_close(request): force_close = request.POST.get("force_close") in TRUE_STR kfet_open.force_close = force_close - async_to_sync(kfet_open.send_ws)() + kfet_open.send_ws() return HttpResponse() - - -def indicator(request): - return render( - request, - "kfetopen/indicator.html", - ) diff --git a/kfet/routing.py b/kfet/routing.py index a015eebc..ceafca06 100644 --- a/kfet/routing.py +++ b/kfet/routing.py @@ -1,13 +1,8 @@ -from channels.routing import URLRouter -from django.urls import path +from channels.routing import include, route_class -from kfet.open.routing import OpenRouter +from . import consumers -from .consumers import KPsul - -KFRouter = URLRouter( - [ - path("k-psul/", KPsul.as_asgi()), - path("open", OpenRouter), - ] -) +routing = [ + route_class(consumers.KPsul, path=r"^/k-psul/$"), + include("kfet.open.routing.routing", path=r"^/open"), +] diff --git a/kfet/static/kfet/css/base/main.css b/kfet/static/kfet/css/base/main.css index f98ae0f4..2ebc90d8 100644 --- a/kfet/static/kfet/css/base/main.css +++ b/kfet/static/kfet/css/base/main.css @@ -1,40 +1,3 @@ -/* Navbar hacks -------------------- */ - -.visible-xl, -.visible-xl-block, -.visible-xl-inline, -.visible-xl-inline-block { - display: none !important; -} - -@media (min-width: 1415px) { - .visible-xl { - display: block !important; - } - table.visible-xl { - display: table !important; - } - tr.visible-xl { - display: table-row !important; - } - th.visible-xl, - td.visible-xl { - display: table-cell !important; - } - - .visible-xl-block { - display: block !important; - } - - .visible-xl-inline { - display: inline !important; - } - - .visible-xl-inline-block { - display: inline-block !important; - } -} - /* Global layout ------------------- */ .main-col, .fixed-col { diff --git a/kfet/static/kfet/css/base/misc.css b/kfet/static/kfet/css/base/misc.css index 97c0ee7e..fab1a33a 100644 --- a/kfet/static/kfet/css/base/misc.css +++ b/kfet/static/kfet/css/base/misc.css @@ -61,7 +61,7 @@ ul { } .table td.no-padding { - padding:0 !important; + padding:0; } .table thead { diff --git a/kfet/static/kfet/css/history.css b/kfet/static/kfet/css/history.css index 437fcd71..9cd4cd28 100644 --- a/kfet/static/kfet/css/history.css +++ b/kfet/static/kfet/css/history.css @@ -20,7 +20,7 @@ z-index:10; } -#history .group { +#history .opegroup { height:30px; line-height:30px; background-color: #c63b52; @@ -30,29 +30,29 @@ overflow:auto; } -#history .group .time { +#history .opegroup .time { width:70px; } -#history .group .trigramme { +#history .opegroup .trigramme { width:55px; text-align:right; } -#history .group .amount { +#history .opegroup .amount { text-align:right; width:90px; } -#history .group .valid_by { +#history .opegroup .valid_by { padding-left:20px } -#history .group .comment { +#history .opegroup .comment { padding-left:20px; } -#history .entry { +#history .ope { position:relative; height:25px; line-height:24px; @@ -61,38 +61,38 @@ overflow:auto; } -#history .entry .amount { +#history .ope .amount { width:50px; text-align:right; } -#history .entry .infos1 { +#history .ope .infos1 { width:80px; text-align:right; } -#history .entry .infos2 { +#history .ope .infos2 { padding-left:15px; } -#history .entry .addcost { +#history .ope .addcost { padding-left:20px; } -#history .entry .canceled { +#history .ope .canceled { padding-left:20px; } -#history div.entry.ui-selected, #history div.entry.ui-selecting { +#history div.ope.ui-selected, #history div.ope.ui-selecting { background-color:rgba(200,16,46,0.6); color:#FFF; } -#history .entry.canceled { +#history .ope.canceled, #history .transfer.canceled { color:#444; } -#history .entry.canceled::before { +#history .ope.canceled::before, #history.transfer.canceled::before { position: absolute; content: ' '; width:100%; @@ -101,15 +101,10 @@ border-top: 1px solid rgba(200,16,46,0.5); } -#history .group .infos { - text-align:center; - width:145px; +#history .transfer .amount { + width:80px; } -#history .entry .glyphicon { - padding-left:15px; -} - -#history-form .form-group { - position: relative; +#history .transfer .from_acc { + padding-left:10px; } diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index d81a5074..fdb86aff 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -41,19 +41,10 @@ } .frozen-account { - background:#000FBA; + background:#5072e0; color:#fff; } -.frozen-account .btn-default { - color: #aaa; -} - -.frozen-account .btn-default:hover, .frozen-account .btn-default.focus, -.frozen-account .btn-default:focus { - color: #ed2545; -} - .main .table a:not(.btn) { color: inherit; @@ -147,7 +138,7 @@ * Specific account create */ -.highlight { +.highlight_autocomplete { font-weight:bold; text-decoration:underline; } @@ -168,7 +159,7 @@ background:rgba(255,255,255,0.9); } -#search_results ul li.autocomplete-header { +#search_results ul li.user_category { font-weight:bold; background:#c8102e; color:#fff; @@ -187,25 +178,13 @@ text-decoration:none; } -#search_results ul li span.autocomplete-item { +#search_results ul li span.text { display:block; padding:5px 20px; } /* Account autocomplete window */ -.jconfirm #search_autocomplete { - margin-bottom: 0; -} - -#account_results { - left:0 !important; -} - -#account_results ul li.autocomplete-header { - display:none; -} - #account_results ul { list-style-type:none; background:rgba(255,255,255,0.9); @@ -219,10 +198,6 @@ width:100%; } -li.autocomplete-value { - cursor: pointer; -} - #account_results .hilight { background:rgba(200,16,46,0.9); color:#fff; diff --git a/kfet/static/kfet/css/libs/jconfirm-kfet.css b/kfet/static/kfet/css/libs/jconfirm-kfet.css index 935d4e97..a50e22d6 100644 --- a/kfet/static/kfet/css/libs/jconfirm-kfet.css +++ b/kfet/static/kfet/css/libs/jconfirm-kfet.css @@ -25,9 +25,6 @@ .jconfirm .jconfirm-box .content-pane { border-bottom:1px solid #ddd; margin: 0px !important; - /* fixes whitespace below block - see https://stackoverflow.com/a/5804278 */ - vertical-align: middle; } .jconfirm .jconfirm-box .content { @@ -54,6 +51,7 @@ } .jconfirm .jconfirm-box .buttons { + margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */ padding:0; height:40px; } diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index b7c3fdaf..5ce3c8cd 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -5,12 +5,12 @@ var Account = Backbone.Model.extend({ 'name': '', 'email': '', 'is_cof': '', - 'is_kfet': '', 'promo': '', 'balance': '', 'is_frozen': false, 'departement': '', 'nickname': '', + 'trigramme': '', }, url: function () { @@ -18,9 +18,8 @@ var Account = Backbone.Model.extend({ }, reset: function () { - // Réinitialise les attributs du modèle à leurs défaults, sauf le trigramme qui est bind à l'input. - // On n'utilise pas .clear() car on ne veut pas clear le trigramme. - this.set(this.defaults) + // On ne veut pas trigger un `change` deux fois + this.clear({ silent: true }).set(this.defaults) }, parse: function (resp, options) { @@ -32,37 +31,27 @@ var Account = Backbone.Model.extend({ }, view: function () { - if (this.is_empty_account()) { - view_class = EmptyAccountView - } else if (this.get("trigramme") == 'LIQ') { - view_class = LIQView - } else { - view_class = AccountView - } + view_class = this.get("trigramme") == 'LIQ' ? LIQView : AccountView; return new view_class({ model: this }) }, render: function () { this.view().render(); - }, - - is_empty_account: function () { - return (this.id == 0) - }, + } }) var AccountView = Backbone.View.extend({ el: '#account', + input: '#id_trigramme', buttons: '.buttons', - id_field: "#id_on_acc", props: _.keys(Account.prototype.defaults), get: function (property) { /* If the function this.get_ is defined, we call it ; else we call this.model.. */ - getter_name = `get_${property}`; + getter_name = 'get_' + property; if (_.functions(this).includes(getter_name)) return this[getter_name]() else @@ -70,7 +59,7 @@ var AccountView = Backbone.View.extend({ }, get_is_cof: function () { - return this.model.get("is_cof") ? 'Membre COF' : (this.model.get("is_kfet") ? 'Membre K-Fêt' : 'Non-COF'); + return this.model.get("is_cof") ? 'COF' : 'Non-COF'; }, get_balance: function () { @@ -78,10 +67,8 @@ var AccountView = Backbone.View.extend({ }, attr_data_balance: function () { - // Cette fonction est utilisée uniquement sur un compte valide - - if (this.model.get("is_frozen")) { - return "frozen"; + if (this.model.id == 0) { + return ''; } else if (this.model.get("balance") < 0) { return 'neg'; } else if (this.model.get("balance") <= 5) { @@ -92,9 +79,23 @@ var AccountView = Backbone.View.extend({ }, get_buttons: function () { - var url = django_urls["kfet.account.read"](encodeURIComponent(this.model.get("trigramme"))); + var buttons = ''; + if (this.model.id != 0) { + var url = django_urls["kfet.account.read"](encodeURIComponent(this.model.get("trigramme"))) + buttons += ``; + } else { + var trigramme = this.$(this.input).val().toUpperCase(); + if (isValidTrigramme(trigramme)) { + trigramme = encodeURIComponent(trigramme); + var url_base = django_urls["kfet.account.create"](); + var url = `${url_base}?trigramme=${trigramme}`; + buttons += ``; + } else { + buttons += ''; + } + } - return ``; + return buttons }, render: function () { @@ -105,7 +106,16 @@ var AccountView = Backbone.View.extend({ this.$el.attr("data-balance", this.attr_data_balance()); this.$(this.buttons).html(this.get_buttons()); - $(this.id_field).val(this.get("id")); + }, + + reset: function () { + for (let prop of this.props) { + var selector = "#account-" + prop; + this.$(selector).text(''); + } + + this.$el.attr("data-balance", ''); + this.$(this.buttons).html(this.get_buttons()); }, }) @@ -119,28 +129,3 @@ var LIQView = AccountView.extend({ } }) -var EmptyAccountView = AccountView.extend({ - get: function () { - return ''; - }, - - attr_data_balance: function () { - return ''; - }, - - get_buttons: function () { - /* Léger changement de fonctionnement : - on affiche *toujours* le bouton de recherche si - le compte est invalide */ - buttons = ''; - trigramme = this.model.get("trigramme") - if (trigramme.is_valid_trigramme()) { - trigramme = encodeURIComponent(trigramme); - var url_base = django_urls["kfet.account.create"](); - var url = `${url_base}?trigramme=${trigramme}`; - buttons += ``; - } - - return buttons - } -}) diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 57829a10..a7372b87 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -2,59 +2,31 @@ function dateUTCToParis(date) { return moment.tz(date, 'UTC').tz('Europe/Paris'); } -// TODO : classifier (later) function KHistory(options = {}) { $.extend(this, KHistory.default_options, options); this.$container = $(this.container); - this.$container.selectable({ - filter: 'div.group, div.entry', - selected: function (e, ui) { - $(ui.selected).each(function () { - if ($(this).hasClass('group')) { - var id = $(this).data('id'); - $(this).siblings('.entry').filter(function () { - return $(this).data('group_id') == id - }).addClass('ui-selected'); - } - }); - }, - }); - this.reset = function () { this.$container.html(''); }; - this.add_history_group = function (group) { - var $day = this._get_or_create_day(group['at']); - var $group = this._group_html(group); + this.addOpeGroup = function (opegroup) { + var $day = this._getOrCreateDay(opegroup['at']); + var $opegroup = this._opeGroupHtml(opegroup); - $day.after($group); + $day.after($opegroup); - var trigramme = group['on_acc_trigramme']; - var is_cof = group['is_cof']; - var type = group['type'] - // TODO : simplifier ça ? - switch (type) { - case 'operation': - for (let ope of group['entries']) { - var $ope = this._ope_html(ope, is_cof, trigramme); - $ope.data('group_id', group['id']); - $group.after($ope); - } - break; - case 'transfer': - for (let transfer of group['entries']) { - var $transfer = this._transfer_html(transfer); - $transfer.data('group_id', group['id']); - $group.after($transfer); - } - break; + var trigramme = opegroup['on_acc_trigramme']; + var is_cof = opegroup['is_cof']; + for (var i = 0; i < opegroup['opes'].length; i++) { + var $ope = this._opeHtml(opegroup['opes'][i], is_cof, trigramme); + $ope.data('opegroup', opegroup['id']); + $opegroup.after($ope); } } - this._ope_html = function (ope, is_cof, trigramme) { + this._opeHtml = function (ope, is_cof, trigramme) { var $ope_html = $(this.template_ope); var parsed_amount = parseFloat(ope['amount']); var amount = amountDisplay(parsed_amount, is_cof, trigramme); @@ -82,8 +54,7 @@ function KHistory(options = {}) { } $ope_html - .data('type', 'operation') - .data('id', ope['id']) + .data('ope', ope['id']) .find('.amount').text(amount).end() .find('.infos1').text(infos1).end() .find('.infos2').text(infos2).end(); @@ -91,89 +62,54 @@ function KHistory(options = {}) { var addcost_for = ope['addcost_for__trigramme']; if (addcost_for) { var addcost_amount = parseFloat(ope['addcost_amount']); - $ope_html.find('.addcost').text('(' + amountDisplay(addcost_amount, is_cof) + ' UKF pour ' + addcost_for + ')'); + $ope_html.find('.addcost').text('(' + amountDisplay(addcost_amount, is_cof) + 'UKF pour ' + addcost_for + ')'); } if (ope['canceled_at']) - this.cancel_entry(ope, $ope_html); + this.cancelOpe(ope, $ope_html); return $ope_html; } - this._transfer_html = function (transfer) { - var $transfer_html = $(this.template_transfer); - var parsed_amount = parseFloat(transfer['amount']); - var amount = parsed_amount.toFixed(2) + '€'; - - $transfer_html - .data('type', 'transfer') - .data('id', transfer['id']) - .find('.amount').text(amount).end() - .find('.infos1').text(transfer['from_acc']).end() - .find('.infos2').text(transfer['to_acc']).end(); - - if (transfer['canceled_at']) - this.cancel_entry(transfer, $transfer_html); - - return $transfer_html; - } - - - this.cancel_entry = function (entry, $entry = null) { - if (!$entry) - $entry = this.find_entry(entry["id"], entry["type"]); + this.cancelOpe = function (ope, $ope = null) { + if (!$ope) + $ope = this.findOpe(ope['id']); var cancel = 'Annulé'; - var canceled_at = dateUTCToParis(entry['canceled_at']); - if (entry['canceled_by__trigramme']) - cancel += ' par ' + entry['canceled_by__trigramme']; + var canceled_at = dateUTCToParis(ope['canceled_at']); + if (ope['canceled_by__trigramme']) + cancel += ' par ' + ope['canceled_by__trigramme']; cancel += ' le ' + canceled_at.format('DD/MM/YY à HH:mm:ss'); - $entry.addClass('canceled').find('.canceled').text(cancel); + $ope.addClass('canceled').find('.canceled').text(cancel); } - this._group_html = function (group) { - var type = group['type']; + this._opeGroupHtml = function (opegroup) { + var $opegroup_html = $(this.template_opegroup); + var at = dateUTCToParis(opegroup['at']).format('HH:mm:ss'); + var trigramme = opegroup['on_acc__trigramme']; + var amount = amountDisplay( + parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme); + var comment = opegroup['comment'] || ''; - switch (type) { - case 'operation': - var $group_html = $(this.template_opegroup); - var trigramme = group['on_acc__trigramme']; - var amount = amountDisplay( - parseFloat(group['amount']), group['is_cof'], trigramme); - break; - case 'transfer': - var $group_html = $(this.template_transfergroup); - $group_html.find('.infos').text('Transferts').end() - var trigramme = ''; - var amount = ''; - break; - } - - - var at = dateUTCToParis(group['at']).format('HH:mm:ss'); - var comment = group['comment'] || ''; - - $group_html - .data('type', type) - .data('id', group['id']) + $opegroup_html + .data('opegroup', opegroup['id']) .find('.time').text(at).end() .find('.amount').text(amount).end() .find('.comment').text(comment).end() .find('.trigramme').text(trigramme).end(); if (!this.display_trigramme) - $group_html.find('.trigramme').remove(); - $group_html.find('.info').remove(); + $opegroup_html.find('.trigramme').remove(); - if (group['valid_by__trigramme']) - $group_html.find('.valid_by').text('Par ' + group['valid_by__trigramme']); + if (opegroup['valid_by__trigramme']) + $opegroup_html.find('.valid_by').text('Par ' + opegroup['valid_by__trigramme']); - return $group_html; + return $opegroup_html; } - this._get_or_create_day = function (date) { + this._getOrCreateDay = function (date) { var at = dateUTCToParis(date); var at_ser = at.format('YYYY-MM-DD'); var $day = this.$container.find('.day').filter(function () { @@ -182,127 +118,35 @@ function KHistory(options = {}) { if ($day.length == 1) return $day; var $day = $(this.template_day).prependTo(this.$container); - return $day.data('date', at_ser).text(at.format('D MMMM YYYY')); + return $day.data('date', at_ser).text(at.format('D MMMM')); } - this.find_group = function (id, type = "operation") { - return this.$container.find('.group').filter(function () { - return ($(this).data('id') == id && $(this).data("type") == type) + this.findOpeGroup = function (id) { + return this.$container.find('.opegroup').filter(function () { + return $(this).data('opegroup') == id }); } - this.find_entry = function (id, type = 'operation') { - return this.$container.find('.entry').filter(function () { - return ($(this).data('id') == id && $(this).data('type') == type) + this.findOpe = function (id) { + return this.$container.find('.ope').filter(function () { + return $(this).data('ope') == id }); } - this.update_opegroup = function (group, type = "operation") { - var $group = this.find_group(group['id'], type); - var trigramme = $group.find('.trigramme').text(); + this.cancelOpeGroup = function (opegroup) { + var $opegroup = this.findOpeGroup(opegroup['id']); + var trigramme = $opegroup.find('.trigramme').text(); var amount = amountDisplay( - parseFloat(group['amount']), group['is_cof'], trigramme); - $group.find('.amount').text(amount); + parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme); + $opegroup.find('.amount').text(amount); } - this.fetch = function (fetch_options = {}) { - if (typeof (fetch_options) == "string") - data = fetch_options - else - data = $.extend({}, this.fetch_options, fetch_options); - - return $.ajax({ - context: this, - dataType: "json", - url: django_urls["kfet.history.json"](), - method: "GET", - data: data, - }).done(function (data) { - for (let group of data['groups']) { - this.add_history_group(group); - } - }); - } - - this._cancel = function (type, opes, password = "") { - if (window.lock == 1) - return false - window.lock = 1; - var that = this; - return $.ajax({ - dataType: "json", - url: django_urls[`kfet.${type}s.cancel`](), - method: "POST", - data: opes, - beforeSend: function ($xhr) { - $xhr.setRequestHeader("X-CSRFToken", csrftoken); - if (password != '') - $xhr.setRequestHeader("KFetPassword", password); - }, - - }).done(function (data) { - window.lock = 0; - that.$container.find('.ui-selected').removeClass('ui-selected'); - for (let entry of data["canceled"]) { - entry["type"] = type; - that.cancel_entry(entry); - } - if (type == "operation") { - for (let opegroup of (data["opegroups_to_update"] || [])) { - that.update_opegroup(opegroup) - } - } - }).fail(function ($xhr) { - var data = $xhr.responseJSON; - switch ($xhr.status) { - case 403: - requestAuth(data, function (password) { - that._cancel(type, opes, password); - }); - break; - case 400: - displayErrors(data); - break; - } - window.lock = 0; - }); - } - - this.cancel_selected = function () { - var opes_to_cancel = { - "transfers": [], - "operations": [], - } - this.$container.find('.entry.ui-selected').each(function () { - type = $(this).data("type"); - opes_to_cancel[`${type}s`].push($(this).data("id")); - }); - if (opes_to_cancel["transfers"].length > 0 && opes_to_cancel["operations"].length > 0) { - // Lancer 2 requêtes AJAX et gérer tous les cas d'erreurs possibles est trop complexe - $.alert({ - title: 'Erreur', - content: "Impossible de supprimer des transferts et des opérations en même temps !", - backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', - keyboardEnabled: true, - }); - } else if (opes_to_cancel["transfers"].length > 0) { - delete opes_to_cancel["operations"]; - this._cancel("transfer", opes_to_cancel); - } else if (opes_to_cancel["operations"].length > 0) { - delete opes_to_cancel["transfers"]; - this._cancel("operation", opes_to_cancel); - } - } } KHistory.default_options = { container: '#history', template_day: '
    ', - template_opegroup: '
    ', - template_transfergroup: '
    ', - template_ope: '
    ', - template_transfer: '
    ', + template_opegroup: '
    ', + template_ope: '
    ', display_trigramme: true, } diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index dbab8937..1002fc32 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -1,17 +1,3 @@ -/* - * Fonctions d'aide à la gestion de trigrammes - */ - -String.prototype.format_trigramme = function () { - return _.toArray(this.toUpperCase()).splice(0,3).join(''); -} - -String.prototype.is_valid_trigramme = function () { - var arr = _.toArray(this); - return arr && arr.length == 3; -} - - /** * CSRF Token */ @@ -28,7 +14,7 @@ function csrfSafeMethod(method) { } $.ajaxSetup({ - beforeSend: function (xhr, settings) { + beforeSend: function(xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } @@ -37,7 +23,7 @@ $.ajaxSetup({ function add_csrf_form($form) { $form.append( - $('', { 'name': 'csrfmiddlewaretoken', 'value': csrftoken }) + $('', {'name': 'csrfmiddlewaretoken', 'value': csrftoken}) ); } @@ -78,9 +64,9 @@ class KfetWebsocket { listen() { var that = this; - this.socket = new ReconnectingWebSocket(this.url, [], { minReconnectionDelay: 100 }); + this.socket = new ReconnectingWebSocket(this.url); - this.socket.onmessage = function (e) { + this.socket.onmessage = function(e) { var data = $.extend({}, that.default_msg, JSON.parse(e.data)); for (let handler of that.handlers) { handler(data); @@ -91,83 +77,119 @@ class KfetWebsocket { var OperationWebSocket = new KfetWebsocket({ 'relative_url': 'k-psul/', - 'default_msg': { 'opegroups': [], 'opes': [], 'checkouts': [], 'articles': [] }, + 'default_msg': {'opegroups':[],'opes':[],'checkouts':[],'articles':[]}, }); -function amountDisplay(amount, is_cof = false, tri = '') { +function amountDisplay(amount, is_cof=false, tri='') { if (tri == 'LIQ') - return (- amount).toFixed(2) + '€'; + return (- amount).toFixed(2) +'€'; return amountToUKF(amount, is_cof); } -function amountToUKF(amount, is_cof = false, account = false) { - var rounding = account ? Math.floor : Math.round; +function amountToUKF(amount, is_cof=false, account=false) { + var rounding = account ? Math.floor : Math.round ; var coef_cof = is_cof ? 1 + settings['subvention_cof'] / 100 : 1; return rounding(amount * coef_cof * 10); } -function getErrorsHtml(data, is_error = true) { - if (is_error) { - data = data.map(error => error.message) - } +function isValidTrigramme(trigramme) { + var pattern = /^[^a-z]{3}$/; + return trigramme.match(pattern); +} - var content = is_error ? "Général :" : "Permissions manquantes :"; - content += "
      "; - for (const message of data) { - content += '
    • ' + message + '
    • '; +function getErrorsHtml(data) { + var content = ''; + if (!data) + return "L'utilisateur n'est pas dans l'équipe"; + if ('operation_group' in data['errors']) { + content += 'Général'; + content += '
        '; + if (data['errors']['operation_group'].indexOf('on_acc') != -1) + content += '
      • Pas de compte sélectionné
      • '; + if (data['errors']['operation_group'].indexOf('checkout') != -1) + content += '
      • Pas de caisse sélectionnée
      • '; + content += '
      '; + } + if ('missing_perms' in data['errors']) { + content += 'Permissions manquantes'; + content += '
        '; + for (var i=0; i'; + content += '
      '; + } + if ('negative' in data['errors']) { + if (window.location.pathname.startsWith('/gestion/')) { + var url_base = '/gestion/k-fet/accounts/'; + } else { + var url_base = '/k-fet/accounts/'; + } + for (var i=0; iAutorisation de négatif requise pour '+data['errors']['negative'][i]+''; + } + } + if ('addcost' in data['errors']) { + content += '
        '; + if (data['errors']['addcost'].indexOf('__all__') != -1) + content += '
      • Compte invalide
      • '; + if (data['errors']['addcost'].indexOf('amount') != -1) + content += '
      • Montant invalide
      • '; + content += '
      '; + } + if ('account' in data['errors']) { + content += 'Général'; + content += '
        '; + content += '
      • Opération invalide sur le compte '+data['errors']['account']+'
      • '; + content += '
      '; } - content += "
    "; - return content; } function requestAuth(data, callback, focus_next = null) { - var content = getErrorsHtml(data["missing_perms"], is_error = false); - content += '
    '; - + var content = getErrorsHtml(data); + content += '
    ', $.confirm({ title: 'Authentification requise', content: content, backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', + animation:'top', + closeAnimation:'bottom', keyboardEnabled: true, - confirm: function () { + confirm: function() { var password = this.$content.find('input').val(); callback(password); }, - onOpen: function () { + onOpen: function() { var that = this; - var capslock = -1; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown - this.$content.find('input').on('keypress', function (e) { + var capslock = -1 ; // 1 -> caps on ; 0 -> caps off ; -1 or 2 -> unknown + this.$content.find('input').on('keypress', function(e) { if (e.keyCode == 13) that.$confirmButton.click(); var s = String.fromCharCode(e.which); - if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey) || //caps on, shift off + if ((s.toUpperCase() === s && s.toLowerCase() !== s && !e.shiftKey)|| //caps on, shift off (s.toUpperCase() !== s && s.toLowerCase() === s && e.shiftKey)) { //caps on, shift on - capslock = 1; - } else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey) || //caps off, shift off - (s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on - capslock = 0; + capslock = 1 ; + } else if ((s.toLowerCase() === s && s.toUpperCase() !== s && !e.shiftKey)|| //caps off, shift off + (s.toLowerCase() !== s && s.toUpperCase() === s && e.shiftKey)) { //caps off, shift on + capslock = 0 ; } if (capslock == 1) - $('.capslock .glyphicon').show(); + $('.capslock .glyphicon').show() ; else if (capslock == 0) - $('.capslock .glyphicon').hide(); + $('.capslock .glyphicon').hide() ; }); // Capslock key is not detected by keypress - this.$content.find('input').on('keydown', function (e) { + this.$content.find('input').on('keydown', function(e) { if (e.which == 20) { - capslock = 1 - capslock; + capslock = 1-capslock ; } if (capslock == 1) - $('.capslock .glyphicon').show(); + $('.capslock .glyphicon').show() ; else if (capslock == 0) - $('.capslock .glyphicon').hide(); + $('.capslock .glyphicon').hide() ; }); }, - onClose: function () { + onClose: function() { if (focus_next) this._lastFocused = focus_next; } @@ -175,18 +197,6 @@ function requestAuth(data, callback, focus_next = null) { }); } -function displayErrors(data) { - const content = getErrorsHtml(data["errors"], is_error = true); - $.alert({ - title: 'Erreurs', - content: content, - backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', - keyboardEnabled: true, - }); -} - /** * Setup jquery-confirm @@ -239,7 +249,7 @@ function submit_url(el) { function registerBoolParser(id, true_str, false_str) { $.tablesorter.addParser({ id: id, - format: function (s) { + format: function(s) { return s.toLowerCase() .replace(true_str, 1) .replace(false_str, 0); @@ -260,9 +270,9 @@ registerBoolParser('article__hidden', 'caché', 'affiché'); $.extend(true, $.tablesorter.defaults, { headerTemplate: '{content} {icon}', - cssIconAsc: 'glyphicon glyphicon-chevron-up', - cssIconDesc: 'glyphicon glyphicon-chevron-down', - cssIconNone: 'glyphicon glyphicon-resize-vertical', + cssIconAsc : 'glyphicon glyphicon-chevron-up', + cssIconDesc : 'glyphicon glyphicon-chevron-down', + cssIconNone : 'glyphicon glyphicon-resize-vertical', // Only four-digits format year is handled by the builtin parser // 'shortDate'. @@ -282,16 +292,16 @@ $.extend(true, $.tablesorter.defaults, { // https://mottie.github.io/tablesorter/docs/index.html#variable-language $.extend($.tablesorter.language, { - sortAsc: 'Trié par ordre croissant, ', - sortDesc: 'Trié par ordre décroissant, ', - sortNone: 'Non trié, ', - sortDisabled: 'tri désactivé et/ou non-modifiable', - nextAsc: 'cliquer pour trier par ordre croissant', - nextDesc: 'cliquer pour trier par ordre décroissant', - nextNone: 'cliquer pour retirer le tri' + sortAsc : 'Trié par ordre croissant, ', + sortDesc : 'Trié par ordre décroissant, ', + sortNone : 'Non trié, ', + sortDisabled : 'tri désactivé et/ou non-modifiable', + nextAsc : 'cliquer pour trier par ordre croissant', + nextDesc : 'cliquer pour trier par ordre décroissant', + nextNone : 'cliquer pour retirer le tri' }); -$(function () { +$( function() { $('.sortable').tablesorter(); }); diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js deleted file mode 100644 index a1ac8d37..00000000 --- a/kfet/static/kfet/js/kpsul.js +++ /dev/null @@ -1,154 +0,0 @@ -class AccountManager { - // Classe pour gérer la partie "compte" de K-Psul - // Devrait être la seule interface entre le JS de K-Psul et la logique des comptes. - constructor() { - // jQuery elements - this._$input = $("#id_trigramme"); - this._$container = $("#account"); - this._$article_select = $("#article_autocomplete") - - // Subordinated classes - this.account = new Account({ "trigramme": "" }); - this.search = new AccountSearch(this) - - // Initialization - this._init_events(); - } - - get data() { - return this.account.toJSON(); - } - - _init_events() { - var that = this; - - // L'input change ; on met à jour le compte - this._$input.on("input", () => this.update()) - - // Raccourci LIQ - this._$input.on("keydown", function (e) { - if (e.key == "ArrowDown") { - that.set("LIQ") - } - }) - - // Fonction de recherche - this._$container.on('click', '.search', function () { - that.search.open(); - }); - - this._$container.on('keydown', function (e) { - if (e.key == "f" && e.ctrlKey) { - // Ctrl + F : universal search shortcut - that.search.open(); - e.preventDefault(); - } - }); - } - - set(trigramme) { - this._$input.val(trigramme); - this.update(); - } - - update() { - var trigramme = this._$input.val().format_trigramme(); - this.account.set({ "trigramme": trigramme }) - if (trigramme.is_valid_trigramme()) { - this.account.fetch({ - "success": this._on_success.bind(this), - "error": this.reset.bind(this, false), - }) - } else { - this.reset() - } - } - - _on_success() { - // On utilise l'objet global window pour accéder aux fonctions nécessaires - this.account.render(); - this._$article_select.focus(); - window.updateBasketAmount(); - window.updateBasketRel(); - } - - reset(hard_reset = false) { - this.account.reset(); - this.account.render(); - - if (hard_reset) { - this._$input.val(""); - this.update() - } - } -} - -class AccountSearch { - - constructor(manager) { - this.manager = manager; - - this._content = '
    '; - this._input = '#search_autocomplete'; - this._results_container = '#account_results'; - - } - - open() { - var that = this; - this._$dialog = $.dialog({ - title: 'Recherche de compte', - content: this._content, - backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', - keyboardEnabled: true, - onOpen: function () { - that._$input = $(that._input); - that._$results_container = $(that._results_container); - that._init_form() - ._init_events(); - }, - }); - } - - _init_form() { - var that = this; - - this._$input.yourlabsAutocomplete({ - url: django_urls['kfet.account.search.autocomplete'](), - minimumCharacters: 2, - id: 'search_autocomplete', - choiceSelector: '.autocomplete-value', - placeholder: "Chercher un utilisateur K-Fêt", - container: that._$results_container, - box: that._$results_container, - fixPosition: function () { }, - }); - - return this; - } - - _init_events() { - this._$input.bind('selectChoice', - (e, choice, autocomplete) => this._on_select(e, choice, autocomplete) - ); - return this; - } - - _on_select(e, choice, autocomplete) { - // Une option est de la forme " ()" - var choice_text = choice.text().trim(); - var trigramme_regex = /\((.{3})\)$/; - // le match est de la forme [, ] - trigramme = choice_text.match(trigramme_regex)[1] - this.manager.set(trigramme); - this.close(); - } - - close() { - if (this._$dialog !== undefined) { - this._$dialog.close(); - } - } -} diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index 4da17672..9baa08c4 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -1,15 +1,28 @@ -(function ($) { +(function($){ window.StatsGroup = function (url, target) { // a class to properly display statictics // url : points to an ObjectResumeStat that lists the options through JSON // target : element of the DOM where to put the stats + var self = this; var element = $(target); var content = $("
    "); var buttons; - function handleTimeChart(data) { + function dictToArray (dict, start) { + // converts the dicts returned by JSONResponse to Arrays + // necessary because for..in does not guarantee the order + if (start === undefined) start = 0; + var array = new Array(); + for (var k in dict) { + array[k] = dict[k]; + } + array.splice(0, start); + return array; + } + + function handleTimeChart (data) { // reads the balance data and put it into chartjs formatting chart_data = new Array(); for (var i = 0; i < data.length; i++) { @@ -23,7 +36,7 @@ return chart_data; } - function showStats() { + function showStats () { // CALLBACK : called when a button is selected // shows the focus on the correct button @@ -31,20 +44,24 @@ $(this).addClass("focus"); // loads data and shows it - $.getJSON(this.stats_target_url, displayStats); + $.getJSON(this.stats_target_url, {format: 'json'}, displayStats); } - function displayStats(data) { + function displayStats (data) { // reads the json data and updates the chart display var chart_datasets = []; + var charts = dictToArray(data.charts); + // are the points indexed by timestamps? var is_time_chart = data.is_time_chart || false; // reads the charts data - for (let chart of data.charts) { + for (var i = 0; i < charts.length; i++) { + var chart = charts[i]; + // format the data - var chart_data = is_time_chart ? handleTimeChart(chart.values) : chart.values; + var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 0); chart_datasets.push( { @@ -59,24 +76,29 @@ // options for chartjs var chart_options = - { - responsive: true, - maintainAspectRatio: false, - tooltips: { - mode: 'index', - intersect: false, - }, - hover: { - mode: 'nearest', - intersect: false, - } - }; + { + responsive: true, + maintainAspectRatio: false, + tooltips: { + mode: 'index', + intersect: false, + }, + hover: { + mode: 'nearest', + intersect: false, + } + }; // additionnal options for time-indexed charts if (is_time_chart) { chart_options['scales'] = { xAxes: [{ type: "time", + display: true, + scaleLabel: { + display: false, + labelString: 'Date' + }, time: { tooltipFormat: 'll HH:mm', displayFormats: { @@ -93,19 +115,26 @@ } }], + yAxes: [{ + display: true, + scaleLabel: { + display: false, + labelString: 'value' + } + }] }; } // global object for the options var chart_model = - { - type: 'line', - options: chart_options, - data: { - labels: data.labels || [], - datasets: chart_datasets, - } - }; + { + type: 'line', + options: chart_options, + data: { + labels: data.labels || [], + datasets: chart_datasets, + } + }; // saves the previous charts to be destroyed var prev_chart = content.children(); @@ -122,30 +151,27 @@ } // initialize the interface - function initialize(data) { + function initialize (data) { // creates the bar with the buttons buttons = $("
    - - - - {% endblock %} {% block main %} diff --git a/kfet/templates/kfet/account_create.html b/kfet/templates/kfet/account_create.html index 13d36748..3fd21a96 100644 --- a/kfet/templates/kfet/account_create.html +++ b/kfet/templates/kfet/account_create.html @@ -1,5 +1,5 @@ {% extends "kfet/base_form.html" %} -{% load static %} +{% load staticfiles %} {% block title %}Nouveau compte{% endblock %} {% block header-title %}Création d'un compte{% endblock %} @@ -36,7 +36,7 @@ // Affichage des résultats d'autocomplétion $('input#search_autocomplete').yourlabsAutocomplete({ url: '{% url "kfet.account.create.autocomplete" %}', - minimumCharacters: 3, + minimumCharacters: 0, id: 'search_autocomplete', choiceSelector: 'li:has(a)', container: $("#search_results"), @@ -60,7 +60,8 @@ $('#id_trigramme').on('input', function() { var trigramme = $('#id_trigramme').val().toUpperCase(); - if (!(trigramme.is_valid_trigramme())) { + var pattern = /^[^a-z]{3}$/; + if (!(trigramme.match(pattern))) { $('#id_trigramme') .css('background', '#fff') .css('color', '#000'); diff --git a/kfet/templates/kfet/account_create_autocomplete.html b/kfet/templates/kfet/account_create_autocomplete.html new file mode 100644 index 00000000..5343b945 --- /dev/null +++ b/kfet/templates/kfet/account_create_autocomplete.html @@ -0,0 +1,50 @@ +{% load kfet_tags %} + +
      +
    • + + Créer un compte + +
    • + {% if kfet %} +
    • Comptes existants
    • + {% for account, user in kfet %} +
    • {{ account }} [{{ user|highlight_user:q }}]
    • + {% endfor %} + {% endif %} + {% if users_cof %} +
    • Membres du COF
    • + {% for user in users_cof %} +
    • + + {{ user|highlight_user:q }} + +
    • + {% endfor %} + {% endif %} + {% if users_notcof %} +
    • Non-membres du COF
    • + {% for user in users_notcof %} +
    • + + {{ user|highlight_user:q }} + +
    • + {% endfor %} + {% endif %} + {% if clippers %} +
    • Utilisateurs clipper
    • + {% for clipper in clippers %} +
    • + + {{ clipper|highlight_clipper:q }} + +
    • + {% endfor %} + {% endif %} + {% if not q %} +
    • Pas de recherche, pas de résultats !
    • + {% elif not options %} +
    • Aucune correspondance trouvée :-(
    • + {% endif %} +
    diff --git a/kfet/templates/kfet/account_create_special.html b/kfet/templates/kfet/account_create_special.html index 5bba9ca7..bc0fe4fe 100644 --- a/kfet/templates/kfet/account_create_special.html +++ b/kfet/templates/kfet/account_create_special.html @@ -1,5 +1,5 @@ {% extends "kfet/base.html" %} -{% load static %} +{% load staticfiles %} {% block title %}Nouveau compte{% endblock %} {% block header-title %}Création d'un compte{% endblock %} diff --git a/kfet/templates/kfet/account_group_form.html b/kfet/templates/kfet/account_group_form.html index c9ee04f6..b309d838 100644 --- a/kfet/templates/kfet/account_group_form.html +++ b/kfet/templates/kfet/account_group_form.html @@ -1,5 +1,5 @@ {% extends 'kfet/base_form.html' %} -{% load static %} +{% load staticfiles %} {% load widget_tweaks %} {% block title %}Permissions - Édition{% endblock %} @@ -7,6 +7,35 @@ {% block main %} -{% include "kfet/form_full_snippet.html" with authz=perms.kfet.manage_perms submit_text="Enregistrer" %} +
    + {% csrf_token %} +
    + +
    +
    + K-Fêt + {{ form.name|add_class:"form-control" }} +
    + {% if form.name.errors %} + {{ form.name.errors }} + {% endif %} + {% if form.name.help_text %} + {{ form.name.help_text }} + {% endif %} +
    +
    + {% include "kfet/form_field_snippet.html" with field=form.permissions %} + {% include "kfet/form_submit_snippet.html" with value="Enregistrer" %} +
    + + {% endblock %} diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index c2390f6d..fa8b508d 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -10,25 +10,45 @@ {{ negatives|length }} compte{{ negatives|length|pluralize }} en négatif
    -
    - {{ negatives_sum|floatformat:2 }}€ - de négatif total +
    + Total: {{ negatives_sum|floatformat:2 }}€ +
    +
    + Plafond par défaut +
      +
    • Montant: {{ kfet_config.overdraft_amount }}€
    • +
    • Pendant: {{ kfet_config.overdraft_duration }}
    • +
    +{% if perms.kfet.change_settings %} +
    +
    +
    +
    +{% endif %} + {% endblock %} {% block main %}
    - +
    + + + + @@ -41,13 +61,23 @@ + + + + {% endfor %}
    Tri. Nom BalanceRéelle DébutDécouvert autoriséJusqu'auBalance offset
    {{ neg.account.name }} {{ neg.account.balance|floatformat:2 }}€ + {% if neg.balance_offset %} + {{ neg.account.real_balance|floatformat:2 }}€ + {% endif %} + {{ neg.start|date:'d/m/Y H:i'}} {{ neg.authz_overdraft_amount|default_if_none:'' }} + {{ neg.authz_overdraft_until|date:'d/m/Y H:i' }} + {{ neg.balance_offset|default_if_none:'' }}
    -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 4c42fb76..bbd1cff7 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -1,14 +1,13 @@ {% extends "kfet/base_col_2.html" %} -{% load static %} +{% load staticfiles %} {% load kfet_tags %} {% load l10n %} {% block extra_head %} - - + {% if account.user == request.user %} @@ -82,7 +81,7 @@ $(document).ready(function() {
    {% endif %} -
    +
    @@ -94,22 +93,29 @@ $(document).ready(function() { khistory = new KHistory({ display_trigramme: false, - fetch_options: { - account: {{ account.pk }}, - } }); - $(document).on('keydown', function (e) { - if (e.keyCode == 46) { - // DEL (Suppr) - khistory.cancel_selected() + function getHistory() { + var data = { + 'accounts': [{{ account.pk }}], } - }); - khistory.fetch().done(function () { + $.ajax({ + dataType: "json", + url : "{% url 'kfet.history.json' %}", + method : "POST", + data : data, + }) + .done(function(data) { + for (var i=0; i diff --git a/kfet/templates/kfet/account_search_autocomplete.html b/kfet/templates/kfet/account_search_autocomplete.html new file mode 100644 index 00000000..e18eb1eb --- /dev/null +++ b/kfet/templates/kfet/account_search_autocomplete.html @@ -0,0 +1,14 @@ +{% load kfet_tags %} + +
      + {% if accounts %} + {% for trigramme, user in accounts %} +
    • {{ user|highlight_text:q }} ({{ trigramme }})
    • + {% endfor %} + {% elif not q %} +
    • Pas de recherche, pas de résultats !
    • + {% else %} +
    • Aucune correspondance trouvée :-(
    • + {% endif %} +
    + diff --git a/kfet/templates/kfet/account_update.html b/kfet/templates/kfet/account_update.html index 7115b9e2..36b3d75d 100644 --- a/kfet/templates/kfet/account_update.html +++ b/kfet/templates/kfet/account_update.html @@ -6,39 +6,51 @@ {% block title %} {% if account.user == request.user %} -Modification de mes informations + Modification de mes informations {% else %} -{{ account.trigramme }} - Édition + {{ account.trigramme }} - Édition {% endif %} {% endblock %} {% block header-title %} {% if account.user == request.user %} -Modification de mes informations + Modification de mes informations {% else %} -Édition du compte {{ account.trigramme }} + Édition du compte {{ account.trigramme }} {% endif %} {% endblock %} {% block footer %} {% if not account.is_team %} -{% include "kfet/base_footer.html" %} + {% include "kfet/base_footer.html" %} {% endif %} {% endblock %} {% block main %} -
    + {% csrf_token %} {% include 'kfet/form_snippet.html' with form=user_info_form %} {% include 'kfet/form_snippet.html' with form=account_form %} - {% include 'kfet/form_snippet.html' with form=frozen_form %} {% include 'kfet/form_snippet.html' with form=group_form %} - {% include 'kfet/form_snippet.html' with form=cof_form %} {% include 'kfet/form_snippet.html' with form=pwd_form %} - - {% include 'kfet/form_authentication_snippet.html' %} + {% include 'kfet/form_snippet.html' with form=negative_form %} + {% if perms.kfet.is_team %} + {% include 'kfet/form_authentication_snippet.html' %} + {% endif %} {% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
    + + {% endblock %} diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index cb52a5ea..6b48ddbb 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -40,7 +40,6 @@ Prix Stock En vente - Reservé aux adhérent⋅e⋅s Affiché Dernier inventaire @@ -64,7 +63,6 @@ {{ article.price }}€ {{ article.stock }} {{ article.is_sold | yesno:"En vente,Non vendu"}} - {{ article.no_exte | yesno:"Réservé,Non réservé"}} {{ article.hidden | yesno:"Caché,Affiché" }} {% with last_inventory=article.inventory.0 %} @@ -90,7 +88,6 @@ Prix Stock En vente - Reservé aux adhérent⋅e⋅s Affiché Dernier inventaire @@ -114,7 +111,6 @@ {{ article.price }}€ {{ article.stock }} {{ article.is_sold | yesno:"En vente,Non vendu"}} - {{ article.no_exte | yesno:"Réservé,Non réservé"}} {{ article.hidden | yesno:"Caché,Affiché" }} {% with last_inventory=article.inventory.0 %} diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html index 52032099..68eee1b7 100644 --- a/kfet/templates/kfet/article_read.html +++ b/kfet/templates/kfet/article_read.html @@ -1,5 +1,5 @@ {% extends 'kfet/base_col_2.html' %} -{% load static kfet_tags %} +{% load staticfiles kfet_tags %} {% block extra_head %} @@ -39,7 +39,6 @@
  • Stock: {{ article.stock }}
  • En vente: {{ article.is_sold|yesno|title }}
  • Affiché: {{ article.hidden|yesno|title }}
  • -
  • Réservé aux adhérent⋅e⋅s: {{ article.no_exte|yesno|title }}
  • @@ -161,4 +160,4 @@ }); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/kfet/templates/kfet/base.html b/kfet/templates/kfet/base.html index 524e3633..9b75af03 100644 --- a/kfet/templates/kfet/base.html +++ b/kfet/templates/kfet/base.html @@ -19,7 +19,6 @@ {# JS #} - @@ -30,17 +29,6 @@ - {% include "kfetopen/init.html" %} diff --git a/kfet/templates/kfet/base_footer.html b/kfet/templates/kfet/base_footer.html index 7a016705..c5333476 100644 --- a/kfet/templates/kfet/base_footer.html +++ b/kfet/templates/kfet/base_footer.html @@ -1,13 +1,17 @@ {% load wagtailcore_tags %} +{% with "k-fet@ens.fr" as kfet_mail %} + +
    + +{% endwith %} diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index 24f6a554..1cded20b 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -25,7 +25,6 @@ . -
    Grand indicateur {% if perms.kfet.is_team %} {% endif %} @@ -33,20 +32,20 @@ {% for item in menu_items %} {% if item.text == "Accueil" %} - {% else %} - {% endif %} {% endfor %} - {% endif %} - {% if perms.kfet.delete_inventory %} -
    -
    - -
    - {% csrf_token %} -
    - {% endif %} -
    @@ -37,12 +26,6 @@ {% block main %} -
    -
    - Les valeurs de stock sont calculées sur la base du prix actuel des articles. -
    -
    -
    Erreur - {% regroup inventoryarts by article.category.name as category_list %} + {% regroup inventoryarts by article.category as category_list %} {% for category in category_list %} - + {% for inventoryart in category.list %} - + {% endfor %} {% endfor %} - - - - - - - -
    {{ category.grouper }}{{ category.grouper.name }}
    - + {{ inventoryart.article.name }} {{ inventoryart.stock_old }} {{ inventoryart.stock_new }}{{ inventoryart.stock_error }} / {{ inventoryart.amount_error|floatformat:"-2" }} €{{ inventoryart.stock_error }}
    Valeurs totales{{ total_amount_old|floatformat:"-2" }} €{{ total_amount_new|floatformat:"-2" }} €{{ total_amount_error|floatformat:"-2" }} €
    - - {% endblock %} diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index b44f1a25..171c7030 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -1,16 +1,15 @@ {% extends 'kfet/base.html' %} -{% load static %} +{% load staticfiles %} {% block extra_head %} - + - {% endblock %} {% block title %}K-Psul{% endblock %} @@ -190,7 +189,7 @@ $(document).ready(function() { // ----- // Lock to avoid multiple requests - window.lock = 0; + lock = 0; // Retrieve settings @@ -214,8 +213,124 @@ $(document).ready(function() { // Account data management // ----- - var account_manager = new AccountManager(); + // Initializing + var account = new Account() + var account_container = $('#account'); var triInput = $('#id_trigramme'); + var account_data = {}; + var account_data_default = { + 'id' : 0, + 'name' : '', + 'email': '', + 'is_cof' : '', + 'promo' : '', + 'balance': '', + 'trigramme' : '', + 'is_frozen' : false, + 'departement': '', + 'nickname' : '', + }; + + // Search for an account + function searchAccount() { + var content = '
    ' ; + $.dialog({ + title: 'Recherche de compte', + content: content, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + + onOpen: function() { + var that=this ; + $('input#search_autocomplete').yourlabsAutocomplete({ + url: '{% url "kfet.account.search.autocomplete" %}', + minimumCharacters: 2, + id: 'search_autocomplete', + choiceSelector: '.choice', + placeholder: "Chercher un utilisateur K-Fêt", + box: $("#account_results"), + fixPosition: function() {}, + }); + $('input#search_autocomplete').bind( + 'selectChoice', + function(e, choice, autocomplete) { + autocomplete.hide() ; + triInput.val(choice.find('.trigramme').text()) ; + triInput.trigger('input') ; + that.close() ; + }); + } + }); + } + + account_container.on('click', '.search', function () { + searchAccount() ; + }) ; + + account_container.on('keydown', function(e) { + if (e.which == 70 && e.ctrlKey) { + // Ctrl + F : universal search shortcut + searchAccount() ; + e.preventDefault() ; + } + }); + + // Clear data + function resetAccountData() { + account_data = account_data_default; + $('#id_on_acc').val(0); + account.reset(); + account.view().reset() + } + + function resetAccount() { + triInput.val(''); + resetAccountData(); + } + + // Store data + function storeAccountData() { + account_data = account.toJSON(); + $('#id_on_acc').val(account.id); + account.render(); + } + + // Retrieve via ajax + function retrieveAccountData(tri) { + account.set({'trigramme': tri}); + account.fetch({ + 'success': function() { + storeAccountData(); + articleSelect.focus(); + updateBasketAmount(); + updateBasketRel(); + }, + 'error': function() { + resetAccountData(); + }, + }) + } + + // Event listener + triInput.on('input', function() { + var tri = triInput.val().toUpperCase(); + // Checking if tri is valid to avoid sending requests + if (isValidTrigramme(tri)) { + retrieveAccountData(tri); + } else { + resetAccountData(); + } + }); + + triInput.on('keydown', function(e) { + if (e.keyCode == 40) { + // Arrow Down - Shorcut to LIQ + triInput.val('LIQ'); + triInput.trigger('input'); + } + }); // ----- @@ -301,7 +416,7 @@ $(document).ready(function() { // Event listener checkoutInput.on('change', function() { retrieveCheckoutData(checkoutInput.val()); - if (account_manager.data['trigramme']) { + if (account_data['trigramme']) { articleSelect.focus().select(); } else { triInput.focus().select(); @@ -340,6 +455,21 @@ $(document).ready(function() { $('#id_comment').val(''); } + // ----- + // Errors ajax + // ----- + + function displayErrors(html) { + $.alert({ + title: 'Erreurs', + content: html, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + }); + } + // ----- // Perform operations // ----- @@ -349,9 +479,9 @@ $(document).ready(function() { var operations = $('#operation_formset'); function performOperations(password = '') { - if (window.lock == 1) + if (lock == 1) return false; - window.lock = 1; + lock = 1; var data = operationGroup.serialize() + '&' + operations.serialize(); $.ajax({ dataType: "json", @@ -367,7 +497,7 @@ $(document).ready(function() { .done(function(data) { updatePreviousOp(); coolReset(); - window.lock = 0; + lock = 0; }) .fail(function($xhr) { var data = $xhr.responseJSON; @@ -376,14 +506,14 @@ $(document).ready(function() { requestAuth(data, performOperations, articleSelect); break; case 400: - if ('need_comment' in data) { + if ('need_comment' in data['errors']) { askComment(performOperations); } else { - displayErrors(data); + displayErrors(getErrorsHtml(data)); } break; } - window.lock = 0; + lock = 0; }); } @@ -392,13 +522,62 @@ $(document).ready(function() { performOperations(); }); + // ----- + // Cancel operations + // ----- + + var cancelButton = $('#cancel_operations'); + var cancelForm = $('#cancel_form'); + + function cancelOperations(opes_array, password = '') { + if (lock == 1) + return false + lock = 1; + var data = { 'operations' : opes_array } + $.ajax({ + dataType: "json", + url : "{% url 'kfet.kpsul.cancel_operations' %}", + method : "POST", + data : data, + beforeSend: function ($xhr) { + $xhr.setRequestHeader("X-CSRFToken", csrftoken); + if (password != '') + $xhr.setRequestHeader("KFetPassword", password); + }, + + }) + .done(function(data) { + coolReset(); + lock = 0; + }) + .fail(function($xhr) { + var data = $xhr.responseJSON; + switch ($xhr.status) { + case 403: + requestAuth(data, function(password) { + cancelOperations(opes_array, password); + }, triInput); + break; + case 400: + displayErrors(getErrorsHtml(data)); + break; + } + lock = 0; + }); + } + + // Event listeners + cancelButton.on('click', function() { + cancelOperations(); + }); + // ----- // Articles data // ----- var articles_container = $('#articles_data tbody'); - var article_category_default_html = ''; - var article_default_html = ''; + var article_category_default_html = ''; + var article_default_html = ''; function addArticle(article) { var article_html = $(article_default_html); @@ -411,7 +590,6 @@ $(document).ready(function() { article_html.addClass('low-stock'); } article_html.find('.price').text(amountToUKF(article['price'], false, false)+' UKF'); - article_html.find('.no_exte').text(article['no_exte'] ? "Réservé aux adhérent⋅e⋅s" : ""); var category_html = articles_container .find('#data-category-'+article['category_id']); if (category_html.length == 0) { @@ -431,7 +609,7 @@ $(document).ready(function() { var $after = articles_container.find('#data-category-'+article['category_id']); articles_container .find('.article.data-category-'+article['category_id']).each(function() { - if (article['name'].toLowerCase() < $('.name', this).text().toLowerCase()) + if (article['name'].toLowerCase < $('.name', this).text().toLowerCase()) return false; $after = $(this); }); @@ -584,7 +762,7 @@ $(document).ready(function() { }); function is_nb_ok(nb) { - return /^[0-9]+$/.test(nb) && nb > 0; + return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24; } articleNb.on('keydown', function(e) { @@ -623,11 +801,11 @@ $(document).ready(function() { var amount_euro = - article_data[3] * nb ; if (settings['addcost_for'] && settings['addcost_amount'] - && account_manager.data['trigramme'] != settings['addcost_for'] + && account_data['trigramme'] != settings['addcost_for'] && article_data[5]) amount_euro -= settings['addcost_amount'] * nb; var reduc_divisor = 1; - if (account_manager.data['is_cof'] && article_data[6]) + if (account_data['is_cof'] && article_data[6]) reduc_divisor = 1 + settings['subvention_cof'] / 100; return (amount_euro / reduc_divisor).toFixed(2); } @@ -650,7 +828,7 @@ $(document).ready(function() { .attr('data-opeindex', index) .find('.number').text('('+nb+'/'+article_data[4]+')').end() .find('.name').text(article_data[0]).end() - .find('.amount').text(amountToUKF(amount_euro, account_manager.data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount_euro, account_data['is_cof'], false)); basket_container.prepend(article_basket_html); if (is_low_stock(id, nb)) article_basket_html.find('.lowstock') @@ -676,7 +854,7 @@ $(document).ready(function() { .attr('data-opeindex', index) .find('.number').text(amount+"€").end() .find('.name').text('Charge').end() - .find('.amount').text(amountToUKF(amount, account_manager.data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false)); basket_container.prepend(deposit_basket_html); updateBasketRel(); } @@ -689,7 +867,7 @@ $(document).ready(function() { .attr('data-opeindex', index) .find('.number').text(amount+"€").end() .find('.name').text('Édition').end() - .find('.amount').text(amountToUKF(amount, account_manager.data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false)); basket_container.prepend(deposit_basket_html); updateBasketRel(); } @@ -702,7 +880,7 @@ $(document).ready(function() { .attr('data-opeindex', index) .find('.number').text(amount+"€").end() .find('.name').text('Retrait').end() - .find('.amount').text(amountToUKF(amount, account_manager.data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false)); basket_container.prepend(withdraw_basket_html); updateBasketRel(); } @@ -758,7 +936,7 @@ $(document).ready(function() { var amount = $(this).find('#id_form-'+opeindex+'-amount'); if (!deleted && type == "purchase") amount.val(amountEuroPurchase(article_id, article_nb)); - basket_container.find('[data-opeindex='+opeindex+'] .amount').text(amountToUKF(amount.val(), account_manager.data['is_cof'], false)); + basket_container.find('[data-opeindex='+opeindex+'] .amount').text(amountToUKF(amount.val(), account_data['is_cof'], false)); }); } @@ -766,7 +944,7 @@ $(document).ready(function() { function updateBasketRel() { var basketrel_html = ''; - if (account_manager.data['trigramme'] == 'LIQ' && !isBasketEmpty()) { + if (account_data['trigramme'] == 'LIQ' && !isBasketEmpty()) { var amount = - getAmountBasket(); basketrel_html += '
    Total: '+amount.toFixed(2)+' €
    '; if (amount < 5) @@ -775,11 +953,11 @@ $(document).ready(function() { basketrel_html += '
    Sur 10€: '+ (10-amount).toFixed(2) +' €
    '; if (amount < 20) basketrel_html += '
    Sur 20€: '+ (20-amount).toFixed(2) +' €
    '; - } else if (account_manager.data['trigramme'] != '' && !isBasketEmpty()) { + } else if (account_data['trigramme'] != '' && !isBasketEmpty()) { var amount = getAmountBasket(); - var amountUKF = amountToUKF(amount, account_manager.data['is_cof'], false); - var newBalance = account_manager.data['balance'] + amount; - var newBalanceUKF = amountToUKF(newBalance, account_manager.data['is_cof'], true); + var amountUKF = amountToUKF(amount, account_data['is_cof'], false); + var newBalance = account_data['balance'] + amount; + var newBalanceUKF = amountToUKF(newBalance, account_data['is_cof'], true); basketrel_html += '
    Total: '+amountUKF+'
    '; basketrel_html += '
    Nouveau solde: '+newBalanceUKF+'
    '; if (newBalance < 0) @@ -800,7 +978,7 @@ $(document).ready(function() { var nb_before = formset_container.find("#id_form-"+opeindex+"-article_nb").val(); var nb_after = parseInt(nb_before) + parseInt(nb); var amountEuro_after = amountEuroPurchase(id, nb_after); - var amountUKF_after = amountToUKF(amountEuro_after, account_manager.data['is_cof']); + var amountUKF_after = amountToUKF(amountEuro_after, account_data['is_cof']); if (type == 'purchase') { if (nb_after == 0) { @@ -1011,18 +1189,30 @@ $(document).ready(function() { // History // ----- - khistory = new KHistory({ - fetch_options: { - start: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss'), - opes_only: true, - }, - }); + khistory = new KHistory(); + + function getHistory() { + var data = { + from: moment().subtract(1, 'days').format('YYYY-MM-DD HH:mm:ss'), + }; + $.ajax({ + dataType: "json", + url : "{% url 'kfet.history.json' %}", + method : "POST", + data : data, + }) + .done(function(data) { + for (var i=0; i'; previousop_html += basketrel_container.html(); previousop_container.html(previousop_html); @@ -1075,7 +1265,7 @@ $(document).ready(function() { }, triInput); break; case 400: - askAddcost(getErrorsHtml(data["errors"], is_error=true)); + askAddcost(getErrorsHtml(data)); break; } }); @@ -1112,10 +1302,29 @@ $(document).ready(function() { // Cancel from history // ----- + khistory.$container.selectable({ + filter: 'div.opegroup, div.ope', + selected: function(e, ui) { + $(ui.selected).each(function() { + if ($(this).hasClass('opegroup')) { + var opegroup = $(this).data('opegroup'); + $(this).siblings('.ope').filter(function() { + return $(this).data('opegroup') == opegroup + }).addClass('ui-selected'); + } + }); + }, + }); + $(document).on('keydown', function (e) { if (e.keyCode == 46) { // DEL (Suppr) - khistory.cancel_selected() + var opes_to_cancel = []; + khistory.$container.find('.ope.ui-selected').each(function () { + opes_to_cancel.push($(this).data('ope')); + }); + if (opes_to_cancel.length > 0) + cancelOperations(opes_to_cancel); } }); @@ -1124,9 +1333,16 @@ $(document).ready(function() { // ----- OperationWebSocket.add_handler(function(data) { - for (var i=0; i - {% if perms.kfet.is_team %}
    Éditer + + Créditer + {% if perms.kfet.delete_account %}
    - {% endif %}

    {{ account.name|title }}

      + {% if perms.kfet.is_team %}
    • {{ account.nickname }}
    • + {% endif %}
    • {{ account.email|default:"Pas d'email!" }}
    • -
    • - {% if account.promo %} - {{ account.departement }} {{ account.promo }} - {% else %} - Sans promo - {% endif %} -
    • +
    • {{ account.departement }} {{ account.promo }}
    • {% if account.is_cof %} - Membre COF - {% elif account.is_kfet %} - Membre K-Fêt + Adhérent COF {% else %} Non-COF {% endif %} @@ -53,13 +47,22 @@
    - {% if account.negative and account.balance_ukf < 0 %} + {% if account.negative %}

    Négatif

      {% if account.negative.start %}
    • Depuis le {{ account.negative.start|date:"d/m/Y à H:i" }}
    • {% endif %} + {% if account.real_balance != account.balance %} +
    • Solde réel: {{ account.real_balance }} €
    • + {% endif %} +
    • + Plafond : + {{ account.negative.authz_overdraft_amount|default:kfet_config.overdraft_amount }} € + jusqu'au + {{ account.negative.authz_overdraft_until|default:account.negative.until_default|date:"d/m/Y à H:i" }} +
    {% endif %} @@ -86,20 +89,20 @@ {% endif %} diff --git a/kfet/templates/kfet/mails/creation_trigramme.txt b/kfet/templates/kfet/mails/creation_trigramme.txt deleted file mode 100644 index 25638186..00000000 --- a/kfet/templates/kfet/mails/creation_trigramme.txt +++ /dev/null @@ -1,12 +0,0 @@ -Salut {{ account.name }}, - -Ton compte K-Fêt a bien été créé le {{ account.created_at }} avec le trigramme {{ account.trigramme }}. - -Tu peux désormais : -- Accéder à ton historique personnel des consommations : https://{{ site }}{{ url_read }} -- Modifier tes informations : https://{{ site }}{{ url_update }} -- Supprimer ton compte : https://{{ site }}{{ url_delete }} - -En espérant te revoir bientôt, --- -L'équipe K-Fêt diff --git a/kfet/templates/kfet/mails/demande_soiree.txt b/kfet/templates/kfet/mails/demande_soiree.txt deleted file mode 100644 index 9fd6d635..00000000 --- a/kfet/templates/kfet/mails/demande_soiree.txt +++ /dev/null @@ -1,16 +0,0 @@ -Bonjour, - -J'aimerais organiser une soirée le {{ date }}, au thème « {{ theme|safe }} », en K-Fêt. -Elle se terminerait à {{ horaire_fin }}, et le service serait en mode {{ service }}. - -Les 4 responsables de la soirée seraient : -- {{ respo1 }} -- {{ respo2 }} -- {{ respo3 }} -- {{ respo4 }} - -Quelques remarques supplémentaires : -{{ remarques|safe }} - -Bien cordialement, -{{ nom|safe }} diff --git a/kfet/templates/kfet/mails/rappel.txt b/kfet/templates/kfet/mails/rappel.txt deleted file mode 100644 index b37b2b0e..00000000 --- a/kfet/templates/kfet/mails/rappel.txt +++ /dev/null @@ -1,8 +0,0 @@ -Bonjour {{ account.first_name }}, - -Nous te rappelons que tu es en négatif de {{ neg_amount }}€ depuis le {{ start_date }}. -N'oublie pas de régulariser ta situation au plus vite. - -En espérant te revoir très bientôt, --- -L'équipe K-Fêt diff --git a/kfet/templates/kfet/nav_item.html b/kfet/templates/kfet/nav_item.html index 76a24ca4..b5981266 100644 --- a/kfet/templates/kfet/nav_item.html +++ b/kfet/templates/kfet/nav_item.html @@ -11,7 +11,7 @@ -->{{ text }}{{ text }} {% if href %} diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index 7e24d1c0..7cb4d1cb 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -7,20 +7,11 @@ {% block main-size %}col-lg-8 col-lg-offset-2{% endblock %} {% block main %} -
    -
    - -
    - -
    -
    -
    {% csrf_token %}
    @@ -34,7 +25,7 @@ - {% regroup formset by is_sold as is_sold_list %} - {% for condition in is_sold_list %} - - - + {% regroup formset by category_name as category_list %} + {% for category in category_list %} + + + - {% regroup condition.list by category_name as category_list %} - {% for category in category_list %} - - - - - - - {% for form in category.list %} - - {{ form.article }} - - {% for v_chunk in form.v_all %} - - {% endfor %} - - - - - - - - - {% endfor %} - - {% endfor %} + + {% for form in category.list %} + + {{ form.article }} + + {% for v_chunk in form.v_all %} + + {% endfor %} + + + + + + + + + {% endfor %} + {% endfor %}
    V. moy.
    - +
    E.T. @@ -44,7 +35,7 @@ Prév.
    - +
    Stock @@ -55,7 +46,7 @@ Rec.
    - +
    Commande @@ -67,39 +58,31 @@ {% endfor %}
    {% if condition.grouper %} Vendu {% else %} Non vendu {% endif %}
    {{ category.grouper }}
    {{ category.grouper }}
    {{ form.name }}{{ v_chunk }}{{ form.v_moy }}{{ form.v_et }}{{ form.v_prev }}{{ form.stock }}{{ form.box_capacity|default:"" }}{{ form.quantity_ordered|add_class:"form-control" }}
    {{ form.name }}{{ v_chunk }}{{ form.v_moy }}{{ form.v_et }}{{ form.v_prev }}{{ form.stock }}{{ form.box_capacity|default:"" }}{{ form.c_rec }}{{ form.quantity_ordered|add_class:"form-control" }}
    @@ -116,42 +99,6 @@ $(document).ready(function () { $('.glyphicon-question-sign').tooltip({'html': true}) ; }); - -function compute_recommended(nb_weeks, prevision_1w, stock, box_capacity) { - if (!box_capacity) box_capacity = 1; - return Math.ceil(Math.max(Number(nb_weeks) * Number(prevision_1w) - Math.max(Number(stock), 0), 0) / Number(box_capacity)) -} - -function reload_recommended(nb_weeks) { - $(".article-row").each(function () { - const article_row = $(this) - article_row.find(".recommended").text(compute_recommended(nb_weeks, article_row.find(".prev-1w").text(), - article_row.find(".stock").text(), - article_row.find(".capacity").text() - )); - }) - $("#new-order-table").trigger("updateAll", [true, () => { - }]); -} - -$("#nb_weeks").on("change", function (e) { - const nb_weeks = e.target.value; - reload_recommended(nb_weeks); -}) - - - {% endblock %} diff --git a/kfet/templates/kfet/search_results.html b/kfet/templates/kfet/search_results.html deleted file mode 100644 index d1ee70ac..00000000 --- a/kfet/templates/kfet/search_results.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "shared/search_results.html" %} -{% load i18n %} - -{% block extra_section %} -
  • - {% if not results %} - - {% trans "Aucune correspondance trouvée" %} - - {% else %} - - {% trans "Pas dans la liste ?" %} - - {% endif %} -
  • -
  • - - {% trans "Créer un compte" %} - -
  • -{% endblock %} diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html index f285b4dc..f6778b3f 100644 --- a/kfet/templates/kfet/transfers.html +++ b/kfet/templates/kfet/transfers.html @@ -1,15 +1,9 @@ {% extends 'kfet/base_col_2.html' %} -{% load l10n static widget_tweaks %} +{% load staticfiles %} {% block title %}Transferts{% endblock %} {% block header-title %}Transferts{% endblock %} -{% block extra_head %} - - - -{% endblock %} - {% block fixed %}
    @@ -22,31 +16,109 @@ {% block main %} - -
    +
    + {% for transfergroup in transfergroups %} +
    + {{ transfergroup.at }} + {{ transfergroup.valid_by.trigramme }} + {{ transfergroup.comment }} +
    + {% for transfer in transfergroup.transfers.all %} +
    + {{ transfer.amount }} € + {{ transfer.from_acc.trigramme }} + + {{ transfer.to_acc.trigramme }} +
    + {% endfor %} + {% endfor %} +
    diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index 41898de2..e4fae405 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -1,5 +1,5 @@ {% extends "kfet/base_col_1.html" %} -{% load static %} +{% load staticfiles %} {% block extra_head %} @@ -35,12 +35,12 @@ - + {{ form.from_acc }} {{ form.amount }} - + {{ form.to_acc }} @@ -72,7 +72,7 @@ $(document).ready(function () { var $next = $form.next('.transfer_form').find('.from_acc input'); } var $input_id = $input.next('input'); - if (trigramme.is_valid_trigramme()) { + if (isValidTrigramme(trigramme)) { getAccountData(trigramme, function(data) { $input_id.val(data.id); $data.text(data.name); @@ -91,8 +91,7 @@ $(document).ready(function () { $(document).on("input", '.input_from_acc, .input_to_acc', function(e) { var target = $(e.target) - var tri = target.val().format_trigramme(); - target.val(tri); + var tri = target.val().toUpperCase(); updateAccountData(tri, target); }); @@ -122,9 +121,6 @@ $(document).ready(function () { case 403: requestAuth(data, performTransfers); break; - case 400: - displayErrors(data); - break; } }); } diff --git a/kfet/templatetags/kfet_tags.py b/kfet/templatetags/kfet_tags.py index db4cfbf1..4c26dd17 100644 --- a/kfet/templatetags/kfet_tags.py +++ b/kfet/templatetags/kfet_tags.py @@ -1,4 +1,8 @@ +import re + from django import template +from django.utils.html import escape +from django.utils.safestring import mark_safe from ..utils import to_ukf @@ -7,14 +11,40 @@ register = template.Library() register.filter("ukf", to_ukf) +@register.filter() +def highlight_text(text, q): + q2 = "|".join(re.escape(word) for word in q.split()) + pattern = re.compile(r"(?P%s)" % q2, re.IGNORECASE) + regex = r"\g" + return mark_safe(re.sub(pattern, regex, escape(text))) + + +@register.filter(is_safe=True) +def highlight_user(user, q): + if user.first_name and user.last_name: + text = "%s %s (%s)" % (user.first_name, user.last_name, user.username) + else: + text = user.username + return highlight_text(text, q) + + +@register.filter(is_safe=True) +def highlight_clipper(clipper, q): + if clipper.fullname: + text = "%s (%s)" % (clipper.fullname, clipper.clipper) + else: + text = clipper.clipper + return highlight_text(text, q) + + @register.filter() def widget_type(field): return field.field.widget.__class__.__name__ @register.filter() -def slice(t, start, end=None): +def slice(l, start, end=None): if end is None: end = start start = 0 - return t[start:end] + return l[start:end] diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py index a534493d..7ce6605c 100644 --- a/kfet/tests/test_models.py +++ b/kfet/tests/test_models.py @@ -1,12 +1,10 @@ -from datetime import datetime, timedelta, timezone as tz -from decimal import Decimal -from unittest import mock +import datetime from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone -from kfet.models import Account, AccountNegative, Checkout +from kfet.models import Account, Checkout from .utils import create_user @@ -30,56 +28,6 @@ class AccountTests(TestCase): with self.assertRaises(Account.DoesNotExist): Account.objects.get_by_password("bernard") - @mock.patch("django.utils.timezone.now") - def test_negative_creation(self, mock_now): - now = datetime(2005, 7, 15, tzinfo=tz.utc) - mock_now.return_value = now - self.account.balance = Decimal(-10) - self.account.update_negative() - - self.assertTrue(hasattr(self.account, "negative")) - self.assertEqual(self.account.negative.start, now) - - @mock.patch("django.utils.timezone.now") - def test_negative_no_reset(self, mock_now): - now = datetime(2005, 7, 15, tzinfo=tz.utc) - mock_now.return_value = now - - self.account.balance = Decimal(-10) - AccountNegative.objects.create( - account=self.account, start=now - timedelta(minutes=3) - ) - self.account.refresh_from_db() - - self.account.balance = Decimal(5) - self.account.update_negative() - self.assertTrue(hasattr(self.account, "negative")) - - self.account.balance = Decimal(-10) - self.account.update_negative() - self.assertEqual(self.account.negative.start, now - timedelta(minutes=3)) - - @mock.patch("django.utils.timezone.now") - def test_negative_eventually_resets(self, mock_now): - now = datetime(2005, 7, 15, tzinfo=tz.utc) - mock_now.return_value = now - - self.account.balance = Decimal(-10) - AccountNegative.objects.create( - account=self.account, start=now - timedelta(minutes=20) - ) - self.account.refresh_from_db() - self.account.balance = Decimal(5) - - mock_now.return_value = now - timedelta(minutes=10) - self.account.update_negative() - - mock_now.return_value = now - self.account.update_negative() - self.account.refresh_from_db() - - self.assertFalse(hasattr(self.account, "negative")) - class CheckoutTests(TestCase): def setUp(self): @@ -91,7 +39,7 @@ class CheckoutTests(TestCase): self.c = Checkout( created_by=self.u_acc, valid_from=self.now, - valid_to=self.now + timedelta(days=1), + valid_to=self.now + datetime.timedelta(days=1), ) def test_initial_statement(self): diff --git a/kfet/tests/test_statistic.py b/kfet/tests/test_statistic.py index 6d8ecb47..a5e3192c 100644 --- a/kfet/tests/test_statistic.py +++ b/kfet/tests/test_statistic.py @@ -44,9 +44,9 @@ class TestStats(TestCase): "/k-fet/accounts/FOO/stat/operations?{}".format( "&".join( [ - "scale-name=day", - "scale-n_steps=7", - "scale-last=True", + "scale=day", + "types=['purchase']", + "scale_args={'n_steps':+7,+'last':+True}", "format=json", ] ) @@ -64,20 +64,10 @@ class TestStats(TestCase): # receives a Redirect response articles_urls = [ "/k-fet/articles/{}/stat/sales/list".format(article.pk), - "/k-fet/articles/{}/stat/sales?{}".format( - article.pk, - "&".join( - [ - "scale-name=day", - "scale-n_steps=7", - "scale-last=True", - "format=json", - ] - ), - ), + "/k-fet/articles/{}/stat/sales".format(article.pk), ] for url in articles_urls: resp = client.get(url) self.assertEqual(200, resp.status_code) resp2 = client2.get(url, follow=True) - self.assertRedirects(resp2, "/gestion/") + self.assertRedirects(resp2, "/") diff --git a/kfet/tests/test_tests_utils.py b/kfet/tests/test_tests_utils.py index 2c42ff79..49661e23 100644 --- a/kfet/tests/test_tests_utils.py +++ b/kfet/tests/test_tests_utils.py @@ -94,7 +94,6 @@ class PermHelpersTest(TestCaseMixin, TestCase): self.assertQuerysetEqual( user.user_permissions.all(), map(repr, [self.perm1, self.perm2, self.perm_team]), - transform=repr, ordered=False, ) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index d09ff3e8..0a5c4e49 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -3,20 +3,17 @@ from datetime import datetime, timedelta from decimal import Decimal from unittest import mock -from asgiref.sync import async_to_sync -from channels.layers import get_channel_layer -from django.contrib.auth.models import User +from django.contrib.auth.models import Group from django.test import Client, TestCase from django.urls import reverse from django.utils import timezone from .. import KFET_DELETED_TRIGRAMME from ..auth import KFET_GENERIC_TRIGRAMME -from ..auth.models import KFetGroup -from ..auth.utils import hash_password from ..config import kfet_config from ..models import ( Account, + AccountNegative, Article, ArticleCategory, Checkout, @@ -184,14 +181,10 @@ class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase): def test_ok(self): r = self.client.get(self.url, {"q": "first"}) self.assertEqual(r.status_code, 200) - self.assertEqual(len(r.context["results"]), 1) - (res,) = r.context["results"] - self.assertEqual(res.name, "kfet") - - u = self.users["user"] + self.assertEqual(len(r.context["users_notcof"]), 0) + self.assertEqual(len(r.context["users_cof"]), 0) self.assertSetEqual( - {e.verbose_name for e in res.entries}, - {"{} ({})".format(u, u.profile.account_kfet.trigramme)}, + set(r.context["kfet"]), set([(self.accounts["user"], self.users["user"])]) ) @@ -205,12 +198,7 @@ class AccountSearchViewTests(ViewTestCaseMixin, TestCase): def test_ok(self): r = self.client.get(self.url, {"q": "first"}) self.assertEqual(r.status_code, 200) - - u = self.users["user"] - self.assertSetEqual( - {e.verbose_name for e in r.context["results"][0].entries}, - {"{} ({})".format(u, u.profile.account_kfet.trigramme)}, - ) + self.assertSetEqual(set(r.context["accounts"]), set([("000", "first last")])) class AccountReadViewTests(ViewTestCaseMixin, TestCase): @@ -233,9 +221,7 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase): if user is None: response = client.get(url) self.assertRedirects( - response, - "/gestion/login?next={}".format(url), - fetch_redirect_response=False, + response, "/login?next={}".format(url), fetch_redirect_response=False ) else: client.login(username=user, password=user) @@ -298,8 +284,8 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase): class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.account.update" - url_kwargs = {"trigramme": "100"} - url_expected = "/k-fet/accounts/100/edit" + url_kwargs = {"trigramme": "001"} + url_expected = "/k-fet/accounts/001/edit" http_methods = ["GET", "POST"] @@ -319,25 +305,33 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): "promo": "", # 'is_frozen': not checked # Account password - "pwd1": "changed_pwd", - "pwd2": "changed_pwd", + "pwd1": "", + "pwd2": "", } def get_users_extra(self): return { + "user1": create_user("user1", "001"), "team1": create_team("team1", "101", perms=["kfet.change_account"]), - "team2": create_team("team2", "102"), } + # Users with forbidden access users should get a 404 here, to avoid leaking trigrams + # See issue #224 + def test_forbidden(self): + for method in ["get", "post"]: + for user in self.auth_forbidden: + self.assertRedirectsToLoginOr404(user, method, self.url_expected) + self.assertRedirectsToLoginOr404( + user, method, "/k-fet/accounts/NEX/edit" + ) + def assertRedirectsToLoginOr404(self, user, method, url): client = Client() meth = getattr(client, method) if user is None: response = meth(url) self.assertRedirects( - response, - "/gestion/login?next={}".format(url), - fetch_redirect_response=False, + response, "/login?next={}".format(url), fetch_redirect_response=False ) else: client.login(username=user, password=user) @@ -348,55 +342,46 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) + def test_get_ok_self(self): + client = Client() + client.login(username="user1", password="user1") + r = client.get(self.url) + self.assertEqual(r.status_code, 200) + def test_post_ok(self): client = Client() client.login(username="team1", password="team1") - r = client.post(self.url, self.post_data, follow=True) + r = client.post(self.url, self.post_data) self.assertRedirects(r, reverse("kfet.account.read", args=["051"])) - # Comportement attendu : compte modifié, - # utilisateur/mdp inchangé, warning pour le mdp - - self.accounts["team"].refresh_from_db() - self.users["team"].refresh_from_db() + self.accounts["user1"].refresh_from_db() + self.users["user1"].refresh_from_db() self.assertInstanceExpected( - self.accounts["team"], - {"first_name": "team", "last_name": "member", "trigramme": "051"}, - ) - self.assertEqual(self.accounts["team"].password, hash_password("kfetpwd_team")) - - self.assertTrue( - any("mot de passe" in str(msg).casefold() for msg in r.context["messages"]) + self.accounts["user1"], + {"first_name": "first", "last_name": "last", "trigramme": "051"}, ) def test_post_ok_self(self): - r = self.client.post(self.url, self.post_data, follow=True) - self.assertRedirects(r, reverse("kfet.account.read", args=["051"])) + client = Client() + client.login(username="user1", password="user1") - self.accounts["team"].refresh_from_db() - self.users["team"].refresh_from_db() + post_data = {"first_name": "The first", "last_name": "The last"} - # Comportement attendu : compte/mdp modifié, utilisateur inchangé + r = client.post(self.url, post_data) + self.assertRedirects(r, reverse("kfet.account.read", args=["001"])) + + self.accounts["user1"].refresh_from_db() + self.users["user1"].refresh_from_db() self.assertInstanceExpected( - self.accounts["team"], - {"first_name": "team", "last_name": "member", "trigramme": "051"}, + self.accounts["user1"], {"first_name": "first", "last_name": "last"} ) - self.assertEqual(self.accounts["team"].password, hash_password("changed_pwd")) def test_post_forbidden(self): - client = Client() - client.login(username="team2", password="team2") - r = client.post(self.url, self.post_data) - - self.assertTrue( - any( - "permission refusée" in str(msg).casefold() - for msg in r.context["messages"] - ) - ) + r = self.client.post(self.url, self.post_data) + self.assertForbiddenKfet(r) class AccountDeleteViewTests(ViewTestCaseMixin, TestCase): @@ -466,18 +451,15 @@ class AccountGroupListViewTests(ViewTestCaseMixin, TestCase): def setUp(self): super().setUp() - self.group1 = KFetGroup.objects.create(name="Group1") - self.group2 = KFetGroup.objects.create(name="Group2") + self.group1 = Group.objects.create(name="K-Fêt - Group1") + self.group2 = Group.objects.create(name="K-Fêt - Group2") def test_ok(self): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context["groups"], - [self.group1.pk, self.group2.pk], - transform=lambda group: group.pk, - ordered=False, + r.context["groups"], map(repr, [self.group1, self.group2]), ordered=False ) @@ -515,12 +497,11 @@ class AccountGroupCreateViewTests(ViewTestCaseMixin, TestCase): r = self.client.post(self.url, self.post_data) self.assertRedirects(r, reverse("kfet.account.group")) - group = KFetGroup.objects.get(name="The Group") + group = Group.objects.get(name="K-Fêt The Group") self.assertQuerysetEqual( group.permissions.all(), map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), - transform=repr, ordered=False, ) @@ -557,7 +538,7 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): def setUp(self): super().setUp() self.perms = get_perms("kfet.is_team", "kfet.manage_perms") - self.group = KFetGroup.objects.create(name="Group") + self.group = Group.objects.create(name="K-Fêt - Group") self.group.permissions.set(self.perms.values()) def test_get_ok(self): @@ -570,11 +551,10 @@ class AccountGroupUpdateViewTests(ViewTestCaseMixin, TestCase): self.group.refresh_from_db() - self.assertEqual(self.group.name, "The Group") + self.assertEqual(self.group.name, "K-Fêt The Group") self.assertQuerysetEqual( self.group.permissions.all(), map(repr, [self.perms["kfet.is_team"], self.perms["kfet.manage_perms"]]), - transform=repr, ordered=False, ) @@ -602,7 +582,6 @@ class AccountNegativeListViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( r.context["negatives"], map(repr, [self.accounts["user"].negative]), - transform=repr, ordered=False, ) @@ -632,9 +611,7 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): if user is None: response = client.get(url) self.assertRedirects( - response, - "/gestion/login?next={}".format(url), - fetch_redirect_response=False, + response, "/login?next={}".format(url), fetch_redirect_response=False ) else: client.login(username=user, password=user) @@ -651,50 +628,38 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): expected_stats = [ { - "label": "Tout le temps", + "label": "Derniers mois", "url": { "path": base_url, "query": { - "scale-name": ["month"], - "scale-last": ["True"], - "scale-begin": [ - self.accounts["user1"] - .created_at.replace(tzinfo=None) - .isoformat(" ") - ], + "scale_n_steps": ["7"], + "scale_name": ["month"], + "types": ["['purchase']"], + "scale_last": ["True"], }, }, }, { - "label": "1 an", + "label": "Dernières semaines", "url": { "path": base_url, "query": { - "scale-n_steps": ["12"], - "scale-name": ["month"], - "scale-last": ["True"], + "scale_n_steps": ["7"], + "scale_name": ["week"], + "types": ["['purchase']"], + "scale_last": ["True"], }, }, }, { - "label": "3 mois", + "label": "Derniers jours", "url": { "path": base_url, "query": { - "scale-n_steps": ["13"], - "scale-name": ["week"], - "scale-last": ["True"], - }, - }, - }, - { - "label": "2 semaines", - "url": { - "path": base_url, - "query": { - "scale-n_steps": ["14"], - "scale-name": ["day"], - "scale-last": ["True"], + "scale_n_steps": ["7"], + "scale_name": ["day"], + "types": ["['purchase']"], + "scale_last": ["True"], }, }, }, @@ -703,7 +668,7 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): for stat, expected in zip(content["stats"], expected_stats): expected_url = expected.pop("url") self.assertUrlsEqual(stat["url"], expected_url) - self.assertEqual(stat, {**stat, **expected}) + self.assertDictContainsSubset(expected, stat) class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): @@ -728,9 +693,7 @@ class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): if user is None: response = client.get(url) self.assertRedirects( - response, - "/gestion/login?next={}".format(url), - fetch_redirect_response=False, + response, "/login?next={}".format(url), fetch_redirect_response=False ) else: client.login(username=user, password=user) @@ -741,9 +704,7 @@ class AccountStatOperationViewTests(ViewTestCaseMixin, TestCase): return {"user1": create_user("user1", "001")} def test_ok(self): - r = self.client.get( - self.url, {"scale-name": "day", "scale-n_steps": 7, "scale-last": True} - ) + r = self.client.get(self.url) self.assertEqual(r.status_code, 200) @@ -769,9 +730,7 @@ class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase): if user is None: response = client.get(url) self.assertRedirects( - response, - "/gestion/login?next={}".format(url), - fetch_redirect_response=False, + response, "/login?next={}".format(url), fetch_redirect_response=False ) else: client.login(username=user, password=user) @@ -812,7 +771,7 @@ class AccountStatBalanceListViewTests(ViewTestCaseMixin, TestCase): for stat, expected in zip(content["stats"], expected_stats): expected_url = expected.pop("url") self.assertUrlsEqual(stat["url"], expected_url) - self.assertEqual(stat, {**stat, **expected}) + self.assertDictContainsSubset(expected, stat) class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): @@ -835,9 +794,7 @@ class AccountStatBalanceViewTests(ViewTestCaseMixin, TestCase): if user is None: response = client.get(url) self.assertRedirects( - response, - "/gestion/login?next={}".format(url), - fetch_redirect_response=False, + response, "/login?next={}".format(url), fetch_redirect_response=False ) else: client.login(username=user, password=user) @@ -880,7 +837,6 @@ class CheckoutListViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( r.context["checkouts"], map(repr, [self.checkout1, self.checkout2]), - transform=repr, ordered=False, ) @@ -1071,7 +1027,6 @@ class CheckoutStatementListViewTests(ViewTestCaseMixin, TestCase): self.assertQuerysetEqual( r.context["checkoutstatements"], map(repr, expected_statements), - transform=repr, ordered=False, ) @@ -1298,9 +1253,7 @@ class ArticleCategoryListViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context["categories"], - map(repr, [self.category1, self.category2]), - transform=repr, + r.context["categories"], map(repr, [self.category1, self.category2]) ) @@ -1375,9 +1328,7 @@ class ArticleListViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context["articles"], - map(repr, [self.article1, self.article2]), - transform=repr, + r.context["articles"], map(repr, [self.article1, self.article2]) ) @@ -1573,21 +1524,6 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): self.article = Article.objects.create( name="Article", category=ArticleCategory.objects.create(name="Category") ) - checkout = Checkout.objects.create( - name="Checkout", - created_by=self.accounts["team"], - balance=5, - valid_from=self.now, - valid_to=self.now + timedelta(days=5), - ) - - self.opegroup = create_operation_group( - on_acc=self.accounts["user"], - checkout=checkout, - content=[ - {"type": Operation.PURCHASE, "article": self.article, "article_nb": 2}, - ], - ) def test_ok(self): r = self.client.get(self.url) @@ -1599,46 +1535,35 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): expected_stats = [ { - "label": "Tout le temps", + "label": "Derniers mois", "url": { "path": base_url, "query": { - "scale-name": ["month"], - "scale-last": ["True"], - "scale-begin": [self.opegroup.at.strftime("%Y-%m-%d %H:%M:%S")], + "scale_n_steps": ["7"], + "scale_name": ["month"], + "scale_last": ["True"], }, }, }, { - "label": "1 an", + "label": "Dernières semaines", "url": { "path": base_url, "query": { - "scale-n_steps": ["12"], - "scale-name": ["month"], - "scale-last": ["True"], + "scale_n_steps": ["7"], + "scale_name": ["week"], + "scale_last": ["True"], }, }, }, { - "label": "3 mois", + "label": "Derniers jours", "url": { "path": base_url, "query": { - "scale-n_steps": ["13"], - "scale-name": ["week"], - "scale-last": ["True"], - }, - }, - }, - { - "label": "2 semaines", - "url": { - "path": base_url, - "query": { - "scale-n_steps": ["14"], - "scale-name": ["day"], - "scale-last": ["True"], + "scale_n_steps": ["7"], + "scale_name": ["day"], + "scale_last": ["True"], }, }, }, @@ -1647,7 +1572,7 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): for stat, expected in zip(content["stats"], expected_stats): expected_url = expected.pop("url") self.assertUrlsEqual(stat["url"], expected_url) - self.assertEqual(stat, {**stat, **expected}) + self.assertDictContainsSubset(expected, stat) class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase): @@ -1671,9 +1596,7 @@ class ArticleStatSalesViewTests(ViewTestCaseMixin, TestCase): ) def test_ok(self): - r = self.client.get( - self.url, {"scale-name": "day", "scale-n_steps": 7, "scale-last": True} - ) + r = self.client.get(self.url) self.assertEqual(r.status_code, 200) @@ -1716,7 +1639,7 @@ class KPsulCheckoutDataViewTests(ViewTestCaseMixin, TestCase): expected = {"name": "Checkout", "balance": "10.00"} - self.assertEqual(content, {**content, **expected}) + self.assertDictContainsSubset(expected, content) self.assertSetEqual( set(content.keys()), @@ -1806,8 +1729,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Another Article, price=2.5, stock=20, no COF reduction self.article_no_reduction = Article.objects.create( category=ArticleCategory.objects.create( - name="Category_no_reduction", - has_reduction=False, + name="Category_no_reduction", has_reduction=False, ), name="Article_no_reduction", price=Decimal("2.5"), @@ -1819,13 +1741,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.balance = Decimal("50.00") self.account.save() - # Create a channel to listen to KPsul's messages - channel_layer = get_channel_layer() - self.channel = async_to_sync(channel_layer.new_channel)() - - async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel) - - self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel) + # Mock consumer of K-Psul websocket to catch what we're sending + kpsul_consumer_patcher = mock.patch("kfet.consumers.KPsul") + self.kpsul_consumer_mock = kpsul_consumer_patcher.start() + self.addCleanup(kpsul_consumer_patcher.stop) # Reset cache of kfet config kfet_config._conf_init = False @@ -1869,10 +1788,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_on_acc", "invalid_formset"], - ) + self.assertEqual(json_data["errors"]["operation_group"], ["on_acc"]) def test_group_on_acc_expects_comment(self): user_add_perms(self.users["team"], ["kfet.perform_commented_operations"]) @@ -1915,7 +1831,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["need_comment"], True) + self.assertEqual(json_data["errors"]["need_comment"], True) def test_invalid_group_on_acc_needs_comment_requires_perm(self): self.account.trigramme = "#13" @@ -1938,11 +1854,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["missing_perms"], - ["Enregistrer des commandes avec commentaires"], + json_data["errors"]["missing_perms"], + ["[kfet] Enregistrer des commandes avec commentaires"], ) - def test_error_on_acc_frozen(self): + def test_group_on_acc_frozen(self): + user_add_perms(self.users["team"], ["kfet.override_frozen_protection"]) self.account.is_frozen = True self.account.save() @@ -1959,9 +1876,30 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) resp = self.client.post(self.url, data) - self.assertEqual(resp.status_code, 400) + self._assertResponseOk(resp) + + def test_invalid_group_on_acc_frozen_requires_perm(self): + self.account.is_frozen = True + self.account.save() + + data = dict( + self.base_post_data, + **{ + "comment": "A comment to explain it", + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual([e["code"] for e in json_data["errors"]], ["frozen_acc"]) + self.assertEqual( + json_data["errors"]["missing_perms"], ["[kfet] Forcer le gel d'un compte"] + ) def test_invalid_group_checkout(self): self.checkout.valid_from -= timedelta(days=300) @@ -1973,10 +1911,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_checkout", "invalid_formset"], - ) + self.assertEqual(json_data["errors"]["operation_group"], ["checkout"]) def test_invalid_group_expects_one_operation(self): data = dict(self.base_post_data) @@ -1984,10 +1919,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], - ) + self.assertEqual(json_data["errors"]["operations"], []) def test_purchase_with_user_is_nof_cof(self): self.account.cofprofile.is_cof = False @@ -2045,7 +1977,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check response content self.assertDictEqual( json_data, - {"errors": []}, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, ) # Check object updates @@ -2057,16 +1994,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(self.article.stock, 18) # Check websocket data - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", { - "type": "kpsul", - "groups": [ + "opegroups": [ { "add": True, - "type": "operation", "at": mock.ANY, "amount": Decimal("-5.00"), "checkout__name": "Checkout", @@ -2075,7 +2008,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": None, - "entries": [ + "opes": [ { "id": operation.pk, "addcost_amount": None, @@ -2199,9 +2132,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], + [{"__all__": ["Un achat nécessite un article et une quantité"]}], ) def test_invalid_purchase_expects_article_nb(self): @@ -2219,9 +2152,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], + [{"__all__": ["Un achat nécessite un article et une quantité"]}], ) def test_invalid_purchase_expects_article_nb_greater_than_1(self): @@ -2239,9 +2172,16 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], + [ + { + "__all__": ["Un achat nécessite un article et une quantité"], + "article_nb": [ + "Assurez-vous que cette valeur est supérieure ou " "égale à 1." + ], + } + ], ) def test_invalid_operation_not_purchase_with_cash(self): @@ -2260,10 +2200,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_liq"], - ) + self.assertEqual(json_data["errors"]["account"], "LIQ") def test_deposit(self): user_add_perms(self.users["team"], ["kfet.perform_deposit"]) @@ -2316,7 +2253,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - {"errors": []}, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, ) self.account.refresh_from_db() @@ -2324,16 +2266,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("110.75")) - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", { - "type": "kpsul", - "groups": [ + "opegroups": [ { "add": True, - "type": "operation", "at": mock.ANY, "amount": Decimal("10.75"), "checkout__name": "Checkout", @@ -2342,7 +2280,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": "100", - "entries": [ + "opes": [ { "id": operation.pk, "addcost_amount": None, @@ -2378,9 +2316,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] ) def test_invalid_deposit_too_many_params(self): @@ -2398,9 +2335,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] ) def test_invalid_deposit_expects_positive_amount(self): @@ -2418,9 +2354,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Charge non positive"]}] ) def test_invalid_deposit_requires_perm(self): @@ -2438,7 +2373,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["missing_perms"], ["Effectuer une charge"]) + self.assertEqual( + json_data["errors"]["missing_perms"], ["[kfet] Effectuer une charge"] + ) def test_withdraw(self): data = dict( @@ -2490,7 +2427,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - {"errors": []}, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, ) self.account.refresh_from_db() @@ -2498,16 +2440,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("89.25")) - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", { - "type": "kpsul", - "groups": [ + "opegroups": [ { "add": True, - "type": "operation", "at": mock.ANY, "amount": Decimal("-10.75"), "checkout__name": "Checkout", @@ -2516,7 +2454,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": None, - "entries": [ + "opes": [ { "id": operation.pk, "addcost_amount": None, @@ -2552,9 +2490,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] ) def test_invalid_withdraw_too_many_params(self): @@ -2572,9 +2509,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] ) def test_invalid_withdraw_expects_negative_amount(self): @@ -2592,9 +2528,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_formset"], + self.assertEqual( + json_data["errors"]["operations"], [{"__all__": ["Retrait non négatif"]}] ) def test_edit(self): @@ -2650,7 +2585,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - {"errors": []}, + { + "operationgroup": operation_group.pk, + "operations": [operation.pk], + "warnings": {}, + "errors": {}, + }, ) self.account.refresh_from_db() @@ -2658,16 +2598,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", { - "type": "kpsul", - "groups": [ + "opegroups": [ { "add": True, - "type": "operation", "at": mock.ANY, "amount": Decimal("10.75"), "checkout__name": "Checkout", @@ -2676,7 +2612,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": "100", - "entries": [ + "opes": [ { "id": operation.pk, "addcost_amount": None, @@ -2714,8 +2650,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["missing_perms"], - ["Modifier la balance d'un compte"], + json_data["errors"]["missing_perms"], + ["[kfet] Modifier la balance d'un compte"], ) def test_invalid_edit_expects_comment(self): @@ -2735,7 +2671,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["need_comment"], True) + self.assertEqual(json_data["errors"]["need_comment"], True) def _setup_addcost(self): self.register_user("addcost", create_user("addcost", "ADD")) @@ -2776,9 +2712,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data = self.receive_msg() - ws_data_ope = ws_data["groups"][0]["entries"][0] - + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2816,9 +2752,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data = self.receive_msg() - ws_data_ope = ws_data["groups"][0]["entries"][0] - + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("0.80")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2854,9 +2790,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("106.00")) - ws_data = self.receive_msg() - ws_data_ope = ws_data["groups"][0]["entries"][0] - + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], Decimal("1.00")) self.assertEqual(ws_data_ope["addcost_for__trigramme"], "ADD") @@ -2890,9 +2826,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.accounts["addcost"].refresh_from_db() self.assertEqual(self.accounts["addcost"].balance, Decimal("15.00")) - ws_data = self.receive_msg() - ws_data_ope = ws_data["groups"][0]["entries"][0] - + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) @@ -2925,9 +2861,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.accounts["addcost"].refresh_from_db() self.assertEqual(self.accounts["addcost"].balance, Decimal("0.00")) - ws_data = self.receive_msg() - ws_data_ope = ws_data["groups"][0]["entries"][0] - + ws_data_ope = self.kpsul_consumer_mock.group_send.call_args[0][1]["opegroups"][ + 0 + ]["opes"][0] self.assertEqual(ws_data_ope["addcost_amount"], None) self.assertEqual(ws_data_ope["addcost_for__trigramme"], None) @@ -3022,10 +2958,62 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["missing_perms"], - ["Enregistrer des commandes en négatif"], + json_data["errors"], + {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, ) + def test_invalid_negative_exceeds_allowed_duration_from_config(self): + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) + kfet_config.set(overdraft_duration=timedelta(days=5)) + self.account.balance = Decimal("1.00") + self.account.save() + self.account.negative = AccountNegative.objects.create( + account=self.account, start=timezone.now() - timedelta(days=5, minutes=1) + ) + + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + + def test_invalid_negative_exceeds_allowed_duration_from_account(self): + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) + kfet_config.set(overdraft_duration=timedelta(days=5)) + self.account.balance = Decimal("1.00") + self.account.save() + self.account.negative = AccountNegative.objects.create( + account=self.account, + start=timezone.now() - timedelta(days=3), + authz_overdraft_until=timezone.now() - timedelta(seconds=1), + ) + + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + def test_invalid_negative_exceeds_amount_allowed_from_config(self): user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) kfet_config.set(overdraft_amount=Decimal("-1.00")) @@ -3045,13 +3033,38 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) resp = self.client.post(self.url, data) - self.assertEqual(resp.status_code, 400) + self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["negative"], + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + + def test_invalid_negative_exceeds_amount_allowed_from_account(self): + user_add_perms(self.users["team"], ["kfet.perform_negative_operations"]) + kfet_config.set(overdraft_amount=Decimal("10.00")) + self.account.balance = Decimal("1.00") + self.account.save() + self.account.update_negative() + self.account.negative = AccountNegative.objects.create( + account=self.account, + start=timezone.now() - timedelta(days=3), + authz_overdraft_amount=Decimal("1.00"), ) + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "1", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article.pk), + "form-0-article_nb": "2", + } + ) + resp = self.client.post(self.url, data) + + self.assertEqual(resp.status_code, 403) + json_data = json.loads(resp.content.decode("utf-8")) + self.assertEqual(json_data["errors"], {"negative": ["000"]}) + def test_multi_0(self): article2 = Article.objects.create( name="Article 2", @@ -3135,7 +3148,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check response content self.assertDictEqual( json_data, - {"errors": []}, + { + "operationgroup": operation_group.pk, + "operations": [operation_list[0].pk, operation_list[1].pk], + "warnings": {}, + "errors": {}, + }, ) # Check object updates @@ -3149,16 +3167,12 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(article2.stock, -6) # Check websocket data - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_once_with( + "kfet.kpsul", { - "type": "kpsul", - "groups": [ + "opegroups": [ { "add": True, - "type": "operation", "at": mock.ANY, "amount": Decimal("-9.00"), "checkout__name": "Checkout", @@ -3167,7 +3181,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "is_cof": False, "on_acc__trigramme": "000", "valid_by__trigramme": None, - "entries": [ + "opes": [ { "id": operation_list[0].pk, "addcost_amount": None, @@ -3220,7 +3234,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): """ - url_name = "kfet.operations.cancel" + url_name = "kfet.kpsul.cancel_operations" url_expected = "/k-fet/k-psul/cancel_operations" http_methods = ["POST"] @@ -3247,14 +3261,10 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.account.balance = Decimal("50.00") self.account.save() - # Create a channel to listen to KPsul's messages - channel_layer = get_channel_layer() - - self.channel = async_to_sync(channel_layer.new_channel)() - - async_to_sync(channel_layer.group_add)("kfet.kpsul", self.channel) - - self.receive_msg = lambda: async_to_sync(channel_layer.receive)(self.channel) + # Mock consumer of K-Psul websocket to catch what we're sending + kpsul_consumer_patcher = mock.patch("kfet.consumers.KPsul") + self.kpsul_consumer_mock = kpsul_consumer_patcher.start() + self.addCleanup(kpsul_consumer_patcher.stop) def _assertResponseOk(self, response): """ @@ -3281,10 +3291,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["invalid_request"], - ) + self.assertEqual(json_data["errors"], {}) def test_invalid_operation_not_exist(self): data = {"operations[]": ["1000"]} @@ -3292,10 +3299,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["cancel_missing"], - ) + self.assertEqual(json_data["errors"], {"opes_notexisting": [1000]}) @mock.patch("django.utils.timezone.now") def test_purchase(self, now_mock): @@ -3304,11 +3308,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): on_acc=self.account, checkout=self.checkout, content=[ - { - "type": Operation.PURCHASE, - "article": self.article, - "article_nb": 2, - } + {"type": Operation.PURCHASE, "article": self.article, "article_nb": 2} ], ) operation = group.opes.get() @@ -3353,26 +3353,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, - { - "canceled": [ - { - "id": operation.id, - # l'encodage des dates en JSON est relou... - "canceled_at": mock.ANY, - "canceled_by__trigramme": None, - } - ], - "errors": [], - "warnings": {}, - "opegroups_to_update": [ - { - "id": group.pk, - "amount": str(group.amount), - "is_cof": group.is_cof, - } - ], - }, + json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} ) self.account.refresh_from_db() @@ -3382,12 +3363,25 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_with( + "kfet.kpsul", { - "type": "kpsul", + "opegroups": [ + { + "cancellation": True, + "id": group.pk, + "amount": Decimal("0.00"), + "is_cof": False, + } + ], + "opes": [ + { + "cancellation": True, + "id": operation.pk, + "canceled_by__trigramme": None, + "canceled_at": self.now + timedelta(seconds=15), + } + ], "checkouts": [], "articles": [{"id": self.article.pk, "stock": 22}], }, @@ -3450,11 +3444,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("95.00")) - ws_data = self.receive_msg() - + ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][ + "checkouts" + ] self.assertListEqual( - ws_data["checkouts"], - [{"id": self.checkout.pk, "balance": Decimal("95.00")}], + ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("95.00")}] ) def test_purchase_cash_with_addcost(self): @@ -3490,11 +3484,11 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): addcost_account.refresh_from_db() self.assertEqual(addcost_account.balance, Decimal("9.00")) - ws_data = self.receive_msg() - + ws_data_checkouts = self.kpsul_consumer_mock.group_send.call_args[0][1][ + "checkouts" + ] self.assertListEqual( - ws_data["checkouts"], - [{"id": self.checkout.pk, "balance": Decimal("94.00")}], + ws_data_checkouts, [{"id": self.checkout.pk, "balance": Decimal("94.00")}] ) @mock.patch("django.utils.timezone.now") @@ -3547,26 +3541,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, - { - "canceled": [ - { - "id": operation.id, - # l'encodage des dates en JSON est relou... - "canceled_at": mock.ANY, - "canceled_by__trigramme": None, - } - ], - "errors": [], - "warnings": {}, - "opegroups_to_update": [ - { - "id": group.pk, - "amount": str(group.amount), - "is_cof": group.is_cof, - } - ], - }, + json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} ) self.account.refresh_from_db() @@ -3576,12 +3551,25 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("89.25")) - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_with( + "kfet.kpsul", { - "type": "kpsul", + "opegroups": [ + { + "cancellation": True, + "id": group.pk, + "amount": Decimal("0.00"), + "is_cof": False, + } + ], + "opes": [ + { + "cancellation": True, + "id": operation.pk, + "canceled_by__trigramme": None, + "canceled_at": self.now + timedelta(seconds=15), + } + ], "checkouts": [{"id": self.checkout.pk, "balance": Decimal("89.25")}], "articles": [], }, @@ -3637,26 +3625,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, - { - "canceled": [ - { - "id": operation.id, - # l'encodage des dates en JSON est relou... - "canceled_at": mock.ANY, - "canceled_by__trigramme": None, - } - ], - "errors": [], - "warnings": {}, - "opegroups_to_update": [ - { - "id": group.pk, - "amount": str(group.amount), - "is_cof": group.is_cof, - } - ], - }, + json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} ) self.account.refresh_from_db() @@ -3666,12 +3635,25 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("110.75")) - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, + self.kpsul_consumer_mock.group_send.assert_called_with( + "kfet.kpsul", { - "type": "kpsul", + "opegroups": [ + { + "cancellation": True, + "id": group.pk, + "amount": Decimal("0.00"), + "is_cof": False, + } + ], + "opes": [ + { + "cancellation": True, + "id": operation.pk, + "canceled_by__trigramme": None, + "canceled_at": self.now + timedelta(seconds=15), + } + ], "checkouts": [{"id": self.checkout.pk, "balance": Decimal("110.75")}], "articles": [], }, @@ -3727,26 +3709,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, - { - "canceled": [ - { - "id": operation.id, - # l'encodage des dates en JSON est relou... - "canceled_at": mock.ANY, - "canceled_by__trigramme": None, - } - ], - "errors": [], - "warnings": {}, - "opegroups_to_update": [ - { - "id": group.pk, - "amount": str(group.amount), - "is_cof": group.is_cof, - } - ], - }, + json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} ) self.account.refresh_from_db() @@ -3756,11 +3719,28 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.checkout.refresh_from_db() self.assertEqual(self.checkout.balance, Decimal("100.00")) - ws_data = self.receive_msg() - - self.assertDictEqual( - ws_data, - {"type": "kpsul", "checkouts": [], "articles": []}, + self.kpsul_consumer_mock.group_send.assert_called_with( + "kfet.kpsul", + { + "opegroups": [ + { + "cancellation": True, + "id": group.pk, + "amount": Decimal("0.00"), + "is_cof": False, + } + ], + "opes": [ + { + "cancellation": True, + "id": operation.pk, + "canceled_by__trigramme": None, + "canceled_at": self.now + timedelta(seconds=15), + } + ], + "checkouts": [], + "articles": [], + }, ) @mock.patch("django.utils.timezone.now") @@ -3802,8 +3782,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["missing_perms"], - ["Annuler des commandes non récentes"], + json_data["errors"], + {"missing_perms": ["[kfet] Annuler des commandes non récentes"]}, ) def test_already_canceled(self): @@ -3927,12 +3907,9 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): data = {"operations[]": [str(operation.pk)]} resp = self.client.post(self.url, data) - self.assertEqual(resp.status_code, 400) + self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) - self.assertCountEqual( - [e["code"] for e in json_data["errors"]], - ["negative"], - ) + self.assertEqual(json_data["errors"], {"negative": [self.account.trigramme]}) def test_invalid_negative_requires_perms(self): kfet_config.set(overdraft_amount=Decimal("40.00")) @@ -3951,8 +3928,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["missing_perms"], - ["Enregistrer des commandes en négatif"], + json_data["errors"], + {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, ) def test_partial_0(self): @@ -3984,33 +3961,13 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): group.refresh_from_db() self.assertEqual(group.amount, Decimal("10.75")) self.assertEqual(group.opes.exclude(canceled_at=None).count(), 3) - self.maxDiff = None + self.assertDictEqual( json_data, { - "canceled": [ - { - "id": operation1.id, - # l'encodage des dates en JSON est relou... - "canceled_at": mock.ANY, - "canceled_by__trigramme": None, - }, - { - "id": operation2.id, - # l'encodage des dates en JSON est relou... - "canceled_at": mock.ANY, - "canceled_by__trigramme": None, - }, - ], - "errors": [], + "canceled": [operation1.pk, operation2.pk], "warnings": {"already_canceled": [operation3.pk]}, - "opegroups_to_update": [ - { - "id": group.pk, - "amount": str(group.amount), - "is_cof": group.is_cof, - } - ], + "errors": {}, }, ) @@ -4100,7 +4057,7 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase): ] for expected, article in zip(expected_list, articles): - self.assertEqual(article, {**article, **expected}) + self.assertDictContainsSubset(expected, article) self.assertSetEqual( set(article.keys()), set( @@ -4163,19 +4120,13 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.history.json" url_expected = "/k-fet/history.json" - auth_user = "team" - auth_forbidden = [None, "user", "noaccount"] + auth_user = "user" + auth_forbidden = [None] def test_ok(self): r = self.client.post(self.url) self.assertEqual(r.status_code, 200) - def get_users_extra(self): - noaccount = User.objects.create(username="noaccount") - noaccount.set_password("noaccount") - noaccount.save() - return {"noaccount": noaccount} - class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.account.read.json" @@ -4200,7 +4151,7 @@ class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): content = json.loads(r.content.decode("utf-8")) expected = {"name": "first last", "trigramme": "000", "balance": "0.00"} - self.assertEqual(content, {**content, **expected}) + self.assertDictContainsSubset(expected, content) self.assertSetEqual( set(content.keys()), @@ -4444,9 +4395,7 @@ class InventoryListViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) inventories = r.context["inventories"] - self.assertQuerysetEqual( - inventories, map(repr, [self.inventory]), transform=repr - ) + self.assertQuerysetEqual(inventories, map(repr, [self.inventory])) class InventoryCreateViewTests(ViewTestCaseMixin, TestCase): @@ -4527,87 +4476,6 @@ class InventoryReadViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) -class InventoryDeleteViewTests(ViewTestCaseMixin, TestCase): - url_name = "kfet.inventory.delete" - - auth_user = "team1" - auth_forbidden = [None, "user", "team"] - - def get_users_extra(self): - return { - "user1": create_user("user1", "001"), - "team1": create_team("team1", "101", perms=["kfet.delete_inventory"]), - } - - @property - def url_kwargs(self): - return {"pk": self.inventory1.pk} - - @property - def url_expected(self): - return "/k-fet/inventaires/{}/delete".format(self.inventory1.pk) - - def setUp(self): - super().setUp() - # Deux inventaires : un avec article 1 + 2, l'autre avec 1 + 3 - self.inventory1 = Inventory.objects.create( - by=self.accounts["team"], at=self.now - ) - self.inventory2 = Inventory.objects.create( - by=self.accounts["team"], at=self.now + timedelta(days=1) - ) - category = ArticleCategory.objects.create(name="Category") - # Le stock des articles correspond à leur dernier inventaire - self.article1 = Article.objects.create( - name="Article1", category=category, stock=51 - ) - self.article2 = Article.objects.create( - name="Article2", category=category, stock=42 - ) - self.article3 = Article.objects.create( - name="Article3", category=category, stock=42 - ) - - InventoryArticle.objects.create( - inventory=self.inventory1, - article=self.article1, - stock_old=23, - stock_new=42, - ) - InventoryArticle.objects.create( - inventory=self.inventory1, - article=self.article2, - stock_old=23, - stock_new=42, - ) - InventoryArticle.objects.create( - inventory=self.inventory2, - article=self.article1, - stock_old=42, - stock_new=51, - ) - InventoryArticle.objects.create( - inventory=self.inventory2, - article=self.article3, - stock_old=23, - stock_new=42, - ) - - def test_ok(self): - r = self.client.post(self.url) - self.assertRedirects(r, reverse("kfet.inventory")) - - # On vérifie que l'inventaire n'existe plus - self.assertFalse(Inventory.objects.filter(pk=self.inventory1.pk).exists()) - # On check les stocks - self.article1.refresh_from_db() - self.article2.refresh_from_db() - self.article3.refresh_from_db() - self.assertEqual(self.article1.stock, 51) - self.assertEqual(self.article2.stock, 23) - self.assertEqual(self.article3.stock, 42) - - class OrderListViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.order" url_expected = "/k-fet/orders/" @@ -4633,7 +4501,7 @@ class OrderListViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) orders = r.context["orders"] - self.assertQuerysetEqual(orders, map(repr, [self.order]), transform=repr) + self.assertQuerysetEqual(orders, map(repr, [self.order])) class OrderReadViewTests(ViewTestCaseMixin, TestCase): @@ -4848,9 +4716,7 @@ class OrderToInventoryViewTests(ViewTestCaseMixin, TestCase): inventory, {"by": self.accounts["team1"], "at": self.now, "order": self.order}, ) - self.assertQuerysetEqual( - inventory.articles.all(), map(repr, [self.article]), transform=repr - ) + self.assertQuerysetEqual(inventory.articles.all(), map(repr, [self.article])) compte = InventoryArticle.objects.get(article=self.article) diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index a7962f33..95b48d42 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -39,7 +39,7 @@ class TestCaseMixin: querystring = QueryDict(mutable=True) querystring["next"] = full_path - login_url = "/gestion/login?" + querystring.urlencode(safe="/") + login_url = "/login?" + querystring.urlencode(safe="/") # We don't focus on what the login view does. # So don't fetch the redirect. @@ -79,15 +79,10 @@ class TestCaseMixin: self.assertEqual(response.status_code, 200) try: form = response.context[form_ctx] - errors = [y for x in form.errors.as_data().values() for y in x] - self.assertTrue(any(e.code == "permission-denied" for e in errors)) + self.assertIn("Permission refusée", form.non_field_errors()) except (AssertionError, AttributeError, KeyError): - self.assertTrue( - any( - "permission-denied" in msg.tags - for msg in response.context["messages"] - ) - ) + messages = [str(msg) for msg in response.context["messages"]] + self.assertIn("Permission refusée", messages) except AssertionError: request = response.wsgi_request raise AssertionError( @@ -253,10 +248,7 @@ class ViewTestCaseMixin(TestCaseMixin): self.register_user(label, user) if self.auth_user: - self.client.force_login( - self.users[self.auth_user], - backend="django.contrib.auth.backends.ModelBackend", - ) + self.client.force_login(self.users[self.auth_user]) def tearDown(self): del self.users_base diff --git a/kfet/urls.py b/kfet/urls.py index f33cea03..03c174f3 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -1,7 +1,7 @@ from django.contrib.auth.decorators import permission_required from django.urls import include, path, register_converter -from kfet import converters, views +from kfet import autocomplete, converters, views from kfet.decorators import teamkfet_required register_converter(converters.TrigrammeConverter, "trigramme") @@ -9,10 +9,6 @@ register_converter(converters.TrigrammeConverter, "trigramme") urlpatterns = [ path("login/generic", views.login_generic, name="kfet.login.generic"), path("history", views.history, name="kfet.history"), - path("contact", views.ContactView.as_view(), name="kfet.contact"), - path( - "demande-soiree", views.DemandeSoireeView.as_view(), name="kfet.demande-soiree" - ), # ----- # Account urls # ----- @@ -42,13 +38,13 @@ urlpatterns = [ ), path( "autocomplete/account_new", - views.AccountCreateAutocompleteView.as_view(), + autocomplete.account_create, name="kfet.account.create.autocomplete", ), # Account - Search path( "autocomplete/account_search", - views.AccountSearchAutocompleteView.as_view(), + autocomplete.account_search, name="kfet.account.search.autocomplete", ), # Account - Read @@ -223,8 +219,8 @@ urlpatterns = [ ), path( "k-psul/cancel_operations", - views.cancel_operations, - name="kfet.operations.cancel", + views.kpsul_cancel_operations, + name="kfet.kpsul.cancel_operations", ), path( "k-psul/articles_data", @@ -256,7 +252,7 @@ urlpatterns = [ # ----- # Transfers urls # ----- - path("transfers/", views.TransferView.as_view(), name="kfet.transfers"), + path("transfers/", views.transfers, name="kfet.transfers"), path("transfers/new", views.transfers_create, name="kfet.transfers.create"), path("transfers/perform", views.perform_transfers, name="kfet.transfers.perform"), path("transfers/cancel", views.cancel_transfers, name="kfet.transfers.cancel"), @@ -274,11 +270,6 @@ urlpatterns = [ teamkfet_required(views.InventoryRead.as_view()), name="kfet.inventory.read", ), - path( - "inventaires//delete", - views.InventoryDelete.as_view(), - name="kfet.inventory.delete", - ), # ----- # Order urls # ----- diff --git a/kfet/utils.py b/kfet/utils.py index d5df3228..0c4f170a 100644 --- a/kfet/utils.py +++ b/kfet/utils.py @@ -1,8 +1,8 @@ import json import math -from asgiref.sync import sync_to_async -from channels.generic.websocket import AsyncJsonWebsocketConsumer +from channels.channel import Group +from channels.generic.websockets import JsonWebsocketConsumer from django.core.cache import cache from django.core.serializers.json import DjangoJSONEncoder @@ -63,7 +63,7 @@ class CachedMixin: # Consumers -class DjangoJsonWebsocketConsumer(AsyncJsonWebsocketConsumer): +class DjangoJsonWebsocketConsumer(JsonWebsocketConsumer): """Custom Json Websocket Consumer. Encode to JSON with DjangoJSONEncoder. @@ -71,7 +71,7 @@ class DjangoJsonWebsocketConsumer(AsyncJsonWebsocketConsumer): """ @classmethod - async def encode_json(cls, content): + def encode_json(cls, content): return json.dumps(content, cls=DjangoJSONEncoder) @@ -89,11 +89,31 @@ class PermConsumerMixin: http_user = True # Enable message.user perms_connect = [] - async def connect(self): + def connect(self, message, **kwargs): """Check permissions on connection.""" - self.user = self.scope["user"] - - if await sync_to_async(self.user.has_perms)(self.perms_connect): - await super().connect() + if message.user.has_perms(self.perms_connect): + super().connect(message, **kwargs) else: - await self.close() + self.close() + + def raw_connect(self, message, **kwargs): + # Same as original raw_connect method of JsonWebsocketConsumer + # We add user to connection_groups call. + groups = self.connection_groups(user=message.user, **kwargs) + for group in groups: + Group(group, channel_layer=message.channel_layer).add(message.reply_channel) + self.connect(message, **kwargs) + + def raw_disconnect(self, message, **kwargs): + # Same as original raw_connect method of JsonWebsocketConsumer + # We add user to connection_groups call. + groups = self.connection_groups(user=message.user, **kwargs) + for group in groups: + Group(group, channel_layer=message.channel_layer).discard( + message.reply_channel + ) + self.disconnect(message, **kwargs) + + def connection_groups(self, user, **kwargs): + """`message.user` is available as `user` arg. Original behavior.""" + return super().connection_groups(user=user, **kwargs) diff --git a/kfet/views.py b/kfet/views.py index f85639b5..655e856d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,41 +1,21 @@ +import ast import heapq import statistics from collections import defaultdict -from datetime import datetime, timedelta from decimal import Decimal -from typing import List, Tuple +from typing import List from urllib.parse import urlencode -from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.models import Permission, User from django.contrib.messages.views import SuccessMessageMixin -from django.core.exceptions import SuspiciousOperation -from django.core.mail import EmailMessage from django.db import transaction -from django.db.models import ( - Count, - DecimalField, - ExpressionWrapper, - F, - Max, - OuterRef, - Prefetch, - Q, - Subquery, - Sum, -) -from django.forms import ValidationError, formset_factory -from django.http import ( - Http404, - HttpResponseBadRequest, - HttpResponseForbidden, - JsonResponse, -) +from django.db.models import Count, F, Prefetch, Sum +from django.forms import formset_factory +from django.http import Http404, JsonResponse from django.shortcuts import get_object_or_404, redirect, render -from django.template import loader from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.decorators import method_decorator @@ -44,18 +24,16 @@ from django.views.generic.detail import BaseDetailView from django.views.generic.edit import CreateView, DeleteView, UpdateView from gestioncof.models import CofProfile -from kfet import KFET_DELETED_TRIGRAMME +from kfet import KFET_DELETED_TRIGRAMME, consumers from kfet.auth.decorators import kfet_password_auth -from kfet.autocomplete import kfet_account_only_autocomplete, kfet_autocomplete from kfet.config import kfet_config -from kfet.consumers import KPsul from kfet.decorators import teamkfet_required from kfet.forms import ( AccountForm, - AccountFrozenForm, + AccountNegativeForm, AccountNoTriForm, AccountPwdForm, - AccountStatForm, + AccountRestrictForm, AccountTriForm, AddcostForm, ArticleForm, @@ -66,9 +44,6 @@ from kfet.forms import ( CheckoutStatementCreateForm, CheckoutStatementUpdateForm, CofForm, - CofKFForm, - ContactForm, - DemandeSoireeForm, FilterHistoryForm, InventoryArticleForm, KFetConfigForm, @@ -78,7 +53,6 @@ from kfet.forms import ( KPsulOperationGroupForm, OrderArticleForm, OrderArticleToInventoryForm, - StatScaleForm, TransferFormSet, UserForm, UserGroupForm, @@ -102,8 +76,7 @@ from kfet.models import ( Transfer, TransferGroup, ) -from kfet.statistic import SCALE_DICT, DayScale, MonthScale, WeekScale, scale_url_params -from shared.views import AutocompleteView +from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest from .auth import KFET_GENERIC_TRIGRAMME from .auth.views import ( # noqa @@ -119,61 +92,6 @@ def put_cleaned_data_in_dict(dict, form): dict[field] = form.cleaned_data[field] -class ContactView(FormView): - template_name = "kfet/contact.html" - form_class = ContactForm - success_url = reverse_lazy("kfet.contact") - - def form_valid(self, form): - # Envoie un mail lorsque le formulaire est valide - EmailMessage( - form.cleaned_data["subject"], - form.cleaned_data["message"], - from_email=form.cleaned_data["from_email"], - to=("chefs-k-fet@ens.psl.eu",), - ).send() - - messages.success( - self.request, - "Votre message a bien été envoyé aux Wo·men K-Fêt.", - ) - - return super().form_valid(form) - - -class DemandeSoireeView(FormView): - template_name = "kfet/demande_soiree.html" - form_class = DemandeSoireeForm - success_url = reverse_lazy("kfet.demande-soiree") - - def form_valid(self, form): - destinataires = ["chefs-k-fet@ens.psl.eu"] - - if form.cleaned_data["contact_boum"]: - destinataires.append("boum@ens.psl.eu") - - if form.cleaned_data["contact_pls"]: - destinataires.append("pls@ens.psl.eu") - - # Envoie un mail lorsque le formulaire est valide - EmailMessage( - f"Demande de soirée le {form.cleaned_data['date']}", - loader.render_to_string( - "kfet/mails/demande_soiree.txt", context=form.cleaned_data - ), - from_email=form.cleaned_data["from_email"], - to=destinataires, - cc=[form.cleaned_data["from_email"]], - ).send() - - messages.success( - self.request, - "Votre demande de soirée a bien été envoyée.", - ) - - return super().form_valid(form) - - # ----- # Account views # ----- @@ -185,20 +103,7 @@ class DemandeSoireeView(FormView): @teamkfet_required def account(request): accounts = Account.objects.select_related("cofprofile__user").order_by("trigramme") - positive_accounts = Account.objects.filter(balance__gte=0).exclude(trigramme="#13") - negative_accounts = Account.objects.filter(balance__lt=0).exclude(trigramme="#13") - - return render( - request, - "kfet/account.html", - { - "accounts": accounts, - "positive_count": positive_accounts.count(), - "positives_sum": sum(acc.balance for acc in positive_accounts), - "negative_count": negative_accounts.count(), - "negatives_sum": sum(acc.balance for acc in negative_accounts), - }, - ) + return render(request, "kfet/account.html", {"accounts": accounts}) @login_required @@ -218,6 +123,7 @@ def account_is_validandfree_ajax(request): @teamkfet_required @kfet_password_auth def account_create(request): + # Enregistrement if request.method == "POST": trigramme_form = AccountTriForm(request.POST) @@ -244,9 +150,7 @@ def account_create(request): ): # Checking permission if not request.user.has_perm("kfet.add_account"): - messages.error( - request, "Permission refusée", extra_tags="permission-denied" - ) + messages.error(request, "Permission refusée") else: data = {} # Fill data for Account.save() @@ -257,13 +161,7 @@ def account_create(request): account = trigramme_form.save(data=data) account_form = AccountNoTriForm(request.POST, instance=account) account_form.save() - was_kfet = account.is_kfet - account.cofprofile.is_kfet = cof_form.cleaned_data["is_kfet"] - account.cofprofile.save() - if account.cofprofile.is_kfet: - account.cofprofile.make_adh_kfet(request, was_kfet) messages.success(request, "Compte créé : %s" % account.trigramme) - account.send_creation_email() return redirect("kfet.account.create") except Account.UserHasAccount as e: messages.error( @@ -291,11 +189,7 @@ def account_create(request): def account_form_set_readonly_fields(user_form, cof_form): user_form.fields["username"].widget.attrs["readonly"] = True - user_form.fields["first_name"].widget.attrs["readonly"] = True - user_form.fields["last_name"].widget.attrs["readonly"] = True - user_form.fields["email"].widget.attrs["readonly"] = True cof_form.fields["login_clipper"].widget.attrs["readonly"] = True - cof_form.fields["departement"].widget.attrs["readonly"] = True cof_form.fields["is_cof"].widget.attrs["disabled"] = True @@ -428,92 +322,125 @@ def account_read(request, trigramme): # Account - Update -@teamkfet_required +@login_required @kfet_password_auth def account_update(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) # Checking permissions - if not account.editable: - # Plus de leak de trigramme ! - return HttpResponseForbidden + if not request.user.has_perm("kfet.is_team") and request.user != account.user: + raise Http404 user_info_form = UserInfoForm(instance=account.user) - account_form = AccountForm(instance=account) - group_form = UserGroupForm(instance=account.user) - frozen_form = AccountFrozenForm(instance=account) - cof_form = CofKFForm(instance=account.cofprofile) - pwd_form = AccountPwdForm() + + if request.user.has_perm("kfet.is_team"): + group_form = UserGroupForm(instance=account.user) + account_form = AccountForm(instance=account) + pwd_form = AccountPwdForm() + if account.balance < 0 and not hasattr(account, "negative"): + AccountNegative.objects.create(account=account, start=timezone.now()) + account.refresh_from_db() + if hasattr(account, "negative"): + negative_form = AccountNegativeForm(instance=account.negative) + else: + negative_form = None + else: + account_form = AccountRestrictForm(instance=account) + group_form = None + negative_form = None + pwd_form = None if request.method == "POST": - self_update = request.user == account.user - account_form = AccountForm(request.POST, instance=account) - group_form = UserGroupForm(request.POST, instance=account.user) - frozen_form = AccountFrozenForm(request.POST, instance=account) - cof_form = CofKFForm(request.POST, instance=account.cofprofile) - pwd_form = AccountPwdForm(request.POST, account=account) + # Update attempt + success = False + missing_perm = True - forms = [] - warnings = [] - - if self_update or request.user.has_perm("kfet.change_account"): - forms.append(account_form) - elif account_form.has_changed(): - warnings.append("compte") - - if request.user.has_perm("kfet.manage_perms"): - forms.append(group_form) - forms.append(frozen_form) - elif group_form.has_changed(): - warnings.append("statut d'équipe") - - if request.user.has_perm("kfet.change_adh"): - forms.append(cof_form) - elif cof_form.has_changed(): - warnings.append("adhésion kfet") - - # Il ne faut pas valider `pwd_form` si elle est inchangée - if pwd_form.has_changed(): - if self_update or request.user.has_perm("kfet.change_account_password"): - forms.append(pwd_form) - else: - warnings.append("mot de passe") - - # Updating account info - if forms == []: - messages.error( - request, - "Informations non mises à jour : permission refusée", - extra_tags="permission-denied", - ) - else: - if all(form.is_valid() for form in forms): - was_kfet = account.is_kfet - for form in forms: - form.save() - if account.is_kfet: - account.cofprofile.make_adh_kfet(request, was_kfet) - - if len(warnings): - messages.warning( - request, - "Permissions insuffisantes pour modifier" - " les informations suivantes : {}.".format(", ".join(warnings)), - ) - if self_update: - messages.success(request, "Vos informations ont été mises à jour !") - else: - messages.success( - request, - "Informations du compte %s mises à jour" % account.trigramme, - ) - - return redirect("kfet.account.read", account.trigramme) - else: - messages.error( - request, "Informations non mises à jour : corrigez les erreurs" + if request.user.has_perm("kfet.is_team"): + account_form = AccountForm(request.POST, instance=account) + group_form = UserGroupForm(request.POST, instance=account.user) + pwd_form = AccountPwdForm(request.POST) + if hasattr(account, "negative"): + negative_form = AccountNegativeForm( + request.POST, instance=account.negative ) + if request.user.has_perm("kfet.change_account") and account_form.is_valid(): + missing_perm = False + + # Updating + account_form.save() + + # Checking perm to update password + if ( + request.user.has_perm("kfet.change_account_password") + and pwd_form.is_valid() + ): + pwd = pwd_form.cleaned_data["pwd1"] + account.change_pwd(pwd) + account.save() + messages.success(request, "Mot de passe mis à jour") + + # Checking perm to manage perms + if request.user.has_perm("kfet.manage_perms") and group_form.is_valid(): + group_form.save() + + # Checking perm to manage negative + if hasattr(account, "negative"): + balance_offset_old = 0 + if account.negative.balance_offset: + balance_offset_old = account.negative.balance_offset + if ( + hasattr(account, "negative") + and request.user.has_perm("kfet.change_accountnegative") + and negative_form.is_valid() + ): + balance_offset_new = negative_form.cleaned_data["balance_offset"] + if not balance_offset_new: + balance_offset_new = 0 + balance_offset_diff = balance_offset_new - balance_offset_old + Account.objects.filter(pk=account.pk).update( + balance=F("balance") + balance_offset_diff + ) + negative_form.save() + if ( + Account.objects.get(pk=account.pk).balance >= 0 + and not balance_offset_new + ): + AccountNegative.objects.get(account=account).delete() + + success = True + messages.success( + request, + "Informations du compte %s mises à jour" % account.trigramme, + ) + + # Modification de ses propres informations + if request.user == account.user: + missing_perm = False + account.refresh_from_db() + account_form = AccountRestrictForm(request.POST, instance=account) + pwd_form = AccountPwdForm(request.POST) + + if account_form.is_valid(): + account_form.save() + success = True + messages.success(request, "Vos informations ont été mises à jour") + + if request.user.has_perm("kfet.is_team") and pwd_form.is_valid(): + pwd = pwd_form.cleaned_data["pwd1"] + account.change_pwd(pwd) + account.save() + messages.success(request, "Votre mot de passe a été mis à jour") + + if missing_perm: + messages.error(request, "Permission refusée") + if success: + return redirect("kfet.account.read", account.trigramme) + else: + messages.error( + request, "Informations non mises à jour. Corrigez les erreurs" + ) + return render( request, "kfet/account_update.html", @@ -521,15 +448,13 @@ def account_update(request, trigramme): "user_info_form": user_info_form, "account": account, "account_form": account_form, - "frozen_form": frozen_form, "group_form": group_form, + "negative_form": negative_form, "pwd_form": pwd_form, - "cof_form": cof_form, }, ) - -# Account - Delete + # Account - Delete class AccountDelete(PermissionRequiredMixin, DeleteView): @@ -568,18 +493,16 @@ class AccountDelete(PermissionRequiredMixin, DeleteView): class AccountNegativeList(ListView): - queryset = ( - AccountNegative.objects.select_related("account", "account__cofprofile__user") - .filter(account__balance__lt=0) - .exclude(account__trigramme="#13") - ) + queryset = AccountNegative.objects.select_related( + "account", "account__cofprofile__user" + ).exclude(account__trigramme="#13") template_name = "kfet/account_negative.html" context_object_name = "negatives" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - balances = (neg.account.balance for neg in self.object_list) - context["negatives_sum"] = sum(balances) + real_balances = (neg.account.real_balance for neg in self.object_list) + context["negatives_sum"] = sum(real_balances) return context @@ -610,9 +533,7 @@ class CheckoutCreate(SuccessMessageMixin, CreateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.add_checkout"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Creating @@ -650,9 +571,7 @@ class CheckoutUpdate(SuccessMessageMixin, UpdateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.change_checkout"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating return super().form_valid(form) @@ -742,9 +661,7 @@ class CheckoutStatementCreate(SuccessMessageMixin, CreateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.add_checkoutstatement"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Creating form.instance.amount_taken = getAmountTaken(form.instance) @@ -776,9 +693,7 @@ class CheckoutStatementUpdate(SuccessMessageMixin, UpdateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.change_checkoutstatement"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating form.instance.amount_taken = getAmountTaken(form.instance) @@ -810,9 +725,7 @@ class CategoryUpdate(SuccessMessageMixin, UpdateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.change_articlecategory"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating @@ -861,9 +774,7 @@ class ArticleCreate(SuccessMessageMixin, CreateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.add_article"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Save ici pour save le manytomany suppliers @@ -929,9 +840,7 @@ class ArticleUpdate(SuccessMessageMixin, UpdateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.change_article"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Save ici pour save le manytomany suppliers @@ -1002,14 +911,11 @@ def kpsul_get_settings(request): @teamkfet_required def account_read_json(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) - if not account.readable: - raise Http404 data = { "id": account.pk, "name": account.name, "email": account.email, "is_cof": account.is_cof, - "is_kfet": account.is_kfet, "promo": account.promo, "balance": account.balance, "is_frozen": account.is_frozen, @@ -1066,18 +972,15 @@ def kpsul_checkout_data(request): @kfet_password_auth def kpsul_update_addcost(request): addcost_form = AddcostForm(request.POST) - data = {"errors": []} if not addcost_form.is_valid(): - for field, errors in addcost_form.errors.items(): - for error in errors: - data["errors"].append({"code": f"invalid_{field}", "message": error}) - + data = {"errors": {"addcost": list(addcost_form.errors)}} return JsonResponse(data, status=400) - required_perms = ["kfet.manage_addcosts"] if not request.user.has_perms(required_perms): - data["missing_perms"] = get_missing_perms(required_perms, request.user) + data = { + "errors": {"missing_perms": get_missing_perms(required_perms, request.user)} + } return JsonResponse(data, status=403) trigramme = addcost_form.cleaned_data["trigramme"] @@ -1086,24 +989,20 @@ def kpsul_update_addcost(request): kfet_config.set(addcost_for=account, addcost_amount=amount) - data = { - "addcost": {"for": account and account.trigramme or None, "amount": amount}, - "type": "kpsul", - } - - KPsul.group_send("kfet.kpsul", data) - + data = {"addcost": {"for": account and account.trigramme or None, "amount": amount}} + consumers.KPsul.group_send("kfet.kpsul", data) return JsonResponse(data) def get_missing_perms(required_perms: List[str], user: User) -> List[str]: - def get_perm_name(app_label: str, codename: str) -> str: - return Permission.objects.values_list("name", flat=True).get( + def get_perm_description(app_label: str, codename: str) -> str: + name = Permission.objects.values_list("name", flat=True).get( codename=codename, content_type__app_label=app_label ) + return "[{}] {}".format(app_label, name) missing_perms = [ - get_perm_name(*perm.split(".")) + get_perm_description(*perm.split(".")) for perm in required_perms if not user.has_perm(perm) ] @@ -1115,31 +1014,17 @@ def get_missing_perms(required_perms: List[str], user: User) -> List[str]: @kfet_password_auth def kpsul_perform_operations(request): # Initializing response data - data = {"errors": []} + data = {"operationgroup": 0, "operations": [], "warnings": {}, "errors": {}} # Checking operationgroup operationgroup_form = KPsulOperationGroupForm(request.POST) if not operationgroup_form.is_valid(): - for field in operationgroup_form.errors: - verbose_field, feminin = ( - ("compte", "") if field == "on_acc" else ("caisse", "e") - ) - data["errors"].append( - { - "code": f"invalid_{field}", - "message": f"Pas de {verbose_field} sélectionné{feminin}", - } - ) + data["errors"]["operation_group"] = list(operationgroup_form.errors) # Checking operation_formset operation_formset = KPsulOperationFormSet(request.POST) if not operation_formset.is_valid(): - data["errors"].append( - { - "code": "invalid_formset", - "message": "Formulaire d'opérations vide ou invalide", - } - ) + data["errors"]["operations"] = list(operation_formset.errors) # Returning BAD REQUEST if errors if data["errors"]: @@ -1148,7 +1033,6 @@ def kpsul_perform_operations(request): # Pre-saving (no commit) operationgroup = operationgroup_form.save(commit=False) operations = operation_formset.save(commit=False) - on_acc = operationgroup.on_acc # Retrieving COF grant cof_grant = kfet_config.subvention_cof @@ -1162,13 +1046,10 @@ def kpsul_perform_operations(request): to_addcost_for_balance = 0 # For balance of addcost_for to_checkout_balance = 0 # For balance of selected checkout to_articles_stocks = defaultdict(lambda: 0) # For stocks articles - is_addcost = all((addcost_for, addcost_amount, addcost_for != on_acc)) - need_comment = on_acc.need_comment - - if on_acc.is_frozen: - data["errors"].append( - {"code": "frozen_acc", "message": f"Le compte {on_acc.trigramme} est gelé"} - ) + is_addcost = all( + (addcost_for, addcost_amount, addcost_for != operationgroup.on_acc) + ) + need_comment = operationgroup.on_acc.need_comment # Filling data of each operations # + operationgroup + calculating other stuffs @@ -1180,91 +1061,62 @@ def kpsul_perform_operations(request): operation.addcost_amount = addcost_amount * operation.article_nb operation.amount -= operation.addcost_amount to_addcost_for_balance += operation.addcost_amount - if on_acc.is_cash: + if operationgroup.on_acc.is_cash: to_checkout_balance += -operation.amount - if on_acc.is_cof and operation.article.category.has_reduction: + if ( + operationgroup.on_acc.is_cof + and operation.article.category.has_reduction + ): if is_addcost and operation.article.category.has_addcost: operation.addcost_amount /= cof_grant_divisor operation.amount = operation.amount / cof_grant_divisor - if not on_acc.is_cof and not on_acc.is_kfet and operation.article.no_exte: - if on_acc.is_cash: - required_perms.add("kfet.perform_liq_reserved") - else: - data["errors"].append( - { - "code": "reserved", - "message": ( - "L'article " - + operation.article.name - + " est réservé aux adhérent⋅e⋅s du COF, or " - + on_acc.trigramme - + " ne l'est pas" - ), - } - ) to_articles_stocks[operation.article] -= operation.article_nb else: - if on_acc.is_cash: - data["errors"].append( - { - "code": "invalid_liq", - "message": ( - "Impossible de compter autre chose que des achats sur LIQ" - ), - } - ) + if operationgroup.on_acc.is_cash: + data["errors"]["account"] = "LIQ" if operation.type != Operation.EDIT: to_checkout_balance += operation.amount operationgroup.amount += operation.amount if operation.type == Operation.DEPOSIT: required_perms.add("kfet.perform_deposit") - if request.user.profile.account_kfet == on_acc: - data["errors"].append( - { - "code": "auto_deposit", - "message": ("Impossible de charger son propre trigramme"), - } - ) if operation.type == Operation.EDIT: required_perms.add("kfet.edit_balance_account") need_comment = True - if on_acc.is_cof: + if operationgroup.on_acc.is_cof: to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor - (perms, stop) = on_acc.perms_to_perform_operation(amount=operationgroup.amount) + (perms, stop) = operationgroup.on_acc.perms_to_perform_operation( + amount=operationgroup.amount + ) required_perms |= perms - if stop: - data["errors"].append( - { - "code": "negative", - "message": f"Le compte {on_acc.trigramme} a un solde insuffisant.", - } - ) - if need_comment: operationgroup.comment = operationgroup.comment.strip() if not operationgroup.comment: - data["need_comment"] = True + data["errors"]["need_comment"] = True - if data["errors"] or "need_comment" in data: + if data["errors"]: return JsonResponse(data, status=400) - if not request.user.has_perms(required_perms): - data["missing_perms"] = get_missing_perms(required_perms, request.user) + if stop or not request.user.has_perms(required_perms): + missing_perms = get_missing_perms(required_perms, request.user) + if missing_perms: + data["errors"]["missing_perms"] = missing_perms + if stop: + data["errors"]["negative"] = [operationgroup.on_acc.trigramme] return JsonResponse(data, status=403) # If 1 perm is required, filling who perform the operations if required_perms: operationgroup.valid_by = request.user.profile.account_kfet # Filling cof status for statistics - operationgroup.is_cof = on_acc.is_cof - operationgroup.is_kfet = on_acc.is_kfet + operationgroup.is_cof = operationgroup.on_acc.is_cof # Starting transaction to ensure data consistency with transaction.atomic(): # If not cash account, # saving account's balance and adding to Negative if not in + on_acc = operationgroup.on_acc if not on_acc.is_cash: ( Account.objects.filter(pk=on_acc.pk).update( @@ -1288,10 +1140,13 @@ def kpsul_perform_operations(request): # Saving operation group operationgroup.save() + data["operationgroup"] = operationgroup.pk + # Filling operationgroup id for each operations and saving for operation in operations: operation.group = operationgroup operation.save() + data["operations"].append(operation.pk) # Updating articles stock for article in to_articles_stocks: @@ -1300,23 +1155,21 @@ def kpsul_perform_operations(request): ) # Websocket data - websocket_data = {"type": "kpsul"} - websocket_data["groups"] = [ + websocket_data = {} + websocket_data["opegroups"] = [ { "add": True, - "type": "operation", "id": operationgroup.pk, "amount": operationgroup.amount, "checkout__name": operationgroup.checkout.name, "at": operationgroup.at, "is_cof": operationgroup.is_cof, - "is_kfet": operationgroup.is_kfet, "comment": operationgroup.comment, "valid_by__trigramme": ( operationgroup.valid_by and operationgroup.valid_by.trigramme or None ), - "on_acc__trigramme": on_acc.trigramme, - "entries": [], + "on_acc__trigramme": operationgroup.on_acc.trigramme, + "opes": [], } ] for operation in operations: @@ -1334,7 +1187,7 @@ def kpsul_perform_operations(request): "canceled_by__trigramme": None, "canceled_at": None, } - websocket_data["groups"][0]["entries"].append(ope_data) + websocket_data["opegroups"][0]["opes"].append(ope_data) # Need refresh from db cause we used update on queryset operationgroup.checkout.refresh_from_db() websocket_data["checkouts"] = [ @@ -1348,17 +1201,15 @@ def kpsul_perform_operations(request): websocket_data["articles"].append( {"id": article["id"], "stock": article["stock"]} ) - - KPsul.group_send("kfet.kpsul", websocket_data) - + consumers.KPsul.group_send("kfet.kpsul", websocket_data) return JsonResponse(data) @teamkfet_required @kfet_password_auth -def cancel_operations(request): +def kpsul_cancel_operations(request): # Pour la réponse - data = {"canceled": [], "warnings": {}, "errors": []} + data = {"canceled": [], "warnings": {}, "errors": {}} # Checking if BAD REQUEST (opes_pk not int or not existing) try: @@ -1367,41 +1218,29 @@ def cancel_operations(request): map(int, filter(None, request.POST.getlist("operations[]", []))) ) except ValueError: - data["errors"].append( - {"code": "invalid_request", "message": "Requête invalide !"} - ) return JsonResponse(data, status=400) - opes_all = Operation.objects.select_related( "group", "group__on_acc", "group__on_acc__negative" ).filter(pk__in=opes_post) opes_pk = [ope.pk for ope in opes_all] opes_notexisting = [ope for ope in opes_post if ope not in opes_pk] if opes_notexisting: - data["errors"].append( - { - "code": "cancel_missing", - "message": "Opérations inexistantes : {}".format( - ", ".join(map(str, opes_notexisting)) - ), - } - ) + data["errors"]["opes_notexisting"] = opes_notexisting return JsonResponse(data, status=400) opes_already_canceled = [] # Déjà annulée opes = [] # Pas déjà annulée required_perms = set() + stop_all = False cancel_duration = kfet_config.cancel_duration - - # Modifs à faire sur les balances des comptes - to_accounts_balances = defaultdict(int) - # ------ sur les montants des groupes d'opé - to_groups_amounts = defaultdict(int) - # ------ sur les balances de caisses - to_checkouts_balances = defaultdict(int) - # ------ sur les stocks d'articles - to_articles_stocks = defaultdict(int) - + to_accounts_balances = defaultdict( + lambda: 0 + ) # Modifs à faire sur les balances des comptes + to_groups_amounts = defaultdict( + lambda: 0 + ) # ------ sur les montants des groupes d'opé + to_checkouts_balances = defaultdict(lambda: 0) # ------ sur les balances de caisses + to_articles_stocks = defaultdict(lambda: 0) # ------ sur les stocks d'articles for ope in opes_all: if ope.canceled_at: # Opération déjà annulée, va pour un warning en Response @@ -1472,22 +1311,16 @@ def cancel_operations(request): amount=to_accounts_balances[account] ) required_perms |= perms + stop_all = stop_all or stop if stop: negative_accounts.append(account.trigramme) - if negative_accounts: - data["errors"].append( - { - "code": "negative", - "message": "Solde insuffisant pour les comptes suivants : {}".format( - ", ".join(negative_accounts) - ), - } - ) - return JsonResponse(data, status=400) - - if not request.user.has_perms(required_perms): - data["missing_perms"] = get_missing_perms(required_perms, request.user) + if stop_all or not request.user.has_perms(required_perms): + missing_perms = get_missing_perms(required_perms, request.user) + if missing_perms: + data["errors"]["missing_perms"] = missing_perms + if stop_all: + data["errors"]["negative"] = negative_accounts return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None @@ -1526,15 +1359,11 @@ def cancel_operations(request): # Sort objects by pk to get deterministic responses. opegroups_pk = [opegroup.pk for opegroup in to_groups_amounts] opegroups = ( - OperationGroup.objects.values("id", "amount", "is_cof", "is_kfet") + OperationGroup.objects.values("id", "amount", "is_cof") .filter(pk__in=opegroups_pk) .order_by("pk") ) - opes = ( - Operation.objects.values("id", "canceled_at", "canceled_by__trigramme") - .filter(pk__in=opes) - .order_by("pk") - ) + opes = sorted(opes) checkouts_pk = [checkout.pk for checkout in to_checkouts_balances] checkouts = ( Checkout.objects.values("id", "balance") @@ -1545,7 +1374,27 @@ def cancel_operations(request): articles = Article.objects.values("id", "stock").filter(pk__in=articles_pk) # Websocket data - websocket_data = {"checkouts": [], "articles": [], "type": "kpsul"} + websocket_data = {"opegroups": [], "opes": [], "checkouts": [], "articles": []} + + for opegroup in opegroups: + websocket_data["opegroups"].append( + { + "cancellation": True, + "id": opegroup["id"], + "amount": opegroup["amount"], + "is_cof": opegroup["is_cof"], + } + ) + canceled_by__trigramme = canceled_by and canceled_by.trigramme or None + for ope in opes: + websocket_data["opes"].append( + { + "cancellation": True, + "id": ope, + "canceled_by__trigramme": canceled_by__trigramme, + "canceled_at": canceled_at, + } + ) for checkout in checkouts: websocket_data["checkouts"].append( {"id": checkout["id"], "balance": checkout["balance"]} @@ -1554,142 +1403,62 @@ def cancel_operations(request): websocket_data["articles"].append( {"id": article["id"], "stock": article["stock"]} ) + consumers.KPsul.group_send("kfet.kpsul", websocket_data) - KPsul.group_send("kfet.kpsul", websocket_data) - - data["canceled"] = list(opes) - data["opegroups_to_update"] = list(opegroups) + data["canceled"] = opes if opes_already_canceled: data["warnings"]["already_canceled"] = opes_already_canceled return JsonResponse(data) -def get_history_limit(user) -> Tuple[datetime, datetime]: - """returns a tuple of 2 dates - - the earliest date the given user can view history of any account - - the earliest date the given user can view history of special accounts - (LIQ and #13)""" - now = timezone.now() - if user.has_perm("kfet.access_old_history"): - return ( - now - settings.KFET_HISTORY_LONG_DATE_LIMIT, - settings.KFET_HISTORY_NO_DATE_LIMIT, - ) - if user.has_perm("kfet.is_team"): - limit = now - settings.KFET_HISTORY_DATE_LIMIT - return limit, limit - # should not happen - future earliest date - future = now + timedelta(days=1) - return future, future - - @login_required def history_json(request): # Récupération des paramètres - form = FilterHistoryForm(request.GET) - - if not form.is_valid(): - return HttpResponseBadRequest() - - start = form.cleaned_data["start"] - end = form.cleaned_data["end"] - account = form.cleaned_data["account"] - checkout = form.cleaned_data["checkout"] - transfers_only = form.cleaned_data["transfers_only"] - opes_only = form.cleaned_data["opes_only"] - - # Construction de la requête (sur les transferts) pour le prefetch - - transfer_queryset_prefetch = Transfer.objects.select_related( - "from_acc", "to_acc", "canceled_by" - ) - - # Le check sur les comptes est dans le prefetch pour les transferts - if account: - transfer_queryset_prefetch = transfer_queryset_prefetch.filter( - Q(from_acc=account) | Q(to_acc=account) - ) - - if not request.user.has_perm("kfet.is_team"): - try: - acc = request.user.profile.account_kfet - transfer_queryset_prefetch = transfer_queryset_prefetch.filter( - Q(from_acc=acc) | Q(to_acc=acc) - ) - except Account.DoesNotExist: - return JsonResponse({}, status=403) - - transfer_prefetch = Prefetch( - "transfers", queryset=transfer_queryset_prefetch, to_attr="filtered_transfers" - ) + from_date = request.POST.get("from", None) + to_date = request.POST.get("to", None) + limit = request.POST.get("limit", None) + checkouts = request.POST.getlist("checkouts[]", None) + accounts = request.POST.getlist("accounts[]", None) # Construction de la requête (sur les opérations) pour le prefetch - ope_queryset_prefetch = Operation.objects.select_related( + queryset_prefetch = Operation.objects.select_related( "article", "canceled_by", "addcost_for" ) - ope_prefetch = Prefetch("opes", queryset=ope_queryset_prefetch) # Construction de la requête principale opegroups = ( - OperationGroup.objects.prefetch_related(ope_prefetch) + OperationGroup.objects.prefetch_related( + Prefetch("opes", queryset=queryset_prefetch) + ) .select_related("on_acc", "valid_by") .order_by("at") ) - transfergroups = ( - TransferGroup.objects.prefetch_related(transfer_prefetch) - .select_related("valid_by") - .order_by("at") - ) - - # limite l'accès à l'historique plus vieux que settings.KFET_HISTORY_DATE_LIMIT - limit_date = True - # Application des filtres - if start: - opegroups = opegroups.filter(at__gte=start) - transfergroups = transfergroups.filter(at__gte=start) - if end: - opegroups = opegroups.filter(at__lt=end) - transfergroups = transfergroups.filter(at__lt=end) - if checkout: - opegroups = opegroups.filter(checkout=checkout) - transfergroups = TransferGroup.objects.none() - if transfers_only: - opegroups = OperationGroup.objects.none() - if opes_only: - transfergroups = TransferGroup.objects.none() - if account: - opegroups = opegroups.filter(on_acc=account) - if account.user == request.user: - limit_date = False # pas de limite de date sur son propre historique + if from_date: + opegroups = opegroups.filter(at__gte=from_date) + if to_date: + opegroups = opegroups.filter(at__lt=to_date) + if checkouts: + opegroups = opegroups.filter(checkout_id__in=checkouts) + if accounts: + opegroups = opegroups.filter(on_acc_id__in=accounts) # Un non-membre de l'équipe n'a que accès à son historique - elif not request.user.has_perm("kfet.is_team"): - # un non membre de la kfet doit avoir le champ account - # pré-rempli, cette requête est douteuse - return JsonResponse({}, status=403) - if limit_date: - # limiter l'accès à l'historique ancien pour confidentialité - earliest_date, earliest_date_no_limit = get_history_limit(request.user) - if ( - account - and account.trigramme in settings.KFET_HISTORY_NO_DATE_LIMIT_TRIGRAMMES - ): - earliest_date = earliest_date_no_limit - opegroups = opegroups.filter(at__gte=earliest_date) - transfergroups = transfergroups.filter(at__gte=earliest_date) + if not request.user.has_perm("kfet.is_team"): + opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet) + if limit: + opegroups = opegroups[:limit] # Construction de la réponse - history_groups = [] + opegroups_list = [] for opegroup in opegroups: opegroup_dict = { - "type": "operation", "id": opegroup.id, "amount": opegroup.amount, "at": opegroup.at, "checkout_id": opegroup.checkout_id, "is_cof": opegroup.is_cof, "comment": opegroup.comment, - "entries": [], + "opes": [], "on_acc__trigramme": opegroup.on_acc and opegroup.on_acc.trigramme or None, } if request.user.has_perm("kfet.is_team"): @@ -1713,40 +1482,9 @@ def history_json(request): ope_dict["canceled_by__trigramme"] = ( ope.canceled_by and ope.canceled_by.trigramme or None ) - opegroup_dict["entries"].append(ope_dict) - history_groups.append(opegroup_dict) - for transfergroup in transfergroups: - if transfergroup.filtered_transfers: - transfergroup_dict = { - "type": "transfer", - "id": transfergroup.id, - "at": transfergroup.at, - "comment": transfergroup.comment, - "entries": [], - } - if request.user.has_perm("kfet.is_team"): - transfergroup_dict["valid_by__trigramme"] = ( - transfergroup.valid_by and transfergroup.valid_by.trigramme or None - ) - - for transfer in transfergroup.filtered_transfers: - transfer_dict = { - "id": transfer.id, - "amount": transfer.amount, - "canceled_at": transfer.canceled_at, - "from_acc": transfer.from_acc.trigramme, - "to_acc": transfer.to_acc.trigramme, - } - if request.user.has_perm("kfet.is_team"): - transfer_dict["canceled_by__trigramme"] = ( - transfer.canceled_by and transfer.canceled_by.trigramme or None - ) - transfergroup_dict["entries"].append(transfer_dict) - history_groups.append(transfergroup_dict) - - history_groups.sort(key=lambda group: group["at"]) - - return JsonResponse({"groups": history_groups}) + opegroup_dict["opes"].append(ope_dict) + opegroups_list.append(opegroup_dict) + return JsonResponse({"opegroups": opegroups_list}) @teamkfet_required @@ -1755,7 +1493,6 @@ def kpsul_articles_data(request): "id", "name", "price", - "no_exte", "stock", "category_id", "category__name", @@ -1767,20 +1504,7 @@ def kpsul_articles_data(request): @teamkfet_required def history(request): - # These limits are only useful for JS datepickers - # They don't enforce anything and can be bypassed - # Serious checks are done in history_json - history_limit, history_no_limit = get_history_limit(request.user) - history_no_limit_account_ids = Account.objects.filter( - trigramme__in=settings.KFET_HISTORY_NO_DATE_LIMIT_TRIGRAMMES - ).values_list("id", flat=True) - format_date = lambda date: date.strftime("%Y-%m-%d %H:%M") - data = { - "filter_form": FilterHistoryForm(), - "history_limit": format_date(history_limit), - "history_no_limit_account_ids": history_no_limit_account_ids, - "history_no_limit": format_date(history_no_limit), - } + data = {"filter_form": FilterHistoryForm()} return render(request, "kfet/history.html", data) @@ -1806,9 +1530,7 @@ class SettingsUpdate(SuccessMessageMixin, FormView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.change_config"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) form.save() return super().form_valid(form) @@ -1822,9 +1544,18 @@ config_update = permission_required("kfet.change_config")(SettingsUpdate.as_view # ----- -@method_decorator(teamkfet_required, name="dispatch") -class TransferView(TemplateView): - template_name = "kfet/transfers.html" +@teamkfet_required +def transfers(request): + transfers_pre = Prefetch( + "transfers", queryset=(Transfer.objects.select_related("from_acc", "to_acc")) + ) + + transfergroups = ( + TransferGroup.objects.select_related("valid_by") + .prefetch_related(transfers_pre) + .order_by("-at") + ) + return render(request, "kfet/transfers.html", {"transfergroups": transfergroups}) @teamkfet_required @@ -1838,36 +1569,12 @@ def transfers_create(request): @teamkfet_required @kfet_password_auth def perform_transfers(request): - data = {"errors": []} + data = {"errors": {}, "transfers": [], "transfergroup": 0} # Checking transfer_formset transfer_formset = TransferFormSet(request.POST) - try: - if not transfer_formset.is_valid(): - for form_errors in transfer_formset.errors: - for field, errors in form_errors.items(): - if field == "amount": - for error in errors: - data["errors"].append({"code": "amount", "message": error}) - else: - # C'est compliqué de trouver le compte qui pose problème... - acc_error = True - - if acc_error: - data["errors"].append( - { - "code": "invalid_acc", - "message": "L'un des comptes est invalide ou manquant", - } - ) - - return JsonResponse(data, status=400) - - except ValidationError: - data["errors"].append( - {"code": "invalid_request", "message": "Requête invalide"} - ) - return JsonResponse(data, status=400) + if not transfer_formset.is_valid(): + return JsonResponse({"errors": list(transfer_formset.errors)}, status=400) transfers = transfer_formset.save(commit=False) @@ -1875,51 +1582,31 @@ def perform_transfers(request): required_perms = set( ["kfet.add_transfer"] ) # Required perms to perform all transfers - to_accounts_balances = defaultdict(int) # For balances of accounts + to_accounts_balances = defaultdict(lambda: 0) # For balances of accounts for transfer in transfers: to_accounts_balances[transfer.from_acc] -= transfer.amount to_accounts_balances[transfer.to_acc] += transfer.amount + stop_all = False + negative_accounts = [] # Checking if ok on all accounts - frozen = set() for account in to_accounts_balances: - if account.is_frozen: - frozen.add(account.trigramme) - (perms, stop) = account.perms_to_perform_operation( amount=to_accounts_balances[account] ) required_perms |= perms + stop_all = stop_all or stop if stop: negative_accounts.append(account.trigramme) - if frozen: - data["errors"].append( - { - "code": "frozen", - "message": "Les comptes suivants sont gelés : {}".format( - ", ".join(frozen) - ), - } - ) - - if negative_accounts: - data["errors"].append( - { - "code": "negative", - "message": "Solde insuffisant pour les comptes suivants : {}".format( - ", ".join(negative_accounts) - ), - } - ) - - if data["errors"]: - return JsonResponse(data, status=400) - - if not request.user.has_perms(required_perms): - data["missing_perms"] = get_missing_perms(required_perms, request.user) + if stop_all or not request.user.has_perms(required_perms): + missing_perms = get_missing_perms(required_perms, request.user) + if missing_perms: + data["errors"]["missing_perms"] = missing_perms + if stop_all: + data["errors"]["negative"] = negative_accounts return JsonResponse(data, status=403) # Creating transfer group @@ -1937,24 +1624,35 @@ def perform_transfers(request): balance=F("balance") + to_accounts_balances[account] ) account.refresh_from_db() - account.update_negative() + if account.balance < 0: + if hasattr(account, "negative"): + if not account.negative.start: + account.negative.start = timezone.now() + account.negative.save() + else: + negative = AccountNegative(account=account, start=timezone.now()) + negative.save() + elif hasattr(account, "negative") and not account.negative.balance_offset: + account.negative.delete() # Saving transfer group transfergroup.save() + data["transfergroup"] = transfergroup.pk # Saving all transfers with group for transfer in transfers: transfer.group = transfergroup transfer.save() + data["transfers"].append(transfer.pk) - return JsonResponse({}) + return JsonResponse(data) @teamkfet_required @kfet_password_auth def cancel_transfers(request): # Pour la réponse - data = {"canceled": [], "warnings": {}, "errors": []} + data = {"canceled": [], "warnings": {}, "errors": {}} # Checking if BAD REQUEST (transfers_pk not int or not existing) try: @@ -1963,11 +1661,7 @@ def cancel_transfers(request): map(int, filter(None, request.POST.getlist("transfers[]", []))) ) except ValueError: - data["errors"].append( - {"code": "invalid_request", "message": "Requête invalide !"} - ) return JsonResponse(data, status=400) - transfers_all = Transfer.objects.select_related( "group", "from_acc", "from_acc__negative", "to_acc", "to_acc__negative" ).filter(pk__in=transfers_post) @@ -1976,23 +1670,17 @@ def cancel_transfers(request): transfer for transfer in transfers_post if transfer not in transfers_pk ] if transfers_notexisting: - data["errors"].append( - { - "code": "cancel_missing", - "message": "Transferts inexistants : {}".format( - ", ".join(map(str, transfers_notexisting)) - ), - } - ) + data["errors"]["transfers_notexisting"] = transfers_notexisting return JsonResponse(data, status=400) - transfers_already_canceled = [] # Déjà annulés - transfers = [] # Pas déjà annulés + transfers_already_canceled = [] # Déjà annulée + transfers = [] # Pas déjà annulée required_perms = set() + stop_all = False cancel_duration = kfet_config.cancel_duration - - # Modifs à faire sur les balances des comptes - to_accounts_balances = defaultdict(int) + to_accounts_balances = defaultdict( + lambda: 0 + ) # Modifs à faire sur les balances des comptes for transfer in transfers_all: if transfer.canceled_at: # Transfert déjà annulé, va pour un warning en Response @@ -2020,22 +1708,16 @@ def cancel_transfers(request): amount=to_accounts_balances[account] ) required_perms |= perms + stop_all = stop_all or stop if stop: negative_accounts.append(account.trigramme) - if negative_accounts: - data["errors"].append( - { - "code": "negative", - "message": "Solde insuffisant pour les comptes suivants : {}".format( - ", ".join(negative_accounts) - ), - } - ) - return JsonResponse(data, status=400) - - if not request.user.has_perms(required_perms): - data["missing_perms"] = get_missing_perms(required_perms, request.user) + if stop_all or not request.user.has_perms(required_perms): + missing_perms = get_missing_perms(required_perms, request.user) + if missing_perms: + data["errors"]["missing_perms"] = missing_perms + if stop_all: + data["errors"]["negative"] = negative_accounts return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None @@ -2053,14 +1735,18 @@ def cancel_transfers(request): balance=F("balance") + to_accounts_balances[account] ) account.refresh_from_db() - account.update_negative() + if account.balance < 0: + if hasattr(account, "negative"): + if not account.negative.start: + account.negative.start = timezone.now() + account.negative.save() + else: + negative = AccountNegative(account=account, start=timezone.now()) + negative.save() + elif hasattr(account, "negative") and not account.negative.balance_offset: + account.negative.delete() - transfers = ( - Transfer.objects.values("id", "canceled_at", "canceled_by__trigramme") - .filter(pk__in=transfers) - .order_by("pk") - ) - data["canceled"] = list(transfers) + data["canceled"] = transfers if transfers_already_canceled: data["warnings"]["already_canceled"] = transfers_already_canceled return JsonResponse(data) @@ -2079,14 +1765,14 @@ class InventoryList(ListView): @teamkfet_required @kfet_password_auth def inventory_create(request): + articles = Article.objects.select_related("category").order_by( - "-is_sold", "category__name", "name" + "category__name", "name" ) initial = [] for article in articles: initial.append( { - "is_sold": article.is_sold, "article": article.pk, "stock_old": article.stock, "name": article.name, @@ -2102,11 +1788,10 @@ def inventory_create(request): formset = cls_formset(request.POST, initial=initial) if not request.user.has_perm("kfet.add_inventory"): - messages.error( - request, "Permission refusée", extra_tags="permission-denied" - ) + messages.error(request, "Permission refusée") elif formset.is_valid(): with transaction.atomic(): + articles = Article.objects.select_for_update() inventory = Inventory() inventory.by = request.user.profile.account_kfet @@ -2147,63 +1832,15 @@ class InventoryRead(DetailView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - output_field = DecimalField(max_digits=10, decimal_places=2, default=0) - inventory_articles = ( + inventoryarticles = ( InventoryArticle.objects.select_related("article", "article__category") .filter(inventory=self.object) - .annotate( - amount_error=ExpressionWrapper( - F("stock_error") * F("article__price"), output_field=output_field - ) - ) .order_by("article__category__name", "article__name") ) - context["inventoryarts"] = inventory_articles - stats = inventory_articles.aggregate( - new=ExpressionWrapper( - Sum(F("stock_new") * F("article__price")), output_field=output_field - ), - error=Sum("amount_error"), - old=ExpressionWrapper( - Sum(F("stock_old") * F("article__price")), output_field=output_field - ), - ) - context.update( - { - "total_amount_old": stats["old"], - "total_amount_new": stats["new"], - "total_amount_error": stats["error"], - } - ) + context["inventoryarts"] = inventoryarticles return context -class InventoryDelete(PermissionRequiredMixin, DeleteView): - model = Inventory - success_url = reverse_lazy("kfet.inventory") - success_message = "Inventaire annulé avec succès !" - permission_required = "kfet.delete_inventory" - - def get(self, request, *args, **kwargs): - return redirect("kfet.inventory.read", self.kwargs.get(self.pk_url_kwarg)) - - def delete(self, request, *args, **kwargs): - inv = self.get_object() - # On met à jour les articles dont c'est le dernier inventaire - # .get() ne marche pas avec OuterRef, donc on utilise .filter() avec [:1] - update_subquery = InventoryArticle.objects.filter( - inventory=inv, article=OuterRef("pk") - ).values("stock_old")[:1] - - Article.objects.annotate(last_env=Max("inventories__at")).filter( - last_env=inv.at - ).update(stock=Subquery(update_subquery)) - - # On a tout mis à jour, on peut delete (avec un message) - messages.success(request, self.success_message) - return super().delete(request, *args, **kwargs) - - # ----- # Order views # ----- @@ -2229,7 +1866,7 @@ def order_create(request, pk): Article.objects.filter(suppliers=supplier.pk) .distinct() .select_related("category") - .order_by("-is_sold", "category__name", "name") + .order_by("category__name", "name") ) # Force hit to cache @@ -2258,11 +1895,21 @@ def order_create(request, pk): v_et = statistics.pstdev(v_3max, v_moy) # Expected sales for next week v_prev = v_moy + v_et - - c_rec_tot = max(v_prev - max(article.stock, 0), 0) + # We want to have 1.5 * the expected sales in stock + # (because sometimes some articles are not delivered) + c_rec_tot = max(v_prev * 1.5 - article.stock, 0) + # If ordered quantity is close enough to a level which can led to free + # boxes, we increase it to this level. if article.box_capacity: c_rec_temp = c_rec_tot / article.box_capacity - c_rec = round(c_rec_temp) + if c_rec_temp >= 10: + c_rec = round(c_rec_temp) + elif c_rec_temp > 5: + c_rec = 10 + elif c_rec_temp > 2: + c_rec = 5 + else: + c_rec = round(c_rec_temp) initial.append( { "article": article.pk, @@ -2275,8 +1922,7 @@ def order_create(request, pk): "v_moy": round(v_moy), "v_et": round(v_et), "v_prev": round(v_prev), - "c_rec_1w": article.box_capacity and c_rec or round(c_rec_tot), - "is_sold": article.is_sold, + "c_rec": article.box_capacity and c_rec or round(c_rec_tot), } ) @@ -2286,9 +1932,7 @@ def order_create(request, pk): formset = cls_formset(request.POST, initial=initial) if not request.user.has_perm("kfet.add_order"): - messages.error( - request, "Permission refusée", extra_tags="permission-denied" - ) + messages.error(request, "Permission refusée") elif formset.is_valid(): order = Order() order.supplier = supplier @@ -2412,9 +2056,7 @@ def order_to_inventory(request, pk): formset = cls_formset(request.POST, initial=initial) if not request.user.has_perm("kfet.order_to_inventory"): - messages.error( - request, "Permission refusée", extra_tags="permission-denied" - ) + messages.error(request, "Permission refusée") elif formset.is_valid(): with transaction.atomic(): inventory = Inventory.objects.create( @@ -2489,9 +2131,7 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView): def form_valid(self, form): # Checking permission if not self.request.user.has_perm("kfet.change_supplier"): - form.add_error( - None, ValidationError("Permission refusée", code="permission-denied") - ) + form.add_error(None, "Permission refusée") return self.form_invalid(form) # Updating return super().form_valid(form) @@ -2501,12 +2141,11 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView): # Statistics # ========== - # --------------- # Vues génériques # --------------- # source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/ -class JSONResponseMixin: +class JSONResponseMixin(object): """ A mixin that can be used to render a JSON response. """ @@ -2535,39 +2174,34 @@ class JSONDetailView(JSONResponseMixin, BaseDetailView): return self.render_to_json_response(context) +class PkUrlMixin(object): + def get_object(self, *args, **kwargs): + get_by = self.kwargs.get(self.pk_url_kwarg) + return get_object_or_404(self.model, **{self.pk_url_kwarg: get_by}) + + class SingleResumeStat(JSONDetailView): - """ - Génère l'interface de sélection pour les statistiques d'un compte/article. - L'interface est constituée d'une série de boutons, qui récupèrent et graphent - des statistiques du même type, sur le même objet mais avec des arguments différents. + """Manifest for a kind of a stat about an object. - Attributs : - - url_stat : URL où récupérer les statistiques - - stats : liste de dictionnaires avec les clés suivantes : - - label : texte du bouton - - url_params : paramètres GET à rajouter à `url_stat` - - default : si `True`, graphe à montrer par défaut + Returns JSON whose payload is an array containing descriptions of a stat: + url to retrieve data, label, ... - On peut aussi définir `stats` dynamiquement, via la fonction `get_stats`. """ - url_stat = None + id_prefix = "" + nb_default = 0 + stats = [] - - def get_stats(self): - return self.stats + url_stat = None def get_context_data(self, **kwargs): # On n'hérite pas + object_id = self.object.id context = {} stats = [] - # On peut avoir récupéré self.object via pk ou slug - if self.pk_url_kwarg in self.kwargs: + prefix = "{}_{}".format(self.id_prefix, object_id) + for i, stat_def in enumerate(self.stats): url_pk = getattr(self.object, self.pk_url_kwarg) - else: - url_pk = getattr(self.object, self.slug_url_kwarg) - - for stat_def in self.get_stats(): url_params_d = stat_def.get("url_params", {}) if len(url_params_d) > 0: url_params = "?{}".format(urlencode(url_params_d)) @@ -2576,21 +2210,42 @@ class SingleResumeStat(JSONDetailView): stats.append( { "label": stat_def["label"], + "btn": "btn_{}_{}".format(prefix, i), "url": "{url}{params}".format( url=reverse(self.url_stat, args=[url_pk]), params=url_params ), - "default": stat_def.get("default", False), } ) + context["id_prefix"] = prefix + context["content_id"] = "content_%s" % prefix context["stats"] = stats + context["default_stat"] = self.nb_default + context["object_id"] = object_id return context -class UserAccountMixin: - """ - Mixin qui vérifie que le compte traité par la vue est celui de l'utilisateur·ice - actuel·le. Dans le cas contraire, renvoie un Http404. - """ +# ----------------------- +# Evolution Balance perso +# ----------------------- +ID_PREFIX_ACC_BALANCE = "balance_acc" + + +class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): + """Manifest for balance stats of an account.""" + + model = Account + context_object_name = "account" + pk_url_kwarg = "trigramme" + url_stat = "kfet.account.stat.balance" + id_prefix = ID_PREFIX_ACC_BALANCE + stats = [ + {"label": "Tout le temps"}, + {"label": "1 an", "url_params": {"last_days": 365}}, + {"label": "6 mois", "url_params": {"last_days": 183}}, + {"label": "3 mois", "url_params": {"last_days": 90}}, + {"label": "30 jours", "url_params": {"last_days": 30}}, + ] + nb_default = 0 def get_object(self, *args, **kwargs): obj = super().get_object(*args, **kwargs) @@ -2598,61 +2253,21 @@ class UserAccountMixin: raise Http404 return obj - -class ScaleMixin(object): - """Mixin pour utiliser les outils de `kfet.statistic`.""" - - def get_context_data(self, *args, **kwargs): - # On n'hérite pas - form = StatScaleForm(self.request.GET, prefix="scale") - - if not form.is_valid(): - raise SuspiciousOperation( - "Invalid StatScaleForm. Did someone tamper with the GET parameters ?" - ) - - scale_name = form.cleaned_data.pop("name") - scale_cls = SCALE_DICT.get(scale_name) - - self.scale = scale_cls(**form.cleaned_data) - - return {"labels": self.scale.get_labels()} + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) -# ----------------------- -# Evolution Balance perso -# ----------------------- +class AccountStatBalance(PkUrlMixin, JSONDetailView): + """Datasets of balance of an account. + Operations and Transfers are taken into account. -@method_decorator(login_required, name="dispatch") -class AccountStatBalanceList(UserAccountMixin, SingleResumeStat): - """ - Menu général pour l'historique de balance d'un compte """ model = Account - slug_url_kwarg = "trigramme" - slug_field = "trigramme" - url_stat = "kfet.account.stat.balance" - stats = [ - {"label": "Tout le temps"}, - {"label": "1 an", "url_params": {"last_days": 365}}, - {"label": "6 mois", "url_params": {"last_days": 183}}, - {"label": "3 mois", "url_params": {"last_days": 90}, "default": True}, - {"label": "30 jours", "url_params": {"last_days": 30}}, - ] - - -@method_decorator(login_required, name="dispatch") -class AccountStatBalance(UserAccountMixin, JSONDetailView): - """ - Statistiques (JSON) d'historique de balance d'un compte. - Prend en compte les opérations et transferts sur la période donnée. - """ - - model = Account - slug_url_kwarg = "trigramme" - slug_field = "trigramme" + pk_url_kwarg = "trigramme" + context_object_name = "account" def get_changes_list(self, last_days=None, begin_date=None, end_date=None): account = self.object @@ -2731,14 +2346,15 @@ class AccountStatBalance(UserAccountMixin, JSONDetailView): def get_context_data(self, *args, **kwargs): context = {} - form = AccountStatForm(self.request.GET) + last_days = self.request.GET.get("last_days", None) + if last_days is not None: + last_days = int(last_days) + begin_date = self.request.GET.get("begin_date", None) + end_date = self.request.GET.get("end_date", None) - if not form.is_valid(): - raise SuspiciousOperation( - "Invalid AccountStatForm. Did someone tamper with the GET parameters ?" - ) - - changes = self.get_changes_list(**form.cleaned_data) + changes = self.get_changes_list( + last_days=last_days, begin_date=begin_date, end_date=end_date + ) context["charts"] = [ {"color": "rgb(200, 20, 60)", "label": "Balance", "values": changes} @@ -2750,63 +2366,90 @@ class AccountStatBalance(UserAccountMixin, JSONDetailView): # TODO: offset return context + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise Http404 + return obj + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + # ------------------------ # Consommation personnelle # ------------------------ +ID_PREFIX_ACC_LAST = "last_acc" +ID_PREFIX_ACC_LAST_DAYS = "last_days_acc" +ID_PREFIX_ACC_LAST_WEEKS = "last_weeks_acc" +ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" -@method_decorator(login_required, name="dispatch") -class AccountStatOperationList(UserAccountMixin, SingleResumeStat): - """ - Menu général pour l'historique de consommation d'un compte - """ +class AccountStatOperationList(PkUrlMixin, SingleResumeStat): + """Manifest for operations stats of an account.""" model = Account - slug_url_kwarg = "trigramme" - slug_field = "trigramme" + context_object_name = "account" + pk_url_kwarg = "trigramme" + id_prefix = ID_PREFIX_ACC_LAST + nb_default = 2 + stats = last_stats_manifest(types=[Operation.PURCHASE]) url_stat = "kfet.account.stat.operation" - def get_stats(self): - scales_def = [ - ( - "Tout le temps", - MonthScale, - {"last": True, "begin": self.object.created_at.replace(tzinfo=None)}, - False, - ), - ("1 an", MonthScale, {"last": True, "n_steps": 12}, False), - ("3 mois", WeekScale, {"last": True, "n_steps": 13}, True), - ("2 semaines", DayScale, {"last": True, "n_steps": 14}, False), - ] + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise Http404 + return obj - return scale_url_params(scales_def) + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) -@method_decorator(login_required, name="dispatch") -class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): - """ - Statistiques (JSON) de consommation (nb d'items achetés) d'un compte. - """ +class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): + """Datasets of operations of an account.""" model = Account - slug_url_kwarg = "trigramme" - slug_field = "trigramme" + pk_url_kwarg = "trigramme" + context_object_name = "account" + id_prefix = "" - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) - - operations = ( - Operation.objects.filter( - type=Operation.PURCHASE, group__on_acc=self.object, canceled_at=None - ) + def get_operations(self, scale, types=None): + # On selectionne les opérations qui correspondent + # à l'article en question et qui ne sont pas annulées + # puis on choisi pour chaques intervalle les opérations + # effectuées dans ces intervalles de temps + all_operations = ( + Operation.objects.filter(group__on_acc=self.object, canceled_at=None) .values("article_nb", "group__at") .order_by("group__at") ) - # On compte les opérations - nb_ventes = self.scale.chunkify_qs( - operations, field="group__at", aggregate=Sum("article_nb") + if types is not None: + all_operations = all_operations.filter(type__in=types) + chunks = scale.get_by_chunks( + all_operations, + field_db="group__at", + field_callback=(lambda d: d["group__at"]), ) + return chunks + + def get_context_data(self, *args, **kwargs): + old_ctx = super().get_context_data(*args, **kwargs) + context = {"labels": old_ctx["labels"]} + scale = self.scale + + types = self.request.GET.get("types", None) + if types is not None: + types = ast.literal_eval(types) + + operations = self.get_operations(types=types, scale=scale) + # On compte les opérations + nb_ventes = [] + for chunk in operations: + ventes = sum(ope["article_nb"] for ope in chunk) + nb_ventes.append(ventes) context["charts"] = [ { @@ -2817,59 +2460,50 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): ] return context + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise Http404 + return obj + + @method_decorator(login_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) + # ------------------------ -# Article Statistiques Last +# Article Satistiques Last # ------------------------ +ID_PREFIX_ART_LAST = "last_art" +ID_PREFIX_ART_LAST_DAYS = "last_days_art" +ID_PREFIX_ART_LAST_WEEKS = "last_weeks_art" +ID_PREFIX_ART_LAST_MONTHS = "last_months_art" -@method_decorator(teamkfet_required, name="dispatch") class ArticleStatSalesList(SingleResumeStat): - """ - Menu pour les statistiques de vente d'un article. - """ + """Manifest for sales stats of an article.""" model = Article + context_object_name = "article" + id_prefix = ID_PREFIX_ART_LAST nb_default = 2 url_stat = "kfet.article.stat.sales" + stats = last_stats_manifest() - def get_stats(self): - first_conso = ( - Operation.objects.filter(article=self.object) - .order_by("group__at") - .values_list("group__at", flat=True) - .first() - ) - if first_conso is None: - # On le crée dans le passé au cas où - first_conso = timezone.now() - timedelta(seconds=1) - scales_def = [ - ( - "Tout le temps", - MonthScale, - {"last": True, "begin": first_conso.strftime("%Y-%m-%d %H:%M:%S")}, - False, - ), - ("1 an", MonthScale, {"last": True, "n_steps": 12}, False), - ("3 mois", WeekScale, {"last": True, "n_steps": 13}, True), - ("2 semaines", DayScale, {"last": True, "n_steps": 14}, False), - ] - - return scale_url_params(scales_def) + @method_decorator(teamkfet_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) -@method_decorator(teamkfet_required, name="dispatch") class ArticleStatSales(ScaleMixin, JSONDetailView): - """ - Statistiques (JSON) de vente d'un article. - Sépare LIQ et les comptes K-Fêt, et rajoute le total. - """ + """Datasets of sales of an article.""" model = Article context_object_name = "article" def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + old_ctx = super().get_context_data(*args, **kwargs) + context = {"labels": old_ctx["labels"]} scale = self.scale all_purchases = ( @@ -2879,16 +2513,26 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): .values("group__at", "article_nb") .order_by("group__at") ) - cof_accts = all_purchases.filter(group__on_acc__cofprofile__is_cof=True) - noncof_accts = all_purchases.exclude(group__on_acc__cofprofile__is_cof=True) + liq_only = all_purchases.filter(group__on_acc__trigramme="LIQ") + liq_exclude = all_purchases.exclude(group__on_acc__trigramme="LIQ") - nb_cof = scale.chunkify_qs( - cof_accts, field="group__at", aggregate=Sum("article_nb") + chunks_liq = scale.get_by_chunks( + liq_only, field_db="group__at", field_callback=lambda d: d["group__at"] ) - nb_noncof = scale.chunkify_qs( - noncof_accts, field="group__at", aggregate=Sum("article_nb") + chunks_no_liq = scale.get_by_chunks( + liq_exclude, field_db="group__at", field_callback=lambda d: d["group__at"] ) - nb_ventes = [n1 + n2 for n1, n2 in zip(nb_cof, nb_noncof)] + + # On compte les opérations + nb_ventes = [] + nb_accounts = [] + nb_liq = [] + for chunk_liq, chunk_no_liq in zip(chunks_liq, chunks_no_liq): + sum_accounts = sum(ope["article_nb"] for ope in chunk_no_liq) + sum_liq = sum(ope["article_nb"] for ope in chunk_liq) + nb_ventes.append(sum_accounts + sum_liq) + nb_accounts.append(sum_accounts) + nb_liq.append(sum_liq) context["charts"] = [ { @@ -2896,27 +2540,15 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): "label": "Toutes consommations", "values": nb_ventes, }, - {"color": "rgb(54, 162, 235)", "label": "Comptes K-Fêt", "values": nb_cof}, + {"color": "rgb(54, 162, 235)", "label": "LIQ", "values": nb_liq}, { "color": "rgb(255, 205, 86)", - "label": "LIQ", - "values": nb_noncof, + "label": "Comptes K-Fêt", + "values": nb_accounts, }, ] return context - -# --- -# Autocompletion views -# --- - - -class AccountCreateAutocompleteView(PermissionRequiredMixin, AutocompleteView): - template_name = "kfet/search_results.html" - permission_required = "kfet.is_team" - search_composer = kfet_autocomplete - - -class AccountSearchAutocompleteView(PermissionRequiredMixin, AutocompleteView): - permission_required = "kfet.is_team" - search_composer = kfet_account_only_autocomplete + @method_decorator(teamkfet_required) + def dispatch(self, *args, **kwargs): + return super().dispatch(*args, **kwargs) diff --git a/manage.py b/manage.py index 00e46405..094ec16f 100755 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings.local") from django.core.management import execute_from_command_line diff --git a/npins/default.nix b/npins/default.nix deleted file mode 100644 index d256a275..00000000 --- a/npins/default.nix +++ /dev/null @@ -1,81 +0,0 @@ -# Generated by npins. Do not modify; will be overwritten regularly -let - data = builtins.fromJSON (builtins.readFile ./sources.json); - version = data.version; - - mkSource = - spec: - assert spec ? type; - let - path = - if spec.type == "Git" then - mkGitSource spec - else if spec.type == "GitRelease" then - mkGitSource spec - else if spec.type == "PyPi" then - mkPyPiSource spec - else if spec.type == "Channel" then - mkChannelSource spec - else - builtins.throw "Unknown source type ${spec.type}"; - in - spec // { outPath = path; }; - - mkGitSource = - { - repository, - revision, - url ? null, - hash, - branch ? null, - ... - }: - assert repository ? type; - # At the moment, either it is a plain git repository (which has an url), or it is a GitHub/GitLab repository - # In the latter case, there we will always be an url to the tarball - if url != null then - (builtins.fetchTarball { - inherit url; - sha256 = hash; # FIXME: check nix version & use SRI hashes - }) - else - assert repository.type == "Git"; - let - urlToName = - url: rev: - let - matched = builtins.match "^.*/([^/]*)(\\.git)?$" repository.url; - - short = builtins.substring 0 7 rev; - - appendShort = if (builtins.match "[a-f0-9]*" rev) != null then "-${short}" else ""; - in - "${if matched == null then "source" else builtins.head matched}${appendShort}"; - name = urlToName repository.url revision; - in - builtins.fetchGit { - url = repository.url; - rev = revision; - inherit name; - allRefs = true; - # hash = hash; - }; - - mkPyPiSource = - { url, hash, ... }: - builtins.fetchurl { - inherit url; - sha256 = hash; - }; - - mkChannelSource = - { url, hash, ... }: - builtins.fetchTarball { - inherit url; - sha256 = hash; - }; -in -if version == 3 then - builtins.mapAttrs (_: mkSource) data.pins -else - throw "Unsupported format version ${toString version} in sources.json. Try running `npins upgrade`" diff --git a/npins/sources.json b/npins/sources.json deleted file mode 100644 index 9ed77931..00000000 --- a/npins/sources.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "pins": { - "kat-pkgs": { - "type": "Git", - "repository": { - "type": "Git", - "url": "https://git.dgnum.eu/lbailly/kat-pkgs.git" - }, - "branch": "master", - "revision": "6b600b716f409c6012b424de006eac3b02148b81", - "url": null, - "hash": "0204f91vxa5qglihpfkf3j5w3k7v98wry861xf2skl024faf9idf" - }, - "nix-pkgs": { - "type": "Git", - "repository": { - "type": "Git", - "url": "https://git.hubrecht.ovh/hubrecht/nix-pkgs" - }, - "branch": "main", - "revision": "ac4ff5a34789ae3398aff9501735b67b6a5a285a", - "url": null, - "hash": "16n37f74p6h30hhid98vab9w5b08xqj4qcshz2kc1jh67z5n49p6" - }, - "nixpkgs": { - "type": "Channel", - "name": "nixos-unstable", - "url": "https://releases.nixos.org/nixos/unstable/nixos-25.05beta719504.a73246e2eef4/nixexprs.tar.xz", - "hash": "1jjmg13jzbqxm5m5ql51n2kq1qggfyb0rhmjwhqhvqxhl350z58a" - } - }, - "version": 3 -} \ No newline at end of file diff --git a/petitscours/forms.py b/petitscours/forms.py index 0d9f38bc..5309b41d 100644 --- a/petitscours/forms.py +++ b/petitscours/forms.py @@ -1,28 +1,37 @@ +from captcha.fields import ReCaptchaField from django import forms from django.contrib.auth.models import User from django.forms import ModelForm -from django.forms.models import inlineformset_factory -from django.utils.translation import gettext_lazy as _ -from hcaptcha.fields import hCaptchaField +from django.forms.models import BaseInlineFormSet, inlineformset_factory from petitscours.models import PetitCoursAbility, PetitCoursDemande -class hCaptchaFieldWithErrors(hCaptchaField): - """ - Pour l'instant, hCaptchaField ne supporte pas le paramètre `error_messages` lors de - l'initialisation. Du coup, on les redéfinit à la main. - """ - - default_error_messages = { - "required": _("Veuillez vérifier que vous êtes bien humain·e."), - "error_hcaptcha": _("Erreur lors de la vérification."), - "invalid_hcaptcha": _("Échec de la vérification !"), - } +class BaseMatieresFormSet(BaseInlineFormSet): + def clean(self): + super().clean() + if any(self.errors): + # Don't bother validating the formset unless each form is + # valid on its own + return + matieres = [] + for i in range(0, self.total_form_count()): + form = self.forms[i] + if not form.cleaned_data: + continue + matiere = form.cleaned_data["matiere"] + niveau = form.cleaned_data["niveau"] + delete = form.cleaned_data["DELETE"] + if not delete and (matiere, niveau) in matieres: + raise forms.ValidationError( + "Vous ne pouvez pas vous inscrire deux fois pour la " + "même matiere avec le même niveau." + ) + matieres.append((matiere, niveau)) class DemandeForm(ModelForm): - captcha = hCaptchaFieldWithErrors() + captcha = ReCaptchaField(attrs={"theme": "clean", "lang": "fr"}) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -49,4 +58,5 @@ MatieresFormSet = inlineformset_factory( User, PetitCoursAbility, fields=("matiere", "niveau", "agrege"), + formset=BaseMatieresFormSet, ) diff --git a/petitscours/models.py b/petitscours/models.py index 0be81449..27b5e931 100644 --- a/petitscours/models.py +++ b/petitscours/models.py @@ -2,7 +2,7 @@ from django.contrib.auth.models import User from django.db import models from django.db.models import Min from django.utils.functional import cached_property -from django.utils.translation import gettext_lazy as _ +from django.utils.translation import ugettext_lazy as _ from shared.utils import choices_length @@ -44,11 +44,6 @@ class PetitCoursAbility(models.Model): class Meta: app_label = "gestioncof" - constraints = [ - models.UniqueConstraint( - fields=["user", "niveau", "matiere"], name="unique_competence_level" - ) - ] verbose_name = "Compétence petits cours" verbose_name_plural = "Compétences des petits cours" @@ -66,7 +61,7 @@ class PetitCoursAbility(models.Model): class PetitCoursDemande(models.Model): name = models.CharField(_("Nom/prénom"), max_length=200) - email = models.EmailField(_("Adresse email"), max_length=300) + email = models.CharField(_("Adresse email"), max_length=300) phone = models.CharField(_("Téléphone (facultatif)"), max_length=20, blank=True) quand = models.CharField( _("Quand ?"), diff --git a/petitscours/templates/petitscours/demande_detail.html b/petitscours/templates/petitscours/demande_detail.html index 8711fcda..e2786599 100644 --- a/petitscours/templates/petitscours/demande_detail.html +++ b/petitscours/templates/petitscours/demande_detail.html @@ -1,5 +1,5 @@ {% extends "petitscours/base_title.html" %} -{% load static %} +{% load staticfiles %} {% block page_size %}col-sm-8{% endblock %} @@ -13,7 +13,7 @@ {% include "petitscours/details_demande_infos.html" %}
    - + {% if demande.traitee %} diff --git a/petitscours/templates/petitscours/demande_list.html b/petitscours/templates/petitscours/demande_list.html index 04132d57..8e5e6d2d 100644 --- a/petitscours/templates/petitscours/demande_list.html +++ b/petitscours/templates/petitscours/demande_list.html @@ -1,5 +1,5 @@ {% extends "petitscours/base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %}

    Demandes de petits cours

    @@ -19,7 +19,7 @@ - + diff --git a/petitscours/templates/petitscours/details_demande_infos.html b/petitscours/templates/petitscours/details_demande_infos.html index 42f37d56..c67095d8 100644 --- a/petitscours/templates/petitscours/details_demande_infos.html +++ b/petitscours/templates/petitscours/details_demande_infos.html @@ -1,4 +1,4 @@ -{% load static %} +{% load staticfiles %}
    Traitée
    Traitée
    Traitée par {{ demande.traitee_par }}
    Traitée le {{ demande.processed }}
    {{ demande.name }} {% for matiere in demande.matieres.all %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %} {{ demande.created|date:"d E Y" }} {% if demande.traitee_par %}{{ demande.traitee_par.username }}{% else %}{% endif %} Détails
    @@ -9,6 +9,6 @@ - +
    Date {{ demande.created }}
    Nom/prénom {{ demande.name }}
    Fréquence {{ demande.freq }}
    Matières {% for matiere in demande.matieres.all %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}
    Niveau souhaité {{ demande.get_niveau_display }}
    Agrégé requis
    Agrégé requis
    Remarques {{ demande.remarques }}
    diff --git a/petitscours/templates/petitscours/inscription.html b/petitscours/templates/petitscours/inscription.html index eaf10524..9512e0b3 100644 --- a/petitscours/templates/petitscours/inscription.html +++ b/petitscours/templates/petitscours/inscription.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} -{% load static %} +{% load staticfiles %} {% block extra_head %} diff --git a/petitscours/templates/petitscours/mails/demandeur.txt b/petitscours/templates/petitscours/mails/demandeur.txt deleted file mode 100644 index 69fed436..00000000 --- a/petitscours/templates/petitscours/mails/demandeur.txt +++ /dev/null @@ -1,17 +0,0 @@ -Bonjour, - -Je vous contacte au sujet de votre annonce passée sur le site du COF pour rentrer en contact avec un élève normalien pour des cours particuliers. Voici les coordonnées d'élèves qui sont motivés par de tels cours et correspondent aux critères que vous nous aviez transmis : - -{% for matiere, proposed in proposals %}¤ {{ matiere }} :{% for user in proposed %} - ¤ {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %} - -{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'élève disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}. - -{% endif %}Si pour une raison ou une autre ces numéros ne suffisaient pas, n'hésitez pas à répondre à cet e-mail et je vous en ferai parvenir d'autres sans problème. -{% if extra|length > 0 %} -{{ extra|safe }} -{% endif %} -Cordialement, - --- -Le COF, BdE de l'ENS \ No newline at end of file diff --git a/petitscours/templates/petitscours/mails/eleve.txt b/petitscours/templates/petitscours/mails/eleve.txt deleted file mode 100644 index 5f2d4750..00000000 --- a/petitscours/templates/petitscours/mails/eleve.txt +++ /dev/null @@ -1,28 +0,0 @@ -Salut, - -Le COF a reçu une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonnées, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les numéros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question : - -¤ Nom : {{ demande.name }} - -¤ Période : {{ demande.quand }} - -¤ Fréquence : {{ demande.freq }} - -¤ Lieu (si préféré) : {{ demande.lieu }} - -{% if matieres|length > 1 %}¤ Matières : -{% for matiere in matieres %} ¤ {{ matiere }} -{% endfor %}{% else %}¤ Matière : {% for matiere in matieres %}{{ matiere }} -{% endfor %}{% endif %} -¤ Niveau : {{ demande.get_niveau_display }} - -¤ Remarques diverses (désolé pour les balises HTML) : {{ demande.remarques }} - -Voilà, cette personne te contactera peut-être sous peu, tu pourras voir les détails directement avec elle (prix, modalités, ...). Pour indication, 30 Euro/h semble être la moyenne. - -Si tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, ça serait cool que tu décoches la case "Recevoir des propositions de petits cours" sur GestioCOF. Ensuite dès que tu voudras réapparaître tu pourras recocher la case et tu seras à nouveau sur la liste. - -À bientôt, - --- -Le COF, pour les petits cours \ No newline at end of file diff --git a/petitscours/templates/petitscours/traitement_demande_autre_niveau.html b/petitscours/templates/petitscours/traitement_demande_autre_niveau.html index c10c8aaf..cb3ec379 100644 --- a/petitscours/templates/petitscours/traitement_demande_autre_niveau.html +++ b/petitscours/templates/petitscours/traitement_demande_autre_niveau.html @@ -1,5 +1,5 @@ {% extends "petitscours/base_title.html" %} -{% load static %} +{% load staticfiles %} {% block realcontent %}

    diff --git a/petitscours/tests/test_views.py b/petitscours/tests/test_petitscours_views.py similarity index 87% rename from petitscours/tests/test_views.py rename to petitscours/tests/test_petitscours_views.py index 9367c258..9a3cc3dc 100644 --- a/petitscours/tests/test_views.py +++ b/petitscours/tests/test_petitscours_views.py @@ -1,13 +1,15 @@ import json -from unittest import mock +import os +from django.contrib import messages from django.contrib.auth import get_user_model -from django.test import TestCase +from django.test import Client, TestCase from django.urls import reverse -from gestioncof.tests.mixins import ViewTestCaseMixin +from gestioncof.tests.testcases import ViewTestCaseMixin from .utils import ( + PetitCoursTestHelpers, create_petitcours_ability, create_petitcours_demande, create_petitcours_subject, @@ -18,7 +20,7 @@ User = get_user_model() class PetitCoursDemandeListViewTestCase(ViewTestCaseMixin, TestCase): url_name = "petits-cours-demandes-list" - url_expected = "/gestion/petitcours/demandes" + url_expected = "/petitcours/demandes" auth_user = "staff" auth_forbidden = [None, "user", "member"] @@ -47,7 +49,7 @@ class PetitCoursDemandeDetailListViewTestCase(ViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/petitcours/demandes/{}".format(self.demande.pk) + return "/petitcours/demandes/{}".format(self.demande.pk) def setUp(self): super().setUp() @@ -60,7 +62,7 @@ class PetitCoursDemandeDetailListViewTestCase(ViewTestCaseMixin, TestCase): class PetitCoursInscriptionViewTestCase(ViewTestCaseMixin, TestCase): url_name = "petits-cours-inscription" - url_expected = "/gestion/petitcours/inscription" + url_expected = "/petitcours/inscription" http_methods = ["GET", "POST"] @@ -77,9 +79,7 @@ class PetitCoursInscriptionViewTestCase(ViewTestCaseMixin, TestCase): self.subject2 = create_petitcours_subject(name="Matière 2") def test_get_forbidden_user_not_cof(self): - self.client.force_login( - self.users["user"], backend="django.contrib.auth.backends.ModelBackend" - ) + self.client.force_login(self.users["user"]) resp = self.client.get(self.url) self.assertRedirects(resp, reverse("cof-denied")) @@ -161,7 +161,9 @@ class PetitCoursInscriptionViewTestCase(ViewTestCaseMixin, TestCase): self.assertFalse(self.user.petitcoursability_set.all()) -class PetitCoursTraitementViewTestCase(ViewTestCaseMixin, TestCase): +class PetitCoursTraitementViewTestCase( + ViewTestCaseMixin, PetitCoursTestHelpers, TestCase +): url_name = "petits-cours-demande-traitement" http_methods = ["GET", "POST"] @@ -175,7 +177,7 @@ class PetitCoursTraitementViewTestCase(ViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/petitcours/demandes/{}/traitement".format(self.demande.pk) + return "/petitcours/demandes/{}/traitement".format(self.demande.pk) def setUp(self): super().setUp() @@ -187,10 +189,14 @@ class PetitCoursTraitementViewTestCase(ViewTestCaseMixin, TestCase): self.demande.matieres.add(self.subject) def test_get(self): + self.require_custommails() + resp = self.client.get(self.url) self.assertEqual(resp.status_code, 200) def test_get_with_match(self): + self.require_custommails() + create_petitcours_ability( user=self.user, matiere=self.subject, niveau="college" ) @@ -206,6 +212,8 @@ class PetitCoursTraitementViewTestCase(ViewTestCaseMixin, TestCase): ) def test_post_with_match(self): + self.require_custommails() + create_petitcours_ability( user=self.user, matiere=self.subject, niveau="college" ) @@ -223,7 +231,9 @@ class PetitCoursTraitementViewTestCase(ViewTestCaseMixin, TestCase): self.assertIsNotNone(self.demande.processed) -class PetitCoursRetraitementViewTestCase(ViewTestCaseMixin, TestCase): +class PetitCoursRetraitementViewTestCase( + ViewTestCaseMixin, PetitCoursTestHelpers, TestCase +): url_name = "petits-cours-demande-retraitement" http_methods = ["GET", "POST"] @@ -237,20 +247,22 @@ class PetitCoursRetraitementViewTestCase(ViewTestCaseMixin, TestCase): @property def url_expected(self): - return "/gestion/petitcours/demandes/{}/retraitement".format(self.demande.pk) + return "/petitcours/demandes/{}/retraitement".format(self.demande.pk) def setUp(self): super().setUp() self.demande = create_petitcours_demande() def test_get(self): + self.require_custommails() + resp = self.client.get(self.url) self.assertEqual(resp.status_code, 200) class PetitCoursDemandeViewTestCase(ViewTestCaseMixin, TestCase): url_name = "petits-cours-demande" - url_expected = "/gestion/petitcours/demande" + url_expected = "/petitcours/demande" http_methods = ["GET", "POST"] @@ -259,15 +271,18 @@ class PetitCoursDemandeViewTestCase(ViewTestCaseMixin, TestCase): def setUp(self): super().setUp() + os.environ["RECAPTCHA_TESTING"] = "True" self.subject1 = create_petitcours_subject() self.subject2 = create_petitcours_subject() + def tearDown(self): + os.environ["RECAPTCHA_TESTING"] = "False" + def test_get(self): resp = self.client.get(self.url) self.assertEqual(resp.status_code, 200) - @mock.patch("hcaptcha.fields.hCaptchaField.clean") - def test_post(self, mock_clean): + def test_post(self): data = { "name": "Le nom", "email": "lemail@mail.net", @@ -279,7 +294,7 @@ class PetitCoursDemandeViewTestCase(ViewTestCaseMixin, TestCase): "agrege_requis": "1", "niveau": "lycee", "remarques": "no comment", - "h-captcha-response": 1, + "g-recaptcha-response": "PASSED", } resp = self.client.post(self.url, data) @@ -289,7 +304,7 @@ class PetitCoursDemandeViewTestCase(ViewTestCaseMixin, TestCase): class PetitCoursDemandeRawViewTestCase(ViewTestCaseMixin, TestCase): url_name = "petits-cours-demande-raw" - url_expected = "/gestion/petitcours/demande-raw" + url_expected = "/petitcours/demande-raw" http_methods = ["GET", "POST"] @@ -298,15 +313,18 @@ class PetitCoursDemandeRawViewTestCase(ViewTestCaseMixin, TestCase): def setUp(self): super().setUp() + os.environ["RECAPTCHA_TESTING"] = "True" self.subject1 = create_petitcours_subject() self.subject2 = create_petitcours_subject() + def tearDown(self): + os.environ["RECAPTCHA_TESTING"] = "False" + def test_get(self): resp = self.client.get(self.url) self.assertEqual(resp.status_code, 200) - @mock.patch("hcaptcha.fields.hCaptchaField.clean") - def test_post(self, mock_clean): + def test_post(self): data = { "name": "Le nom", "email": "lemail@mail.net", @@ -318,7 +336,7 @@ class PetitCoursDemandeRawViewTestCase(ViewTestCaseMixin, TestCase): "agrege_requis": "1", "niveau": "lycee", "remarques": "no comment", - "h-captcha-response": 1, + "g-recaptcha-response": "PASSED", } resp = self.client.post(self.url, data) diff --git a/petitscours/tests/utils.py b/petitscours/tests/utils.py index 131f14dc..99113f9e 100644 --- a/petitscours/tests/utils.py +++ b/petitscours/tests/utils.py @@ -1,4 +1,8 @@ -from gestioncof.tests.utils import create_user +import os + +from django.conf import settings +from django.core.management import call_command + from petitscours.models import ( PetitCoursAbility, PetitCoursAttributionCounter, @@ -9,7 +13,7 @@ from petitscours.models import ( def create_petitcours_ability(**kwargs): if "user" not in kwargs: - kwargs["user"] = create_user("toto") + kwargs["user"] = create_user() if "matiere" not in kwargs: kwargs["matiere"] = create_petitcours_subject() if "niveau" not in kwargs: @@ -25,3 +29,11 @@ def create_petitcours_demande(**kwargs): def create_petitcours_subject(**kwargs): return PetitCoursSubject.objects.create(**kwargs) + + +class PetitCoursTestHelpers: + def require_custommails(self): + data_file = os.path.join( + settings.BASE_DIR, "gestioncof", "management", "data", "custommail.json" + ) + call_command("syncmails", data_file, verbosity=0) diff --git a/petitscours/views.py b/petitscours/views.py index 616e38d0..e309f24a 100644 --- a/petitscours/views.py +++ b/petitscours/views.py @@ -1,5 +1,6 @@ import json +from custommail.shortcuts import render_custom_mail from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import login_required @@ -7,9 +8,7 @@ from django.contrib.auth.models import User from django.core import mail from django.db import transaction from django.shortcuts import get_object_or_404, redirect, render -from django.template import loader from django.utils import timezone -from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.decorators.csrf import csrf_exempt from django.views.generic import DetailView, ListView @@ -71,18 +70,15 @@ def _finalize_traitement( proposed_for.setdefault(user, []).append(matiere) proposed_mails = _generate_eleve_email(demande, proposed_for) - mainmail = ( - "Cours particuliers ENS", - loader.render_to_string( - "petitscours/mails/demandeur.txt", - context={ - "proposals": proposals.items(), - "unsatisfied": unsatisfied, - "extra": '", - }, - ), + mainmail = render_custom_mail( + "petits-cours-mail-demandeur", + { + "proposals": proposals.items(), + "unsatisfied": unsatisfied, + "extra": '", + }, ) if errors is not None: for error in errors: @@ -104,16 +100,11 @@ def _finalize_traitement( def _generate_eleve_email(demande, proposed_for): - subject = "Petits cours ENS par le COF" return [ ( user, - ( - subject, - loader.render_to_string( - "petitscours/mails/eleve.txt", - context={"demande": demande, "matieres": matieres}, - ), + render_custom_mail( + "petit-cours-mail-eleve", {"demande": demande, "matieres": matieres} ), ) for user, matieres in proposed_for.items() @@ -206,20 +197,15 @@ def _traitement_post(request, demande): else: proposed_for[user].append(matiere) proposed_mails = _generate_eleve_email(demande, proposed_for) - mainmail_object = "Cours particuliers ENS" - mainmail_body = loader.render_to_string( - "petitscours/mails/demandeur.txt", - context={ - "proposals": proposals.items(), - "unsatisfied": unsatisfied, - "extra": extra, - }, + mainmail_object, mainmail_body = render_custom_mail( + "petits-cours-mail-demandeur", + {"proposals": proposals.items(), "unsatisfied": unsatisfied, "extra": extra}, ) frommail = settings.MAIL_DATA["petits_cours"]["FROM"] bccaddress = settings.MAIL_DATA["petits_cours"]["BCC"] replyto = settings.MAIL_DATA["petits_cours"]["REPLYTO"] mails_to_send = [] - for user, (mail_object, body) in proposed_mails: + for (user, (mail_object, body)) in proposed_mails: msg = mail.EmailMessage( mail_object, body, @@ -301,7 +287,6 @@ def inscription(request): @csrf_exempt -@xframe_options_sameorigin def demande(request, *, raw: bool = False): success = False if request.method == "POST": diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index a298dfae..cb6917a7 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -1,141 +1,74 @@ #!/bin/sh -# Arête le script quand : -# - une erreur survient -# - on essaie d'utiliser une variable non définie -# - on essaie d'écraser un fichier avec une redirection (>). -set -euC +# Stop if an error is encountered +set -e -# Configuration de la base de données, redis, Django, etc. -# Tous les mots de passe sont constant et en clair dans le fichier car c'est +# Configuration de la base de données. Le mot de passe est constant car c'est # pour une installation de dév locale qui ne sera accessible que depuis la # machine virtuelle. -readonly DBUSER="cof_gestion" -readonly DBNAME="cof_gestion" -readonly DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" -readonly REDIS_PASSWD="dummy" -readonly DJANGO_SETTINGS_MODULE="gestioasso.settings.dev" +DBUSER="cof_gestion" +DBNAME="cof_gestion" +DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" - -# --- -# Installation des paquets systèmes -# --- - -get_packages_list () { - sed 's/#.*$//' /vagrant/provisioning/packages.list | grep -v '^ *$' -} - -# https://github.com/chef/bento/issues/661 -export DEBIAN_FRONTEND=noninteractive - -apt-get update -apt-get -y upgrade -get_packages_list | xargs apt-get install -y - - -# --- -# Configuration de la base de données -# --- +# Installation de paquets utiles +apt-get update && apt-get upgrade -y +apt-get install -y python3-pip python3-dev python3-venv libpq-dev postgresql \ + postgresql-contrib libjpeg-dev nginx git redis-server # Postgresql -pg_user_exists () { - sudo -u postgres psql postgres -tAc \ - "SELECT 1 FROM pg_roles WHERE rolname='$1'" \ - | grep -q '^1$' -} - -pg_db_exists () { - sudo -u postgres psql postgres -tAc \ - "SELECT 1 FROM pg_database WHERE datname='$1'" \ - | grep -q '^1$' -} - -pg_db_exists "$DBNAME" || sudo -u postgres createdb "$DBNAME" -pg_user_exists "$DBUSER" || sudo -u postgres createuser -SdR "$DBUSER" +sudo -u postgres createdb $DBNAME +sudo -u postgres createuser -SdR $DBUSER sudo -u postgres psql -c "ALTER USER $DBUSER WITH PASSWORD '$DBPASSWD';" sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE $DBNAME TO $DBUSER;" -# --- -# Configuration de redis (pour django-channels) -# --- - # Redis -redis-cli CONFIG SET requirepass "$REDIS_PASSWD" -redis-cli -a "$REDIS_PASSWD" CONFIG REWRITE +REDIS_PASSWD="dummy" +redis-cli CONFIG SET requirepass $REDIS_PASSWD +redis-cli -a $REDIS_PASSWD CONFIG REWRITE - -# --- -# Préparation de Django -# --- - -# Dossiers pour le contenu statique +# Contenu statique mkdir -p /srv/gestiocof/media mkdir -p /srv/gestiocof/static -chown -R vagrant:www-data /srv/gestiocof +chown -R ubuntu:www-data /srv/gestiocof + +# Nginx +ln -s -f /vagrant/provisioning/nginx.conf /etc/nginx/sites-enabled/gestiocof.conf +rm -f /etc/nginx/sites-enabled/default +systemctl reload nginx # Environnement virtuel python -sudo -H -u vagrant python3 -m venv ~vagrant/venv -sudo -H -u vagrant ~vagrant/venv/bin/pip install -U pip -sudo -H -u vagrant ~vagrant/venv/bin/pip install \ - -r /vagrant/requirements-prod.txt \ - -r /vagrant/requirements-devel.txt \ +sudo -H -u ubuntu python3 -m venv ~ubuntu/venv +sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -U pip +sudo -H -u ubuntu ~ubuntu/venv/bin/pip install -r /vagrant/requirements-devel.txt # Préparation de Django cd /vagrant -ln -s -f secret_example.py gestioasso/settings/secret.py -sudo -H -u vagrant \ - DJANGO_SETTINGS_MODULE="$DJANGO_SETTINGS_MODULE"\ - /bin/sh -c ". ~vagrant/venv/bin/activate && /bin/sh provisioning/prepare_django.sh" -~vagrant/venv/bin/python manage.py collectstatic \ - --noinput \ - --settings "$DJANGO_SETTINGS_MODULE" +ln -s -f secret_example.py cof/settings/secret.py +sudo -H -u ubuntu \ + DJANGO_SETTINGS_MODULE='cof.settings.dev' \ + bash -c ". ~/venv/bin/activate && bash provisioning/prepare_django.sh" +/home/ubuntu/venv/bin/python manage.py collectstatic --noinput --settings cof.settings.dev +# Installation du cron pour les mails de rappels +sudo -H -u ubuntu crontab provisioning/cron.dev -# --- -# Units systemd -# --- +# Daphne + runworker +cp /vagrant/provisioning/daphne.service /etc/systemd/system/daphne.service +cp /vagrant/provisioning/worker.service /etc/systemd/system/worker.service +systemctl enable daphne.service +systemctl enable worker.service +systemctl start daphne.service +systemctl start worker.service -# - Daphne fait tourner le serveur asgi -# - worker = https://channels.readthedocs.io/en/stable/topics/worker.html -# - Mails de rappels du BdA -# - Mails de revente du BdA -ln -sf /vagrant/provisioning/systemd/daphne.service /etc/systemd/system/daphne.service -ln -sf /vagrant/provisioning/systemd/worker.service /etc/systemd/system/worker.service -ln -sf /vagrant/provisioning/systemd/reventes.service /etc/systemd/system/reventes.service -ln -sf /vagrant/provisioning/systemd/rappels.service /etc/systemd/system/rappels.service -ln -sf /vagrant/provisioning/systemd/reventes.timer /etc/systemd/system/reventes.timer -ln -sf /vagrant/provisioning/systemd/rappels.timer /etc/systemd/system/rappels.timer -systemctl enable --now daphne.service -systemctl enable --now worker.service -systemctl enable rappels.timer -systemctl enable reventes.timer - - -# --- -# Configuration du shell de l'utilisateur 'vagrant' pour utiliser le bon fichier -# de settings et et bon virtualenv. -# --- - -# On utilise .bash_aliases au lieu de .bashrc pour ne pas écraser la -# configuration par défaut. -rm -f ~vagrant/.bash_aliases -cat > ~vagrant/.bash_aliases <> ~ubuntu/.bashrc <> /vagrant/rappels.log ; /ubuntu/home/venv/bin/python /vagrant/manage.py sendrappels >> /vagrant/rappels.log 2>&1 +*/5 * * * * /ubuntu/home/venv/bin/python /vagrant/manage.py manage_revente >> /vagrant/reventes.log 2>&1 diff --git a/provisioning/daphne.service b/provisioning/daphne.service new file mode 100644 index 00000000..41327ce5 --- /dev/null +++ b/provisioning/daphne.service @@ -0,0 +1,16 @@ +Description="GestioCOF" +After=syslog.target +After=network.target + +[Service] +Type=simple +User=ubuntu +Group=ubuntu +TimeoutSec=300 +WorkingDirectory=/vagrant +Environment="DJANGO_SETTINGS_MODULE=cof.settings.dev" +ExecStart=/home/ubuntu/venv/bin/daphne -u /srv/gestiocof/gestiocof.sock \ + cof.asgi:channel_layer + +[Install] +WantedBy=multi-user.target diff --git a/provisioning/nginx.conf b/provisioning/nginx.conf new file mode 100644 index 00000000..015e1712 --- /dev/null +++ b/provisioning/nginx.conf @@ -0,0 +1,56 @@ +upstream gestiocof { + # Daphne listens on a unix socket + server unix:/srv/gestiocof/gestiocof.sock; +} + +server { + listen 80; + + server_name localhost; + root /srv/gestiocof/; + + # / → /gestion/ + # /gestion → /gestion/ + rewrite ^/$ /gestion/; + rewrite ^/gestion$ /gestion/; + + # Static files + location /static/ { + access_log off; + add_header Cache-Control "public"; + expires 7d; + } + + # Uploaded media + location /media/ { + access_log off; + add_header Cache-Control "public"; + expires 7d; + } + + location /gestion/ { + # A copy-paste of what we have in production + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-SSL-Client-Serial $ssl_client_serial; + proxy_set_header X-SSL-Client-Verify $ssl_client_verify; + proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn; + proxy_set_header Daphne-Root-Path /gestion; + + location /gestion/ws/ { + # See http://nginx.org/en/docs/http/websocket.html + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_pass http://gestiocof/ws/; + } + + location /gestion/ { + proxy_pass http://gestiocof; + } + } +} diff --git a/provisioning/nginx/gestiocof.conf b/provisioning/nginx/gestiocof.conf deleted file mode 100644 index 7d7567c6..00000000 --- a/provisioning/nginx/gestiocof.conf +++ /dev/null @@ -1,42 +0,0 @@ -upstream gestiocof { - # Daphne listens on a unix socket - server unix:/srv/gestiocof/gestiocof.sock; -} - -server { - listen 80; - listen [::]:80; - - server_name _; - root /srv/gestiocof/; - - # Redirection: - rewrite ^/$ http://localhost:8080/gestion/ redirect; - rewrite ^/gestion$ http://localhost:8080/gestion/ redirect; - - # Les pages statiques sont servies à part. - location /static { try_files $uri $uri/ =404; } - location /media { try_files $uri $uri/ =404; } - - # On proxy-pass les requêtes vers les pages dynamiques à daphne - location / { - proxy_set_header Host $http_host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_set_header X-SSL-Client-Serial $ssl_client_serial; - proxy_set_header X-SSL-Client-Verify $ssl_client_verify; - proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn; - proxy_pass http://gestiocof; - } - - # Pour les websockets : - # See http://nginx.org/en/docs/http/websocket.html. - location /ws/ { - proxy_buffering off; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_pass http://gestiocof/ws/; - } -} diff --git a/provisioning/packages.list b/provisioning/packages.list deleted file mode 100644 index 34714442..00000000 --- a/provisioning/packages.list +++ /dev/null @@ -1,25 +0,0 @@ -# Python -python3-pip -python3-dev -python3-venv - -# Pour installer authens depuis git.eleves -git - -# Postgres -libpq-dev -postgresql -postgresql-contrib - -# Pour Pillow -libjpeg-dev - -# Outils de prod -nginx # Test -redis-server - -# Le LDAP -libldap2-dev -libsasl2-dev -slapd -ldap-utils diff --git a/provisioning/prepare_django.sh b/provisioning/prepare_django.sh index 324deaf1..f3358873 100644 --- a/provisioning/prepare_django.sh +++ b/provisioning/prepare_django.sh @@ -1,10 +1,11 @@ -#!/bin/sh +#!/bin/bash -set -euC +# Stop if an error is encountered. +set -e -mkdir -p .static -python manage.py migrate --noinput +python manage.py migrate python manage.py sync_page_translation_fields python manage.py update_translation_fields python manage.py loaddata gestion sites articles python manage.py loaddevdata +python manage.py syncmails gestioncof/management/data/custommail.json diff --git a/provisioning/systemd/daphne.service b/provisioning/systemd/daphne.service deleted file mode 100644 index bae9f3ca..00000000 --- a/provisioning/systemd/daphne.service +++ /dev/null @@ -1,17 +0,0 @@ -Description="GestioCOF - Daphne" -After=syslog.target -After=network.target - -[Service] -Type=simple -User=vagrant -Group=vagrant -TimeoutSec=300 -WorkingDirectory=/vagrant -Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" -ExecStart=/home/vagrant/venv/bin/daphne \ - -u /srv/gestiocof/gestiocof.sock \ - gestioasso.asgi:application - -[Install] -WantedBy=multi-user.target diff --git a/provisioning/systemd/rappels.service b/provisioning/systemd/rappels.service deleted file mode 100644 index 0a4986f9..00000000 --- a/provisioning/systemd/rappels.service +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] -Description=Envoi des mails de rappel des spectales BdA - -[Service] -Type=oneshot -User=vagrant -Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" -ExecStart=/home/vagrant/venv/bin/python /vagrant/manage.py sendrappels diff --git a/provisioning/systemd/rappels.timer b/provisioning/systemd/rappels.timer deleted file mode 100644 index f05c54e0..00000000 --- a/provisioning/systemd/rappels.timer +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Envoi des mails de rappel des spectales BdA - -[Timer] -OnBootSec=10min -OnUnitActiveSec=3h - -[Install] -WantedBy=timers.target diff --git a/provisioning/systemd/reventes.service b/provisioning/systemd/reventes.service deleted file mode 100644 index 266c0646..00000000 --- a/provisioning/systemd/reventes.service +++ /dev/null @@ -1,8 +0,0 @@ -[Unit] -Description=Envoi des mails de BdA-Revente - -[Service] -Type=oneshot -User=vagrant -Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" -ExecStart=/home/vagrant/venv/bin/python /vagrant/manage.py manage_reventes diff --git a/provisioning/systemd/reventes.timer b/provisioning/systemd/reventes.timer deleted file mode 100644 index 2ccaf7bf..00000000 --- a/provisioning/systemd/reventes.timer +++ /dev/null @@ -1,9 +0,0 @@ -[Unit] -Description=Envoi des mails de BdA-Revente - -[Timer] -OnBootSec=15min -OnUnitActiveSec=15min - -[Install] -WantedBy=timers.target diff --git a/provisioning/systemd/worker.service b/provisioning/systemd/worker.service deleted file mode 100644 index 0d97e9a4..00000000 --- a/provisioning/systemd/worker.service +++ /dev/null @@ -1,19 +0,0 @@ -[Unit] -Description="GestioCOF" -After=syslog.target -After=network.target - -[Service] -Type=simple -User=vagrant -Group=vagrant -TimeoutSec=300 -WorkingDirectory=/vagrant -Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" -ExecStart=/home/vagrant/venv/bin/python manage.py runworker \ - 'kfet.open.team' \ - 'kfet.open.base' \ - 'kpsul' - -[Install] -WantedBy=multi-user.target diff --git a/provisioning/worker.service b/provisioning/worker.service new file mode 100644 index 00000000..42836cfe --- /dev/null +++ b/provisioning/worker.service @@ -0,0 +1,16 @@ +[Unit] +Description="GestioCOF" +After=syslog.target +After=network.target + +[Service] +Type=simple +User=ubuntu +Group=ubuntu +TimeoutSec=300 +WorkingDirectory=/vagrant +Environment="DJANGO_SETTINGS_MODULE=cof.settings.dev" +ExecStart=/home/ubuntu/venv/bin/python manage.py runworker + +[Install] +WantedBy=multi-user.target diff --git a/requirements-devel.txt b/requirements-devel.txt index 2de02a5d..7907bcd9 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -1,8 +1,8 @@ -r requirements.txt -django-debug-toolbar==4.4.6 +django-debug-toolbar ipython # Tools -black==22.3.0 +# black # Uncomment when GC & most distros run with Python>=3.6 flake8 isort diff --git a/requirements-prod.txt b/requirements-prod.txt index 45ac4920..e08ac120 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -1,15 +1,14 @@ -r requirements.txt # Postgresql bindings -psycopg2==2.9.10 +psycopg2<2.8 # Redis -django-redis-cache==3.0.1 -redis==3.5.3 -channels-redis==3.4.1 +django-redis-cache==2.1.* # ASGI protocol and HTTP server -daphne==3.0.2 +asgiref==1.1.1 +daphne==1.3.0 # ldap bindings -python-ldap +ldap3 diff --git a/requirements.txt b/requirements.txt index 65d1d380..6c3d799c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,19 +1,20 @@ -Django==4.2.17 -Pillow==11.0.0 -authens==0.2.0 -channels==3.0.5 -configparser==7.1.0 -django-autocomplete-light==3.11.0 -django-bootstrap-form==3.4 -django-cas-ng==5.0.1 -django-cors-headers==4.6.0 -django-djconfig==0.11.0 -django-hCaptcha==0.2.0 -django-js-reverse==0.10.2 -django-widget-tweaks==1.5.0 -icalendar==6.1.0 -python-dateutil==2.9.0.post0 +configparser==3.5.0 +Django==2.2.* +django-autocomplete-light==3.3.* +django-cas-ng==3.6.* +django-djconfig==0.8.0 +django-recaptcha==1.4.0 +icalendar +Pillow +django-bootstrap-form==3.3 +asgi-redis==1.3.0 statistics==1.0.3.5 -wagtail-modeltranslation==0.15.1 -wagtail==6.3.1 -wagtailmenus==4.0.1 +django-widget-tweaks==1.4.1 +git+https://git.eleves.ens.fr/cof-geek/django_custommail.git#egg=django_custommail +channels==1.1.5 +python-dateutil +wagtail==2.7.* +wagtailmenus==3.* +wagtail-modeltranslation==0.10.* +django-cors-headers==2.2.0 +django-js-reverse diff --git a/setup.cfg b/setup.cfg index 9b1c72d0..100ddb22 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,12 +3,13 @@ source = bda bds clubs + cof events - gestioasso gestioncof kfet petitscours shared + utils omit = *migrations* *test*.py @@ -35,7 +36,9 @@ combine_as_imports = true default_section = THIRDPARTY force_grid_wrap = 0 include_trailing_comma = true -known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared +known_django = django +known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared,utils line_length = 88 multi_line_output = 3 +not_skip = __init__.py sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER diff --git a/shared/autocomplete.py b/shared/autocomplete.py deleted file mode 100644 index a601d5f9..00000000 --- a/shared/autocomplete.py +++ /dev/null @@ -1,251 +0,0 @@ -import logging -from collections import namedtuple - -from django.conf import settings -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ - -if getattr(settings, "LDAP_SERVER_URL", None): - import ldap -else: - # shared.tests.testcases.TestCaseMixin.mockLDAP needs - # an ldap object to be in the scope - ldap = None - - -django_logger = logging.getLogger("django.request") - - -class SearchUnit: - """Base class for all the search utilities. - - A search unit should implement a `search` method taking a list of keywords as - argument and returning an iterable of search results. - - It might optionally implement the following methods and attributes: - - - verbose_name (attribute): a nice name to refer to the results of this search unit - in templates. Examples: "COF Members", "K-Fêt accounts", etc. - - - result_verbose_name (method): a callable that takes one search result as an input - and returns a nice name to refer to this particular result in templates. - Example: `lambda user: user.get_full_name()` - - - result_link (method): a callable that takes one search result and returns a url - to make this particular search result clickable on the search page. For instance - this can be a link to a detail view of the object. - - - result_uuid (method): a callable that takes one result as an input and returns an - identifier that is globally unique across search units for this object. - This is used to compare results coming from different search units in the - `Compose` class. For instance, if the same user can be returned by the LDAP - search and a model search instance, using the clipper login as a UUID in both - units avoids this user to be returned twice by `Compose`. - Returning `None` means that the object should be considered unique. - """ - - # Mandatory method - - def search(self, _keywords): - raise NotImplementedError( - "Class implementing the SearchUnit interface should implement the search " - "method" - ) - - # Optional attributes and methods - - verbose_name = None - - def result_verbose_name(self, result): - """Hook to customize the way results are displayed.""" - return str(result) - - def result_link(self, result): - """Hook to add a link on individual results on the search page.""" - return None - - def result_uuid(self, result): - """A universal unique identifier for the search results.""" - return None - - -# --- -# Model-based search -# --- - - -class ModelSearch(SearchUnit): - """Basic search engine for models based on filtering. - - The class should be configured through its `model` class attribute: the `search` - method will return a queryset of instances of this model. The `search_fields` - attributes indicates which fields to search in. - - Example: - - >>> from django.contrib.auth.models import User - >>> - >>> class UserSearch(ModelSearch): - ... model = User - ... search_fields = ["username", "first_name", "last_name"] - >>> - >>> user_search = UserSearch() # has type ModelSearch[User] - >>> user_search.search(["toto", "foo"]) # returns a queryset of Users - """ - - model = None - search_fields = [] - - def __init__(self): - if self.verbose_name is None: - self.verbose_name = "{} search".format(self.model._meta.verbose_name) - - def get_queryset_filter(self, keywords): - filter_q = Q() - - if not keywords: - return filter_q - - for keyword in keywords: - kw_filter = Q() - for field in self.search_fields: - kw_filter |= Q(**{"{}__icontains".format(field): keyword}) - filter_q &= kw_filter - - return filter_q - - def search(self, keywords): - """Returns the queryset of model instances matching all the keywords. - - The semantic of the search is the following: a model instance appears in the - search results iff all of the keywords given as arguments occur in at least one - of the search fields. - """ - - return self.model.objects.filter(self.get_queryset_filter(keywords)) - - -# --- -# LDAP search -# --- - -Clipper = namedtuple("Clipper", ["clipper", "fullname", "mail"]) - - -class LDAPSearch(SearchUnit): - ldap_server_url = getattr(settings, "LDAP_SERVER_URL", None) - domain_component = "dc=spi,dc=ens,dc=fr" - search_fields = ["cn", "uid"] - attr_list = ["cn", "uid", "mail"] - - verbose_name = _("Comptes clippers") - - def get_ldap_query(self, keywords): - """Return a search query with the following semantics: - - A Clipper appears in the search results iff all of the keywords given as - arguments occur in at least one of the search fields. - """ - - # Dumb but safe - keywords = filter(str.isalnum, keywords) - - ldap_filters = [] - - for keyword in keywords: - ldap_filter = "(|{})".format( - "".join( - "({}=*{}*)".format(field, keyword) for field in self.search_fields - ) - ) - ldap_filters.append(ldap_filter) - - return "(&{})".format("".join(ldap_filters)) - - def search(self, keywords): - """Return a list of Clipper objects matching all the keywords.""" - - query = self.get_ldap_query(keywords) - - if ldap is None or query == "(&)": - return [] - - try: - ldap_obj = ldap.initialize(self.ldap_server_url) - res = ldap_obj.search_s( - self.domain_component, ldap.SCOPE_SUBTREE, query, self.attr_list - ) - return [ - Clipper( - clipper=attrs["uid"][0].decode("utf-8"), - fullname=attrs["cn"][0].decode("utf-8"), - mail=attrs["mail"][0].decode("utf-8"), - ) - for (_, attrs) in res - if "uid" in attrs # Hack to discard weird accounts like root - ] - except ldap.LDAPError as err: - django_logger.error("An LDAP error occurred", exc_info=err) - return [] - - def result_verbose_name(self, clipper): - return "{} ({})".format(clipper.fullname, clipper.clipper) - - def result_uuid(self, clipper): - return clipper.clipper - - -# --- -# Composition of autocomplete units -# --- - - -class Compose: - """Search with several units and remove duplicate results. - - The `search_units` class attribute should be a list of pairs of the form `(name, - search_unit)`. - - The `search` method produces a dictionary whose keys are the `name`s given in - `search_units` and whose values are iterables produced by the different search - units. - - Typical Example: - - >>> from django.contrib.auth.models import User - >>> - >>> class UserSearch(ModelSearch): - ... model = User - ... search_fields = ["username", "first_name", "last_name"] - ... - ... def result_uuid(self, user): - ... # Assuming that `.username` stores the clipper login of already - ... # registered users, this avoids showing the same user twice (here and in - ... # then ldap unit). - ... return user.username - >>> - >>> class UserAndClipperSearch(Compose): - ... search_units = [ - ... ("users", UserSearch()), - ... ("clippers", LDAPSearch()), - ... ] - - In this example, clipper accounts that already have an associated user (i.e. with a - username equal to the clipper login), will not appear in the results. - """ - - search_units = [] - - def search(self, keywords): - seen_uuids = set() - results = {} - for name, search_unit in self.search_units: - uniq_res = [] - for r in search_unit.search(keywords): - uuid = search_unit.result_uuid(r) - if uuid is None or uuid not in seen_uuids: - uniq_res.append(r) - if uuid is not None: - seen_uuids.add(uuid) - results[name] = uniq_res - return results diff --git a/shared/channels.py b/shared/channels.py deleted file mode 100644 index ae8c1248..00000000 --- a/shared/channels.py +++ /dev/null @@ -1,45 +0,0 @@ -import datetime -import random -from decimal import Decimal - -import msgpack -from channels_redis.core import RedisChannelLayer - - -def encode_kf(obj): - if isinstance(obj, Decimal): - return {"__decimal__": True, "as_str": str(obj)} - elif isinstance(obj, datetime.datetime): - return {"__datetime__": True, "as_str": obj.strftime("%Y%m%dT%H:%M:%S.%f")} - return obj - - -def decode_kf(obj): - if "__decimal__" in obj: - obj = Decimal(obj["as_str"]) - elif "__datetime__" in obj: - obj = datetime.datetime.strptime(obj["as_str"], "%Y%m%dT%H:%M:%S.%f") - return obj - - -class ChannelLayer(RedisChannelLayer): - def serialize(self, message): - """Serializes to a byte string.""" - value = msgpack.packb(message, default=encode_kf, use_bin_type=True) - - if self.crypter: - value = self.crypter.encrypt(value) - - # As we use an sorted set to expire messages - # we need to guarantee uniqueness, with 12 bytes. - random_prefix = random.getrandbits(8 * 12).to_bytes(12, "big") - return random_prefix + value - - def deserialize(self, message): - """Deserializes from a byte string.""" - # Removes the random prefix - message = message[12:] - - if self.crypter: - message = self.crypter.decrypt(message, self.expiry + 10) - return msgpack.unpackb(message, object_hook=decode_kf, raw=False) diff --git a/shared/forms.py b/shared/forms.py deleted file mode 100644 index 97094e29..00000000 --- a/shared/forms.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.forms.models import ModelForm - - -class ProtectedModelForm(ModelForm): - """ - Extension de `ModelForm` - - Quand on save un champ `ManyToMany` dans un `ModelForm`, la méthode appelée - est .set(), qui écrase l'intégralité du contenu. - Le problème survient quand le `field` a un queryset restreint, et qu'on ne - veut pas toucher aux choix qui ne sont pas dans ce queryset... - C'est le but de ce mixin. - - Attributs : - - `protected_fields` : champs qu'on souhaite protéger. - """ - - protected_fields = [] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - for field_name in self.protected_fields: - if field_name not in self.fields: - raise ValueError("Le champ %s n'existe pas !" % field_name) - - def _get_protected_elts(self, field_name): - """ - Renvoie tous les éléments de `instance.` qui ne sont pas - dans `self..queryset` (et sont donc à conserver). - - NB : on "désordonne" tous les querysets via `.order_by()` car Django - ne peut pas effectuer une union de QS ordonnés. - """ - if self.instance.pk: - previous = getattr(self.instance, field_name).order_by() - selectable = self.fields[field_name].queryset.order_by() - return previous.difference(selectable) - else: - # Nouvelle instance, rien à protéger. - return self.fields[field_name].queryset.none() - - def clean(self): - cleaned_data = super().clean() - for field_name in self.protected_fields: - selected_elts = cleaned_data[field_name].order_by() - protected_elts = self._get_protected_elts(field_name) - cleaned_data[field_name] = selected_elts.union(protected_elts) - - return cleaned_data diff --git a/shared/static/fonts/CarterOne/carterOne.css b/shared/static/fonts/CarterOne/carterOne.css deleted file mode 100644 index 1380934a..00000000 --- a/shared/static/fonts/CarterOne/carterOne.css +++ /dev/null @@ -1,10 +0,0 @@ -/* carter-one-regular - latin */ -@font-face { - font-family: 'Carter One'; - font-style: normal; - font-weight: 400; - src: local('Carter One'), local('CarterOne'), - url('./fonts/carter-one-v11-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('./fonts/carter-one-v11-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - diff --git a/shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff b/shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff deleted file mode 100644 index 851ed743..00000000 Binary files a/shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff and /dev/null differ diff --git a/shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff2 b/shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff2 deleted file mode 100644 index 132f3c5a..00000000 Binary files a/shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff2 and /dev/null differ diff --git a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff b/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff deleted file mode 100644 index 98bafe5e..00000000 Binary files a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff and /dev/null differ diff --git a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff2 b/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff2 deleted file mode 100644 index af998cac..00000000 Binary files a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff2 and /dev/null differ diff --git a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff b/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff deleted file mode 100644 index 42232ee2..00000000 Binary files a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff and /dev/null differ diff --git a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff2 b/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff2 deleted file mode 100644 index 6daac0dd..00000000 Binary files a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff2 and /dev/null differ diff --git a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff b/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff deleted file mode 100644 index f2a7dd34..00000000 Binary files a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff and /dev/null differ diff --git a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff2 b/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff2 deleted file mode 100644 index ce34a9fe..00000000 Binary files a/shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff2 and /dev/null differ diff --git a/shared/static/fonts/SourceSansPro/sourceSansPro.css b/shared/static/fonts/SourceSansPro/sourceSansPro.css deleted file mode 100644 index f7bfbe2f..00000000 --- a/shared/static/fonts/SourceSansPro/sourceSansPro.css +++ /dev/null @@ -1,30 +0,0 @@ -/* source-sans-pro-300 - latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 300; - src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), - url('./fonts/source-sans-pro-v13-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('./fonts/source-sans-pro-v13-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-300italic - latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: italic; - font-weight: 300; - src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'), - url('./fonts/source-sans-pro-v13-latin-300italic.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('./fonts/source-sans-pro-v13-latin-300italic.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - -/* source-sans-pro-700 - latin */ -@font-face { - font-family: 'Source Sans Pro'; - font-style: normal; - font-weight: 700; - src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), - url('./fonts/source-sans-pro-v13-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ - url('./fonts/source-sans-pro-v13-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ -} - diff --git a/shared/static/src/bulma/bulma-rtl.sass b/shared/static/src/bulma/bulma-rtl.sass deleted file mode 100644 index daeba985..00000000 --- a/shared/static/src/bulma/bulma-rtl.sass +++ /dev/null @@ -1,3 +0,0 @@ -@charset "utf-8" -$rtl: true -@import "bulma" diff --git a/shared/static/src/bulma/bulma.sass b/shared/static/src/bulma/bulma.sass deleted file mode 100644 index 4b7b7a66..00000000 --- a/shared/static/src/bulma/bulma.sass +++ /dev/null @@ -1,10 +0,0 @@ -@charset "utf-8" -/*! bulma.io v0.9.0 | MIT License | github.com/jgthms/bulma */ -@import "sass/utilities/_all" -@import "sass/base/_all" -@import "sass/elements/_all" -@import "sass/form/_all" -@import "sass/components/_all" -@import "sass/grid/_all" -@import "sass/helpers/_all" -@import "sass/layout/_all" diff --git a/shared/static/src/bulma/sass/base/_all.sass b/shared/static/src/bulma/sass/base/_all.sass deleted file mode 100644 index ce1dddc9..00000000 --- a/shared/static/src/bulma/sass/base/_all.sass +++ /dev/null @@ -1,4 +0,0 @@ -@charset "utf-8" - -@import "minireset.sass" -@import "generic.sass" diff --git a/shared/static/src/bulma/sass/base/generic.sass b/shared/static/src/bulma/sass/base/generic.sass deleted file mode 100644 index 75d6efd8..00000000 --- a/shared/static/src/bulma/sass/base/generic.sass +++ /dev/null @@ -1,142 +0,0 @@ -$body-background-color: $scheme-main !default -$body-size: 16px !default -$body-min-width: 300px !default -$body-rendering: optimizeLegibility !default -$body-family: $family-primary !default -$body-overflow-x: hidden !default -$body-overflow-y: scroll !default - -$body-color: $text !default -$body-font-size: 1em !default -$body-weight: $weight-normal !default -$body-line-height: 1.5 !default - -$code-family: $family-code !default -$code-padding: 0.25em 0.5em 0.25em !default -$code-weight: normal !default -$code-size: 0.875em !default - -$small-font-size: 0.875em !default - -$hr-background-color: $background !default -$hr-height: 2px !default -$hr-margin: 1.5rem 0 !default - -$strong-color: $text-strong !default -$strong-weight: $weight-bold !default - -$pre-font-size: 0.875em !default -$pre-padding: 1.25rem 1.5rem !default -$pre-code-font-size: 1em !default - -html - background-color: $body-background-color - font-size: $body-size - -moz-osx-font-smoothing: grayscale - -webkit-font-smoothing: antialiased - min-width: $body-min-width - overflow-x: $body-overflow-x - overflow-y: $body-overflow-y - text-rendering: $body-rendering - text-size-adjust: 100% - -article, -aside, -figure, -footer, -header, -hgroup, -section - display: block - -body, -button, -input, -select, -textarea - font-family: $body-family - -code, -pre - -moz-osx-font-smoothing: auto - -webkit-font-smoothing: auto - font-family: $code-family - -body - color: $body-color - font-size: $body-font-size - font-weight: $body-weight - line-height: $body-line-height - -// Inline - -a - color: $link - cursor: pointer - text-decoration: none - strong - color: currentColor - &:hover - color: $link-hover - -code - background-color: $code-background - color: $code - font-size: $code-size - font-weight: $code-weight - padding: $code-padding - -hr - background-color: $hr-background-color - border: none - display: block - height: $hr-height - margin: $hr-margin - -img - height: auto - max-width: 100% - -input[type="checkbox"], -input[type="radio"] - vertical-align: baseline - -small - font-size: $small-font-size - -span - font-style: inherit - font-weight: inherit - -strong - color: $strong-color - font-weight: $strong-weight - -// Block - -fieldset - border: none - -pre - +overflow-touch - background-color: $pre-background - color: $pre - font-size: $pre-font-size - overflow-x: auto - padding: $pre-padding - white-space: pre - word-wrap: normal - code - background-color: transparent - color: currentColor - font-size: $pre-code-font-size - padding: 0 - -table - td, - th - vertical-align: top - &:not([align]) - text-align: inherit - th - color: $text-strong diff --git a/shared/static/src/bulma/sass/base/helpers.sass b/shared/static/src/bulma/sass/base/helpers.sass deleted file mode 100644 index e356830f..00000000 --- a/shared/static/src/bulma/sass/base/helpers.sass +++ /dev/null @@ -1 +0,0 @@ -@warn "The helpers.sass file is DEPRECATED. It has moved into its own /helpers folder. Please import sass/helpers/_all instead." diff --git a/shared/static/src/bulma/sass/base/minireset.sass b/shared/static/src/bulma/sass/base/minireset.sass deleted file mode 100644 index aa2b6f3a..00000000 --- a/shared/static/src/bulma/sass/base/minireset.sass +++ /dev/null @@ -1,79 +0,0 @@ -/*! minireset.css v0.0.6 | MIT License | github.com/jgthms/minireset.css */ -// Blocks -html, -body, -p, -ol, -ul, -li, -dl, -dt, -dd, -blockquote, -figure, -fieldset, -legend, -textarea, -pre, -iframe, -hr, -h1, -h2, -h3, -h4, -h5, -h6 - margin: 0 - padding: 0 - -// Headings -h1, -h2, -h3, -h4, -h5, -h6 - font-size: 100% - font-weight: normal - -// List -ul - list-style: none - -// Form -button, -input, -select, -textarea - margin: 0 - -// Box sizing -html - box-sizing: border-box - -* - &, - &::before, - &::after - box-sizing: inherit - -// Media -img, -video - height: auto - max-width: 100% - -// Iframe -iframe - border: 0 - -// Table -table - border-collapse: collapse - border-spacing: 0 - -td, -th - padding: 0 - &:not([align]) - text-align: inherit diff --git a/shared/static/src/bulma/sass/components/_all.sass b/shared/static/src/bulma/sass/components/_all.sass deleted file mode 100644 index 1de2c214..00000000 --- a/shared/static/src/bulma/sass/components/_all.sass +++ /dev/null @@ -1,14 +0,0 @@ -@charset "utf-8" - -@import "breadcrumb.sass" -@import "card.sass" -@import "dropdown.sass" -@import "level.sass" -@import "media.sass" -@import "menu.sass" -@import "message.sass" -@import "modal.sass" -@import "navbar.sass" -@import "pagination.sass" -@import "panel.sass" -@import "tabs.sass" diff --git a/shared/static/src/bulma/sass/components/breadcrumb.sass b/shared/static/src/bulma/sass/components/breadcrumb.sass deleted file mode 100644 index f42b0b84..00000000 --- a/shared/static/src/bulma/sass/components/breadcrumb.sass +++ /dev/null @@ -1,75 +0,0 @@ -$breadcrumb-item-color: $link !default -$breadcrumb-item-hover-color: $link-hover !default -$breadcrumb-item-active-color: $text-strong !default - -$breadcrumb-item-padding-vertical: 0 !default -$breadcrumb-item-padding-horizontal: 0.75em !default - -$breadcrumb-item-separator-color: $border-hover !default - -.breadcrumb - @extend %block - @extend %unselectable - font-size: $size-normal - white-space: nowrap - a - align-items: center - color: $breadcrumb-item-color - display: flex - justify-content: center - padding: $breadcrumb-item-padding-vertical $breadcrumb-item-padding-horizontal - &:hover - color: $breadcrumb-item-hover-color - li - align-items: center - display: flex - &:first-child a - +ltr-property("padding", 0, false) - &.is-active - a - color: $breadcrumb-item-active-color - cursor: default - pointer-events: none - & + li::before - color: $breadcrumb-item-separator-color - content: "\0002f" - ul, - ol - align-items: flex-start - display: flex - flex-wrap: wrap - justify-content: flex-start - .icon - &:first-child - +ltr-property("margin", 0.5em) - &:last-child - +ltr-property("margin", 0.5em, false) - // Alignment - &.is-centered - ol, - ul - justify-content: center - &.is-right - ol, - ul - justify-content: flex-end - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - &.is-large - font-size: $size-large - // Styles - &.has-arrow-separator - li + li::before - content: "\02192" - &.has-bullet-separator - li + li::before - content: "\02022" - &.has-dot-separator - li + li::before - content: "\000b7" - &.has-succeeds-separator - li + li::before - content: "\0227B" diff --git a/shared/static/src/bulma/sass/components/card.sass b/shared/static/src/bulma/sass/components/card.sass deleted file mode 100644 index db1e5d9b..00000000 --- a/shared/static/src/bulma/sass/components/card.sass +++ /dev/null @@ -1,79 +0,0 @@ -$card-color: $text !default -$card-background-color: $scheme-main !default -$card-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default - -$card-header-background-color: transparent !default -$card-header-color: $text-strong !default -$card-header-padding: 0.75rem 1rem !default -$card-header-shadow: 0 0.125em 0.25em rgba($scheme-invert, 0.1) !default -$card-header-weight: $weight-bold !default - -$card-content-background-color: transparent !default -$card-content-padding: 1.5rem !default - -$card-footer-background-color: transparent !default -$card-footer-border-top: 1px solid $border-light !default -$card-footer-padding: 0.75rem !default - -$card-media-margin: $block-spacing !default - -.card - background-color: $card-background-color - box-shadow: $card-shadow - color: $card-color - max-width: 100% - position: relative - -.card-header - background-color: $card-header-background-color - align-items: stretch - box-shadow: $card-header-shadow - display: flex - -.card-header-title - align-items: center - color: $card-header-color - display: flex - flex-grow: 1 - font-weight: $card-header-weight - padding: $card-header-padding - &.is-centered - justify-content: center - -.card-header-icon - align-items: center - cursor: pointer - display: flex - justify-content: center - padding: $card-header-padding - -.card-image - display: block - position: relative - -.card-content - background-color: $card-content-background-color - padding: $card-content-padding - -.card-footer - background-color: $card-footer-background-color - border-top: $card-footer-border-top - align-items: stretch - display: flex - -.card-footer-item - align-items: center - display: flex - flex-basis: 0 - flex-grow: 1 - flex-shrink: 0 - justify-content: center - padding: $card-footer-padding - &:not(:last-child) - +ltr-property("border", $card-footer-border-top) - -// Combinations - -.card - .media:not(:last-child) - margin-bottom: $card-media-margin diff --git a/shared/static/src/bulma/sass/components/dropdown.sass b/shared/static/src/bulma/sass/components/dropdown.sass deleted file mode 100644 index 62cb66e4..00000000 --- a/shared/static/src/bulma/sass/components/dropdown.sass +++ /dev/null @@ -1,81 +0,0 @@ -$dropdown-menu-min-width: 12rem !default - -$dropdown-content-background-color: $scheme-main !default -$dropdown-content-arrow: $link !default -$dropdown-content-offset: 4px !default -$dropdown-content-padding-bottom: 0.5rem !default -$dropdown-content-padding-top: 0.5rem !default -$dropdown-content-radius: $radius !default -$dropdown-content-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default -$dropdown-content-z: 20 !default - -$dropdown-item-color: $text !default -$dropdown-item-hover-color: $scheme-invert !default -$dropdown-item-hover-background-color: $background !default -$dropdown-item-active-color: $link-invert !default -$dropdown-item-active-background-color: $link !default - -$dropdown-divider-background-color: $border-light !default - -.dropdown - display: inline-flex - position: relative - vertical-align: top - &.is-active, - &.is-hoverable:hover - .dropdown-menu - display: block - &.is-right - .dropdown-menu - left: auto - right: 0 - &.is-up - .dropdown-menu - bottom: 100% - padding-bottom: $dropdown-content-offset - padding-top: initial - top: auto - -.dropdown-menu - display: none - +ltr-position(0, false) - min-width: $dropdown-menu-min-width - padding-top: $dropdown-content-offset - position: absolute - top: 100% - z-index: $dropdown-content-z - -.dropdown-content - background-color: $dropdown-content-background-color - border-radius: $dropdown-content-radius - box-shadow: $dropdown-content-shadow - padding-bottom: $dropdown-content-padding-bottom - padding-top: $dropdown-content-padding-top - -.dropdown-item - color: $dropdown-item-color - display: block - font-size: 0.875rem - line-height: 1.5 - padding: 0.375rem 1rem - position: relative - -a.dropdown-item, -button.dropdown-item - +ltr-property("padding", 3rem) - text-align: inherit - white-space: nowrap - width: 100% - &:hover - background-color: $dropdown-item-hover-background-color - color: $dropdown-item-hover-color - &.is-active - background-color: $dropdown-item-active-background-color - color: $dropdown-item-active-color - -.dropdown-divider - background-color: $dropdown-divider-background-color - border: none - display: block - height: 1px - margin: 0.5rem 0 diff --git a/shared/static/src/bulma/sass/components/level.sass b/shared/static/src/bulma/sass/components/level.sass deleted file mode 100644 index 8f731202..00000000 --- a/shared/static/src/bulma/sass/components/level.sass +++ /dev/null @@ -1,77 +0,0 @@ -$level-item-spacing: ($block-spacing / 2) !default - -.level - @extend %block - align-items: center - justify-content: space-between - code - border-radius: $radius - img - display: inline-block - vertical-align: top - // Modifiers - &.is-mobile - display: flex - .level-left, - .level-right - display: flex - .level-left + .level-right - margin-top: 0 - .level-item - &:not(:last-child) - margin-bottom: 0 - +ltr-property("margin", $level-item-spacing) - &:not(.is-narrow) - flex-grow: 1 - // Responsiveness - +tablet - display: flex - & > .level-item - &:not(.is-narrow) - flex-grow: 1 - -.level-item - align-items: center - display: flex - flex-basis: auto - flex-grow: 0 - flex-shrink: 0 - justify-content: center - .title, - .subtitle - margin-bottom: 0 - // Responsiveness - +mobile - &:not(:last-child) - margin-bottom: $level-item-spacing - -.level-left, -.level-right - flex-basis: auto - flex-grow: 0 - flex-shrink: 0 - .level-item - // Modifiers - &.is-flexible - flex-grow: 1 - // Responsiveness - +tablet - &:not(:last-child) - +ltr-property("margin", $level-item-spacing) - -.level-left - align-items: center - justify-content: flex-start - // Responsiveness - +mobile - & + .level-right - margin-top: 1.5rem - +tablet - display: flex - -.level-right - align-items: center - justify-content: flex-end - // Responsiveness - +tablet - display: flex diff --git a/shared/static/src/bulma/sass/components/media.sass b/shared/static/src/bulma/sass/components/media.sass deleted file mode 100644 index 777755b2..00000000 --- a/shared/static/src/bulma/sass/components/media.sass +++ /dev/null @@ -1,52 +0,0 @@ -$media-border-color: bulmaRgba($border, 0.5) !default -$media-spacing: 1rem -$media-spacing-large: 1.5rem - -.media - align-items: flex-start - display: flex - text-align: inherit - .content:not(:last-child) - margin-bottom: 0.75rem - .media - border-top: 1px solid $media-border-color - display: flex - padding-top: 0.75rem - .content:not(:last-child), - .control:not(:last-child) - margin-bottom: 0.5rem - .media - padding-top: 0.5rem - & + .media - margin-top: 0.5rem - & + .media - border-top: 1px solid $media-border-color - margin-top: $media-spacing - padding-top: $media-spacing - // Sizes - &.is-large - & + .media - margin-top: $media-spacing-large - padding-top: $media-spacing-large - -.media-left, -.media-right - flex-basis: auto - flex-grow: 0 - flex-shrink: 0 - -.media-left - +ltr-property("margin", $media-spacing) - -.media-right - +ltr-property("margin", $media-spacing, false) - -.media-content - flex-basis: auto - flex-grow: 1 - flex-shrink: 1 - text-align: inherit - -+mobile - .media-content - overflow-x: auto diff --git a/shared/static/src/bulma/sass/components/menu.sass b/shared/static/src/bulma/sass/components/menu.sass deleted file mode 100644 index 1bf78297..00000000 --- a/shared/static/src/bulma/sass/components/menu.sass +++ /dev/null @@ -1,57 +0,0 @@ -$menu-item-color: $text !default -$menu-item-radius: $radius-small !default -$menu-item-hover-color: $text-strong !default -$menu-item-hover-background-color: $background !default -$menu-item-active-color: $link-invert !default -$menu-item-active-background-color: $link !default - -$menu-list-border-left: 1px solid $border !default -$menu-list-line-height: 1.25 !default -$menu-list-link-padding: 0.5em 0.75em !default -$menu-nested-list-margin: 0.75em !default -$menu-nested-list-padding-left: 0.75em !default - -$menu-label-color: $text-light !default -$menu-label-font-size: 0.75em !default -$menu-label-letter-spacing: 0.1em !default -$menu-label-spacing: 1em !default - -.menu - font-size: $size-normal - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - &.is-large - font-size: $size-large - -.menu-list - line-height: $menu-list-line-height - a - border-radius: $menu-item-radius - color: $menu-item-color - display: block - padding: $menu-list-link-padding - &:hover - background-color: $menu-item-hover-background-color - color: $menu-item-hover-color - // Modifiers - &.is-active - background-color: $menu-item-active-background-color - color: $menu-item-active-color - li - ul - +ltr-property("border", $menu-list-border-left, false) - margin: $menu-nested-list-margin - +ltr-property("padding", $menu-nested-list-padding-left, false) - -.menu-label - color: $menu-label-color - font-size: $menu-label-font-size - letter-spacing: $menu-label-letter-spacing - text-transform: uppercase - &:not(:first-child) - margin-top: $menu-label-spacing - &:not(:last-child) - margin-bottom: $menu-label-spacing diff --git a/shared/static/src/bulma/sass/components/message.sass b/shared/static/src/bulma/sass/components/message.sass deleted file mode 100644 index 180fbe94..00000000 --- a/shared/static/src/bulma/sass/components/message.sass +++ /dev/null @@ -1,99 +0,0 @@ -$message-background-color: $background !default -$message-radius: $radius !default - -$message-header-background-color: $text !default -$message-header-color: $text-invert !default -$message-header-weight: $weight-bold !default -$message-header-padding: 0.75em 1em !default -$message-header-radius: $radius !default - -$message-body-border-color: $border !default -$message-body-border-width: 0 0 0 4px !default -$message-body-color: $text !default -$message-body-padding: 1.25em 1.5em !default -$message-body-radius: $radius !default - -$message-body-pre-background-color: $scheme-main !default -$message-body-pre-code-background-color: transparent !default - -$message-header-body-border-width: 0 !default -$message-colors: $colors !default - -.message - @extend %block - background-color: $message-background-color - border-radius: $message-radius - font-size: $size-normal - strong - color: currentColor - a:not(.button):not(.tag):not(.dropdown-item) - color: currentColor - text-decoration: underline - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - &.is-large - font-size: $size-large - // Colors - @each $name, $components in $message-colors - $color: nth($components, 1) - $color-invert: nth($components, 2) - $color-light: null - $color-dark: null - - @if length($components) >= 3 - $color-light: nth($components, 3) - @if length($components) >= 4 - $color-dark: nth($components, 4) - @else - $color-luminance: colorLuminance($color) - $darken-percentage: $color-luminance * 70% - $desaturate-percentage: $color-luminance * 30% - $color-dark: desaturate(darken($color, $darken-percentage), $desaturate-percentage) - @else - $color-lightning: max((100% - lightness($color)) - 2%, 0%) - $color-light: lighten($color, $color-lightning) - - &.is-#{$name} - background-color: $color-light - .message-header - background-color: $color - color: $color-invert - .message-body - border-color: $color - color: $color-dark - -.message-header - align-items: center - background-color: $message-header-background-color - border-radius: $message-header-radius $message-header-radius 0 0 - color: $message-header-color - display: flex - font-weight: $message-header-weight - justify-content: space-between - line-height: 1.25 - padding: $message-header-padding - position: relative - .delete - flex-grow: 0 - flex-shrink: 0 - +ltr-property("margin", 0.75em, false) - & + .message-body - border-width: $message-header-body-border-width - border-top-left-radius: 0 - border-top-right-radius: 0 - -.message-body - border-color: $message-body-border-color - border-radius: $message-body-radius - border-style: solid - border-width: $message-body-border-width - color: $message-body-color - padding: $message-body-padding - code, - pre - background-color: $message-body-pre-background-color - pre code - background-color: $message-body-pre-code-background-color diff --git a/shared/static/src/bulma/sass/components/modal.sass b/shared/static/src/bulma/sass/components/modal.sass deleted file mode 100644 index f352744a..00000000 --- a/shared/static/src/bulma/sass/components/modal.sass +++ /dev/null @@ -1,113 +0,0 @@ -$modal-z: 40 !default - -$modal-background-background-color: bulmaRgba($scheme-invert, 0.86) !default - -$modal-content-width: 640px !default -$modal-content-margin-mobile: 20px !default -$modal-content-spacing-mobile: 160px !default -$modal-content-spacing-tablet: 40px !default - -$modal-close-dimensions: 40px !default -$modal-close-right: 20px !default -$modal-close-top: 20px !default - -$modal-card-spacing: 40px !default - -$modal-card-head-background-color: $background !default -$modal-card-head-border-bottom: 1px solid $border !default -$modal-card-head-padding: 20px !default -$modal-card-head-radius: $radius-large !default - -$modal-card-title-color: $text-strong !default -$modal-card-title-line-height: 1 !default -$modal-card-title-size: $size-4 !default - -$modal-card-foot-radius: $radius-large !default -$modal-card-foot-border-top: 1px solid $border !default - -$modal-card-body-background-color: $scheme-main !default -$modal-card-body-padding: 20px !default - -.modal - @extend %overlay - align-items: center - display: none - flex-direction: column - justify-content: center - overflow: hidden - position: fixed - z-index: $modal-z - // Modifiers - &.is-active - display: flex - -.modal-background - @extend %overlay - background-color: $modal-background-background-color - -.modal-content, -.modal-card - margin: 0 $modal-content-margin-mobile - max-height: calc(100vh - #{$modal-content-spacing-mobile}) - overflow: auto - position: relative - width: 100% - // Responsiveness - +tablet - margin: 0 auto - max-height: calc(100vh - #{$modal-content-spacing-tablet}) - width: $modal-content-width - -.modal-close - @extend %delete - background: none - height: $modal-close-dimensions - position: fixed - +ltr-position($modal-close-right) - top: $modal-close-top - width: $modal-close-dimensions - -.modal-card - display: flex - flex-direction: column - max-height: calc(100vh - #{$modal-card-spacing}) - overflow: hidden - -ms-overflow-y: visible - -.modal-card-head, -.modal-card-foot - align-items: center - background-color: $modal-card-head-background-color - display: flex - flex-shrink: 0 - justify-content: flex-start - padding: $modal-card-head-padding - position: relative - -.modal-card-head - border-bottom: $modal-card-head-border-bottom - border-top-left-radius: $modal-card-head-radius - border-top-right-radius: $modal-card-head-radius - -.modal-card-title - color: $modal-card-title-color - flex-grow: 1 - flex-shrink: 0 - font-size: $modal-card-title-size - line-height: $modal-card-title-line-height - -.modal-card-foot - border-bottom-left-radius: $modal-card-foot-radius - border-bottom-right-radius: $modal-card-foot-radius - border-top: $modal-card-foot-border-top - .button - &:not(:last-child) - +ltr-property("margin", 0.5em) - -.modal-card-body - +overflow-touch - background-color: $modal-card-body-background-color - flex-grow: 1 - flex-shrink: 1 - overflow: auto - padding: $modal-card-body-padding diff --git a/shared/static/src/bulma/sass/components/navbar.sass b/shared/static/src/bulma/sass/components/navbar.sass deleted file mode 100644 index a34718ec..00000000 --- a/shared/static/src/bulma/sass/components/navbar.sass +++ /dev/null @@ -1,441 +0,0 @@ -$navbar-background-color: $scheme-main !default -$navbar-box-shadow-size: 0 2px 0 0 !default -$navbar-box-shadow-color: $background !default -$navbar-height: 3.25rem !default -$navbar-padding-vertical: 1rem !default -$navbar-padding-horizontal: 2rem !default -$navbar-z: 30 !default -$navbar-fixed-z: 30 !default - -$navbar-item-color: $text !default -$navbar-item-hover-color: $link !default -$navbar-item-hover-background-color: $scheme-main-bis !default -$navbar-item-active-color: $scheme-invert !default -$navbar-item-active-background-color: transparent !default -$navbar-item-img-max-height: 1.75rem !default - -$navbar-burger-color: $navbar-item-color !default - -$navbar-tab-hover-background-color: transparent !default -$navbar-tab-hover-border-bottom-color: $link !default -$navbar-tab-active-color: $link !default -$navbar-tab-active-background-color: transparent !default -$navbar-tab-active-border-bottom-color: $link !default -$navbar-tab-active-border-bottom-style: solid !default -$navbar-tab-active-border-bottom-width: 3px !default - -$navbar-dropdown-background-color: $scheme-main !default -$navbar-dropdown-border-top: 2px solid $border !default -$navbar-dropdown-offset: -4px !default -$navbar-dropdown-arrow: $link !default -$navbar-dropdown-radius: $radius-large !default -$navbar-dropdown-z: 20 !default - -$navbar-dropdown-boxed-radius: $radius-large !default -$navbar-dropdown-boxed-shadow: 0 8px 8px bulmaRgba($scheme-invert, 0.1), 0 0 0 1px bulmaRgba($scheme-invert, 0.1) !default - -$navbar-dropdown-item-hover-color: $scheme-invert !default -$navbar-dropdown-item-hover-background-color: $background !default -$navbar-dropdown-item-active-color: $link !default -$navbar-dropdown-item-active-background-color: $background !default - -$navbar-divider-background-color: $background !default -$navbar-divider-height: 2px !default - -$navbar-bottom-box-shadow-size: 0 -2px 0 0 !default - -$navbar-breakpoint: $desktop !default - -=navbar-fixed - left: 0 - position: fixed - right: 0 - z-index: $navbar-fixed-z - -.navbar - background-color: $navbar-background-color - min-height: $navbar-height - position: relative - z-index: $navbar-z - @each $name, $pair in $colors - $color: nth($pair, 1) - $color-invert: nth($pair, 2) - &.is-#{$name} - background-color: $color - color: $color-invert - .navbar-brand - & > .navbar-item, - .navbar-link - color: $color-invert - & > a.navbar-item, - .navbar-link - &:focus, - &:hover, - &.is-active - background-color: bulmaDarken($color, 5%) - color: $color-invert - .navbar-link - &::after - border-color: $color-invert - .navbar-burger - color: $color-invert - +from($navbar-breakpoint) - .navbar-start, - .navbar-end - & > .navbar-item, - .navbar-link - color: $color-invert - & > a.navbar-item, - .navbar-link - &:focus, - &:hover, - &.is-active - background-color: bulmaDarken($color, 5%) - color: $color-invert - .navbar-link - &::after - border-color: $color-invert - .navbar-item.has-dropdown:focus .navbar-link, - .navbar-item.has-dropdown:hover .navbar-link, - .navbar-item.has-dropdown.is-active .navbar-link - background-color: bulmaDarken($color, 5%) - color: $color-invert - .navbar-dropdown - a.navbar-item - &.is-active - background-color: $color - color: $color-invert - & > .container - align-items: stretch - display: flex - min-height: $navbar-height - width: 100% - &.has-shadow - box-shadow: $navbar-box-shadow-size $navbar-box-shadow-color - &.is-fixed-bottom, - &.is-fixed-top - +navbar-fixed - &.is-fixed-bottom - bottom: 0 - &.has-shadow - box-shadow: $navbar-bottom-box-shadow-size $navbar-box-shadow-color - &.is-fixed-top - top: 0 - -html, -body - &.has-navbar-fixed-top - padding-top: $navbar-height - &.has-navbar-fixed-bottom - padding-bottom: $navbar-height - -.navbar-brand, -.navbar-tabs - align-items: stretch - display: flex - flex-shrink: 0 - min-height: $navbar-height - -.navbar-brand - a.navbar-item - &:focus, - &:hover - background-color: transparent - -.navbar-tabs - +overflow-touch - max-width: 100vw - overflow-x: auto - overflow-y: hidden - -.navbar-burger - color: $navbar-burger-color - +hamburger($navbar-height) - +ltr-property("margin", auto, false) - -.navbar-menu - display: none - -.navbar-item, -.navbar-link - color: $navbar-item-color - display: block - line-height: 1.5 - padding: 0.5rem 0.75rem - position: relative - .icon - &:only-child - margin-left: -0.25rem - margin-right: -0.25rem - -a.navbar-item, -.navbar-link - cursor: pointer - &:focus, - &:focus-within, - &:hover, - &.is-active - background-color: $navbar-item-hover-background-color - color: $navbar-item-hover-color - -.navbar-item - flex-grow: 0 - flex-shrink: 0 - img - max-height: $navbar-item-img-max-height - &.has-dropdown - padding: 0 - &.is-expanded - flex-grow: 1 - flex-shrink: 1 - &.is-tab - border-bottom: 1px solid transparent - min-height: $navbar-height - padding-bottom: calc(0.5rem - 1px) - &:focus, - &:hover - background-color: $navbar-tab-hover-background-color - border-bottom-color: $navbar-tab-hover-border-bottom-color - &.is-active - background-color: $navbar-tab-active-background-color - border-bottom-color: $navbar-tab-active-border-bottom-color - border-bottom-style: $navbar-tab-active-border-bottom-style - border-bottom-width: $navbar-tab-active-border-bottom-width - color: $navbar-tab-active-color - padding-bottom: calc(0.5rem - #{$navbar-tab-active-border-bottom-width}) - -.navbar-content - flex-grow: 1 - flex-shrink: 1 - -.navbar-link:not(.is-arrowless) - +ltr-property("padding", 2.5em) - &::after - @extend %arrow - border-color: $navbar-dropdown-arrow - margin-top: -0.375em - +ltr-position(1.125em) - -.navbar-dropdown - font-size: 0.875rem - padding-bottom: 0.5rem - padding-top: 0.5rem - .navbar-item - padding-left: 1.5rem - padding-right: 1.5rem - -.navbar-divider - background-color: $navbar-divider-background-color - border: none - display: none - height: $navbar-divider-height - margin: 0.5rem 0 - -+until($navbar-breakpoint) - .navbar > .container - display: block - .navbar-brand, - .navbar-tabs - .navbar-item - align-items: center - display: flex - .navbar-link - &::after - display: none - .navbar-menu - background-color: $navbar-background-color - box-shadow: 0 8px 16px bulmaRgba($scheme-invert, 0.1) - padding: 0.5rem 0 - &.is-active - display: block - // Fixed navbar - .navbar - &.is-fixed-bottom-touch, - &.is-fixed-top-touch - +navbar-fixed - &.is-fixed-bottom-touch - bottom: 0 - &.has-shadow - box-shadow: 0 -2px 3px bulmaRgba($scheme-invert, 0.1) - &.is-fixed-top-touch - top: 0 - &.is-fixed-top, - &.is-fixed-top-touch - .navbar-menu - +overflow-touch - max-height: calc(100vh - #{$navbar-height}) - overflow: auto - html, - body - &.has-navbar-fixed-top-touch - padding-top: $navbar-height - &.has-navbar-fixed-bottom-touch - padding-bottom: $navbar-height - -+from($navbar-breakpoint) - .navbar, - .navbar-menu, - .navbar-start, - .navbar-end - align-items: stretch - display: flex - .navbar - min-height: $navbar-height - &.is-spaced - padding: $navbar-padding-vertical $navbar-padding-horizontal - .navbar-start, - .navbar-end - align-items: center - a.navbar-item, - .navbar-link - border-radius: $radius - &.is-transparent - a.navbar-item, - .navbar-link - &:focus, - &:hover, - &.is-active - background-color: transparent !important - .navbar-item.has-dropdown - &.is-active, - &.is-hoverable:focus, - &.is-hoverable:focus-within, - &.is-hoverable:hover - .navbar-link - background-color: transparent !important - .navbar-dropdown - a.navbar-item - &:focus, - &:hover - background-color: $navbar-dropdown-item-hover-background-color - color: $navbar-dropdown-item-hover-color - &.is-active - background-color: $navbar-dropdown-item-active-background-color - color: $navbar-dropdown-item-active-color - .navbar-burger - display: none - .navbar-item, - .navbar-link - align-items: center - display: flex - .navbar-item - &.has-dropdown - align-items: stretch - &.has-dropdown-up - .navbar-link::after - transform: rotate(135deg) translate(0.25em, -0.25em) - .navbar-dropdown - border-bottom: $navbar-dropdown-border-top - border-radius: $navbar-dropdown-radius $navbar-dropdown-radius 0 0 - border-top: none - bottom: 100% - box-shadow: 0 -8px 8px bulmaRgba($scheme-invert, 0.1) - top: auto - &.is-active, - &.is-hoverable:focus, - &.is-hoverable:focus-within, - &.is-hoverable:hover - .navbar-dropdown - display: block - .navbar.is-spaced &, - &.is-boxed - opacity: 1 - pointer-events: auto - transform: translateY(0) - .navbar-menu - flex-grow: 1 - flex-shrink: 0 - .navbar-start - justify-content: flex-start - +ltr-property("margin", auto) - .navbar-end - justify-content: flex-end - +ltr-property("margin", auto, false) - .navbar-dropdown - background-color: $navbar-dropdown-background-color - border-bottom-left-radius: $navbar-dropdown-radius - border-bottom-right-radius: $navbar-dropdown-radius - border-top: $navbar-dropdown-border-top - box-shadow: 0 8px 8px bulmaRgba($scheme-invert, 0.1) - display: none - font-size: 0.875rem - +ltr-position(0, false) - min-width: 100% - position: absolute - top: 100% - z-index: $navbar-dropdown-z - .navbar-item - padding: 0.375rem 1rem - white-space: nowrap - a.navbar-item - +ltr-property("padding", 3rem) - &:focus, - &:hover - background-color: $navbar-dropdown-item-hover-background-color - color: $navbar-dropdown-item-hover-color - &.is-active - background-color: $navbar-dropdown-item-active-background-color - color: $navbar-dropdown-item-active-color - .navbar.is-spaced &, - &.is-boxed - border-radius: $navbar-dropdown-boxed-radius - border-top: none - box-shadow: $navbar-dropdown-boxed-shadow - display: block - opacity: 0 - pointer-events: none - top: calc(100% + (#{$navbar-dropdown-offset})) - transform: translateY(-5px) - transition-duration: $speed - transition-property: opacity, transform - &.is-right - left: auto - right: 0 - .navbar-divider - display: block - .navbar > .container, - .container > .navbar - .navbar-brand - +ltr-property("margin", -.75rem, false) - .navbar-menu - +ltr-property("margin", -.75rem) - // Fixed navbar - .navbar - &.is-fixed-bottom-desktop, - &.is-fixed-top-desktop - +navbar-fixed - &.is-fixed-bottom-desktop - bottom: 0 - &.has-shadow - box-shadow: 0 -2px 3px bulmaRgba($scheme-invert, 0.1) - &.is-fixed-top-desktop - top: 0 - html, - body - &.has-navbar-fixed-top-desktop - padding-top: $navbar-height - &.has-navbar-fixed-bottom-desktop - padding-bottom: $navbar-height - &.has-spaced-navbar-fixed-top - padding-top: $navbar-height + ($navbar-padding-vertical * 2) - &.has-spaced-navbar-fixed-bottom - padding-bottom: $navbar-height + ($navbar-padding-vertical * 2) - // Hover/Active states - a.navbar-item, - .navbar-link - &.is-active - color: $navbar-item-active-color - &.is-active:not(:focus):not(:hover) - background-color: $navbar-item-active-background-color - .navbar-item.has-dropdown - &:focus, - &:hover, - &.is-active - .navbar-link - background-color: $navbar-item-hover-background-color - -// Combination - -.hero - &.is-fullheight-with-navbar - min-height: calc(100vh - #{$navbar-height}) diff --git a/shared/static/src/bulma/sass/components/pagination.sass b/shared/static/src/bulma/sass/components/pagination.sass deleted file mode 100644 index 822c2e81..00000000 --- a/shared/static/src/bulma/sass/components/pagination.sass +++ /dev/null @@ -1,150 +0,0 @@ -$pagination-color: $text-strong !default -$pagination-border-color: $border !default -$pagination-margin: -0.25rem !default -$pagination-min-width: $control-height !default - -$pagination-item-font-size: 1em !default -$pagination-item-margin: 0.25rem !default -$pagination-item-padding-left: 0.5em !default -$pagination-item-padding-right: 0.5em !default - -$pagination-hover-color: $link-hover !default -$pagination-hover-border-color: $link-hover-border !default - -$pagination-focus-color: $link-focus !default -$pagination-focus-border-color: $link-focus-border !default - -$pagination-active-color: $link-active !default -$pagination-active-border-color: $link-active-border !default - -$pagination-disabled-color: $text-light !default -$pagination-disabled-background-color: $border !default -$pagination-disabled-border-color: $border !default - -$pagination-current-color: $link-invert !default -$pagination-current-background-color: $link !default -$pagination-current-border-color: $link !default - -$pagination-ellipsis-color: $grey-light !default - -$pagination-shadow-inset: inset 0 1px 2px rgba($scheme-invert, 0.2) - -.pagination - @extend %block - font-size: $size-normal - margin: $pagination-margin - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - &.is-large - font-size: $size-large - &.is-rounded - .pagination-previous, - .pagination-next - padding-left: 1em - padding-right: 1em - border-radius: $radius-rounded - .pagination-link - border-radius: $radius-rounded - -.pagination, -.pagination-list - align-items: center - display: flex - justify-content: center - text-align: center - -.pagination-previous, -.pagination-next, -.pagination-link, -.pagination-ellipsis - @extend %control - @extend %unselectable - font-size: $pagination-item-font-size - justify-content: center - margin: $pagination-item-margin - padding-left: $pagination-item-padding-left - padding-right: $pagination-item-padding-right - text-align: center - -.pagination-previous, -.pagination-next, -.pagination-link - border-color: $pagination-border-color - color: $pagination-color - min-width: $pagination-min-width - &:hover - border-color: $pagination-hover-border-color - color: $pagination-hover-color - &:focus - border-color: $pagination-focus-border-color - &:active - box-shadow: $pagination-shadow-inset - &[disabled] - background-color: $pagination-disabled-background-color - border-color: $pagination-disabled-border-color - box-shadow: none - color: $pagination-disabled-color - opacity: 0.5 - -.pagination-previous, -.pagination-next - padding-left: 0.75em - padding-right: 0.75em - white-space: nowrap - -.pagination-link - &.is-current - background-color: $pagination-current-background-color - border-color: $pagination-current-border-color - color: $pagination-current-color - -.pagination-ellipsis - color: $pagination-ellipsis-color - pointer-events: none - -.pagination-list - flex-wrap: wrap - -+mobile - .pagination - flex-wrap: wrap - .pagination-previous, - .pagination-next - flex-grow: 1 - flex-shrink: 1 - .pagination-list - li - flex-grow: 1 - flex-shrink: 1 - -+tablet - .pagination-list - flex-grow: 1 - flex-shrink: 1 - justify-content: flex-start - order: 1 - .pagination-previous - order: 2 - .pagination-next - order: 3 - .pagination - justify-content: space-between - &.is-centered - .pagination-previous - order: 1 - .pagination-list - justify-content: center - order: 2 - .pagination-next - order: 3 - &.is-right - .pagination-previous - order: 1 - .pagination-next - order: 2 - .pagination-list - justify-content: flex-end - order: 3 diff --git a/shared/static/src/bulma/sass/components/panel.sass b/shared/static/src/bulma/sass/components/panel.sass deleted file mode 100644 index 2f7e2754..00000000 --- a/shared/static/src/bulma/sass/components/panel.sass +++ /dev/null @@ -1,119 +0,0 @@ -$panel-margin: $block-spacing !default -$panel-item-border: 1px solid $border-light !default -$panel-radius: $radius-large !default -$panel-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default - -$panel-heading-background-color: $border-light !default -$panel-heading-color: $text-strong !default -$panel-heading-line-height: 1.25 !default -$panel-heading-padding: 0.75em 1em !default -$panel-heading-radius: $radius !default -$panel-heading-size: 1.25em !default -$panel-heading-weight: $weight-bold !default - -$panel-tabs-font-size: 0.875em !default -$panel-tab-border-bottom: 1px solid $border !default -$panel-tab-active-border-bottom-color: $link-active-border !default -$panel-tab-active-color: $link-active !default - -$panel-list-item-color: $text !default -$panel-list-item-hover-color: $link !default - -$panel-block-color: $text-strong !default -$panel-block-hover-background-color: $background !default -$panel-block-active-border-left-color: $link !default -$panel-block-active-color: $link-active !default -$panel-block-active-icon-color: $link !default - -$panel-icon-color: $text-light !default -$panel-colors: $colors !default - -.panel - border-radius: $panel-radius - box-shadow: $panel-shadow - font-size: $size-normal - &:not(:last-child) - margin-bottom: $panel-margin - // Colors - @each $name, $components in $panel-colors - $color: nth($components, 1) - $color-invert: nth($components, 2) - &.is-#{$name} - .panel-heading - background-color: $color - color: $color-invert - .panel-tabs a.is-active - border-bottom-color: $color - .panel-block.is-active .panel-icon - color: $color - -.panel-tabs, -.panel-block - &:not(:last-child) - border-bottom: $panel-item-border - -.panel-heading - background-color: $panel-heading-background-color - border-radius: $panel-radius $panel-radius 0 0 - color: $panel-heading-color - font-size: $panel-heading-size - font-weight: $panel-heading-weight - line-height: $panel-heading-line-height - padding: $panel-heading-padding - -.panel-tabs - align-items: flex-end - display: flex - font-size: $panel-tabs-font-size - justify-content: center - a - border-bottom: $panel-tab-border-bottom - margin-bottom: -1px - padding: 0.5em - // Modifiers - &.is-active - border-bottom-color: $panel-tab-active-border-bottom-color - color: $panel-tab-active-color - -.panel-list - a - color: $panel-list-item-color - &:hover - color: $panel-list-item-hover-color - -.panel-block - align-items: center - color: $panel-block-color - display: flex - justify-content: flex-start - padding: 0.5em 0.75em - input[type="checkbox"] - +ltr-property("margin", 0.75em) - & > .control - flex-grow: 1 - flex-shrink: 1 - width: 100% - &.is-wrapped - flex-wrap: wrap - &.is-active - border-left-color: $panel-block-active-border-left-color - color: $panel-block-active-color - .panel-icon - color: $panel-block-active-icon-color - &:last-child - border-bottom-left-radius: $panel-radius - border-bottom-right-radius: $panel-radius - -a.panel-block, -label.panel-block - cursor: pointer - &:hover - background-color: $panel-block-hover-background-color - -.panel-icon - +fa(14px, 1em) - color: $panel-icon-color - +ltr-property("margin", 0.75em) - .fa - font-size: inherit - line-height: inherit diff --git a/shared/static/src/bulma/sass/components/tabs.sass b/shared/static/src/bulma/sass/components/tabs.sass deleted file mode 100644 index 2308bf09..00000000 --- a/shared/static/src/bulma/sass/components/tabs.sass +++ /dev/null @@ -1,174 +0,0 @@ -$tabs-border-bottom-color: $border !default -$tabs-border-bottom-style: solid !default -$tabs-border-bottom-width: 1px !default -$tabs-link-color: $text !default -$tabs-link-hover-border-bottom-color: $text-strong !default -$tabs-link-hover-color: $text-strong !default -$tabs-link-active-border-bottom-color: $link !default -$tabs-link-active-color: $link !default -$tabs-link-padding: 0.5em 1em !default - -$tabs-boxed-link-radius: $radius !default -$tabs-boxed-link-hover-background-color: $background !default -$tabs-boxed-link-hover-border-bottom-color: $border !default - -$tabs-boxed-link-active-background-color: $scheme-main !default -$tabs-boxed-link-active-border-color: $border !default -$tabs-boxed-link-active-border-bottom-color: transparent !default - -$tabs-toggle-link-border-color: $border !default -$tabs-toggle-link-border-style: solid !default -$tabs-toggle-link-border-width: 1px !default -$tabs-toggle-link-hover-background-color: $background !default -$tabs-toggle-link-hover-border-color: $border-hover !default -$tabs-toggle-link-radius: $radius !default -$tabs-toggle-link-active-background-color: $link !default -$tabs-toggle-link-active-border-color: $link !default -$tabs-toggle-link-active-color: $link-invert !default - -.tabs - @extend %block - +overflow-touch - @extend %unselectable - align-items: stretch - display: flex - font-size: $size-normal - justify-content: space-between - overflow: hidden - overflow-x: auto - white-space: nowrap - a - align-items: center - border-bottom-color: $tabs-border-bottom-color - border-bottom-style: $tabs-border-bottom-style - border-bottom-width: $tabs-border-bottom-width - color: $tabs-link-color - display: flex - justify-content: center - margin-bottom: -#{$tabs-border-bottom-width} - padding: $tabs-link-padding - vertical-align: top - &:hover - border-bottom-color: $tabs-link-hover-border-bottom-color - color: $tabs-link-hover-color - li - display: block - &.is-active - a - border-bottom-color: $tabs-link-active-border-bottom-color - color: $tabs-link-active-color - ul - align-items: center - border-bottom-color: $tabs-border-bottom-color - border-bottom-style: $tabs-border-bottom-style - border-bottom-width: $tabs-border-bottom-width - display: flex - flex-grow: 1 - flex-shrink: 0 - justify-content: flex-start - &.is-left - padding-right: 0.75em - &.is-center - flex: none - justify-content: center - padding-left: 0.75em - padding-right: 0.75em - &.is-right - justify-content: flex-end - padding-left: 0.75em - .icon - &:first-child - +ltr-property("margin", 0.5em) - &:last-child - +ltr-property("margin", 0.5em, false) - // Alignment - &.is-centered - ul - justify-content: center - &.is-right - ul - justify-content: flex-end - // Styles - &.is-boxed - a - border: 1px solid transparent - +ltr - border-radius: $tabs-boxed-link-radius $tabs-boxed-link-radius 0 0 - +rtl - border-radius: 0 0 $tabs-boxed-link-radius $tabs-boxed-link-radius - &:hover - background-color: $tabs-boxed-link-hover-background-color - border-bottom-color: $tabs-boxed-link-hover-border-bottom-color - li - &.is-active - a - background-color: $tabs-boxed-link-active-background-color - border-color: $tabs-boxed-link-active-border-color - border-bottom-color: $tabs-boxed-link-active-border-bottom-color !important - &.is-fullwidth - li - flex-grow: 1 - flex-shrink: 0 - &.is-toggle - a - border-color: $tabs-toggle-link-border-color - border-style: $tabs-toggle-link-border-style - border-width: $tabs-toggle-link-border-width - margin-bottom: 0 - position: relative - &:hover - background-color: $tabs-toggle-link-hover-background-color - border-color: $tabs-toggle-link-hover-border-color - z-index: 2 - li - & + li - +ltr-property("margin", -#{$tabs-toggle-link-border-width}, false) - &:first-child a - +ltr - border-top-left-radius: $tabs-toggle-link-radius - border-bottom-left-radius: $tabs-toggle-link-radius - +rtl - border-top-right-radius: $tabs-toggle-link-radius - border-bottom-right-radius: $tabs-toggle-link-radius - &:last-child a - +ltr - border-top-right-radius: $tabs-toggle-link-radius - border-bottom-right-radius: $tabs-toggle-link-radius - +rtl - border-top-left-radius: $tabs-toggle-link-radius - border-bottom-left-radius: $tabs-toggle-link-radius - &.is-active - a - background-color: $tabs-toggle-link-active-background-color - border-color: $tabs-toggle-link-active-border-color - color: $tabs-toggle-link-active-color - z-index: 1 - ul - border-bottom: none - &.is-toggle-rounded - li - &:first-child a - +ltr - border-bottom-left-radius: $radius-rounded - border-top-left-radius: $radius-rounded - padding-left: 1.25em - +rtl - border-bottom-right-radius: $radius-rounded - border-top-right-radius: $radius-rounded - padding-right: 1.25em - &:last-child a - +ltr - border-bottom-right-radius: $radius-rounded - border-top-right-radius: $radius-rounded - padding-right: 1.25em - +rtl - border-bottom-left-radius: $radius-rounded - border-top-left-radius: $radius-rounded - padding-left: 1.25em - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - &.is-large - font-size: $size-large diff --git a/shared/static/src/bulma/sass/elements/_all.sass b/shared/static/src/bulma/sass/elements/_all.sass deleted file mode 100644 index 7490c00d..00000000 --- a/shared/static/src/bulma/sass/elements/_all.sass +++ /dev/null @@ -1,15 +0,0 @@ -@charset "utf-8" - -@import "box.sass" -@import "button.sass" -@import "container.sass" -@import "content.sass" -@import "icon.sass" -@import "image.sass" -@import "notification.sass" -@import "progress.sass" -@import "table.sass" -@import "tag.sass" -@import "title.sass" - -@import "other.sass" diff --git a/shared/static/src/bulma/sass/elements/box.sass b/shared/static/src/bulma/sass/elements/box.sass deleted file mode 100644 index 2fd18d49..00000000 --- a/shared/static/src/bulma/sass/elements/box.sass +++ /dev/null @@ -1,24 +0,0 @@ -$box-color: $text !default -$box-background-color: $scheme-main !default -$box-radius: $radius-large !default -$box-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0px 0 1px rgba($scheme-invert, 0.02) !default -$box-padding: 1.25rem !default - -$box-link-hover-shadow: 0 0.5em 1em -0.125em rgba($scheme-invert, 0.1), 0 0 0 1px $link !default -$box-link-active-shadow: inset 0 1px 2px rgba($scheme-invert, 0.2), 0 0 0 1px $link !default - -.box - @extend %block - background-color: $box-background-color - border-radius: $box-radius - box-shadow: $box-shadow - color: $box-color - display: block - padding: $box-padding - -a.box - &:hover, - &:focus - box-shadow: $box-link-hover-shadow - &:active - box-shadow: $box-link-active-shadow diff --git a/shared/static/src/bulma/sass/elements/button.sass b/shared/static/src/bulma/sass/elements/button.sass deleted file mode 100644 index 4bdf2534..00000000 --- a/shared/static/src/bulma/sass/elements/button.sass +++ /dev/null @@ -1,323 +0,0 @@ -$button-color: $text-strong !default -$button-background-color: $scheme-main !default -$button-family: false !default - -$button-border-color: $border !default -$button-border-width: $control-border-width !default - -$button-padding-vertical: calc(0.5em - #{$button-border-width}) !default -$button-padding-horizontal: 1em !default - -$button-hover-color: $link-hover !default -$button-hover-border-color: $link-hover-border !default - -$button-focus-color: $link-focus !default -$button-focus-border-color: $link-focus-border !default -$button-focus-box-shadow-size: 0 0 0 0.125em !default -$button-focus-box-shadow-color: bulmaRgba($link, 0.25) !default - -$button-active-color: $link-active !default -$button-active-border-color: $link-active-border !default - -$button-text-color: $text !default -$button-text-decoration: underline !default -$button-text-hover-background-color: $background !default -$button-text-hover-color: $text-strong !default - -$button-disabled-background-color: $scheme-main !default -$button-disabled-border-color: $border !default -$button-disabled-shadow: none !default -$button-disabled-opacity: 0.5 !default - -$button-static-color: $text-light !default -$button-static-background-color: $scheme-main-ter !default -$button-static-border-color: $border !default - -// The button sizes use mixins so they can be used at different breakpoints -=button-small - border-radius: $radius-small - font-size: $size-small -=button-normal - font-size: $size-normal -=button-medium - font-size: $size-medium -=button-large - font-size: $size-large - -.button - @extend %control - @extend %unselectable - background-color: $button-background-color - border-color: $button-border-color - border-width: $button-border-width - color: $button-color - cursor: pointer - @if $button-family - font-family: $button-family - justify-content: center - padding-bottom: $button-padding-vertical - padding-left: $button-padding-horizontal - padding-right: $button-padding-horizontal - padding-top: $button-padding-vertical - text-align: center - white-space: nowrap - strong - color: inherit - .icon - &, - &.is-small, - &.is-medium, - &.is-large - height: 1.5em - width: 1.5em - &:first-child:not(:last-child) - +ltr-property("margin", calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}), false) - +ltr-property("margin", $button-padding-horizontal / 4) - &:last-child:not(:first-child) - +ltr-property("margin", $button-padding-horizontal / 4, false) - +ltr-property("margin", calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width})) - &:first-child:last-child - margin-left: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}) - margin-right: calc(#{-1 / 2 * $button-padding-horizontal} - #{$button-border-width}) - // States - &:hover, - &.is-hovered - border-color: $button-hover-border-color - color: $button-hover-color - &:focus, - &.is-focused - border-color: $button-focus-border-color - color: $button-focus-color - &:not(:active) - box-shadow: $button-focus-box-shadow-size $button-focus-box-shadow-color - &:active, - &.is-active - border-color: $button-active-border-color - color: $button-active-color - // Colors - &.is-text - background-color: transparent - border-color: transparent - color: $button-text-color - text-decoration: $button-text-decoration - &:hover, - &.is-hovered, - &:focus, - &.is-focused - background-color: $button-text-hover-background-color - color: $button-text-hover-color - &:active, - &.is-active - background-color: bulmaDarken($button-text-hover-background-color, 5%) - color: $button-text-hover-color - &[disabled], - fieldset[disabled] & - background-color: transparent - border-color: transparent - box-shadow: none - @each $name, $pair in $colors - $color: nth($pair, 1) - $color-invert: nth($pair, 2) - &.is-#{$name} - background-color: $color - border-color: transparent - color: $color-invert - &:hover, - &.is-hovered - background-color: bulmaDarken($color, 2.5%) - border-color: transparent - color: $color-invert - &:focus, - &.is-focused - border-color: transparent - color: $color-invert - &:not(:active) - box-shadow: $button-focus-box-shadow-size bulmaRgba($color, 0.25) - &:active, - &.is-active - background-color: bulmaDarken($color, 5%) - border-color: transparent - color: $color-invert - &[disabled], - fieldset[disabled] & - background-color: $color - border-color: transparent - box-shadow: none - &.is-inverted - background-color: $color-invert - color: $color - &:hover, - &.is-hovered - background-color: bulmaDarken($color-invert, 5%) - &[disabled], - fieldset[disabled] & - background-color: $color-invert - border-color: transparent - box-shadow: none - color: $color - &.is-loading - &::after - border-color: transparent transparent $color-invert $color-invert !important - &.is-outlined - background-color: transparent - border-color: $color - color: $color - &:hover, - &.is-hovered, - &:focus, - &.is-focused - background-color: $color - border-color: $color - color: $color-invert - &.is-loading - &::after - border-color: transparent transparent $color $color !important - &:hover, - &.is-hovered, - &:focus, - &.is-focused - &::after - border-color: transparent transparent $color-invert $color-invert !important - &[disabled], - fieldset[disabled] & - background-color: transparent - border-color: $color - box-shadow: none - color: $color - &.is-inverted.is-outlined - background-color: transparent - border-color: $color-invert - color: $color-invert - &:hover, - &.is-hovered, - &:focus, - &.is-focused - background-color: $color-invert - color: $color - &.is-loading - &:hover, - &.is-hovered, - &:focus, - &.is-focused - &::after - border-color: transparent transparent $color $color !important - &[disabled], - fieldset[disabled] & - background-color: transparent - border-color: $color-invert - box-shadow: none - color: $color-invert - // If light and dark colors are provided - @if length($pair) >= 4 - $color-light: nth($pair, 3) - $color-dark: nth($pair, 4) - &.is-light - background-color: $color-light - color: $color-dark - &:hover, - &.is-hovered - background-color: bulmaDarken($color-light, 2.5%) - border-color: transparent - color: $color-dark - &:active, - &.is-active - background-color: bulmaDarken($color-light, 5%) - border-color: transparent - color: $color-dark - // Sizes - &.is-small - +button-small - &.is-normal - +button-normal - &.is-medium - +button-medium - &.is-large - +button-large - // Modifiers - &[disabled], - fieldset[disabled] & - background-color: $button-disabled-background-color - border-color: $button-disabled-border-color - box-shadow: $button-disabled-shadow - opacity: $button-disabled-opacity - &.is-fullwidth - display: flex - width: 100% - &.is-loading - color: transparent !important - pointer-events: none - &::after - @extend %loader - +center(1em) - position: absolute !important - &.is-static - background-color: $button-static-background-color - border-color: $button-static-border-color - color: $button-static-color - box-shadow: none - pointer-events: none - &.is-rounded - border-radius: $radius-rounded - padding-left: calc(#{$button-padding-horizontal} + 0.25em) - padding-right: calc(#{$button-padding-horizontal} + 0.25em) - -.buttons - align-items: center - display: flex - flex-wrap: wrap - justify-content: flex-start - .button - margin-bottom: 0.5rem - &:not(:last-child):not(.is-fullwidth) - +ltr-property("margin", 0.5rem) - &:last-child - margin-bottom: -0.5rem - &:not(:last-child) - margin-bottom: 1rem - // Sizes - &.are-small - .button:not(.is-normal):not(.is-medium):not(.is-large) - +button-small - &.are-medium - .button:not(.is-small):not(.is-normal):not(.is-large) - +button-medium - &.are-large - .button:not(.is-small):not(.is-normal):not(.is-medium) - +button-large - &.has-addons - .button - &:not(:first-child) - border-bottom-left-radius: 0 - border-top-left-radius: 0 - &:not(:last-child) - border-bottom-right-radius: 0 - border-top-right-radius: 0 - +ltr-property("margin", -1px) - &:last-child - +ltr-property("margin", 0) - &:hover, - &.is-hovered - z-index: 2 - &:focus, - &.is-focused, - &:active, - &.is-active, - &.is-selected - z-index: 3 - &:hover - z-index: 4 - &.is-expanded - flex-grow: 1 - flex-shrink: 1 - &.is-centered - justify-content: center - &:not(.has-addons) - .button:not(.is-fullwidth) - margin-left: 0.25rem - margin-right: 0.25rem - &.is-right - justify-content: flex-end - &:not(.has-addons) - .button:not(.is-fullwidth) - margin-left: 0.25rem - margin-right: 0.25rem diff --git a/shared/static/src/bulma/sass/elements/container.sass b/shared/static/src/bulma/sass/elements/container.sass deleted file mode 100644 index d88eb94a..00000000 --- a/shared/static/src/bulma/sass/elements/container.sass +++ /dev/null @@ -1,24 +0,0 @@ -$container-offset: (2 * $gap) !default - -.container - flex-grow: 1 - margin: 0 auto - position: relative - width: auto - &.is-fluid - max-width: none - padding-left: $gap - padding-right: $gap - width: 100% - +desktop - max-width: $desktop - $container-offset - +until-widescreen - &.is-widescreen - max-width: $widescreen - $container-offset - +until-fullhd - &.is-fullhd - max-width: $fullhd - $container-offset - +widescreen - max-width: $widescreen - $container-offset - +fullhd - max-width: $fullhd - $container-offset diff --git a/shared/static/src/bulma/sass/elements/content.sass b/shared/static/src/bulma/sass/elements/content.sass deleted file mode 100644 index 800268b7..00000000 --- a/shared/static/src/bulma/sass/elements/content.sass +++ /dev/null @@ -1,155 +0,0 @@ -$content-heading-color: $text-strong !default -$content-heading-weight: $weight-semibold !default -$content-heading-line-height: 1.125 !default - -$content-blockquote-background-color: $background !default -$content-blockquote-border-left: 5px solid $border !default -$content-blockquote-padding: 1.25em 1.5em !default - -$content-pre-padding: 1.25em 1.5em !default - -$content-table-cell-border: 1px solid $border !default -$content-table-cell-border-width: 0 0 1px !default -$content-table-cell-padding: 0.5em 0.75em !default -$content-table-cell-heading-color: $text-strong !default -$content-table-head-cell-border-width: 0 0 2px !default -$content-table-head-cell-color: $text-strong !default -$content-table-foot-cell-border-width: 2px 0 0 !default -$content-table-foot-cell-color: $text-strong !default - -.content - @extend %block - // Inline - li + li - margin-top: 0.25em - // Block - p, - dl, - ol, - ul, - blockquote, - pre, - table - &:not(:last-child) - margin-bottom: 1em - h1, - h2, - h3, - h4, - h5, - h6 - color: $content-heading-color - font-weight: $content-heading-weight - line-height: $content-heading-line-height - h1 - font-size: 2em - margin-bottom: 0.5em - &:not(:first-child) - margin-top: 1em - h2 - font-size: 1.75em - margin-bottom: 0.5714em - &:not(:first-child) - margin-top: 1.1428em - h3 - font-size: 1.5em - margin-bottom: 0.6666em - &:not(:first-child) - margin-top: 1.3333em - h4 - font-size: 1.25em - margin-bottom: 0.8em - h5 - font-size: 1.125em - margin-bottom: 0.8888em - h6 - font-size: 1em - margin-bottom: 1em - blockquote - background-color: $content-blockquote-background-color - +ltr-property("border", $content-blockquote-border-left, false) - padding: $content-blockquote-padding - ol - list-style-position: outside - +ltr-property("margin", 2em, false) - margin-top: 1em - &:not([type]) - list-style-type: decimal - &.is-lower-alpha - list-style-type: lower-alpha - &.is-lower-roman - list-style-type: lower-roman - &.is-upper-alpha - list-style-type: upper-alpha - &.is-upper-roman - list-style-type: upper-roman - ul - list-style: disc outside - +ltr-property("margin", 2em, false) - margin-top: 1em - ul - list-style-type: circle - margin-top: 0.5em - ul - list-style-type: square - dd - +ltr-property("margin", 2em, false) - figure - margin-left: 2em - margin-right: 2em - text-align: center - &:not(:first-child) - margin-top: 2em - &:not(:last-child) - margin-bottom: 2em - img - display: inline-block - figcaption - font-style: italic - pre - +overflow-touch - overflow-x: auto - padding: $content-pre-padding - white-space: pre - word-wrap: normal - sup, - sub - font-size: 75% - table - width: 100% - td, - th - border: $content-table-cell-border - border-width: $content-table-cell-border-width - padding: $content-table-cell-padding - vertical-align: top - th - color: $content-table-cell-heading-color - &:not([align]) - text-align: inherit - thead - td, - th - border-width: $content-table-head-cell-border-width - color: $content-table-head-cell-color - tfoot - td, - th - border-width: $content-table-foot-cell-border-width - color: $content-table-foot-cell-color - tbody - tr - &:last-child - td, - th - border-bottom-width: 0 - .tabs - li + li - margin-top: 0 - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - &.is-large - font-size: $size-large diff --git a/shared/static/src/bulma/sass/elements/form.sass b/shared/static/src/bulma/sass/elements/form.sass deleted file mode 100644 index 3122dc4c..00000000 --- a/shared/static/src/bulma/sass/elements/form.sass +++ /dev/null @@ -1 +0,0 @@ -@warn "The form.sass file is DEPRECATED. It has moved into its own /form folder. Please import sass/form/_all instead." diff --git a/shared/static/src/bulma/sass/elements/icon.sass b/shared/static/src/bulma/sass/elements/icon.sass deleted file mode 100644 index 988546c7..00000000 --- a/shared/static/src/bulma/sass/elements/icon.sass +++ /dev/null @@ -1,21 +0,0 @@ -$icon-dimensions: 1.5rem !default -$icon-dimensions-small: 1rem !default -$icon-dimensions-medium: 2rem !default -$icon-dimensions-large: 3rem !default - -.icon - align-items: center - display: inline-flex - justify-content: center - height: $icon-dimensions - width: $icon-dimensions - // Sizes - &.is-small - height: $icon-dimensions-small - width: $icon-dimensions-small - &.is-medium - height: $icon-dimensions-medium - width: $icon-dimensions-medium - &.is-large - height: $icon-dimensions-large - width: $icon-dimensions-large diff --git a/shared/static/src/bulma/sass/elements/image.sass b/shared/static/src/bulma/sass/elements/image.sass deleted file mode 100644 index 7547abcf..00000000 --- a/shared/static/src/bulma/sass/elements/image.sass +++ /dev/null @@ -1,71 +0,0 @@ -$dimensions: 16 24 32 48 64 96 128 !default - -.image - display: block - position: relative - img - display: block - height: auto - width: 100% - &.is-rounded - border-radius: $radius-rounded - &.is-fullwidth - width: 100% - // Ratio - &.is-square, - &.is-1by1, - &.is-5by4, - &.is-4by3, - &.is-3by2, - &.is-5by3, - &.is-16by9, - &.is-2by1, - &.is-3by1, - &.is-4by5, - &.is-3by4, - &.is-2by3, - &.is-3by5, - &.is-9by16, - &.is-1by2, - &.is-1by3 - img, - .has-ratio - @extend %overlay - height: 100% - width: 100% - &.is-square, - &.is-1by1 - padding-top: 100% - &.is-5by4 - padding-top: 80% - &.is-4by3 - padding-top: 75% - &.is-3by2 - padding-top: 66.6666% - &.is-5by3 - padding-top: 60% - &.is-16by9 - padding-top: 56.25% - &.is-2by1 - padding-top: 50% - &.is-3by1 - padding-top: 33.3333% - &.is-4by5 - padding-top: 125% - &.is-3by4 - padding-top: 133.3333% - &.is-2by3 - padding-top: 150% - &.is-3by5 - padding-top: 166.6666% - &.is-9by16 - padding-top: 177.7777% - &.is-1by2 - padding-top: 200% - &.is-1by3 - padding-top: 300% - // Sizes - @each $dimension in $dimensions - &.is-#{$dimension}x#{$dimension} - height: $dimension * 1px - width: $dimension * 1px diff --git a/shared/static/src/bulma/sass/elements/notification.sass b/shared/static/src/bulma/sass/elements/notification.sass deleted file mode 100644 index af1c7be5..00000000 --- a/shared/static/src/bulma/sass/elements/notification.sass +++ /dev/null @@ -1,48 +0,0 @@ -$notification-background-color: $background !default -$notification-code-background-color: $scheme-main !default -$notification-radius: $radius !default -$notification-padding: 1.25rem 2.5rem 1.25rem 1.5rem !default -$notification-padding-ltr: 1.25rem 2.5rem 1.25rem 1.5rem !default -$notification-padding-rtl: 1.25rem 1.5rem 1.25rem 2.5rem !default - -.notification - @extend %block - background-color: $notification-background-color - border-radius: $notification-radius - position: relative - +ltr - padding: $notification-padding-ltr - +rtl - padding: $notification-padding-rtl - a:not(.button):not(.dropdown-item) - color: currentColor - text-decoration: underline - strong - color: currentColor - code, - pre - background: $notification-code-background-color - pre code - background: transparent - & > .delete - +ltr-position(0.5rem) - position: absolute - top: 0.5rem - .title, - .subtitle, - .content - color: currentColor - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - $color-invert: nth($pair, 2) - &.is-#{$name} - background-color: $color - color: $color-invert - // If light and dark colors are provided - @if length($pair) >= 4 - $color-light: nth($pair, 3) - $color-dark: nth($pair, 4) - &.is-light - background-color: $color-light - color: $color-dark diff --git a/shared/static/src/bulma/sass/elements/other.sass b/shared/static/src/bulma/sass/elements/other.sass deleted file mode 100644 index 5725617c..00000000 --- a/shared/static/src/bulma/sass/elements/other.sass +++ /dev/null @@ -1,39 +0,0 @@ -.block - @extend %block - -.delete - @extend %delete - -.heading - display: block - font-size: 11px - letter-spacing: 1px - margin-bottom: 5px - text-transform: uppercase - -.highlight - @extend %block - font-weight: $weight-normal - max-width: 100% - overflow: hidden - padding: 0 - pre - overflow: auto - max-width: 100% - -.loader - @extend %loader - -.number - align-items: center - background-color: $background - border-radius: $radius-rounded - display: inline-flex - font-size: $size-medium - height: 2em - justify-content: center - margin-right: 1.5rem - min-width: 2.5em - padding: 0.25rem 0.5rem - text-align: center - vertical-align: top diff --git a/shared/static/src/bulma/sass/elements/progress.sass b/shared/static/src/bulma/sass/elements/progress.sass deleted file mode 100644 index bb43bb60..00000000 --- a/shared/static/src/bulma/sass/elements/progress.sass +++ /dev/null @@ -1,67 +0,0 @@ -$progress-bar-background-color: $border-light !default -$progress-value-background-color: $text !default -$progress-border-radius: $radius-rounded !default - -$progress-indeterminate-duration: 1.5s !default - -.progress - @extend %block - -moz-appearance: none - -webkit-appearance: none - border: none - border-radius: $progress-border-radius - display: block - height: $size-normal - overflow: hidden - padding: 0 - width: 100% - &::-webkit-progress-bar - background-color: $progress-bar-background-color - &::-webkit-progress-value - background-color: $progress-value-background-color - &::-moz-progress-bar - background-color: $progress-value-background-color - &::-ms-fill - background-color: $progress-value-background-color - border: none - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - &.is-#{$name} - &::-webkit-progress-value - background-color: $color - &::-moz-progress-bar - background-color: $color - &::-ms-fill - background-color: $color - &:indeterminate - background-image: linear-gradient(to right, $color 30%, $progress-bar-background-color 30%) - - &:indeterminate - animation-duration: $progress-indeterminate-duration - animation-iteration-count: infinite - animation-name: moveIndeterminate - animation-timing-function: linear - background-color: $progress-bar-background-color - background-image: linear-gradient(to right, $text 30%, $progress-bar-background-color 30%) - background-position: top left - background-repeat: no-repeat - background-size: 150% 150% - &::-webkit-progress-bar - background-color: transparent - &::-moz-progress-bar - background-color: transparent - - // Sizes - &.is-small - height: $size-small - &.is-medium - height: $size-medium - &.is-large - height: $size-large - -@keyframes moveIndeterminate - from - background-position: 200% 0 - to - background-position: -200% 0 diff --git a/shared/static/src/bulma/sass/elements/table.sass b/shared/static/src/bulma/sass/elements/table.sass deleted file mode 100644 index 48d7d93e..00000000 --- a/shared/static/src/bulma/sass/elements/table.sass +++ /dev/null @@ -1,129 +0,0 @@ -$table-color: $text-strong !default -$table-background-color: $scheme-main !default - -$table-cell-border: 1px solid $border !default -$table-cell-border-width: 0 0 1px !default -$table-cell-padding: 0.5em 0.75em !default -$table-cell-heading-color: $text-strong !default - -$table-head-cell-border-width: 0 0 2px !default -$table-head-cell-color: $text-strong !default -$table-foot-cell-border-width: 2px 0 0 !default -$table-foot-cell-color: $text-strong !default - -$table-head-background-color: transparent !default -$table-body-background-color: transparent !default -$table-foot-background-color: transparent !default - -$table-row-hover-background-color: $scheme-main-bis !default - -$table-row-active-background-color: $primary !default -$table-row-active-color: $primary-invert !default - -$table-striped-row-even-background-color: $scheme-main-bis !default -$table-striped-row-even-hover-background-color: $scheme-main-ter !default - -.table - @extend %block - background-color: $table-background-color - color: $table-color - td, - th - border: $table-cell-border - border-width: $table-cell-border-width - padding: $table-cell-padding - vertical-align: top - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - $color-invert: nth($pair, 2) - &.is-#{$name} - background-color: $color - border-color: $color - color: $color-invert - // Modifiers - &.is-narrow - white-space: nowrap - width: 1% - &.is-selected - background-color: $table-row-active-background-color - color: $table-row-active-color - a, - strong - color: currentColor - &.is-vcentered - vertical-align: middle - th - color: $table-cell-heading-color - &:not([align]) - text-align: inherit - tr - &.is-selected - background-color: $table-row-active-background-color - color: $table-row-active-color - a, - strong - color: currentColor - td, - th - border-color: $table-row-active-color - color: currentColor - thead - background-color: $table-head-background-color - td, - th - border-width: $table-head-cell-border-width - color: $table-head-cell-color - tfoot - background-color: $table-foot-background-color - td, - th - border-width: $table-foot-cell-border-width - color: $table-foot-cell-color - tbody - background-color: $table-body-background-color - tr - &:last-child - td, - th - border-bottom-width: 0 - // Modifiers - &.is-bordered - td, - th - border-width: 1px - tr - &:last-child - td, - th - border-bottom-width: 1px - &.is-fullwidth - width: 100% - &.is-hoverable - tbody - tr:not(.is-selected) - &:hover - background-color: $table-row-hover-background-color - &.is-striped - tbody - tr:not(.is-selected) - &:hover - background-color: $table-row-hover-background-color - &:nth-child(even) - background-color: $table-striped-row-even-hover-background-color - &.is-narrow - td, - th - padding: 0.25em 0.5em - &.is-striped - tbody - tr:not(.is-selected) - &:nth-child(even) - background-color: $table-striped-row-even-background-color - -.table-container - @extend %block - +overflow-touch - overflow: auto - overflow-y: hidden - max-width: 100% diff --git a/shared/static/src/bulma/sass/elements/tag.sass b/shared/static/src/bulma/sass/elements/tag.sass deleted file mode 100644 index f3c20a21..00000000 --- a/shared/static/src/bulma/sass/elements/tag.sass +++ /dev/null @@ -1,136 +0,0 @@ -$tag-background-color: $background !default -$tag-color: $text !default -$tag-radius: $radius !default -$tag-delete-margin: 1px !default - -.tags - align-items: center - display: flex - flex-wrap: wrap - justify-content: flex-start - .tag - margin-bottom: 0.5rem - &:not(:last-child) - +ltr-property("margin", 0.5rem) - &:last-child - margin-bottom: -0.5rem - &:not(:last-child) - margin-bottom: 1rem - // Sizes - &.are-medium - .tag:not(.is-normal):not(.is-large) - font-size: $size-normal - &.are-large - .tag:not(.is-normal):not(.is-medium) - font-size: $size-medium - &.is-centered - justify-content: center - .tag - margin-right: 0.25rem - margin-left: 0.25rem - &.is-right - justify-content: flex-end - .tag - &:not(:first-child) - margin-left: 0.5rem - &:not(:last-child) - margin-right: 0 - &.has-addons - .tag - +ltr-property("margin", 0) - &:not(:first-child) - +ltr-property("margin", 0, false) - +ltr - border-top-left-radius: 0 - border-bottom-left-radius: 0 - +rtl - border-top-right-radius: 0 - border-bottom-right-radius: 0 - &:not(:last-child) - +ltr - border-top-right-radius: 0 - border-bottom-right-radius: 0 - +rtl - border-top-left-radius: 0 - border-bottom-left-radius: 0 - -.tag:not(body) - align-items: center - background-color: $tag-background-color - border-radius: $tag-radius - color: $tag-color - display: inline-flex - font-size: $size-small - height: 2em - justify-content: center - line-height: 1.5 - padding-left: 0.75em - padding-right: 0.75em - white-space: nowrap - .delete - +ltr-property("margin", 0.25rem, false) - +ltr-property("margin", -0.375rem) - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - $color-invert: nth($pair, 2) - &.is-#{$name} - background-color: $color - color: $color-invert - // If a light and dark colors are provided - @if length($pair) > 3 - $color-light: nth($pair, 3) - $color-dark: nth($pair, 4) - &.is-light - background-color: $color-light - color: $color-dark - // Sizes - &.is-normal - font-size: $size-small - &.is-medium - font-size: $size-normal - &.is-large - font-size: $size-medium - .icon - &:first-child:not(:last-child) - +ltr-property("margin", -0.375em, false) - +ltr-property("margin", 0.1875em) - &:last-child:not(:first-child) - +ltr-property("margin", 0.1875em, false) - +ltr-property("margin", -0.375em) - &:first-child:last-child - +ltr-property("margin", -0.375em, false) - +ltr-property("margin", -0.375em) - // Modifiers - &.is-delete - +ltr-property("margin", $tag-delete-margin, false) - padding: 0 - position: relative - width: 2em - &::before, - &::after - background-color: currentColor - content: "" - display: block - left: 50% - position: absolute - top: 50% - transform: translateX(-50%) translateY(-50%) rotate(45deg) - transform-origin: center center - &::before - height: 1px - width: 50% - &::after - height: 50% - width: 1px - &:hover, - &:focus - background-color: darken($tag-background-color, 5%) - &:active - background-color: darken($tag-background-color, 10%) - &.is-rounded - border-radius: $radius-rounded - -a.tag - &:hover - text-decoration: underline diff --git a/shared/static/src/bulma/sass/elements/title.sass b/shared/static/src/bulma/sass/elements/title.sass deleted file mode 100644 index fa9947dd..00000000 --- a/shared/static/src/bulma/sass/elements/title.sass +++ /dev/null @@ -1,70 +0,0 @@ -$title-color: $text-strong !default -$title-family: false !default -$title-size: $size-3 !default -$title-weight: $weight-semibold !default -$title-line-height: 1.125 !default -$title-strong-color: inherit !default -$title-strong-weight: inherit !default -$title-sub-size: 0.75em !default -$title-sup-size: 0.75em !default - -$subtitle-color: $text !default -$subtitle-family: false !default -$subtitle-size: $size-5 !default -$subtitle-weight: $weight-normal !default -$subtitle-line-height: 1.25 !default -$subtitle-strong-color: $text-strong !default -$subtitle-strong-weight: $weight-semibold !default -$subtitle-negative-margin: -1.25rem !default - -.title, -.subtitle - @extend %block - word-break: break-word - em, - span - font-weight: inherit - sub - font-size: $title-sub-size - sup - font-size: $title-sup-size - .tag - vertical-align: middle - -.title - color: $title-color - @if $title-family - font-family: $title-family - font-size: $title-size - font-weight: $title-weight - line-height: $title-line-height - strong - color: $title-strong-color - font-weight: $title-strong-weight - & + .highlight - margin-top: -0.75rem - &:not(.is-spaced) + .subtitle - margin-top: $subtitle-negative-margin - // Sizes - @each $size in $sizes - $i: index($sizes, $size) - &.is-#{$i} - font-size: $size - -.subtitle - color: $subtitle-color - @if $subtitle-family - font-family: $subtitle-family - font-size: $subtitle-size - font-weight: $subtitle-weight - line-height: $subtitle-line-height - strong - color: $subtitle-strong-color - font-weight: $subtitle-strong-weight - &:not(.is-spaced) + .title - margin-top: $subtitle-negative-margin - // Sizes - @each $size in $sizes - $i: index($sizes, $size) - &.is-#{$i} - font-size: $size diff --git a/shared/static/src/bulma/sass/form/_all.sass b/shared/static/src/bulma/sass/form/_all.sass deleted file mode 100644 index d9a2b955..00000000 --- a/shared/static/src/bulma/sass/form/_all.sass +++ /dev/null @@ -1,8 +0,0 @@ -@charset "utf-8" - -@import "shared.sass" -@import "input-textarea.sass" -@import "checkbox-radio.sass" -@import "select.sass" -@import "file.sass" -@import "tools.sass" diff --git a/shared/static/src/bulma/sass/form/checkbox-radio.sass b/shared/static/src/bulma/sass/form/checkbox-radio.sass deleted file mode 100644 index 96486673..00000000 --- a/shared/static/src/bulma/sass/form/checkbox-radio.sass +++ /dev/null @@ -1,21 +0,0 @@ -%checkbox-radio - cursor: pointer - display: inline-block - line-height: 1.25 - position: relative - input - cursor: pointer - &:hover - color: $input-hover-color - &[disabled], - fieldset[disabled] & - color: $input-disabled-color - cursor: not-allowed - -.checkbox - @extend %checkbox-radio - -.radio - @extend %checkbox-radio - & + .radio - +ltr-property("margin", 0.5em, false) diff --git a/shared/static/src/bulma/sass/form/file.sass b/shared/static/src/bulma/sass/form/file.sass deleted file mode 100644 index 5fe0eee2..00000000 --- a/shared/static/src/bulma/sass/form/file.sass +++ /dev/null @@ -1,180 +0,0 @@ -$file-border-color: $border !default -$file-radius: $radius !default - -$file-cta-background-color: $scheme-main-ter !default -$file-cta-color: $text !default -$file-cta-hover-color: $text-strong !default -$file-cta-active-color: $text-strong !default - -$file-name-border-color: $border !default -$file-name-border-style: solid !default -$file-name-border-width: 1px 1px 1px 0 !default -$file-name-max-width: 16em !default - -.file - @extend %unselectable - align-items: stretch - display: flex - justify-content: flex-start - position: relative - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - $color-invert: nth($pair, 2) - &.is-#{$name} - .file-cta - background-color: $color - border-color: transparent - color: $color-invert - &:hover, - &.is-hovered - .file-cta - background-color: bulmaDarken($color, 2.5%) - border-color: transparent - color: $color-invert - &:focus, - &.is-focused - .file-cta - border-color: transparent - box-shadow: 0 0 0.5em bulmaRgba($color, 0.25) - color: $color-invert - &:active, - &.is-active - .file-cta - background-color: bulmaDarken($color, 5%) - border-color: transparent - color: $color-invert - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - .file-icon - .fa - font-size: 21px - &.is-large - font-size: $size-large - .file-icon - .fa - font-size: 28px - // Modifiers - &.has-name - .file-cta - border-bottom-right-radius: 0 - border-top-right-radius: 0 - .file-name - border-bottom-left-radius: 0 - border-top-left-radius: 0 - &.is-empty - .file-cta - border-radius: $file-radius - .file-name - display: none - &.is-boxed - .file-label - flex-direction: column - .file-cta - flex-direction: column - height: auto - padding: 1em 3em - .file-name - border-width: 0 1px 1px - .file-icon - height: 1.5em - width: 1.5em - .fa - font-size: 21px - &.is-small - .file-icon .fa - font-size: 14px - &.is-medium - .file-icon .fa - font-size: 28px - &.is-large - .file-icon .fa - font-size: 35px - &.has-name - .file-cta - border-radius: $file-radius $file-radius 0 0 - .file-name - border-radius: 0 0 $file-radius $file-radius - border-width: 0 1px 1px - &.is-centered - justify-content: center - &.is-fullwidth - .file-label - width: 100% - .file-name - flex-grow: 1 - max-width: none - &.is-right - justify-content: flex-end - .file-cta - border-radius: 0 $file-radius $file-radius 0 - .file-name - border-radius: $file-radius 0 0 $file-radius - border-width: 1px 0 1px 1px - order: -1 - -.file-label - align-items: stretch - display: flex - cursor: pointer - justify-content: flex-start - overflow: hidden - position: relative - &:hover - .file-cta - background-color: bulmaDarken($file-cta-background-color, 2.5%) - color: $file-cta-hover-color - .file-name - border-color: bulmaDarken($file-name-border-color, 2.5%) - &:active - .file-cta - background-color: bulmaDarken($file-cta-background-color, 5%) - color: $file-cta-active-color - .file-name - border-color: bulmaDarken($file-name-border-color, 5%) - -.file-input - height: 100% - left: 0 - opacity: 0 - outline: none - position: absolute - top: 0 - width: 100% - -.file-cta, -.file-name - @extend %control - border-color: $file-border-color - border-radius: $file-radius - font-size: 1em - padding-left: 1em - padding-right: 1em - white-space: nowrap - -.file-cta - background-color: $file-cta-background-color - color: $file-cta-color - -.file-name - border-color: $file-name-border-color - border-style: $file-name-border-style - border-width: $file-name-border-width - display: block - max-width: $file-name-max-width - overflow: hidden - text-align: inherit - text-overflow: ellipsis - -.file-icon - align-items: center - display: flex - height: 1em - justify-content: center - +ltr-property("margin", 0.5em) - width: 1em - .fa - font-size: 14px diff --git a/shared/static/src/bulma/sass/form/input-textarea.sass b/shared/static/src/bulma/sass/form/input-textarea.sass deleted file mode 100644 index a5aef556..00000000 --- a/shared/static/src/bulma/sass/form/input-textarea.sass +++ /dev/null @@ -1,64 +0,0 @@ -$textarea-padding: $control-padding-horizontal !default -$textarea-max-height: 40em !default -$textarea-min-height: 8em !default - -%input-textarea - @extend %input - box-shadow: $input-shadow - max-width: 100% - width: 100% - &[readonly] - box-shadow: none - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - &.is-#{$name} - border-color: $color - &:focus, - &.is-focused, - &:active, - &.is-active - box-shadow: $input-focus-box-shadow-size bulmaRgba($color, 0.25) - // Sizes - &.is-small - +control-small - &.is-medium - +control-medium - &.is-large - +control-large - // Modifiers - &.is-fullwidth - display: block - width: 100% - &.is-inline - display: inline - width: auto - -.input - @extend %input-textarea - &.is-rounded - border-radius: $radius-rounded - padding-left: calc(#{$control-padding-horizontal} + 0.375em) - padding-right: calc(#{$control-padding-horizontal} + 0.375em) - &.is-static - background-color: transparent - border-color: transparent - box-shadow: none - padding-left: 0 - padding-right: 0 - -.textarea - @extend %input-textarea - display: block - max-width: 100% - min-width: 100% - padding: $textarea-padding - resize: vertical - &:not([rows]) - max-height: $textarea-max-height - min-height: $textarea-min-height - &[rows] - height: initial - // Modifiers - &.has-fixed-size - resize: none diff --git a/shared/static/src/bulma/sass/form/select.sass b/shared/static/src/bulma/sass/form/select.sass deleted file mode 100644 index 21d62d0b..00000000 --- a/shared/static/src/bulma/sass/form/select.sass +++ /dev/null @@ -1,85 +0,0 @@ -.select - display: inline-block - max-width: 100% - position: relative - vertical-align: top - &:not(.is-multiple) - height: $input-height - &:not(.is-multiple):not(.is-loading) - &::after - @extend %arrow - border-color: $input-arrow - +ltr-position(1.125em) - z-index: 4 - &.is-rounded - select - border-radius: $radius-rounded - +ltr-property("padding", 1em, false) - select - @extend %input - cursor: pointer - display: block - font-size: 1em - max-width: 100% - outline: none - &::-ms-expand - display: none - &[disabled]:hover, - fieldset[disabled] &:hover - border-color: $input-disabled-border-color - &:not([multiple]) - +ltr-property("padding", 2.5em) - &[multiple] - height: auto - padding: 0 - option - padding: 0.5em 1em - // States - &:not(.is-multiple):not(.is-loading):hover - &::after - border-color: $input-hover-color - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - &.is-#{$name} - &:not(:hover)::after - border-color: $color - select - border-color: $color - &:hover, - &.is-hovered - border-color: bulmaDarken($color, 5%) - &:focus, - &.is-focused, - &:active, - &.is-active - box-shadow: $input-focus-box-shadow-size bulmaRgba($color, 0.25) - // Sizes - &.is-small - +control-small - &.is-medium - +control-medium - &.is-large - +control-large - // Modifiers - &.is-disabled - &::after - border-color: $input-disabled-color - &.is-fullwidth - width: 100% - select - width: 100% - &.is-loading - &::after - @extend %loader - margin-top: 0 - position: absolute - +ltr-position(0.625em) - top: 0.625em - transform: none - &.is-small:after - font-size: $size-small - &.is-medium:after - font-size: $size-medium - &.is-large:after - font-size: $size-large diff --git a/shared/static/src/bulma/sass/form/shared.sass b/shared/static/src/bulma/sass/form/shared.sass deleted file mode 100644 index 230a00cb..00000000 --- a/shared/static/src/bulma/sass/form/shared.sass +++ /dev/null @@ -1,55 +0,0 @@ -$input-color: $text-strong !default -$input-background-color: $scheme-main !default -$input-border-color: $border !default -$input-height: $control-height !default -$input-shadow: inset 0 0.0625em 0.125em rgba($scheme-invert, 0.05) !default -$input-placeholder-color: bulmaRgba($input-color, 0.3) !default - -$input-hover-color: $text-strong !default -$input-hover-border-color: $border-hover !default - -$input-focus-color: $text-strong !default -$input-focus-border-color: $link !default -$input-focus-box-shadow-size: 0 0 0 0.125em !default -$input-focus-box-shadow-color: bulmaRgba($link, 0.25) !default - -$input-disabled-color: $text-light !default -$input-disabled-background-color: $background !default -$input-disabled-border-color: $background !default -$input-disabled-placeholder-color: bulmaRgba($input-disabled-color, 0.3) !default - -$input-arrow: $link !default - -$input-icon-color: $border !default -$input-icon-active-color: $text !default - -$input-radius: $radius !default - -=input - @extend %control - background-color: $input-background-color - border-color: $input-border-color - border-radius: $input-radius - color: $input-color - +placeholder - color: $input-placeholder-color - &:hover, - &.is-hovered - border-color: $input-hover-border-color - &:focus, - &.is-focused, - &:active, - &.is-active - border-color: $input-focus-border-color - box-shadow: $input-focus-box-shadow-size $input-focus-box-shadow-color - &[disabled], - fieldset[disabled] & - background-color: $input-disabled-background-color - border-color: $input-disabled-border-color - box-shadow: none - color: $input-disabled-color - +placeholder - color: $input-disabled-placeholder-color - -%input - +input diff --git a/shared/static/src/bulma/sass/form/tools.sass b/shared/static/src/bulma/sass/form/tools.sass deleted file mode 100644 index d97427c4..00000000 --- a/shared/static/src/bulma/sass/form/tools.sass +++ /dev/null @@ -1,213 +0,0 @@ -$label-color: $text-strong !default -$label-weight: $weight-bold !default - -$help-size: $size-small !default - -.label - color: $label-color - display: block - font-size: $size-normal - font-weight: $label-weight - &:not(:last-child) - margin-bottom: 0.5em - // Sizes - &.is-small - font-size: $size-small - &.is-medium - font-size: $size-medium - &.is-large - font-size: $size-large - -.help - display: block - font-size: $help-size - margin-top: 0.25rem - @each $name, $pair in $colors - $color: nth($pair, 1) - &.is-#{$name} - color: $color - -// Containers - -.field - &:not(:last-child) - margin-bottom: 0.75rem - // Modifiers - &.has-addons - display: flex - justify-content: flex-start - .control - &:not(:last-child) - +ltr-property("margin", -1px) - &:not(:first-child):not(:last-child) - .button, - .input, - .select select - border-radius: 0 - &:first-child:not(:only-child) - .button, - .input, - .select select - +ltr - border-bottom-right-radius: 0 - border-top-right-radius: 0 - +rtl - border-bottom-left-radius: 0 - border-top-left-radius: 0 - &:last-child:not(:only-child) - .button, - .input, - .select select - +ltr - border-bottom-left-radius: 0 - border-top-left-radius: 0 - +rtl - border-bottom-right-radius: 0 - border-top-right-radius: 0 - .button, - .input, - .select select - &:not([disabled]) - &:hover, - &.is-hovered - z-index: 2 - &:focus, - &.is-focused, - &:active, - &.is-active - z-index: 3 - &:hover - z-index: 4 - &.is-expanded - flex-grow: 1 - flex-shrink: 1 - &.has-addons-centered - justify-content: center - &.has-addons-right - justify-content: flex-end - &.has-addons-fullwidth - .control - flex-grow: 1 - flex-shrink: 0 - &.is-grouped - display: flex - justify-content: flex-start - & > .control - flex-shrink: 0 - &:not(:last-child) - margin-bottom: 0 - +ltr-property("margin", 0.75rem) - &.is-expanded - flex-grow: 1 - flex-shrink: 1 - &.is-grouped-centered - justify-content: center - &.is-grouped-right - justify-content: flex-end - &.is-grouped-multiline - flex-wrap: wrap - & > .control - &:last-child, - &:not(:last-child) - margin-bottom: 0.75rem - &:last-child - margin-bottom: -0.75rem - &:not(:last-child) - margin-bottom: 0 - &.is-horizontal - +tablet - display: flex - -.field-label - .label - font-size: inherit - +mobile - margin-bottom: 0.5rem - +tablet - flex-basis: 0 - flex-grow: 1 - flex-shrink: 0 - +ltr-property("margin", 1.5rem) - text-align: right - &.is-small - font-size: $size-small - padding-top: 0.375em - &.is-normal - padding-top: 0.375em - &.is-medium - font-size: $size-medium - padding-top: 0.375em - &.is-large - font-size: $size-large - padding-top: 0.375em - -.field-body - .field .field - margin-bottom: 0 - +tablet - display: flex - flex-basis: 0 - flex-grow: 5 - flex-shrink: 1 - .field - margin-bottom: 0 - & > .field - flex-shrink: 1 - &:not(.is-narrow) - flex-grow: 1 - &:not(:last-child) - +ltr-property("margin", 0.75rem) - -.control - box-sizing: border-box - clear: both - font-size: $size-normal - position: relative - text-align: inherit - // Modifiers - &.has-icons-left, - &.has-icons-right - .input, - .select - &:focus - & ~ .icon - color: $input-icon-active-color - &.is-small ~ .icon - font-size: $size-small - &.is-medium ~ .icon - font-size: $size-medium - &.is-large ~ .icon - font-size: $size-large - .icon - color: $input-icon-color - height: $input-height - pointer-events: none - position: absolute - top: 0 - width: $input-height - z-index: 4 - &.has-icons-left - .input, - .select select - padding-left: $input-height - .icon.is-left - left: 0 - &.has-icons-right - .input, - .select select - padding-right: $input-height - .icon.is-right - right: 0 - &.is-loading - &::after - @extend %loader - position: absolute !important - +ltr-position(0.625em) - top: 0.625em - z-index: 4 - &.is-small:after - font-size: $size-small - &.is-medium:after - font-size: $size-medium - &.is-large:after - font-size: $size-large diff --git a/shared/static/src/bulma/sass/grid/_all.sass b/shared/static/src/bulma/sass/grid/_all.sass deleted file mode 100644 index e53070f6..00000000 --- a/shared/static/src/bulma/sass/grid/_all.sass +++ /dev/null @@ -1,4 +0,0 @@ -@charset "utf-8" - -@import "columns.sass" -@import "tiles.sass" diff --git a/shared/static/src/bulma/sass/grid/columns.sass b/shared/static/src/bulma/sass/grid/columns.sass deleted file mode 100644 index 34a83533..00000000 --- a/shared/static/src/bulma/sass/grid/columns.sass +++ /dev/null @@ -1,504 +0,0 @@ -$column-gap: 0.75rem !default - -.column - display: block - flex-basis: 0 - flex-grow: 1 - flex-shrink: 1 - padding: $column-gap - .columns.is-mobile > &.is-narrow - flex: none - .columns.is-mobile > &.is-full - flex: none - width: 100% - .columns.is-mobile > &.is-three-quarters - flex: none - width: 75% - .columns.is-mobile > &.is-two-thirds - flex: none - width: 66.6666% - .columns.is-mobile > &.is-half - flex: none - width: 50% - .columns.is-mobile > &.is-one-third - flex: none - width: 33.3333% - .columns.is-mobile > &.is-one-quarter - flex: none - width: 25% - .columns.is-mobile > &.is-one-fifth - flex: none - width: 20% - .columns.is-mobile > &.is-two-fifths - flex: none - width: 40% - .columns.is-mobile > &.is-three-fifths - flex: none - width: 60% - .columns.is-mobile > &.is-four-fifths - flex: none - width: 80% - .columns.is-mobile > &.is-offset-three-quarters - margin-left: 75% - .columns.is-mobile > &.is-offset-two-thirds - margin-left: 66.6666% - .columns.is-mobile > &.is-offset-half - margin-left: 50% - .columns.is-mobile > &.is-offset-one-third - margin-left: 33.3333% - .columns.is-mobile > &.is-offset-one-quarter - margin-left: 25% - .columns.is-mobile > &.is-offset-one-fifth - margin-left: 20% - .columns.is-mobile > &.is-offset-two-fifths - margin-left: 40% - .columns.is-mobile > &.is-offset-three-fifths - margin-left: 60% - .columns.is-mobile > &.is-offset-four-fifths - margin-left: 80% - @for $i from 0 through 12 - .columns.is-mobile > &.is-#{$i} - flex: none - width: percentage($i / 12) - .columns.is-mobile > &.is-offset-#{$i} - margin-left: percentage($i / 12) - +mobile - &.is-narrow-mobile - flex: none - &.is-full-mobile - flex: none - width: 100% - &.is-three-quarters-mobile - flex: none - width: 75% - &.is-two-thirds-mobile - flex: none - width: 66.6666% - &.is-half-mobile - flex: none - width: 50% - &.is-one-third-mobile - flex: none - width: 33.3333% - &.is-one-quarter-mobile - flex: none - width: 25% - &.is-one-fifth-mobile - flex: none - width: 20% - &.is-two-fifths-mobile - flex: none - width: 40% - &.is-three-fifths-mobile - flex: none - width: 60% - &.is-four-fifths-mobile - flex: none - width: 80% - &.is-offset-three-quarters-mobile - margin-left: 75% - &.is-offset-two-thirds-mobile - margin-left: 66.6666% - &.is-offset-half-mobile - margin-left: 50% - &.is-offset-one-third-mobile - margin-left: 33.3333% - &.is-offset-one-quarter-mobile - margin-left: 25% - &.is-offset-one-fifth-mobile - margin-left: 20% - &.is-offset-two-fifths-mobile - margin-left: 40% - &.is-offset-three-fifths-mobile - margin-left: 60% - &.is-offset-four-fifths-mobile - margin-left: 80% - @for $i from 0 through 12 - &.is-#{$i}-mobile - flex: none - width: percentage($i / 12) - &.is-offset-#{$i}-mobile - margin-left: percentage($i / 12) - +tablet - &.is-narrow, - &.is-narrow-tablet - flex: none - &.is-full, - &.is-full-tablet - flex: none - width: 100% - &.is-three-quarters, - &.is-three-quarters-tablet - flex: none - width: 75% - &.is-two-thirds, - &.is-two-thirds-tablet - flex: none - width: 66.6666% - &.is-half, - &.is-half-tablet - flex: none - width: 50% - &.is-one-third, - &.is-one-third-tablet - flex: none - width: 33.3333% - &.is-one-quarter, - &.is-one-quarter-tablet - flex: none - width: 25% - &.is-one-fifth, - &.is-one-fifth-tablet - flex: none - width: 20% - &.is-two-fifths, - &.is-two-fifths-tablet - flex: none - width: 40% - &.is-three-fifths, - &.is-three-fifths-tablet - flex: none - width: 60% - &.is-four-fifths, - &.is-four-fifths-tablet - flex: none - width: 80% - &.is-offset-three-quarters, - &.is-offset-three-quarters-tablet - margin-left: 75% - &.is-offset-two-thirds, - &.is-offset-two-thirds-tablet - margin-left: 66.6666% - &.is-offset-half, - &.is-offset-half-tablet - margin-left: 50% - &.is-offset-one-third, - &.is-offset-one-third-tablet - margin-left: 33.3333% - &.is-offset-one-quarter, - &.is-offset-one-quarter-tablet - margin-left: 25% - &.is-offset-one-fifth, - &.is-offset-one-fifth-tablet - margin-left: 20% - &.is-offset-two-fifths, - &.is-offset-two-fifths-tablet - margin-left: 40% - &.is-offset-three-fifths, - &.is-offset-three-fifths-tablet - margin-left: 60% - &.is-offset-four-fifths, - &.is-offset-four-fifths-tablet - margin-left: 80% - @for $i from 0 through 12 - &.is-#{$i}, - &.is-#{$i}-tablet - flex: none - width: percentage($i / 12) - &.is-offset-#{$i}, - &.is-offset-#{$i}-tablet - margin-left: percentage($i / 12) - +touch - &.is-narrow-touch - flex: none - &.is-full-touch - flex: none - width: 100% - &.is-three-quarters-touch - flex: none - width: 75% - &.is-two-thirds-touch - flex: none - width: 66.6666% - &.is-half-touch - flex: none - width: 50% - &.is-one-third-touch - flex: none - width: 33.3333% - &.is-one-quarter-touch - flex: none - width: 25% - &.is-one-fifth-touch - flex: none - width: 20% - &.is-two-fifths-touch - flex: none - width: 40% - &.is-three-fifths-touch - flex: none - width: 60% - &.is-four-fifths-touch - flex: none - width: 80% - &.is-offset-three-quarters-touch - margin-left: 75% - &.is-offset-two-thirds-touch - margin-left: 66.6666% - &.is-offset-half-touch - margin-left: 50% - &.is-offset-one-third-touch - margin-left: 33.3333% - &.is-offset-one-quarter-touch - margin-left: 25% - &.is-offset-one-fifth-touch - margin-left: 20% - &.is-offset-two-fifths-touch - margin-left: 40% - &.is-offset-three-fifths-touch - margin-left: 60% - &.is-offset-four-fifths-touch - margin-left: 80% - @for $i from 0 through 12 - &.is-#{$i}-touch - flex: none - width: percentage($i / 12) - &.is-offset-#{$i}-touch - margin-left: percentage($i / 12) - +desktop - &.is-narrow-desktop - flex: none - &.is-full-desktop - flex: none - width: 100% - &.is-three-quarters-desktop - flex: none - width: 75% - &.is-two-thirds-desktop - flex: none - width: 66.6666% - &.is-half-desktop - flex: none - width: 50% - &.is-one-third-desktop - flex: none - width: 33.3333% - &.is-one-quarter-desktop - flex: none - width: 25% - &.is-one-fifth-desktop - flex: none - width: 20% - &.is-two-fifths-desktop - flex: none - width: 40% - &.is-three-fifths-desktop - flex: none - width: 60% - &.is-four-fifths-desktop - flex: none - width: 80% - &.is-offset-three-quarters-desktop - margin-left: 75% - &.is-offset-two-thirds-desktop - margin-left: 66.6666% - &.is-offset-half-desktop - margin-left: 50% - &.is-offset-one-third-desktop - margin-left: 33.3333% - &.is-offset-one-quarter-desktop - margin-left: 25% - &.is-offset-one-fifth-desktop - margin-left: 20% - &.is-offset-two-fifths-desktop - margin-left: 40% - &.is-offset-three-fifths-desktop - margin-left: 60% - &.is-offset-four-fifths-desktop - margin-left: 80% - @for $i from 0 through 12 - &.is-#{$i}-desktop - flex: none - width: percentage($i / 12) - &.is-offset-#{$i}-desktop - margin-left: percentage($i / 12) - +widescreen - &.is-narrow-widescreen - flex: none - &.is-full-widescreen - flex: none - width: 100% - &.is-three-quarters-widescreen - flex: none - width: 75% - &.is-two-thirds-widescreen - flex: none - width: 66.6666% - &.is-half-widescreen - flex: none - width: 50% - &.is-one-third-widescreen - flex: none - width: 33.3333% - &.is-one-quarter-widescreen - flex: none - width: 25% - &.is-one-fifth-widescreen - flex: none - width: 20% - &.is-two-fifths-widescreen - flex: none - width: 40% - &.is-three-fifths-widescreen - flex: none - width: 60% - &.is-four-fifths-widescreen - flex: none - width: 80% - &.is-offset-three-quarters-widescreen - margin-left: 75% - &.is-offset-two-thirds-widescreen - margin-left: 66.6666% - &.is-offset-half-widescreen - margin-left: 50% - &.is-offset-one-third-widescreen - margin-left: 33.3333% - &.is-offset-one-quarter-widescreen - margin-left: 25% - &.is-offset-one-fifth-widescreen - margin-left: 20% - &.is-offset-two-fifths-widescreen - margin-left: 40% - &.is-offset-three-fifths-widescreen - margin-left: 60% - &.is-offset-four-fifths-widescreen - margin-left: 80% - @for $i from 0 through 12 - &.is-#{$i}-widescreen - flex: none - width: percentage($i / 12) - &.is-offset-#{$i}-widescreen - margin-left: percentage($i / 12) - +fullhd - &.is-narrow-fullhd - flex: none - &.is-full-fullhd - flex: none - width: 100% - &.is-three-quarters-fullhd - flex: none - width: 75% - &.is-two-thirds-fullhd - flex: none - width: 66.6666% - &.is-half-fullhd - flex: none - width: 50% - &.is-one-third-fullhd - flex: none - width: 33.3333% - &.is-one-quarter-fullhd - flex: none - width: 25% - &.is-one-fifth-fullhd - flex: none - width: 20% - &.is-two-fifths-fullhd - flex: none - width: 40% - &.is-three-fifths-fullhd - flex: none - width: 60% - &.is-four-fifths-fullhd - flex: none - width: 80% - &.is-offset-three-quarters-fullhd - margin-left: 75% - &.is-offset-two-thirds-fullhd - margin-left: 66.6666% - &.is-offset-half-fullhd - margin-left: 50% - &.is-offset-one-third-fullhd - margin-left: 33.3333% - &.is-offset-one-quarter-fullhd - margin-left: 25% - &.is-offset-one-fifth-fullhd - margin-left: 20% - &.is-offset-two-fifths-fullhd - margin-left: 40% - &.is-offset-three-fifths-fullhd - margin-left: 60% - &.is-offset-four-fifths-fullhd - margin-left: 80% - @for $i from 0 through 12 - &.is-#{$i}-fullhd - flex: none - width: percentage($i / 12) - &.is-offset-#{$i}-fullhd - margin-left: percentage($i / 12) - -.columns - margin-left: (-$column-gap) - margin-right: (-$column-gap) - margin-top: (-$column-gap) - &:last-child - margin-bottom: (-$column-gap) - &:not(:last-child) - margin-bottom: calc(1.5rem - #{$column-gap}) - // Modifiers - &.is-centered - justify-content: center - &.is-gapless - margin-left: 0 - margin-right: 0 - margin-top: 0 - & > .column - margin: 0 - padding: 0 !important - &:not(:last-child) - margin-bottom: 1.5rem - &:last-child - margin-bottom: 0 - &.is-mobile - display: flex - &.is-multiline - flex-wrap: wrap - &.is-vcentered - align-items: center - // Responsiveness - +tablet - &:not(.is-desktop) - display: flex - +desktop - // Modifiers - &.is-desktop - display: flex - -@if $variable-columns - .columns.is-variable - --columnGap: 0.75rem - margin-left: calc(-1 * var(--columnGap)) - margin-right: calc(-1 * var(--columnGap)) - .column - padding-left: var(--columnGap) - padding-right: var(--columnGap) - @for $i from 0 through 8 - &.is-#{$i} - --columnGap: #{$i * 0.25rem} - +mobile - &.is-#{$i}-mobile - --columnGap: #{$i * 0.25rem} - +tablet - &.is-#{$i}-tablet - --columnGap: #{$i * 0.25rem} - +tablet-only - &.is-#{$i}-tablet-only - --columnGap: #{$i * 0.25rem} - +touch - &.is-#{$i}-touch - --columnGap: #{$i * 0.25rem} - +desktop - &.is-#{$i}-desktop - --columnGap: #{$i * 0.25rem} - +desktop-only - &.is-#{$i}-desktop-only - --columnGap: #{$i * 0.25rem} - +widescreen - &.is-#{$i}-widescreen - --columnGap: #{$i * 0.25rem} - +widescreen-only - &.is-#{$i}-widescreen-only - --columnGap: #{$i * 0.25rem} - +fullhd - &.is-#{$i}-fullhd - --columnGap: #{$i * 0.25rem} diff --git a/shared/static/src/bulma/sass/grid/tiles.sass b/shared/static/src/bulma/sass/grid/tiles.sass deleted file mode 100644 index 15648c29..00000000 --- a/shared/static/src/bulma/sass/grid/tiles.sass +++ /dev/null @@ -1,34 +0,0 @@ -$tile-spacing: 0.75rem !default - -.tile - align-items: stretch - display: block - flex-basis: 0 - flex-grow: 1 - flex-shrink: 1 - min-height: min-content - // Modifiers - &.is-ancestor - margin-left: $tile-spacing * -1 - margin-right: $tile-spacing * -1 - margin-top: $tile-spacing * -1 - &:last-child - margin-bottom: $tile-spacing * -1 - &:not(:last-child) - margin-bottom: $tile-spacing - &.is-child - margin: 0 !important - &.is-parent - padding: $tile-spacing - &.is-vertical - flex-direction: column - & > .tile.is-child:not(:last-child) - margin-bottom: 1.5rem !important - // Responsiveness - +tablet - &:not(.is-child) - display: flex - @for $i from 1 through 12 - &.is-#{$i} - flex: none - width: ($i / 12) * 100% diff --git a/shared/static/src/bulma/sass/helpers/_all.sass b/shared/static/src/bulma/sass/helpers/_all.sass deleted file mode 100644 index 89ef0a7f..00000000 --- a/shared/static/src/bulma/sass/helpers/_all.sass +++ /dev/null @@ -1,10 +0,0 @@ -@charset "utf-8" - -@import "color.sass" -@import "float.sass" -@import "other.sass" -@import "overflow.sass" -@import "position.sass" -@import "spacing.sass" -@import "typography.sass" -@import "visibility.sass" diff --git a/shared/static/src/bulma/sass/helpers/color.sass b/shared/static/src/bulma/sass/helpers/color.sass deleted file mode 100644 index 22ac8c51..00000000 --- a/shared/static/src/bulma/sass/helpers/color.sass +++ /dev/null @@ -1,37 +0,0 @@ -@each $name, $pair in $colors - $color: nth($pair, 1) - .has-text-#{$name} - color: $color !important - a.has-text-#{$name} - &:hover, - &:focus - color: bulmaDarken($color, 10%) !important - .has-background-#{$name} - background-color: $color !important - @if length($pair) >= 4 - $color-light: nth($pair, 3) - $color-dark: nth($pair, 4) - // Light - .has-text-#{$name}-light - color: $color-light !important - a.has-text-#{$name}-light - &:hover, - &:focus - color: bulmaDarken($color-light, 10%) !important - .has-background-#{$name}-light - background-color: $color-light !important - // Dark - .has-text-#{$name}-dark - color: $color-dark !important - a.has-text-#{$name}-dark - &:hover, - &:focus - color: bulmaLighten($color-dark, 10%) !important - .has-background-#{$name}-dark - background-color: $color-dark !important - -@each $name, $shade in $shades - .has-text-#{$name} - color: $shade !important - .has-background-#{$name} - background-color: $shade !important diff --git a/shared/static/src/bulma/sass/helpers/float.sass b/shared/static/src/bulma/sass/helpers/float.sass deleted file mode 100644 index fc77f179..00000000 --- a/shared/static/src/bulma/sass/helpers/float.sass +++ /dev/null @@ -1,8 +0,0 @@ -.is-clearfix - +clearfix - -.is-pulled-left - float: left !important - -.is-pulled-right - float: right !important diff --git a/shared/static/src/bulma/sass/helpers/other.sass b/shared/static/src/bulma/sass/helpers/other.sass deleted file mode 100644 index 9aa271b3..00000000 --- a/shared/static/src/bulma/sass/helpers/other.sass +++ /dev/null @@ -1,8 +0,0 @@ -.is-radiusless - border-radius: 0 !important - -.is-shadowless - box-shadow: none !important - -.is-unselectable - @extend %unselectable diff --git a/shared/static/src/bulma/sass/helpers/overflow.sass b/shared/static/src/bulma/sass/helpers/overflow.sass deleted file mode 100644 index ef1e3ef0..00000000 --- a/shared/static/src/bulma/sass/helpers/overflow.sass +++ /dev/null @@ -1,2 +0,0 @@ -.is-clipped - overflow: hidden !important diff --git a/shared/static/src/bulma/sass/helpers/position.sass b/shared/static/src/bulma/sass/helpers/position.sass deleted file mode 100644 index 083b36b7..00000000 --- a/shared/static/src/bulma/sass/helpers/position.sass +++ /dev/null @@ -1,5 +0,0 @@ -.is-overlay - @extend %overlay - -.is-relative - position: relative !important diff --git a/shared/static/src/bulma/sass/helpers/spacing.sass b/shared/static/src/bulma/sass/helpers/spacing.sass deleted file mode 100644 index b7e571e8..00000000 --- a/shared/static/src/bulma/sass/helpers/spacing.sass +++ /dev/null @@ -1,28 +0,0 @@ -.is-marginless - margin: 0 !important - -.is-paddingless - padding: 0 !important - -$spacing-shortcuts: ("margin": "m", "padding": "p") !default -$spacing-directions: ("top": "t", "right": "r", "bottom": "b", "left": "l") !default -$spacing-horizontal: "x" !default -$spacing-vertical: "y" !default -$spacing-values: ("0": 0, "1": 0.25rem, "2": 0.5rem, "3": 0.75rem, "4": 1rem, "5": 1.5rem, "6": 3rem) !default - -@each $property, $shortcut in $spacing-shortcuts - @each $name, $value in $spacing-values - // Cardinal directions - @each $direction, $suffix in $spacing-directions - .#{$shortcut}#{$suffix}-#{$name} - #{$property}-#{$direction}: $value !important - // Horizontal axis - @if $spacing-horizontal != null - .#{$shortcut}#{$spacing-horizontal}-#{$name} - #{$property}-left: $value !important - #{$property}-right: $value !important - // Vertical axis - @if $spacing-vertical != null - .#{$shortcut}#{$spacing-vertical}-#{$name} - #{$property}-top: $value !important - #{$property}-bottom: $value !important diff --git a/shared/static/src/bulma/sass/helpers/typography.sass b/shared/static/src/bulma/sass/helpers/typography.sass deleted file mode 100644 index eafd7e09..00000000 --- a/shared/static/src/bulma/sass/helpers/typography.sass +++ /dev/null @@ -1,98 +0,0 @@ -=typography-size($target:'') - @each $size in $sizes - $i: index($sizes, $size) - .is-size-#{$i}#{if($target == '', '', '-' + $target)} - font-size: $size !important - -+typography-size() - -+mobile - +typography-size('mobile') - -+tablet - +typography-size('tablet') - -+touch - +typography-size('touch') - -+desktop - +typography-size('desktop') - -+widescreen - +typography-size('widescreen') - -+fullhd - +typography-size('fullhd') - -$alignments: ('centered': 'center', 'justified': 'justify', 'left': 'left', 'right': 'right') - -@each $alignment, $text-align in $alignments - .has-text-#{$alignment} - text-align: #{$text-align} !important - -@each $alignment, $text-align in $alignments - +mobile - .has-text-#{$alignment}-mobile - text-align: #{$text-align} !important - +tablet - .has-text-#{$alignment}-tablet - text-align: #{$text-align} !important - +tablet-only - .has-text-#{$alignment}-tablet-only - text-align: #{$text-align} !important - +touch - .has-text-#{$alignment}-touch - text-align: #{$text-align} !important - +desktop - .has-text-#{$alignment}-desktop - text-align: #{$text-align} !important - +desktop-only - .has-text-#{$alignment}-desktop-only - text-align: #{$text-align} !important - +widescreen - .has-text-#{$alignment}-widescreen - text-align: #{$text-align} !important - +widescreen-only - .has-text-#{$alignment}-widescreen-only - text-align: #{$text-align} !important - +fullhd - .has-text-#{$alignment}-fullhd - text-align: #{$text-align} !important - -.is-capitalized - text-transform: capitalize !important - -.is-lowercase - text-transform: lowercase !important - -.is-uppercase - text-transform: uppercase !important - -.is-italic - font-style: italic !important - -.has-text-weight-light - font-weight: $weight-light !important -.has-text-weight-normal - font-weight: $weight-normal !important -.has-text-weight-medium - font-weight: $weight-medium !important -.has-text-weight-semibold - font-weight: $weight-semibold !important -.has-text-weight-bold - font-weight: $weight-bold !important - -.is-family-primary - font-family: $family-primary !important - -.is-family-secondary - font-family: $family-secondary !important - -.is-family-sans-serif - font-family: $family-sans-serif !important - -.is-family-monospace - font-family: $family-monospace !important - -.is-family-code - font-family: $family-code !important diff --git a/shared/static/src/bulma/sass/helpers/visibility.sass b/shared/static/src/bulma/sass/helpers/visibility.sass deleted file mode 100644 index 92477f3a..00000000 --- a/shared/static/src/bulma/sass/helpers/visibility.sass +++ /dev/null @@ -1,122 +0,0 @@ - - -$displays: 'block' 'flex' 'inline' 'inline-block' 'inline-flex' - -@each $display in $displays - .is-#{$display} - display: #{$display} !important - +mobile - .is-#{$display}-mobile - display: #{$display} !important - +tablet - .is-#{$display}-tablet - display: #{$display} !important - +tablet-only - .is-#{$display}-tablet-only - display: #{$display} !important - +touch - .is-#{$display}-touch - display: #{$display} !important - +desktop - .is-#{$display}-desktop - display: #{$display} !important - +desktop-only - .is-#{$display}-desktop-only - display: #{$display} !important - +widescreen - .is-#{$display}-widescreen - display: #{$display} !important - +widescreen-only - .is-#{$display}-widescreen-only - display: #{$display} !important - +fullhd - .is-#{$display}-fullhd - display: #{$display} !important - -.is-hidden - display: none !important - -.is-sr-only - border: none !important - clip: rect(0, 0, 0, 0) !important - height: 0.01em !important - overflow: hidden !important - padding: 0 !important - position: absolute !important - white-space: nowrap !important - width: 0.01em !important - -+mobile - .is-hidden-mobile - display: none !important - -+tablet - .is-hidden-tablet - display: none !important - -+tablet-only - .is-hidden-tablet-only - display: none !important - -+touch - .is-hidden-touch - display: none !important - -+desktop - .is-hidden-desktop - display: none !important - -+desktop-only - .is-hidden-desktop-only - display: none !important - -+widescreen - .is-hidden-widescreen - display: none !important - -+widescreen-only - .is-hidden-widescreen-only - display: none !important - -+fullhd - .is-hidden-fullhd - display: none !important - -.is-invisible - visibility: hidden !important - -+mobile - .is-invisible-mobile - visibility: hidden !important - -+tablet - .is-invisible-tablet - visibility: hidden !important - -+tablet-only - .is-invisible-tablet-only - visibility: hidden !important - -+touch - .is-invisible-touch - visibility: hidden !important - -+desktop - .is-invisible-desktop - visibility: hidden !important - -+desktop-only - .is-invisible-desktop-only - visibility: hidden !important - -+widescreen - .is-invisible-widescreen - visibility: hidden !important - -+widescreen-only - .is-invisible-widescreen-only - visibility: hidden !important - -+fullhd - .is-invisible-fullhd - visibility: hidden !important diff --git a/shared/static/src/bulma/sass/layout/_all.sass b/shared/static/src/bulma/sass/layout/_all.sass deleted file mode 100644 index 143ada35..00000000 --- a/shared/static/src/bulma/sass/layout/_all.sass +++ /dev/null @@ -1,5 +0,0 @@ -@charset "utf-8" - -@import "hero.sass" -@import "section.sass" -@import "footer.sass" diff --git a/shared/static/src/bulma/sass/layout/footer.sass b/shared/static/src/bulma/sass/layout/footer.sass deleted file mode 100644 index 8faa11ed..00000000 --- a/shared/static/src/bulma/sass/layout/footer.sass +++ /dev/null @@ -1,9 +0,0 @@ -$footer-background-color: $scheme-main-bis !default -$footer-color: false !default -$footer-padding: 3rem 1.5rem 6rem !default - -.footer - background-color: $footer-background-color - padding: $footer-padding - @if $footer-color - color: $footer-color diff --git a/shared/static/src/bulma/sass/layout/hero.sass b/shared/static/src/bulma/sass/layout/hero.sass deleted file mode 100644 index 925c98c2..00000000 --- a/shared/static/src/bulma/sass/layout/hero.sass +++ /dev/null @@ -1,145 +0,0 @@ -$hero-body-padding: 3rem 1.5rem !default -$hero-body-padding-small: 1.5rem !default -$hero-body-padding-medium: 9rem 1.5rem !default -$hero-body-padding-large: 18rem 1.5rem !default - -// Main container -.hero - align-items: stretch - display: flex - flex-direction: column - justify-content: space-between - .navbar - background: none - .tabs - ul - border-bottom: none - // Colors - @each $name, $pair in $colors - $color: nth($pair, 1) - $color-invert: nth($pair, 2) - &.is-#{$name} - background-color: $color - color: $color-invert - a:not(.button):not(.dropdown-item):not(.tag):not(.pagination-link.is-current), - strong - color: inherit - .title - color: $color-invert - .subtitle - color: bulmaRgba($color-invert, 0.9) - a:not(.button), - strong - color: $color-invert - .navbar-menu - +touch - background-color: $color - .navbar-item, - .navbar-link - color: bulmaRgba($color-invert, 0.7) - a.navbar-item, - .navbar-link - &:hover, - &.is-active - background-color: bulmaDarken($color, 5%) - color: $color-invert - .tabs - a - color: $color-invert - opacity: 0.9 - &:hover - opacity: 1 - li - &.is-active a - opacity: 1 - &.is-boxed, - &.is-toggle - a - color: $color-invert - &:hover - background-color: bulmaRgba($scheme-invert, 0.1) - li.is-active a - &, - &:hover - background-color: $color-invert - border-color: $color-invert - color: $color - // Modifiers - @if type-of($color) == 'color' - &.is-bold - $gradient-top-left: darken(saturate(adjust-hue($color, -10deg), 10%), 10%) - $gradient-bottom-right: lighten(saturate(adjust-hue($color, 10deg), 5%), 5%) - background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%) - +mobile - .navbar-menu - background-image: linear-gradient(141deg, $gradient-top-left 0%, $color 71%, $gradient-bottom-right 100%) - // Sizes - &.is-small - .hero-body - padding: $hero-body-padding-small - &.is-medium - +tablet - .hero-body - padding: $hero-body-padding-medium - &.is-large - +tablet - .hero-body - padding: $hero-body-padding-large - &.is-halfheight, - &.is-fullheight, - &.is-fullheight-with-navbar - .hero-body - align-items: center - display: flex - & > .container - flex-grow: 1 - flex-shrink: 1 - &.is-halfheight - min-height: 50vh - &.is-fullheight - min-height: 100vh - -// Components - -.hero-video - @extend %overlay - overflow: hidden - video - left: 50% - min-height: 100% - min-width: 100% - position: absolute - top: 50% - transform: translate3d(-50%, -50%, 0) - // Modifiers - &.is-transparent - opacity: 0.3 - // Responsiveness - +mobile - display: none - -.hero-buttons - margin-top: 1.5rem - // Responsiveness - +mobile - .button - display: flex - &:not(:last-child) - margin-bottom: 0.75rem - +tablet - display: flex - justify-content: center - .button:not(:last-child) - +ltr-property("margin", 1.5rem) - -// Containers - -.hero-head, -.hero-foot - flex-grow: 0 - flex-shrink: 0 - -.hero-body - flex-grow: 1 - flex-shrink: 0 - padding: $hero-body-padding diff --git a/shared/static/src/bulma/sass/layout/section.sass b/shared/static/src/bulma/sass/layout/section.sass deleted file mode 100644 index 6f2d3523..00000000 --- a/shared/static/src/bulma/sass/layout/section.sass +++ /dev/null @@ -1,13 +0,0 @@ -$section-padding: 3rem 1.5rem !default -$section-padding-medium: 9rem 1.5rem !default -$section-padding-large: 18rem 1.5rem !default - -.section - padding: $section-padding - // Responsiveness - +desktop - // Sizes - &.is-medium - padding: $section-padding-medium - &.is-large - padding: $section-padding-large diff --git a/shared/static/src/bulma/sass/utilities/_all.sass b/shared/static/src/bulma/sass/utilities/_all.sass deleted file mode 100644 index b471577c..00000000 --- a/shared/static/src/bulma/sass/utilities/_all.sass +++ /dev/null @@ -1,8 +0,0 @@ -@charset "utf-8" - -@import "initial-variables.sass" -@import "functions.sass" -@import "derived-variables.scss" -@import "animations.sass" -@import "mixins.sass" -@import "controls.sass" diff --git a/shared/static/src/bulma/sass/utilities/animations.sass b/shared/static/src/bulma/sass/utilities/animations.sass deleted file mode 100644 index a14525d7..00000000 --- a/shared/static/src/bulma/sass/utilities/animations.sass +++ /dev/null @@ -1,5 +0,0 @@ -@keyframes spinAround - from - transform: rotate(0deg) - to - transform: rotate(359deg) diff --git a/shared/static/src/bulma/sass/utilities/controls.sass b/shared/static/src/bulma/sass/utilities/controls.sass deleted file mode 100644 index cc7672a1..00000000 --- a/shared/static/src/bulma/sass/utilities/controls.sass +++ /dev/null @@ -1,50 +0,0 @@ -$control-radius: $radius !default -$control-radius-small: $radius-small !default - -$control-border-width: 1px !default - -$control-height: 2.5em !default -$control-line-height: 1.5 !default - -$control-padding-vertical: calc(0.5em - #{$control-border-width}) !default -$control-padding-horizontal: calc(0.75em - #{$control-border-width}) !default - -=control - -moz-appearance: none - -webkit-appearance: none - align-items: center - border: $control-border-width solid transparent - border-radius: $control-radius - box-shadow: none - display: inline-flex - font-size: $size-normal - height: $control-height - justify-content: flex-start - line-height: $control-line-height - padding-bottom: $control-padding-vertical - padding-left: $control-padding-horizontal - padding-right: $control-padding-horizontal - padding-top: $control-padding-vertical - position: relative - vertical-align: top - // States - &:focus, - &.is-focused, - &:active, - &.is-active - outline: none - &[disabled], - fieldset[disabled] & - cursor: not-allowed - -%control - +control - -// The controls sizes use mixins so they can be used at different breakpoints -=control-small - border-radius: $control-radius-small - font-size: $size-small -=control-medium - font-size: $size-medium -=control-large - font-size: $size-large diff --git a/shared/static/src/bulma/sass/utilities/derived-variables.scss b/shared/static/src/bulma/sass/utilities/derived-variables.scss deleted file mode 100644 index 54a03585..00000000 --- a/shared/static/src/bulma/sass/utilities/derived-variables.scss +++ /dev/null @@ -1,132 +0,0 @@ -$primary: $turquoise !default; - -$info : $cyan !default; -$success: $green !default; -$warning: $yellow !default; -$danger : $red !default; - -$light : $white-ter !default; -$dark : $grey-darker !default; - -// Invert colors - -$orange-invert : findColorInvert($orange) !default; -$yellow-invert : findColorInvert($yellow) !default; -$green-invert : findColorInvert($green) !default; -$turquoise-invert: findColorInvert($turquoise) !default; -$cyan-invert : findColorInvert($cyan) !default; -$blue-invert : findColorInvert($blue) !default; -$purple-invert : findColorInvert($purple) !default; -$red-invert : findColorInvert($red) !default; - -$primary-invert : findColorInvert($primary) !default; -$primary-light : findLightColor($primary) !default; -$primary-dark : findDarkColor($primary) !default; -$info-invert : findColorInvert($info) !default; -$info-light : findLightColor($info) !default; -$info-dark : findDarkColor($info) !default; -$success-invert : findColorInvert($success) !default; -$success-light : findLightColor($success) !default; -$success-dark : findDarkColor($success) !default; -$warning-invert : findColorInvert($warning) !default; -$warning-light : findLightColor($warning) !default; -$warning-dark : findDarkColor($warning) !default; -$danger-invert : findColorInvert($danger) !default; -$danger-light : findLightColor($danger) !default; -$danger-dark : findDarkColor($danger) !default; -$light-invert : findColorInvert($light) !default; -$dark-invert : findColorInvert($dark) !default; - -// General colors - -$scheme-main : $white !default; -$scheme-main-bis : $white-bis !default; -$scheme-main-ter : $white-ter !default; -$scheme-invert : $black !default; -$scheme-invert-bis : $black-bis !default; -$scheme-invert-ter : $black-ter !default; - -$background : $white-ter !default; - -$border : $grey-lighter !default; -$border-hover : $grey-light !default; -$border-light : $grey-lightest !default; -$border-light-hover: $grey-light !default; - -// Text colors - -$text : $grey-dark !default; -$text-invert: findColorInvert($text) !default; -$text-light : $grey !default; -$text-strong: $grey-darker !default; - -// Code colors - -$code : $red !default; -$code-background: $background !default; - -$pre : $text !default; -$pre-background : $background !default; - -// Link colors - -$link : $blue !default; -$link-invert : findColorInvert($link) !default; -$link-light : findLightColor($link) !default; -$link-dark : findDarkColor($link) !default; -$link-visited : $purple !default; - -$link-hover : $grey-darker !default; -$link-hover-border : $grey-light !default; - -$link-focus : $grey-darker !default; -$link-focus-border : $blue !default; - -$link-active : $grey-darker !default; -$link-active-border: $grey-dark !default; - -// Typography - -$family-primary : $family-sans-serif !default; -$family-secondary: $family-sans-serif !default; -$family-code : $family-monospace !default; - -$size-small : $size-7 !default; -$size-normal: $size-6 !default; -$size-medium: $size-5 !default; -$size-large : $size-4 !default; - -// Lists and maps -$custom-colors: null !default; -$custom-shades: null !default; - -$colors: mergeColorMaps( -( - "white" : ($white, $black), - "black" : ($black, $white), - "light" : ($light, $light-invert), - "dark" : ($dark, $dark-invert), - "primary": ($primary, $primary-invert, $primary-light, $primary-dark), - "link" : ($link, $link-invert, $link-light, $link-dark), - "info" : ($info, $info-invert, $info-light, $info-dark), - "success": ($success, $success-invert, $success-light, $success-dark), - "warning": ($warning, $warning-invert, $warning-light, $warning-dark), - "danger" : ($danger, $danger-invert, $danger-light, $danger-dark)), - $custom-colors -) !default; - -$shades: mergeColorMaps( -( - "black-bis" : $black-bis, - "black-ter" : $black-ter, - "grey-darker" : $grey-darker, - "grey-dark" : $grey-dark, - "grey" : $grey, - "grey-light" : $grey-light, - "grey-lighter": $grey-lighter, - "white-ter" : $white-ter, - "white-bis" : $white-bis), - $custom-shades -) !default; - -$sizes: $size-1 $size-2 $size-3 $size-4 $size-5 $size-6 $size-7 !default; diff --git a/shared/static/src/bulma/sass/utilities/functions.sass b/shared/static/src/bulma/sass/utilities/functions.sass deleted file mode 100644 index 270121f6..00000000 --- a/shared/static/src/bulma/sass/utilities/functions.sass +++ /dev/null @@ -1,115 +0,0 @@ -@function mergeColorMaps($bulma-colors, $custom-colors) - // We return at least Bulma's hard-coded colors - $merged-colors: $bulma-colors - - // We want a map as input - @if type-of($custom-colors) == 'map' - @each $name, $components in $custom-colors - // The color name should be a string - // and the components either a single color - // or a colors list with at least one element - @if type-of($name) == 'string' and (type-of($components) == 'list' or type-of($components) == 'color') and length($components) >= 1 - $color-base: null - $color-invert: null - $color-light: null - $color-dark: null - $value: null - - // The param can either be a single color - // or a list of 2 colors - @if type-of($components) == 'color' - $color-base: $components - $color-invert: findColorInvert($color-base) - $color-light: findLightColor($color-base) - $color-dark: findDarkColor($color-base) - @else if type-of($components) == 'list' - $color-base: nth($components, 1) - // If Invert, Light and Dark are provided - @if length($components) > 3 - $color-invert: nth($components, 2) - $color-light: nth($components, 3) - $color-dark: nth($components, 4) - // If only Invert and Light are provided - @else if length($components) > 2 - $color-invert: nth($components, 2) - $color-light: nth($components, 3) - $color-dark: findDarkColor($color-base) - // If only Invert is provided - @else - $color-invert: nth($components, 2) - $color-light: findLightColor($color-base) - $color-dark: findDarkColor($color-base) - - $value: ($color-base, $color-invert, $color-light, $color-dark) - - // We only want to merge the map if the color base is an actual color - @if type-of($color-base) == 'color' - // We merge this colors elements as map with Bulma's colors map - // (we can override them this way, no multiple definition for the same name) - // $merged-colors: map_merge($merged-colors, ($name: ($color-base, $color-invert, $color-light, $color-dark))) - $merged-colors: map_merge($merged-colors, ($name: $value)) - - @return $merged-colors - -@function powerNumber($number, $exp) - $value: 1 - @if $exp > 0 - @for $i from 1 through $exp - $value: $value * $number - @else if $exp < 0 - @for $i from 1 through -$exp - $value: $value / $number - @return $value - -@function colorLuminance($color) - @if type-of($color) != 'color' - @return 0.55 - $color-rgb: ('red': red($color),'green': green($color),'blue': blue($color)) - @each $name, $value in $color-rgb - $adjusted: 0 - $value: $value / 255 - @if $value < 0.03928 - $value: $value / 12.92 - @else - $value: ($value + .055) / 1.055 - $value: powerNumber($value, 2) - $color-rgb: map-merge($color-rgb, ($name: $value)) - @return (map-get($color-rgb, 'red') * .2126) + (map-get($color-rgb, 'green') * .7152) + (map-get($color-rgb, 'blue') * .0722) - -@function findColorInvert($color) - @if (colorLuminance($color) > 0.55) - @return rgba(#000, 0.7) - @else - @return #fff - -@function findLightColor($color) - @if type-of($color) == 'color' - $l: 96% - @if lightness($color) > 96% - $l: lightness($color) - @return change-color($color, $lightness: $l) - @return $background - -@function findDarkColor($color) - @if type-of($color) == 'color' - $base-l: 29% - $luminance: colorLuminance($color) - $luminance-delta: (0.53 - $luminance) - $target-l: round($base-l + ($luminance-delta * 53)) - @return change-color($color, $lightness: max($base-l, $target-l)) - @return $text-strong - -@function bulmaRgba($color, $alpha) - @if type-of($color) != 'color' - @return $color - @return rgba($color, $alpha) - -@function bulmaDarken($color, $amount) - @if type-of($color) != 'color' - @return $color - @return darken($color, $amount) - -@function bulmaLighten($color, $amount) - @if type-of($color) != 'color' - @return $color - @return lighten($color, $amount) diff --git a/shared/static/src/bulma/sass/utilities/initial-variables.sass b/shared/static/src/bulma/sass/utilities/initial-variables.sass deleted file mode 100644 index a1d688b6..00000000 --- a/shared/static/src/bulma/sass/utilities/initial-variables.sass +++ /dev/null @@ -1,78 +0,0 @@ -// Colors - -$black: hsl(0, 0%, 4%) !default -$black-bis: hsl(0, 0%, 7%) !default -$black-ter: hsl(0, 0%, 14%) !default - -$grey-darker: hsl(0, 0%, 21%) !default -$grey-dark: hsl(0, 0%, 29%) !default -$grey: hsl(0, 0%, 48%) !default -$grey-light: hsl(0, 0%, 71%) !default -$grey-lighter: hsl(0, 0%, 86%) !default -$grey-lightest: hsl(0, 0%, 93%) !default - -$white-ter: hsl(0, 0%, 96%) !default -$white-bis: hsl(0, 0%, 98%) !default -$white: hsl(0, 0%, 100%) !default - -$orange: hsl(14, 100%, 53%) !default -$yellow: hsl(48, 100%, 67%) !default -$green: hsl(141, 53%, 53%) !default -$turquoise: hsl(171, 100%, 41%) !default -$cyan: hsl(204, 71%, 53%) !default -$blue: hsl(217, 71%, 53%) !default -$purple: hsl(271, 100%, 71%) !default -$red: hsl(348, 86%, 61%) !default - -// Typography - -$family-sans-serif: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif !default -$family-monospace: monospace !default -$render-mode: optimizeLegibility !default - -$size-1: 3rem !default -$size-2: 2.5rem !default -$size-3: 2rem !default -$size-4: 1.5rem !default -$size-5: 1.25rem !default -$size-6: 1rem !default -$size-7: 0.75rem !default - -$weight-light: 300 !default -$weight-normal: 400 !default -$weight-medium: 500 !default -$weight-semibold: 600 !default -$weight-bold: 700 !default - -// Spacing - -$block-spacing: 1.5rem !default - -// Responsiveness - -// The container horizontal gap, which acts as the offset for breakpoints -$gap: 32px !default -// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16 -$tablet: 769px !default -// 960px container + 4rem -$desktop: 960px + (2 * $gap) !default -// 1152px container + 4rem -$widescreen: 1152px + (2 * $gap) !default -$widescreen-enabled: true !default -// 1344px container + 4rem -$fullhd: 1344px + (2 * $gap) !default -$fullhd-enabled: true !default - -// Miscellaneous - -$easing: ease-out !default -$radius-small: 2px !default -$radius: 4px !default -$radius-large: 6px !default -$radius-rounded: 290486px !default -$speed: 86ms !default - -// Flags - -$variable-columns: true !default -$rtl: false !default diff --git a/shared/static/src/bulma/sass/utilities/mixins.sass b/shared/static/src/bulma/sass/utilities/mixins.sass deleted file mode 100644 index 0ed78c15..00000000 --- a/shared/static/src/bulma/sass/utilities/mixins.sass +++ /dev/null @@ -1,285 +0,0 @@ -@import "initial-variables" - -=clearfix - &::after - clear: both - content: " " - display: table - -=center($width, $height: 0) - position: absolute - @if $height != 0 - left: calc(50% - (#{$width} / 2)) - top: calc(50% - (#{$height} / 2)) - @else - left: calc(50% - (#{$width} / 2)) - top: calc(50% - (#{$width} / 2)) - -=fa($size, $dimensions) - display: inline-block - font-size: $size - height: $dimensions - line-height: $dimensions - text-align: center - vertical-align: top - width: $dimensions - -=hamburger($dimensions) - cursor: pointer - display: block - height: $dimensions - position: relative - width: $dimensions - span - background-color: currentColor - display: block - height: 1px - left: calc(50% - 8px) - position: absolute - transform-origin: center - transition-duration: $speed - transition-property: background-color, opacity, transform - transition-timing-function: $easing - width: 16px - &:nth-child(1) - top: calc(50% - 6px) - &:nth-child(2) - top: calc(50% - 1px) - &:nth-child(3) - top: calc(50% + 4px) - &:hover - background-color: bulmaRgba(black, 0.05) - // Modifers - &.is-active - span - &:nth-child(1) - transform: translateY(5px) rotate(45deg) - &:nth-child(2) - opacity: 0 - &:nth-child(3) - transform: translateY(-5px) rotate(-45deg) - -=overflow-touch - -webkit-overflow-scrolling: touch - -=placeholder - $placeholders: ':-moz' ':-webkit-input' '-moz' '-ms-input' - @each $placeholder in $placeholders - &:#{$placeholder}-placeholder - @content - -// Responsiveness - -=from($device) - @media screen and (min-width: $device) - @content - -=until($device) - @media screen and (max-width: $device - 1px) - @content - -=mobile - @media screen and (max-width: $tablet - 1px) - @content - -=tablet - @media screen and (min-width: $tablet), print - @content - -=tablet-only - @media screen and (min-width: $tablet) and (max-width: $desktop - 1px) - @content - -=touch - @media screen and (max-width: $desktop - 1px) - @content - -=desktop - @media screen and (min-width: $desktop) - @content - -=desktop-only - @if $widescreen-enabled - @media screen and (min-width: $desktop) and (max-width: $widescreen - 1px) - @content - -=until-widescreen - @if $widescreen-enabled - @media screen and (max-width: $widescreen - 1px) - @content - -=widescreen - @if $widescreen-enabled - @media screen and (min-width: $widescreen) - @content - -=widescreen-only - @if $widescreen-enabled and $fullhd-enabled - @media screen and (min-width: $widescreen) and (max-width: $fullhd - 1px) - @content - -=until-fullhd - @if $fullhd-enabled - @media screen and (max-width: $fullhd - 1px) - @content - -=fullhd - @if $fullhd-enabled - @media screen and (min-width: $fullhd) - @content - -=ltr - @if not $rtl - @content - -=rtl - @if $rtl - @content - -=ltr-property($property, $spacing, $right: true) - $normal: if($right, "right", "left") - $opposite: if($right, "left", "right") - @if $rtl - #{$property}-#{$opposite}: $spacing - @else - #{$property}-#{$normal}: $spacing - -=ltr-position($spacing, $right: true) - $normal: if($right, "right", "left") - $opposite: if($right, "left", "right") - @if $rtl - #{$opposite}: $spacing - @else - #{$normal}: $spacing - -// Placeholders - -=unselectable - -webkit-touch-callout: none - -webkit-user-select: none - -moz-user-select: none - -ms-user-select: none - user-select: none - -%unselectable - +unselectable - -=arrow($color: transparent) - border: 3px solid $color - border-radius: 2px - border-right: 0 - border-top: 0 - content: " " - display: block - height: 0.625em - margin-top: -0.4375em - pointer-events: none - position: absolute - top: 50% - transform: rotate(-45deg) - transform-origin: center - width: 0.625em - -%arrow - +arrow - -=block($spacing: $block-spacing) - &:not(:last-child) - margin-bottom: $spacing - -%block - +block - -=delete - @extend %unselectable - -moz-appearance: none - -webkit-appearance: none - background-color: bulmaRgba($scheme-invert, 0.2) - border: none - border-radius: $radius-rounded - cursor: pointer - pointer-events: auto - display: inline-block - flex-grow: 0 - flex-shrink: 0 - font-size: 0 - height: 20px - max-height: 20px - max-width: 20px - min-height: 20px - min-width: 20px - outline: none - position: relative - vertical-align: top - width: 20px - &::before, - &::after - background-color: $scheme-main - content: "" - display: block - left: 50% - position: absolute - top: 50% - transform: translateX(-50%) translateY(-50%) rotate(45deg) - transform-origin: center center - &::before - height: 2px - width: 50% - &::after - height: 50% - width: 2px - &:hover, - &:focus - background-color: bulmaRgba($scheme-invert, 0.3) - &:active - background-color: bulmaRgba($scheme-invert, 0.4) - // Sizes - &.is-small - height: 16px - max-height: 16px - max-width: 16px - min-height: 16px - min-width: 16px - width: 16px - &.is-medium - height: 24px - max-height: 24px - max-width: 24px - min-height: 24px - min-width: 24px - width: 24px - &.is-large - height: 32px - max-height: 32px - max-width: 32px - min-height: 32px - min-width: 32px - width: 32px - -%delete - +delete - -=loader - animation: spinAround 500ms infinite linear - border: 2px solid $grey-lighter - border-radius: $radius-rounded - border-right-color: transparent - border-top-color: transparent - content: "" - display: block - height: 1em - position: relative - width: 1em - -%loader - +loader - -=overlay($offset: 0) - bottom: $offset - left: $offset - position: absolute - right: $offset - top: $offset - -%overlay - +overlay diff --git a/shared/templates/shared/search_results.html b/shared/templates/shared/search_results.html deleted file mode 100644 index 66661e8c..00000000 --- a/shared/templates/shared/search_results.html +++ /dev/null @@ -1,17 +0,0 @@ -{% load i18n %} - -
      - {% for section in results %} - {% include "shared/search_results_section.html" with section=section %} - {% endfor %} - - {% block extra_section %} - {% if not results %} -
    • - - {% trans "Aucune correspondance trouvée" %} - -
    • - {% endif %} - {% endblock %} -
    diff --git a/shared/templates/shared/search_results_section.html b/shared/templates/shared/search_results_section.html deleted file mode 100644 index 800d6557..00000000 --- a/shared/templates/shared/search_results_section.html +++ /dev/null @@ -1,25 +0,0 @@ -{% load search_utils %} - -
  • - {{ section.verbose_name }} -
  • - -{% for entry in section.entries %} - {% if forloop.counter < 5 %} -
  • - {% if entry.link %} - - {{ entry.verbose_name | highlight:q }} - - {% else %} - - {{ entry.verbose_name | highlight:q }} - - {% endif %} -
  • - {% elif forloop.counter == 5 %} -
  • - ... -
  • - {% endif %} -{% endfor %} diff --git a/shared/templatetags/search_utils.py b/shared/templatetags/search_utils.py deleted file mode 100644 index a98c36e5..00000000 --- a/shared/templatetags/search_utils.py +++ /dev/null @@ -1,15 +0,0 @@ -import re - -from django import template -from django.utils.safestring import mark_safe - -register = template.Library() - - -@register.filter -def highlight(text, q): - q2 = "|".join(re.escape(word) for word in q.split()) - pattern = re.compile(r"(?P%s)" % q2, re.IGNORECASE) - return mark_safe( - re.sub(pattern, r"\g", text) - ) diff --git a/shared/tests/mixins.py b/shared/tests/testcases.py similarity index 52% rename from shared/tests/mixins.py rename to shared/tests/testcases.py index ea83616a..35d697e7 100644 --- a/shared/tests/mixins.py +++ b/shared/tests/testcases.py @@ -13,138 +13,6 @@ from django.utils.functional import cached_property User = get_user_model() -class MockLDAPMixin: - """ - Mixin pour simuler un appel à un serveur LDAP (e.g., celui de l'ENS) dans des - tests unitaires. La réponse est une instance d'une classe Entry, qui simule - grossièrement l'interface de ldap3. - Cette classe patche la méthode magique `__enter__`, le code correspondant doit donc - appeler `with Connection(*args, **kwargs) as foo` pour que le test fonctionne. - """ - - class MockLDAPModule: - SCOPE_SUBTREE = None # whatever - - def __init__(self, ldap_obj): - self.ldap_obj = ldap_obj - - def initialize(self, *args): - """Always return the same ldap object.""" - return self.ldap_obj - - def mockLDAP(self, results): - entries = [ - ( - "whatever", - { - "cn": [name.encode("utf-8")], - "uid": [uid.encode("utf-8")], - "mail": [mail.encode("utf-8")], - }, - ) - for uid, name, mail in results - ] - # Mock ldap object whose `search_s` method always returns the same results. - mock_ldap_obj = mock.Mock() - mock_ldap_obj.search_s = mock.Mock(return_value=entries) - - # Mock ldap module whose `initialize_method` always return the same ldap object. - mock_ldap_module = self.MockLDAPModule(mock_ldap_obj) - - patcher = mock.patch("shared.autocomplete.ldap", new=mock_ldap_module) - patcher.start() - self.addCleanup(patcher.stop) - - return mock_ldap_module - - -class CSVResponseMixin: - """ - Mixin pour manipuler des réponses données via CSV. Deux choix sont possibles: - - si `as_dict=False`, convertit le CSV en une liste de listes (une liste par ligne) - - si `as_dict=True`, convertit le CSV en une liste de dicts, avec les champs donnés - par la première ligne du CSV. - """ - - def _load_from_csv_response(self, r, as_dict=False, **reader_kwargs): - content = r.content.decode("utf-8") - - # la dernière ligne du fichier CSV est toujours vide - content = content.split("\n")[:-1] - if as_dict: - content = csv.DictReader(content, **reader_kwargs) - # en python3.7, content est une liste d'OrderedDicts - return list(map(dict, content)) - else: - content = csv.reader(content, **reader_kwargs) - return list(content) - - def assertCSVEqual(self, response, expected): - if type(expected[0]) == list: - as_dict = False - elif type(expected[0]) == dict: - as_dict = True - else: - raise AssertionError( - "Unsupported type in `assertCSVEqual`: " - "%(expected)s is not of type `list` nor `dict` !" - % {"expected": str(expected[0])} - ) - - content = self._load_from_csv_response(response, as_dict=as_dict) - self.assertCountEqual(content, expected) - - -class ICalMixin: - """ - Mixin pour manipuler des iCalendars. Permet de tester l'égalité entre - in iCal d'une part, et une liste d'évènements (représentés par des dicts) - d'autre part. - """ - - def _test_event_equal(self, event, exp): - """ - Les éléments du dict peuvent être de deux types: - - un tuple `(getter, expected_value)`, auquel cas on teste l'égalité - `getter(event[key]) == value)`; - - une variable `value` de n'importe quel autre type, auquel cas on teste - `event[key] == value`. - """ - for key, value in exp.items(): - if isinstance(value, tuple): - getter = value[0] - v = value[1] - else: - getter = lambda v: v - v = value - # dans un iCal, les fields sont en majuscules - if getter(event[key.upper()]) != v: - return False - return True - - def _find_event(self, ev, l): - for i, elt in enumerate(l): - if self._test_event_equal(ev, elt): - return i - return None - - def assertCalEqual(self, ical_content, expected): - remaining = expected.copy() - unexpected = [] - - cal = icalendar.Calendar.from_ical(ical_content) - - for ev in cal.walk("vevent"): - i_found = self._find_event(ev, remaining) - if i_found is not None: - remaining.pop(i_found) - else: - unexpected.append(ev) - - self.assertListEqual(remaining, []) - self.assertListEqual(unexpected, []) - - class TestCaseMixin: def assertForbidden(self, response): """ @@ -223,69 +91,140 @@ class TestCaseMixin: else: self.assertEqual(actual, expected) + def mockLDAP(self, results): + class Elt: + def __init__(self, value): + self.value = value + + class Entry: + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, Elt(v)) + + results_as_ldap = [Entry(uid=uid, cn=name) for uid, name in results] + + mock_connection = mock.MagicMock() + mock_connection.entries = results_as_ldap + + # Connection is used as a context manager. + mock_context_manager = mock.MagicMock() + mock_context_manager.return_value.__enter__.return_value = mock_connection + + patcher = mock.patch( + "gestioncof.autocomplete.Connection", new=mock_context_manager + ) + patcher.start() + self.addCleanup(patcher.stop) + + return mock_connection + + def load_from_csv_response(self, r): + decoded = r.content.decode("utf-8") + return list(csv.reader(decoded.split("\n")[:-1])) + + def _test_event_equal(self, event, exp): + for k, v_desc in exp.items(): + if isinstance(v_desc, tuple): + v_getter = v_desc[0] + v = v_desc[1] + else: + v_getter = lambda v: v + v = v_desc + if v_getter(event[k.upper()]) != v: + return False + return True + + def _find_event(self, ev, l): + for i, elt in enumerate(l): + if self._test_event_equal(ev, elt): + return elt, i + return False, -1 + + def assertCalEqual(self, ical_content, expected): + remaining = expected.copy() + unexpected = [] + + cal = icalendar.Calendar.from_ical(ical_content) + + for ev in cal.walk("vevent"): + found, i_found = self._find_event(ev, remaining) + if found: + remaining.pop(i_found) + else: + unexpected.append(ev) + + self.assertListEqual(unexpected, []) + self.assertListEqual(remaining, []) + class ViewTestCaseMixin(TestCaseMixin): """ - Utilitaire pour automatiser certains tests sur les vues Django. + TestCase extension to ease tests of kfet views. - Création d'utilisateurs - ------------------------ - # Données de base - On crée dans tous les cas deux utilisateurs : un utilisateur normal "user", - et un superutilisateur "root", avec un mot de passe identique au username. - # Accès et utilisateurs supplémentaires - Les utilisateurs créés sont accessibles dans le dict `self.users`, qui associe - un label à une instance de User. + Urls concerns + ------------- - Pour rajouter des utilisateurs supplémentaires (et s'assurer qu'ils sont - disponibles dans `self.users`), on peut redéfinir la fonction `get_users_extra()`, - qui doit renvoyer là aussi un dict . + # Basic usage - Misc QoL - ------------------------ - Pour éviter une erreur de login (puisque les messages de Django ne sont pas - disponibles), les messages de bienvenue de GestioCOF sont patchés. - Un attribut `self.now` est fixé au démarrage, pour être donné comme valeur - de retour à un patch local de `django.utils.timezone.now`. Cela permet de - tester des dates/heures de manière robuste. + Attributes: + url_name (str): Name of view under test, as given to 'reverse' + function. + url_args (list, optional): Will be given to 'reverse' call. + url_kwargs (dict, optional): Same. + url_expcted (str): What 'reverse' should return given previous + attributes. - Test d'URLS - ------------------------ + View url can then be accessed at the 'url' attribute. - # Usage basique - Teste que l'URL générée par `reverse` correspond bien à l'URL théorique. - Attributs liés : - - `url_name` : nom de l'URL qui sera donné à `reverse`, - - `url_expected` : URL attendue en retour. - - (optionnels) `url_args` et `url_kwargs` : arguments de l'URL pour `reverse`. + # Advanced usage - # Usage avancé - On peut tester plusieurs URLs pour une même vue, en redéfinissant la fonction - `urls_conf()`. Cette fonction doit retourner une liste de dicts, avec les clés - suivantes : `name`, `args`, `kwargs`, `expected`. + If multiple combinations of url name, args, kwargs can be used for a view, + it is possible to define 'urls_conf' attribute. It must be a list whose + each item is a dict defining arguments for 'reverse' call ('name', 'args', + 'kwargs' keys) and its expected result ('expected' key). - # Accès aux URLs générées - Dans le cas d'usage basique, l'attribut `self.url` contient l'URL de la vue testée - (telle que renvoyée par `reverse()`). Si plusieurs URLs sont définies dans - `urls_conf()`, elles sont accessibles par la suite dans `self.reversed_urls`. + The reversed urls can be accessed at the 't_urls' attribute. - Authentification - ------------------------ - Si l'attribut `auth_user` est dans `self.users`, l'utilisateur correspondant - est authentifié avant chaque test (cela n'empêche bien sûr pas de login un autre - utilisateur à la main). - Test de restrictions d'accès - ------------------------ - L'utilitaire vérifie automatiquement que certains utilisateurs n'ont pas accès à la - vue. Plus spécifiquement, sont testés toutes les méthodes dans `self.http_methods` - et tous les utilisateurs dans `self.auth_forbidden`. Pour rappel, l'utilisateur - `None` sert à tester la vue sans authentification. - On peut donner des paramètres GET/POST/etc. aux tests en définissant un attribut - _data. + Users concerns + -------------- + + During setup, the following users are created: + - 'user': a basic user without any permission, + - 'root': a superuser, account trigramme: 200. + Their password is their username. + + One can create additionnal users with 'get_users_extra' method, or prevent + these users to be created with 'get_users_base' method. See these two + methods for further informations. + + By using 'register_user' method, these users can then be accessed at + 'users' attribute by their label. + + A user label can be given to 'auth_user' attribute. The related user is + then authenticated on self.client during test setup. Its value defaults to + 'None', meaning no user is authenticated. + + + Automated tests + --------------- + + # Url reverse + + Based on url-related attributes/properties, the test 'test_urls' checks + that expected url is returned by 'reverse' (once with basic url usage and + each for advanced usage). + + # Forbidden responses + + The 'test_forbidden' test verifies that each user, from labels of + 'auth_forbidden' attribute, can't access the url(s), i.e. response should + be a 403, or a redirect to login view. + + Tested HTTP requests are given by 'http_methods' attribute. Additional data + can be given by defining an attribute '_data'. - TODO (?): faire pareil pour vérifier les GET/POST classiques (code 200) """ url_name = None @@ -300,13 +239,19 @@ class ViewTestCaseMixin(TestCaseMixin): """ Warning: Do not forget to call super().setUp() in subclasses. """ - + # Signals handlers on login/logout send messages. + # Due to the way the Django' test Client performs login, this raise an + # error. As workaround, we mock the Django' messages module. patcher_messages = mock.patch("gestioncof.signals.messages") patcher_messages.start() self.addCleanup(patcher_messages.stop) + # A test can mock 'django.utils.timezone.now' and give this as return + # value. E.g. it is useful if the test checks values of 'auto_now' or + # 'auto_now_add' fields. self.now = timezone.now() + # Register of User instances. self.users = {} for label, user in dict(self.users_base, **self.users_extra).items(): @@ -377,7 +322,7 @@ class ViewTestCaseMixin(TestCaseMixin): ] @property - def reversed_urls(self): + def t_urls(self): return [ reverse( url_conf["name"], @@ -390,16 +335,16 @@ class ViewTestCaseMixin(TestCaseMixin): @property def url(self): - return self.reversed_urls[0] + return self.t_urls[0] def test_urls(self): - for url, conf in zip(self.reversed_urls, self.urls_conf): + for url, conf in zip(self.t_urls, self.urls_conf): self.assertEqual(url, conf["expected"]) def test_forbidden(self): for method in self.http_methods: for user in self.auth_forbidden: - for url in self.reversed_urls: + for url in self.t_urls: self.check_forbidden(method, url, user) def check_forbidden(self, method, url, user=None): diff --git a/shared/views.py b/shared/views.py deleted file mode 100644 index fbff4ec0..00000000 --- a/shared/views.py +++ /dev/null @@ -1,100 +0,0 @@ -import base64 -from collections import namedtuple -from typing import Any - -from dal import autocomplete -from django.conf import settings -from django.contrib.auth import get_user_model -from django.core.exceptions import ImproperlyConfigured -from django.http import Http404, HttpResponse -from django.views.generic import TemplateView, View - -from shared.autocomplete import ModelSearch - -User = get_user_model() - - -class SympaListView(View): - realm = "sympa" - - username = settings.SYMPA_USERNAME - password = settings.SYMPA_PASSWORD - - filters: dict[str, Any] = {} - - def dispatch(self, request, *args, **kwargs): - if "HTTP_AUTHORIZATION" in request.META: - auth = request.META["HTTP_AUTHORIZATION"].split() - - if len(auth) == 2 and auth[0].lower() == "basic": - name, passwd = base64.b64decode(auth[1]).split(b":") - - if name == self.username and passwd == self.password: - return self.render_to_response(request, *args, **kwargs) - - return HttpResponse( - status=401, headers={"WWW-Authenticate": f'Basic realm="{self.realm}"'} - ) - - def render_to_response(self, request, *args, **kwargs): - """ - Renders a list of emails in a text response. - """ - - users = User.objects.filter(**self.filters) - - return HttpResponse( - b"\n".join(u.email.encode("utf-8") for u in users if u.email), - content_type="text/plain", - ) - - -class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView): - """Compatibility layer between ModelSearch and Select2QuerySetView.""" - - paginate_by = None - - def get_queryset(self): - keywords = self.q.split() - return super().search(keywords) - - -Section = namedtuple("Section", ("name", "verbose_name", "entries")) -Entry = namedtuple("Entry", ("verbose_name", "link")) - - -class AutocompleteView(TemplateView): - template_name = "shared/search_results.html" - search_composer = None - - def get_search_composer(self): - if self.search_composer is None: - raise ImproperlyConfigured("Please specify a search composer") - return self.search_composer - - def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) - if "q" not in self.request.GET: - raise Http404 - q = self.request.GET["q"] - ctx["q"] = q - ctx["results"] = self.search(keywords=q.split()) - return ctx - - def search(self, keywords): - search_composer = self.get_search_composer() - raw_results = search_composer.search(keywords) - sections = [] - for name, unit in search_composer.search_units: - entries = [ - Entry( - verbose_name=unit.result_verbose_name(res), - link=unit.result_link(res), - ) - for res in raw_results[name] - ] - if entries: - sections.append( - Section(name=name, verbose_name=unit.verbose_name, entries=entries) - ) - return sections diff --git a/shell.nix b/shell.nix deleted file mode 100644 index e426574e..00000000 --- a/shell.nix +++ /dev/null @@ -1,83 +0,0 @@ -{ - sources ? import ./npins, - pkgs ? import sources.nixpkgs { overlays = [ ]; }, -}: - -let - nix-pkgs = import sources.nix-pkgs { inherit pkgs; }; - kat-pkgs = import sources.kat-pkgs { inherit pkgs; }; - - python3 = pkgs.python3.override { - packageOverrides = final: prev: { - inherit (nix-pkgs) - authens - django-bootstrap-form - django-cas-ng - loadcredential - ; - - inherit (kat-pkgs.python3Packages) - django-djconfig - django-hCaptcha - wagtail-modeltranslation - wagtailmenus - ; - }; - }; -in -pkgs.mkShell { - shellHook = '' - if [ ! -d .static ]; then - mkdir .static - fi - ''; - - env = { - CREDENTIALS_DIRECTORY = builtins.toString ./.credentials; - DJANGO_SETTINGS_MODULE = "gestioasso.settings.local"; - - GESTIOCOF_DEBUG = true; - GESTIOCOF_STATIC_ROOT = builtins.toString ./.static; - - GESTIOBDS_DEBUG = true; - GESTIOBDS_STATIC_ROOT = builtins.toString ./.static; - }; - - packages = [ - (python3.withPackages ( - ps: with ps; [ - authens - channels - configparser - django - django-autocomplete-light - django-bootstrap-form - django-cas-ng - django-cors-headers - django-djconfig - django-hCaptcha - django-js-reverse - django-widget-tweaks - icalendar - loadcredential - pillow - python-dateutil - statistics - wagtail - wagtail-modeltranslation - wagtailmenus - - django-debug-toolbar - ipython - black - flake8 - isort - - daphne - ] - )) - pkgs.npins - ]; - - allowSubstitutes = false; -} diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utils/views/__init__.py b/utils/views/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/utils/views/autocomplete.py b/utils/views/autocomplete.py new file mode 100644 index 00000000..c5d51343 --- /dev/null +++ b/utils/views/autocomplete.py @@ -0,0 +1,25 @@ +from dal import autocomplete +from django.db.models import Q + + +class Select2QuerySetView(autocomplete.Select2QuerySetView): + model = None + search_fields = [] + + def get_queryset_filter(self): + q = self.q + filter_q = Q() + + if not q: + return filter_q + + words = q.split() + + for word in words: + for field in self.search_fields: + filter_q |= Q(**{"{}__icontains".format(field): word}) + + return filter_q + + def get_queryset(self): + return self.model.objects.filter(self.get_queryset_filter())