From d04b79bcb51ffc401818a28376c57e7e3c5c0e54 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 25 Nov 2019 10:48:43 +0100 Subject: [PATCH 001/573] Disable autoescape in js code --- kfet/templates/kfet/account_read.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kfet/templates/kfet/account_read.html b/kfet/templates/kfet/account_read.html index 698c512e..bbd1cff7 100644 --- a/kfet/templates/kfet/account_read.html +++ b/kfet/templates/kfet/account_read.html @@ -11,7 +11,7 @@ {% if account.user == request.user %} - +{% autoescape off %} +{% endautoescape %} {% endif %} {% endblock %} From b90e749a7f31e7bf5b33552a7d9f6982d9119cc5 Mon Sep 17 00:00:00 2001 From: Antonin Reitz Date: Mon, 25 Nov 2019 23:31:26 +0100 Subject: [PATCH 002/573] Fix typo and hence cash transaction cancel --- kfet/static/kfet/js/history.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 7807ee90..a7372b87 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -137,7 +137,7 @@ function KHistory(options = {}) { var $opegroup = this.findOpeGroup(opegroup['id']); var trigramme = $opegroup.find('.trigramme').text(); var amount = amountDisplay( - parseFloat(opegroup['amount'], opegroup['is_cof'], trigramme)); + parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme); $opegroup.find('.amount').text(amount); } From ac4d5cf7d59d4cf9df513df6aa0ac09492ff52bc Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 13:03:28 +0100 Subject: [PATCH 003/573] Patch CAS redirect parameter in logout view --- gestioncof/views.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/gestioncof/views.py b/gestioncof/views.py index 6c7bf337..87ee9d2e 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -2,6 +2,7 @@ import csv import uuid from datetime import timedelta from smtplib import SMTPRecipientsRefused +from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from custommail.shortcuts import send_custom_mail from django.contrib import messages @@ -98,6 +99,30 @@ class LoginExtView(DjangoLoginView): return super().form_invalid(form) +class CustomCasLogoutView(CasLogoutView): + """ + Actuellement, le CAS de l'ENS est pété et n'a pas le bon paramètre GET + pour rediriger après déconnexion. On change la redirection à la main + dans la vue de logout. + """ + + def get(self, request): + # CasLogoutView.get() retourne un HttpResponseRedirect + response = super().get(request) + parse_result = urlparse(response.url) + qd = parse_qs(parse_result.query) + + if "url" in qd.keys(): + # je ne vois pas bien pourquoi il faut un 2e "pop"... + qd["service"] = qd.pop("url").pop() + + # La méthode _replace est documentée ! + new_url = parse_result._replace(query=urlencode(qd)) + print(qd) + print(urlunparse(new_url)) + return redirect(urlunparse(new_url)) + + @login_required def logout(request, next_page=None): if next_page is None: @@ -107,7 +132,7 @@ def logout(request, next_page=None): if profile and profile.login_clipper: msg = _("Déconnexion de GestioCOF et CAS réussie. À bientôt {}.") - logout_view = CasLogoutView.as_view() + logout_view = CustomCasLogoutView.as_view() else: msg = _("Déconnexion de GestioCOF réussie. À bientôt {}.") logout_view = DjangoLogoutView.as_view( From 5c581d898438de1141a6e7d11f449ec27975d0b4 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 13:14:20 +0100 Subject: [PATCH 004/573] Cleanup + no msg on CAS logout --- gestioncof/views.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/gestioncof/views.py b/gestioncof/views.py index 87ee9d2e..ced35cfc 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -113,13 +113,13 @@ class CustomCasLogoutView(CasLogoutView): qd = parse_qs(parse_result.query) if "url" in qd.keys(): - # je ne vois pas bien pourquoi il faut un 2e "pop"... + # Le 2e pop est nécessaire car CAS n'aime pas + # les paramètres sous forme de liste qd["service"] = qd.pop("url").pop() # La méthode _replace est documentée ! new_url = parse_result._replace(query=urlencode(qd)) - print(qd) - print(urlunparse(new_url)) + return redirect(urlunparse(new_url)) @@ -131,7 +131,11 @@ def logout(request, next_page=None): profile = getattr(request.user, "profile", None) if profile and profile.login_clipper: - msg = _("Déconnexion de GestioCOF et CAS réussie. À bientôt {}.") + if next_page is None: + # On ne voit pas les messages quand on se déconnecte de CAS + msg = None + else: + msg = _("Déconnexion de GestioCOF et CAS réussie. À bientôt {}.") logout_view = CustomCasLogoutView.as_view() else: msg = _("Déconnexion de GestioCOF réussie. À bientôt {}.") @@ -139,7 +143,8 @@ def logout(request, next_page=None): next_page=next_page, template_name="logout.html" ) - messages.success(request, msg.format(request.user.get_short_name())) + if msg is not None: + messages.success(request, msg.format(request.user.get_short_name())) return logout_view(request) From 20ceec0e648e2a3697c7a75997a3bd072d4b0e2b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 14:11:53 +0100 Subject: [PATCH 005/573] Add has_reduction property --- .../0070_articlecategory_has_reduction.py | 22 +++++++++++++++++++ kfet/models.py | 6 +++++ 2 files changed, 28 insertions(+) create mode 100644 kfet/migrations/0070_articlecategory_has_reduction.py diff --git a/kfet/migrations/0070_articlecategory_has_reduction.py b/kfet/migrations/0070_articlecategory_has_reduction.py new file mode 100644 index 00000000..c657dfdd --- /dev/null +++ b/kfet/migrations/0070_articlecategory_has_reduction.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.7 on 2019-11-27 12:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kfet", "0069_happy_new_year"), + ] + + operations = [ + migrations.AddField( + model_name="articlecategory", + name="has_reduction", + field=models.BooleanField( + default=True, + help_text="Si oui, la réduction COF s'applique aux articles de cette catégorie", + verbose_name="réduction COF", + ), + ), + ] diff --git a/kfet/models.py b/kfet/models.py index a2d776b9..814f857a 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -464,6 +464,12 @@ class ArticleCategory(models.Model): "appliquée aux articles de " "cette catégorie.", ) + has_reduction = models.BooleanField( + "réduction COF", + default=True, + help_text="Si oui, la réduction COF s'applique" + " aux articles de cette catégorie", + ) def __str__(self): return self.name From affdf43e0bc6c3b525b2ef6d4872cf42454144fb Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 14:14:33 +0100 Subject: [PATCH 006/573] Add logic in views and templates --- kfet/forms.py | 2 +- kfet/templates/kfet/category.html | 2 ++ kfet/views.py | 5 ++++- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 2a7f111a..b6fad26f 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -236,7 +236,7 @@ class CheckoutStatementUpdateForm(forms.ModelForm): class CategoryForm(forms.ModelForm): class Meta: model = ArticleCategory - fields = ["name", "has_addcost"] + fields = ["name", "has_addcost", "has_reduction"] # ----- diff --git a/kfet/templates/kfet/category.html b/kfet/templates/kfet/category.html index 0a8b58be..5692725c 100644 --- a/kfet/templates/kfet/category.html +++ b/kfet/templates/kfet/category.html @@ -26,6 +26,7 @@ Nom Nombre d'articles Peut être majorée + Réduction COF appliquée @@ -38,6 +39,7 @@ {{ category.articles.all|length }} {{ category.has_addcost | yesno:"Oui,Non"}} + {{ category.has_reduction | yesno:"Oui,Non"}} {% endfor %} diff --git a/kfet/views.py b/kfet/views.py index c5d5082b..7ae1de82 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1056,7 +1056,10 @@ def kpsul_perform_operations(request): to_addcost_for_balance += operation.addcost_amount if operationgroup.on_acc.is_cash: to_checkout_balance += -operation.amount - if operationgroup.on_acc.is_cof: + 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 From ac3bfbe368aadd3ec791ad7c618b0726d170f05b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 14:14:42 +0100 Subject: [PATCH 007/573] Display in kfet js --- kfet/templates/kfet/kpsul.html | 6 +++--- kfet/views.py | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index ff24fcb4..c520aa30 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -661,7 +661,7 @@ $(document).ready(function() { }); $after.after(article_html); // Pour l'autocomplétion - articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock'],article['category__has_addcost']]); + articlesList.push([article['name'],article['id'],article['category_id'],article['price'], article['stock'],article['category__has_addcost'],article['category__has_reduction']]); } function getArticles() { @@ -851,7 +851,7 @@ $(document).ready(function() { && article_data[5]) amount_euro -= settings['addcost_amount'] * nb; var reduc_divisor = 1; - if (account_data['is_cof']) + if (account_data['is_cof'] && article_data[6]) reduc_divisor = 1 + settings['subvention_cof'] / 100; return (amount_euro / reduc_divisor).toFixed(2); } @@ -874,7 +874,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_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') diff --git a/kfet/views.py b/kfet/views.py index 7ae1de82..ef124c92 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1490,6 +1490,7 @@ def kpsul_articles_data(request): "category_id", "category__name", "category__has_addcost", + "category__has_reduction", ).filter(is_sold=True) return JsonResponse({"articles": list(articles)}) From e62756ed2907d5ec27e882bfed2b473523b00925 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 14:20:24 +0100 Subject: [PATCH 008/573] Fix tests --- kfet/tests/test_views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 432a77e8..82997e44 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -4028,6 +4028,7 @@ class KPsulArticlesData(ViewTestCaseMixin, TestCase): "category_id", "category__name", "category__has_addcost", + "category__has_reduction", ] ), ) From e0ffee295d74c52a4e253e006ae2b228244b9aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 27 Nov 2019 14:29:41 +0100 Subject: [PATCH 009/573] Fix static urls for multiple-select --- kfet/templates/kfet/account_group_form.html | 4 ++-- kfet/templates/kfet/history.html | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/kfet/templates/kfet/account_group_form.html b/kfet/templates/kfet/account_group_form.html index a6faf8c5..5e240649 100644 --- a/kfet/templates/kfet/account_group_form.html +++ b/kfet/templates/kfet/account_group_form.html @@ -3,8 +3,8 @@ {% load widget_tweaks %} {% block extra_head %} - - + + {% endblock %} {% block title %}Permissions - Édition{% endblock %} diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index 6e61540f..ae63358e 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -2,8 +2,8 @@ {% load l10n staticfiles widget_tweaks %} {% block extra_head %} - - + + {{ filter_form.media }} From 38aecdd741fdbfa81664643f485da1956a32ddba Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 14:41:20 +0100 Subject: [PATCH 010/573] Typo --- kfet/templates/kfet/kpsul.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index c520aa30..c5c48d0d 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -874,7 +874,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_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') From 61efded673409ee997df4114ea47d55be3fc4064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 27 Nov 2019 15:46:50 +0100 Subject: [PATCH 011/573] Remove unused references to multiple-select.* --- kfet/templates/kfet/account_group_form.html | 5 ----- 1 file changed, 5 deletions(-) diff --git a/kfet/templates/kfet/account_group_form.html b/kfet/templates/kfet/account_group_form.html index 5e240649..b309d838 100644 --- a/kfet/templates/kfet/account_group_form.html +++ b/kfet/templates/kfet/account_group_form.html @@ -2,11 +2,6 @@ {% load staticfiles %} {% load widget_tweaks %} -{% block extra_head %} - - -{% endblock %} - {% block title %}Permissions - Édition{% endblock %} {% block header-title %}Modification des permissions{% endblock %} From 11159601075680efe2bd30d61fdebbc26c9b26c6 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 27 Nov 2019 16:57:48 +0100 Subject: [PATCH 012/573] Add unit test --- kfet/tests/test_views.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 82997e44..34127cb5 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1726,6 +1726,15 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): price=Decimal("2.5"), stock=20, ) + # 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="Article_no_reduction", + price=Decimal("2.5"), + stock=20, + ) # An Account, trigramme=000, balance=50 # Do not assume user is cof, nor not cof. self.account = self.accounts["user"] @@ -2079,6 +2088,35 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.article.refresh_from_db() self.assertEqual(self.article.stock, 18) + def test_purchase_no_reduction(self): + kfet_config.set(kfet_reduction_cof=Decimal("20")) + self.account.cofprofile.is_cof = True + self.account.cofprofile.save() + data = dict( + self.base_post_data, + **{ + "form-TOTAL_FORMS": "2", + "form-0-type": "purchase", + "form-0-amount": "", + "form-0-article": str(self.article_no_reduction.pk), + "form-0-article_nb": "1", + "form-1-type": "purchase", + "form-1-amount": "", + "form-1-article": str(self.article.pk), + "form-1-article_nb": "1", + } + ) + + resp = self.client.post(self.url, data) + self._assertResponseOk(resp) + + operation_group = OperationGroup.objects.get() + self.assertEqual(operation_group.amount, Decimal("-4.50")) + operation = Operation.objects.get(article=self.article) + self.assertEqual(operation.amount, Decimal("-2.00")) + operation = Operation.objects.get(article=self.article_no_reduction) + self.assertEqual(operation.amount, Decimal("-2.50")) + def test_invalid_purchase_expects_article(self): data = dict( self.base_post_data, From 8dcc1f012aae23de9afd0fa130e743c445cade3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 28 Nov 2019 14:17:59 +0100 Subject: [PATCH 013/573] Update CHANGELOG --- CHANGELOG | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 64816348..9813b64d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,10 @@ - Nouveau module BDS - Nouveau module clubs +* Version 0.3.3 - ??? + +- Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF + * Version 0.3.2 - 04/11/2019 - Bugfix: modifier un compte K-Fêt ne supprime plus nom/prénom From a521caba8dc5105d3dd4908388a737b09aabd882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 28 Nov 2019 14:53:44 +0100 Subject: [PATCH 014/573] Update changelog wrt the lastest merged patches. --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9813b64d..7bd4311a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,9 @@ * Version 0.3.3 - ??? - Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF +- Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ +- Corrige une privilege escalation liée aux sessions partagées en K-Fêt + https://git.eleves.ens.fr/klub-dev-ens/gestioCOF/issues/240 * Version 0.3.2 - 04/11/2019 From 4c7993f48f77d6f5b993041b72fcb0113d7f8a08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 28 Nov 2019 16:09:47 +0100 Subject: [PATCH 015/573] Forgot CHANGELOG for !385 --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 7bd4311a..30ae66df 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ * Version 0.3.3 - ??? +- Corrige un problème de redirection lors de la déconnexion (CAS seulement) - Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF - Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ - Corrige une privilege escalation liée aux sessions partagées en K-Fêt From 7df8a9ef6bd711aa70ed7477fe7dbd5347037c4c Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 28 Nov 2019 18:26:39 +0100 Subject: [PATCH 016/573] Add vendor library and their sources --- kfet/static/kfet/src/backbone.js | 2096 +++++++++++++++++++++ kfet/static/kfet/src/underscore.js | 1692 +++++++++++++++++ kfet/static/kfet/vendor/backbone-min.js | 2 + kfet/static/kfet/vendor/underscore-min.js | 5 + 4 files changed, 3795 insertions(+) create mode 100644 kfet/static/kfet/src/backbone.js create mode 100644 kfet/static/kfet/src/underscore.js create mode 100644 kfet/static/kfet/vendor/backbone-min.js create mode 100644 kfet/static/kfet/vendor/underscore-min.js diff --git a/kfet/static/kfet/src/backbone.js b/kfet/static/kfet/src/backbone.js new file mode 100644 index 00000000..3e09d0dc --- /dev/null +++ b/kfet/static/kfet/src/backbone.js @@ -0,0 +1,2096 @@ +// Backbone.js 1.4.0 + +// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud +// Backbone may be freely distributed under the MIT license. +// For all details and documentation: +// http://backbonejs.org + +(function(factory) { + + // Establish the root object, `window` (`self`) in the browser, or `global` on the server. + // We use `self` instead of `window` for `WebWorker` support. + var root = typeof self == 'object' && self.self === self && self || + typeof global == 'object' && global.global === global && global; + + // Set up Backbone appropriately for the environment. Start with AMD. + if (typeof define === 'function' && define.amd) { + define(['underscore', 'jquery', 'exports'], function(_, $, exports) { + // Export global even in AMD case in case this script is loaded with + // others that may still expect a global Backbone. + root.Backbone = factory(root, exports, _, $); + }); + + // Next for Node.js or CommonJS. jQuery may not be needed as a module. + } else if (typeof exports !== 'undefined') { + var _ = require('underscore'), $; + try { $ = require('jquery'); } catch (e) {} + factory(root, exports, _, $); + + // Finally, as a browser global. + } else { + root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$); + } + +})(function(root, Backbone, _, $) { + + // Initial Setup + // ------------- + + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + + // Create a local reference to a common array method we'll want to use later. + var slice = Array.prototype.slice; + + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = '1.4.0'; + + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = $; + + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... this will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + + // Backbone.Events + // --------------- + + // A module that can be mixed in to *any object* in order to provide it with + // a custom event channel. You may bind a callback to an event with `on` or + // remove with `off`; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = {}; + + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + + // A private global variable to share between listeners and listenees. + var _listening; + + // Iterates over the standard `event, callback` (as well as the fancy multiple + // space-separated events `"change blur", callback` and jQuery-style event + // maps `{event: callback}`). + var eventsApi = function(iteratee, events, name, callback, opts) { + var i = 0, names; + if (name && typeof name === 'object') { + // Handle event maps. + if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback; + for (names = _.keys(name); i < names.length ; i++) { + events = eventsApi(iteratee, events, names[i], name[names[i]], opts); + } + } else if (name && eventSplitter.test(name)) { + // Handle space-separated event names by delegating them individually. + for (names = name.split(eventSplitter); i < names.length; i++) { + events = iteratee(events, names[i], callback, opts); + } + } else { + // Finally, standard events. + events = iteratee(events, name, callback, opts); + } + return events; + }; + + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + Events.on = function(name, callback, context) { + this._events = eventsApi(onApi, this._events || {}, name, callback, { + context: context, + ctx: this, + listening: _listening + }); + + if (_listening) { + var listeners = this._listeners || (this._listeners = {}); + listeners[_listening.id] = _listening; + // Allow the listening to use a counter, instead of tracking + // callbacks for library interop + _listening.interop = false; + } + + return this; + }; + + // Inversion-of-control versions of `on`. Tell *this* object to listen to + // an event in another object... keeping track of what it's listening to + // for easier unbinding later. + Events.listenTo = function(obj, name, callback) { + if (!obj) return this; + var id = obj._listenId || (obj._listenId = _.uniqueId('l')); + var listeningTo = this._listeningTo || (this._listeningTo = {}); + var listening = _listening = listeningTo[id]; + + // This object is not listening to any other events on `obj` yet. + // Setup the necessary references to track the listening callbacks. + if (!listening) { + this._listenId || (this._listenId = _.uniqueId('l')); + listening = _listening = listeningTo[id] = new Listening(this, obj); + } + + // Bind callbacks on obj. + var error = tryCatchOn(obj, name, callback, this); + _listening = void 0; + + if (error) throw error; + // If the target obj is not Backbone.Events, track events manually. + if (listening.interop) listening.on(name, callback); + + return this; + }; + + // The reducing API that adds a callback to the `events` object. + var onApi = function(events, name, callback, options) { + if (callback) { + var handlers = events[name] || (events[name] = []); + var context = options.context, ctx = options.ctx, listening = options.listening; + if (listening) listening.count++; + + handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening}); + } + return events; + }; + + // An try-catch guarded #on function, to prevent poisoning the global + // `_listening` variable. + var tryCatchOn = function(obj, name, callback, context) { + try { + obj.on(name, callback, context); + } catch (e) { + return e; + } + }; + + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + Events.off = function(name, callback, context) { + if (!this._events) return this; + this._events = eventsApi(offApi, this._events, name, callback, { + context: context, + listeners: this._listeners + }); + + return this; + }; + + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + Events.stopListening = function(obj, name, callback) { + var listeningTo = this._listeningTo; + if (!listeningTo) return this; + + var ids = obj ? [obj._listenId] : _.keys(listeningTo); + for (var i = 0; i < ids.length; i++) { + var listening = listeningTo[ids[i]]; + + // If listening doesn't exist, this object is not currently + // listening to obj. Break out early. + if (!listening) break; + + listening.obj.off(name, callback, this); + if (listening.interop) listening.off(name, callback); + } + if (_.isEmpty(listeningTo)) this._listeningTo = void 0; + + return this; + }; + + // The reducing API that removes a callback from the `events` object. + var offApi = function(events, name, callback, options) { + if (!events) return; + + var context = options.context, listeners = options.listeners; + var i = 0, names; + + // Delete all event listeners and "drop" events. + if (!name && !context && !callback) { + for (names = _.keys(listeners); i < names.length; i++) { + listeners[names[i]].cleanup(); + } + return; + } + + names = name ? [name] : _.keys(events); + for (; i < names.length; i++) { + name = names[i]; + var handlers = events[name]; + + // Bail out if there are no events stored. + if (!handlers) break; + + // Find any remaining events. + var remaining = []; + for (var j = 0; j < handlers.length; j++) { + var handler = handlers[j]; + if ( + callback && callback !== handler.callback && + callback !== handler.callback._callback || + context && context !== handler.context + ) { + remaining.push(handler); + } else { + var listening = handler.listening; + if (listening) listening.off(name, callback); + } + } + + // Replace events if there are any remaining. Otherwise, clean up. + if (remaining.length) { + events[name] = remaining; + } else { + delete events[name]; + } + } + + return events; + }; + + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, its listener will be removed. If multiple events + // are passed in using the space-separated syntax, the handler will fire + // once for each event, not once for a combination of all events. + Events.once = function(name, callback, context) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this)); + if (typeof name === 'string' && context == null) callback = void 0; + return this.on(events, callback, context); + }; + + // Inversion-of-control versions of `once`. + Events.listenToOnce = function(obj, name, callback) { + // Map the event into a `{event: once}` object. + var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj)); + return this.listenTo(obj, events); + }; + + // Reduces the event callbacks into a map of `{event: onceWrapper}`. + // `offer` unbinds the `onceWrapper` after it has been called. + var onceMap = function(map, name, callback, offer) { + if (callback) { + var once = map[name] = _.once(function() { + offer(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + } + return map; + }; + + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + Events.trigger = function(name) { + if (!this._events) return this; + + var length = Math.max(0, arguments.length - 1); + var args = Array(length); + for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; + + eventsApi(triggerApi, this._events, name, void 0, args); + return this; + }; + + // Handles triggering the appropriate event callbacks. + var triggerApi = function(objEvents, name, callback, args) { + if (objEvents) { + var events = objEvents[name]; + var allEvents = objEvents.all; + if (events && allEvents) allEvents = allEvents.slice(); + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, [name].concat(args)); + } + return objEvents; + }; + + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return; + case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return; + case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return; + case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return; + default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return; + } + }; + + // A listening class that tracks and cleans up memory bindings + // when all callbacks have been offed. + var Listening = function(listener, obj) { + this.id = listener._listenId; + this.listener = listener; + this.obj = obj; + this.interop = true; + this.count = 0; + this._events = void 0; + }; + + Listening.prototype.on = Events.on; + + // Offs a callback (or several). + // Uses an optimized counter if the listenee uses Backbone.Events. + // Otherwise, falls back to manual tracking to support events + // library interop. + Listening.prototype.off = function(name, callback) { + var cleanup; + if (this.interop) { + this._events = eventsApi(offApi, this._events, name, callback, { + context: void 0, + listeners: void 0 + }); + cleanup = !this._events; + } else { + this.count--; + cleanup = this.count === 0; + } + if (cleanup) this.cleanup(); + }; + + // Cleans up memory bindings between the listener and the listenee. + Listening.prototype.cleanup = function() { + delete this.listener._listeningTo[this.obj._listenId]; + if (!this.interop) delete this.obj._listeners[this.id]; + }; + + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + + // Backbone.Model + // -------------- + + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var attrs = attributes || {}; + options || (options = {}); + this.preinitialize.apply(this, arguments); + this.cid = _.uniqueId(this.cidPrefix); + this.attributes = {}; + if (options.collection) this.collection = options.collection; + if (options.parse) attrs = this.parse(attrs, options) || {}; + var defaults = _.result(this, 'defaults'); + attrs = _.defaults(_.extend({}, defaults, attrs), defaults); + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + + // A hash of attributes whose current and previous value differ. + changed: null, + + // The value returned during the last failed validation. + validationError: null, + + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: 'id', + + // The prefix is used to create the client id which is used to identify models locally. + // You may want to override this if you're experiencing name clashes with model ids. + cidPrefix: 'c', + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Model. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + + // Special-cased proxy to underscore's `_.matches` method. + matches: function(attrs) { + return !!_.iteratee(attrs, this)(this.attributes); + }, + + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + if (key == null) return this; + + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options || (options = {}); + + // Run validation. + if (!this._validate(attrs, options)) return false; + + // Extract attributes and options. + var unset = options.unset; + var silent = options.silent; + var changes = []; + var changing = this._changing; + this._changing = true; + + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + + var current = this.attributes; + var changed = this.changed; + var prev = this._previousAttributes; + + // For each `set` attribute, update or delete the current value. + for (var attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + changed[attr] = val; + } else { + delete changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + + // Update the `id`. + if (this.idAttribute in attrs) this.id = this.get(this.idAttribute); + + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = options; + for (var i = 0; i < changes.length; i++) { + this.trigger('change:' + changes[i], this, current[changes[i]], options); + } + } + + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + options = this._pending; + this._pending = false; + this.trigger('change', this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, {unset: true})); + }, + + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, {unset: true})); + }, + + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var old = this._changing ? this._previousAttributes : this.attributes; + var changed = {}; + var hasChanged; + for (var attr in diff) { + var val = diff[attr]; + if (_.isEqual(old[attr], val)) continue; + changed[attr] = val; + hasChanged = true; + } + return hasChanged ? changed : false; + }, + + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + + // Fetch the model from the server, merging the response with the model's + // local attributes. Any changed attributes will trigger a "change" event. + fetch: function(options) { + options = _.extend({parse: true}, options); + var model = this; + var success = options.success; + options.success = function(resp) { + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (!model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + // Handle both `"key", value` and `{key: value}` -style arguments. + var attrs; + if (key == null || typeof key === 'object') { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + + options = _.extend({validate: true, parse: true}, options); + var wait = options.wait; + + // If we're not waiting and attributes exist, save acts as + // `set(attr).save(null, opts)` with validation. Otherwise, check if + // the model will be valid when the attributes, if any, are set. + if (attrs && !wait) { + if (!this.set(attrs, options)) return false; + } else if (!this._validate(attrs, options)) { + return false; + } + + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + var model = this; + var success = options.success; + var attributes = this.attributes; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = options.parse ? model.parse(resp, options) : resp; + if (wait) serverAttrs = _.extend({}, attrs, serverAttrs); + if (serverAttrs && !model.set(serverAttrs, options)) return false; + if (success) success.call(options.context, model, resp, options); + model.trigger('sync', model, resp, options); + }; + wrapError(this, options); + + // Set temporary attributes if `{wait: true}` to properly find new ids. + if (attrs && wait) this.attributes = _.extend({}, attributes, attrs); + + var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update'; + if (method === 'patch' && !options.attrs) options.attrs = attrs; + var xhr = this.sync(method, this, options); + + // Restore attributes. + this.attributes = attributes; + + return xhr; + }, + + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + var wait = options.wait; + + var destroy = function() { + model.stopListening(); + model.trigger('destroy', model, model.collection, options); + }; + + options.success = function(resp) { + if (wait) destroy(); + if (success) success.call(options.context, model, resp, options); + if (!model.isNew()) model.trigger('sync', model, resp, options); + }; + + var xhr = false; + if (this.isNew()) { + _.defer(options.success); + } else { + wrapError(this, options); + xhr = this.sync('delete', this, options); + } + if (!wait) destroy(); + return xhr; + }, + + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = + _.result(this, 'urlRoot') || + _.result(this.collection, 'url') || + urlError(); + if (this.isNew()) return base; + var id = this.get(this.idAttribute); + return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id); + }, + + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return !this.has(this.idAttribute); + }, + + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend({}, options, {validate: true})); + }, + + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger('invalid', this, error, _.extend(options, {validationError: error})); + return false; + } + + }); + + // Backbone.Collection + // ------------------- + + // If models tend to represent a single row of data, a Backbone Collection is + // more analogous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + this.preinitialize.apply(this, arguments); + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({silent: true}, options)); + }; + + // Default options for `Collection#set`. + var setOptions = {add: true, remove: true, merge: true}; + var addOptions = {add: true, remove: false}; + + // Splices `insert` into `array` at index `at`. + var splice = function(array, insert, at) { + at = Math.min(Math.max(at, 0), array.length); + var tail = Array(array.length - at); + var length = insert.length; + var i; + for (i = 0; i < tail.length; i++) tail[i] = array[i + at]; + for (i = 0; i < length; i++) array[i + at] = insert[i]; + for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i]; + }; + + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Collection. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model) { return model.toJSON(options); }); + }, + + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + + // Add a model, or list of models to the set. `models` may be Backbone + // Models or raw JavaScript objects to be converted to Models, or any + // combination of the two. + add: function(models, options) { + return this.set(models, _.extend({merge: false}, options, addOptions)); + }, + + // Remove a model, or a list of models from the set. + remove: function(models, options) { + options = _.extend({}, options); + var singular = !_.isArray(models); + models = singular ? [models] : models.slice(); + var removed = this._removeModels(models, options); + if (!options.silent && removed.length) { + options.changes = {added: [], merged: [], removed: removed}; + this.trigger('update', this, options); + } + return singular ? removed[0] : removed; + }, + + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + if (models == null) return; + + options = _.extend({}, setOptions, options); + if (options.parse && !this._isModel(models)) { + models = this.parse(models, options) || []; + } + + var singular = !_.isArray(models); + models = singular ? [models] : models.slice(); + + var at = options.at; + if (at != null) at = +at; + if (at > this.length) at = this.length; + if (at < 0) at += this.length + 1; + + var set = []; + var toAdd = []; + var toMerge = []; + var toRemove = []; + var modelMap = {}; + + var add = options.add; + var merge = options.merge; + var remove = options.remove; + + var sort = false; + var sortable = this.comparator && at == null && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + + // Turn bare objects into model references, and prevent invalid models + // from being added. + var model, i; + for (i = 0; i < models.length; i++) { + model = models[i]; + + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + var existing = this.get(model); + if (existing) { + if (merge && model !== existing) { + var attrs = this._isModel(model) ? model.attributes : model; + if (options.parse) attrs = existing.parse(attrs, options); + existing.set(attrs, options); + toMerge.push(existing); + if (sortable && !sort) sort = existing.hasChanged(sortAttr); + } + if (!modelMap[existing.cid]) { + modelMap[existing.cid] = true; + set.push(existing); + } + models[i] = existing; + + // If this is a new, valid model, push it to the `toAdd` list. + } else if (add) { + model = models[i] = this._prepareModel(model, options); + if (model) { + toAdd.push(model); + this._addReference(model, options); + modelMap[model.cid] = true; + set.push(model); + } + } + } + + // Remove stale models. + if (remove) { + for (i = 0; i < this.length; i++) { + model = this.models[i]; + if (!modelMap[model.cid]) toRemove.push(model); + } + if (toRemove.length) this._removeModels(toRemove, options); + } + + // See if sorting is needed, update `length` and splice in new models. + var orderChanged = false; + var replace = !sortable && add && remove; + if (set.length && replace) { + orderChanged = this.length !== set.length || _.some(this.models, function(m, index) { + return m !== set[index]; + }); + this.models.length = 0; + splice(this.models, set, 0); + this.length = this.models.length; + } else if (toAdd.length) { + if (sortable) sort = true; + splice(this.models, toAdd, at == null ? this.length : at); + this.length = this.models.length; + } + + // Silently sort the collection if appropriate. + if (sort) this.sort({silent: true}); + + // Unless silenced, it's time to fire all appropriate add/sort/update events. + if (!options.silent) { + for (i = 0; i < toAdd.length; i++) { + if (at != null) options.index = at + i; + model = toAdd[i]; + model.trigger('add', model, this, options); + } + if (sort || orderChanged) this.trigger('sort', this, options); + if (toAdd.length || toRemove.length || toMerge.length) { + options.changes = { + added: toAdd, + removed: toRemove, + merged: toMerge + }; + this.trigger('update', this, options); + } + } + + // Return the added (or merged) model (or models). + return singular ? models[0] : models; + }, + + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options = options ? _.clone(options) : {}; + for (var i = 0; i < this.models.length; i++) { + this._removeReference(this.models[i], options); + } + options.previousModels = this.models; + this._reset(); + models = this.add(models, _.extend({silent: true}, options)); + if (!options.silent) this.trigger('reset', this, options); + return models; + }, + + // Add a model to the end of the collection. + push: function(model, options) { + return this.add(model, _.extend({at: this.length}, options)); + }, + + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + return this.remove(model, options); + }, + + // Add a model to the beginning of the collection. + unshift: function(model, options) { + return this.add(model, _.extend({at: 0}, options)); + }, + + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + return this.remove(model, options); + }, + + // Slice out a sub-array of models from the collection. + slice: function() { + return slice.apply(this.models, arguments); + }, + + // Get a model from the set by id, cid, model object with id or cid + // properties, or an attributes object that is transformed through modelId. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj] || + this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj)] || + obj.cid && this._byId[obj.cid]; + }, + + // Returns `true` if the model is in the collection. + has: function(obj) { + return this.get(obj) != null; + }, + + // Get the model at the given index. + at: function(index) { + if (index < 0) index += this.length; + return this.models[index]; + }, + + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + return this[first ? 'find' : 'filter'](attrs); + }, + + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + var comparator = this.comparator; + if (!comparator) throw new Error('Cannot sort a set without a comparator'); + options || (options = {}); + + var length = comparator.length; + if (_.isFunction(comparator)) comparator = comparator.bind(this); + + // Run sort based on type of `comparator`. + if (length === 1 || _.isString(comparator)) { + this.models = this.sortBy(comparator); + } else { + this.models.sort(comparator); + } + if (!options.silent) this.trigger('sort', this, options); + return this; + }, + + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return this.map(attr + ''); + }, + + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = _.extend({parse: true}, options); + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? 'reset' : 'set'; + collection[method](resp, options); + if (success) success.call(options.context, collection, resp, options); + collection.trigger('sync', collection, resp, options); + }; + wrapError(this, options); + return this.sync('read', this, options); + }, + + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + var wait = options.wait; + model = this._prepareModel(model, options); + if (!model) return false; + if (!wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(m, resp, callbackOpts) { + if (wait) collection.add(m, callbackOpts); + if (success) success.call(callbackOpts.context, m, resp, callbackOpts); + }; + model.save(null, options); + return model; + }, + + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models, { + model: this.model, + comparator: this.comparator + }); + }, + + // Define how to uniquely identify models in the collection. + modelId: function(attrs) { + return attrs[this.model.prototype.idAttribute || 'id']; + }, + + // Get an iterator of all models in this collection. + values: function() { + return new CollectionIterator(this, ITERATOR_VALUES); + }, + + // Get an iterator of all model IDs in this collection. + keys: function() { + return new CollectionIterator(this, ITERATOR_KEYS); + }, + + // Get an iterator of all [ID, model] tuples in this collection. + entries: function() { + return new CollectionIterator(this, ITERATOR_KEYSVALUES); + }, + + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (this._isModel(attrs)) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options = options ? _.clone(options) : {}; + options.collection = this; + var model = new this.model(attrs, options); + if (!model.validationError) return model; + this.trigger('invalid', this, model.validationError, options); + return false; + }, + + // Internal method called by both remove and set. + _removeModels: function(models, options) { + var removed = []; + for (var i = 0; i < models.length; i++) { + var model = this.get(models[i]); + if (!model) continue; + + var index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + + // Remove references before triggering 'remove' event to prevent an + // infinite loop. #3693 + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + + if (!options.silent) { + options.index = index; + model.trigger('remove', model, this, options); + } + + removed.push(model); + this._removeReference(model, options); + } + return removed; + }, + + // Method for checking whether an object should be considered a model for + // the purposes of adding to the collection. + _isModel: function(model) { + return model instanceof Model; + }, + + // Internal method to create a model's ties to a collection. + _addReference: function(model, options) { + this._byId[model.cid] = model; + var id = this.modelId(model.attributes); + if (id != null) this._byId[id] = model; + model.on('all', this._onModelEvent, this); + }, + + // Internal method to sever a model's ties to a collection. + _removeReference: function(model, options) { + delete this._byId[model.cid]; + var id = this.modelId(model.attributes); + if (id != null) delete this._byId[id]; + if (this === model.collection) delete model.collection; + model.off('all', this._onModelEvent, this); + }, + + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if (model) { + if ((event === 'add' || event === 'remove') && collection !== this) return; + if (event === 'destroy') this.remove(model, options); + if (event === 'change') { + var prevId = this.modelId(model.previousAttributes()); + var id = this.modelId(model.attributes); + if (prevId !== id) { + if (prevId != null) delete this._byId[prevId]; + if (id != null) this._byId[id] = model; + } + } + } + this.trigger.apply(this, arguments); + } + + }); + + // Defining an @@iterator method implements JavaScript's Iterable protocol. + // In modern ES2015 browsers, this value is found at Symbol.iterator. + /* global Symbol */ + var $$iterator = typeof Symbol === 'function' && Symbol.iterator; + if ($$iterator) { + Collection.prototype[$$iterator] = Collection.prototype.values; + } + + // CollectionIterator + // ------------------ + + // A CollectionIterator implements JavaScript's Iterator protocol, allowing the + // use of `for of` loops in modern browsers and interoperation between + // Backbone.Collection and other JavaScript functions and third-party libraries + // which can operate on Iterables. + var CollectionIterator = function(collection, kind) { + this._collection = collection; + this._kind = kind; + this._index = 0; + }; + + // This "enum" defines the three possible kinds of values which can be emitted + // by a CollectionIterator that correspond to the values(), keys() and entries() + // methods on Collection, respectively. + var ITERATOR_VALUES = 1; + var ITERATOR_KEYS = 2; + var ITERATOR_KEYSVALUES = 3; + + // All Iterators should themselves be Iterable. + if ($$iterator) { + CollectionIterator.prototype[$$iterator] = function() { + return this; + }; + } + + CollectionIterator.prototype.next = function() { + if (this._collection) { + + // Only continue iterating if the iterated collection is long enough. + if (this._index < this._collection.length) { + var model = this._collection.at(this._index); + this._index++; + + // Construct a value depending on what kind of values should be iterated. + var value; + if (this._kind === ITERATOR_VALUES) { + value = model; + } else { + var id = this._collection.modelId(model.attributes); + if (this._kind === ITERATOR_KEYS) { + value = id; + } else { // ITERATOR_KEYSVALUES + value = [id, model]; + } + } + return {value: value, done: false}; + } + + // Once exhausted, remove the reference to the collection so future + // calls to the next method always return done. + this._collection = void 0; + } + + return {value: void 0, done: true}; + }; + + // Backbone.View + // ------------- + + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId('view'); + this.preinitialize.apply(this, arguments); + _.extend(this, _.pick(options, viewOptions)); + this._ensureElement(); + this.initialize.apply(this, arguments); + }; + + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + + // List of view options to be set as properties. + var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events']; + + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + + // The default `tagName` of a View's element is `"div"`. + tagName: 'div', + + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be preferred to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the View + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this._removeElement(); + this.stopListening(); + return this; + }, + + // Remove this view's element from the document and all event listeners + // attached to it. Exposed for subclasses using an alternative DOM + // manipulation API. + _removeElement: function() { + this.$el.remove(); + }, + + // Change the view's element (`this.el` property) and re-delegate the + // view's events on the new element. + setElement: function(element) { + this.undelegateEvents(); + this._setElement(element); + this.delegateEvents(); + return this; + }, + + // Creates the `this.el` and `this.$el` references for this view using the + // given `el`. `el` can be a CSS selector or an HTML string, a jQuery + // context or an element. Subclasses can override this to utilize an + // alternative DOM manipulation API and are only required to set the + // `this.el` property. + _setElement: function(el) { + this.$el = el instanceof Backbone.$ ? el : Backbone.$(el); + this.el = this.$el[0]; + }, + + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save', + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + delegateEvents: function(events) { + events || (events = _.result(this, 'events')); + if (!events) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[method]; + if (!method) continue; + var match = key.match(delegateEventSplitter); + this.delegate(match[1], match[2], method.bind(this)); + } + return this; + }, + + // Add a single event listener to the view's element (or a child element + // using `selector`). This only works for delegate-able events: not `focus`, + // `blur`, and not `change`, `submit`, and `reset` in Internet Explorer. + delegate: function(eventName, selector, listener) { + this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Clears all callbacks previously bound to the view by `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + if (this.$el) this.$el.off('.delegateEvents' + this.cid); + return this; + }, + + // A finer-grained `undelegateEvents` for removing a single delegated event. + // `selector` and `listener` are both optional. + undelegate: function(eventName, selector, listener) { + this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener); + return this; + }, + + // Produces a DOM element to be assigned to your view. Exposed for + // subclasses using an alternative DOM manipulation API. + _createElement: function(tagName) { + return document.createElement(tagName); + }, + + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, 'attributes')); + if (this.id) attrs.id = _.result(this, 'id'); + if (this.className) attrs['class'] = _.result(this, 'className'); + this.setElement(this._createElement(_.result(this, 'tagName'))); + this._setAttributes(attrs); + } else { + this.setElement(_.result(this, 'el')); + } + }, + + // Set attributes from a hash on this view's element. Exposed for + // subclasses using an alternative DOM manipulation API. + _setAttributes: function(attributes) { + this.$el.attr(attributes); + } + + }); + + // Proxy Backbone class methods to Underscore functions, wrapping the model's + // `attributes` object or collection's `models` array behind the scenes. + // + // collection.filter(function(model) { return model.get('age') > 10 }); + // collection.each(this.addView); + // + // `Function#apply` can be slow so we use the method's arg count, if we know it. + var addMethod = function(base, length, method, attribute) { + switch (length) { + case 1: return function() { + return base[method](this[attribute]); + }; + case 2: return function(value) { + return base[method](this[attribute], value); + }; + case 3: return function(iteratee, context) { + return base[method](this[attribute], cb(iteratee, this), context); + }; + case 4: return function(iteratee, defaultVal, context) { + return base[method](this[attribute], cb(iteratee, this), defaultVal, context); + }; + default: return function() { + var args = slice.call(arguments); + args.unshift(this[attribute]); + return base[method].apply(base, args); + }; + } + }; + + var addUnderscoreMethods = function(Class, base, methods, attribute) { + _.each(methods, function(length, method) { + if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute); + }); + }; + + // Support `collection.sortBy('attr')` and `collection.findWhere({id: 1})`. + var cb = function(iteratee, instance) { + if (_.isFunction(iteratee)) return iteratee; + if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee); + if (_.isString(iteratee)) return function(model) { return model.get(iteratee); }; + return iteratee; + }; + var modelMatcher = function(attrs) { + var matcher = _.matches(attrs); + return function(model) { + return matcher(model.attributes); + }; + }; + + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0, + foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3, + select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3, + contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3, + head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3, + without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3, + isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3, + sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3}; + + + // Underscore methods that we want to implement on the Model, mapped to the + // number of arguments they take. + var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0, + omit: 0, chain: 1, isEmpty: 1}; + + // Mix in each Underscore method as a proxy to `Collection#models`. + + _.each([ + [Collection, collectionMethods, 'models'], + [Model, modelMethods, 'attributes'] + ], function(config) { + var Base = config[0], + methods = config[1], + attribute = config[2]; + + Base.mixin = function(obj) { + var mappings = _.reduce(_.functions(obj), function(memo, name) { + memo[name] = 0; + return memo; + }, {}); + addUnderscoreMethods(Base, obj, mappings, attribute); + }; + + addUnderscoreMethods(Base, _, methods, attribute); + }); + + // Backbone.sync + // ------------- + + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + + // Default JSON-request options. + var params = {type: type, dataType: 'json'}; + + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, 'url') || urlError(); + } + + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { + params.contentType = 'application/json'; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = 'application/x-www-form-urlencoded'; + params.data = params.data ? {model: params.data} : {}; + } + + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { + params.type = 'POST'; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader('X-HTTP-Method-Override', type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + + // Don't process data on a non-GET request. + if (params.type !== 'GET' && !options.emulateJSON) { + params.processData = false; + } + + // Pass along `textStatus` and `errorThrown` from jQuery. + var error = options.error; + options.error = function(xhr, textStatus, errorThrown) { + options.textStatus = textStatus; + options.errorThrown = errorThrown; + if (error) error.call(options.context, xhr, textStatus, errorThrown); + }; + + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger('request', model, xhr, options); + return xhr; + }; + + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + create: 'POST', + update: 'PUT', + patch: 'PATCH', + delete: 'DELETE', + read: 'GET' + }; + + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + + // Backbone.Router + // --------------- + + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + this.preinitialize.apply(this, arguments); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + + // preinitialize is an empty function by default. You can override it with a function + // or object. preinitialize will run before any instantiation logic is run in the Router. + preinitialize: function(){}, + + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function(){}, + + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ''; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + if (router.execute(callback, args, name) !== false) { + router.trigger.apply(router, ['route:' + name].concat(args)); + router.trigger('route', name, args); + Backbone.history.trigger('route', router, name, args); + } + }); + return this; + }, + + // Execute a route handler with the provided parameters. This is an + // excellent place to do pre-route setup or post-route cleanup. + execute: function(callback, args, name) { + if (callback) callback.apply(this, args); + }, + + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, 'routes'); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, '\\$&') + .replace(optionalParam, '(?:$1)?') + .replace(namedParam, function(match, optional) { + return optional ? match : '([^/?]+)'; + }) + .replace(splatParam, '([^?]*?)'); + return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'); + }, + + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param, i) { + // Don't decode the search params. + if (i === params.length - 1) return param || null; + return param ? decodeURIComponent(param) : null; + }); + } + + }); + + // Backbone.History + // ---------------- + + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + this.checkUrl = this.checkUrl.bind(this); + + // Ensure that `History` can be used outside of the browser. + if (typeof window !== 'undefined') { + this.location = window.location; + this.history = window.history; + } + }; + + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + + // Cached regex for stripping urls of hash. + var pathStripper = /#.*$/; + + // Has the history handling already been started? + History.started = false; + + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + + // Are we at the app root? + atRoot: function() { + var path = this.location.pathname.replace(/[^\/]$/, '$&/'); + return path === this.root && !this.getSearch(); + }, + + // Does the pathname match the root? + matchRoot: function() { + var path = this.decodeFragment(this.location.pathname); + var rootPath = path.slice(0, this.root.length - 1) + '/'; + return rootPath === this.root; + }, + + // Unicode characters in `location.pathname` are percent encoded so they're + // decoded for comparison. `%25` should not be decoded since it may be part + // of an encoded parameter. + decodeFragment: function(fragment) { + return decodeURI(fragment.replace(/%25/g, '%2525')); + }, + + // In IE6, the hash fragment and search params are incorrect if the + // fragment contains `?`. + getSearch: function() { + var match = this.location.href.replace(/#.*/, '').match(/\?.+/); + return match ? match[0] : ''; + }, + + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ''; + }, + + // Get the pathname and search params, without the root. + getPath: function() { + var path = this.decodeFragment( + this.location.pathname + this.getSearch() + ).slice(this.root.length - 1); + return path.charAt(0) === '/' ? path.slice(1) : path; + }, + + // Get the cross-browser normalized URL fragment from the path or hash. + getFragment: function(fragment) { + if (fragment == null) { + if (this._usePushState || !this._wantsHashChange) { + fragment = this.getPath(); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ''); + }, + + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error('Backbone.history has already been started'); + History.started = true; + + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({root: '/'}, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7); + this._useHashChange = this._wantsHashChange && this._hasHashChange; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.history && this.history.pushState); + this._usePushState = this._wantsPushState && this._hasPushState; + this.fragment = this.getFragment(); + + // Normalize root to always include a leading and trailing slash. + this.root = ('/' + this.root + '/').replace(rootStripper, '/'); + + // Transition from hashChange to pushState or vice versa if both are + // requested. + if (this._wantsHashChange && this._wantsPushState) { + + // If we've started off with a route from a `pushState`-enabled + // browser, but we're currently in a browser that doesn't support it... + if (!this._hasPushState && !this.atRoot()) { + var rootPath = this.root.slice(0, -1) || '/'; + this.location.replace(rootPath + '#' + this.getPath()); + // Return immediately as browser will do redirect to new url + return true; + + // Or if we've started out with a hash-based route, but we're currently + // in a browser where it could be `pushState`-based instead... + } else if (this._hasPushState && this.atRoot()) { + this.navigate(this.getHash(), {replace: true}); + } + + } + + // Proxy an iframe to handle location events if the browser doesn't + // support the `hashchange` event, HTML5 history, or the user wants + // `hashChange` but not `pushState`. + if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) { + this.iframe = document.createElement('iframe'); + this.iframe.src = 'javascript:0'; + this.iframe.style.display = 'none'; + this.iframe.tabIndex = -1; + var body = document.body; + // Using `appendChild` will throw on IE < 9 if the document is not ready. + var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow; + iWindow.document.open(); + iWindow.document.close(); + iWindow.location.hash = '#' + this.fragment; + } + + // Add a cross-platform `addEventListener` shim for older browsers. + var addEventListener = window.addEventListener || function(eventName, listener) { + return attachEvent('on' + eventName, listener); + }; + + // Depending on whether we're using pushState or hashes, and whether + // 'onhashchange' is supported, determine how we check the URL state. + if (this._usePushState) { + addEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + addEventListener('hashchange', this.checkUrl, false); + } else if (this._wantsHashChange) { + this._checkUrlInterval = setInterval(this.checkUrl, this.interval); + } + + if (!this.options.silent) return this.loadUrl(); + }, + + // Disable Backbone.history, perhaps temporarily. Not useful in a real app, + // but possibly useful for unit testing Routers. + stop: function() { + // Add a cross-platform `removeEventListener` shim for older browsers. + var removeEventListener = window.removeEventListener || function(eventName, listener) { + return detachEvent('on' + eventName, listener); + }; + + // Remove window listeners. + if (this._usePushState) { + removeEventListener('popstate', this.checkUrl, false); + } else if (this._useHashChange && !this.iframe) { + removeEventListener('hashchange', this.checkUrl, false); + } + + // Clean up the iframe if necessary. + if (this.iframe) { + document.body.removeChild(this.iframe); + this.iframe = null; + } + + // Some environments will throw when clearing an undefined interval. + if (this._checkUrlInterval) clearInterval(this._checkUrlInterval); + History.started = false; + }, + + // Add a route to be tested when the fragment changes. Routes added later + // may override previous routes. + route: function(route, callback) { + this.handlers.unshift({route: route, callback: callback}); + }, + + // Checks the current URL to see if it has changed, and if it has, + // calls `loadUrl`, normalizing across the hidden iframe. + checkUrl: function(e) { + var current = this.getFragment(); + + // If the user pressed the back button, the iframe's hash will have + // changed and we should use that for comparison. + if (current === this.fragment && this.iframe) { + current = this.getHash(this.iframe.contentWindow); + } + + if (current === this.fragment) return false; + if (this.iframe) this.navigate(current); + this.loadUrl(); + }, + + // Attempt to load the current URL fragment. If a route succeeds with a + // match, returns `true`. If no defined routes matches the fragment, + // returns `false`. + loadUrl: function(fragment) { + // If the root doesn't match, no routes can match either. + if (!this.matchRoot()) return false; + fragment = this.fragment = this.getFragment(fragment); + return _.some(this.handlers, function(handler) { + if (handler.route.test(fragment)) { + handler.callback(fragment); + return true; + } + }); + }, + + // Save a fragment into the hash history, or replace the URL state if the + // 'replace' option is passed. You are responsible for properly URL-encoding + // the fragment in advance. + // + // The options object can contain `trigger: true` if you wish to have the + // route callback be fired (not usually desirable), or `replace: true`, if + // you wish to modify the current URL without adding an entry to the history. + navigate: function(fragment, options) { + if (!History.started) return false; + if (!options || options === true) options = {trigger: !!options}; + + // Normalize the fragment. + fragment = this.getFragment(fragment || ''); + + // Don't include a trailing slash on the root. + var rootPath = this.root; + if (fragment === '' || fragment.charAt(0) === '?') { + rootPath = rootPath.slice(0, -1) || '/'; + } + var url = rootPath + fragment; + + // Strip the fragment of the query and hash for matching. + fragment = fragment.replace(pathStripper, ''); + + // Decode for matching. + var decodedFragment = this.decodeFragment(fragment); + + if (this.fragment === decodedFragment) return; + this.fragment = decodedFragment; + + // If pushState is available, we use it to set the fragment as a real URL. + if (this._usePushState) { + this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url); + + // If hash changes haven't been explicitly disabled, update the hash + // fragment to store history. + } else if (this._wantsHashChange) { + this._updateHash(this.location, fragment, options.replace); + if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) { + var iWindow = this.iframe.contentWindow; + + // Opening and closing the iframe tricks IE7 and earlier to push a + // history entry on hash-tag change. When replace is true, we don't + // want this. + if (!options.replace) { + iWindow.document.open(); + iWindow.document.close(); + } + + this._updateHash(iWindow.location, fragment, options.replace); + } + + // If you've told us that you explicitly don't want fallback hashchange- + // based history, then `navigate` becomes a page refresh. + } else { + return this.location.assign(url); + } + if (options.trigger) return this.loadUrl(fragment); + }, + + // Update the hash location, either replacing the current entry, or adding + // a new one to the browser history. + _updateHash: function(location, fragment, replace) { + if (replace) { + var href = location.href.replace(/(javascript:|#).*$/, ''); + location.replace(href + '#' + fragment); + } else { + // Some browsers require that `hash` contains a leading #. + location.hash = '#' + fragment; + } + } + + }); + + // Create the default Backbone.history. + Backbone.history = new History; + + // Helpers + // ------- + + // Helper function to correctly set up the prototype chain for subclasses. + // Similar to `goog.inherits`, but uses a hash of prototype properties and + // class properties to be extended. + var extend = function(protoProps, staticProps) { + var parent = this; + var child; + + // The constructor function for the new subclass is either defined by you + // (the "constructor" property in your `extend` definition), or defaulted + // by us to simply call the parent constructor. + if (protoProps && _.has(protoProps, 'constructor')) { + child = protoProps.constructor; + } else { + child = function(){ return parent.apply(this, arguments); }; + } + + // Add static properties to the constructor function, if supplied. + _.extend(child, parent, staticProps); + + // Set the prototype chain to inherit from `parent`, without calling + // `parent`'s constructor function and add the prototype properties. + child.prototype = _.create(parent.prototype, protoProps); + child.prototype.constructor = child; + + // Set a convenience property in case the parent's prototype is needed + // later. + child.__super__ = parent.prototype; + + return child; + }; + + // Set up inheritance for the model, collection, router, view and history. + Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; + + // Throw an error when a URL is needed, and none is supplied. + var urlError = function() { + throw new Error('A "url" property or function must be specified'); + }; + + // Wrap an optional error callback with a fallback error event. + var wrapError = function(model, options) { + var error = options.error; + options.error = function(resp) { + if (error) error.call(options.context, model, resp, options); + model.trigger('error', model, resp, options); + }; + }; + + return Backbone; +}); diff --git a/kfet/static/kfet/src/underscore.js b/kfet/static/kfet/src/underscore.js new file mode 100644 index 00000000..8219dc50 --- /dev/null +++ b/kfet/static/kfet/src/underscore.js @@ -0,0 +1,1692 @@ +// Underscore.js 1.9.1 +// http://underscorejs.org +// (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` (`self`) in the browser, `global` + // on the server, or `this` in some virtual machines. We use `self` + // instead of `window` for `WebWorker` support. + var root = typeof self == 'object' && self.self === self && self || + typeof global == 'object' && global.global === global && global || + this || + {}; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype; + var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null; + + // Create quick reference variables for speed access to core prototypes. + var push = ArrayProto.push, + slice = ArrayProto.slice, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeCreate = Object.create; + + // Naked function reference for surrogate-prototype-swapping. + var Ctor = function(){}; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for their old module API. If we're in + // the browser, add `_` as a global object. + // (`nodeType` is checked to ensure that `module` + // and `exports` are not HTML elements.) + if (typeof exports != 'undefined' && !exports.nodeType) { + if (typeof module != 'undefined' && !module.nodeType && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.9.1'; + + // Internal function that returns an efficient (for current engines) version + // of the passed-in callback, to be repeatedly applied in other Underscore + // functions. + var optimizeCb = function(func, context, argCount) { + if (context === void 0) return func; + switch (argCount == null ? 3 : argCount) { + case 1: return function(value) { + return func.call(context, value); + }; + // The 2-argument case is omitted because we’re not using it. + case 3: return function(value, index, collection) { + return func.call(context, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(context, accumulator, value, index, collection); + }; + } + return function() { + return func.apply(context, arguments); + }; + }; + + var builtinIteratee; + + // An internal function to generate callbacks that can be applied to each + // element in a collection, returning the desired result — either `identity`, + // an arbitrary callback, a property matcher, or a property accessor. + var cb = function(value, context, argCount) { + if (_.iteratee !== builtinIteratee) return _.iteratee(value, context); + if (value == null) return _.identity; + if (_.isFunction(value)) return optimizeCb(value, context, argCount); + if (_.isObject(value) && !_.isArray(value)) return _.matcher(value); + return _.property(value); + }; + + // External wrapper for our callback generator. Users may customize + // `_.iteratee` if they want additional predicate/iteratee shorthand styles. + // This abstraction hides the internal-only argCount argument. + _.iteratee = builtinIteratee = function(value, context) { + return cb(value, context, Infinity); + }; + + // Some functions take a variable number of arguments, or a few expected + // arguments at the beginning and then a variable number of values to operate + // on. This helper accumulates all remaining arguments past the function’s + // argument length (or an explicit `startIndex`), into an array that becomes + // the last argument. Similar to ES6’s "rest parameter". + var restArguments = function(func, startIndex) { + startIndex = startIndex == null ? func.length - 1 : +startIndex; + return function() { + var length = Math.max(arguments.length - startIndex, 0), + rest = Array(length), + index = 0; + for (; index < length; index++) { + rest[index] = arguments[index + startIndex]; + } + switch (startIndex) { + case 0: return func.call(this, rest); + case 1: return func.call(this, arguments[0], rest); + case 2: return func.call(this, arguments[0], arguments[1], rest); + } + var args = Array(startIndex + 1); + for (index = 0; index < startIndex; index++) { + args[index] = arguments[index]; + } + args[startIndex] = rest; + return func.apply(this, args); + }; + }; + + // An internal function for creating a new object that inherits from another. + var baseCreate = function(prototype) { + if (!_.isObject(prototype)) return {}; + if (nativeCreate) return nativeCreate(prototype); + Ctor.prototype = prototype; + var result = new Ctor; + Ctor.prototype = null; + return result; + }; + + var shallowProperty = function(key) { + return function(obj) { + return obj == null ? void 0 : obj[key]; + }; + }; + + var has = function(obj, path) { + return obj != null && hasOwnProperty.call(obj, path); + } + + var deepGet = function(obj, path) { + var length = path.length; + for (var i = 0; i < length; i++) { + if (obj == null) return void 0; + obj = obj[path[i]]; + } + return length ? obj : void 0; + }; + + // Helper for collection methods to determine whether a collection + // should be iterated as an array or as an object. + // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength + // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 + var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; + var getLength = shallowProperty('length'); + var isArrayLike = function(collection) { + var length = getLength(collection); + return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; + }; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles raw objects in addition to array-likes. Treats all + // sparse array-likes as if they were dense. + _.each = _.forEach = function(obj, iteratee, context) { + iteratee = optimizeCb(iteratee, context); + var i, length; + if (isArrayLike(obj)) { + for (i = 0, length = obj.length; i < length; i++) { + iteratee(obj[i], i, obj); + } + } else { + var keys = _.keys(obj); + for (i = 0, length = keys.length; i < length; i++) { + iteratee(obj[keys[i]], keys[i], obj); + } + } + return obj; + }; + + // Return the results of applying the iteratee to each element. + _.map = _.collect = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + results = Array(length); + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + results[index] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Create a reducing function iterating left or right. + var createReduce = function(dir) { + // Wrap code that reassigns argument variables in a separate function than + // the one that accesses `arguments.length` to avoid a perf hit. (#1991) + var reducer = function(obj, iteratee, memo, initial) { + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length, + index = dir > 0 ? 0 : length - 1; + if (!initial) { + memo = obj[keys ? keys[index] : index]; + index += dir; + } + for (; index >= 0 && index < length; index += dir) { + var currentKey = keys ? keys[index] : index; + memo = iteratee(memo, obj[currentKey], currentKey, obj); + } + return memo; + }; + + return function(obj, iteratee, memo, context) { + var initial = arguments.length >= 3; + return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial); + }; + }; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. + _.reduce = _.foldl = _.inject = createReduce(1); + + // The right-associative version of reduce, also known as `foldr`. + _.reduceRight = _.foldr = createReduce(-1); + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var keyFinder = isArrayLike(obj) ? _.findIndex : _.findKey; + var key = keyFinder(obj, predicate, context); + if (key !== void 0 && key !== -1) return obj[key]; + }; + + // Return all the elements that pass a truth test. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + predicate = cb(predicate, context); + _.each(obj, function(value, index, list) { + if (predicate(value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, _.negate(cb(predicate)), context); + }; + + // Determine whether all of the elements match a truth test. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (!predicate(obj[currentKey], currentKey, obj)) return false; + } + return true; + }; + + // Determine if at least one element in the object matches a truth test. + // Aliased as `any`. + _.some = _.any = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = !isArrayLike(obj) && _.keys(obj), + length = (keys || obj).length; + for (var index = 0; index < length; index++) { + var currentKey = keys ? keys[index] : index; + if (predicate(obj[currentKey], currentKey, obj)) return true; + } + return false; + }; + + // Determine if the array or object contains a given item (using `===`). + // Aliased as `includes` and `include`. + _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + if (typeof fromIndex != 'number' || guard) fromIndex = 0; + return _.indexOf(obj, item, fromIndex) >= 0; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = restArguments(function(obj, path, args) { + var contextPath, func; + if (_.isFunction(path)) { + func = path; + } else if (_.isArray(path)) { + contextPath = path.slice(0, -1); + path = path[path.length - 1]; + } + return _.map(obj, function(context) { + var method = func; + if (!method) { + if (contextPath && contextPath.length) { + context = deepGet(context, contextPath); + } + if (context == null) return void 0; + method = context[path]; + } + return method == null ? method : method.apply(context, args); + }); + }); + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matcher(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matcher(attrs)); + }; + + // Return the maximum element (or element-based computation). + _.max = function(obj, iteratee, context) { + var result = -Infinity, lastComputed = -Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value > result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed > lastComputed || computed === -Infinity && result === -Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iteratee, context) { + var result = Infinity, lastComputed = Infinity, + value, computed; + if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) { + obj = isArrayLike(obj) ? obj : _.values(obj); + for (var i = 0, length = obj.length; i < length; i++) { + value = obj[i]; + if (value != null && value < result) { + result = value; + } + } + } else { + iteratee = cb(iteratee, context); + _.each(obj, function(v, index, list) { + computed = iteratee(v, index, list); + if (computed < lastComputed || computed === Infinity && result === Infinity) { + result = v; + lastComputed = computed; + } + }); + } + return result; + }; + + // Shuffle a collection. + _.shuffle = function(obj) { + return _.sample(obj, Infinity); + }; + + // Sample **n** random values from a collection using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (!isArrayLike(obj)) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj); + var length = getLength(sample); + n = Math.max(Math.min(n, length), 0); + var last = length - 1; + for (var index = 0; index < n; index++) { + var rand = _.random(index, last); + var temp = sample[index]; + sample[index] = sample[rand]; + sample[rand] = temp; + } + return sample.slice(0, n); + }; + + // Sort the object's values by a criterion produced by an iteratee. + _.sortBy = function(obj, iteratee, context) { + var index = 0; + iteratee = cb(iteratee, context); + return _.pluck(_.map(obj, function(value, key, list) { + return { + value: value, + index: index++, + criteria: iteratee(value, key, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior, partition) { + return function(obj, iteratee, context) { + var result = partition ? [[], []] : {}; + iteratee = cb(iteratee, context); + _.each(obj, function(value, index) { + var key = iteratee(value, index, obj); + behavior(result, value, key); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, value, key) { + if (has(result, key)) result[key].push(value); else result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, value, key) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, value, key) { + if (has(result, key)) result[key]++; else result[key] = 1; + }); + + var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g; + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (_.isString(obj)) { + // Keep surrogate pair characters together + return obj.match(reStrSymbol); + } + if (isArrayLike(obj)) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return isArrayLike(obj) ? obj.length : _.keys(obj).length; + }; + + // Split a collection into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = group(function(result, value, pass) { + result[pass ? 0 : 1].push(value); + }, true); + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null || array.length < 1) return n == null ? void 0 : []; + if (n == null || guard) return array[0]; + return _.initial(array, array.length - n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. + _.initial = function(array, n, guard) { + return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. + _.last = function(array, n, guard) { + if (array == null || array.length < 1) return n == null ? void 0 : []; + if (n == null || guard) return array[array.length - 1]; + return _.rest(array, Math.max(0, array.length - n)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, n == null || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, Boolean); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, strict, output) { + output = output || []; + var idx = output.length; + for (var i = 0, length = getLength(input); i < length; i++) { + var value = input[i]; + if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { + // Flatten current level of array or arguments object. + if (shallow) { + var j = 0, len = value.length; + while (j < len) output[idx++] = value[j++]; + } else { + flatten(value, shallow, strict, output); + idx = output.length; + } + } else if (!strict) { + output[idx++] = value; + } + } + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, false); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = restArguments(function(array, otherArrays) { + return _.difference(array, otherArrays); + }); + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // The faster algorithm will not work with an iteratee if the iteratee + // is not a one-to-one function, so providing an iteratee will disable + // the faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iteratee, context) { + if (!_.isBoolean(isSorted)) { + context = iteratee; + iteratee = isSorted; + isSorted = false; + } + if (iteratee != null) iteratee = cb(iteratee, context); + var result = []; + var seen = []; + for (var i = 0, length = getLength(array); i < length; i++) { + var value = array[i], + computed = iteratee ? iteratee(value, i, array) : value; + if (isSorted && !iteratee) { + if (!i || seen !== computed) result.push(value); + seen = computed; + } else if (iteratee) { + if (!_.contains(seen, computed)) { + seen.push(computed); + result.push(value); + } + } else if (!_.contains(result, value)) { + result.push(value); + } + } + return result; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = restArguments(function(arrays) { + return _.uniq(flatten(arrays, true, true)); + }); + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var result = []; + var argsLength = arguments.length; + for (var i = 0, length = getLength(array); i < length; i++) { + var item = array[i]; + if (_.contains(result, item)) continue; + var j; + for (j = 1; j < argsLength; j++) { + if (!_.contains(arguments[j], item)) break; + } + if (j === argsLength) result.push(item); + } + return result; + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = restArguments(function(array, rest) { + rest = flatten(rest, true, true); + return _.filter(array, function(value){ + return !_.contains(rest, value); + }); + }); + + // Complement of _.zip. Unzip accepts an array of arrays and groups + // each array's elements on shared indices. + _.unzip = function(array) { + var length = array && _.max(array, getLength).length || 0; + var result = Array(length); + + for (var index = 0; index < length; index++) { + result[index] = _.pluck(array, index); + } + return result; + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = restArguments(_.unzip); + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. Passing by pairs is the reverse of _.pairs. + _.object = function(list, values) { + var result = {}; + for (var i = 0, length = getLength(list); i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // Generator function to create the findIndex and findLastIndex functions. + var createPredicateIndexFinder = function(dir) { + return function(array, predicate, context) { + predicate = cb(predicate, context); + var length = getLength(array); + var index = dir > 0 ? 0 : length - 1; + for (; index >= 0 && index < length; index += dir) { + if (predicate(array[index], index, array)) return index; + } + return -1; + }; + }; + + // Returns the first index on an array-like that passes a predicate test. + _.findIndex = createPredicateIndexFinder(1); + _.findLastIndex = createPredicateIndexFinder(-1); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iteratee, context) { + iteratee = cb(iteratee, context, 1); + var value = iteratee(obj); + var low = 0, high = getLength(array); + while (low < high) { + var mid = Math.floor((low + high) / 2); + if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; + } + return low; + }; + + // Generator function to create the indexOf and lastIndexOf functions. + var createIndexFinder = function(dir, predicateFind, sortedIndex) { + return function(array, item, idx) { + var i = 0, length = getLength(array); + if (typeof idx == 'number') { + if (dir > 0) { + i = idx >= 0 ? idx : Math.max(idx + length, i); + } else { + length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; + } + } else if (sortedIndex && idx && length) { + idx = sortedIndex(array, item); + return array[idx] === item ? idx : -1; + } + if (item !== item) { + idx = predicateFind(slice.call(array, i, length), _.isNaN); + return idx >= 0 ? idx + i : -1; + } + for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { + if (array[idx] === item) return idx; + } + return -1; + }; + }; + + // Return the position of the first occurrence of an item in an array, + // or -1 if the item is not included in the array. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); + _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (stop == null) { + stop = start || 0; + start = 0; + } + if (!step) { + step = stop < start ? -1 : 1; + } + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var range = Array(length); + + for (var idx = 0; idx < length; idx++, start += step) { + range[idx] = start; + } + + return range; + }; + + // Chunk a single array into multiple arrays, each containing `count` or fewer + // items. + _.chunk = function(array, count) { + if (count == null || count < 1) return []; + var result = []; + var i = 0, length = array.length; + while (i < length) { + result.push(slice.call(array, i, i += count)); + } + return result; + }; + + // Function (ahem) Functions + // ------------------ + + // Determines whether to execute a function as a constructor + // or a normal function with the provided arguments. + var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { + if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); + var self = baseCreate(sourceFunc.prototype); + var result = sourceFunc.apply(self, args); + if (_.isObject(result)) return result; + return self; + }; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = restArguments(function(func, context, args) { + if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); + var bound = restArguments(function(callArgs) { + return executeBound(func, bound, context, this, args.concat(callArgs)); + }); + return bound; + }); + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder by default, allowing any combination of arguments to be + // pre-filled. Set `_.partial.placeholder` for a custom placeholder argument. + _.partial = restArguments(function(func, boundArgs) { + var placeholder = _.partial.placeholder; + var bound = function() { + var position = 0, length = boundArgs.length; + var args = Array(length); + for (var i = 0; i < length; i++) { + args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i]; + } + while (position < arguments.length) args.push(arguments[position++]); + return executeBound(func, bound, this, this, args); + }; + return bound; + }); + + _.partial.placeholder = _; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = restArguments(function(obj, keys) { + keys = flatten(keys, false, false); + var index = keys.length; + if (index < 1) throw new Error('bindAll must be passed function names'); + while (index--) { + var key = keys[index]; + obj[key] = _.bind(obj[key], obj); + } + }); + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memoize = function(key) { + var cache = memoize.cache; + var address = '' + (hasher ? hasher.apply(this, arguments) : key); + if (!has(cache, address)) cache[address] = func.apply(this, arguments); + return cache[address]; + }; + memoize.cache = {}; + return memoize; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = restArguments(function(func, wait, args) { + return setTimeout(function() { + return func.apply(null, args); + }, wait); + }); + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = _.partial(_.delay, _, 1); + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var timeout, context, args, result; + var previous = 0; + if (!options) options = {}; + + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + if (!timeout) context = args = null; + }; + + var throttled = function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout); + timeout = null; + } + previous = now; + result = func.apply(context, args); + if (!timeout) context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + + throttled.cancel = function() { + clearTimeout(timeout); + previous = 0; + timeout = context = args = null; + }; + + return throttled; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, result; + + var later = function(context, args) { + timeout = null; + if (args) result = func.apply(context, args); + }; + + var debounced = restArguments(function(args) { + if (timeout) clearTimeout(timeout); + if (immediate) { + var callNow = !timeout; + timeout = setTimeout(later, wait); + if (callNow) result = func.apply(this, args); + } else { + timeout = _.delay(later, wait, this, args); + } + + return result; + }); + + debounced.cancel = function() { + clearTimeout(timeout); + timeout = null; + }; + + return debounced; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a negated version of the passed-in predicate. + _.negate = function(predicate) { + return function() { + return !predicate.apply(this, arguments); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var args = arguments; + var start = args.length - 1; + return function() { + var i = start; + var result = args[start].apply(this, arguments); + while (i--) result = args[i].call(this, result); + return result; + }; + }; + + // Returns a function that will only be executed on and after the Nth call. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Returns a function that will only be executed up to (but not including) the Nth call. + _.before = function(times, func) { + var memo; + return function() { + if (--times > 0) { + memo = func.apply(this, arguments); + } + if (times <= 1) func = null; + return memo; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = _.partial(_.before, 2); + + _.restArguments = restArguments; + + // Object Functions + // ---------------- + + // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. + var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); + var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', + 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; + + var collectNonEnumProps = function(obj, keys) { + var nonEnumIdx = nonEnumerableProps.length; + var constructor = obj.constructor; + var proto = _.isFunction(constructor) && constructor.prototype || ObjProto; + + // Constructor is a special case. + var prop = 'constructor'; + if (has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); + + while (nonEnumIdx--) { + prop = nonEnumerableProps[nonEnumIdx]; + if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { + keys.push(prop); + } + } + }; + + // Retrieve the names of an object's own properties. + // Delegates to **ECMAScript 5**'s native `Object.keys`. + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (has(obj, key)) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve all the property names of an object. + _.allKeys = function(obj) { + if (!_.isObject(obj)) return []; + var keys = []; + for (var key in obj) keys.push(key); + // Ahem, IE < 9. + if (hasEnumBug) collectNonEnumProps(obj, keys); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Returns the results of applying the iteratee to each element of the object. + // In contrast to _.map it returns an object. + _.mapObject = function(obj, iteratee, context) { + iteratee = cb(iteratee, context); + var keys = _.keys(obj), + length = keys.length, + results = {}; + for (var index = 0; index < length; index++) { + var currentKey = keys[index]; + results[currentKey] = iteratee(obj[currentKey], currentKey, obj); + } + return results; + }; + + // Convert an object into a list of `[key, value]` pairs. + // The opposite of _.object. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods`. + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // An internal function for creating assigner functions. + var createAssigner = function(keysFunc, defaults) { + return function(obj) { + var length = arguments.length; + if (defaults) obj = Object(obj); + if (length < 2 || obj == null) return obj; + for (var index = 1; index < length; index++) { + var source = arguments[index], + keys = keysFunc(source), + l = keys.length; + for (var i = 0; i < l; i++) { + var key = keys[i]; + if (!defaults || obj[key] === void 0) obj[key] = source[key]; + } + } + return obj; + }; + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = createAssigner(_.allKeys); + + // Assigns a given object with all the own properties in the passed-in object(s). + // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) + _.extendOwn = _.assign = createAssigner(_.keys); + + // Returns the first key on an object that passes a predicate test. + _.findKey = function(obj, predicate, context) { + predicate = cb(predicate, context); + var keys = _.keys(obj), key; + for (var i = 0, length = keys.length; i < length; i++) { + key = keys[i]; + if (predicate(obj[key], key, obj)) return key; + } + }; + + // Internal pick helper function to determine if `obj` has key `key`. + var keyInObj = function(value, key, obj) { + return key in obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = restArguments(function(obj, keys) { + var result = {}, iteratee = keys[0]; + if (obj == null) return result; + if (_.isFunction(iteratee)) { + if (keys.length > 1) iteratee = optimizeCb(iteratee, keys[1]); + keys = _.allKeys(obj); + } else { + iteratee = keyInObj; + keys = flatten(keys, false, false); + obj = Object(obj); + } + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + var value = obj[key]; + if (iteratee(value, key, obj)) result[key] = value; + } + return result; + }); + + // Return a copy of the object without the blacklisted properties. + _.omit = restArguments(function(obj, keys) { + var iteratee = keys[0], context; + if (_.isFunction(iteratee)) { + iteratee = _.negate(iteratee); + if (keys.length > 1) context = keys[1]; + } else { + keys = _.map(flatten(keys, false, false), String); + iteratee = function(value, key) { + return !_.contains(keys, key); + }; + } + return _.pick(obj, iteratee, context); + }); + + // Fill in a given object with default properties. + _.defaults = createAssigner(_.allKeys, true); + + // Creates an object that inherits from the given prototype object. + // If additional properties are provided then they will be added to the + // created object. + _.create = function(prototype, props) { + var result = baseCreate(prototype); + if (props) _.extendOwn(result, props); + return result; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Returns whether an object has a given set of `key:value` pairs. + _.isMatch = function(object, attrs) { + var keys = _.keys(attrs), length = keys.length; + if (object == null) return !length; + var obj = Object(object); + for (var i = 0; i < length; i++) { + var key = keys[i]; + if (attrs[key] !== obj[key] || !(key in obj)) return false; + } + return true; + }; + + + // Internal recursive comparison function for `isEqual`. + var eq, deepEq; + eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a === 1 / b; + // `null` or `undefined` only equal to itself (strict comparison). + if (a == null || b == null) return false; + // `NaN`s are equivalent, but non-reflexive. + if (a !== a) return b !== b; + // Exhaust primitive checks + var type = typeof a; + if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; + return deepEq(a, b, aStack, bStack); + }; + + // Internal recursive comparison function for `isEqual`. + deepEq = function(a, b, aStack, bStack) { + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className !== toString.call(b)) return false; + switch (className) { + // Strings, numbers, regular expressions, dates, and booleans are compared by value. + case '[object RegExp]': + // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return '' + a === '' + b; + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. + // Object(NaN) is equivalent to NaN. + if (+a !== +a) return +b !== +b; + // An `egal` comparison is performed for other numeric values. + return +a === 0 ? 1 / +a === 1 / b : +a === +b; + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a === +b; + case '[object Symbol]': + return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b); + } + + var areArrays = className === '[object Array]'; + if (!areArrays) { + if (typeof a != 'object' || typeof b != 'object') return false; + + // Objects with different constructors are not equivalent, but `Object`s or `Array`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && + _.isFunction(bCtor) && bCtor instanceof bCtor) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + } + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + + // Initializing stack of traversed objects. + // It's done here since we only need them for objects and arrays comparison. + aStack = aStack || []; + bStack = bStack || []; + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] === a) return bStack[length] === b; + } + + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + + // Recursively compare objects and arrays. + if (areArrays) { + // Compare array lengths to determine if a deep comparison is necessary. + length = a.length; + if (length !== b.length) return false; + // Deep compare the contents, ignoring non-numeric properties. + while (length--) { + if (!eq(a[length], b[length], aStack, bStack)) return false; + } + } else { + // Deep compare objects. + var keys = _.keys(a), key; + length = keys.length; + // Ensure that both objects contain the same number of properties before comparing deep equality. + if (_.keys(b).length !== length) return false; + while (length--) { + // Deep compare each member + key = keys[length]; + if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return true; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; + return _.keys(obj).length === 0; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + var type = typeof obj; + return type === 'function' || type === 'object' && !!obj; + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError, isMap, isWeakMap, isSet, isWeakSet. + _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error', 'Symbol', 'Map', 'WeakMap', 'Set', 'WeakSet'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) === '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE < 9), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return has(obj, 'callee'); + }; + } + + // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, + // IE 11 (#1621), Safari 8 (#1929), and PhantomJS (#2236). + var nodelist = root.document && root.document.childNodes; + if (typeof /./ != 'function' && typeof Int8Array != 'object' && typeof nodelist != 'function') { + _.isFunction = function(obj) { + return typeof obj == 'function' || false; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return !_.isSymbol(obj) && isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? + _.isNaN = function(obj) { + return _.isNumber(obj) && isNaN(obj); + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, path) { + if (!_.isArray(path)) { + return has(obj, path); + } + var length = path.length; + for (var i = 0; i < length; i++) { + var key = path[i]; + if (obj == null || !hasOwnProperty.call(obj, key)) { + return false; + } + obj = obj[key]; + } + return !!length; + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iteratees. + _.identity = function(value) { + return value; + }; + + // Predicate-generating functions. Often useful outside of Underscore. + _.constant = function(value) { + return function() { + return value; + }; + }; + + _.noop = function(){}; + + // Creates a function that, when passed an object, will traverse that object’s + // properties down the given `path`, specified as an array of keys or indexes. + _.property = function(path) { + if (!_.isArray(path)) { + return shallowProperty(path); + } + return function(obj) { + return deepGet(obj, path); + }; + }; + + // Generates a function for a given object that returns a given property. + _.propertyOf = function(obj) { + if (obj == null) { + return function(){}; + } + return function(path) { + return !_.isArray(path) ? obj[path] : deepGet(obj, path); + }; + }; + + // Returns a predicate for checking whether an object has a given set of + // `key:value` pairs. + _.matcher = _.matches = function(attrs) { + attrs = _.extendOwn({}, attrs); + return function(obj) { + return _.isMatch(obj, attrs); + }; + }; + + // Run a function **n** times. + _.times = function(n, iteratee, context) { + var accum = Array(Math.max(0, n)); + iteratee = optimizeCb(iteratee, context, 1); + for (var i = 0; i < n; i++) accum[i] = iteratee(i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { + return new Date().getTime(); + }; + + // List of HTML entities for escaping. + var escapeMap = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''', + '`': '`' + }; + var unescapeMap = _.invert(escapeMap); + + // Functions for escaping and unescaping strings to/from HTML interpolation. + var createEscaper = function(map) { + var escaper = function(match) { + return map[match]; + }; + // Regexes for identifying a key that needs to be escaped. + var source = '(?:' + _.keys(map).join('|') + ')'; + var testRegexp = RegExp(source); + var replaceRegexp = RegExp(source, 'g'); + return function(string) { + string = string == null ? '' : '' + string; + return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; + }; + }; + _.escape = createEscaper(escapeMap); + _.unescape = createEscaper(unescapeMap); + + // Traverses the children of `obj` along `path`. If a child is a function, it + // is invoked with its parent as context. Returns the value of the final + // child, or `fallback` if any child is undefined. + _.result = function(obj, path, fallback) { + if (!_.isArray(path)) path = [path]; + var length = path.length; + if (!length) { + return _.isFunction(fallback) ? fallback.call(obj) : fallback; + } + for (var i = 0; i < length; i++) { + var prop = obj == null ? void 0 : obj[path[i]]; + if (prop === void 0) { + prop = fallback; + i = length; // Ensure we don't continue iterating. + } + obj = _.isFunction(prop) ? prop.call(obj) : prop; + } + return obj; + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate: /<%([\s\S]+?)%>/g, + interpolate: /<%=([\s\S]+?)%>/g, + escape: /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; + + var escapeChar = function(match) { + return '\\' + escapes[match]; + }; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + // NB: `oldSettings` only exists for backwards compatibility. + _.template = function(text, settings, oldSettings) { + if (!settings && oldSettings) settings = oldSettings; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset).replace(escapeRegExp, escapeChar); + index = offset + match.length; + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } else if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } else if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + + // Adobe VMs need the match returned to produce the correct offset. + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + 'return __p;\n'; + + var render; + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled source as a convenience for precompilation. + var argument = settings.variable || 'obj'; + template.source = 'function(' + argument + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function. Start chaining a wrapped Underscore object. + _.chain = function(obj) { + var instance = _(obj); + instance._chain = true; + return instance; + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var chainResult = function(instance, obj) { + return instance._chain ? _(obj).chain() : obj; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + _.each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return chainResult(this, func.apply(_, args)); + }; + }); + return _; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; + return chainResult(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + _.each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return chainResult(this, method.apply(this._wrapped, arguments)); + }; + }); + + // Extracts the result from a wrapped and chained object. + _.prototype.value = function() { + return this._wrapped; + }; + + // Provide unwrapping proxy for some methods used in engine operations + // such as arithmetic and JSON stringification. + _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; + + _.prototype.toString = function() { + return String(this._wrapped); + }; + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define == 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}()); diff --git a/kfet/static/kfet/vendor/backbone-min.js b/kfet/static/kfet/vendor/backbone-min.js new file mode 100644 index 00000000..c8c33e0d --- /dev/null +++ b/kfet/static/kfet/vendor/backbone-min.js @@ -0,0 +1,2 @@ +(function(t){var e=typeof self=="object"&&self.self===self&&self||typeof global=="object"&&global.global===global&&global;if(typeof define==="function"&&define.amd){define(["underscore","jquery","exports"],function(i,n,r){e.Backbone=t(e,r,i,n)})}else if(typeof exports!=="undefined"){var i=require("underscore"),n;try{n=require("jquery")}catch(r){}t(e,exports,i,n)}else{e.Backbone=t(e,{},e._,e.jQuery||e.Zepto||e.ender||e.$)}})(function(t,e,i,n){var r=t.Backbone;var s=Array.prototype.slice;e.VERSION="1.4.0";e.$=n;e.noConflict=function(){t.Backbone=r;return this};e.emulateHTTP=false;e.emulateJSON=false;var a=e.Events={};var o=/\s+/;var h;var u=function(t,e,n,r,s){var a=0,h;if(n&&typeof n==="object"){if(r!==void 0&&"context"in s&&s.context===void 0)s.context=r;for(h=i.keys(n);athis.length)r=this.length;if(r<0)r+=this.length+1;var s=[];var a=[];var o=[];var h=[];var u={};var l=e.add;var c=e.merge;var f=e.remove;var d=false;var v=this.comparator&&r==null&&e.sort!==false;var p=i.isString(this.comparator)?this.comparator:null;var g,m;for(m=0;m7);this._useHashChange=this._wantsHashChange&&this._hasHashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=!!(this.history&&this.history.pushState);this._usePushState=this._wantsPushState&&this._hasPushState;this.fragment=this.getFragment();this.root=("/"+this.root+"/").replace(L,"/");if(this._wantsHashChange&&this._wantsPushState){if(!this._hasPushState&&!this.atRoot()){var e=this.root.slice(0,-1)||"/";this.location.replace(e+"#"+this.getPath());return true}else if(this._hasPushState&&this.atRoot()){this.navigate(this.getHash(),{replace:true})}}if(!this._hasHashChange&&this._wantsHashChange&&!this._usePushState){this.iframe=document.createElement("iframe");this.iframe.src="javascript:0";this.iframe.style.display="none";this.iframe.tabIndex=-1;var n=document.body;var r=n.insertBefore(this.iframe,n.firstChild).contentWindow;r.document.open();r.document.close();r.location.hash="#"+this.fragment}var s=window.addEventListener||function(t,e){return attachEvent("on"+t,e)};if(this._usePushState){s("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){s("hashchange",this.checkUrl,false)}else if(this._wantsHashChange){this._checkUrlInterval=setInterval(this.checkUrl,this.interval)}if(!this.options.silent)return this.loadUrl()},stop:function(){var t=window.removeEventListener||function(t,e){return detachEvent("on"+t,e)};if(this._usePushState){t("popstate",this.checkUrl,false)}else if(this._useHashChange&&!this.iframe){t("hashchange",this.checkUrl,false)}if(this.iframe){document.body.removeChild(this.iframe);this.iframe=null}if(this._checkUrlInterval)clearInterval(this._checkUrlInterval);B.started=false},route:function(t,e){this.handlers.unshift({route:t,callback:e})},checkUrl:function(t){var e=this.getFragment();if(e===this.fragment&&this.iframe){e=this.getHash(this.iframe.contentWindow)}if(e===this.fragment)return false;if(this.iframe)this.navigate(e);this.loadUrl()},loadUrl:function(t){if(!this.matchRoot())return false;t=this.fragment=this.getFragment(t);return i.some(this.handlers,function(e){if(e.route.test(t)){e.callback(t);return true}})},navigate:function(t,e){if(!B.started)return false;if(!e||e===true)e={trigger:!!e};t=this.getFragment(t||"");var i=this.root;if(t===""||t.charAt(0)==="?"){i=i.slice(0,-1)||"/"}var n=i+t;t=t.replace(W,"");var r=this.decodeFragment(t);if(this.fragment===r)return;this.fragment=r;if(this._usePushState){this.history[e.replace?"replaceState":"pushState"]({},document.title,n)}else if(this._wantsHashChange){this._updateHash(this.location,t,e.replace);if(this.iframe&&t!==this.getHash(this.iframe.contentWindow)){var s=this.iframe.contentWindow;if(!e.replace){s.document.open();s.document.close()}this._updateHash(s.location,t,e.replace)}}else{return this.location.assign(n)}if(e.trigger)return this.loadUrl(t)},_updateHash:function(t,e,i){if(i){var n=t.href.replace(/(javascript:|#).*$/,"");t.replace(n+"#"+e)}else{t.hash="#"+e}}});e.history=new B;var D=function(t,e){var n=this;var r;if(t&&i.has(t,"constructor")){r=t.constructor}else{r=function(){return n.apply(this,arguments)}}i.extend(r,n,e);r.prototype=i.create(n.prototype,t);r.prototype.constructor=r;r.__super__=n.prototype;return r};m.extend=_.extend=O.extend=T.extend=B.extend=D;var V=function(){throw new Error('A "url" property or function must be specified')};var G=function(t,e){var i=e.error;e.error=function(n){if(i)i.call(e.context,t,n,e);t.trigger("error",t,n,e)}};return e}); +//# sourceMappingURL=backbone-min.map \ No newline at end of file diff --git a/kfet/static/kfet/vendor/underscore-min.js b/kfet/static/kfet/vendor/underscore-min.js new file mode 100644 index 00000000..f2a20a54 --- /dev/null +++ b/kfet/static/kfet/vendor/underscore-min.js @@ -0,0 +1,5 @@ +// Underscore.js 1.9.1 +// http://underscorejs.org +// (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. +!function(){var n="object"==typeof self&&self.self===self&&self||"object"==typeof global&&global.global===global&&global||this||{},r=n._,e=Array.prototype,o=Object.prototype,s="undefined"!=typeof Symbol?Symbol.prototype:null,u=e.push,c=e.slice,p=o.toString,i=o.hasOwnProperty,t=Array.isArray,a=Object.keys,l=Object.create,f=function(){},h=function(n){return n instanceof h?n:this instanceof h?void(this._wrapped=n):new h(n)};"undefined"==typeof exports||exports.nodeType?n._=h:("undefined"!=typeof module&&!module.nodeType&&module.exports&&(exports=module.exports=h),exports._=h),h.VERSION="1.9.1";var v,y=function(u,i,n){if(void 0===i)return u;switch(null==n?3:n){case 1:return function(n){return u.call(i,n)};case 3:return function(n,r,t){return u.call(i,n,r,t)};case 4:return function(n,r,t,e){return u.call(i,n,r,t,e)}}return function(){return u.apply(i,arguments)}},d=function(n,r,t){return h.iteratee!==v?h.iteratee(n,r):null==n?h.identity:h.isFunction(n)?y(n,r,t):h.isObject(n)&&!h.isArray(n)?h.matcher(n):h.property(n)};h.iteratee=v=function(n,r){return d(n,r,1/0)};var g=function(u,i){return i=null==i?u.length-1:+i,function(){for(var n=Math.max(arguments.length-i,0),r=Array(n),t=0;t":">",'"':""","'":"'","`":"`"},P=h.invert(L),W=function(r){var t=function(n){return r[n]},n="(?:"+h.keys(r).join("|")+")",e=RegExp(n),u=RegExp(n,"g");return function(n){return n=null==n?"":""+n,e.test(n)?n.replace(u,t):n}};h.escape=W(L),h.unescape=W(P),h.result=function(n,r,t){h.isArray(r)||(r=[r]);var e=r.length;if(!e)return h.isFunction(t)?t.call(n):t;for(var u=0;u/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var J=/(.)^/,U={"'":"'","\\":"\\","\r":"r","\n":"n","\u2028":"u2028","\u2029":"u2029"},V=/\\|'|\r|\n|\u2028|\u2029/g,$=function(n){return"\\"+U[n]};h.template=function(i,n,r){!n&&r&&(n=r),n=h.defaults({},n,h.templateSettings);var t,e=RegExp([(n.escape||J).source,(n.interpolate||J).source,(n.evaluate||J).source].join("|")+"|$","g"),o=0,a="__p+='";i.replace(e,function(n,r,t,e,u){return a+=i.slice(o,u).replace(V,$),o=u+n.length,r?a+="'+\n((__t=("+r+"))==null?'':_.escape(__t))+\n'":t?a+="'+\n((__t=("+t+"))==null?'':__t)+\n'":e&&(a+="';\n"+e+"\n__p+='"),n}),a+="';\n",n.variable||(a="with(obj||{}){\n"+a+"}\n"),a="var __t,__p='',__j=Array.prototype.join,"+"print=function(){__p+=__j.call(arguments,'');};\n"+a+"return __p;\n";try{t=new Function(n.variable||"obj","_",a)}catch(n){throw n.source=a,n}var u=function(n){return t.call(this,n,h)},c=n.variable||"obj";return u.source="function("+c+"){\n"+a+"}",u},h.chain=function(n){var r=h(n);return r._chain=!0,r};var G=function(n,r){return n._chain?h(r).chain():r};h.mixin=function(t){return h.each(h.functions(t),function(n){var r=h[n]=t[n];h.prototype[n]=function(){var n=[this._wrapped];return u.apply(n,arguments),G(this,r.apply(h,n))}}),h},h.mixin(h),h.each(["pop","push","reverse","shift","sort","splice","unshift"],function(r){var t=e[r];h.prototype[r]=function(){var n=this._wrapped;return t.apply(n,arguments),"shift"!==r&&"splice"!==r||0!==n.length||delete n[0],G(this,n)}}),h.each(["concat","join","slice"],function(n){var r=e[n];h.prototype[n]=function(){return G(this,r.apply(this._wrapped,arguments))}}),h.prototype.value=function(){return this._wrapped},h.prototype.valueOf=h.prototype.toJSON=h.prototype.value,h.prototype.toString=function(){return String(this._wrapped)},"function"==typeof define&&define.amd&&define("underscore",[],function(){return h})}(); \ No newline at end of file From 091208b66c733aba10da6a103a55622dbea83992 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 29 Nov 2019 14:47:12 +0100 Subject: [PATCH 017/573] Make `kfet.account.read.json` accessible with GET --- kfet/urls.py | 6 +++++- kfet/views.py | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/kfet/urls.py b/kfet/urls.py index 681b7c31..03c174f3 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -239,7 +239,11 @@ urlpatterns = [ # JSON urls # ----- path("history.json", views.history_json, name="kfet.history.json"), - path("accounts/read.json", views.account_read_json, name="kfet.account.read.json"), + path( + "accounts//.json", + views.account_read_json, + name="kfet.account.read.json", + ), # ----- # Settings urls # ----- diff --git a/kfet/views.py b/kfet/views.py index c5d5082b..0861964e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -905,8 +905,7 @@ def kpsul_get_settings(request): @teamkfet_required -def account_read_json(request): - trigramme = request.POST.get("trigramme", "") +def account_read_json(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) data = { "id": account.pk, From 4e15ab80417f09ab56f8b935658216e8faa48920 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 29 Nov 2019 14:50:44 +0100 Subject: [PATCH 018/573] Install `django-js-reverse` --- cof/settings/common.py | 9 ++++++++- cof/urls.py | 4 ++++ requirements.txt | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/cof/settings/common.py b/cof/settings/common.py index dd5b67b1..ecf464fe 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -64,7 +64,8 @@ INSTALLED_APPS = [ "django.contrib.sessions", "django.contrib.sites", "django.contrib.messages", - "cof.apps.IgnoreSrcStaticFilesConfig", # Must be before django admin + "cof.apps.IgnoreSrcStaticFilesConfig", + # Must be before django admin # https://github.com/infoportugal/wagtail-modeltranslation/issues/193 "wagtail_modeltranslation", "wagtail_modeltranslation.makemigrations", @@ -102,6 +103,7 @@ INSTALLED_APPS = [ "kfet.auth", "kfet.cms", "gestioncof.cms", + "django_js_reverse", ] @@ -259,3 +261,8 @@ FORMAT_MODULE_PATH = "cof.locale" 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/urls.py b/cof/urls.py index 1baa2a8e..f6f1325d 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -8,8 +8,10 @@ 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 @@ -121,6 +123,8 @@ urlpatterns = [ path("documents/", include(wagtaildocs_urls)), # djconfig path("config", gestioncof_views.ConfigUpdate.as_view(), name="config.edit"), + # js-reverse + path("jsreverse/", cache_page(3600)(urls_js), name="js_reverse"), ] if "events" in settings.INSTALLED_APPS: diff --git a/requirements.txt b/requirements.txt index be12f457..6c3d799c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,4 @@ wagtail==2.7.* wagtailmenus==3.* wagtail-modeltranslation==0.10.* django-cors-headers==2.2.0 +django-js-reverse From 361ad46be47d64088566f25dfb975b79a06bf94f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 29 Nov 2019 14:51:54 +0100 Subject: [PATCH 019/573] First steps in Account logic --- kfet/static/kfet/css/kpsul.css | 4 + kfet/static/kfet/js/account.js | 131 +++++++++++++++++++++++++++++++++ kfet/templates/kfet/kpsul.html | 90 ++++++---------------- 3 files changed, 157 insertions(+), 68 deletions(-) create mode 100644 kfet/static/kfet/js/account.js diff --git a/kfet/static/kfet/css/kpsul.css b/kfet/static/kfet/css/kpsul.css index 6ae9ebc4..1616ce3e 100644 --- a/kfet/static/kfet/css/kpsul.css +++ b/kfet/static/kfet/css/kpsul.css @@ -98,6 +98,10 @@ input[type=number]::-webkit-outer-spin-button { font-weight:bold; } +#account_data #account-is_cof { + font-weight:bold; +} + #account-name { font-weight:bold; } diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js new file mode 100644 index 00000000..4022f094 --- /dev/null +++ b/kfet/static/kfet/js/account.js @@ -0,0 +1,131 @@ +var Account = Backbone.Model.extend({ + + defaults: { + 'id': 0, + 'name': '', + 'email': '', + 'is_cof': '', + 'promo': '', + 'balance': '', + 'is_frozen': false, + 'departement': '', + 'nickname': '', + }, + + url: function () { + return django_urls["kfet.account.read.json"](this.get("trigramme")) + }, + + reset: function () { + // On ne veut pas trigger un `change` deux fois + this.clear({ silent: true }).set(this.defaults) + }, + + parse: function (resp, options) { + if (_.has(resp, 'balance')) { + return _.extend(resp, { 'balance': parseFloat(resp.balance) }); + } else { + return resp; + } + }, + + view: function () { + view_class = this.get("trigramme") == 'LIQ' ? LIQView : AccountView; + return new view_class({ model: this }) + }, + + render: function () { + this.view().render(); + } +}) + +var AccountView = Backbone.View.extend({ + + el: '#account', + input: '#id_trigramme', + buttons: '.buttons', + + 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; + if (_.functions(this).includes(getter_name)) + return this[getter_name]() + else + return this.model.get(property) + }, + + get_is_cof: function () { + return this.model.get("is_cof") ? 'COF' : 'Non-COF'; + }, + + get_balance: function () { + return amountToUKF(this.model.get("balance"), this.model.get("is_COF"), true) + }, + + attr_data_balance: function () { + if (this.model.id == 0) { + return ''; + } else if (this.model.get("balance") < 0) { + return 'neg'; + } else if (this.model.get("balance") <= 5) { + return 'low'; + } else { + return 'ok'; + } + }, + + get_buttons: function () { + var buttons = ''; + if (this.model.id != 0) { + var url = django_urls["kfet.account.read"](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 buttons + }, + + render: function () { + for (let prop of this.props) { + var selector = "#account-" + prop; + this.$(selector).text(this.get(prop)); + } + + this.$el.attr("data-balance", this.attr_data_balance()); + this.$(this.buttons).html(this.get_buttons()); + }, + + reset: function () { + for (let prop of this.props) { + console.log(prop) + var selector = "#account-" + prop; + this.$(selector).text(''); + } + + this.$el.attr("data-balance", ''); + this.$(this.buttons).html(this.get_buttons()); + }, +}) + +var LIQView = AccountView.extend({ + get_balance: function () { + return ""; + }, + + attr_data_balance: function () { + return 'ok'; + } +}) + diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index ff24fcb4..2912ad4b 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -6,6 +6,10 @@ + + + + {% endblock %} {% block title %}K-Psul{% endblock %} @@ -210,6 +214,7 @@ $(document).ready(function() { // ----- // Initializing + var account = new Account() var account_container = $('#account'); var triInput = $('#id_trigramme'); var account_data = {}; @@ -226,56 +231,6 @@ $(document).ready(function() { 'nickname' : '', }; - // Display data - function displayAccountData() { - var balance = account_data['trigramme'] != 'LIQ' ? account_data['balance'] : ''; - if (balance != '') - balance = amountToUKF(account_data['balance'], account_data['is_cof'], true); - var is_cof = account_data['trigramme'] ? account_data['is_cof'] : ''; - if (is_cof !== '') - is_cof = is_cof ? 'COF' : 'Non-COF'; - for (var elem in account_data) { - if (elem == 'balance') { - $('#account-balance').text(balance); - } else if (elem == 'is_cof') { - $('#account-is_cof').html(is_cof); - } else { - $('#account-'+elem).text(account_data[elem]); - } - } - if (account_data['is_frozen']) { - $('#account').attr('data-balance', 'frozen'); - } else if (account_data['balance'] >= 5 || account_data['trigramme'] == 'LIQ') { - $('#account').attr('data-balance', 'ok'); - } else if (account_data['balance'] == '') { - $('#account').attr('data-balance', ''); - } else if (account_data['balance'] >= 0) { - $('#account').attr('data-balance', 'low'); - } else { - $('#account').attr('data-balance', 'neg'); - } - - var buttons = ''; - if (account_data['id'] != 0) { - var url_base = '{% url 'kfet.account.read' 'LIQ' %}'; - url_base = url_base.substr(0, url_base.length - 3); - trigramme = encodeURIComponent(account_data['trigramme']) ; - buttons += ''; - } - if (account_data['id'] == 0) { - var trigramme = triInput.val().toUpperCase(); - if (isValidTrigramme(trigramme)) { - var url_base = '{% url 'kfet.account.create' %}' - trigramme = encodeURIComponent(trigramme); - buttons += ''; - } else { - var url_base = '{% url 'kfet.account' %}' - buttons += ''; - } - } - account_container.find('.buttons').html(buttons); - } - // Search for an account function searchAccount() { var content = '
' ; @@ -326,7 +281,8 @@ $(document).ready(function() { function resetAccountData() { account_data = account_data_default; $('#id_on_acc').val(0); - displayAccountData(); + account.reset(); + account.view().reset() } function resetAccount() { @@ -335,28 +291,26 @@ $(document).ready(function() { } // Store data - function storeAccountData(data) { - account_data = $.extend({}, account_data_default, data); - account_data['balance'] = parseFloat(account_data['balance']); - $('#id_on_acc').val(account_data['id']); - displayAccountData(); + function storeAccountData() { + account_data = account.toJSON(); + $('#id_on_acc').val(account.id); + account.render(); } // Retrieve via ajax function retrieveAccountData(tri) { - $.ajax({ - dataType: "json", - url : "{% url 'kfet.account.read.json' %}", - method : "POST", - data : { trigramme: tri }, + account.set({'trigramme': tri}); + account.fetch({ + 'success': function() { + storeAccountData(); + articleSelect.focus(); + updateBasketAmount(); + updateBasketRel(); + }, + 'error': function() { + resetAccountData(); + }, }) - .done(function(data) { - storeAccountData(data); - articleSelect.focus(); - updateBasketAmount(); - updateBasketRel(); - }) - .fail(function() { resetAccountData() }); } // Event listener From 85aa56d030d0418062a6ab959b92ebb929185b9f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 29 Nov 2019 15:33:03 +0100 Subject: [PATCH 020/573] Fix tests --- kfet/templates/kfet/transfers_create.html | 6 +++--- kfet/tests/test_views.py | 13 ++++++++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index b44bfd2d..d3d7ec77 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -3,6 +3,7 @@ {% block extra_head %} + {% endblock %} {% block title %}Nouveaux transferts{% endblock %} @@ -54,9 +55,8 @@ $(document).ready(function () { function getAccountData(trigramme, callback = function() {}) { $.ajax({ dataType: "json", - url : "{% url 'kfet.account.read.json' %}", - method : "POST", - data : { trigramme: trigramme }, + url : django_urls["kfet.account.read.json"]({trigramme: trigramme}), + method : "GET", success : callback, }); } diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 432a77e8..b52d363a 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -4088,15 +4088,22 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): class AccountReadJSONViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.account.read.json" - url_expected = "/k-fet/accounts/read.json" - http_methods = ["POST"] + http_methods = ["GET"] auth_user = "team" auth_forbidden = [None, "user"] + @property + def url_kwargs(self): + return {"trigramme": self.accounts["user"].trigramme} + + @property + def url_expected(self): + return "/k-fet/accounts/{}/.json".format(self.accounts["user"].trigramme) + def test_ok(self): - r = self.client.post(self.url, {"trigramme": "000"}) + r = self.client.get(self.url) self.assertEqual(r.status_code, 200) content = json.loads(r.content.decode("utf-8")) From b1747f61fe2833a7b702e020e8404c1b2b2d0eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 30 Nov 2019 19:04:56 +0100 Subject: [PATCH 021/573] Version 0.3.3 --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 30ae66df..15f3cbdf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,7 +4,7 @@ - Nouveau module BDS - Nouveau module clubs -* Version 0.3.3 - ??? +* Version 0.3.3 - 30/11/2019 - Corrige un problème de redirection lors de la déconnexion (CAS seulement) - Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF From 0bd3bd63aa58d925a626692717ca019be7965269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 1 Dec 2019 11:24:21 +0100 Subject: [PATCH 022/573] Update changelog wrt last MR (!382) --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 15f3cbdf..5133924e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,6 +6,7 @@ * Version 0.3.3 - 30/11/2019 +- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux - Corrige un problème de redirection lors de la déconnexion (CAS seulement) - Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF - Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ From 381b52f46c12e58acb5f087a503db2d453c9b65f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 6 Nov 2019 23:13:14 +0100 Subject: [PATCH 023/573] =?UTF-8?q?Fix:=20les=20participants=20nouvellemen?= =?UTF-8?q?t=20cr=C3=A9=C3=A9s=20ont=20pay=C3=A9=20leurs=20places=20BdA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Si un participanti est créé avec `get_or_create`, son champ `paid` n'était pas créé... C'est difficile à insérer dans la logique du Manager, donc on fix ça dans la vue concernée. --- bda/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/views.py b/bda/views.py index 94e57b7b..29812b90 100644 --- a/bda/views.py +++ b/bda/views.py @@ -385,7 +385,7 @@ def revente_manage(request, tirage_id): user=request.user, tirage=tirage ) - if not participant.paid: + if not created and not participant.paid: return render(request, "bda/revente/notpaid.html", {}) resellform = ResellForm(participant, prefix="resell") From 085013b256f2d32c8c7b5278d1e95528629b1b59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 1 Dec 2019 11:33:44 +0100 Subject: [PATCH 024/573] Add some explanations about !379 --- bda/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bda/views.py b/bda/views.py index 29812b90..f33b7013 100644 --- a/bda/views.py +++ b/bda/views.py @@ -385,6 +385,9 @@ 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", {}) From 77ceae37ef5d23c527abff3f2bc48ceaee30e320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 1 Dec 2019 11:37:43 +0100 Subject: [PATCH 025/573] Update CHANGELOG --- CHANGELOG | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 5133924e..2fd475d8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,9 +4,13 @@ - Nouveau module BDS - Nouveau module clubs +* Version ??? - ??? + +- Corrige un crash sur la page des reventes pour les nouveaux participants. +- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux + * Version 0.3.3 - 30/11/2019 -- Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux - Corrige un problème de redirection lors de la déconnexion (CAS seulement) - Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF - Corrige un bug d'affichage dans K-Psul quand on annule une transaction sur LIQ From a4fdb578bce160dd8a87d25bbe896755f2b5fe97 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 2 Dec 2019 20:41:19 +0100 Subject: [PATCH 026/573] Add forgotten kfet_password decorators --- kfet/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kfet/views.py b/kfet/views.py index c5d5082b..76c500fd 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -559,6 +559,7 @@ class CheckoutRead(DetailView): # Checkout - Update +@method_decorator(kfet_password_auth, name="dispatch") class CheckoutUpdate(SuccessMessageMixin, UpdateView): model = Checkout template_name = "kfet/checkout_update.html" @@ -761,6 +762,7 @@ class ArticleList(ListView): # Article - Create +@method_decorator(kfet_password_auth, name="dispatch") class ArticleCreate(SuccessMessageMixin, CreateView): model = Article template_name = "kfet/article_create.html" @@ -826,6 +828,7 @@ class ArticleRead(DetailView): # Article - Update +@method_decorator(kfet_password_auth, name="dispatch") class ArticleUpdate(SuccessMessageMixin, UpdateView): model = Article template_name = "kfet/article_update.html" @@ -1509,6 +1512,7 @@ class SettingsList(TemplateView): config_list = permission_required("kfet.see_config")(SettingsList.as_view()) +@method_decorator(kfet_password_auth, name="dispatch") class SettingsUpdate(SuccessMessageMixin, FormView): form_class = KFetConfigForm template_name = "kfet/settings_update.html" From 83ce873e25e3e8512a4024d1df57830a3ae553a3 Mon Sep 17 00:00:00 2001 From: Antonin Reitz Date: Wed, 11 Dec 2019 22:36:40 +0100 Subject: [PATCH 027/573] Remove unnecessary caching --- cof/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cof/urls.py b/cof/urls.py index f6f1325d..374c0f1a 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -124,7 +124,7 @@ urlpatterns = [ # djconfig path("config", gestioncof_views.ConfigUpdate.as_view(), name="config.edit"), # js-reverse - path("jsreverse/", cache_page(3600)(urls_js), name="js_reverse"), + path("jsreverse/", urls_js, name="js_reverse"), ] if "events" in settings.INSTALLED_APPS: From f151ad75c6b1fc2bbd2025dd09ffb02602cb876c Mon Sep 17 00:00:00 2001 From: Antonin Reitz Date: Wed, 11 Dec 2019 23:05:39 +0100 Subject: [PATCH 028/573] For the sake of clarity --- kfet/static/kfet/js/account.js | 1 + 1 file changed, 1 insertion(+) diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index 4022f094..dfcc22cb 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -10,6 +10,7 @@ var Account = Backbone.Model.extend({ 'is_frozen': false, 'departement': '', 'nickname': '', + 'trigramme': '', }, url: function () { From 2c848a564c1d1c61e573be7fc32c77a93eab2e2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 12 Dec 2019 21:58:05 +0100 Subject: [PATCH 029/573] Some changes were missing in CHANGELOG --- CHANGELOG | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 2fd475d8..fd45844b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,8 +4,11 @@ - Nouveau module BDS - Nouveau module clubs -* Version ??? - ??? +* Upcoming +- Certaines opérations sont à nouveau accessibles depuis la session partagée + K-Fêt. +- Le bouton "déconnexion" déconnecte vraiment du CAS pour les comptes clipper - Corrige un crash sur la page des reventes pour les nouveaux participants. - Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux From d7e1583a8e6fb8b3b4318d774863d71b629141da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 12 Dec 2019 22:02:43 +0100 Subject: [PATCH 030/573] Nicer format for the CHANGELOG(.md) file --- CHANGELOG => CHANGELOG.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) rename CHANGELOG => CHANGELOG.md (85%) diff --git a/CHANGELOG b/CHANGELOG.md similarity index 85% rename from CHANGELOG rename to CHANGELOG.md index fd45844b..cd49aa84 100644 --- a/CHANGELOG +++ b/CHANGELOG.md @@ -1,10 +1,15 @@ -* Le FUTUR ! (pas prêt pour la prod) +# Changelog + +Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre +2018). + +## Le FUTUR ! (pas prêt pour la prod) - Nouveau module de gestion des événements - Nouveau module BDS - Nouveau module clubs -* Upcoming +## Upcoming - Certaines opérations sont à nouveau accessibles depuis la session partagée K-Fêt. @@ -12,7 +17,7 @@ - Corrige un crash sur la page des reventes pour les nouveaux participants. - Corrige un bug d'affichage pour les trigrammes avec caractères spéciaux -* Version 0.3.3 - 30/11/2019 +## Version 0.3.3 - 30/11/2019 - Corrige un problème de redirection lors de la déconnexion (CAS seulement) - Les catégories d'articles K-Fêt peuvent être exemptées de subvention COF @@ -20,15 +25,15 @@ - Corrige une privilege escalation liée aux sessions partagées en K-Fêt https://git.eleves.ens.fr/klub-dev-ens/gestioCOF/issues/240 -* Version 0.3.2 - 04/11/2019 +## Version 0.3.2 - 04/11/2019 - Bugfix: modifier un compte K-Fêt ne supprime plus nom/prénom -* Version 0.3.1 - 19/10/2019 +## Version 0.3.1 - 19/10/2019 - Bugfix: l'historique des utilisateurices s'affiche à nouveau -* Version 0.3 - 16/10/2019 +## Version 0.3 - 16/10/2019 - Comptes extés: lien pour changer son mot de passe sur la page d'accueil - Les utilisateurices non-COF peuvent éditer leur profil @@ -52,12 +57,12 @@ - Bugfix : les pages de la revente ne sont plus accessibles qu'aux membres du COF -* Version 0.2 - 07/11/2018 +## Version 0.2 - 07/11/2018 - Corrections de bugs d'interface dans l'inscription aux tirages BdA - On peut annuler une revente à tout moment - Pleiiiiin de tests -* Version 0.1 - 09/09/2018 +## Version 0.1 - 09/09/2018 -Début de la numérotation des versions +Début de la numérotation des versions, début du changelog From 2df4e931d404fde510dd9d7f80e6bf35e8df219f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 18 Dec 2019 21:15:19 +0100 Subject: [PATCH 031/573] Remove log --- kfet/static/kfet/js/account.js | 1 - 1 file changed, 1 deletion(-) diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index dfcc22cb..398f5f1b 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -110,7 +110,6 @@ var AccountView = Backbone.View.extend({ reset: function () { for (let prop of this.props) { - console.log(prop) var selector = "#account-" + prop; this.$(selector).text(''); } From 59d93900a3af4c930a09aa667b4d9928a20599cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 20 Dec 2019 17:08:43 +0100 Subject: [PATCH 032/573] Update the k-fet calendar url on the home page --- gestioncof/templates/gestioncof/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/templates/gestioncof/home.html b/gestioncof/templates/gestioncof/home.html index 8b014ee8..e534f687 100644 --- a/gestioncof/templates/gestioncof/home.html +++ b/gestioncof/templates/gestioncof/home.html @@ -49,7 +49,7 @@
    {# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #}
  • Page d'accueil
  • -
  • Calendrier
  • +
  • Calendrier
  • {% if perms.kfet.is_team %}
  • K-Psul
  • {% endif %} From 00bad52570c0339254fa927f70e1277ad2f28c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 20 Dec 2019 17:49:55 +0100 Subject: [PATCH 033/573] Remove the obsolete TODO_PROD file --- TODO_PROD.md | 1 - 1 file changed, 1 deletion(-) delete mode 100644 TODO_PROD.md diff --git a/TODO_PROD.md b/TODO_PROD.md deleted file mode 100644 index 1a7d0736..00000000 --- a/TODO_PROD.md +++ /dev/null @@ -1 +0,0 @@ -- Changer les urls dans les mails "bda-revente" et "bda-shotgun" From ac901e5b770865494d8f9085a13b4bf0c2e40da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 22 Dec 2019 23:49:52 +0100 Subject: [PATCH 034/573] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd49aa84..8004f215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Upcoming +- Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil - Certaines opérations sont à nouveau accessibles depuis la session partagée K-Fêt. - Le bouton "déconnexion" déconnecte vraiment du CAS pour les comptes clipper From 64c792b11ffb67387f0d402728d3b086bb1cfe3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 21 Dec 2019 16:26:59 +0100 Subject: [PATCH 035/573] Disambiguation in kfet's permission handling In some places we used to refer to permissions based on their codename only (the part after the dot "." in the following examples) which can be ambiguous. Typically, we might define permissions like "bds.is_team" or "cof.is_team" in the near future ;) --- kfet/management/commands/loadkfetdevdata.py | 8 +++----- kfet/open/tests.py | 14 ++++++++++---- kfet/tests/test_statistic.py | 2 +- kfet/tests/test_views.py | 17 ++++++++++------- kfet/views.py | 21 +++++++++++++-------- 5 files changed, 37 insertions(+), 25 deletions(-) diff --git a/kfet/management/commands/loadkfetdevdata.py b/kfet/management/commands/loadkfetdevdata.py index 0543be80..43154d6e 100644 --- a/kfet/management/commands/loadkfetdevdata.py +++ b/kfet/management/commands/loadkfetdevdata.py @@ -6,7 +6,7 @@ import os import random from datetime import timedelta -from django.contrib.auth.models import ContentType, Group, Permission, User +from django.contrib.auth.models import Group, Permission, User from django.core.management import call_command from django.utils import timezone @@ -41,11 +41,9 @@ class Command(MyBaseCommand): group_chef.save() group_boy.save() - permissions_chef = Permission.objects.filter( - content_type__in=ContentType.objects.filter(app_label="kfet") - ) + permissions_chef = Permission.objects.filter(content_type__app_label="kfet",) permissions_boy = Permission.objects.filter( - codename__in=["is_team", "perform_deposit"] + content_type__app_label="kfet", codename__in=["is_team", "perform_deposit"] ) group_chef.permissions.add(*permissions_chef) diff --git a/kfet/open/tests.py b/kfet/open/tests.py index 4e652cb6..ae9bfa4b 100644 --- a/kfet/open/tests.py +++ b/kfet/open/tests.py @@ -84,7 +84,8 @@ class OpenKfetTest(ChannelTestCase): def test_export_team(self): """Export all values for a team member.""" user = User.objects.create_user("team", "", "team") - user.user_permissions.add(Permission.objects.get(codename="is_team")) + is_team = Permission.objects.get_by_natural_key("is_team", "kfet", "account") + user.user_permissions.add(is_team) export = self.kfet_open.export(user) self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(export)) @@ -114,8 +115,12 @@ class OpenKfetViewsTest(ChannelTestCase): # get some permissions perms = { - "kfet.is_team": Permission.objects.get(codename="is_team"), - "kfet.can_force_close": Permission.objects.get(codename="can_force_close"), + "kfet.is_team": Permission.objects.get_by_natural_key( + "is_team", "kfet", "account" + ), + "kfet.can_force_close": Permission.objects.get_by_natural_key( + "can_force_close", "kfet", "account" + ), } # authenticated user and its client @@ -199,7 +204,8 @@ class OpenKfetConsumerTest(ChannelTestCase): """Team user is added to kfet.open.team group.""" # setup team user and its client t = User.objects.create_user("team", "", "team") - t.user_permissions.add(Permission.objects.get(codename="is_team")) + is_team = Permission.objects.get_by_natural_key("is_team", "kfet", "account") + t.user_permissions.add(is_team) c = WSClient() c.force_login(t) diff --git a/kfet/tests/test_statistic.py b/kfet/tests/test_statistic.py index eda386b7..9fafada4 100644 --- a/kfet/tests/test_statistic.py +++ b/kfet/tests/test_statistic.py @@ -18,7 +18,7 @@ class TestStats(TestCase): user.set_password("foobar") user.save() Account.objects.create(trigramme="FOO", cofprofile=user.profile) - perm = Permission.objects.get(codename="is_team") + perm = Permission.objects.get_by_natural_key("is_team", "kfet", "account") user.user_permissions.add(perm) user2 = User.objects.create(username="Barfoo") diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index decc8915..0a5c4e49 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1855,7 +1855,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( json_data["errors"]["missing_perms"], - ["Enregistrer des commandes avec commentaires"], + ["[kfet] Enregistrer des commandes avec commentaires"], ) def test_group_on_acc_frozen(self): @@ -1898,7 +1898,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["missing_perms"], ["Forcer le gel d'un compte"] + json_data["errors"]["missing_perms"], ["[kfet] Forcer le gel d'un compte"] ) def test_invalid_group_checkout(self): @@ -2373,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["errors"]["missing_perms"], ["Effectuer une charge"]) + self.assertEqual( + json_data["errors"]["missing_perms"], ["[kfet] Effectuer une charge"] + ) def test_withdraw(self): data = dict( @@ -2648,7 +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["errors"]["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): @@ -2956,7 +2959,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( json_data["errors"], - {"missing_perms": ["Enregistrer des commandes en négatif"]}, + {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, ) def test_invalid_negative_exceeds_allowed_duration_from_config(self): @@ -3780,7 +3783,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( json_data["errors"], - {"missing_perms": ["Annuler des commandes non récentes"]}, + {"missing_perms": ["[kfet] Annuler des commandes non récentes"]}, ) def test_already_canceled(self): @@ -3926,7 +3929,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( json_data["errors"], - {"missing_perms": ["Enregistrer des commandes en négatif"]}, + {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, ) def test_partial_0(self): diff --git a/kfet/views.py b/kfet/views.py index a5babe52..655e856d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -3,6 +3,7 @@ import heapq import statistics from collections import defaultdict from decimal import Decimal +from typing import List from urllib.parse import urlencode from django.contrib import messages @@ -993,15 +994,19 @@ def kpsul_update_addcost(request): return JsonResponse(data) -def get_missing_perms(required_perms, user): - missing_perms_codenames = [ - (perm.split("."))[1] for perm in required_perms if not user.has_perm(perm) - ] - missing_perms = list( - Permission.objects.filter(codename__in=missing_perms_codenames).values_list( - "name", flat=True +def get_missing_perms(required_perms: List[str], user: User) -> List[str]: + 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_description(*perm.split(".")) + for perm in required_perms + if not user.has_perm(perm) + ] + return missing_perms From 1f945d1af3a2d8a72d9e1a9caf465e4dfe1346d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 24 Dec 2019 17:13:27 +0100 Subject: [PATCH 036/573] Avoid using `get_by_natural_key` --- events/tests/test_views.py | 4 ++-- kfet/open/tests.py | 16 ++++++++++------ kfet/tests/test_statistic.py | 4 +++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 8dd81df7..5dc01fbb 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -15,8 +15,8 @@ def make_user(name): def make_staff_user(name): - view_event_perm = Permission.objects.get_by_natural_key( - codename="view_event", app_label="events", model="event" + view_event_perm = Permission.objects.get( + codename="view_event", content_type__app_label="events", ) user = make_user(name) user.user_permissions.add(view_event_perm) diff --git a/kfet/open/tests.py b/kfet/open/tests.py index ae9bfa4b..0d527644 100644 --- a/kfet/open/tests.py +++ b/kfet/open/tests.py @@ -84,7 +84,9 @@ class OpenKfetTest(ChannelTestCase): def test_export_team(self): """Export all values for a team member.""" user = User.objects.create_user("team", "", "team") - is_team = Permission.objects.get_by_natural_key("is_team", "kfet", "account") + is_team = Permission.objects.get( + codename="is_team", content_type__app_label="kfet" + ) user.user_permissions.add(is_team) export = self.kfet_open.export(user) self.assertSetEqual(set(["status", "admin_status", "force_close"]), set(export)) @@ -115,11 +117,11 @@ class OpenKfetViewsTest(ChannelTestCase): # get some permissions perms = { - "kfet.is_team": Permission.objects.get_by_natural_key( - "is_team", "kfet", "account" + "kfet.is_team": Permission.objects.get( + codename="is_team", content_type__app_label="kfet" ), - "kfet.can_force_close": Permission.objects.get_by_natural_key( - "can_force_close", "kfet", "account" + "kfet.can_force_close": Permission.objects.get( + codename="can_force_close", content_type__app_label="kfet" ), } @@ -204,7 +206,9 @@ class OpenKfetConsumerTest(ChannelTestCase): """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_by_natural_key("is_team", "kfet", "account") + 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) diff --git a/kfet/tests/test_statistic.py b/kfet/tests/test_statistic.py index 9fafada4..a5e3192c 100644 --- a/kfet/tests/test_statistic.py +++ b/kfet/tests/test_statistic.py @@ -18,7 +18,9 @@ class TestStats(TestCase): user.set_password("foobar") user.save() Account.objects.create(trigramme="FOO", cofprofile=user.profile) - perm = Permission.objects.get_by_natural_key("is_team", "kfet", "account") + perm = Permission.objects.get( + codename="is_team", content_type__app_label="kfet" + ) user.user_permissions.add(perm) user2 = User.objects.create(username="Barfoo") From 229b6e55f521ecce4fb1b8554d933f03dcbab32b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 20 Dec 2019 17:29:35 +0100 Subject: [PATCH 037/573] cofsite: make club links optional --- .../0003_directory_entry_optional_links.py | 107 ++++++++++++++++++ gestioncof/cms/models.py | 3 +- 2 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 gestioncof/cms/migrations/0003_directory_entry_optional_links.py diff --git a/gestioncof/cms/migrations/0003_directory_entry_optional_links.py b/gestioncof/cms/migrations/0003_directory_entry_optional_links.py new file mode 100644 index 00000000..10cf5c5e --- /dev/null +++ b/gestioncof/cms/migrations/0003_directory_entry_optional_links.py @@ -0,0 +1,107 @@ +# Generated by Django 2.2.8 on 2019-12-20 16:22 + +import wagtail.core.blocks +import wagtail.core.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("cofcms", "0002_auto_20190523_1521"), + ] + + operations = [ + migrations.AlterField( + model_name="cofdirectoryentrypage", + name="links", + field=wagtail.core.fields.StreamField( + [ + ( + "lien", + wagtail.core.blocks.StructBlock( + [ + ("url", wagtail.core.blocks.URLBlock(required=True)), + ("texte", wagtail.core.blocks.CharBlock()), + ] + ), + ), + ( + "contact", + wagtail.core.blocks.StructBlock( + [ + ( + "email", + wagtail.core.blocks.EmailBlock(required=True), + ), + ("texte", wagtail.core.blocks.CharBlock()), + ] + ), + ), + ], + blank=True, + ), + ), + migrations.AlterField( + model_name="cofdirectoryentrypage", + name="links_en", + field=wagtail.core.fields.StreamField( + [ + ( + "lien", + wagtail.core.blocks.StructBlock( + [ + ("url", wagtail.core.blocks.URLBlock(required=True)), + ("texte", wagtail.core.blocks.CharBlock()), + ] + ), + ), + ( + "contact", + wagtail.core.blocks.StructBlock( + [ + ( + "email", + wagtail.core.blocks.EmailBlock(required=True), + ), + ("texte", wagtail.core.blocks.CharBlock()), + ] + ), + ), + ], + blank=True, + null=True, + ), + ), + migrations.AlterField( + model_name="cofdirectoryentrypage", + name="links_fr", + field=wagtail.core.fields.StreamField( + [ + ( + "lien", + wagtail.core.blocks.StructBlock( + [ + ("url", wagtail.core.blocks.URLBlock(required=True)), + ("texte", wagtail.core.blocks.CharBlock()), + ] + ), + ), + ( + "contact", + wagtail.core.blocks.StructBlock( + [ + ( + "email", + wagtail.core.blocks.EmailBlock(required=True), + ), + ("texte", wagtail.core.blocks.CharBlock()), + ] + ), + ), + ], + blank=True, + null=True, + ), + ), + ] diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index 0da0f687..3bb757b7 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -182,7 +182,8 @@ class COFDirectoryEntryPage(Page): ] ), ), - ] + ], + blank=True, ) image = models.ForeignKey( From 2e4d7101ce4f9db42ec07a50a57598c63309f3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 22 Dec 2019 23:52:53 +0100 Subject: [PATCH 038/573] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8004f215..178b810c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Upcoming +- Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs - Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil - Certaines opérations sont à nouveau accessibles depuis la session partagée K-Fêt. From 4d5419fdbc84ad580303f9121b131e6e54661039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 20 Dec 2019 23:49:24 +0100 Subject: [PATCH 039/573] Use permissions to authenticate bds buro members I prefer using a permission (namely `bds.is_team`) to determine if a user is member of the BDS staff rather that using a `is_buro` boolean field. We already use this approach is the kfet app --- bds/migrations/0003_staff_permission.py | 22 ++++++++++++++++++++++ bds/models.py | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 bds/migrations/0003_staff_permission.py diff --git a/bds/migrations/0003_staff_permission.py b/bds/migrations/0003_staff_permission.py new file mode 100644 index 00000000..1f038eaa --- /dev/null +++ b/bds/migrations/0003_staff_permission.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.8 on 2019-12-20 22:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("bds", "0002_bds_group"), + ] + + operations = [ + migrations.AlterModelOptions( + name="bdsprofile", + options={ + "permissions": (("is_team", "est membre du burô"),), + "verbose_name": "Profil BDS", + "verbose_name_plural": "Profils BDS", + }, + ), + migrations.RemoveField(model_name="bdsprofile", name="is_buro",), + ] diff --git a/bds/models.py b/bds/models.py index f262ee26..eb5a67aa 100644 --- a/bds/models.py +++ b/bds/models.py @@ -63,7 +63,6 @@ class BDSProfile(models.Model): ) mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False) - is_buro = models.BooleanField(_("membre du Burô du BDS"), default=False) has_certificate = models.BooleanField(_("certificat médical"), default=False) certificate_file = models.FileField( @@ -90,6 +89,7 @@ class BDSProfile(models.Model): class Meta: verbose_name = _("Profil BDS") verbose_name_plural = _("Profils BDS") + permissions = (("is_team", _("est membre du burô")),) def __str__(self): return self.user.username From 8bae013152ecbbf403196763e5d59960f1bcc9a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 22 Dec 2019 11:15:16 +0100 Subject: [PATCH 040/573] BDSProfile: add is_member & cotisation_type fields --- bds/migrations/0004_is_member_cotiz_type.py | 34 +++++++++++++++++++++ bds/models.py | 5 ++- 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 bds/migrations/0004_is_member_cotiz_type.py diff --git a/bds/migrations/0004_is_member_cotiz_type.py b/bds/migrations/0004_is_member_cotiz_type.py new file mode 100644 index 00000000..53d332d8 --- /dev/null +++ b/bds/migrations/0004_is_member_cotiz_type.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.8 on 2019-12-22 10:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bds", "0003_staff_permission"), + ] + + operations = [ + migrations.AddField( + model_name="bdsprofile", + name="cotisation_type", + field=models.CharField( + choices=[ + ("ETU", "Étudiant"), + ("NOR", "Normalien"), + ("EXT", "Extérieur"), + ("ARC", "Archicube"), + ], + default="Normalien", + max_length=9, + verbose_name="type de cotisation", + ), + preserve_default=False, + ), + migrations.AddField( + model_name="bdsprofile", + name="is_member", + field=models.BooleanField(default=False, verbose_name="membre du BDS"), + ), + ] diff --git a/bds/models.py b/bds/models.py index eb5a67aa..60778f4d 100644 --- a/bds/models.py +++ b/bds/models.py @@ -78,13 +78,16 @@ class BDSProfile(models.Model): _("numéro FFSU"), max_length=50, blank=True, null=True ) + is_member = models.BooleanField(_("membre du BDS"), default=False) cotisation_period = models.CharField( _("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3 ) - registration_date = models.DateField( auto_now_add=True, verbose_name=_("date d'inscription") ) + cotisation_type = models.CharField( + _("type de cotisation"), choices=TYPE_COTIZ_CHOICES, max_length=9 + ) class Meta: verbose_name = _("Profil BDS") From 858759865e29f7661fe00f6e613b50c00aa286f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 26 Dec 2019 23:19:41 +0100 Subject: [PATCH 041/573] =?UTF-8?q?BDSProfile:=20s/membre/adh=C3=A9rent?= =?UTF-8?q?=E2=8B=85e/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bds/migrations/0004_is_member_cotiz_type.py | 2 +- bds/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bds/migrations/0004_is_member_cotiz_type.py b/bds/migrations/0004_is_member_cotiz_type.py index 53d332d8..2910ee85 100644 --- a/bds/migrations/0004_is_member_cotiz_type.py +++ b/bds/migrations/0004_is_member_cotiz_type.py @@ -29,6 +29,6 @@ class Migration(migrations.Migration): migrations.AddField( model_name="bdsprofile", name="is_member", - field=models.BooleanField(default=False, verbose_name="membre du BDS"), + field=models.BooleanField(default=False, verbose_name="adhérent⋅e du BDS"), ), ] diff --git a/bds/models.py b/bds/models.py index 60778f4d..1d0072a6 100644 --- a/bds/models.py +++ b/bds/models.py @@ -78,7 +78,7 @@ class BDSProfile(models.Model): _("numéro FFSU"), max_length=50, blank=True, null=True ) - is_member = models.BooleanField(_("membre du BDS"), default=False) + 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 ) From a1a2aac1f3ad11fce4cd158f6c179e0a011c4fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 3 Jan 2020 17:33:27 +0100 Subject: [PATCH 042/573] =?UTF-8?q?K-F=C3=AAt:=20new=20year,=20no=20valid?= =?UTF-8?q?=20promo=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/migrations/0071_promo_2020.py | 65 ++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 kfet/migrations/0071_promo_2020.py diff --git a/kfet/migrations/0071_promo_2020.py b/kfet/migrations/0071_promo_2020.py new file mode 100644 index 00000000..910c12cf --- /dev/null +++ b/kfet/migrations/0071_promo_2020.py @@ -0,0 +1,65 @@ +# Generated by Django 2.2.9 on 2020-01-03 16:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kfet", "0070_articlecategory_has_reduction"), + ] + + 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=2019, + null=True, + ), + ), + ] From 87e3795c76a5d9325dd5372b2e087f75720c042b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 4 Jan 2020 15:31:14 +0100 Subject: [PATCH 043/573] Ajout d'un nouveau transfert si formulaire rempli --- kfet/templates/kfet/transfers_create.html | 48 ++++++++++++++++++++--- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index d3d7ec77..982de6c0 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -20,7 +20,7 @@ --> {{ transfer_formset.management_form }} - +
    @@ -52,6 +52,7 @@ From f19b257afdc502d47e8e2827fadd41e5d482f8d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 4 Jan 2020 16:33:32 +0100 Subject: [PATCH 044/573] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 178b810c..61b3bc54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Upcoming +- La page des transferts permet de créer un nombre illimité de transferts en + une fois. - Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs - Mise à jour du lien vers le calendire de la K-Fêt sur la page d'accueil - Certaines opérations sont à nouveau accessibles depuis la session partagée From ee4d2d7f0e96f2295a1a3f21bfe564c63d731866 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 3 Jan 2020 16:40:18 +0100 Subject: [PATCH 045/573] CI: run tests on python:3.5 and python:3.7 --- .gitlab-ci.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6a6ce23a..a8bece7d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,8 +18,7 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD -test: - stage: test +.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 @@ -44,6 +43,16 @@ test: # Keep this disabled for now, as it may kill GitLab... # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' +test35: + extends: ".test_template" + image: "python:3.5" + stage: test + +test37: + extends: ".test_template" + image: "python:3.7" + stage: test + linters: image: python:3.6 stage: test From 08d7e12c3898921defc20d293ba37cb220144141 Mon Sep 17 00:00:00 2001 From: Julien Malka Date: Tue, 7 Jan 2020 22:37:37 +0100 Subject: [PATCH 046/573] Fixed images not showing up in petitscours --- petitscours/templates/petitscours/demande_detail.html | 2 +- petitscours/templates/petitscours/demande_list.html | 2 +- petitscours/templates/petitscours/details_demande_infos.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/petitscours/templates/petitscours/demande_detail.html b/petitscours/templates/petitscours/demande_detail.html index 6f8b1f2b..5cac9450 100644 --- a/petitscours/templates/petitscours/demande_detail.html +++ b/petitscours/templates/petitscours/demande_detail.html @@ -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 74654e44..592af153 100644 --- a/petitscours/templates/petitscours/demande_list.html +++ b/petitscours/templates/petitscours/demande_list.html @@ -19,7 +19,7 @@ - + diff --git a/petitscours/templates/petitscours/details_demande_infos.html b/petitscours/templates/petitscours/details_demande_infos.html index 26bf470e..06ebe445 100644 --- a/petitscours/templates/petitscours/details_demande_infos.html +++ b/petitscours/templates/petitscours/details_demande_infos.html @@ -9,6 +9,6 @@ - +
    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
    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 }}
    From f9feff4b249e68723dcd75c95a8bf5e9ec476a8f Mon Sep 17 00:00:00 2001 From: Julien Malka Date: Tue, 7 Jan 2020 23:01:19 +0100 Subject: [PATCH 047/573] Wrong use of src -> replaced by vendor --- petitscours/templates/petitscours/demande_detail.html | 2 +- petitscours/templates/petitscours/demande_list.html | 2 +- petitscours/templates/petitscours/details_demande_infos.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/petitscours/templates/petitscours/demande_detail.html b/petitscours/templates/petitscours/demande_detail.html index 5cac9450..e2786599 100644 --- a/petitscours/templates/petitscours/demande_detail.html +++ b/petitscours/templates/petitscours/demande_detail.html @@ -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 592af153..8e5e6d2d 100644 --- a/petitscours/templates/petitscours/demande_list.html +++ b/petitscours/templates/petitscours/demande_list.html @@ -19,7 +19,7 @@ - + diff --git a/petitscours/templates/petitscours/details_demande_infos.html b/petitscours/templates/petitscours/details_demande_infos.html index 06ebe445..c67095d8 100644 --- a/petitscours/templates/petitscours/details_demande_infos.html +++ b/petitscours/templates/petitscours/details_demande_infos.html @@ -9,6 +9,6 @@ - +
    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
    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 }}
    From 84c36b9903b4d8f0396df4fc7a0cbf1865835329 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 9 Jan 2020 10:42:43 +0100 Subject: [PATCH 048/573] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b3bc54..8ab459e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Upcoming +- Corrige un bug d'affichage d'images sur l'interface des petits cours - La page des transferts permet de créer un nombre illimité de transferts en une fois. - Nouveau site du COF : les liens sont optionnels dans les descriptions de clubs From ff968b68b2c588682b19e9dfe9b08a18dcf2c3cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 15 Jan 2020 22:42:24 +0100 Subject: [PATCH 049/573] Version 0.4 --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ab459e7..2e176ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Upcoming +## Version 0.4 - 15/01/2020 + - Corrige un bug d'affichage d'images sur l'interface des petits cours - La page des transferts permet de créer un nombre illimité de transferts en une fois. From 4d3531c2cb1e4119753486f4ac4133117ceece0c Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 16 Jan 2020 23:20:18 +0100 Subject: [PATCH 050/573] Fix special chars in trigramme --- kfet/static/kfet/js/account.js | 4 ++-- kfet/templates/kfet/transfers_create.html | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index 398f5f1b..52136659 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -14,7 +14,7 @@ var Account = Backbone.Model.extend({ }, url: function () { - return django_urls["kfet.account.read.json"](this.get("trigramme")) + return django_urls["kfet.account.read.json"](encodeURIComponent(this.get("trigramme"))) }, reset: function () { @@ -81,7 +81,7 @@ var AccountView = Backbone.View.extend({ get_buttons: function () { var buttons = ''; if (this.model.id != 0) { - var url = django_urls["kfet.account.read"](this.model.get("trigramme")) + var url = django_urls["kfet.account.read"](encodeURIComponent(this.model.get("trigramme"))) buttons += ``; } else { var trigramme = this.$(this.input).val().toUpperCase(); diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index 982de6c0..e4fae405 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -56,7 +56,7 @@ $(document).ready(function () { function getAccountData(trigramme, callback = function() {}) { $.ajax({ dataType: "json", - url : django_urls["kfet.account.read.json"]({trigramme: trigramme}), + url : django_urls["kfet.account.read.json"]({trigramme: encodeURIComponent(trigramme)}), method : "GET", success : callback, }); From bb05edfd6bc01748d7abe4e48596ec62422ca4f3 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 16 Jan 2020 23:24:07 +0100 Subject: [PATCH 051/573] CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e176ff0..e0d40ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Upcoming +- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés aux urls (\#, /...) + ## Version 0.4 - 15/01/2020 - Corrige un bug d'affichage d'images sur l'interface des petits cours From 28cb35e0b0dddd5c6555f0841776145f6b1bffcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 17 Jan 2020 21:54:00 +0100 Subject: [PATCH 052/573] Version 0.4.1 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0d40ec1..6e70ba4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,10 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ## Upcoming -- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés aux urls (\#, /...) +## Version 0.4.1 - 17/01/2020 + +- Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés + aux urls (\#, /...) ## Version 0.4 - 15/01/2020 From fb3f6b9073259995cc48068adcbf8adb058c2cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 18 Jan 2020 12:20:39 +0100 Subject: [PATCH 053/573] Add missing + {% endblock %} From 68b7219cf50b5e622f44d176267009d4401dab7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 8 Feb 2020 11:06:34 +0100 Subject: [PATCH 059/573] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 211431db..f10ca21c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ### Problèmes corrigés +- 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. From 03e6fe3ef61b7747a3fb220b7b1adb37a83da8a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 20 Dec 2019 17:39:30 +0100 Subject: [PATCH 060/573] Default template for cof directory entries --- .../cms/templates/cofcms/cof_directory_entry_page.html | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 gestioncof/cms/templates/cofcms/cof_directory_entry_page.html diff --git a/gestioncof/cms/templates/cofcms/cof_directory_entry_page.html b/gestioncof/cms/templates/cofcms/cof_directory_entry_page.html new file mode 100644 index 00000000..3e250a5f --- /dev/null +++ b/gestioncof/cms/templates/cofcms/cof_directory_entry_page.html @@ -0,0 +1,7 @@ +{% extends "cofcms/base.html" %} + +{% block content %} +

    + Comment t'es arrivé⋅e ici toi ? +

    +{% endblock %} From 4580f8bf0fed5ea29ae872f42502c9672696b6ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 22 Dec 2019 23:55:01 +0100 Subject: [PATCH 061/573] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f10ca21c..269e5194 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ### Problèmes corrigés +- Cliquer sur "visualiser" sur les pages de clubs dans wagtail ne provoque plus + 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 From 80188fa88d28b7384a759d7ab676090191da68aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 26 Dec 2019 23:31:42 +0100 Subject: [PATCH 062/573] CMS club page: redirection to parent page --- gestioncof/cms/templates/cofcms/cof_directory_entry_page.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gestioncof/cms/templates/cofcms/cof_directory_entry_page.html b/gestioncof/cms/templates/cofcms/cof_directory_entry_page.html index 3e250a5f..21090f25 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_entry_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_entry_page.html @@ -1,5 +1,9 @@ {% extends "cofcms/base.html" %} +{% block extra_head %} + +{% endblock %} + {% block content %}

    Comment t'es arrivé⋅e ici toi ? From 137dd655d13127f7e10a9eecd7878093ce573f67 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 11 Mar 2020 22:30:47 +0100 Subject: [PATCH 063/573] =?UTF-8?q?Harmonise=20les=20comptes=20non-lisible?= =?UTF-8?q?s=20ou=20=C3=A9ditables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/forms.py | 7 ++++++- kfet/models.py | 9 +++++++++ kfet/views.py | 6 +++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index b6fad26f..9419d9f8 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -24,6 +24,8 @@ from kfet.models import ( TransferGroup, ) +from . import KFET_DELETED_TRIGRAMME +from .auth import KFET_GENERIC_TRIGRAMME from .auth.forms import UserGroupForm # noqa # ----- @@ -324,7 +326,10 @@ class KPsulOperationGroupForm(forms.ModelForm): widget=forms.HiddenInput(), ) on_acc = forms.ModelChoiceField( - queryset=Account.objects.exclude(trigramme="GNR"), widget=forms.HiddenInput() + queryset=Account.objects.exclude( + trigramme__in=[KFET_DELETED_TRIGRAMME, KFET_GENERIC_TRIGRAMME] + ), + widget=forms.HiddenInput(), ) class Meta: diff --git a/kfet/models.py b/kfet/models.py index 814f857a..2eacf06f 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -150,6 +150,15 @@ 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") diff --git a/kfet/views.py b/kfet/views.py index 655e856d..0b1c5f91 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -328,7 +328,9 @@ def account_update(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) # Checking permissions - if not request.user.has_perm("kfet.is_team") and request.user != account.user: + if not account.editable or ( + not request.user.has_perm("kfet.is_team") and request.user != account.user + ): raise Http404 user_info_form = UserInfoForm(instance=account.user) @@ -911,6 +913,8 @@ 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, From fe2f8aaa5a11be05a92bd94171fc363db74eb237 Mon Sep 17 00:00:00 2001 From: Guillaume Bertholon Date: Sat, 28 Mar 2020 13:59:07 +0100 Subject: [PATCH 064/573] Servir les polices de sitecof en local MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le nouveau site du COF réintroduisait des fontes hostées chez Google. On s'en débarasse en utilisant des webfontes locales. --- gestioncof/cms/static/cofcms/css/screen.css | 171 +++++++++--------- gestioncof/cms/static/cofcms/sass/screen.scss | 2 - gestioncof/cms/templates/cofcms/base.html | 2 + shared/static/fonts/CarterOne/carterOne.css | 10 + .../fonts/carter-one-v11-latin-regular.woff | Bin 0 -> 33744 bytes .../fonts/carter-one-v11-latin-regular.woff2 | Bin 0 -> 28048 bytes .../fonts/source-sans-pro-v13-latin-300.woff | Bin 0 -> 20204 bytes .../fonts/source-sans-pro-v13-latin-300.woff2 | Bin 0 -> 16064 bytes .../source-sans-pro-v13-latin-300italic.woff | Bin 0 -> 19408 bytes .../source-sans-pro-v13-latin-300italic.woff2 | Bin 0 -> 15316 bytes .../fonts/source-sans-pro-v13-latin-700.woff | Bin 0 -> 19896 bytes .../fonts/source-sans-pro-v13-latin-700.woff2 | Bin 0 -> 15764 bytes .../fonts/SourceSansPro/sourceSansPro.css | 30 +++ 13 files changed, 127 insertions(+), 88 deletions(-) create mode 100644 shared/static/fonts/CarterOne/carterOne.css create mode 100644 shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff create mode 100644 shared/static/fonts/CarterOne/fonts/carter-one-v11-latin-regular.woff2 create mode 100644 shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff create mode 100644 shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300.woff2 create mode 100644 shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff create mode 100644 shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-300italic.woff2 create mode 100644 shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff create mode 100644 shared/static/fonts/SourceSansPro/fonts/source-sans-pro-v13-latin-700.woff2 create mode 100644 shared/static/fonts/SourceSansPro/sourceSansPro.css diff --git a/gestioncof/cms/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css index 4cab72c5..7d15b36b 100644 --- a/gestioncof/cms/static/cofcms/css/screen.css +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -2,8 +2,7 @@ * In this file you should write your main styles. (or centralize your imports) * 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"); -/* line 5, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 5, ../../../../../../../../../../var/lib/gems/2.5.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, @@ -25,141 +24,141 @@ time, mark, audio, video { vertical-align: baseline; } -/* line 22, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 22, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ html { line-height: 1; } -/* line 24, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 24, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ ol, ul { list-style: none; } -/* line 26, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 26, ../../../../../../../../../../var/lib/gems/2.5.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.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 28, ../../../../../../../../../../var/lib/gems/2.5.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.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 30, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ q, blockquote { quotes: none; } -/* line 103, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 103, ../../../../../../../../../../var/lib/gems/2.5.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.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 32, ../../../../../../../../../../var/lib/gems/2.5.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ a img { border: none; } -/* line 116, ../../../../../../../../../../var/lib/gems/2.3.0/gems/compass-core-1.0.3/stylesheets/compass/reset/_utilities.scss */ +/* line 116, ../../../../../../../../../../var/lib/gems/2.5.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 12, ../sass/screen.scss */ +/* line 10, ../sass/screen.scss */ *, *:after, *:before { box-sizing: border-box; } -/* line 16, ../sass/screen.scss */ +/* line 14, ../sass/screen.scss */ body { background: #fefefe; font: 17px "Source Sans Pro", "sans-serif"; } -/* line 21, ../sass/screen.scss */ +/* line 19, ../sass/screen.scss */ header { background: #5B0012; } -/* line 25, ../sass/screen.scss */ +/* line 23, ../sass/screen.scss */ h1, h2 { font-family: "Carter One", "serif"; color: #90001C; } -/* line 30, ../sass/screen.scss */ +/* line 28, ../sass/screen.scss */ h1 { font-size: 2.3em; } -/* line 34, ../sass/screen.scss */ +/* line 32, ../sass/screen.scss */ h2 { font-size: 1.6em; } -/* line 38, ../sass/screen.scss */ +/* line 36, ../sass/screen.scss */ a { color: #CC9500; text-decoration: none; font-weight: bold; } -/* line 44, ../sass/screen.scss */ +/* line 42, ../sass/screen.scss */ h2 a { font-weight: inherit; color: inherit; } -/* line 50, ../sass/screen.scss */ +/* line 48, ../sass/screen.scss */ header a { color: #fefefe; } -/* line 53, ../sass/screen.scss */ +/* line 51, ../sass/screen.scss */ header section { display: flex; width: 100%; justify-content: space-between; align-items: stretch; } -/* line 59, ../sass/screen.scss */ +/* line 57, ../sass/screen.scss */ header section.bottom-menu { justify-content: space-around; text-align: center; background: #90001C; } -/* line 65, ../sass/screen.scss */ +/* line 63, ../sass/screen.scss */ header h1 { padding: 0 15px; } -/* line 69, ../sass/screen.scss */ +/* line 67, ../sass/screen.scss */ header nav ul { display: inline-flex; } -/* line 71, ../sass/screen.scss */ +/* line 69, ../sass/screen.scss */ header nav ul li { display: inline-block; } -/* line 73, ../sass/screen.scss */ +/* line 71, ../sass/screen.scss */ header nav ul li > * { display: block; padding: 10px 15px; font-weight: bold; } -/* line 78, ../sass/screen.scss */ +/* line 76, ../sass/screen.scss */ header nav ul li > *:hover { background: #280008; } -/* line 84, ../sass/screen.scss */ +/* line 82, ../sass/screen.scss */ header nav .lang-select { display: inline-block; height: 100%; vertical-align: top; position: relative; } -/* line 90, ../sass/screen.scss */ +/* line 88, ../sass/screen.scss */ header nav .lang-select:before { content: ""; color: #fff; @@ -171,12 +170,12 @@ header nav .lang-select:before { margin: 10px 0; padding-left: 10px; } -/* line 102, ../sass/screen.scss */ +/* line 100, ../sass/screen.scss */ header nav .lang-select a { padding: 10px 20px; display: block; } -/* line 106, ../sass/screen.scss */ +/* line 104, ../sass/screen.scss */ header nav .lang-select a img { display: block; width: auto; @@ -184,34 +183,34 @@ header nav .lang-select a img { vertical-align: middle; } -/* line 117, ../sass/screen.scss */ +/* line 115, ../sass/screen.scss */ article { line-height: 1.4; } -/* line 119, ../sass/screen.scss */ +/* line 117, ../sass/screen.scss */ article p, article ul { margin: 0.4em 0; } -/* line 122, ../sass/screen.scss */ +/* line 120, ../sass/screen.scss */ article ul { padding-left: 20px; } -/* line 124, ../sass/screen.scss */ +/* line 122, ../sass/screen.scss */ article ul li { list-style: outside; } -/* line 128, ../sass/screen.scss */ +/* line 126, ../sass/screen.scss */ article:last-child { margin-bottom: 30px; } -/* line 133, ../sass/screen.scss */ +/* line 131, ../sass/screen.scss */ .container { max-width: 1000px; margin: 0 auto; position: relative; } -/* line 138, ../sass/screen.scss */ +/* line 136, ../sass/screen.scss */ .container .aside-wrap { position: absolute; top: 30px; @@ -219,7 +218,7 @@ article:last-child { width: 25%; left: 6px; } -/* line 145, ../sass/screen.scss */ +/* line 143, ../sass/screen.scss */ .container .aside-wrap .aside { color: #222; position: fixed; @@ -230,33 +229,33 @@ article:last-child { padding: 15px; box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.3); } -/* line 155, ../sass/screen.scss */ +/* line 153, ../sass/screen.scss */ .container .aside-wrap .aside h2 { color: #fff; } -/* line 159, ../sass/screen.scss */ +/* line 157, ../sass/screen.scss */ .container .aside-wrap .aside .calendar { margin: 0 auto; display: block; } -/* line 164, ../sass/screen.scss */ +/* line 162, ../sass/screen.scss */ .container .aside-wrap .aside a { color: #997000; } -/* line 170, ../sass/screen.scss */ +/* line 168, ../sass/screen.scss */ .container .content { max-width: 900px; margin-left: auto; margin-right: 6px; } -/* line 175, ../sass/screen.scss */ +/* line 173, ../sass/screen.scss */ .container .content .intro { border-bottom: 3px solid #7f7f7f; margin: 20px 0; margin-top: 5px; padding: 15px 5px; } -/* line 184, ../sass/screen.scss */ +/* line 182, ../sass/screen.scss */ .container .content section article { background: #fff; padding: 20px 30px; @@ -264,31 +263,31 @@ article:last-child { border: 1px solid rgba(153, 118, 0, 0.1); border-radius: 2px; } -/* line 190, ../sass/screen.scss */ +/* line 188, ../sass/screen.scss */ .container .content section article a { color: #CC9500; } -/* line 195, ../sass/screen.scss */ +/* line 193, ../sass/screen.scss */ .container .content section article + h2 { margin-top: 15px; } -/* line 199, ../sass/screen.scss */ +/* line 197, ../sass/screen.scss */ .container .content section article + article { margin-top: 25px; } -/* line 203, ../sass/screen.scss */ +/* line 201, ../sass/screen.scss */ .container .content section .image { margin: 15px 0; text-align: center; padding: 20px; } -/* line 208, ../sass/screen.scss */ +/* line 206, ../sass/screen.scss */ .container .content section .image img { max-width: 100%; height: auto; box-shadow: -7px 7px 1px rgba(153, 118, 0, 0.2); } -/* line 216, ../sass/screen.scss */ +/* line 214, ../sass/screen.scss */ .container .content section.directory article.entry { width: 80%; max-width: 600px; @@ -296,7 +295,7 @@ article:last-child { position: relative; margin-left: 6%; } -/* line 223, ../sass/screen.scss */ +/* line 221, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image { display: block; float: right; @@ -311,31 +310,31 @@ article:last-child { margin-bottom: 10px; transform: translateX(10px); } -/* line 237, ../sass/screen.scss */ +/* line 235, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image img { width: auto; height: auto; max-width: 100%; max-height: 100%; } -/* line 245, ../sass/screen.scss */ +/* line 243, ../sass/screen.scss */ .container .content section.directory article.entry ul.links { margin-top: 10px; border-top: 1px solid #90001C; padding-top: 10px; } -/* line 253, ../sass/screen.scss */ +/* line 251, ../sass/screen.scss */ .container .content section.actuhome { display: flex; flex-wrap: wrap; justify-content: space-around; align-items: top; } -/* line 259, ../sass/screen.scss */ +/* line 257, ../sass/screen.scss */ .container .content section.actuhome article + article { margin: 0; } -/* line 263, ../sass/screen.scss */ +/* line 261, ../sass/screen.scss */ .container .content section.actuhome article.actu { position: relative; background: none; @@ -345,7 +344,7 @@ article:last-child { min-width: 300px; flex: 1; } -/* line 272, ../sass/screen.scss */ +/* line 270, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header { position: relative; box-shadow: -4px 5px 1px rgba(153, 118, 0, 0.3); @@ -359,7 +358,7 @@ article:last-child { background-position: center center; background-repeat: no-repeat; } -/* line 285, ../sass/screen.scss */ +/* line 283, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 { position: absolute; width: 100%; @@ -369,11 +368,11 @@ article:last-child { text-shadow: 0 0 5px rgba(153, 118, 0, 0.8); background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); } -/* line 293, ../sass/screen.scss */ +/* line 291, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 a { color: #fff; } -/* line 299, ../sass/screen.scss */ +/* line 297, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc { background: white; box-shadow: -2px 2px 1px rgba(153, 118, 0, 0.2); @@ -383,17 +382,17 @@ article:last-child { padding: 15px; padding-top: 5px; } -/* line 308, ../sass/screen.scss */ +/* line 306, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-minical { display: block; } -/* line 311, ../sass/screen.scss */ +/* line 309, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-dates { display: block; text-align: right; font-size: 0.9em; } -/* line 318, ../sass/screen.scss */ +/* line 316, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-overlay { display: block; background: none; @@ -405,81 +404,81 @@ article:last-child { z-index: 5; opacity: 0; } -/* line 334, ../sass/screen.scss */ +/* line 332, ../sass/screen.scss */ .container .content section.actulist article.actu { display: flex; width: 100%; padding: 0; } -/* line 339, ../sass/screen.scss */ +/* line 337, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-image { width: 30%; max-width: 200px; background-size: cover; background-position: center center; } -/* line 345, ../sass/screen.scss */ +/* line 343, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos { padding: 15px; flex: 1; } -/* line 349, ../sass/screen.scss */ +/* line 347, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos .actu-dates { font-weight: bold; font-size: 0.9em; } -/* line 359, ../sass/screen.scss */ +/* line 357, ../sass/screen.scss */ .container .aside-wrap + .content { max-width: 70%; } -/* line 364, ../sass/screen.scss */ +/* line 362, ../sass/screen.scss */ .calendar { color: rgba(0, 0, 0, 0.8); width: 200px; } -/* line 368, ../sass/screen.scss */ +/* line 366, ../sass/screen.scss */ .calendar td, .calendar th { text-align: center; vertical-align: middle; border: 2px solid transparent; padding: 1px; } -/* line 375, ../sass/screen.scss */ +/* line 373, ../sass/screen.scss */ .calendar th { font-weight: bold; } -/* line 379, ../sass/screen.scss */ +/* line 377, ../sass/screen.scss */ .calendar td { font-size: 0.8em; width: 28px; height: 28px; } -/* line 384, ../sass/screen.scss */ +/* line 382, ../sass/screen.scss */ .calendar td.out { opacity: 0.3; } -/* line 387, ../sass/screen.scss */ +/* line 385, ../sass/screen.scss */ .calendar td.today { border-bottom-color: #000; } -/* line 390, ../sass/screen.scss */ +/* line 388, ../sass/screen.scss */ .calendar td:nth-child(7), .calendar td:nth-child(6) { background: rgba(0, 0, 0, 0.2); } -/* line 393, ../sass/screen.scss */ +/* line 391, ../sass/screen.scss */ .calendar td.hasevent { position: relative; font-weight: bold; color: #90001C; font-size: 1em; } -/* line 399, ../sass/screen.scss */ +/* line 397, ../sass/screen.scss */ .calendar td.hasevent > a { padding: 3px; color: #90001C !important; } -/* line 404, ../sass/screen.scss */ +/* line 402, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events { text-align: left; display: none; @@ -492,11 +491,11 @@ article:last-child { padding: 5px; background-color: #90001C; } -/* line 417, ../sass/screen.scss */ +/* line 415, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events .datename { display: none; } -/* line 420, ../sass/screen.scss */ +/* line 418, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events:before { top: -12px; left: 38px; @@ -505,33 +504,33 @@ article:last-child { border: 6px solid transparent; border-bottom-color: #90001C; } -/* line 428, ../sass/screen.scss */ +/* line 426, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events a { color: #fff; } -/* line 433, ../sass/screen.scss */ +/* line 431, ../sass/screen.scss */ .calendar td.hasevent > a:hover { background-color: #90001C; color: #fff !important; } -/* line 437, ../sass/screen.scss */ +/* line 435, ../sass/screen.scss */ .calendar td.hasevent > a:hover + ul.cal-events { display: block; } -/* line 445, ../sass/screen.scss */ +/* line 443, ../sass/screen.scss */ #calendar-wrap .details { border-top: 1px solid #90001C; margin-top: 15px; padding-top: 10px; } -/* line 450, ../sass/screen.scss */ +/* line 448, ../sass/screen.scss */ #calendar-wrap .details li.datename { font-weight: bold; font-size: 1.1em; margin-bottom: 5px; } -/* line 451, ../sass/screen.scss */ +/* line 449, ../sass/screen.scss */ #calendar-wrap .details li.datename:after { content: " :"; } diff --git a/gestioncof/cms/static/cofcms/sass/screen.scss b/gestioncof/cms/static/cofcms/sass/screen.scss index 43ad8216..5b532373 100644 --- a/gestioncof/cms/static/cofcms/sass/screen.scss +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -3,8 +3,6 @@ * 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"; diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index c11a2761..ec3e51d5 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -5,6 +5,8 @@ {% block title %}Association des élèves de l'ENS Ulm{% endblock %} + + {% block extra_head %}{% endblock %} diff --git a/shared/static/fonts/CarterOne/carterOne.css b/shared/static/fonts/CarterOne/carterOne.css new file mode 100644 index 00000000..1380934a --- /dev/null +++ b/shared/static/fonts/CarterOne/carterOne.css @@ -0,0 +1,10 @@ +/* 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 new file mode 100644 index 0000000000000000000000000000000000000000..851ed743994dccd7ef2f280661ac2c1e9346a4f4 GIT binary patch literal 33744 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 z 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..132f3c5aca37c67a7ff1c9eafcb3c1b7f7d54d4e GIT binary patch literal 28048 zcmXT-cQayOWME)m$eq9-2%;ZlFfiomLc~CPR4gSX-PjW#BgK}$&=|tO#Mx9V$idY# zPonX=KX8Sxuty+0DSAHGoR|9fv5d(!sBLM>@C zHw$jNE$EfH`FZO9|MmU>$vpCFm;+Yd(s7VDl(K}O+aqa#!Q9Q>-BWj`#~%sHEYDon zXTYH=;@85tu$PljY@w;M=CT*s-ofR&bT%(rcgT51MMuG*Gwi~<4(HBJ-tj|5;2clu zFRuM>vh$bwR7>dO@2`^!{>9SP*dF+2{@Y}``u-{H|K~p3So-dM2JaUS`8nHv&e>)e z68-j%uhoy9^-RgvokMy{fAab7`uYCb?un;k^^yfQaZX@RT;tHd!xVjEQ>u1Tb?EJ^ z?cCNYxQ`k}zm2jGo7<;XpZ#{*->d&uZ@bNSD~#XGL&EiXM=NAX={})s9#qQ6ktM#8h{EeEw{Qd6j)z{9JOCRfV+IpXrL$pz8lBWE^ zhBr(*Z*(qRdF8}}-d`V{m7Z=ljeLFQcR(=zlkTdvw^nQ7wX|X^7ph-#*w=aN{c_EM zt_1?Y5|?;i)-F6U=Z4My+xy}SjTP)3C@oqcd`i`2kz5e-bDp<{6<7<~JIhKh&ox`M zqE2Y5+kD$w?&>#At^ajrz1*p@@Au2(Rom4GM+-hb`7JaQ!*>A9f%kR$hfIYXkqI&cBa-+(2@2j_awy6HWq%G6t zlwLUgAVqqm>{jIsTRUqFFEDQCPk3Cr!ftv*2$O)w-8nm_2rStj-jee=vCD>$N#dXX z)lElrg|Uxu}0(>EgpV zX^hXDI@isM`8G#CTqS$m+?Kf-zQvcHOpmHmyVt;MuGJ)eO77wDb#ZWvWWW9K?Xu5bZA29q9=M3NK8R=Y zt}*2iHh=Oo{k0YY2Oooh%AVh?NfSu6qU>CPkgCto7^)md5!V*$-fVO z-u-6X9N}Fpy-U_EdERCDazc?G?+Wk9M_=W>-7s&yP1=up9?H)vRwwbzTzACj-u~qM z+f|O2FEhS8bDG%e+W6<^=iffOg-!Y%pKNSjuSwK}_k~A4Cfriz%;n5i%Tu>2SH9yp zaf`>&-RJI3+WG2TY5k7f^8B}r-&k&Y_}wcgVai1p9udA%5nd@eFMGOw_kW)vdEnIE zrC!0?YYrMdxs>$r?-n-g=B(gfq6ycvlh-IdUi^Ch{U7ITO4E{`iJQOK_0Z;hkS$k$ z5)&uGEWuM}&aS-qFQ1!4*{`}(=p5j>7PhmYEB(UMW6u9-N^qtKI6nIr&ZJp~DYc zH1GPY@%UyPkx@uIfuK~_1lq#*7T>h9tdV!Io2~>vL`v{+e7C| ztZ#qqXit1IE$gAk0>^KxXP9q2C|nWpvikXDeX*F-o%bXR%YW^cOOKE`m%Ttva^Wgt zfo+P{E$`2D2+!ErYwA)hbR;}+U)QmRb3%Vt_}gdUSh zp1bj2!HGAgWlzZp&&=6-h0SLn>z+w(i)!~MHHMyEu;JiY)oGf~cj^@{ELc>s>eS<_ z3yxJ9Z)05@d?lIJU|;Fvd6HR2BP(+|dT;Z6)A$xOzmA!WpTE-N%&fdsmX|A|H)Ndj zy34vNwxVLXU9P>E!a<=TA-5;mi+>sV)&Kq<;CI$5%1}AdvNAeTdVBKqd%x_yUs!&8 znfjZ<&65S3pR7&Lo9{1b&{87kx=78o>+X%h`2F+Ef>tQx%$WaTquM35)@ZS7eA^AO zGAs;?=6}s=Y_66{*RwC@H%e=lKXTA`{m;{m7YyvBWttNoY*08nd+kwG=|__)s>FA0SS-(OpNY2eu|`M{!M`vWs? zxg*azi_;%!d}P1zwdbAZ8H=~g?Qc02n*LFJa7;qtxs1Ht1m`6i1oir!x2#vaAauz^ z?=3%f?B-PS-Rd!WFD<{{aB#-VmgHY2Usv^gjgWt+x5^nbXKz~D0pgawv+JEn>&uWE??De&{1e27`6SLSDyHu z)C-%1_g({;{mUSqZ`SsNi<74ATDamL43u#m+qzvdsS@u}sO~(Y5ox)B0Qu7;e4mUw1OR zY_p6*d9U7@9SU0l)-f|o)!}@(;)dq-tLooPKRZe&&uO2qBF8SCqced$MtaWsLuIVn zU(fi!W_v%qS#RyMBIB)&&c}i;XX*G}N?XKQbu8!AmU(ZvmxZOKTtAt+zAgK_@$AzT zON7==i%OkeIDt=g?{}s@WfLYi&OP81cEzbBsVRz27r zQCoR6MY|7&-qGo={P}~GC76S! zyQ6Dq`iWK~0!=~Ox0glH672;Ai?zrIF zd@9+%f$c~_q1JQthdHh6%uNa$O$uV&M|Z7E4Qn~c;3&Z2$k%>2kxOfHXVJ-hAOFwa z{Zyh_M0Ev+oTbZB#l2lz93H|_N4|Ka7OpZ;xS`~A!fop|4W(&1$JN5WxSSEb*1g1V z@=A>WuUt{DwM!1Z7U8X!eB_O2xBA+7+bU1Vt>#-kcg;c%-rKV#o;i8$wB8B5Q+g-Y zomkg(&E@qX-igsi7e5O9xbjCRla}LD0j-{{1&dZlXvHmY*{-^F<&EUG)vp4cM}+M? zwKDwt78`dx?YCNY|771|D|s#=>$m*U<*RZ{$5LXsX8S(Au=7++z2J}D0Hu%ax$n~_ zZoTzx-h2_!n&+1lqzrgfn9m*+)OvaK*Mr#x`*ylMnsD-#&G%O-CPu}E->yj|>}U(r zIa*$%VIpWLv{_|aYQvnhH&!wSqz10e+7+MMu&(K_n}F0ICkNFV8-t6dx4X@4N<7Aq ze&txta?X=WG%uf)TpO9vH{*Ddt9RI%sLgrFMHWTh__@J-4Xe3+`#bl!E^6$#kYOU# z%XZk{jgVDwWM8EEi-n$z}kH*ROp_etC#Eg zqhTga%a%OQSXt-F{Q7YDs>3Uq>keHoT(|w>n)0@s=8c0xoR_OjMe&R z{))erx`ETayC$wHGFdZOdCvQP(=tAl%RD->=dbYEi@mD(EZa)0=UZJ^=cjQ;ey>L6!Gm6t9*RhLDTnG^-6iF4y|r0@yLAI07c9V>2BE!yt2 z%Og8YM_WB#JJ-AX&8^oJ9=!(n^G?m1s5jwt(eot%Za*;*J&N{43->0)fB(!b!mA;+@ob!S-LWH2RsI?tJttK3Wlx6qjn{SU zSHC+3SRZ9+Sw4YF!=HKEM!!>>p=+d!?g=rjSQ?lSlDbRsux$64gmV0>1>ynJpB0JYA zov%@^HebolJaB2jQpNW?%*9VCKQUEK+~X3bxI5&on2p$;BkRPYHYg}7Z=MsmnEBhY~8@L>AR@Za&P~;e^T>oYnOi&y(D_2(2)6| z^AasDwt~K^ddtH$X3tPyGjiYS_h|lpm&q4ec}%+dl0WBKx=&na5_-CNVPM`2HnD78 z)56)JQ86WEU%l9FAA4@R@XW5Y=X8(VS=;~nyKG5<_0IWR0={K@>Sxq)PnFDCd>-6P zuz0=H`)uy*!@I&yE`O}f@=nmjBjU;8&PmC4($}1o6Mw&6l~2?2=M<+~pmI<3eTjGK z?Vo&Q{_iv2J?85uf7`8m&G2tZkYDKeDSEXPp9Allx|6tHXpg*z<}+nmC+VroQ}s+a zB;q{_e4iZ9aXoor<`K~&6W>J}{r=>*ec^{&=2xbfu9M#3UwKRJ%86P_fh~Ny*gj6% zVATHZwoT6U*9A9kKJ_pBer=uQxjB}_&)%fZe|hy%>XU8n4fW;sHzaV~R_tVdgcGOBQdIv9_K$CAj~b&Apv_)^e^8ns>HS zX92_S8AVa@FCQzyC_TwB+z)8o8! z_3yfgPmlipc&>ohn{{;O)vp1rsyoH&O5b|Q9eJ{kY1(?0b)AY2O;&L(D6aeEwMd3@zk6kJMElvp z4PuV>tIOCXccy;4*=OlsvChWv&cm0#^5(wxZQR~^&GlzZQe~(45;w6Ky@tY@q(25p z9_+t0*MpcvvyBgvn2G{tWxRH_U+#n9=3Ql?RlH{jMXJ`xzFru zp7eUn--zuYg1!d?W(gK{)Ycs?S3IF~Wuk?)>yN#A!h-?=*7Bb^;O(Hs_)2pEYp>hf z9XwjPpD7Tu$Y$^i!JXq0&3;@C6QwtmMiXjR>8vZM!tie-oU2Hf{4w z8NX!dqX+HR$zAxq!a@8}-naA1V||64_&lATE@rmy(GQhU&Dkg>xcbe~4__I=SX3m1 zPW|$CI%;s@%!`k!`sV97=BvEf>#}#Eg@Na{C7hEwMQfIN9KGIAy20^JmFZ34>&n-) zud83zH&-y%FjtbC+T(dl)%Zfr7n!{iqu&U=ekfMQc#xxYgNKWtwe0zbMW-i;^By+f z*)G9(SxR5OYOD9o*CoQ+ds2YVP~$=6F{k zL_U+7Lp+Ydi?i#9>7)pkpe)&-qLU&U0^a=6w&aQtXZ~xwkxNU&7C-P^7hcaK=nYs+_{Cn{B}QJDLl+*H0fZq?ar%{zb825SP4hYj;y`& z)_%R*%M@HZEIm?LpI{1jRq% zo$gPC0uHTuyru8Eg<;Otpsu31{c@j7Qh!zE-$^uzxOAK+a<8Pk*baq5vy}NfvXrV@ z7wLJ;)5=tnJ&y*jjh-#}ybaX8-jcN~?XKJ1gN2`M z_tgAhYSLtBcXc`X%Cl_N)QM9kpN~$`NqzY7-|YuwS_T)->}xJ&YJYt{V8fNkhWFA7 zZoXRmY2yyDpLRkKQgh-KgQ$%7;^N)* z`^@q@>kfwgcH1An`aAm+oAZ9tCvEVF(GQorwux=yFE62)KDD`zPqi`ZHNJdiT8IC( zlbb{JpGb3RIL%h-*|AFHSdi$<5XG4f0#to3b@J?=9eaPrG{H|RdL{jZ-*^TulfNnX zWlg4Fi_<~@<*8mvgI0!saz@fb4;7)%z`kdTW_S7illoHKl`Oe2EA;x2Afsvh`Q-~D z?$42aw8NsSlq>LbgZp!<2T`picXs{%$=&(xqU#~Gl^Lgk<+ImreDR|#{l<4M{$1gq z2KbGY^A;{#sMzYX(8Bz|#|j%c{w9Ttx`$$w&{UKd;S!=_`}yOt9WK%DJ(QI}^?2W| zmov(nR;^mqD{`y)hP`Xnqb&OZ7S%M zx3k@-a*YWTnzU|~^;T0iX|1`N5_{E-%r|8!u#n+vKRk1WiP=209g~;&yifDFb@9T6 zgNszP?>`p}*_Unj^4^x!%a$z@+j=213!{CTu?`itco7ig*RmmRn=zcn}Gxmm)h^UMD|O3%&Vurb^rKkI)@%DUEv6ZeTw3zi&OMJ&A!|z z;ip!1ODjnuMtY7^(MvxbX?y#1?gyC{wX>Z%6y@~P?zzHAC96Q!Wxqa5{p)qBTl1!v zblB`~1ron=oTpAP(XEgXE$NbMK6Ei=r&_i6`}38r|NOad zTtCOCWs2a$)6$_6TkiduU-4(}oOXBqeRE!aKNxoGlcXREmwunQpp?$~AM=0TuKsAx zaqZJ4o1bC~4KvOOY(Bdqvo@B2Yp&;)r7i*Dz28o*e0JQU?9YTNs-8qRTgEnUZql>=-FynyJtDQU(asbKktg9z{(vD zxvUE(g&)YgnV`7Ox?yG}8=o12;)NCbOU=)l_ihhZ6yd3LasGjwpV!E;&$xW7`;XKf z>-RET*IGQLs$Yo8oAGb)Q{#ZAS{E`bTlQ)#DXM9zcqruAboPOh=tuj?N1vH(co~;@ zbQHT;YwDDz&ECShYaRErV|Vs^TryeWjkeSBiy0R4RG%+?d-rT*}B{RFQbIP<7 zP+NK{XV9FKonQ7#+umNQ?$sQy(CNk@`Nk#M?u+;`&N@{zCpxg~v)FprSMs@*_>|Qp zceNCrtZ1~oo*i>w(MDg#Ny67>)$h8yq@TO$PU*W?)%7foE?f->=zKFR==S;DCclgd z*`hx!^iymcTfWpgF`FBIW?$nxSdr8Ex+6Pj` z4d1ORo^jk_e<;>m9#UR(bj$It7e3GXnphR*l`w}b_f;9w4yQhbeU2;b^Q)d^H91?} z6?u30(=YY&k2RzhGA_+pnp75OLsoP6l(>*&`Pw=yg=sz>b7LlTTU||?#N6c7Zhrcr>vUy_R%N}79olb1qIn+PpWgMD)BB6#`B0?; zKm1oRcL=1G{<80px34(1_?>Nim1*N$=b8m=GfUWxCQmzd|EHc~Pyc5dM>{;effIbMm@{JEU^^Nj8W z7wZ-%zOxo$TN1nFM&5qK&_ikaeVcFId$iZ8##%~oYVVAYhjtr``2RWW*EG)WyD@R+ z=4o|jf)AV*6_GR-O8T%S>0^cME3O1X)AobiKeoTgT0Z4vw~70O@chSIyB}>km^*LR zjm|T=hhzWRWEO5c6evH{ho}7?{qOE`J>{}woyGUpEBrk&Lc;&v`?+J0!@|PX zy1H+Rjy}+Ry+u@uZ>@x_j)Q~P`3>PW3{Udx|Fc=HGOAgx{;H_lC-p^>IJej4I-hRb zrX5ndMMdkOmBq%{@*ZC-Uzu){T$|N*a*yk)BcjR{TXR^7D;Os1sN3Jd&C>4TH+i3( zo{>>j`}MS>m7gs5o|kTl?>{zgDlgxyk4m4zFWr*ep6BslPWqMy2@;Bd)^2ZqoXfO0 zzd*gFbnh_{qiDHzZcQvFvOe!=xw-zem8r_D%{>p7ZoFi;&Dmy|ynEE!zUiU7Px7C| zR=$&E+r9r-fZ?3WrAOZTt>5%?!3Ft-h-$|42l=~lB>0z{j+JxEu50{OkQU$n=27re z>n!&EuhX`RPWZv3SZBTVQOfOiCJXI8)m+$Tc5dZ&kGhmIJRib?&e-N#-s>|jIudf! zOIEY#B9Gsf_}~Z={z#vKk8Imze&^(=u2U(Dsg&X@d!Twc@Al(G-U-w8WXnDJnj3v8 z!r9s@^iaWixjzPv9?#Yk3~pQ6zm(nbMV{m_wByxO+KU`mahB3>ekw?zh>p1mYWe3XSp`1 z(Q3k>{F58L^Z#X+ee|>Bt()@gtj~ECnqG_=%#S89CZx0|EPlh^RakUT^@PZF9@X&7 zxRsl)8mByDc``}cK}};ytVq4G%Tkq?(4dbO9QxNxl|1@D@ptr}#XK7h$(2R(oZfgg zQBgWB;O(697k57GvdQ1M{>Y`-4`dArjZYnaAbQo4A%K&~+a#^F>FC70^G;5lE_6^p zv1^r!XK5^B!>mi!FL=4RpI4ARx}`JD_oeT~xf(_+JQJ6(mIK;MVp*>T!hoauCd1*R6gb;aO~k_8^+Vqx3|f!7jQ5U3@%GLZ5%B< zNoT2zw!51_6Zah*iBHLl-RoF0og!X|WiH?LI5=~|maT>yb*ZbSZ;5qyEM>%?q2YV3 zKxp=dk~IvD&TI^mu3a_!S)ju@Y14$?XBWx66J=;=ELCIRF?}0fMn<=5%C(Lk939LKGk@s%*Kgd&w_tJ1iiY`b_OUS3=juLp-nNaA zVZ!}aa;5UD%l0VGSh$7B^3;y)wdS9b&h=;SbY+(<%m2)`wb0tGr~iETtgmU4Z)TOv z+I*Jv+4i&d;}f=Oy}QeNX4~zD8x~J}Icv4b=4&}MBKF=||B}iso!)z8dG+&q_W%Fv zXg6DK_IvhT-@MIRPv+da^Cv_2d+M|ES`456vCk@yD7gR6sN%fxZ00wo-Q(r{{r|I} zskpCU!}9ryExvbC@y_rs^piwn%|O*6iB^Y(;;0Tb0%hAa!(eYZKziFcx= zu+oK4H~v+%vmPw8KgRUO%+83NWrxQ1W%s9TU#)I^$J?7bdP1w=gw3sAPWivy%d}VS zrN`Uygb$yLH>`|);i_fZ{`Tqf^aV|A4prtypPahMGT}XA0e3|o<4$#kFZK+ZxDU*~ z5~RRZT*y-U(nY;VY1+=^-&y%&WqQ21!`dE4_TQQ5K@+jrOfljLIEqp7=clg`ok zG7g`<1b+JvFv&vb?%Rh_Tbq6Q76)WR-pgBNR?{{2Ve$Fb6Cw{g7j<0`;$QEx{>O!! zu&L8sMMExbzVs(j(os=x-H|gX*PmD{m(pE+`TBKpBlWWI$>t}O4=Ni^j9>0GO*HMu zY2Tv#{rdAaU)f!DWWv`deSINK)hoJoc5#O$URn2Ukwsl{LY%JU;oG;Tx_#zL2$p+f z{_o!bwjU+kC;X3yzV1nNsp*{O!{<{zDYLkCYSc8T`D;u3IXOLjJUv_L%yv&|f0QA8 z|HV0-t9swe+CDl>oVLrS+o3VoPh5C|qfkL}(*E98o|2xI)>PL9>$Kcyy1s0IrtOa@ zTXxLVzbTMC%~7j=))Ie<00u9+|9?;2{Ac<#;T-q<^rp;?I;JwKQ!$Z}ZW|{tet!0U z(z?5U#3qS0nDj3&Hf8h-=<&GglrH$(@f&ZO6uVOxKU=AWN2{UQpkF>>up&f&EZooGMlZvsa_4qGR) z#;Fg9RqC&ge|3BAYFE$c|9fue*HbNqb5ow2DLCMB+MdNnN$YO%az>5Rd+)boSsqxL z<)JC{{z}EVqiaI0Cm4I@$Y0358>cPLA7r%Pb1T2U$keaLHdo90H{MO#T>A1-%SN_I zMw8~5yL|cR;kkLmmXA`!o~u*>ZM7?UC!Dx6XLhr%`i06F_tuG;SGh~Pe<`DVrm8?A zc4Mh%Nr7D0j$Jc(cQ!;+1@xS)E#2Q~c*b!Te=+CYD(w~z59y`_i4kJP`}f5+wjvU-L;`E|PugG`b( zayETGwVtJH{=|@yjR_OZG%h-t#V6;?_bMRgW$4%U_dA#FuAbX2bCQ)gVdiQ^Hlgc$ zA1d!g>8#!UkOtfT@X<4$JuxB!-YL>nB}@#*GY1GHZtLv zpY@~XsyGklXOE`Hi3>e{7)i5i+CHIVZ-&5vYdV3^4|W!;3-rwI*d^#J)>yhm?(D66 z&&?%4w|Jx8ey-_lSup2E&KbE4B6(c1V^9Cm+II7qw#>%Bt{zK?(wB>ui8X9IcHB9q zF@!Pvh?{X2Tm8w0Gt|R!n2Zy?s%?F)opirCPF;zsK!!WALaty*WP7uQQ{%Isn8k;V z>1uy0o%z^ssn}u{qvxOHLcYsw4CVdebDO>GY0myIjwp@quKpP>Jh-KccSmbaGqRuW ztswU3>J`QIKpB@p)rECyE4Q;GSmrXsG2EFM6mnf~d4!GH%+#>E>Gy38vTt+IzOW*m z=bnf8yWc(~CQh??9egXlH~$ts$nUpnbHQsH!;~3}doHX}v{Y2f=J3gjS!8-(ZS=&@ z(gf$svwzJeJ<%3^Tz$N+b4#_vr?zfgek)zIbD`&z(*nJ=#&%{;k^EG7^77g_JQZD6 zroU!`1EV50pnQEIzR z@9ukz-+iyO%yI9H73s0YABvX%5j=&il^Vdc^l88aKpU3u!Vg6+@>Z>G&MYxmVX@LMO`tbmab14GG{b2g_w)R|@nv3QwCLE{)mqniWm>G?#HIk@ zF`;#JWh&Du38(_B`G7)r>S zR~o%%F>X<+VAZ#t_RS?AeE!yghKgKnO%;`EXZYRzYcJ>j^QTR#My}`6io}8@h0>X? zIIJ)8wn<7_it_OJYB_}+k$)s*oto5P@FSNSz_ zhq`{L(|dVSk>(8<-){x1NOBB%{&})GtBcBcp|+DX?8!go%deXK;o=>B*A2;8eX+rY z2^&tDJ>2wSdsT+YF%ggKu7JGhb&3(YqZ6Ah?_GFfQt*Gx^P1b^U*t3k@W`<1J9S^2 zd;d{$UG@L@=NI-wrT^Qa#~%3n!2-4)8+zW_XMNbepTpo#_)jhygF6}}w*xw+Tz&Mi z&cbW^t=FNKZ`G|-H(LGU;Fd!!S{5A3zA14mn!CV1S*Ii8g<#A%-@D!?d+YWyRdkdV z)~>v4RARB>`sYJ(Tv`nWd|91pr+R#P&-*kfpUJg4*!^+$VULPrafJ)pdW%{MUvuo* zwY7S4^{%@ma(j8BJw5hb@A}wsWWh7lxU)<@b7t^~afJGO<7c_`##7reIwkno)oE1q&wji)DDkuzn3 zhSfgK45y-twqXbP&PKoKYK_||9_ev@<5o5;g((J`UYjh2Axo|e8G z8gh##`QG`Mu)w=vSInZ%4_9%l-TCOts$xUO*DA|wX1`XdwK6smpI<#+Kv!;dMr^_cyGW5o5vrRY7_+lk>XlRVQMvF3ye9NHs~X zTyes(PQs-!?QkPM!+kUB59OW*pM}Q+e7t_ai+RDG3odJ8Dz>#N$6cs&T=8U@^Y@>L zsxn)v1lm?!Ic{&#S>LjCTH%`Z^@}6?jx>fWxIJ^1x*PlDY@aKECIxw2m7ZahTeg(f zP11Q#_#ke@ZPBk)TAaUiMUuDQme#|k?ctP_^1oEEbZ^Zv&B+kI8^k`2Z>@iIROejRu& z9GJ_k+axsoVWbS}yu+Cu{C_jm#gVXoY%FYxf3-`Py5r00H>^rpjiPM$- z`8;ZOYC9K9`E=)-O-lS^z5e6s%*n?d{c%g*Jh|+6Kx*od-v*}^&rN1O8r!Mz>E5+D z>#7#r*NO_QPGxIPb`h=5yDP<<^rnAfN6;iC4r6hV$7YA8w&;q!oK)1-dt<_)(+YnA zWZ67dZ<4g$9I+r->Cl!VYExO3GOx`@Kx~bTj8N~=YkDY+kL+zF64G(dlYiu_mZaHHH-Fo z?CSsMHsjvR_jOy7?mSx=?w@IK?(pN*ushE$tY};pxhZAh#n-NKuDt>)*^FAYvY6k` zn|)#0-m~u}h)3--ID37S*w@32srn^T*Ql>jd8e)aqrWqO{r&?h%`(N;3P10EyR`Zk zi`t?Khi#{BB3FK#a7HxjW0BRXC3^9- zv3oYWpCu*1^J$ED?e9oZIE*>AdE3y;rZ_2xu>*}@Y<_f=$$DC4@a&2i3xpjw8 zG(+hPhwW5}hux8@tC&ATTr~P5q#tY|GSC71@>HtFK4_8vi*B=?vBS(Cv)%XcrNxY>Q?Z^fOS(&ul3NV^ErQawfZ@? z6;rCRw=JIbrJOrO+$i9{_mCao2@NL-X6;g(1?F>}BAb@Jv3+D5 zF?I8YCljw>wXHo@-v1sQF%gCX2^n zrj})Qi<7P$>~DSaJuUcHl=|Jij7kFd_KkL+%!vPvwp4~H_c#dz< z+${E!Oq}MeHfc6b_^+3+PmW$Iwd9$}r@Wu+&VG%aH>0O{N&4)oiShkxoImTBd#6m< z53whm{%L#Kz9$;YnEvGV!b5Y^nj}3BxxA27GxFbKAadhwta71DOVztS2cCS)T;_Ch zS^tjT`wo3549bXnyriJ`zeeaG&MQ5l`le#vVWfz8%41sFd)`>h`> z^7N8P#M`%TKX83mF1B@k)q;O*(vBb@% zxjD~^Jhhp5RNpIQ?zGDJF}117KCBGNe0tID)lpaO>+>pRrWU+SoTsh2X~CbHBEs)C zSZ$P4`IdS2*_$U(vGr5;S^dpSoB3m-kmX9NbMIf3$9XzeeDi(vTS&q0)<1?UmRAaV zJeS{Wn!98|t?rQy!2=~<_{+)`-Pn^U_(X6s-~MZ|y-w=P4Qn;dtPGhW>&mfMxJC29 z;-nafMMnEQug*So@O9Vai$B{sZ_WI~x>(pfMzzLY+43Kg&M@j4*-7s$DPejnH7lvSB|oa#+i&XcTkCgS z{k(YB?nv{hqAOE2@A&##V&1OWrK`mMW&fCPm)rZ!jqg)B*YDihUf?F{mEgVo{kHQW z4|V+>NiZ=pp8H<)>xh+RNz=<;_RFPZb&lOQ$vbC$_jE62F3Swd9|mGwTOzhe3i(cp z{_6gkG34;hc_+`DKJhIh&wExrx3Ipqkx<2|pqHP0*L)VrxD_wk=4(7dQ8)ikv2fh= zOMdFy``7fV`VuG)P zj_g=b;y(NJ&4p6lELpm7P6CVzFMo-t>dR~l(@_$A;}FQsGjT?QbfJ@^X#MGumomE! zpD>EL_NIHX_L=8pAIfj-YAuMH5v!h&dM#`A;~U#%e*b>;uX$sa-03Ch>2fzF&Dg`c zbDR35rap$J2U>LlxLarB8nJd$PdH`ly)|mb?27 z+?EWCcfFl)fbHs0u?^bx`-25my>q>OyS+QN|AOkRuyE zpKmk+Z8?~i+X=p&6U!^|g*(Vy`xU>Xx5%UqqO%P+4y>)(m@@5w%dXIh@XS!$D?>J<}t7f~T&S>Fg;UByB zxh5pN%c-#PWD-7N=9Q4MB~l~jlUj-MoculNb?7e<_sr{r0IrZo2=bKNT@@+wICVyC) zq%rqWp$BYy3+J}o?s+_Ox%%EK6Iv=)rFe>;og21VEL_)xfq!}xOPk!Q3|qr1&pk>W z`CVkbem6mMiM*tVoY%69x2NqlObig(e(jS-X}x8Bs_#8p>7wnIJE!y?Za(nw$=*WQ zLvp&w@0|G0H)~nvmopq+o;lC{V&kf%p0l=|3TkjJQ7o+pNV0Oj71%x_oO{yCvkAx3 zWN#WS_$c!$Ex~`n`*UmUH|l=fJZ0N{q4yRtdkWrM*OL9Z)r~t=^T&55CiMohc74g| zuX&dp*v$4Jb#1_p*v(~88#@EOzTLr8B7Fa>#QETbckUIW2`!tPZoRW;>xtd>;_ruC za;SD+pXS|ZwLw!tBjVbENT%nRavlwp8Mimj%1TjP$7D76z;@|`Oj(yB`l4KxcH1?& z(q5JaYOD{moSgbLzB#SxXj0|e1l}`~$}GEBI!c*5)76{1>|>S$<<)MlWN`?Y{p07e z>zfaj+fCS!>?jc=GS%|zK?kOun6Gn>Ug>(QDW=PsBAH^TJ|VsPW|LHGCfD*w4kA*< z$#d9Mw-(%6XBu>X-PJ+$7oTk5kH>u7Tjr)QZjWM-PZU0(IP>}Ej58?-?^zeU2(S1s zb7|zA%-1iD#Hq-x^KjnvfsNhp#+Oz3#}B`3e>&l-F8Ay9!;AiC%rQAx>X|Az=h)&X zwF=LzNuf=pB1<(a`O05=>{)ioe&$}&naa&~(iX-$92I-lcWPdpWgMr`8D`eL)H&U^ z0}j-0(V6_o%~imb*$#}C6k_bt`duxz32Dc^~*AO z3O`RWVwn+kK54D5-|XCXK1+OaDa zX7tO*#7!=~RsZ_>3x{W)o^E%!xH>gA%-L<3*7Jg`9k-3Al$-B7e&E>-wrTChOkV%t z{P)T!H*#LX$IDY|dwA~&{W$8LwOe;$$61Si&lc``x88a0vd<6yEIJjgclGR)LV}U%axod3kB+5`#NOzqU3``(JnWlUr5ailV@Me;%}%btwId zew19s`g=e7j;l>M|E~9IhUT*A{C)MN4UjiJ;N*X8xBo#F1> zTdo=S;rd)}&nA<9Npm*8e0;cTxz@o4T(_qFX}D-IQ7GrJbol!HedRv;8&bMG?`?Xj zE)a5#?X1mGr^$6LF7Kc2wAp_nd+95^m`-WTVcc>(*BB#YZ~)SrYfJGT&du${4oociPLB&D#xT z)Y|Ch`|!q8u(qbLf0c+*o3_k?zcWO8Lqm-D!R8M~G&Q%KJTv_fx6kC4jEmRTHz+l% z-gfAG)2R!oe5V92o;1v9w=&;ZqJMEEr)96+0$GoO2OS+XDVZ|S-`|;Dc=AEX0qGMrz|I4oT&TR(mItK~;HSN)#Arx#iu zHZ8-hW$z{7_|NKfTX|w1ZM+hD;r`kGOcC3EuTXt+E428x_1W}E&#SZCT~;?fSBur- z-~V~uvYh`+t6ubKm%TYz{`A(=fcV~14%^*6cqM;M(%SjUM{C>d3C}M+-~8Hnl0@vn zri>oTWi6{2D@2z|e38uBcjSY03Ey%3=3HxA|MZ#jjtiK*V7%1A_K~fk&6#_z!skb~ zW;*iBw>mDhAw^Q?hCdU>ld7%>UK;8Y)?$VS^ zuY*#j6@G0!w)ki8he+Q0GF`XhAG-Oj-2RPiLykJn#GG3(oz4fQ{(a}%H|6xobJ5L!My|wf8 z@;&;(X&%de_D?-jCsxw&eL1j(R-)Qdz0-q|U?MPkt;Av9MHS6eKtDd^>opoGk zTCL0$y=Cz=o%-lqk6yiEpVJevcJsz4=lGTHW?pA{b;acSr+u-UOI|HuWw)MQAf{#Z zjJ2S|NZJ4L!3XLqk{0kzS;`mTeC)si$M{CY!rOA)(^|Ec+*R!PzV+=&-`1a%Of!R{ z&R&lwi#e3A_KMsp{%wJ^S4^hKwq**w*ts+>V6j}V!nqB6oxqRsmDgir!;>&bv|X|Um-7(1GBH_AK#N>cUJOV!RnIOtB*p~2;VKZDE`0t ztnm34lR7vIjT`&fBL9atm=-Em7j66GlI?q~^WZwoyH>fuYft7sc-O*rpmE`qhlQ7K zu|B))`}U&yYQ8Bx*L|+^A1M}VEL{8N{w^N%fB%Jlt8FvB{g+28d$!e$9`31t$VT0Z^XKD3s7x~|aa|snENuPeoxmWjF*iC_4ulwJ_1a_TN znJfJ?)zWQ2zPHj3>#I>LR~?^ByngYs-X9MhyF)D2OzJ9TdKaqVZ~Z)A|KRms9&QoO z*Gy?YF0-t@ou)6-uO*s!EwIwtK3)0xq&@eHEh2W!uQ!P>e;-tM-|-)FwfWbYqizqQ zzSi7zVLE-wec`V8cY|vge!jSCQtrKKftAtJFMm!Z_vfxt2>MZX*#Ggf6i2rfFAav} z>kf3tcJ7b2XKP64h-GhDRgrkCu6fC6)?LLvKc@bFt^6SMicQEugO(%Dll_8)b_JEE zpZS@-+2U7zWu#HyIeDShSH07xU7JzIR=)839F-D=ZQa~lWruQCFqF1gD$S^fRerl* z!r26KH`!mxd+X=VzVkV3?RlkY&mse%#umQfpoQljq(xnei@K#C*z!lwX-?1Nzj9a4 zr=9&-o?fz4Ox+^!bD@A^K!o?%#Y#(G-8-|Xe4gVs&;8p~viAqCU@Ek>xOIRpXLeqm zcaFFBxd$Sx$NrZ1l+?Gk=>N$G?wC^k?!udTakWR$v(?o;pTD$8Jm&HGwSql`K~36+ z%4+vr4OwujEK*rEth0-|`gn!>Uca-ZmO4-L1k)nEbk)D*|J8h9&3f7HA514!7W{e{ zzc~D)_K~Ly`;IC56jWNTewkO7l~Yx@X_;YOdMab`-bIN$iVM5m{9nS>>38S+7Uh=< z*9+LBTBOW<6uW-+lwbeiVqZNv^o;vY-=t*}v}J(+ju#b+P=c(d#6j=f5)Z!Q_< zm)zbM@Gq_K>$ihP|I5vNba=PDW%Y;0$M$dfwf+Cqf)Dc?&F-Gx6Z`i|-sYgq(JTht z?<+6He@J<;)R2c&S*-5w>b~sxjg3bx9r<)bN>q8@)=!J}-|;sT)l5neWD0(^s-Vtz z)*Yi4^7V%j{Oo7cDLifn+-vpb=w*c$JA`6AcJKtM)Q1>P6q7Jqq4HJcC|kTY;}*l~ zVe4)k{JizJf$4omp^}JdCB-A6y>^+0A~OZ9FLZozt*pPscjvOZ|M{nvdA2D^m@fDc z_%-Bf%gkPdQ}sWi^`swOD6HzNKX5{uJC<|J?rT#nWnN#Ay!O<^I~y1R#jIz&`d3{! z?WV%Hko5CkO8akxx28Lvy_OX@dy(hclj{msUjMu9#)MMI$Q)Jew ztgUEdj?r6id{O;~hA(=Kn#(L|nR90OnkDy|Jx{t%yHP0rMs8cP-W!hY z)&HL#pMUJtQnO2&UdLZM;@>U(WT^suuVklWNYkZP?4U`q{72daaI!ndUCxd)O4kFXiS;7GJaWv7^gP^QW0zBA$}hkGAf( zDA@dx@0zLDt<2?}FA9sgHaz~4_teqZ<@lN)?E(WgqkW28Zxb$5gtI8MygA}!z`2IU zJmcJ@eNnd#pA&B{m)G4n+o(<5VcSmCe*xRimFI6zGVh+Ke|fUo{i5tHS)&5GlB3y+ zxJss}etYnE`JDY7g=R-)MCdBlSk4xW6V&0)4?8JW8&*+s`rH3<>2EBo`(`a$y|eN8 z{0qr#g=W?tY!a^*-4RLk`E}75sUU}oz#XZ9fy&#Dh)GG=R(-u-O;RqpF^Biujmgi{dLQx$F2socLarGpQC`=~wT z@68RtT$c(hWA4^wc^QmMP7AKT;yo>#Z2n6=h+vw=b27v?Du%V z(vflaiiM-{ZKIisV+xPiENYk>bopua!Kob6+ZY(RjDq}Ji_@F)+;!iVwC)wZ;;)o7 zvrDzEKZ93h(wA`OxXJfz8vnb~2tI~%W{%dEcZ`@K9JlQKB+I_-dCO0 zDt_5)yy)dV(UYa&lQW)~HtR|@dS8yWy}>Y?a0qScbB8~(T--YLtY$ycW)9aMOBNk_=UMU!5se+-$ohHw4b z+q0$b3jA1V8Xhodn)G3l!V2lriYA)J(G$uQ;5KI! z4UeO9J!iVzKd8OL!^KdwS*)k?^^H^~b!(yioDUXyPL4Mo{FiILFSuiebNsG5$w5&M zH>n@ayOl1rsr-6M>+Qo?l5_oYZ*uTgDZTuCUnsS4)2V$GhV|RBxR@X6&#~C7^3bX9 zz;AuA1+nuk$eiL?of=*9)XurIOSksZhWpcQ{K?VfY3tzna&Oi=la*fe(#!gH+AP+8 zJMmlEy912d6c2cbKYx+^{rr`^hvVM<*%PB@tnhu$`<@j_-yT|hbe^_WzFvQl!nMG? zU-V^{{Wu(AUf#AyWNpag1^!Q}qO`lcr<`4RciEa7GFJ|){IV;aJ6Z7P7l*n7)nBK` zg+Gq64tTWd9Gm79lg-S9yNYGG&iz08@9e$)fGqL5|rsl{MOh?}*KGa$C``NMg4vh`s;!lz*y<^rYTHQD;0<_X>Lo9W3~K+9&R`@xMvWpXAHA{`g;i z_uYphdAHePc;BT9{y)L#z3gHCr}A&c_6v`#>yQrKHNS1I!lL+=jKk?=ea(~W)sN1J zH27?K`9TYJTA5l_+^noUMQbYfzKU_juCFcMYWsdoyuxD_u{{g_i9A{7#G%U9b#HCQ z^UcAsp1jXmKGp8tTlQbSPrA<>V>*#NOR{wFTW$eL)PsFxYZoeWayJgS8v%Ghv zJrK&ex8q=a`J?&KG>HyURa+CIroPo7XAnUnaf_;|{4PF9ZTer&&X z$xEMG5x+F*l;fSOdmC4)zsU}>+p2tJt^9%`&$1i~=d>NEH=k#0!OSY-erJB&yW+5{>8emJA*RK3nT$_Z z?&no}E&p&!MJk?UlcU`t#_k_mCtkIGl(Mx>W6l4Ad+wj%WS$w-V7YbU|D(_3+nzCq zSnui7!D2Y#ANyBi#V7CW`dlAL-rtL;C3R6>BW{Qp;hS#v#R zEm*{Fr4TsB;pOMWQP28#nDQK2k~RE7FNnVJ+toDb!Jj8T7hKuqo-5mHbzSvy+=&@$ zUTPSASMj)&Cd#vG_w`Pve3&A(;L1Ux?Bp+t8ad{d`)$gc{>*H` z$BFh*J#OBiht~M`ChqZ#ee8GSo3!b6=F*Hx&PBh}WZXHK7N76m6lD>uuIF(@k*ip& z>&vve#|}x}I@{RZd;7EX{Y&Rhui|742-v)mZI#*1<=YwfSl>-vJNelT(a=og)|rcz zHvbGa&SK^<`+D>KldcYly$cLFbk4;lD@;gl&Qb|@eqe8yhnduwEZOJg6>%0eN&7DE zyR~8Op7+JCg-RkCO=rsG+r+By9np-DbD2JwyN+ewbIDCJ);&>s^7@eEjN1A~@;39T zbcJUmXHOJMd3)6T^c&@n%Bvd_=Bld5@Us8^C@8tE;K9nPdFv)QEluC$!S^U9QKiJ$ zO=mUV1poght4>^0+p9Kj67TZE505OJa7NBkS)it| z?S2!chw51gqMDk*i$5=N_ddq9I!(oU;U*T@g+eaNz6(53UmCMEa^6LshJ-VwVva%F z>_RGE16@~FT-1Nsq4qv7%6(DXyJ=PWDSJb1N6x!FT_byj&|Kvm+pIdg4;wkHd-B8P z@oT3`77xnh*)FowPCd%2(ZHmx^&+ZlLhjPdw)ekQb?LZv&EVM`8YOzu_T*tzo0?@T zX+Dh-bN&^c&b@Z7gy~>A%QUCpL@Uj??7mA3gl8piwsToU)!tn?>B?liGY(UoUoTNm zw^_Vzs&G#W4`V*hL$~^EYc;(R{>r^sz~^jlWb8Jn^~j0pDQcB>Kh|_tYOW2gO)(AS z;MO$$m->&nOF8A>Ql5obmMaduZ#eFMJ~GioxqI5b+5Cqt)o+vZxZ{^o z4lQYHI^;4->y(jrADf@tFX?=HyGz|JH=fxRyw7U+6h2kP`a^1fk@=D|Y2&FY&vd`> zWp;CSiQ3WW=^P-CoeNr(K(yF3nQnR zrIwbI?{4?D_?p4lGg~L&lEU)lH%qPrA96JGIu{af%HY(yk1yY?6tyVwe0H*fJJLx! z_z#yld;E$H!G;wM3lpAyIR3A7$1J8PTG7#m`7*sUFV0T9u~@rL=IHS+KfZollYd?H z@?+yYx0f={$$slUPiK>Go$5`4)<}n#)UDOuI^tH}pY#9jM%nn)#Yw`R-%NaMd;clk ze&;Euv1E$Iv}cp&D`)k1`G$)eyAfil@KIDr+)U_D&JTyl2HKvH6C&~?I#hPfkbT4M zq;a1w_IFf4aO&J{ktaM|ymueA-xk@Iy|Y-CnU{64=B!Nzx2$fNVsycEN@CM1j|o0y z71|5S_TSOo&+zh^Iiu>Kr^U-k)SH0**}-1|-7`{HwDr}v0&T9##f>TE6NrelTYVl^~*CfH6o;-Ty=Rq2^DsX|ku!Lrdo z{f^m6jdCT0;s?9;r&RrIeirEJm)hhj&7?CmfnnmulKXoWy(oG9((~{-m92?Uh6%TS z^YtbR1a+z8d1g zv&Uu~lzUt?#p=cbNya0;Wm1B-e82c?$K}ia3YT_>SRNNtxNO-W{I^lTa9_?ry~W$( z7C)%{ZnLwjWsaX?`RC;tuKRz^&z2K0bX8e>Zr82S@T04rL_PJN(NHL$8*=Z#v;wyO zGZ>oGZ)q6Mh}t}LQSagggFc6sZzP4ae;%K)rrEf=J0)K-Sl`~|`s70bN6k;?{od9W z&L#CS-h}n3!mHd&_R5mJHB-a3BuLfVvu~C48z=Njh(DLMhYco&3_nG$a`CKjK7zd3-jDPQ0KCpbT_s#B!RlbWW zdD{PPb+X71m(pC&6SvoQ%Vq!Po7ATqpMLW!n_S1Mf5v|jr&iohG<-1gUTrwv@&p6M z?72&f7UVv=>HR**`mFV@;v*LXt@Ce(%$OdZxTgLO&u5D-iSjpp#lG&GZfkz$`jlIe z>sG9qlP1F)_nFyR_(|qZ>#0qDL@Id`o;y73UNLLE0EgUUfzoyz+nHg8-{*r6K@=?*+eTLG-?I+tJwewG>|0ybNb}p)^Tb$JRwC9MLWATIS z@lG=Ne-(S2=QqB7@4xn+R)$LA+09c+G&8-IF4Fwwdi#==k5#+vx;v3+hS`;KWeko! zx_yLW$8sC3*rzO$FZ6XkG}|I^*747b#Eus`uYMBCT+CKHNjE)V)q%_sPQ?j7KX;UT zy>$BV59gL0 zdoo+TPHC5he2lyy<0^RuzgY+Tz3TdAw>o;duKAF~wn_NH$5yu5x-a&guPUE5KVkU4 z>QD01{D$n?rH|GL8K}+^yWdLVfq8 z-5GzTd@9k>*NZ%eo==Cj+CL51dGB#x z&P0~K+Q#J!yEuj2w=X}xb=vl2pJQ^G*6rjo<6m>`p_Fyc>+2dTRWo>7PRh*M+qv`I z&wYFUO`ciH?>#+fL1fc|8P<1mE?TO++$jB9^`79?d&SYRGiSSBdbEji=E2j^>-4|N zb*3kpcKcTpOlJ}~n4BcA%0c6i-3Uf;*90LyW@>-3QWEAKm6Xz4T8}r-)DY1^L5`g);QDUhi@mvFP#v1?rOxw zr#h^w?=AV%^TD;!=U~~>qyM^p7w+@i&8yz_P&dNvL8H&d)>VJ69Ms(saDq=Tpy$gX zW)E>DgZ;mJ8uz}n*)ieho|Kc8cdr_EIJ#%0-s--4Z_Xd1zulKsx#^_k`d@!5bJ}v+ zjt^o68#UG}zag^qcSFeSknUsOEzfWDJ;brxc-Nx#&0V4X_s*uiaoCuxb$(~^OG|F^ z|K0PhdIhPzW0`kwhR8d;RM&4mDhi!!+}D41*>SqEk=ddya8l5usAsyK22nRwdlbkm zGb>epku&G*{nNh=dx}_zKjl>r+;sERpW8=jV(wbAwG`;>SthXY-kEuIJH(InJ>gY9 zEuI_NwKJpU)rddKJi+9QgMsn4pFLLt_nj3IW~^is+hb7P zyY5T*!#xlEQs$gF`R={hs>eUuyUJ94NL;RT{rL9#sr?=a&!?1n+&VNvboU3J*|)te zN((=`bzW5Oh;{ot{SE3?uK%xf>77|Ux#5P1+^eRRHSD~{eCkzgd4vshi}*uR4u>mk z;E>{E3*=pNC!?z*rP{nk)tY4rQwIO@b>}r)jwDDLCY)pWuy#&P1h1_2YW;BD;PBh0 zTf={s$A*NI)-JZ(vj4^7gD>CQj&*+iwQK9m`&r-bnj}0v|Mh3x&vna--h2Cd_2oqV zm&x|5uD|th=Em;I_m}S3Sgw6j^#6rrWn|a1`hEYERg%}bNJc$zvHZ4hD%1KCEV)i? zCt~J^KVRpc?@Vpv=el>!0v?u?9(qsSgiil@oB6K)b4?FZ)z5Ec?!V+Qhw1x=JI7C}J-J)J zy6I$lwr!wHYUQcqE~-KhJO)Z{$|(e9NZ4)N8afsQ-F0KqEss@xiq(s*O^;dbW=8ToQBsJ$+-VHoszx zdB?n&(#LOIQvT1YBGf+rWcBl}Pp&$%&h|Lde>ivDaW==vH9ygkG$!PB8h!n9kUS$?>f5+KVuTF3aUv#`N)wMw+vFUt)@fYLeuG1xiy;+2=f8p$& zpH{ub|8RoF=UhuwwL51kc5oTKX;hqW{ZMGofj$j}`;-5*GM%13!#pcP)5_uQ!IQ@% zbDcszRI5yN&e-{&_qxFouM-h#rd9Bi#Tf*>{@ls7uW#n1AM#d3eS2&|rX&}rD>?kE zIKe6Xb*@TIxACpzGTz!U50)ILy&k=9i`0*?KNmkyl+p0)-^}SjOLMtIUaStdxBq38znkmoZ8j?Y zjVF8)Idj*Ah&+1vhKE<}&M)pAvrY)!JK1;g^^y(YtdC`9uzk83a%iiQEAOp5k>4*p z>ufIbik~k0@oU7&iWWyR6*QD=r@H0Rrd(>0ITrn9gdkepSQ z`Xzh5>T2QX$_);NZ@ieVdDgMlb^m;{<1d@niFp$pQl4dQ+hwG4vE}CH)_c3Ea&xQt zlcyi(lq;y|Tt9uiX^nJX^8r<_qZbsGEP1ldF$S_m&bQF`xB-#;bx zJ+2>5Pk9=hyn1Th4x_6qXXo{8|8R4T%mROv^*)t5kDm?gKiajpGb%Ww!c1C$LFBc@ z+&O(JfluC@jyEiM$bZHzrg&0IQB$GI$)DTL=e{pc=qjoW`=arw>&$nvV=ZeUOdq^n ztnl||f5w-Yd@oiq?FoBV`{~=toL@c`&-4Y4aaNdbJ(%b)Z)wt$@S77%!k7Ead6y$G zMYB+vWs3iSxSH?mYkt4FBo_5bHU0N(f8%-cdDS&y&v<0W7*7&g^XTlkfbx_6In^Ow z%>8yIFwFV-MpXO2MC-;yh1wHx)0+Nj>gU_m$Va4^hhE;z9HeqL{kc%))rIxzBISNN z{?f@;x^-IEyJ01R!jJe5GtAC@2$WC{{Tgz_8!6R;KG?Jt{>=nn6pK!CgEyF*#Ys5l1+V1;*xc0p{K-bS}N?GYfPI{ zl)JlW&&Ex!=XQtZJ(2tyA>^~=nmb=h>&2TBCd?0#`_R$&|NrW~Y!Qh`A>no@wt+R< z9z0%yY0$7c{JsP z3VV+I=10+U8a`;(*_ujBEj8nu!K396so|nSszg?qtZBpB` z(|NOB{+4?EJ0g9y?~N0a-`DJK3zR&})@l6X*xU(qB^pXoLK@2rAMH9R{^a7m4PV~J z{63=lY`MncuQ5|Ls79`4s!F4eIx-2Ty6l=`Q5o|ebjKR2Il5^b^d zpKDWk|3do8BB$K!oEjhf3VzoAb`xx59!-}up4IL3@xtoEcfPG(xR~X^8OEpaauN6L z{uC;Tc3T?Re=UB+PqrlwofT4d>$}QJtuV6kbU7lve69WI?sXBmx8#emf;bX;4j;Tb z*IdEPJ1AOv-qPUQ?uy3;SNA5m9u|*Ty871qFVEA=mR(QZdV^(e{y#lIqavF-n8>H%FVB#bR2{CrkMww<0F~Ij?uqob}Uw z=D?ms1(QXj7l&&kx$sS&IlGjB0UM?SHf_fPdcGC4!_LvME2eu9s`xGVScQj849@Z{)wHTMb@*Il!EJt?gTlNY>`Bk%_6NG8Rco^@$l=|0QrC z{n@@FJ~!7W-@5#9`tHan_X>(9=DO^W4BYqb?H}ImN4r~PJ)b{&bL)cH@=sT<_%3dc zh!)wo_`vE_9zJ~k(zHcxW}Fb3xA1Y)EI(mk)^GEtaPg}!M1QxdKd^SgiB_rW7q0te zC+ct5{6e{df79PLo0|6gu-ms}`lrYqUK^M5%Ig(ZKVY0N=l6D&zttD|XL=rKZqo57 zu-=`Yep%^DuJ5X%il(-uvyI<=<%&*Pow%c7!w}Q8+*ofEI9+nrqwQXA9{Oc2;$l3}E+-+hU% z8!{ad95>YQ=LIw;rp;E^=fEFTb3MW;();GZxtkj7pU;!ts%*aX#*aNuw*TVm<(ECb zQ#Ab2#!08XwN4GVe$(wy>EoC^4_4WnjpuciepXqnynunDNzF5}XuG`8w=4Am{J%Bk z&2(_L>WWHz{`Abg!zRD8B&W|JiUEr_BpC{5e-MM9gx{z9}2`EX@A< zt=+9rLsc|&{U4sd(vU;nGY?<8pt17h?Ua{mL*-do)$ersI=*^Q^tk+L#ec3pW{o4mBwa=y-H^R%P!nOb%pW0<#oA>QrrfgQ} z*}d$lq5A{}wKT!E{6{aFoz)0ryuq_C^LQQO&a?|%Jk38lWMnRXFD~M{Aj06X;J@G2 ze@%bC?TnVV^R3=uq(zB&d$ho{bVoyT+L zvph+#49_Sx==e5A+E|5s%7wkhf0j+TpEJ>N$MkxW#7oIv?o68TaFfi#8uy08h6^oGt@c0PsrtCKbTPCrt&yRkg&$^KIf7R9A`S0>Cpu+e$O zR1J?gm-jXsDb`OgQ`#Z)e=$pL|Fxg~x@O%encFg@ZP!RH)!o36qTI9Ui&)&F_wVy} zy@{NDjH$HVIx*vGS5V*~(*wKiyQwJN`SdoLNzsn0Z`HXHZoBtye{6D(yZ5T@O!PLc zKhZtY)J-lrU6?9hZD`!z%ysx8z_Mao||ZzsGy{6yo~>4HUFxOj<4-S@NWzo}2!U|xLfL;tzj)dF)?-Fdcd9@nFvAG4FE?`7sG zxu2b1$R*CJFajecHO#WcK!Z(Z3lujL&#=^C?L4Dlg~h+fQ;k~__7+PO z3$6)x+fbBJQ9t1v!*ib9jK}w|S6t#eP`6k^z0GX>vtpW=rFmmfK<)U!FGvTzHk{{AY@tLDS^*`_Fy3Z<=uDJd;!6 zB+0JZ_dW|m^T=DTK3F%~cdOpU{lSmB zzc1e~mG7L-`OC|K#omRzGj+cBbywO(<0%`a@`+u(=yAV$X2;cAG0(f^y{cZj>b3Wg zh3mF|?>IH%UDUOFx4$VGe|5RdLwO91ZRh73T@F=rZdqHh;f34MTK)NK%V=uHLlw?vEE=b?ExN(3p8mbkgltexWl+ur>(EPqbs z>Ra_5(rImIjNPobna}$DJbt$w>itD8O-sEWUbkIS%d2-{GTUyg-)g!`i>}vde>l0V zan{tysTx-noUh@RFnZ;>Joabx&B?bt9z_3p7qQU)me7oN*G|cv17&x1HPEYqQ!OYC;45pL6Cw88mK9x7cP~q!D-@x=7<+oMSMXp3$%WKNBF|mJ?k*#aSC4Apd zTW|U5?Qd>{Et>cG*a1te)8fuaV$usPCLQ7Et-f}#lxJN<%xvA9oZPc_Tdlvpt1bFJ zd-hKIvf2KBcP0w*sU;ei9F$1&k286+>CeQ=E0yFSC=`eLU0lC8%Vn{HRy`7So< z%8edOkt z6@vdez`#gw>a2enOAOz=J=@=&=)Ak_&l1hu%nS@3hAew!i?-LUv97wLnC!p2AYv0( zeaDIBg1S}lTyNLCE#Jm^^7-ahM>Dq{Uz@qn$17*ys$=QW_Sbp;IIw|So6xbN@R!FV z6;GqdH4_(B%}6@Bw=!nm9FBEmXXC;QXTMkRc0KFyIavLoN>Nb}mzYk3fx5c-@&`H* z2G7&mEvM^WUbBC#^$oTc@4ZF@ZBZls#Msm{^o6K582&zk-47Z-!E_Ngs5(xZ7QR5G8X zY>0iC>f2YDRFf4rwPS_uH1T_%1da5c+sx394t}n)x?N=B`NJQl_e8zD_-+2Kk5*Mr zZ(E--4PCZ%_y4wa*`5>sS}xf4>C6>Ae&2=9o*$o*88mDD=B*B=CuMJKHF&z>hlu83 z@9Eb?!aH_)UJKs1ET#F7*|H6DzJ&I)1}-o;^=Z^eZ>!J(-9^}Ab+Ew6gaUcD%^WM)YCjD;PhGIdRi-@}yd zn%6EVyHk);^W@Bx*9T>cPwafEYZUh Mo(IzzyOwA_~~e_iT$+Ma)fkCk`gx0SVv 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..f2a7dd342071fe49ffcd0a6202b239cb124121f0 GIT binary patch literal 19896 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 literal 0 HcmV?d00001 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 new file mode 100644 index 0000000000000000000000000000000000000000..ce34a9fecbfdccb581ac412bead05a16c39c22cf GIT binary patch literal 15764 zcmXT-cQayOWME)mu${sn2%_f}FfiCc#Xx*iEafJ>B~7MHfh~cdkB>u(v#D5+gRAkZ zMC0ZX?iMWuMl&WA=8gas1=dz6_RdtEFzLcx(~8e7y5*}^xiC+2(z>xxobfs5_JAGr zc3dab)91bU|9{?sl%BtKdw3awye(RU9J!XRvWiuh)aW&JRyKdHu<7qIc@b6 zGy6kJZm_BH@rd7Wx4!tlMecr<+iv3xxljCyK7IOA|L^bj;W~ zS1D3uBjaNAL_yOpqAs<8@r)aptS9uFt-6=-#rtp8|Bk{BfBqjoX!YH+kTFdlV2i>I zhHw94e*d@fWZ!Ukg2~6XzdP6&dS@DD9sA66dd;;Jn*V$6`#KywzS3;XiR{w*e#dd_X+DAUVGOv1f zM(uy==}z;*m_>^ELZCC@gls{^4=kZNUq!aQ#zEVErn5`iHQN{ETg> zk@w#0WfNjKb^V);oVI2|!~e}Me{-Jq$Ui^1`If;c#Z^|(?#Y6v|{_x@a!+Fui^B@11 z@#BOLPeE=Lb06=V!e@mIpIAR0uHRVlO|PGS<7_1ruQxo8`R909ZTOOQJ}^Y{s9~X; z4cFR0jUUhd3jciaTxja7S=Uyqv^#LL-C)VX0~rnhE3UqLc*v{itjc>iyJ!FRXRWv}Wo z`PY}}$a21$xOC3`aMQ>(?dv(#woK#P?O<>vM63wZ-IH1qbs5rbU zL@j7%&J7N)#TtTV{!F>3y}W(15a*|$yhT5mAK#hlc`NIZ|NLJq{x|RHumu{doEsjn zXU$!Y>)MLHCf(|I{+QjRe!}kfia_zjIkzfnOD3}GUtfLbZl1ki@Gr@#>eGoU&gH*9 zm@~t(f#YlQ{GU7Re+d6+RcRFVY*Lw2BCcHjq5b9jXK5_eoYNGgoSa{)NX=}U&ON0m zY?GFsZpMk3E=Hs9tuydO}6dW_A6Odx~=Qq$?af zUEGm1dQ~cFbo>q{L?B+`gYRxl6ywK0I!&V)Iu^$mjp#r8AqmtyKyOyO!Jk5}$YH zYot2g%k}Fr-t_sJY(DojBW(Gz%(ms$>-_h`zDh}zj2AOsmLu?KhK#SI{p@7xI?B&WbSBc)Qh6)z8rWXCnLGf*{MODq_r24Z>#&ImMKO zoIUa2g^8-$*%zm|d{(ZX70jrdTphB?bjH4K4z(U99we{)SN^tqX1D(5o9#jg0esWv zvIf4hceIME{AhjF(6~0irGLiFuAQCb(wFL=gin%}(p*?Bq#ATuA}%pESy5g$Y0`zz z*zb4Cg9T6D=o8I1;a++|>FLJPSEcrT>1_2W`^_Jam%8-FaaOMQIKNj9{lDw9yzG6u z^mF{7smssqyw4Ey?v=TD|BhdlyLL=Jto^sMNg#?*vQF!?=+h$2&=bO-j~POIO{0s`AYyeOX>%>Evf0I>Wv`vGk8R%iB@4@0-2M?ys-D1ReM& zkg#I*?@#XWTPEjJ2rRzs+I%wK|9?o>S$pM=TvGxbOsaavx0c~XK+j9A!`#b4A2Fo% zH{`0uEiX1b6Si9Z;EzbP>D?>57Muz&RBPMKvDxkF+`1m-b#L6SqzQk>ZZ0*Ox03hu zrt4Qf7OhuQ_?)&*FS|f!O5PHkJO8UZ8%5_FO=OkjeHfQ`?dHmlMVFSFoRJscxPEv^ zOwhm3slTTFvJsu!Z?Ig@z1i=AMbM+ee92R;ihdE|n60h7RL*Ek){{QD7kn$%y)ORQ zw{TXq)O-CWNm}^m1k*~+VZo{u$RJ76S-IR7W zE50A$GJG;C*q<I~U$_G-bbQwLv$HhVQb zG}$^!S7D~)mZl4g|2zaba+m*ix_&M=tDpJe$Mm%^uVT6mt;${!x@*qNANIGrqpX#_ zMu~rYB)4bjM1h@cZHw~edKXR04i@ru6VMH@T`Cpz;?}Zu#koct(>Dgj{r>f|MlRO0 z;S(Frzxf{Atd|*^8SI)$pDu55cE9^Ii2Y!dlR=ObuY{xVnh2$22}i{hCRK}XzIyON zq(Pign&ELAdLKE3c|$EqnN zD&Nu=`d@e5aNiK@;?t42){8XKdP_( zbF`&C;_duvKl)>T%oqLlKl|~A>`sOs_Obr68EWj*4p!+FeE6;RV*R~?AE(~DF!NAG z_B;0*TdS(&*6)m-5Lwao!TYva)_umW-v#4zHDk6kTtD&UY}@ppC#OB1s@z9K)KmOXC_cr|d_sdRs zp+B}HD;`^>m@)lPMPU0h=4HEUpLZW9NNza&=_iZW`DdvsSL*!tdAnQQ#2|cnlVgGh zONXG6QwIkRD=({T%VhzzMU^&shq)&^uMTv*5{ z)!H%PK^IT_o(ce5pHR zQd67UsiJ%ArtilW*Mv{3lydLm&|DF3=;PvfSa{=KK}#N%cQ^K^G8?j+>L)2g+fGn; z<`gaX_|iAkn92|LY+X^>1{C@fNTyfd^lV7bByYV|>-}j>5Qhf5y)31iieE8}T5|dCMhLW5dtA$9saNio{wajx_& zpR`YCZvK)T*DqdHS|^bI>{rE(*Dc-uzAn(X`tRrLq}?*|e_Gw2aQw(!%g)K>baTg% zsb2S=8mJ!)cUE@O3gB|ux7}-AmEwI-8 zf*$h%cZQ#twg(y*+#Hz0*xeRPQ&vbQWiedw^u!;gvWbmOm%XPf+`F#o_4@{Hxr+6I zl9v|kTlKhf_d7lfx5FeXg+rcIiWbL`-nxi9M`KYOQo^8VxaWP2V9&mJp-*U1Z; z^@@H8WX`&-aI`HYr02WjJkOfj>jAA)b^;= zN59@F_)H@2sr&r&kH@m}dpF+}z^C#{288l_5)`djDUk zTG|)d{Qu^S(&qtZk|v#8HZy6}%OwpL{Tb%4@f=`aiKu+v7PfQiRh#>-?DBZyw=dI< zKCOFgTi>rp`TKn=^VCeNPjtCh3Ey^dQuWe3JLl5gtUc3HD`J=Z%zVRq+IY``e@@$` z<|nh>3(JU2lo!o(Tyu5VqHWjGiZAo5-KyW#`l)H%wcXd*w!bdl-1~0J$!})2Z;Jij zcJ3*M@|L4|ru^;CwHRnE`%l)jUn0H=z?WUT-_T+a>>#Nmvz1OXGezo`W@=yOh zE#hBQ`(?f7Uv~ZJS6X+T$Z48Uw!@0_V?=S1N*PJJF|cF{46i=Y)>(x z4NF^VWXo+CG4t}q#LQ_A`P!fE*xeSoVf||JHJ#6@403+uu{|g^$eE{G;LmG)@YsvC z?uPKJ3MR98+lxfo(hr`x{b9BC`vdvwX7$R)nDWP4U1N@KI`i<{y8W7IjKROPGv`ed z)3xCJeq6dpX71k92@S_sRHA;_yDetP`{SatTg9bw-_AUyED7Pgme1Dt9^iE_jgC`W zl$f-x>Cx8plQ^Ak{5ACHIs5vqGC!x)BKc^W*0|dn#QwdlIyUXzr?QX}x&AJH)h39% zQ#|6Sk$A87?t7Dt|BWhFXI$8PyJ>pe)18H^_s#_#j1vC%d#U)RX`5X71kO&%x_&%* zn!cFJizp5*&NIFy%iDL}Y7Kh1Hfo>VIsKNaFN%~bDt1g~)Kz9nVP3s}!=~V79$$-& z$`jY18C>Vn)^RR6mhxCReB#Ro|JQU%Hk61e6fG+%zT)(VcCjWi*ZS}uI-gOJQxaNObGvl0sl&hU(Uv*qo@Wxo zGCN7AFxw)t)xz{o>)D5@d-l~mKOyyY@7%mCRx(N3U)+%KImKgqIJIh8>KtJwo6clK zIk}Ds?VUdwBi^&mkI?*6y-oSvi&r6=w+823jN;GCy5v^%_-w{Hy>@Fmf#zv$n$96d zrz~Q*Exc$=N2RZ7=d!*R9k)2jFFN?RKHohjf2E24k;me93vVj?D~(W|o6hfi(aKNI zM0;Yh=dG!WJR25B?z(8EU^c77T6jrcnsed9){2rHPjf|cQf?|NC~SK+$HYmn#LVUS z^%KUn9nl3B6Eb{d6|!23*VxYA_Vl-e;ic?dK~4rPTAm_uN&hnCj>vKzZ|S`VP7>___^)ug9nYwGrPU*1!h*5}{+_SIA;6n6;?&HrJ<-h1zB;WX^W)v4&SYoUE za(-!hYAa7!;k(00s>&T}|Doo zoE7b}pP}G8PlvrFd+KDLRSO>e*LlY&IrY6`mG&Z)nD1gAZ#I<6hFhNDw?7pq%z1h8 zw!Yscl2-~9&Cl%Im=Sp2t2|x!QSJ-gqzz9rRy-2xQ#Q+uUsl2|$+*5|o>9N7cVSKG zOaJ*Pi6VCdrdyvs9=qCMk=F!wzVi%1_ufuB{or)6n4YER1*Uu%jq+)mu2;)$*f4p$ z`~5Iks)`>%MU^-*nULd);_jscCF3mmA79hpXtW%(D7(XG%=O zAwdO!-nPx}&BU4uMRJ4vzIB~`J6U*kpLAcl&?Ef`N)I}e>UK&c#RzttNjx>r^VsWs zhRS01)k2%Jt#28>YTBl$wbS?C&Ctmeo7rEvlsG3|ef;jW@Jxff%u`Ee`)av+I!wGS z_0B7nOGkR%t&UeZtMla?PjeQA)IDpO`cb2V&8?(Dsh4;97kf$j+V`6sxg!g8THZZ9 zqtD`ISy@zJ5-9pyYsJqW6U?TVU!Hz$m*=T@k?WVgS95I@h&kiNeCmJXaU19SX5Am< zs{dQuHhc>@nSZu|ElIN}=H2fUUa^NBEQN7R9pS3=i_G$WNktw1|9gR4_)}#?%j?Y- z?GN6yzFQq^w=iwjQJ(TM7sFQFF59Z|kh5V!fPU!A#ccC~1UHE>eOC3IvE|=Aoj;0? zU%00qUhX-qLG;K)2bs*Ue-9TFY@YRa%1X8Q*S;<;|8F2_wDhk_pl#XVopqg# zPV&t>xA)^5mf4}Nrt%3+$Xc-X=Ox=>Tc#^EZYFc;l#}d?4y?H`|4HFn?eAxIcfNnL zzO+I!pC^rYkUPkSqK)(C`Fe5u)?C21=b7Ih{uy7<|l z;Pgp<^!{zGk+^%rUSol|$o9BhrFT;-HA;9+#wnjnToquzvq0+dJ*As=MYe%AopYbQ zsn1;^sGz3*t}|jwfymw27BvcK7Y?stz8;V@N%O5mB>QsKS!$2%%iEp?&kbz0{noK# zpY!ftKMwcy%=`NO$&Ssp3-1FIi4oarDxMRJVfyF^dCNsTkhdvW`Xf zk=v;r+0FmedT!cpRL}iqza%LqFAvjTSRYv)L`Z(K;jCGTn*o-Hi_m&rCfdyi)1bNf{l6T|T@f7dP_n&b4T} z8TaO7_{ON5%FK$01M}_0x7{cf-nwRG_=Fj`@|AK73$xBnnvlDorGMQ)!CSvnPp@3- z8S^`x>Cd&TZAl*T(-+@rl$@d%yN>1SggYlb-sRl-`1EuZKjxWNiUase8+J2pTFajE zd(V`sEEBG*;qM7!S|=&RA^z-B%d@1JlRY#`Cco^KXjG9|^eAb|TANLFI-L&3Ung}Q z;&@SiAhvKfOW^wyp=}#@=l3k#vP0q~!w1zr$*wQDROC8K+>Yo+Ry{P-4qUw>=8Q&g z$kY`o-9F0eo~Fq2HSV3pbgNnWZ?u_EV85ZyC!Y*P%`Ypa9KX!|SDQKbRkuj(61DXi z-cdTq6A#9_$+EXOCdSRO=nS&HdpT;ak<9J%{w0h>p*JPI?cnd1TFlbWuFB24$@1t% zW2t|8-c*Mr_^$7?+;uqbv|&f(lva-E0sC^5*85$``xrWB@77~F3Wf6XfCU*_%0pQm>w^|9Z@G;yODEF}*d;^Zgz zf6{I95&ZQ#Nt5~VVpGj0dsR+P+;Or_zFy4JI%9Ig>H~p+w|sngqOZjDId$%Y zaJiIq*L%cmc1|^~+&|k*k-FX8o_nc_EW( zem$`>K5_rfL#xQ8`*@@4-m2u@kP1_rn^}Br*{*BdTi@-?&MiDJZTAcLJr=vyonWe+ zV*Pvakt6uK>b#G~fM&KHWClm7j*h!eyRcQG7|9f_Y zHJs_S@Y(o}X_x3MKKWcV4-9LRn_g983Q&(}QPCN3y z@Xq;%XYc+#8lnFrfkP^|>A?4ouj{)i&8qLdQi(qKZqxUWM;~7N)!nS;6zv(qy2fdZ z&hoWyU)6MnK7CriaP-p!_VO3aHI@fXo(xXB|LpeeU)x?8i8EOpDfv2CuWsX)nV%V% z7lr;cdBMK*s#3Mt{3lk#eSFDU)*ngM6Z_3#j)f+b-*^+NFIwp_w<9@xwz1Q(yoBU0t-n(( zy#LO1`Lx+;H3MhXvd$Ctf=(QN)VWW+_vg>zi8qhW_xhb!}9AL{e2@+y?uQ*w`Plp zd^!JSYW?TSd#09seEj6n@dKhhFQ@8!oFxA|GtK<$Yhf=N&Zlt;tXI}-bi9-{LB~t$ zJ)@Uk>QSeYe{NMW&zw``GL7$p(xz9JEqNa>9>4bGU!m@7oASokTdJR?g+`bB_We7n zukn%f0l)LtzVtn~e!lM3hN!F6lB@SK#TIZ_d(t_ap_%=1$tSHNnx9Pqr=cVGmE- z4UXrRpDHQ8oUqTq!&frD`6172mfyi`t1i4ezWQFHlD6|gRk1#Ke|?$dTa7IKB*d@D z&HjGfH=gM<&zHpB8$xe1UuuasuQZKx-6dwUg6C3cR9K>$mHL{CRgx1f9V~dnR5V5A z^y{mqQ@p0DsGGY)t-E#pyA#iD>-E0T%*-r)lM?qgsx0P6`WVVAaj4SFa=Y1f-4E{; zU9hV9_uuU93C2smHCh&)JfO5LjN{^#STU1u4!Jc&Vdoq;-_7dvkb7Bq{|n0#gMHc} zVXrN6dObth!e!K5f6U?N3_1AJeaF>J-z(0jOq2OvcRx+$raCL_ zrAAl%EZa5x`S1NRKV0V)XV$Kd+Wq-T;IUbr_fsXJPxUpvxgx&(W>216+Km-A1mA7E z%HG3Nu_BZA!!d;^TBbGU3^uzw+Z6Upe@^1x_kS)Q-}iiuby4-|#`@etQ%!jnITo75 zau*+xlysW0Ci&>21HMhcT~Z1s*bYx>a&c0f5xCNF&(fFP4_aoPYxLpKtZHZ8YTNpUKg@Z?#1xS^7p~t(622X6C6z7R|8b_uku~q0c%@#b zWWB<^>)Us)yc2h9uZXPg|9SY#_jmWdY*YVS=^u1WXo0cjYWL4I|8{MkHSgN??xX#+ zi+$=A@FdQh`KhAn)#E;vVA}w*OCefSHp?Hr<$HKO*wL9aKrmlJ?2nAhUGtoSudgzt z_ivS7arpYhIo{&`wwGjVy-~a16~l#v^NyB9OYJ}Y=bv_7kYJI@>awRT`EuVR^w&#g zzhGRZCY8P{cHaKKf2YrK-N+5T{b{<8vVr~dxG#d;HQR-j zb_x77n!38a)%k7sW6s>K$JqR~dEa{V*>iTWjIPe*fSxGj9<5P_@5VOuokW=`OS2=yiKn?^apCv+#>hpXB4JMF(~ryry`* z`r}8F{#Vr#s@x_r>ClBkrBsGl zCopSvY&R@9AF9@5K+^?FFW@LKZ7Xp4!iULc8YQA;k&iA3T;k zDNIQD(0mu=>C@x+*Dsk^Wd2ORqfkU}NoaiRmk_6t^(U4tJ-wM@k!Z-XC#xq(x$cb2tVZ*q#?Gag zKKJvY?JegD zRgM`Y{F=0FZSLKF-=bOCdN0-HtDIU{S@`1>^InZV={#q`Y}otr)SUY_^v#&-bA0c{ z)3>5NJ+q9~RQ?DW?a<$S3$cTCtrG&_iVq3k3h3`TF&#cx))y7O}UaB@ok;pf;*?f zt5>egGv6Bd_G8i0B1NyTsM%}9HvYe^I=iXHp7mjCVoUlh>y%5oGdblX{66?qt~W8O zGhAu7!bot!RfpuoeNV09qH_2O)|Pw}m@k-7W+f+b^2;NgB-xWnk1p9fwn_6pdZq5n z^S~drFIG${C|wv_#pZwKfQvoH0S;FegZ{s4o~x5DXXix}@wffV3<@v0{@*mRYaXkQ zQCccT@Uo`Z&Ph-IJy5>6ae>Bx3%4wI_r?6k5lL-qRN;Aneu4tg83soS{r<+f#`IVd4}cpnE|5!p-cgPg_O9@1)#* zskBk=a>^Pniz!ywiBD#(nRP?lD4}$fNa*|T9H08#|MjSRI4@EYSkUOVFKXL%f%7Rl}p5;ni!oT(!uB3>(=l#>!1IPW&rBzjc0N zfA${6{0hTh{#JfbuX@h^Ol9qUE45dtoLm;gv*=!5#*D6`Gngv(te;=>Y{9i%8=QYU zP7lm^6Yyu_=A7djuW*RJ-^}N~T*g^x^|Y6s+UlJ*Yu0|eXS~Pm$>-Idcld8TI4P`# zOMT_j6%#*QE%7*UJ>-pD$HMb}GY%Czx5zKQU)LY>Oqe$-&tdAxk1s_Pw>VYwTr~)~ z&GD2~M3=99k>&a88doNT&v@h`uhEQ}{PYv%ZJZ}|DPw{b)6J%J~E$N zVRM;PbNLUO%P$S3o}TabS+(iVk%SpqQrRk}CDHsM9i77eD_N{}F%Q=lS>ZW;45fI$qwYK4aTf zll%tfcQZdFG?pwod_cSXtDcnmhw!%_bYv$_$}5s|d6KjA-XY-!OzQ3TIh?i!)Wv=h zGYUHGttfi%8dsz1ZG{yFS6B=E{9zZ41xCVK!>LYh+m@o23mSgEWs_ zvp*5B_iw~)l^bh%n@i05l1(-hScugs6wQz>+!=NAa_Uugrnw;=xw7#Q2bAtAFW9j0 z_@y~JH#pDlbe*$FQu%z}FZ>>0YZ||j}-D{aGBCjo8a4Gd)K)3XP$M=#wo!;EY z-TqFs?!VTJ=l>I*dHku&Vw?FSPk-thhS-0eKb3s01?zgA$d@~N`Q9IX)fbmNj%x9) zQ94|ucDC2XDdg12X0e@1;w)~p85@1|E_=H1>f(yB9@~5FPx+d*EZUaC^Y?h{fkpl) zb544xO*#=K+^4aE@!ZzU29~++j`VN-cw*O*jjEk5b_h0~+#WC^GJVp^7ngrEhf4%@ z>qu6%blm?IU~ur8&dKhl$&+?$ufAwDQ6hWyGG)(cPj_jYw7S6NxFA%0`3@P8*y7#u zu6Z#ne`ck*BvZC__qxa|wgl~MQ=3GUIf|4VJuCM87kpZAw8vqC7)t@G%AVN{?MGMn zsMj5HDU|b{xmHWAQ|tJmbM7;DNf@^+HjK)NuNG%aUVqf;*p)e6uC2!=O=T8d>FxLa z(Z{m+?-mFB_Iv;AW7*s|r^3nKKK!WmTvA$dFY3&~pDDFFr+a;z`ub$*+#s&4Pj;$H zo=B6r>ZA5FAk%WjgyT7u&P^)|>udJ5|Jo}bByxkj_Pb(M#>x75GfyV0@jARbPJ63y zpg>E~quQy}TbF%tn4x=4hwo*lPn{rh{H8GXyOGS1v5Cn}u7Nk_s~wE&F6W+h>df-w z!`7u!=H~Ps$NtOIQOKyifWjV@zDJ4>7LU;OEXU=rOjWH%+I!uy~f|enSDH=8`0L zt~b(OewaK~n_D;eV})at>r0XOs#<<;HTVBEc^_8V#qde0d08mO#XA{|`!-~qn|ETK z?$zjN%ZfQxD=k-kyd-)0`D}|{CeD{z=Q8X(vxZ~u(wNnc76d;2zis8&u#VVBPTsvg znk0X^ADVdiN=yHaPv7F&?c{hHGM3k+h$WKcBnmT>M<$~&yFW!Aqx%cRN zTkfLV2P?KPYj+<$&6nh!Yj-6<|AyInL9>}kI|I26iz$_GeF(_BdPenpY1U%L1L~!( zm)_~jzW#IbA#M}pgD)fQUM@Q_Bk+)0-3;5yi}a3rHa4$UJicST!P9=%j*DJ(jxjxF z7X&}LxlQWJoV;{9bNMxL?zxL5%wBPYW2TVht_fRTda7jpTGDAyI3dVUEj-}Ehv+wI z8e0lx1!zo;s^szW{H0|1vwO*t{qB4x=@UXG>|1F$BXIYz zxZ1Q^KGoNcAC_5MWAP!_HDRChlc1||Z?Env3Ac*dz16*XtN-s(2li5r_EH!3(w@hb zyygy1()Jf0jFNfvML7Kzqwt$oiW{cg{jPV){nmB0sHxve*6#Z5^X*!1?Br?tA2{eA zE$qHm_&EFL>g|S{lPAYJb3Dq=7E?6!63$5SUE*14`Kf8PeBj&jzrX(ezJKff1LFMG z8ziqU{TTnh+#%WeLVe?{jlRvxF4xSe+?07EPjg+G7TeE_>sWNVE6o2g?|Y@svha?G z!#3gl7ry7DHO+m0wdRIjb`11~ zbp{DB?Ngn-zt1yVby7a@F5`9o?7aB%zxF3ZZ!vFb+V6K!>R};Q z6JJLEUiGCepBViF3NGARcIn^L{R~m>ZMFu#|17A@u#8hb?DfXJm5=7E`}*jzjd)h? zoqnoHcLn?edvb{MG4pZ~fexS8PJh?Rm(yK6BeutI6V% zZ}zr44d12wmc{yiYd7Oog$ZB0O~or1Uo2^IuvcYRP`)hO#P@rO@|s8It-U_^xKzGq zpPSb->&uclGp~tdSt7w}I{24w*_syQHqE?s-7e-$m9IG(pC+&GEDhQ35pv%V6$;nsFh2Apw*}OR}rhk2a^mm3^%2T(n22OkN<|$|3!7>Jme??2aTI^V7#w)@$ z@7Q*~B|W#EcI4fYlLEdOyYI@D>ecEAw{|9uxn@p5u<|3*3D9juMlZ ze}`49>Ui#ryBvv3MjJ_T<%ErIg%>&a2kQEnjne>q^T@Z2ew~ z4IL))>3zD8zB0IXn&z#_D{M*&xEp1=PE4GTs(AU^a*hbskmZv@5*kdUy&iTjzgsbD zjoS6A!kr6NJ^!k}A&?fcA?&H7MyASai<}xOzO*}!R6@V%Z<*ZnQ)||_DY93Tk`9EZ zSuv$fKXu~i6!rd1W{TaWS6Wh*&iQhR=S0iq-0LZ;4k^2wEZ#J^%+3E=m6D)%s zKYsE_&P`L_B>o~xSLQwadmUBv`nTgPAO<_C9L?Oii#b*S-Iea$~? zt&=ZW&dJ=z&Gb;p(kEM}@a#(M50lf1*7rZNZIe6`cYMiio7ZtBzckuySOr&oowoAy z^VfKmFISJ5!aqP8Dt{zG=mp zPXOsqtr*OO5}Q`=L%vt{YcKK2eo9wdK2T zT(OtO5xoZyEi*brC(le^nq7OekL@{I;r}E?@ul-u8Y{fD36F2!lANZT=@I9zAAK_M z=cVM}?|Rlhi%ibRKHV}~vzvM1lG>vEuIabuUHrWFd3mF8UFpxQocE4IJMaZJ1effw z*rOkF>-pa9d;Z_EpUPZ39)H2W^ny~RV)GoUqe4tU7kJz2Vhw{|X=~39y5RZv$=tog z7Z=Cvp7Q_Qb*}Bpr(D0IWS0LbrFfnAG~W5y%=&y+PxC(YDK^pDx=wmQ+*0<(eB0g& zPTa&To-oBn?QdpI&kNHhMv;w2POiG~^+fEds`DI^nEecPX=V1#U1GC+b@@hpM*(A& z$(KqO%kRBcawc@mmv4rwOJ~-8&CT0XbN}1vlE4Ou^HG})E@_=3F2KLS`ia}We}~Ez zWT~%wA+qkow6A(5+vJuWuod!Y{rm8d;GFL^QAaH0)0vweugH01-Po&t;M?y*6D56S zRP$X-Ra&!G`=UYAym;ZiQfC-#CZB1$*~5KKKiiQ@)z2<_c7s{YN^VtVGoBO|t2u#R zT>d^XTe4&AcP8d#zl)drk9A{N-tcO^NO^^I{D=6QBe^UZ6}uVE_t>3YtnkY=ukb#b z)jPq%WuZ)KJA!W}Oxd=ga6|R(X&bG7Jl>Nr@uEe4bA`9g(g$L<58bYiws^Fv=FLpK zE79hY&Z`=3U^;MmDO20i-M3BRJaYFeYyX}v^`nJ=s$Ayx51ro;)JN z+G!N?qgjeY)`+K!C+G9ZbIWq3*Ify_eAarZ?@YO@q}jRttAr1^IwjZg_U)3%GFT|a zwz*Vo^`}*wpY5keS@nF)Sj@3KHfFZ(rh}_=b3(RU`7FIoG}io#4U5czi;cgVLK)K79 z@4VZp5_IRbY}&3Erk4z5Vh@zHBeSmfWnDAfcck3VVWB2}eq&y_!J^3)QgU5QHbpH} z8HfF*^qaA)nCj>xH?Lumc8J~+?^8mTQdju3_ohgSbuu1nHD0K-=;WOK-Z{6_V)$4@ zF5J9qaVz9+CBL5@(-al&_miup&1CQqJdku`v&gDlH8$ZQ`hmMNs+s5IJXhS${m-Gt znDw-0{1@3nKNie>##JuEyQ5$oZ+{P`5{KeE?m4Yug1cgDblV=r-uZWB+p`IaBbMh~ zRkFQ4sj~SoC(qMg<<6`4XFl9+^1G$zNxI#%+WqNk%FI^DM=q^8v$Ah>m*>%K_4016 z4!y6dRwUiLcwFmJy?CEe9K*d?Ykr9~lyB$FmD>Gk#)V7w99;|?9DXPGFwUsRSMS&_ zQS^DU@piN9S1K#kRxkS`dcI?=UDgbymYHGf-Qi6Iuj@5e2Yr0>f%Ej4zIoZ2frbKH z3X8Y#i(Xj&<|W(2a2wB)QvbVDHi!tst?Y>vT5-Kqr;*eBt5>lUi^f_ z%#RaK?%Q2XU$D{i-QP_oD=O=G=DGj* zy6EO3xfSzz>cl5FEk1tqyY#k>Rc1k3&9i1*KOgHD>*d%J^Wp)gd4(d+L=OKy!kVR> zqWKxqXMD)FtKsIPzhN$u4?mPk+Cv)0u^bLVs+nI#`8tX!{a3*Wy`lo=&at1Woi zfAVMc9dmy+xA%XYe`~iI$HB8gD}NT;oWjF$i^2RNYi{DP1RuX?&3P`|GRMM17ML1M zY}vJeqt?{?=u?Xx6e@t)SW8K#$RR`RxI z^*lSI7i)I6)9h?&X#5$C#I%PWO=UtZG=-X*rE1w_C9Iya+~D`dzwHY(rC0lem7e+- zAv4d>XmQTWSsrG_eLn&Dz(-sw_hgiJa!u`3Y^Hhiazs*_AT9b21?uK3useZe3 z`@i7D(_H6PaYz1L-`E>#&Ug1~fwTYKub15IYjroB)L{N%enBDs&9_s#6I>L80u>$| zo6GJW7jng9%buM99~N$A%jB%@?hM>^J8^YxpH4bUCUDCbN+f_{y}c#nfrdY`7ONF>FDUp8Z0qWS63_N%flp&`5BSNcD|hUF>m+t zx+J05JC3L;c6xdh1x=c@Wr@0S!Iq1z^3FYL|J?BUXrVESjq}y{l@AvgZ+`OmgyiOx zoYGb~TMv0Y583c7kGCxG?$T?{s`pC8ET%2mo7K#`&A%YW&{l#+leK|a&$42g#opcL zq_!^q-V)pQkn@eR%p7&Kd3I3&`^4AhERS69Cafh#N#ka&c%Iaa>dQ+lwYRO2V+i76 zV2@!^U}lhb(9;}WWWbfPfSP<7e9Mgbg$y^^t4@@&3c|$oV3|?=FWvHJpb5w zzdzra@jpYGOC|MJU*J_Uy^CcL%ndO_Z5< z_U*@ilRBLX?w6FVAX8-ls@&JL4JZJiowQsF_z5ea7 NGi{Uqov&bE000STYe)b9 literal 0 HcmV?d00001 diff --git a/shared/static/fonts/SourceSansPro/sourceSansPro.css b/shared/static/fonts/SourceSansPro/sourceSansPro.css new file mode 100644 index 00000000..f7bfbe2f --- /dev/null +++ b/shared/static/fonts/SourceSansPro/sourceSansPro.css @@ -0,0 +1,30 @@ +/* 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+ */ +} + From 7b554e477831541283019c8b64269f8c5a01c483 Mon Sep 17 00:00:00 2001 From: Guillaume Bertholon Date: Sat, 28 Mar 2020 14:08:48 +0100 Subject: [PATCH 065/573] Corrige les chemins vers jquery pour sitecof --- gestioncof/cms/templates/cofcms/cof_actu_index_page.html | 2 +- gestioncof/cms/templates/cofcms/cof_directory_page.html | 2 +- gestioncof/cms/templates/cofcms/cof_root_page.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index a6a909db..7a6e1c7c 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -3,7 +3,7 @@ {% block extra_head %} {{ block.super }} - + {% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_directory_page.html b/gestioncof/cms/templates/cofcms/cof_directory_page.html index 6ac5491b..8fe69bc5 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -3,7 +3,7 @@ {% block extra_head %} {{ block.super }} - + {% endblock %} {% block aside_title %}Accès rapide{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_root_page.html b/gestioncof/cms/templates/cofcms/cof_root_page.html index 984b72dc..141ab8e2 100644 --- a/gestioncof/cms/templates/cofcms/cof_root_page.html +++ b/gestioncof/cms/templates/cofcms/cof_root_page.html @@ -4,7 +4,7 @@ {% block extra_head %} {{ block.super }} - + {% endblock %} From 8a27f70e890daf1917537e3a56ce52cbb527f05f Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 29 Mar 2020 15:36:19 +0200 Subject: [PATCH 066/573] =?UTF-8?q?Limite=20=C3=A0=204=20news=20sur=20la?= =?UTF-8?q?=20page=20d'accueil?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/cms/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index 3bb757b7..1c88cccf 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -31,6 +31,10 @@ 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): From 2ad400c5e7e36653217a837ae7daa1159eb11df1 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 29 Mar 2020 15:36:47 +0200 Subject: [PATCH 067/573] Fixes interface cofcms --- gestioncof/cms/fixtures/examplesite0.json | 641 ++++++++++++++++++ gestioncof/cms/static/cofcms/css/screen.css | 131 ++-- .../cms/static/cofcms/sass/_responsive.scss | 4 + gestioncof/cms/static/cofcms/sass/screen.scss | 20 +- gestioncof/cms/templates/cofcms/base.html | 2 + .../templates/cofcms/cof_directory_page.html | 2 +- 6 files changed, 743 insertions(+), 57 deletions(-) create mode 100644 gestioncof/cms/fixtures/examplesite0.json diff --git a/gestioncof/cms/fixtures/examplesite0.json b/gestioncof/cms/fixtures/examplesite0.json new file mode 100644 index 00000000..816ebe82 --- /dev/null +++ b/gestioncof/cms/fixtures/examplesite0.json @@ -0,0 +1,641 @@ +[ +{ + "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/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css index 7d15b36b..5065be39 100644 --- a/gestioncof/cms/static/cofcms/css/screen.css +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -2,7 +2,7 @@ * 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 */ +/* 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,45 +24,45 @@ 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; } @@ -240,22 +240,39 @@ article:last-child { } /* line 162, ../sass/screen.scss */ .container .aside-wrap .aside a { - color: #997000; + color: #805d00; } -/* line 168, ../sass/screen.scss */ +/* line 164, ../sass/screen.scss */ +.container .aside-wrap .aside a:hover { + text-decoration: underline; +} +/* line 169, ../sass/screen.scss */ +.container .aside-wrap .aside .aside-content { + max-height: 70vh; + max-height: calc(80vh - 150px); + overflow-y: auto; +} +/* line 176, ../sass/screen.scss */ +.container .aside-wrap .aside ul.directory li { + list-style: "." inside; + padding-left: 10px; + text-indent: -10px; + margin-bottom: 5px; +} +/* line 186, ../sass/screen.scss */ .container .content { max-width: 900px; margin-left: auto; margin-right: 6px; } -/* line 173, ../sass/screen.scss */ +/* line 191, ../sass/screen.scss */ .container .content .intro { border-bottom: 3px solid #7f7f7f; margin: 20px 0; margin-top: 5px; padding: 15px 5px; } -/* line 182, ../sass/screen.scss */ +/* line 200, ../sass/screen.scss */ .container .content section article { background: #fff; padding: 20px 30px; @@ -263,31 +280,31 @@ article:last-child { border: 1px solid rgba(153, 118, 0, 0.1); border-radius: 2px; } -/* line 188, ../sass/screen.scss */ +/* line 206, ../sass/screen.scss */ .container .content section article a { color: #CC9500; } -/* line 193, ../sass/screen.scss */ +/* line 211, ../sass/screen.scss */ .container .content section article + h2 { margin-top: 15px; } -/* line 197, ../sass/screen.scss */ +/* line 215, ../sass/screen.scss */ .container .content section article + article { margin-top: 25px; } -/* line 201, ../sass/screen.scss */ +/* line 219, ../sass/screen.scss */ .container .content section .image { margin: 15px 0; text-align: center; padding: 20px; } -/* line 206, ../sass/screen.scss */ +/* line 224, ../sass/screen.scss */ .container .content section .image img { max-width: 100%; height: auto; box-shadow: -7px 7px 1px rgba(153, 118, 0, 0.2); } -/* line 214, ../sass/screen.scss */ +/* line 232, ../sass/screen.scss */ .container .content section.directory article.entry { width: 80%; max-width: 600px; @@ -295,7 +312,7 @@ article:last-child { position: relative; margin-left: 6%; } -/* line 221, ../sass/screen.scss */ +/* line 239, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image { display: block; float: right; @@ -310,31 +327,31 @@ article:last-child { margin-bottom: 10px; transform: translateX(10px); } -/* line 235, ../sass/screen.scss */ +/* line 253, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image img { width: auto; height: auto; max-width: 100%; max-height: 100%; } -/* line 243, ../sass/screen.scss */ +/* line 261, ../sass/screen.scss */ .container .content section.directory article.entry ul.links { margin-top: 10px; border-top: 1px solid #90001C; padding-top: 10px; } -/* line 251, ../sass/screen.scss */ +/* line 269, ../sass/screen.scss */ .container .content section.actuhome { display: flex; flex-wrap: wrap; justify-content: space-around; align-items: top; } -/* line 257, ../sass/screen.scss */ +/* line 275, ../sass/screen.scss */ .container .content section.actuhome article + article { margin: 0; } -/* line 261, ../sass/screen.scss */ +/* line 279, ../sass/screen.scss */ .container .content section.actuhome article.actu { position: relative; background: none; @@ -344,7 +361,7 @@ article:last-child { min-width: 300px; flex: 1; } -/* line 270, ../sass/screen.scss */ +/* line 288, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header { position: relative; box-shadow: -4px 5px 1px rgba(153, 118, 0, 0.3); @@ -358,7 +375,7 @@ article:last-child { background-position: center center; background-repeat: no-repeat; } -/* line 283, ../sass/screen.scss */ +/* line 301, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 { position: absolute; width: 100%; @@ -368,11 +385,11 @@ article:last-child { text-shadow: 0 0 5px rgba(153, 118, 0, 0.8); background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); } -/* line 291, ../sass/screen.scss */ +/* line 309, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 a { color: #fff; } -/* line 297, ../sass/screen.scss */ +/* line 315, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc { background: white; box-shadow: -2px 2px 1px rgba(153, 118, 0, 0.2); @@ -382,17 +399,17 @@ article:last-child { padding: 15px; padding-top: 5px; } -/* line 306, ../sass/screen.scss */ +/* line 324, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-minical { display: block; } -/* line 309, ../sass/screen.scss */ +/* line 327, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-dates { display: block; text-align: right; font-size: 0.9em; } -/* line 316, ../sass/screen.scss */ +/* line 334, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-overlay { display: block; background: none; @@ -404,81 +421,81 @@ article:last-child { z-index: 5; opacity: 0; } -/* line 332, ../sass/screen.scss */ +/* line 350, ../sass/screen.scss */ .container .content section.actulist article.actu { display: flex; width: 100%; padding: 0; } -/* line 337, ../sass/screen.scss */ +/* line 355, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-image { width: 30%; max-width: 200px; background-size: cover; background-position: center center; } -/* line 343, ../sass/screen.scss */ +/* line 361, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos { padding: 15px; flex: 1; } -/* line 347, ../sass/screen.scss */ +/* line 365, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos .actu-dates { font-weight: bold; font-size: 0.9em; } -/* line 357, ../sass/screen.scss */ +/* line 375, ../sass/screen.scss */ .container .aside-wrap + .content { max-width: 70%; } -/* line 362, ../sass/screen.scss */ +/* line 380, ../sass/screen.scss */ .calendar { color: rgba(0, 0, 0, 0.8); width: 200px; } -/* line 366, ../sass/screen.scss */ +/* line 384, ../sass/screen.scss */ .calendar td, .calendar th { text-align: center; vertical-align: middle; border: 2px solid transparent; padding: 1px; } -/* line 373, ../sass/screen.scss */ +/* line 391, ../sass/screen.scss */ .calendar th { font-weight: bold; } -/* line 377, ../sass/screen.scss */ +/* line 395, ../sass/screen.scss */ .calendar td { font-size: 0.8em; width: 28px; height: 28px; } -/* line 382, ../sass/screen.scss */ +/* line 400, ../sass/screen.scss */ .calendar td.out { opacity: 0.3; } -/* line 385, ../sass/screen.scss */ +/* line 403, ../sass/screen.scss */ .calendar td.today { border-bottom-color: #000; } -/* line 388, ../sass/screen.scss */ +/* line 406, ../sass/screen.scss */ .calendar td:nth-child(7), .calendar td:nth-child(6) { background: rgba(0, 0, 0, 0.2); } -/* line 391, ../sass/screen.scss */ +/* line 409, ../sass/screen.scss */ .calendar td.hasevent { position: relative; font-weight: bold; color: #90001C; font-size: 1em; } -/* line 397, ../sass/screen.scss */ +/* line 415, ../sass/screen.scss */ .calendar td.hasevent > a { padding: 3px; color: #90001C !important; } -/* line 402, ../sass/screen.scss */ +/* line 420, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events { text-align: left; display: none; @@ -491,11 +508,11 @@ article:last-child { padding: 5px; background-color: #90001C; } -/* line 415, ../sass/screen.scss */ +/* line 433, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events .datename { display: none; } -/* line 418, ../sass/screen.scss */ +/* line 436, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events:before { top: -12px; left: 38px; @@ -504,33 +521,33 @@ article:last-child { border: 6px solid transparent; border-bottom-color: #90001C; } -/* line 426, ../sass/screen.scss */ +/* line 444, ../sass/screen.scss */ .calendar td.hasevent ul.cal-events a { color: #fff; } -/* line 431, ../sass/screen.scss */ +/* line 449, ../sass/screen.scss */ .calendar td.hasevent > a:hover { background-color: #90001C; color: #fff !important; } -/* line 435, ../sass/screen.scss */ +/* line 453, ../sass/screen.scss */ .calendar td.hasevent > a:hover + ul.cal-events { display: block; } -/* line 443, ../sass/screen.scss */ +/* line 461, ../sass/screen.scss */ #calendar-wrap .details { border-top: 1px solid #90001C; margin-top: 15px; padding-top: 10px; } -/* line 448, ../sass/screen.scss */ +/* line 466, ../sass/screen.scss */ #calendar-wrap .details li.datename { font-weight: bold; font-size: 1.1em; margin-bottom: 5px; } -/* line 449, ../sass/screen.scss */ +/* line 467, ../sass/screen.scss */ #calendar-wrap .details li.datename:after { content: " :"; } @@ -665,4 +682,8 @@ 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/sass/_responsive.scss b/gestioncof/cms/static/cofcms/sass/_responsive.scss index 28216a98..7cf4cb29 100644 --- a/gestioncof/cms/static/cofcms/sass/_responsive.scss +++ b/gestioncof/cms/static/cofcms/sass/_responsive.scss @@ -118,6 +118,10 @@ 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 5b532373..0f2001f8 100644 --- a/gestioncof/cms/static/cofcms/sass/screen.scss +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -160,7 +160,25 @@ article { } a { - color: darken($lien, 10%); + color: darken($lien, 15%); + &:hover { + text-decoration: underline; + } + } + + .aside-content { + max-height: 70vh; + max-height: calc(80vh - 150px); + overflow-y: auto; + } + + ul.directory { + li { + list-style: "." inside; + padding-left: 10px; + text-indent: -10px; + margin-bottom: 5px; + } } } } diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index ec3e51d5..2b027e70 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -9,6 +9,8 @@ {% block extra_head %}{% endblock %} + + diff --git a/gestioncof/cms/templates/cofcms/cof_directory_page.html b/gestioncof/cms/templates/cofcms/cof_directory_page.html index 8fe69bc5..62c199ef 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -8,7 +8,7 @@ {% endblock %} {% block aside_title %}Accès rapide{% endblock %} {% block aside %} -
      +
        {% for entry in page.entries %}
      • {{ entry.title }}
      • {% endfor %} From 9dabab51dbd9040d2009707a9d3901a6d7aa105b Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 29 Mar 2020 16:11:02 +0200 Subject: [PATCH 068/573] I18n --- .../cms/locale/en/LC_MESSAGES/django.mo | Bin 0 -> 2147 bytes .../cms/locale/en/LC_MESSAGES/django.po | 117 ++++++++++++++++++ gestioncof/cms/templates/cofcms/base.html | 2 +- .../templates/cofcms/cof_actu_index_page.html | 12 +- .../cms/templates/cofcms/cof_actu_page.html | 2 +- .../templates/cofcms/cof_directory_page.html | 6 +- .../cms/templates/cofcms/cof_root_page.html | 2 +- gestioncof/cms/templatetags/cofcms_tags.py | 33 ++--- 8 files changed, 148 insertions(+), 26 deletions(-) create mode 100644 gestioncof/cms/locale/en/LC_MESSAGES/django.mo create mode 100644 gestioncof/cms/locale/en/LC_MESSAGES/django.po diff --git a/gestioncof/cms/locale/en/LC_MESSAGES/django.mo b/gestioncof/cms/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..ad390d635413656536e15974ade455db8d784883 GIT binary patch literal 2147 zcmca7#4?qEfq_AUfq_AWfq`KU0|SFB0|P@0BS@5i;Q}KA!wm)ohKr003~mez46B$J z7}yvX7(OyFFz_-kF#KR*V31;9U|?g0(2C3q4B`w73^vRVbs@|Q45|za4AIOC3|tHh z42{eT3_1)93_Z*Y49pA+3_F<_7&sXi81^zVFmN+4FdS!QU{GdYV7Sc8z+lh7!0?@! zfq{>Kfx(=GfkBIbfx(Rh;=V!_1_ob728MnX28P`X3=DT!85q167#L==5^HazOkg!2xlfD+dDuGzb}lLE*!| zz`zd)Qm{WkOpw1pG$+N0I?I~4;CmNLsoSB~o;^Zb~ z<|rIqppc{P>KCj~oT`w1ctufaUWr0+X^}!sszPyUQCVtf5nO3-q;G&D7lyR~`K3h) zl?sW;$%j{_q!uYyDfp%qC1--P6&+q#ke`=QlnSyYIkluDRiUsnwYUW2fW*9#Ox>dV zr2LX%Jrpw;Y)Ufhd~EbfGVDMsUo0#yxL6pR6%1#&fLRQV3OSjnr3{YA$%j`ID-@qvtdN+OoSB-Jms*S|Rdje|a%x@)NGdHYGdUx*NFhf( zF{LQAxHwe-6nG4d>8W`si42a##resZiC}M~q!ue2UYT=vMHwW-A%Pp3lgr?on3I~9 zQk0om#NeEt3yK|uV)ev=g4CQGP@sb%t{^|NxHvzrSRtiUAt|xAI5SVdp1~(ofx#!U zC>88p1|LXVf~`*}Rj5u$EJ-acNh~U0ghBC9U6PrL zO)FduWDY}4DzY7DTu>Y%8*_Mp0+@?MC)`wsV;KU9^0QKtOLRR`bi-1MKuOX{fh#Dr zAitw?diuH|TxqChz~!8uSCX1nq8n0KkZPq+l3G!sUyzfSnP;t#oRL^moLXWV z8sesFflvpsD=oE1*EKIWKP59S-AchCDYJwtAg8n_F-O-ezbLoZN+GWR%q_MxvIa$V zQDTm*MxKJAt%9MZHCKeLdum>45yZU#`Kc+HB?^Xm7JBA-hFlCN83;2PbhzAy9;w z>pk-n@{2&(y(Ax$^;3(B6*5b76bf=u6N^(767!17Q;QTzGBQCqtR%CzATbx7^|54k zL?#a@1?OH+&JTyPLNee?H#jRe5y6CLV(Z1`dqFoyqw*sS1AiMY)MNsS3fR1&3D_Wu}%E zr9!iGViAK!ez`(oQK~|6VonY?EK?QIGK(`5GV{Pj6enlo=jW&wD}YKvdj|hJ1%{y1 z#1w_x{GwC_-^9!uSQHgAq!s1oBIh`Tl6(jkl&4_X7ef_H0+Rh8Sqw!dL;_?6Lw+8_ cK!_?dt^&kx8#9XLR0N)5, YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-03-29 16:04+0200\n" +"PO-Revision-Date: 2020-03-29 16:08+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 1.8.7.1\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:6 +msgid "Listes mail" +msgstr "Mailing lists" + +#: templates/cofcms/sympa.html:11 +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" +" " + +#: templates/cofcms/sympa.html:21 +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:162 +#, 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:164 +#, python-brace-format +msgid "le {datestart} à {timestart}" +msgstr "on {datestart} at {timestart}" diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index 2b027e70..f5d3a3f8 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -4,7 +4,7 @@ - {% block title %}Association des élèves de l'ENS Ulm{% endblock %} + {% block title %}{% trans "Association des élèves de l'ENS Ulm" %}{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index 7a6e1c7c..9ddd4550 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -7,7 +7,7 @@ {% endblock %} -{% block aside_title %}Calendrier{% endblock %} +{% block aside_title %}{% trans "Calendrier" %}{% endblock %} {% block aside %}
        {% calendar %} @@ -22,10 +22,10 @@
        {% if actus.has_previous %} - Actualités plus récentes + {% trans "Actualités plus récentes" %} {% endif %} {% if actus.has_next %} - Actualités plus anciennes + {% trans "Actualités plus anciennes" %} {% endif %} {% for actu in page.actus %} @@ -38,16 +38,16 @@ {% else %} {{ actu.body|safe|truncatewords_html:15 }} {% endif %} - Lire plus > + {% trans "Lire plus" %} >
        {% endfor %} {% if actus.has_previous %} - Actualités plus récentes + {% trans "Actualités plus récentes" %} {% endif %} {% if actus.has_next %} - Actualités plus anciennes + {% trans "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 b531aedc..09e42e91 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_page.html @@ -4,7 +4,7 @@ {% block content %}

        {{ page.title }}

        -

        A lieu {{ page|dates }}

        +

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

        {{ page.chapo }}

        diff --git a/gestioncof/cms/templates/cofcms/cof_directory_page.html b/gestioncof/cms/templates/cofcms/cof_directory_page.html index 62c199ef..595848c9 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -1,12 +1,12 @@ {% extends "cofcms/base_aside.html" %} -{% load wagtailimages_tags cofcms_tags static %} +{% load wagtailimages_tags cofcms_tags static i18n %} {% block extra_head %} {{ block.super }} {% endblock %} -{% block aside_title %}Accès rapide{% endblock %} +{% block aside_title %}{% trans "Accès rapide" %}{% endblock %} {% block aside %}
          {% for entry in page.entries %} @@ -36,7 +36,7 @@ {% if block.block_type == "lien" %} {{ block.value.texte }} {% else %} - {{ block.value.texte }} :
        {% endif %} -
        +
        @@ -93,29 +94,22 @@ $(document).ready(function() { khistory = new KHistory({ display_trigramme: false, - }); - - function getHistory() { - var data = { + fetch_options: { 'accounts': [{{ account.pk }}], } + }); - $.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/history.html b/kfet/templates/kfet/history.html index ae63358e..204e0d57 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -5,6 +5,7 @@ {{ filter_form.media }} + {% endblock %} @@ -40,6 +41,8 @@ $(document).ready(function() { settings = { 'subvention_cof': parseFloat({{ kfet_config.subvention_cof|unlocalize }})} + window.lock = 0; + khistory = new KHistory(); var $from_date = $('#id_from_date'); @@ -67,16 +70,7 @@ $(document).ready(function() { var accounts = getSelectedMultiple($accounts); data['accounts'] = accounts; - $.ajax({ - dataType: "json", - url : "{% url 'kfet.history.json' %}", - method : "POST", - data : data, - }) - .done(function(data) { - for (var i=0; i 0) - confirmCancel(opes_to_cancel); + khistory.cancel_selected() } }); - - function confirmCancel(opes_to_cancel) { - var nb = opes_to_cancel.length; - var content = nb+" opérations vont être annulées"; - $.confirm({ - title: 'Confirmation', - content: content, - backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', - keyboardEnabled: true, - confirm: function() { - cancelOperations(opes_to_cancel); - } - }); - } - - function requestAuth(data, callback) { - var content = getErrorsHtml(data); - content += '', - $.confirm({ - title: 'Authentification requise', - content: content, - backgroundDismiss: true, - animation:'top', - closeAnimation:'bottom', - keyboardEnabled: true, - confirm: function() { - var password = this.$content.find('input').val(); - callback(password); - }, - onOpen: function() { - var that = this; - this.$content.find('input').on('keypress', function(e) { - if (e.keyCode == 13) - that.$confirmButton.click(); - }); - }, - }); - } - - function getErrorsHtml(data) { - var content = ''; - if ('missing_perms' in data['errors']) { - content += 'Permissions manquantes'; - content += '
          '; - for (var i=0; i'; - content += '
        '; - } - if ('negative' in data['errors']) { - var url_base = "{% url 'kfet.account.update' LIQ}"; - url_base = base_url(0, url_base.length-8); - for (var i=0; iAutorisation de négatif requise pour '+data['errors']['negative'][i]+''; - } - } - return content; - } - - function cancelOperations(opes_array, password = '') { - 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) { - khistory.$container.find('.ui-selected').removeClass('ui-selected'); - }) - .fail(function($xhr) { - var data = $xhr.responseJSON; - switch ($xhr.status) { - case 403: - requestAuth(data, function(password) { - cancelOperations(opes_array, password); - }); - break; - case 400: - displayErrors(getErrorsHtml(data)); - break; - } - - }); - } - - getHistory(); }); diff --git a/kfet/templates/kfet/transfers.html b/kfet/templates/kfet/transfers.html index f6778b3f..83f20c70 100644 --- a/kfet/templates/kfet/transfers.html +++ b/kfet/templates/kfet/transfers.html @@ -1,9 +1,16 @@ {% extends 'kfet/base_col_2.html' %} {% load staticfiles %} +{% load l10n staticfiles widget_tweaks %} {% block title %}Transferts{% endblock %} {% block header-title %}Transferts{% endblock %} +{% block extra_head %} + + + +{% endblock %} + {% block fixed %}
        @@ -16,109 +23,31 @@ {% 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 %} -
        + +
        From 74384451109b95dc73558d7235b6013e90276c40 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Dec 2019 18:55:15 +0100 Subject: [PATCH 081/573] Last tweaks --- kfet/static/kfet/js/history.js | 14 +++++++------- kfet/views.py | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 5608c02f..98bc7a2a 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -14,7 +14,7 @@ function KHistory(options = {}) { if ($(this).hasClass('opegroup')) { var opegroup = $(this).data('id'); $(this).siblings('.ope').filter(function () { - return $(this).data('opegroup') == opegroup + return $(this).data('group') == opegroup }).addClass('ui-selected'); } }); @@ -38,14 +38,16 @@ function KHistory(options = {}) { case 'operation': for (let ope of opegroup['opes']) { var $ope = this._opeHtml(ope, is_cof, trigramme); - $ope.data('opegroup', opegroup['id']); + $ope.data('group', opegroup['id']); + $ope.data('group_type', type); $opegroup.after($ope); } break; case 'transfer': for (let transfer of opegroup['opes']) { var $transfer = this._transferHtml(transfer); - $transfer.data('transfergroup', opegroup['id']); + $transfer.data('group', opegroup['id']); + $transfer.data('group_type', type); $opegroup.after($transfer); } break; @@ -268,10 +270,8 @@ function KHistory(options = {}) { "operations": [], } this.$container.find('.ope.ui-selected').each(function () { - if ($(this).data("transfergroup")) - opes_to_cancel["transfers"].push($(this).data("id")); - else - opes_to_cancel["operations"].push($(this).data("id")); + type = $(this).data("group_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 diff --git a/kfet/views.py b/kfet/views.py index 4944546e..e4fd2564 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1799,7 +1799,7 @@ def cancel_transfers(request): .filter(pk__in=transfers) .order_by("pk") ) - data["canceled"] = transfers + data["canceled"] = list(transfers) if transfers_already_canceled: data["warnings"]["already_canceled"] = transfers_already_canceled return JsonResponse(data) From fb4455af39cbfbb32346863dab3421270892f228 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Dec 2019 18:55:45 +0100 Subject: [PATCH 082/573] Fix tests 3 --- kfet/tests/test_views.py | 185 ++++++++++++++++++++++----------------- 1 file changed, 105 insertions(+), 80 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 852d5bf1..853ec449 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -3358,7 +3358,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + 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, + } + ], + }, ) self.account.refresh_from_db() @@ -3370,26 +3389,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): 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": [{"id": self.article.pk, "stock": 22}], - }, + {"checkouts": [], "articles": [{"id": self.article.pk, "stock": 22}]}, ) def test_purchase_with_addcost(self): @@ -3546,7 +3546,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + 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, + } + ], + }, ) self.account.refresh_from_db() @@ -3559,22 +3578,6 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): 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": [{"id": self.checkout.pk, "balance": Decimal("89.25")}], "articles": [], }, @@ -3630,7 +3633,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + 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, + } + ], + }, ) self.account.refresh_from_db() @@ -3643,22 +3665,6 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): 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": [{"id": self.checkout.pk, "balance": Decimal("110.75")}], "articles": [], }, @@ -3714,7 +3720,26 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): ) self.assertDictEqual( - json_data, {"canceled": [operation.pk], "errors": {}, "warnings": {}} + 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, + } + ], + }, ) self.account.refresh_from_db() @@ -3725,27 +3750,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(self.checkout.balance, Decimal("100.00")) 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": [], - }, + "kfet.kpsul", {"checkouts": [], "articles": []}, ) @mock.patch("django.utils.timezone.now") @@ -3966,13 +3971,33 @@ 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": [operation1.pk, operation2.pk], - "warnings": {"already_canceled": [operation3.pk]}, + "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": {}, + "warnings": {"already_canceled": [operation3.pk]}, + "opegroups_to_update": [ + { + "id": group.pk, + "amount": str(group.amount), + "is_cof": group.is_cof, + } + ], }, ) From 677ba5b92e7b5883ed1f648294f7707dabcbeb0d Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 11:34:34 +0100 Subject: [PATCH 083/573] Fix : le ws K-Psul remarche --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index e4fd2564..3122636b 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1159,7 +1159,7 @@ def kpsul_perform_operations(request): websocket_data["opegroups"] = [ { "add": True, - "type": "opegroup", + "type": "operation", "id": operationgroup.pk, "amount": operationgroup.amount, "checkout__name": operationgroup.checkout.name, From 786c8f132f03fa5f5c03a5ebfef823240c3eb4ed Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 11:53:08 +0100 Subject: [PATCH 084/573] =?UTF-8?q?Fix:=20tests=20cass=C3=A9s=20par=20comm?= =?UTF-8?q?it=20pr=C3=A9c=C3=A9dent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/tests/test_views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 853ec449..e69c81d9 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -2000,7 +2000,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "opegroups": [ { "add": True, - "type": "opegroup", + "type": "operation", "at": mock.ANY, "amount": Decimal("-5.00"), "checkout__name": "Checkout", @@ -2273,7 +2273,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "opegroups": [ { "add": True, - "type": "opegroup", + "type": "operation", "at": mock.ANY, "amount": Decimal("10.75"), "checkout__name": "Checkout", @@ -2448,7 +2448,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "opegroups": [ { "add": True, - "type": "opegroup", + "type": "operation", "at": mock.ANY, "amount": Decimal("-10.75"), "checkout__name": "Checkout", @@ -2607,7 +2607,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "opegroups": [ { "add": True, - "type": "opegroup", + "type": "operation", "at": mock.ANY, "amount": Decimal("10.75"), "checkout__name": "Checkout", @@ -3177,7 +3177,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): "opegroups": [ { "add": True, - "type": "opegroup", + "type": "operation", "at": mock.ANY, "amount": Decimal("-9.00"), "checkout__name": "Checkout", From 8d11044610dd25c5656e97947e5afd9f2552d5f0 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 12:35:46 +0100 Subject: [PATCH 085/573] =?UTF-8?q?Fix:=20pas=20d'erreur=20quand=20pas=20d?= =?UTF-8?q?e=20compte=20K-F=C3=AAt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/tests/test_views.py | 10 ++++++++-- kfet/views.py | 11 +++++++---- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index e69c81d9..3baed2c3 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from decimal import Decimal from unittest import mock -from django.contrib.auth.models import Group +from django.contrib.auth.models import Group, User from django.test import Client, TestCase from django.urls import reverse from django.utils import timezone @@ -4151,12 +4151,18 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): url_expected = "/k-fet/history.json" auth_user = "user" - auth_forbidden = [None] + auth_forbidden = [None, "noaccount"] 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" diff --git a/kfet/views.py b/kfet/views.py index 3122636b..9d2d2c09 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1423,10 +1423,13 @@ def history_json(request): ) if not request.user.has_perm("kfet.is_team"): - acc = request.user.profile.account_kfet - transfer_queryset_prefetch = transfer_queryset_prefetch.filter( - Q(from_acc=acc) | Q(to_acc=acc) - ) + 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 b450cb09e681a163d1b7fa0e1b1164c856c43640 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 12:39:41 +0100 Subject: [PATCH 086/573] Petit refactor --- kfet/views.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 9d2d2c09..3d9ff79a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1407,16 +1407,13 @@ def history_json(request): transfers_only = request.POST.get("transfersonly", None) opes_only = request.POST.get("opesonly", None) - # Construction de la requête (sur les opérations) pour le prefetch - ope_queryset_prefetch = Operation.objects.select_related( - "article", "canceled_by", "addcost_for" - ) - ope_prefetch = Prefetch("opes", queryset=ope_queryset_prefetch) + # 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 accounts: transfer_queryset_prefetch = transfer_queryset_prefetch.filter( Q(from_acc__trigramme__in=accounts) | Q(to_acc__trigramme__in=accounts) @@ -1435,6 +1432,12 @@ def history_json(request): "transfers", queryset=transfer_queryset_prefetch, to_attr="filtered_transfers" ) + # Construction de la requête (sur les opérations) pour le prefetch + ope_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) From 931b2c4e1f23ed940f96ed0dc508cfcbda28fe24 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 17:28:36 +0100 Subject: [PATCH 087/573] Refactor js code Harmonize history denominations * opegroups/transfergroups -> groups * opes/transfers -> entries * snake/camel case -> snake case --- kfet/static/kfet/css/history.css | 42 ++++------ kfet/static/kfet/js/history.js | 140 +++++++++++++++---------------- kfet/templates/kfet/kpsul.html | 6 +- kfet/tests/test_views.py | 50 +++++------ kfet/views.py | 24 +++--- 5 files changed, 127 insertions(+), 135 deletions(-) diff --git a/kfet/static/kfet/css/history.css b/kfet/static/kfet/css/history.css index e1e1ab42..42e73527 100644 --- a/kfet/static/kfet/css/history.css +++ b/kfet/static/kfet/css/history.css @@ -20,7 +20,7 @@ z-index:10; } -#history .opegroup { +#history .group { height:30px; line-height:30px; background-color: #c63b52; @@ -30,29 +30,29 @@ overflow:auto; } -#history .opegroup .time { +#history .group .time { width:70px; } -#history .opegroup .trigramme { +#history .group .trigramme { width:55px; text-align:right; } -#history .opegroup .amount { +#history .group .amount { text-align:right; width:90px; } -#history .opegroup .valid_by { +#history .group .valid_by { padding-left:20px } -#history .opegroup .comment { +#history .group .comment { padding-left:20px; } -#history .ope { +#history .entry { position:relative; height:25px; line-height:24px; @@ -61,38 +61,38 @@ overflow:auto; } -#history .ope .amount { +#history .entry .amount { width:50px; text-align:right; } -#history .ope .infos1 { +#history .entry .infos1 { width:80px; text-align:right; } -#history .ope .infos2 { +#history .entry .infos2 { padding-left:15px; } -#history .ope .addcost { +#history .entry .addcost { padding-left:20px; } -#history .ope .canceled { +#history .entry .canceled { padding-left:20px; } -#history div.ope.ui-selected, #history div.ope.ui-selecting { +#history div.entry.ui-selected, #history div.entry.ui-selecting { background-color:rgba(200,16,46,0.6); color:#FFF; } -#history .ope.canceled, #history .transfer.canceled { +#history .entry.canceled { color:#444; } -#history .ope.canceled::before, #history.transfer.canceled::before { +#history .entry.canceled::before { position: absolute; content: ' '; width:100%; @@ -101,19 +101,11 @@ border-top: 1px solid rgba(200,16,46,0.5); } -#history .transfer .amount { - width:80px; -} - -#history .transfer .from_acc { - padding-left:10px; -} - -#history .opegroup .infos { +#history .group .infos { text-align:center; width:145px; } -#history .ope .glyphicon { +#history .entry .glyphicon { padding-left:15px; } diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 98bc7a2a..540c8239 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -2,19 +2,20 @@ 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.opegroup, div.ope', + filter: 'div.group, div.entry', selected: function (e, ui) { $(ui.selected).each(function () { - if ($(this).hasClass('opegroup')) { - var opegroup = $(this).data('id'); - $(this).siblings('.ope').filter(function () { - return $(this).data('group') == opegroup + if ($(this).hasClass('group')) { + var id = $(this).data('id'); + $(this).siblings('.entry').filter(function () { + return $(this).data('group_id') == id }).addClass('ui-selected'); } }); @@ -25,36 +26,35 @@ function KHistory(options = {}) { this.$container.html(''); }; - this.addOpeGroup = function (opegroup) { - var $day = this._getOrCreateDay(opegroup['at']); - var $opegroup = this._opeGroupHtml(opegroup); + this.add_history_group = function (group) { + var $day = this._get_or_create_day(group['at']); + var $group = this._group_html(group); - $day.after($opegroup); + $day.after($group); - var trigramme = opegroup['on_acc_trigramme']; - var is_cof = opegroup['is_cof']; - var type = opegroup['type'] + 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 opegroup['opes']) { - var $ope = this._opeHtml(ope, is_cof, trigramme); - $ope.data('group', opegroup['id']); - $ope.data('group_type', type); - $opegroup.after($ope); + 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 opegroup['opes']) { - var $transfer = this._transferHtml(transfer); - $transfer.data('group', opegroup['id']); - $transfer.data('group_type', type); - $opegroup.after($transfer); + for (let transfer of group['entries']) { + var $transfer = this._transfer_html(transfer); + $transfer.data('group_id', group['id']); + $group.after($transfer); } break; } } - this._opeHtml = function (ope, is_cof, trigramme) { + this._ope_html = 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); @@ -95,12 +95,12 @@ function KHistory(options = {}) { } if (ope['canceled_at']) - this.cancelOpe(ope, $ope_html); + this.cancel_entry(ope, $ope_html); return $ope_html; } - this._transferHtml = function (transfer) { + this._transfer_html = function (transfer) { var $transfer_html = $(this.template_transfer); var parsed_amount = parseFloat(transfer['amount']); var amount = parsed_amount.toFixed(2) + '€'; @@ -113,67 +113,67 @@ function KHistory(options = {}) { .find('.infos2').text(transfer['to_acc']).end(); if (transfer['canceled_at']) - this.cancelOpe(transfer, $transfer_html); + this.cancel_entry(transfer, $transfer_html); return $transfer_html; } - this.cancelOpe = function (ope, $ope = null) { - if (!$ope) - $ope = this.findOpe(ope["id"], ope["type"]); + this.cancel_entry = function (entry, $entry = null) { + if (!$entry) + $entry = this.find_entry(entry["id"], entry["type"]); var cancel = 'Annulé'; - var canceled_at = dateUTCToParis(ope['canceled_at']); - if (ope['canceled_by__trigramme']) - cancel += ' par ' + ope['canceled_by__trigramme']; + var canceled_at = dateUTCToParis(entry['canceled_at']); + if (entry['canceled_by__trigramme']) + cancel += ' par ' + entry['canceled_by__trigramme']; cancel += ' le ' + canceled_at.format('DD/MM/YY à HH:mm:ss'); - $ope.addClass('canceled').find('.canceled').text(cancel); + $entry.addClass('canceled').find('.canceled').text(cancel); } - this._opeGroupHtml = function (opegroup) { - var type = opegroup['type']; + this._group_html = function (group) { + var type = group['type']; switch (type) { case 'operation': - var $opegroup_html = $(this.template_opegroup); - var trigramme = opegroup['on_acc__trigramme']; + var $group_html = $(this.template_opegroup); + var trigramme = group['on_acc__trigramme']; var amount = amountDisplay( - parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme); + parseFloat(group['amount']), group['is_cof'], trigramme); break; case 'transfer': - var $opegroup_html = $(this.template_transfergroup); - $opegroup_html.find('.infos').text('Transferts').end() + var $group_html = $(this.template_transfergroup); + $group_html.find('.infos').text('Transferts').end() var trigramme = ''; var amount = ''; break; } - var at = dateUTCToParis(opegroup['at']).format('HH:mm:ss'); - var comment = opegroup['comment'] || ''; + var at = dateUTCToParis(group['at']).format('HH:mm:ss'); + var comment = group['comment'] || ''; - $opegroup_html + $group_html .data('type', type) - .data('id', opegroup['id']) + .data('id', group['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) - $opegroup_html.find('.trigramme').remove(); - $opegroup_html.find('.info').remove(); + $group_html.find('.trigramme').remove(); + $group_html.find('.info').remove(); - if (opegroup['valid_by__trigramme']) - $opegroup_html.find('.valid_by').text('Par ' + opegroup['valid_by__trigramme']); + if (group['valid_by__trigramme']) + $group_html.find('.valid_by').text('Par ' + group['valid_by__trigramme']); - return $opegroup_html; + return $group_html; } - this._getOrCreateDay = function (date) { + this._get_or_create_day = function (date) { var at = dateUTCToParis(date); var at_ser = at.format('YYYY-MM-DD'); var $day = this.$container.find('.day').filter(function () { @@ -185,24 +185,24 @@ function KHistory(options = {}) { return $day.data('date', at_ser).text(at.format('D MMMM YYYY')); } - this.findOpeGroup = function (id, type = "operation") { - return this.$container.find('.opegroup').filter(function () { + this.find_group = function (id, type = "operation") { + return this.$container.find('.group').filter(function () { return ($(this).data('id') == id && $(this).data("type") == type) }); } - this.findOpe = function (id, type = 'operation') { - return this.$container.find('.ope').filter(function () { + this.find_entry = function (id, type = 'operation') { + return this.$container.find('.entry').filter(function () { return ($(this).data('id') == id && $(this).data('type') == type) }); } - this.update_opegroup = function (opegroup, type = "operation") { - var $opegroup = this.findOpeGroup(opegroup['id'], type); - var trigramme = $opegroup.find('.trigramme').text(); + this.update_opegroup = function (group, type = "operation") { + var $group = this.find_group(group['id'], type); + var trigramme = $group.find('.trigramme').text(); var amount = amountDisplay( - parseFloat(opegroup['amount']), opegroup['is_cof'], trigramme); - $opegroup.find('.amount').text(amount); + parseFloat(group['amount']), group['is_cof'], trigramme); + $group.find('.amount').text(amount); } this.fetch = function (fetch_options) { @@ -214,8 +214,8 @@ function KHistory(options = {}) { method: "POST", data: options, }).done(function (data) { - for (let opegroup of data['opegroups']) { - that.addOpeGroup(opegroup); + for (let group of data['groups']) { + that.add_history_group(group); } }); } @@ -239,9 +239,9 @@ function KHistory(options = {}) { }).done(function (data) { window.lock = 0; that.$container.find('.ui-selected').removeClass('ui-selected'); - for (let ope of data["canceled"]) { - ope["type"] = type; - that.cancelOpe(ope); + for (let entry of data["canceled"]) { + entry["type"] = type; + that.cancel_entry(entry); } if (type == "operation") { for (let opegroup of data["opegroups_to_update"]) { @@ -269,8 +269,8 @@ function KHistory(options = {}) { "transfers": [], "operations": [], } - this.$container.find('.ope.ui-selected').each(function () { - type = $(this).data("group_type"); + 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) { @@ -296,9 +296,9 @@ function KHistory(options = {}) { KHistory.default_options = { container: '#history', template_day: '
        ', - template_opegroup: '
        ', - template_transfergroup: '
        ', - template_ope: '
        ', - template_transfer: '
        ', + template_opegroup: '
        ', + template_transfergroup: '
        ', + template_ope: '
        ', + template_transfer: '
        ', display_trigramme: true, } diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 0b7f946e..7b292087 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -1253,9 +1253,9 @@ $(document).ready(function() { // ----- OperationWebSocket.add_handler(function(data) { - for (var i=0; i Date: Thu, 26 Dec 2019 18:58:55 +0100 Subject: [PATCH 088/573] Simplify transfer view --- kfet/urls.py | 2 +- kfet/views.py | 15 +++------------ 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/kfet/urls.py b/kfet/urls.py index 88220845..12c06d26 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -252,7 +252,7 @@ urlpatterns = [ # ----- # Transfers urls # ----- - path("transfers/", views.transfers, name="kfet.transfers"), + path("transfers/", views.TransferView.as_view(), 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"), diff --git a/kfet/views.py b/kfet/views.py index d5ab30a7..70e5d453 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1598,18 +1598,9 @@ config_update = permission_required("kfet.change_config")(SettingsUpdate.as_view # ----- -@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}) +@method_decorator(teamkfet_required, name="dispatch") +class TransferView(TemplateView): + template_name = "kfet/transfers.html" @teamkfet_required From 9eebc7fb2285bc8dd940ab9041ce4ceef99445ca Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 23 Apr 2020 13:13:31 +0200 Subject: [PATCH 089/573] Fix: les transferts apparaissent dans l'historique perso --- kfet/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 70e5d453..2d13b3d3 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1404,8 +1404,8 @@ def history_json(request): to_date = request.POST.get("to", None) checkouts = request.POST.getlist("checkouts[]", None) accounts = request.POST.getlist("accounts[]", None) - transfers_only = request.POST.get("transfersonly", None) - opes_only = request.POST.get("opesonly", None) + transfers_only = request.POST.get("transfersonly", False) + opes_only = request.POST.get("opesonly", False) # Construction de la requête (sur les transferts) pour le prefetch @@ -1416,7 +1416,7 @@ def history_json(request): # Le check sur les comptes est dans le prefetch pour les transferts if accounts: transfer_queryset_prefetch = transfer_queryset_prefetch.filter( - Q(from_acc__trigramme__in=accounts) | Q(to_acc__trigramme__in=accounts) + Q(from_acc__in=accounts) | Q(to_acc__in=accounts) ) if not request.user.has_perm("kfet.is_team"): @@ -1458,14 +1458,14 @@ def history_json(request): opegroups = opegroups.filter(at__lt=to_date) transfergroups = transfergroups.filter(at__lt=to_date) if checkouts: - opegroups = opegroups.filter(checkout_id__in=checkouts) + opegroups = opegroups.filter(checkout__in=checkouts) transfergroups = TransferGroup.objects.none() if transfers_only: opegroups = OperationGroup.objects.none() if opes_only: transfergroups = TransferGroup.objects.none() if accounts: - opegroups = opegroups.filter(on_acc_id__in=accounts) + opegroups = opegroups.filter(on_acc__in=accounts) # Un non-membre de l'équipe n'a que accès à son historique if not request.user.has_perm("kfet.is_team"): opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet) From 6362740a77618743c52e664dc18a6aaf17709821 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 23 Apr 2020 13:54:10 +0200 Subject: [PATCH 090/573] =?UTF-8?q?Fix:=20`history.html`=20=20marche=20(?= =?UTF-8?q?=C3=A0=20peu=20pr=C3=A8s)=20correctement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/templates/kfet/history.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index 204e0d57..94bba48c 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -28,6 +28,9 @@
      • Comptes {{ filter_form.accounts }}
      +
      + +
      {% endblock %} @@ -71,7 +74,7 @@ $(document).ready(function() { data['accounts'] = accounts; khistory.fetch(data).done(function () { - var nb_opes = khistory.$container.find('.ope:not(.canceled)').length; + var nb_opes = khistory.$container.find('.entry:not(.canceled)').length; $('#nb_opes').text(nb_opes); }); } @@ -106,7 +109,7 @@ $(document).ready(function() { countSelected: "# sur %" }); - $("input").on('dp.change change', function() { + $("#btn-fetch").on('click', function() { khistory.reset(); getHistory(); }); From c8b8c90580a4c8ea858bfc0a1cbc898ca0b6799e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 24 Apr 2020 21:03:16 +0200 Subject: [PATCH 091/573] CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 269e5194..9ecea3ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,11 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre - Les boutons "afficher/cacher" des mails et noms des participant⋅e⋅s à un spectacle BdA fonctionnent à nouveau. +### Nouvelles fonctionnalités + +- Les transferts apparaissent maintenant dans l'historique K-Fêt et l'historique + personnel. + ## Version 0.4.1 - 17/01/2020 - Corrige un bug sur K-Psul lorsqu'un trigramme contient des caractères réservés From 914888d18aee68f813b3ce5ce863e72c7a460aa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 2 Jan 2020 16:01:13 +0100 Subject: [PATCH 092/573] Merge the utils and shared apps --- .gitlab-ci.yml | 4 ++-- bda/views.py | 2 +- gestioncof/views.py | 2 +- setup.cfg | 3 +-- shared/views/autocomplete.py | 45 ++++++++++++++++++++++++++++++++++++ utils/__init__.py | 0 utils/views/__init__.py | 0 utils/views/autocomplete.py | 25 -------------------- 8 files changed, 50 insertions(+), 31 deletions(-) create mode 100644 shared/views/autocomplete.py delete mode 100644 utils/__init__.py delete mode 100644 utils/views/__init__.py delete mode 100644 utils/views/autocomplete.py diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a8bece7d..9bad2072 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -61,9 +61,9 @@ linters: - pip install --upgrade black isort flake8 script: - black --check . - - isort --recursive --check-only --diff bda bds clubs cof events gestioncof kfet petitscours provisioning shared utils + - isort --recursive --check-only --diff bda bds clubs cof events gestioncof kfet petitscours provisioning shared # Print errors only - - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared utils + - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared cache: key: linters paths: diff --git a/bda/views.py b/bda/views.py index f33b7013..f799360d 100644 --- a/bda/views.py +++ b/bda/views.py @@ -42,7 +42,7 @@ from bda.models import ( Tirage, ) from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required -from utils.views.autocomplete import Select2QuerySetView +from shared.views.autocomplete import Select2QuerySetView @cof_required diff --git a/gestioncof/views.py b/gestioncof/views.py index ced35cfc..07a0ae03 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -58,7 +58,7 @@ from gestioncof.models import ( SurveyQuestion, SurveyQuestionAnswer, ) -from utils.views.autocomplete import Select2QuerySetView +from shared.views.autocomplete import Select2QuerySetView class HomeView(LoginRequiredMixin, TemplateView): diff --git a/setup.cfg b/setup.cfg index 100ddb22..1a9901cb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,7 +9,6 @@ source = kfet petitscours shared - utils omit = *migrations* *test*.py @@ -37,7 +36,7 @@ default_section = THIRDPARTY force_grid_wrap = 0 include_trailing_comma = true known_django = django -known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared,utils +known_first_party = bda,bds,clubs,cof,events,gestioncof,kfet,petitscours,shared line_length = 88 multi_line_output = 3 not_skip = __init__.py diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py new file mode 100644 index 00000000..7fc7a886 --- /dev/null +++ b/shared/views/autocomplete.py @@ -0,0 +1,45 @@ +from dal import autocomplete +from django.db.models import Q + + +class ModelSearch: + """Basic search engine for models based on filtering. + + Subclasses should override the ``model`` class attribute and specify the list of + search fields to be searched in. + """ + + model = None + search_fields = [] + + 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)) + + +class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView): + """Compatibility layer between ModelSearch and Select2QuerySetView.""" + + def get_queryset(self): + keywords = self.q.split() + return super().search(keywords) diff --git a/utils/__init__.py b/utils/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/views/__init__.py b/utils/views/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/utils/views/autocomplete.py b/utils/views/autocomplete.py deleted file mode 100644 index c5d51343..00000000 --- a/utils/views/autocomplete.py +++ /dev/null @@ -1,25 +0,0 @@ -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()) From d2c6c9da7ae51fa993cca7146ebf4ef6dbd1b822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 2 Jan 2020 17:07:50 +0100 Subject: [PATCH 093/573] Type hints in shared.views.autocomplete --- shared/views/autocomplete.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 7fc7a886..270eae63 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -1,18 +1,22 @@ -from dal import autocomplete -from django.db.models import Q +from typing import Generic, Iterable, Type, TypeVar + +from dal import autocomplete # type: ignore +from django.db.models import Model, Q + +M = TypeVar("M", bound=Model) -class ModelSearch: +class ModelSearch(Generic[M]): """Basic search engine for models based on filtering. Subclasses should override the ``model`` class attribute and specify the list of search fields to be searched in. """ - model = None - search_fields = [] + model: Type[M] + search_fields: Iterable[str] - def get_queryset_filter(self, keywords): + def get_queryset_filter(self, keywords: Iterable[str]) -> Q: filter_q = Q() if not keywords: @@ -26,7 +30,7 @@ class ModelSearch: return filter_q - def search(self, keywords): + def search(self, keywords: Iterable[str]) -> Iterable[M]: """Returns the queryset of model instances matching all the keywords. The semantic of the search is the following: a model instance appears in the From e45ee3fb40358036211116d803547788f0b43ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 3 Jan 2020 17:26:12 +0100 Subject: [PATCH 094/573] More documentation for ModelSearch --- shared/views/autocomplete.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 270eae63..e8d90590 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -9,8 +9,23 @@ M = TypeVar("M", bound=Model) class ModelSearch(Generic[M]): """Basic search engine for models based on filtering. - Subclasses should override the ``model`` class attribute and specify the list of - search fields to be searched in. + As the type hints indicate, the class is generic with respect to the model. This + means that the ``search`` method only returns instances of the model specified as + the ``model`` class attribute in subclasses. + + The ``search_fields`` attributes indicates which fields to search in during the + search. + + 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: Type[M] From a259b04d9cf9b3d8ddd4fe3f96a16cd75a5d6a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 3 Jan 2020 17:29:55 +0100 Subject: [PATCH 095/573] Explicative comment about the Type[M] annotation --- shared/views/autocomplete.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index e8d90590..708fe554 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -28,6 +28,8 @@ class ModelSearch(Generic[M]): >>> user_search.search(["toto", "foo"]) # returns a queryset of Users """ + # This says that `model` is the class corresponding to the type variable M (or a + # subclass). model: Type[M] search_fields: Iterable[str] From b8cd5f1da50a60cce446f3d2c1b270f6d3462c66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 12 Feb 2020 19:01:08 +0100 Subject: [PATCH 096/573] Drop type hints in shared.views.autocomplete --- shared/views/autocomplete.py | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 708fe554..095dc3f8 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -1,20 +1,13 @@ -from typing import Generic, Iterable, Type, TypeVar - -from dal import autocomplete # type: ignore -from django.db.models import Model, Q - -M = TypeVar("M", bound=Model) +from dal import autocomplete +from django.db.models import Q -class ModelSearch(Generic[M]): +class ModelSearch: """Basic search engine for models based on filtering. - As the type hints indicate, the class is generic with respect to the model. This - means that the ``search`` method only returns instances of the model specified as - the ``model`` class attribute in subclasses. - - The ``search_fields`` attributes indicates which fields to search in during the - search. + 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: @@ -28,12 +21,10 @@ class ModelSearch(Generic[M]): >>> user_search.search(["toto", "foo"]) # returns a queryset of Users """ - # This says that `model` is the class corresponding to the type variable M (or a - # subclass). - model: Type[M] - search_fields: Iterable[str] + model = None + search_fields = [] - def get_queryset_filter(self, keywords: Iterable[str]) -> Q: + def get_queryset_filter(self, keywords): filter_q = Q() if not keywords: @@ -47,7 +38,7 @@ class ModelSearch(Generic[M]): return filter_q - def search(self, keywords: Iterable[str]) -> Iterable[M]: + 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 From b1d8bb04c4d9e772c4cc205de22147b893e01991 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 11 Dec 2019 22:00:10 +0100 Subject: [PATCH 097/573] Generic auto-completion mechanism --- gestioncof/autocomplete.py | 118 ++++++---------- gestioncof/templates/autocomplete_user.html | 29 ---- .../templates/gestioncof/search_results.html | 56 ++++++++ gestioncof/tests/test_views.py | 24 ++-- shared/__init__.py | 0 shared/tests/testcases.py | 2 +- shared/views/autocomplete.py | 128 +++++++++++++++++- 7 files changed, 235 insertions(+), 122 deletions(-) delete mode 100644 gestioncof/templates/autocomplete_user.html create mode 100644 gestioncof/templates/gestioncof/search_results.html create mode 100644 shared/__init__.py diff --git a/gestioncof/autocomplete.py b/gestioncof/autocomplete.py index e27cdb92..239317f8 100644 --- a/gestioncof/autocomplete.py +++ b/gestioncof/autocomplete.py @@ -1,94 +1,56 @@ -from django import shortcuts -from django.conf import settings -from django.contrib.auth.models import User +from django.contrib.auth import get_user_model from django.db.models import Q from django.http import Http404 +from django.views.generic import TemplateView from gestioncof.decorators import buro_required -from gestioncof.models import CofProfile +from shared.views import autocomplete -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 +User = get_user_model() -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"] - def __str__(self): - return "{} ({})".format(self.clipper, self.fullname) - - def __eq__(self, other): - return self.clipper == other.clipper and self.fullname == other.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 -@buro_required -def autocomplete(request): - if "q" not in request.GET: - raise Http404 - q = request.GET["q"] - data = {"q": q} +class COFOthersSearch(autocomplete.ModelSearch): + model = User + search_fields = ["username", "first_name", "last_name"] - queries = {} - bits = q.split() + 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 - # 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() - # Clearing redundancies - usernames = set(queries["members"].values_list("login_clipper", flat="True")) | set( - queries["users"].values_list("profile__login_clipper", flat="True") - ) +class COFSearch(autocomplete.Compose): + search_units = [ + ("members", "username", COFMemberSearch), + ("others", "username", COFOthersSearch), + ("clippers", "clipper", autocomplete.LDAPSearch), + ] - # 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 - ] - # Resulting data - data.update(queries) - data["options"] = sum(len(query) for query in queries) +cof_search = COFSearch() - return shortcuts.render(request, "autocomplete_user.html", data) + +class AutocompleteView(TemplateView): + template_name = "gestioncof/search_results.html" + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + if "q" not in self.request.GET: + raise Http404 + q = self.request.GET["q"] + ctx["q"] = q + ctx.update(cof_search.search(q.split())) + return ctx + + +autocomplete = buro_required(AutocompleteView.as_view()) diff --git a/gestioncof/templates/autocomplete_user.html b/gestioncof/templates/autocomplete_user.html deleted file mode 100644 index face824d..00000000 --- a/gestioncof/templates/autocomplete_user.html +++ /dev/null @@ -1,29 +0,0 @@ -{% load utils %} -
        -{% if members %} -
      • Membres du COF
      • - {% for member in members %}{% if forloop.counter < 5 %} -
      • {{ member.user|highlight_user:q }}
      • - {% elif forloop.counter == 5 %}
      • ...{% endif %}{% endfor %} -{% endif %} -{% if users %} -
      • Utilisateurs de GestioCOF
      • - {% for user in users %}{% if forloop.counter < 5 %} -
      • {{ user|highlight_user:q }}
      • - {% elif forloop.counter == 5 %}
      • ...{% endif %}{% endfor %} -{% endif %} -{% if clippers %} -
      • Utilisateurs clipper
      • - {% for clipper in clippers %}{% if forloop.counter < 5 %} -
      • {{ clipper|highlight_clipper:q }}
      • - {% elif forloop.counter == 5 %}
      • ...{% endif %}{% endfor %} -{% endif %} - -{% if not options %} -
      • Aucune correspondance trouvée
      • -{% else %} -
      • Pas dans la liste ?
      • -{% endif %} -
      • Créer un compte
      • - -
      diff --git a/gestioncof/templates/gestioncof/search_results.html b/gestioncof/templates/gestioncof/search_results.html new file mode 100644 index 00000000..ba8b6580 --- /dev/null +++ b/gestioncof/templates/gestioncof/search_results.html @@ -0,0 +1,56 @@ +{% load utils %} + +
        + {% if members %} +
      • Membres
      • + {% for user in members %} + {% if forloop.counter < 5 %} +
      • + + {{ user|highlight_user:q }} + +
      • + {% elif forloop.counter == 5 %} +
      • ...
      • + {% endif %} + {% endfor %} + {% endif %} + + {% if others %} +
      • Non-membres
      • + {% for user in others %} + {% if forloop.counter < 5 %} +
      • + + {{ user|highlight_user:q }} + +
      • + {% elif forloop.counter == 5 %} +
      • ...
      • + {% endif %} + {% endfor %} + {% endif %} + + {% if clippers %} +
      • Utilisateurs clipper
      • + {% for clipper in clippers %} + {% if forloop.counter < 5 %} +
      • + + {{ clipper|highlight_clipper:q }} + +
      • + {% elif forloop.counter == 5 %} +
      • ...
      • + {% endif %} + {% endfor %} + {% endif %} + + {% if total %} +
      • Pas dans la liste ?
      • + {% else %} +
      • Aucune correspondance trouvée
      • + {% endif %} + +
      • Créer un compte
      • +
      diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 31cb8d8a..f757b4c2 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -15,9 +15,9 @@ from django.test import Client, TestCase, override_settings from django.urls import reverse from bda.models import Salle, Tirage -from gestioncof.autocomplete import Clipper from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer from gestioncof.tests.testcases import ViewTestCaseMixin +from shared.views.autocomplete import Clipper from .utils import create_member, create_root, create_user @@ -285,21 +285,19 @@ class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase): self.mockLDAP([]) - def _test(self, query, expected_users, expected_members, expected_clippers): + def _test(self, query, expected_others, expected_members, expected_clippers): r = self.client.get(self.url, {"q": query}) self.assertEqual(r.status_code, 200) self.assertQuerysetEqual( - r.context["users"], map(repr, expected_users), ordered=False + r.context["others"], map(repr, expected_others), ordered=False ) self.assertQuerysetEqual( - r.context["members"], - map(lambda u: repr(u.profile), expected_members), - ordered=False, + r.context["members"], map(repr, expected_members), ordered=False, ) self.assertCountEqual( - map(str, r.context.get("clippers", [])), map(str, expected_clippers) + map(str, r.context["clippers"]), map(str, expected_clippers) ) def test_username(self): @@ -322,7 +320,7 @@ class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase): mock_ldap.search.assert_called_once_with( "dc=spi,dc=ens,dc=fr", "(&(|(cn=*aa*)(uid=*aa*))(|(cn=*bb*)(uid=*bb*)))", - attributes=["uid", "cn"], + attributes=["cn", "uid"], ) def test_clipper_escaped(self): @@ -333,14 +331,14 @@ class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase): mock_ldap.search.assert_not_called() def test_clipper_no_duplicate(self): - self.mockLDAP([("uid", "uu_u1")]) + self.mockLDAP([("uid", "abc")]) - self._test("uu u1", [self.u1], [], [Clipper("uid", "uu_u1")]) + self._test("abc", [self.u1], [], [Clipper("uid", "abc")]) - self.u1.profile.login_clipper = "uid" - self.u1.profile.save() + self.u1.username = "uid" + self.u1.save() - self._test("uu u1", [self.u1], [], []) + self._test("abc", [self.u1], [], []) class HomeViewTests(ViewTestCaseMixin, TestCase): diff --git a/shared/__init__.py b/shared/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 35d697e7..507e1361 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -111,7 +111,7 @@ class TestCaseMixin: mock_context_manager.return_value.__enter__.return_value = mock_connection patcher = mock.patch( - "gestioncof.autocomplete.Connection", new=mock_context_manager + "shared.views.autocomplete.Connection", new=mock_context_manager ) patcher.start() self.addCleanup(patcher.stop) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 095dc3f8..5254f8c8 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -1,8 +1,37 @@ +from collections import namedtuple + from dal import autocomplete +from django.conf import settings from django.db.models import Q +if getattr(settings, "LDAP_SERVER_URL", None): + from ldap3 import Connection +else: + # shared.tests.testcases.TestCaseMixin.mockLDAP needs + # Connection to be defined + Connection = None -class ModelSearch: + +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. + """ + + def search(self, _keywords): + raise NotImplementedError( + "Class implementing the SeachUnit interface should implement the search " + "method" + ) + + +# --- +# 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`` @@ -55,3 +84,100 @@ class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView): def get_queryset(self): keywords = self.q.split() return super().search(keywords) + + +# --- +# LDAP search +# --- + +Clipper = namedtuple("Clipper", "clipper fullname") + + +class LDAPSearch(SearchUnit): + ldap_server_url = getattr(settings, "LDAP_SERVER_URL", None) + domain_component = "dc=spi,dc=ens,dc=fr" + search_fields = ["cn", "uid"] + + def get_ldap_query(self, keywords): + # 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. + + The semantic of the search is the following: a Clipper appears in the + search results iff all of the keywords given as arguments occur in at least one + of the search fields. + """ + + query = self.get_ldap_query(keywords) + + if Connection is None or query == "(&)": + return [] + + with Connection(self.ldap_server_url) as conn: + conn.search(self.domain_component, query, attributes=self.search_fields) + return [Clipper(entry.uid.value, entry.cn.value) for entry in conn.entries] + + +# --- +# Composition of autocomplete units +# --- + + +class Compose: + """Search with several units and remove duplicate results. + + The ``search_units`` class attribute should be a list of tuples of the form ``(name, + uniq_key, search_unit)``. + + The ``search`` method produces a dictionnary whose keys are the ``name``s given in + ``search_units`` and whose values are iterables produced by the different search + units. + + The ``uniq_key``s are used to remove duplicates: for instance, say that search unit + 1 has ``uniq_key = "username"`` and search unit 2 has ``uniq_key = "clipper"``, then + search results from unit 2 whose ``.clipper`` attribute is equal to the + ``.username`` attribute of some result from unit 1 are omitted. + + Typical Example: + + >>> from django.contrib.auth.models import User + >>> + >>> class UserSearch(ModelSearch): + ... model = User + ... search_fields = ["username", "first_name", "last_name"] + >>> + >>> class UserAndClipperSearch(Compose): + ... search_units = [ + ... ("users", "username", UserSearch), + ... ("clippers", "clipper", 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): + uniq_results = set() + results = {} + for name, uniq_key, search_unit in self.search_units: + res = search_unit().search(keywords) + res = [r for r in res if getattr(r, uniq_key) not in uniq_results] + uniq_results |= set((getattr(r, uniq_key) for r in res)) + results[name] = res + return results From 3b0d4ba58fca9dcd22969fff46b17e122b4a524b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 7 May 2020 15:44:37 +0200 Subject: [PATCH 098/573] lstephan's suggestions --- shared/views/autocomplete.py | 37 ++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index 5254f8c8..af5e3980 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -15,7 +15,7 @@ else: class SearchUnit: """Base class for all the search utilities. - A search unit should implement a ``search`` method taking a list of keywords as + A search unit should implement a `search` method taking a list of keywords as argument and returning an iterable of search results. """ @@ -34,8 +34,8 @@ class SearchUnit: 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`` + 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: @@ -90,7 +90,7 @@ class Select2QuerySetView(ModelSearch, autocomplete.Select2QuerySetView): # LDAP search # --- -Clipper = namedtuple("Clipper", "clipper fullname") +Clipper = namedtuple("Clipper", ["clipper", "fullname"]) class LDAPSearch(SearchUnit): @@ -99,6 +99,12 @@ class LDAPSearch(SearchUnit): search_fields = ["cn", "uid"] 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) @@ -115,12 +121,7 @@ class LDAPSearch(SearchUnit): return "(&{})".format("".join(ldap_filters)) def search(self, keywords): - """Return a list of Clipper objects matching all the keywords. - - The semantic of the search is the following: a Clipper appears in the - search results iff all of the keywords given as arguments occur in at least one - of the search fields. - """ + """Return a list of Clipper objects matching all the keywords.""" query = self.get_ldap_query(keywords) @@ -140,17 +141,17 @@ class LDAPSearch(SearchUnit): class Compose: """Search with several units and remove duplicate results. - The ``search_units`` class attribute should be a list of tuples of the form ``(name, - uniq_key, search_unit)``. + The `search_units` class attribute should be a list of tuples of the form `(name, + uniq_key, search_unit)`. - The ``search`` method produces a dictionnary whose keys are the ``name``s given in - ``search_units`` and whose values are iterables produced by the different search + 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. - The ``uniq_key``s are used to remove duplicates: for instance, say that search unit - 1 has ``uniq_key = "username"`` and search unit 2 has ``uniq_key = "clipper"``, then - search results from unit 2 whose ``.clipper`` attribute is equal to the - ``.username`` attribute of some result from unit 1 are omitted. + The `uniq_key`s are used to remove duplicates: for instance, say that search unit + 1 has `uniq_key = "username"` and search unit 2 has `uniq_key = "clipper"`, then + search results from unit 2 whose `.clipper` attribute is equal to the + `.username` attribute of some result from unit 1 are omitted. Typical Example: From 4f15bb962417b5f5525c51ac54a0c41dbaa43003 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 7 May 2020 18:40:07 +0200 Subject: [PATCH 099/573] CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecea3ce..6af67f68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre - Nouveau module de gestion des événements - Nouveau module BDS - Nouveau module clubs +- Module d'autocomplétion indépendant des apps ## Upcoming @@ -19,6 +20,8 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre - 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 From 6767ba8e8c925c3917272f848c6ff4ab91226907 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 15:06:55 +0100 Subject: [PATCH 100/573] Rajoute de la doc partout --- kfet/statistic.py | 39 +++++++++++++++++++++++++++++---------- kfet/views.py | 41 +++++++++++++++++++++++++++++++---------- 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 02171267..f308011e 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -10,12 +10,16 @@ KFET_WAKES_UP_AT = time(7, 0) def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): - """datetime wrapper with time offset.""" + """Étant donné une date, renvoie un objet `datetime` + correspondant au début du 'jour K-Fêt' correspondant.""" naive = datetime.combine(date(year, month, day), start_at) return pytz.timezone("Europe/Paris").localize(naive, is_dst=None) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): + """ + Retourne le 'jour K-Fêt' correspondant à un objet `datetime` donné + """ kfet_dt = kfet_day(year=dt.year, month=dt.month, day=dt.day) if dt.time() < start_at: kfet_dt -= timedelta(days=1) @@ -23,6 +27,17 @@ def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): class Scale(object): + """ + Classe utilisée pour subdiviser un QuerySet (e.g. des opérations) sur + une échelle de temps donnée, avec un pas de temps fixe. + Cette échelle peut être spécifiée : + - par un début et une fin, + - par un début/une fin et un nombre de subdivisions. + + Si le booléen `std_chunk` est activé, les subdivisions sont standardisées : + on appelle `get_chunk_start` sur toutes les subdivisions (enfin, sur la première). + """ + name = None step = None @@ -92,6 +107,10 @@ class Scale(object): def chunkify_qs(self, qs, field=None): if field is None: field = "at" + """ + Découpe un queryset en subdivisions, avec agrégation optionnelle des résultats + NB : on pourrait faire ça en une requête, au détriment de la lisibilité... + """ begin_f = "{}__gte".format(field) end_f = "{}__lte".format(field) return [qs.filter(**{begin_f: begin, end_f: end}) for begin, end in self] @@ -247,6 +266,13 @@ def last_stats_manifest( scale_prefix=scale_prefix, **url_params ) + """ + Convertit une spécification de scales en arguments GET utilisables par ScaleMixin. + La spécification est de la forme suivante : + - scales_def : liste de champs de la forme (label, scale) + - scale_args : arguments à passer à Scale.__init__ + - other_url_params : paramètres GET supplémentaires + """ # Étant donné un queryset d'operations @@ -260,16 +286,9 @@ class ScaleMixin(object): scale_args_prefix = "scale_" def get_scale_args(self, params=None, prefix=None): - """Retrieve scale args from params. - - Should search the same args of Scale constructor. - - Args: - params (dict, optional): Scale args are searched in this. - Default to GET params of request. - prefix (str, optional): Appended at the begin of scale args names. - Default to `self.scale_args_prefix`. + """ + Récupère les paramètres de subdivision encodés dans une requête GET. """ if params is None: params = self.request.GET diff --git a/kfet/views.py b/kfet/views.py index a04cda24..b9c690dd 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2235,11 +2235,19 @@ class PkUrlMixin(object): class SingleResumeStat(JSONDetailView): - """Manifest for a kind of a stat about an object. + """ + 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. - Returns JSON whose payload is an array containing descriptions of a stat: - url to retrieve data, label, ... + 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 + On peut aussi définir `stats` dynamiquement, via la fonction `get_stats`. """ id_prefix = "" @@ -2285,7 +2293,8 @@ ID_PREFIX_ACC_BALANCE = "balance_acc" class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): - """Manifest for balance stats of an account.""" + Menu général pour l'historique de balance d'un compte + """ model = Account context_object_name = "account" @@ -2313,10 +2322,11 @@ class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): class AccountStatBalance(PkUrlMixin, JSONDetailView): - """Datasets of balance of an account. - Operations and Transfers are taken into account. + """ + 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 @@ -2441,7 +2451,10 @@ ID_PREFIX_ACC_LAST_MONTHS = "last_months_acc" class AccountStatOperationList(PkUrlMixin, SingleResumeStat): - """Manifest for operations stats of an account.""" +@method_decorator(login_required, name="dispatch") + """ + Menu général pour l'historique de consommation d'un compte + """ model = Account context_object_name = "account" @@ -2463,7 +2476,10 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): - """Datasets of operations of an account.""" +@method_decorator(login_required, name="dispatch") + """ + Statistiques (JSON) de consommation (nb d'items achetés) d'un compte. + """ model = Account pk_url_kwarg = "trigramme" @@ -2535,7 +2551,9 @@ ID_PREFIX_ART_LAST_MONTHS = "last_months_art" class ArticleStatSalesList(SingleResumeStat): - """Manifest for sales stats of an article.""" + """ + Menu pour les statistiques de vente d'un article. + """ model = Article context_object_name = "article" @@ -2550,7 +2568,10 @@ class ArticleStatSalesList(SingleResumeStat): class ArticleStatSales(ScaleMixin, JSONDetailView): - """Datasets of sales of an article.""" + """ + Statistiques (JSON) de vente d'un article. + Sépare LIQ et les comptes K-Fêt, et rajoute le total. + """ model = Article context_object_name = "article" From 78ad4402b03bd215bcb360cf64683aa29aa40dae Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 15:10:02 +0100 Subject: [PATCH 101/573] Plus de timezones --- kfet/statistic.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index f308011e..81f81c1d 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -1,19 +1,17 @@ from datetime import date, datetime, time, timedelta -import pytz from dateutil.parser import parse as dateutil_parse from dateutil.relativedelta import relativedelta from django.db.models import Sum from django.utils import timezone -KFET_WAKES_UP_AT = time(7, 0) +KFET_WAKES_UP_AT = time(5, 0) # La K-Fêt ouvre à 5h (UTC) du matin def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): """Étant donné une date, renvoie un objet `datetime` correspondant au début du 'jour K-Fêt' correspondant.""" - naive = datetime.combine(date(year, month, day), start_at) - return pytz.timezone("Europe/Paris").localize(naive, is_dst=None) + return datetime.combine(date(year, month, day), start_at) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): From 26bcd729bbccf31a19f8f3126d14f37a36e363ba Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:09:12 +0100 Subject: [PATCH 102/573] Supprime le code mort ou redondant --- kfet/statistic.py | 34 +++++-------------- kfet/views.py | 85 +++++++++++++---------------------------------- 2 files changed, 33 insertions(+), 86 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 81f81c1d..45f8fb65 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -65,7 +65,7 @@ class Scale(object): "or use last and n_steps" ) - self.datetimes = self.get_datetimes() + self._gen_datetimes() @staticmethod def by_name(name): @@ -74,9 +74,6 @@ class Scale(object): return cls return None - def get_from(self, dt): - return self.std_chunk and self.get_chunk_start(dt) or dt - def __getitem__(self, i): return self.datetimes[i], self.datetimes[i + 1] @@ -86,13 +83,13 @@ class Scale(object): def do_step(self, dt, n_steps=1): return dt + self.step * n_steps - def get_datetimes(self): + def _gen_datetimes(self): datetimes = [self.begin] tmp = self.begin while tmp < self.end: tmp = self.do_step(tmp) datetimes.append(tmp) - return datetimes + self.datetimes = datetimes def get_labels(self, label_fmt=None): if label_fmt is None: @@ -273,45 +270,32 @@ def last_stats_manifest( """ -# Étant donné un queryset d'operations -# rend la somme des article_nb -def tot_ventes(queryset): - res = queryset.aggregate(Sum("article_nb"))["article_nb__sum"] - return res and res or 0 class ScaleMixin(object): - scale_args_prefix = "scale_" - - def get_scale_args(self, params=None, prefix=None): - + def parse_scale_args(self): """ Récupère les paramètres de subdivision encodés dans une requête GET. """ - if params is None: - params = self.request.GET - if prefix is None: - prefix = self.scale_args_prefix - scale_args = {} - name = params.get(prefix + "name", None) + name = self.request.GET.get("scale_name", None) if name is not None: scale_args["name"] = name - n_steps = params.get(prefix + "n_steps", None) + n_steps = self.request.GET.get("scale_n_steps", None) if n_steps is not None: scale_args["n_steps"] = int(n_steps) - begin = params.get(prefix + "begin", None) + begin = self.request.GET.get("scale_begin", None) if begin is not None: scale_args["begin"] = dateutil_parse(begin) - end = params.get(prefix + "send", None) + end = self.request.GET.get("scale_send", None) if end is not None: scale_args["end"] = dateutil_parse(end) - last = params.get(prefix + "last", None) + last = self.request.GET.get("scale_last", None) if last is not None: scale_args["last"] = last in ["true", "True", "1"] and True or False diff --git a/kfet/views.py b/kfet/views.py index b9c690dd..5455be8a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2199,7 +2199,7 @@ class SupplierUpdate(SuccessMessageMixin, UpdateView): # Vues génériques # --------------- # source : docs.djangoproject.com/fr/1.10/topics/class-based-views/mixins/ -class JSONResponseMixin(object): +class JSONResponseMixin: """ A mixin that can be used to render a JSON response. """ @@ -2228,12 +2228,6 @@ 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. @@ -2286,13 +2280,28 @@ class SingleResumeStat(JSONDetailView): 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. + """ + + def get_object(self, *args, **kwargs): + obj = super().get_object(*args, **kwargs) + if self.request.user != obj.user: + raise Http404 + return obj + + # ----------------------- # Evolution Balance perso # ----------------------- ID_PREFIX_ACC_BALANCE = "balance_acc" -class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): +@method_decorator(login_required, name="dispatch") +class AccountStatBalanceList(UserAccountMixin, SingleResumeStat): + """ Menu général pour l'historique de balance d'un compte """ @@ -2310,20 +2319,11 @@ class AccountStatBalanceList(PkUrlMixin, SingleResumeStat): ] nb_default = 0 - 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) -class AccountStatBalance(PkUrlMixin, JSONDetailView): - +@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. @@ -2430,28 +2430,15 @@ class AccountStatBalance(PkUrlMixin, 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" -class AccountStatOperationList(PkUrlMixin, SingleResumeStat): @method_decorator(login_required, name="dispatch") +class AccountStatOperationList(UserAccountMixin, SingleResumeStat): """ Menu général pour l'historique de consommation d'un compte """ @@ -2464,19 +2451,11 @@ class AccountStatOperationList(PkUrlMixin, SingleResumeStat): stats = last_stats_manifest(types=[Operation.PURCHASE]) url_stat = "kfet.account.stat.operation" - 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) -class AccountStatOperation(ScaleMixin, PkUrlMixin, JSONDetailView): + @method_decorator(login_required, name="dispatch") +class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): """ Statistiques (JSON) de consommation (nb d'items achetés) d'un compte. """ @@ -2530,26 +2509,13 @@ class AccountStatOperation(ScaleMixin, PkUrlMixin, 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 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. @@ -2567,6 +2533,7 @@ class ArticleStatSalesList(SingleResumeStat): return super().dispatch(*args, **kwargs) +@method_decorator(teamkfet_required, name="dispatch") class ArticleStatSales(ScaleMixin, JSONDetailView): """ Statistiques (JSON) de vente d'un article. @@ -2623,7 +2590,3 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): }, ] return context - - @method_decorator(teamkfet_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) From ef35f45ad2aafa638674ce4a1aa6946125d40617 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:11:08 +0100 Subject: [PATCH 103/573] Fusionne deux fonctions `chunkify` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On rajoute de l'agrégation optionnelle dans la fonction. --- kfet/statistic.py | 92 ++++------------------------------------------- kfet/views.py | 42 +++++++--------------- 2 files changed, 18 insertions(+), 116 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 45f8fb65..98bcee32 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -2,7 +2,6 @@ from datetime import date, datetime, time, timedelta from dateutil.parser import parse as dateutil_parse from dateutil.relativedelta import relativedelta -from django.db.models import Sum from django.utils import timezone KFET_WAKES_UP_AT = time(5, 0) # La K-Fêt ouvre à 5h (UTC) du matin @@ -99,97 +98,18 @@ class Scale(object): for i, (begin, end) in enumerate(self) ] - def chunkify_qs(self, qs, field=None): - if field is None: - field = "at" + def chunkify_qs(self, qs, field="at", aggregate=None): """ Découpe un queryset en subdivisions, avec agrégation optionnelle des résultats NB : on pourrait faire ça en une requête, au détriment de la lisibilité... """ begin_f = "{}__gte".format(field) end_f = "{}__lte".format(field) - return [qs.filter(**{begin_f: begin, end_f: end}) for begin, end in self] - - def get_by_chunks(self, qs, field_callback=None, field_db="at"): - """Objects of queryset ranked according to the scale. - - Returns a generator whose each item, corresponding to a scale chunk, - is a generator of objects from qs for this chunk. - - Args: - qs: Queryset of source objects, must be ordered *first* on the - same field returned by `field_callback`. - field_callback: Callable which gives value from an object used - to compare against limits of the scale chunks. - Default to: lambda obj: getattr(obj, field_db) - field_db: Used to filter against `scale` limits. - Default to 'at'. - - Examples: - If queryset `qs` use `values()`, `field_callback` must be set and - could be: `lambda d: d['at']` - If `field_db` use foreign attributes (eg with `__`), it should be - something like: `lambda obj: obj.group.at`. - - """ - if field_callback is None: - - def field_callback(obj): - return getattr(obj, field_db) - - begin_f = "{}__gte".format(field_db) - end_f = "{}__lte".format(field_db) - - qs = qs.filter(**{begin_f: self.begin, end_f: self.end}) - - obj_iter = iter(qs) - - last_obj = None - - def _objects_until(obj_iter, field_callback, end): - """Generator of objects until `end`. - - Ends if objects source is empty or when an object not verifying - field_callback(obj) <= end is met. - - If this object exists, it is stored in `last_obj` which is found - from outer scope. - Also, if this same variable is non-empty when the function is - called, it first yields its content. - - Args: - obj_iter: Source used to get objects. - field_callback: Returned value, when it is called on an object - will be used to test ordering against `end`. - end - - """ - nonlocal last_obj - - if last_obj is not None: - yield last_obj - last_obj = None - - for obj in obj_iter: - if field_callback(obj) <= end: - yield obj - else: - last_obj = obj - return - - for begin, end in self: - # forward last seen object, if it exists, to the right chunk, - # and fill with empty generators for intermediate chunks of scale - if last_obj is not None: - if field_callback(last_obj) > end: - yield iter(()) - continue - - # yields generator for this chunk - # this set last_obj to None if obj_iter reach its end, otherwise - # it's set to the first met object from obj_iter which doesn't - # belong to this chunk - yield _objects_until(obj_iter, field_callback, end) + chunks = [qs.filter(**{begin_f: begin, end_f: end}) for begin, end in self] + if aggregate is None: + return chunks + else: + return [chunk.aggregate(agg=aggregate)["agg"] or 0 for chunk in chunks] class DayScale(Scale): diff --git a/kfet/views.py b/kfet/views.py index 5455be8a..647d78d9 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2465,7 +2465,7 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): context_object_name = "account" id_prefix = "" - def get_operations(self, scale, types=None): + def get_operations(self, 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 @@ -2477,28 +2477,20 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): ) 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 + return all_operations def get_context_data(self, *args, **kwargs): - old_ctx = super().get_context_data(*args, **kwargs) - context = {"labels": old_ctx["labels"]} - scale = self.scale + context = super().get_context_data(*args, **kwargs) 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) + operations = self.get_operations(types=types) # On compte les opérations - nb_ventes = [] - for chunk in operations: - ventes = sum(ope["article_nb"] for ope in chunk) - nb_ventes.append(ventes) + nb_ventes = self.scale.chunkify_qs( + operations, field="group__at", aggregate=Sum("article_nb") + ) context["charts"] = [ { @@ -2558,23 +2550,13 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): liq_only = all_purchases.filter(group__on_acc__trigramme="LIQ") liq_exclude = all_purchases.exclude(group__on_acc__trigramme="LIQ") - chunks_liq = scale.get_by_chunks( - liq_only, field_db="group__at", field_callback=lambda d: d["group__at"] + nb_liq = scale.chunkify_qs( + liq_only, 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_accounts = scale.chunkify_qs( + liq_exclude, field="group__at", aggregate=Sum("article_nb") ) - - # 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) + nb_ventes = [n1 + n2 for n1, n2 in zip(nb_liq, nb_accounts)] context["charts"] = [ { From 48ad5cd1c711b09359350f1333d7cb0cc5025f66 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:15:15 +0100 Subject: [PATCH 104/573] Misc cleanup On utilise SingleObjectMixin partout, et on simplifie 2-3 trucs --- kfet/statistic.py | 14 ++++++-------- kfet/views.py | 42 +++++++++++++++--------------------------- 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 98bcee32..1578101b 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -125,7 +125,7 @@ class DayScale(Scale): class WeekScale(Scale): name = "week" step = timedelta(days=7) - label_fmt = "Semaine %W" + label_fmt = "%d %b." @classmethod def get_chunk_start(cls, dt): @@ -222,20 +222,18 @@ class ScaleMixin(object): return scale_args def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + # On n'hérite pas - scale_args = self.get_scale_args() + scale_args = self.parse_scale_args() scale_name = scale_args.pop("name", None) scale_cls = Scale.by_name(scale_name) if scale_cls is None: - scale = self.get_default_scale() + self.scale = self.get_default_scale() else: - scale = scale_cls(**scale_args) + self.scale = scale_cls(**scale_args) - self.scale = scale - context["labels"] = scale.get_labels() - return context + return {"labels": self.scale.get_labels()} def get_default_scale(self): return DayScale(n_steps=7, last=True) diff --git a/kfet/views.py b/kfet/views.py index 647d78d9..1dfde369 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2244,7 +2244,6 @@ class SingleResumeStat(JSONDetailView): On peut aussi définir `stats` dynamiquement, via la fonction `get_stats`. """ - id_prefix = "" nb_default = 0 stats = [] @@ -2252,12 +2251,15 @@ class SingleResumeStat(JSONDetailView): def get_context_data(self, **kwargs): # On n'hérite pas - object_id = self.object.id context = {} stats = [] - prefix = "{}_{}".format(self.id_prefix, object_id) - for i, stat_def in enumerate(self.stats): + # On peut avoir récupéré self.object via pk ou slug + if self.pk_url_kwarg in self.kwargs: 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)) @@ -2266,17 +2268,13 @@ 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 ), } ) - 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 @@ -2296,7 +2294,6 @@ class UserAccountMixin: # ----------------------- # Evolution Balance perso # ----------------------- -ID_PREFIX_ACC_BALANCE = "balance_acc" @method_decorator(login_required, name="dispatch") @@ -2306,10 +2303,9 @@ class AccountStatBalanceList(UserAccountMixin, SingleResumeStat): """ model = Account - context_object_name = "account" - pk_url_kwarg = "trigramme" + slug_url_kwarg = "trigramme" + slug_field = "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}}, @@ -2330,8 +2326,8 @@ class AccountStatBalance(UserAccountMixin, JSONDetailView): """ model = Account - pk_url_kwarg = "trigramme" - context_object_name = "account" + slug_url_kwarg = "trigramme" + slug_field = "trigramme" def get_changes_list(self, last_days=None, begin_date=None, end_date=None): account = self.object @@ -2444,9 +2440,8 @@ class AccountStatOperationList(UserAccountMixin, SingleResumeStat): """ model = Account - context_object_name = "account" - pk_url_kwarg = "trigramme" - id_prefix = ID_PREFIX_ACC_LAST + slug_url_kwarg = "trigramme" + slug_field = "trigramme" nb_default = 2 stats = last_stats_manifest(types=[Operation.PURCHASE]) url_stat = "kfet.account.stat.operation" @@ -2461,9 +2456,8 @@ class AccountStatOperation(UserAccountMixin, ScaleMixin, JSONDetailView): """ model = Account - pk_url_kwarg = "trigramme" - context_object_name = "account" - id_prefix = "" + slug_url_kwarg = "trigramme" + slug_field = "trigramme" def get_operations(self, types=None): # On selectionne les opérations qui correspondent @@ -2514,15 +2508,10 @@ class ArticleStatSalesList(SingleResumeStat): """ 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() - @method_decorator(teamkfet_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) @method_decorator(teamkfet_required, name="dispatch") @@ -2536,8 +2525,7 @@ class ArticleStatSales(ScaleMixin, JSONDetailView): context_object_name = "article" def get_context_data(self, *args, **kwargs): - old_ctx = super().get_context_data(*args, **kwargs) - context = {"labels": old_ctx["labels"]} + context = super().get_context_data(*args, **kwargs) scale = self.scale all_purchases = ( From c66fb7eb6fb8417857aeaa08faca158736e7b120 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:19:06 +0100 Subject: [PATCH 105/573] Simplify statistic.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On supprime des fonctions inutiles, on lint, et on simplifie 2-3 options inutilisées. --- kfet/static/kfet/js/statistic.js | 108 ++++++++++++------------------- 1 file changed, 41 insertions(+), 67 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index 9baa08c4..23d66efe 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -1,28 +1,15 @@ -(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 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) { + 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++) { @@ -36,7 +23,7 @@ return chart_data; } - function showStats () { + function showStats() { // CALLBACK : called when a button is selected // shows the focus on the correct button @@ -44,24 +31,20 @@ $(this).addClass("focus"); // loads data and shows it - $.getJSON(this.stats_target_url, {format: 'json'}, displayStats); + $.getJSON(this.stats_target_url, 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 (var i = 0; i < charts.length; i++) { - var chart = charts[i]; - + for (let chart of data.charts) { // format the data - var chart_data = is_time_chart ? handleTimeChart(chart.values) : dictToArray(chart.values, 0); + var chart_data = is_time_chart ? handleTimeChart(chart.values) : chart.values; chart_datasets.push( { @@ -76,29 +59,24 @@ // 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: { @@ -115,26 +93,19 @@ } }], - 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(); @@ -151,23 +122,26 @@ } // initialize the interface - function initialize (data) { + function initialize(data) { // creates the bar with the buttons buttons = $("
        ", - {class: "nav stat-nav", - "aria-label": "select-period"}); + { + class: "nav stat-nav", + "aria-label": "select-period" + }); var to_click; - var context = data.stats; - for (var i = 0; i < context.length; i++) { + for (let stat of data.stats) { // creates the button - var btn_wrapper = $("
      • ", {role:"presentation"}); + var btn_wrapper = $("
      • ", { role: "presentation" }); var btn = $("", - {class: "btn btn-nav", - type: "button"}) - .text(context[i].label) - .prop("stats_target_url", context[i].url) + { + class: "btn btn-nav", + type: "button" + }) + .text(stat.label) + .prop("stats_target_url", stat.url) .on("click", showStats); // saves the default option to select @@ -189,7 +163,7 @@ // constructor (function () { - $.getJSON(url, {format: 'json'}, initialize); + $.getJSON(url, initialize); })(); }; })(jQuery); From 97cb9d1f3bfd88cac73c646e08243ea19249d877 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:20:49 +0100 Subject: [PATCH 106/573] Rework `stats_manifest` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On change la façon dont les vues gèrent l'interface avec `Scale`. Side effect : on peut avoir l'historique sur tout le temps --- kfet/static/kfet/js/statistic.js | 2 +- kfet/statistic.py | 45 ++++++------------------------ kfet/views.py | 48 +++++++++++++++++++++++--------- 3 files changed, 44 insertions(+), 51 deletions(-) diff --git a/kfet/static/kfet/js/statistic.js b/kfet/static/kfet/js/statistic.js index 23d66efe..4da17672 100644 --- a/kfet/static/kfet/js/statistic.js +++ b/kfet/static/kfet/js/statistic.js @@ -145,7 +145,7 @@ .on("click", showStats); // saves the default option to select - if (i == data.default_stat || i == 0) + if (stat.default) to_click = btn; // append the elements to the parent diff --git a/kfet/statistic.py b/kfet/statistic.py index 1578101b..b98ab4fb 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -144,43 +144,7 @@ class MonthScale(Scale): return to_kfet_day(dt).replace(day=1) -def stat_manifest( - scales_def=None, scale_args=None, scale_prefix=None, **other_url_params -): - if scale_prefix is None: - scale_prefix = "scale_" - if scales_def is None: - scales_def = [] - if scale_args is None: - scale_args = {} - manifest = [] - for label, cls in scales_def: - url_params = {scale_prefix + "name": cls.name} - url_params.update( - {scale_prefix + key: value for key, value in scale_args.items()} - ) - url_params.update(other_url_params) - manifest.append(dict(label=label, url_params=url_params)) - return manifest - - -def last_stats_manifest( - scales_def=None, scale_args=None, scale_prefix=None, **url_params -): - scales_def = [ - ("Derniers mois", MonthScale), - ("Dernières semaines", WeekScale), - ("Derniers jours", DayScale), - ] - if scale_args is None: - scale_args = {} - scale_args.update(dict(last=True, n_steps=7)) - return stat_manifest( - scales_def=scales_def, - scale_args=scale_args, - scale_prefix=scale_prefix, - **url_params - ) +def scale_url_params(scales_def, **other_url_params): """ Convertit une spécification de scales en arguments GET utilisables par ScaleMixin. La spécification est de la forme suivante : @@ -189,7 +153,14 @@ def last_stats_manifest( - other_url_params : paramètres GET supplémentaires """ + params_list = [] + for label, cls, params, default in scales_def: + url_params = {"scale_name": cls.name} + url_params.update({"scale_" + key: value for key, value in params.items()}) + url_params.update(other_url_params) + params_list.append(dict(label=label, url_params=url_params, default=default)) + return params_list class ScaleMixin(object): diff --git a/kfet/views.py b/kfet/views.py index 1dfde369..a0e3115c 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -76,7 +76,7 @@ from kfet.models import ( Transfer, TransferGroup, ) -from kfet.statistic import ScaleMixin, WeekScale, last_stats_manifest +from kfet.statistic import DayScale, MonthScale, ScaleMixin, WeekScale, scale_url_params from .auth import KFET_GENERIC_TRIGRAMME from .auth.views import ( # noqa @@ -2244,10 +2244,11 @@ class SingleResumeStat(JSONDetailView): On peut aussi définir `stats` dynamiquement, via la fonction `get_stats`. """ - nb_default = 0 - - stats = [] url_stat = None + stats = [] + + def get_stats(self): + return self.stats def get_context_data(self, **kwargs): # On n'hérite pas @@ -2271,10 +2272,10 @@ class SingleResumeStat(JSONDetailView): "url": "{url}{params}".format( url=reverse(self.url_stat, args=[url_pk]), params=url_params ), + "default": stat_def.get("default", False), } ) context["stats"] = stats - context["default_stat"] = self.nb_default return context @@ -2310,12 +2311,9 @@ class AccountStatBalanceList(UserAccountMixin, SingleResumeStat): {"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": "3 mois", "url_params": {"last_days": 90}, "default": True}, {"label": "30 jours", "url_params": {"last_days": 30}}, ] - nb_default = 0 - - @method_decorator(login_required, name="dispatch") @@ -2427,7 +2425,6 @@ class AccountStatBalance(UserAccountMixin, JSONDetailView): return context - # ------------------------ # Consommation personnelle # ------------------------ @@ -2442,11 +2439,22 @@ class AccountStatOperationList(UserAccountMixin, SingleResumeStat): model = Account slug_url_kwarg = "trigramme" slug_field = "trigramme" - 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}, + 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, types=[Operation.PURCHASE]) @method_decorator(login_required, name="dispatch") @@ -2510,8 +2518,22 @@ class ArticleStatSalesList(SingleResumeStat): model = Article 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") + .first() + .group.at + ) + scales_def = [ + ("Tout le temps", MonthScale, {"last": True, "begin": first_conso}, 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, name="dispatch") From f10d6d1a71d6075ebeddf77b8777a727a9a5a2c0 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 16:32:38 +0100 Subject: [PATCH 107/573] Bugfix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Quand un article n'a pas de conso, il a été créé il y a 1s --- kfet/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index a0e3115c..b6c49f72 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2,6 +2,7 @@ import ast import heapq import statistics from collections import defaultdict +from datetime import timedelta from decimal import Decimal from typing import List from urllib.parse import urlencode @@ -2523,9 +2524,12 @@ class ArticleStatSalesList(SingleResumeStat): first_conso = ( Operation.objects.filter(article=self.object) .order_by("group__at") + .values_list("group__at", flat=True) .first() - .group.at ) + 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}, False), ("1 an", MonthScale, {"last": True, "n_steps": 12}, False), From c9dad9465a5eea0408fd37d1499114a2502dcb44 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 9 Mar 2020 17:00:56 +0100 Subject: [PATCH 108/573] Fix tests --- kfet/tests/test_views.py | 68 +++++++++++++++++++++++++++++++--------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 08d2cb32..bcd9a9b4 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -628,37 +628,51 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): expected_stats = [ { - "label": "Derniers mois", + "label": "Tout le temps", "url": { "path": base_url, "query": { - "scale_n_steps": ["7"], + "types": ["['purchase']"], "scale_name": ["month"], + "scale_last": ["True"], + "scale_begin": [ + self.accounts["user1"].created_at.isoformat(" ") + ], + }, + }, + }, + { + "label": "1 an", + "url": { + "path": base_url, + "query": { "types": ["['purchase']"], + "scale_n_steps": ["12"], + "scale_name": ["month"], "scale_last": ["True"], }, }, }, { - "label": "Dernières semaines", + "label": "3 mois", "url": { "path": base_url, "query": { - "scale_n_steps": ["7"], + "types": ["['purchase']"], + "scale_n_steps": ["13"], "scale_name": ["week"], - "types": ["['purchase']"], "scale_last": ["True"], }, }, }, { - "label": "Derniers jours", + "label": "2 semaines", "url": { "path": base_url, "query": { - "scale_n_steps": ["7"], - "scale_name": ["day"], "types": ["['purchase']"], + "scale_n_steps": ["14"], + "scale_name": ["day"], "scale_last": ["True"], }, }, @@ -1524,6 +1538,21 @@ 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) @@ -1535,33 +1564,44 @@ class ArticleStatSalesListViewTests(ViewTestCaseMixin, TestCase): expected_stats = [ { - "label": "Derniers mois", + "label": "Tout le temps", "url": { "path": base_url, "query": { - "scale_n_steps": ["7"], + "scale_name": ["month"], + "scale_last": ["True"], + "scale_begin": [self.opegroup.at.isoformat(" ")], + }, + }, + }, + { + "label": "1 an", + "url": { + "path": base_url, + "query": { + "scale_n_steps": ["12"], "scale_name": ["month"], "scale_last": ["True"], }, }, }, { - "label": "Dernières semaines", + "label": "3 mois", "url": { "path": base_url, "query": { - "scale_n_steps": ["7"], + "scale_n_steps": ["13"], "scale_name": ["week"], "scale_last": ["True"], }, }, }, { - "label": "Derniers jours", + "label": "2 semaines", "url": { "path": base_url, "query": { - "scale_n_steps": ["7"], + "scale_n_steps": ["14"], "scale_name": ["day"], "scale_last": ["True"], }, From 61e4ad974132ffb52d06cf689bff0f860d072cbc Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 8 May 2020 11:09:29 +0200 Subject: [PATCH 109/573] Better docstring --- kfet/statistic.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index b98ab4fb..b2c1d882 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -31,8 +31,8 @@ class Scale(object): - par un début et une fin, - par un début/une fin et un nombre de subdivisions. - Si le booléen `std_chunk` est activé, les subdivisions sont standardisées : - on appelle `get_chunk_start` sur toutes les subdivisions (enfin, sur la première). + Si le booléen `std_chunk` est activé, le début de la première subdivision + est généré via la fonction `get_chunk_start`. """ name = None From c9136dbcfa6118eea5b41616eb7e76b830e6c06f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 8 May 2020 11:15:12 +0200 Subject: [PATCH 110/573] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6af67f68..639a9a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre - 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 From abb8cc5a2d66a58b4bf288801e2cac1aaca97d28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 8 May 2020 12:47:03 +0200 Subject: [PATCH 111/573] Bump python and postrgres in CI --- .gitlab-ci.yml | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9bad2072..6bb31a5f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,4 @@ -image: "python:3.5" +image: "python:3.7" variables: # GestioCOF settings @@ -18,7 +18,8 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD -.test_template: +test: + stage: test before_script: - mkdir -p vendor/{pip,apt} - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client @@ -33,7 +34,7 @@ variables: after_script: - coverage report services: - - postgres:9.6 + - postgres:11.7 - redis:latest cache: key: test @@ -43,18 +44,7 @@ variables: # Keep this disabled for now, as it may kill GitLab... # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' -test35: - extends: ".test_template" - image: "python:3.5" - stage: test - -test37: - extends: ".test_template" - image: "python:3.7" - stage: test - linters: - image: python:3.6 stage: test before_script: - mkdir -p vendor/pip @@ -81,7 +71,7 @@ migration_checks: script: python manage.py makemigrations --dry-run --check services: # this should not be necessary… - - postgres:9.6 + - postgres:11.7 cache: key: migration_checks paths: From 1ada8645b83bd719ad38effa0e67647623ff4535 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 8 May 2020 15:52:13 +0200 Subject: [PATCH 112/573] Black --- gestioncof/cms/templatetags/cofcms_tags.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/gestioncof/cms/templatetags/cofcms_tags.py b/gestioncof/cms/templatetags/cofcms_tags.py index 31368f1a..f9e62aed 100644 --- a/gestioncof/cms/templatetags/cofcms_tags.py +++ b/gestioncof/cms/templatetags/cofcms_tags.py @@ -145,12 +145,13 @@ def dates(event): ) if event.all_day: return _("du {datestart} au {dateend}{common}").format( - datestart=diffstart, - dateend=diffend, - common=common) + datestart=diffstart, dateend=diffend, common=common + ) else: - return _("du {datestart}{common} à {timestart} au {dateend} à {timeend}").format( + return _( + "du {datestart}{common} à {timestart} au {dateend} à {timeend}" + ).format( datestart=diffstart, common=common, timestart=timestart_string, @@ -162,5 +163,5 @@ def dates(event): return _("le {datestart}").format(datestart=datestart_string) else: return _("le {datestart} à {timestart}").format( - datestart=datestart_string, - timestart=timestart_string) + datestart=datestart_string, timestart=timestart_string + ) From 6384cfc7017a44b76dfeffe99c14fc2a0e291240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 8 May 2020 16:04:05 +0200 Subject: [PATCH 113/573] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 639a9a0b..bec3bfde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ### Problèmes corrigés +- Bug d'affichage quand on a beaucoup de clubs dans le cadre "Accès rapide" sur + la page des clubs +- 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. - L'historique des ventes des articles fonctionne à nouveau @@ -25,6 +28,8 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre ### Nouvelles fonctionnalités +- On n'affiche que 4 articles sur la pages "nouveautés" +- 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é) From d5e9d09044604d60bca74a6e7fff7b4a8a600007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 22 Dec 2019 21:27:28 +0100 Subject: [PATCH 114/573] Events are configurable This commit mostly reproduces the structure of gestioncof's events, renames some stuff and adds a generic export view. --- .../0003_options_and_extra_fields.py | 199 ++++++++++++++++++ events/models.py | 95 ++++++++- events/tests/test_views.py | 84 +++++++- events/views.py | 43 +++- 4 files changed, 407 insertions(+), 14 deletions(-) create mode 100644 events/migrations/0003_options_and_extra_fields.py diff --git a/events/migrations/0003_options_and_extra_fields.py b/events/migrations/0003_options_and_extra_fields.py new file mode 100644 index 00000000..8e6e624d --- /dev/null +++ b/events/migrations/0003_options_and_extra_fields.py @@ -0,0 +1,199 @@ +# 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/models.py b/events/models.py index b2876301..5838e513 100644 --- a/events/models.py +++ b/events/models.py @@ -16,7 +16,9 @@ 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, verbose_name=_("inscrit⋅e⋅s")) + subscribers = models.ManyToManyField( + User, through="Registration", verbose_name=_("inscrit⋅e⋅s") + ) class Meta: verbose_name = _("événement") @@ -26,8 +28,91 @@ class Event(models.Model): return self.title -# TODO: gérer les options (EventOption & EventOptionChoice de gestioncof) -# par exemple: "option végé au Mega (oui / non)" +class Option(models.Model): + """Event options to be selected by participants at registration. -# TODO: gérer les champs commentaires (EventCommentField & EventCommentChoice) -# par exemple: "champ "allergies / régime particulier" au Mega + The possible choices are instances of `OptionChoice` (see below). A typical example + is when the participants have the choice between different meal types (e.g. vegan / + vegetarian / no pork / with meat). In this case, the "meal type" is an `Option` and + the three alternatives are `OptionChoice`s. + """ + + 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: + 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 (see Option).""" + + option = models.ForeignKey(Option, on_delete=models.CASCADE, related_name="choices") + choice = models.CharField(_("choix"), max_length=200) + + class Meta: + 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, for event creators. + + 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 questions + such as "emergency contact". + """ + + 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 ExtraFieldContent(models.Model): + 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")) + + class Meta: + 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): + event = models.ForeignKey(Event, on_delete=models.CASCADE) + user = models.ForeignKey(User, on_delete=models.CASCADE) + options_choices = models.ManyToManyField(OptionChoice) + + class Meta: + verbose_name = _("inscription à un événement") + verbose_name_plural = _("inscriptions à un événement") + + def __str__(self): + return "inscription de {} à {}".format(self.user, self.event) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 5dc01fbb..d9a978e1 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -1,3 +1,4 @@ +import csv from unittest import mock from django.contrib.auth import get_user_model @@ -5,7 +6,14 @@ from django.contrib.auth.models import Permission from django.test import Client, TestCase from django.urls import reverse -from events.models import Event +from events.models import ( + Event, + ExtraField, + ExtraFieldContent, + Option, + OptionChoice, + Registration, +) User = get_user_model() @@ -23,7 +31,7 @@ def make_staff_user(name): return user -class CSVExportTest(TestCase): +class MessagePatch: def setUp(self): # Signals handlers on login/logout send messages. # Due to the way the Django' test Client performs login, this raise an @@ -32,11 +40,14 @@ class CSVExportTest(TestCase): 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): @@ -57,3 +68,68 @@ class CSVExportTest(TestCase): client.force_login(self.u1) r = client.get(self.url) self.assertEqual(r.status_code, 403) + + +class CSVExportContentTest(MessagePatch, 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) + + def test_simple_event(self): + self.event.subscribers.set([self.u1, self.u2]) + + participants = self.client.get(self.url).content.decode("utf-8") + participants = [ + line for line in csv.reader(participants.split("\n")) if line != [] + ] + self.assertEqual(len(participants), 3) + self.assertEqual(participants[1], ["toto_foo", "toto@a.b", "toto", "foo"]) + self.assertEqual(participants[2], ["titi_bar", "titi@a.b", "titi", "bar"]) + + 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" + ) + + participants = self.client.get(self.url).content.decode("utf-8") + participants = list(csv.reader(participants.split("\n"))) + self.assertEqual( + ["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"], + participants[1], + ) diff --git a/events/views.py b/events/views.py index 6f49cdb7..71000ed2 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 +from events.models import Event, Registration @login_required @@ -13,13 +13,46 @@ from events.models import Event 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) - 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]) + + # 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 + options_choices = list( + " & ".join( + registration.options_choices.filter(option__id=id).values_list( + "choice", flat=True + ) + ) + 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) return response From e0fd3db638700d13316b2d48e6e5ac5df8710cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 22 Dec 2019 23:08:27 +0100 Subject: [PATCH 115/573] Make events tests deterministic --- events/tests/test_views.py | 8 +++++++- events/views.py | 11 ++++------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index d9a978e1..ee17128b 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -129,7 +129,13 @@ class CSVExportContentTest(MessagePatch, TestCase): participants = self.client.get(self.url).content.decode("utf-8") participants = list(csv.reader(participants.split("\n"))) + toto_registration = participants[1] + + # This is not super nice, but it makes the test deterministic. + if toto_registration[5] == "f & d": + toto_registration[5] = "d & f" + self.assertEqual( ["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"], - participants[1], + toto_registration, ) diff --git a/events/views.py b/events/views.py index 71000ed2..248c4284 100644 --- a/events/views.py +++ b/events/views.py @@ -36,14 +36,11 @@ def participants_csv(request, event_id): row = [user.username, user.email, user.first_name, user.last_name] # Options - options_choices = list( - " & ".join( - registration.options_choices.filter(option__id=id).values_list( - "choice", flat=True - ) - ) + all_choices = registration.options_choices.values_list("choice", flat=True) + options_choices = [ + " & ".join(all_choices.filter(option__id=id)) for id in event.options.values_list("id", flat=True).order_by("id") - ) + ] row += options_choices # Extra info extra_info = list( From 8778695e951e6efbe5913eefbf1464a24bdc019e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 22 Dec 2019 23:37:20 +0100 Subject: [PATCH 116/573] Add some more documentation in events.models --- events/models.py | 51 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/events/models.py b/events/models.py index 5838e513..e334784e 100644 --- a/events/models.py +++ b/events/models.py @@ -1,3 +1,33 @@ +""" +Event framework for GestioCOF and GestioBDS. + +The events implemented in this module provide two type of customisation to event +creators (the COF and BDS staff): options and extra (text) field. + +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 if for 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.db import models from django.utils.translation import gettext_lazy as _ @@ -29,12 +59,11 @@ class Event(models.Model): class Option(models.Model): - """Event options to be selected by participants at registration. + """Extra form fields with a limited set of available choices. - The possible choices are instances of `OptionChoice` (see below). A typical example - is when the participants have the choice between different meal types (e.g. vegan / - vegetarian / no pork / with meat). In this case, the "meal type" is an `Option` and - the three alternatives are `OptionChoice`s. + The available choices are given by `OptionChoice`s (see below). A typical use-case + is for events where the participants have the choice between different menus (e.g. + vegan / vegetarian / without-pork / whatever). """ event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name="options") @@ -50,7 +79,7 @@ class Option(models.Model): class OptionChoice(models.Model): - """A possible choice for an event option (see Option).""" + """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) @@ -64,11 +93,11 @@ class OptionChoice(models.Model): class ExtraField(models.Model): - """Extra event field, for event creators. + """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 questions - such as "emergency contact". + Typical examples are "remarks" fields (of type LONGTEXT) or more specific fields + such as "emergency contact" (of type SHORTTEXT probably?). """ LONGTEXT = "longtext" @@ -87,6 +116,8 @@ class ExtraField(models.Model): 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" @@ -106,6 +137,8 @@ class ExtraFieldContent(models.Model): 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) From c2f6622a9fc2220ee8edfa58f5994103cee08a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 23 Dec 2019 00:02:29 +0100 Subject: [PATCH 117/573] Update changelog --- CHANGELOG.md | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bec3bfde..adb4e464 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,25 @@ 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 BDS -- Nouveau module clubs -- Module d'autocomplétion indépendant des apps +### Nouveau module de gestion des événements + +- 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 BDS + +Uniquement un modèle BDSProfile pour le moment… + +### Nouveau module de gestion des clubs + +Uniquement un modèle simple de clubs avec des respos. Aucune gestion des +adhérents ni des cotisations. ## Upcoming From d7d4d73af33f698e47ae833172cdfeba41671e67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 8 May 2020 16:34:19 +0200 Subject: [PATCH 118/573] typos --- events/models.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/events/models.py b/events/models.py index e334784e..9b166599 100644 --- a/events/models.py +++ b/events/models.py @@ -1,8 +1,8 @@ """ Event framework for GestioCOF and GestioBDS. -The events implemented in this module provide two type of customisation to event -creators (the COF and BDS staff): options and extra (text) field. +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 ------- @@ -10,7 +10,7 @@ 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 if for events where meals are served to participants +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. @@ -62,8 +62,8 @@ class Option(models.Model): """Extra form fields with a limited set of available choices. The available choices are given by `OptionChoice`s (see below). A typical use-case - is for events where the participants have the choice between different menus (e.g. - vegan / vegetarian / without-pork / whatever). + 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") From 5a0cf58d8a75c605c57ad15685fe24e98a3b1706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 8 May 2020 16:34:35 +0200 Subject: [PATCH 119/573] Events: more validation & uniqueness constraints --- events/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/events/models.py b/events/models.py index 9b166599..99e97a97 100644 --- a/events/models.py +++ b/events/models.py @@ -29,6 +29,7 @@ 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 _ @@ -73,6 +74,7 @@ class Option(models.Model): class Meta: verbose_name = _("option d'événement") verbose_name_plural = _("options d'événement") + unique_together = [["event", "name"]] def __str__(self): return self.name @@ -87,6 +89,7 @@ class OptionChoice(models.Model): class Meta: verbose_name = _("choix d'option d'événement") verbose_name_plural = _("choix d'option d'événement") + unique_together = [["option", "choice"]] def __str__(self): return self.choice @@ -114,6 +117,9 @@ class ExtraField(models.Model): 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: + unique_together = [["event", "name"]] + class ExtraFieldContent(models.Model): """Value entered in an extra field.""" @@ -124,9 +130,16 @@ class ExtraFieldContent(models.Model): ) 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: verbose_name = _("contenu d'un champ événement supplémentaire") verbose_name_plural = _("contenus d'un champ événement supplémentaire") + unique_together = [["field", "registration"]] def __str__(self): max_length = 50 @@ -146,6 +159,7 @@ class Registration(models.Model): class Meta: verbose_name = _("inscription à un événement") verbose_name_plural = _("inscriptions à un événement") + unique_together = [["event", "user"]] def __str__(self): return "inscription de {} à {}".format(self.user, self.event) From 24180e747e62e4a2c1b3e0ef068eafb7cc74e4f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 8 May 2020 16:40:18 +0200 Subject: [PATCH 120/573] Events: one more validation check --- events/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/events/models.py b/events/models.py index 99e97a97..7b536c86 100644 --- a/events/models.py +++ b/events/models.py @@ -156,6 +156,12 @@ class Registration(models.Model): 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: verbose_name = _("inscription à un événement") verbose_name_plural = _("inscriptions à un événement") From f642b218d0e24f57fbdb69e1aa9bbb6fe57c2242 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:44:02 +0200 Subject: [PATCH 121/573] Consistance dans les noms de fichiers --- petitscours/tests/{test_petitscours_views.py => test_views.py} | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename petitscours/tests/{test_petitscours_views.py => test_views.py} (99%) diff --git a/petitscours/tests/test_petitscours_views.py b/petitscours/tests/test_views.py similarity index 99% rename from petitscours/tests/test_petitscours_views.py rename to petitscours/tests/test_views.py index 9a3cc3dc..fed8f0a0 100644 --- a/petitscours/tests/test_petitscours_views.py +++ b/petitscours/tests/test_views.py @@ -1,9 +1,8 @@ import json import os -from django.contrib import messages from django.contrib.auth import get_user_model -from django.test import Client, TestCase +from django.test import TestCase from django.urls import reverse from gestioncof.tests.testcases import ViewTestCaseMixin From bbe831a2269a813eb230ba9b581f82ce649bc503 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:54:21 +0200 Subject: [PATCH 122/573] =?UTF-8?q?S=C3=A9pare=20un=20gros=20fourre-tout?= =?UTF-8?q?=20en=20plus=20petits=20mixins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/tests/test_views.py | 5 +- gestioncof/tests/testcases.py | 5 +- shared/tests/testcases.py | 169 ++++++++++++++++++++------------- 3 files changed, 112 insertions(+), 67 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index f757b4c2..7a21fafe 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -17,6 +17,7 @@ from django.urls import reverse from bda.models import Salle, Tirage from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer from gestioncof.tests.testcases import ViewTestCaseMixin +from shared.tests.testcases import ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper from .utils import create_member, create_root, create_user @@ -267,7 +268,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): @override_settings(LDAP_SERVER_URL="ldap_url") -class RegistrationAutocompleteViewTests(ViewTestCaseMixin, TestCase): +class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCase): url_name = "cof.registration.autocomplete" url_expected = "/autocomplete/registration" @@ -815,7 +816,7 @@ class CalendarViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) -class CalendarICSViewTests(ViewTestCaseMixin, TestCase): +class CalendarICSViewTests(ICalMixin, ViewTestCaseMixin, TestCase): url_name = "calendar.ics" auth_user = None diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/testcases.py index 43f69bbc..6da8a28f 100644 --- a/gestioncof/tests/testcases.py +++ b/gestioncof/tests/testcases.py @@ -1,4 +1,7 @@ -from shared.tests.testcases import ViewTestCaseMixin as BaseViewTestCaseMixin +from shared.tests.testcases import ( + CSVResponseMixin, + ViewTestCaseMixin as BaseViewTestCaseMixin, +) from .utils import create_member, create_staff, create_user diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 507e1361..65725af2 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -13,6 +13,111 @@ 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. + """ + + 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( + "shared.views.autocomplete.Connection", new=mock_context_manager + ) + patcher.start() + self.addCleanup(patcher.stop) + + return mock_connection + + +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: + reader_class = csv.DictReader + else: + reader_class = csv.reader + + return list(reader_class(content, **reader_kwargs)) + + +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 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) + + class TestCaseMixin: def assertForbidden(self, response): """ @@ -91,70 +196,6 @@ 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( - "shared.views.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): From 88c9187e2eb2dba79e48d67b997a2bbde0deca8e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:56:45 +0200 Subject: [PATCH 123/573] MegaHelpers devient un mixin --- gestioncof/tests/test_views.py | 51 ++++------------------------------ gestioncof/tests/testcases.py | 47 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 46 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 7a21fafe..37f105fd 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -16,7 +16,7 @@ from django.urls import reverse from bda.models import Salle, Tirage from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer -from gestioncof.tests.testcases import ViewTestCaseMixin +from gestioncof.tests.testcases import MegaHelperMixin, ViewTestCaseMixin from shared.tests.testcases import ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper @@ -505,48 +505,7 @@ class ExportMembersViewTests(ViewTestCaseMixin, TestCase): self.assertListEqual(data, expected) -class MegaHelpers: - def setUp(self): - super().setUp() - - u1 = create_user("u1") - u1.first_name = "first" - u1.last_name = "last" - u1.email = "user@mail.net" - u1.save() - u1.profile.phone = "0123456789" - u1.profile.departement = "Dept" - u1.profile.comments = "profile.comments" - u1.profile.save() - - u2 = create_user("u2") - u2.profile.save() - - m = Event.objects.create(title="MEGA 2018") - - cf1 = m.commentfields.create(name="Commentaires") - cf2 = m.commentfields.create(name="Comment Field 2", fieldtype="char") - - option_type = m.options.create(name="Orga ? Conscrit ?") - choice_orga = option_type.choices.create(value="Orga") - choice_conscrit = option_type.choices.create(value="Conscrit") - - mr1 = m.eventregistration_set.create(user=u1) - mr1.options.add(choice_orga) - mr1.comments.create(commentfield=cf1, content="Comment 1") - mr1.comments.create(commentfield=cf2, content="Comment 2") - - mr2 = m.eventregistration_set.create(user=u2) - mr2.options.add(choice_conscrit) - - self.u1 = u1 - self.u2 = u2 - self.m = m - self.choice_orga = choice_orga - self.choice_conscrit = choice_conscrit - - -class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): +class ExportMegaViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): url_name = "cof.mega_export" url_expected = "/export/mega" @@ -575,7 +534,7 @@ class ExportMegaViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): ) -class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): +class ExportMegaOrgasViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): url_name = "cof.mega_export_orgas" url_expected = "/export/mega/orgas" @@ -604,7 +563,7 @@ class ExportMegaOrgasViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): ) -class ExportMegaParticipantsViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): +class ExportMegaParticipantsViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): url_name = "cof.mega_export_participants" url_expected = "/export/mega/participants" @@ -621,7 +580,7 @@ class ExportMegaParticipantsViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): ) -class ExportMegaRemarksViewTests(MegaHelpers, ViewTestCaseMixin, TestCase): +class ExportMegaRemarksViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): url_name = "cof.mega_export_remarks" url_expected = "/export/mega/avecremarques" diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/testcases.py index 6da8a28f..2c6cbb9d 100644 --- a/gestioncof/tests/testcases.py +++ b/gestioncof/tests/testcases.py @@ -1,3 +1,4 @@ +from gestioncof.models import Event from shared.tests.testcases import ( CSVResponseMixin, ViewTestCaseMixin as BaseViewTestCaseMixin, @@ -6,6 +7,52 @@ from shared.tests.testcases import ( from .utils import create_member, create_staff, create_user +class MegaHelperMixin(CSVResponseMixin): + """ + Mixin pour aider aux tests du MEGA: création de l'event et de plusieurs + inscriptions, avec options et commentaires. + """ + + def setUp(self): + super().setUp() + + u1 = create_user("u1") + u1.first_name = "first" + u1.last_name = "last" + u1.email = "user@mail.net" + u1.save() + u1.profile.phone = "0123456789" + u1.profile.departement = "Dept" + u1.profile.comments = "profile.comments" + u1.profile.save() + + u2 = create_user("u2") + u2.profile.save() + + m = Event.objects.create(title="MEGA 2018") + + cf1 = m.commentfields.create(name="Commentaires") + cf2 = m.commentfields.create(name="Comment Field 2", fieldtype="char") + + option_type = m.options.create(name="Orga ? Conscrit ?") + choice_orga = option_type.choices.create(value="Orga") + choice_conscrit = option_type.choices.create(value="Conscrit") + + mr1 = m.eventregistration_set.create(user=u1) + mr1.options.add(choice_orga) + mr1.comments.create(commentfield=cf1, content="Comment 1") + mr1.comments.create(commentfield=cf2, content="Comment 2") + + mr2 = m.eventregistration_set.create(user=u2) + mr2.options.add(choice_conscrit) + + self.u1 = u1 + self.u2 = u2 + self.m = m + self.choice_orga = choice_orga + self.choice_conscrit = choice_conscrit + + class ViewTestCaseMixin(BaseViewTestCaseMixin): """ TestCase extension to ease testing of cof views. From b1c69eddb56974f4cc0573aa024107a8403af2b9 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:58:13 +0200 Subject: [PATCH 124/573] =?UTF-8?q?Meilleure=20doc=20(j'esp=C3=A8re=20!)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shared/tests/testcases.py | 113 +++++++++++++++++--------------------- 1 file changed, 50 insertions(+), 63 deletions(-) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index 65725af2..ae0eeb02 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -197,75 +197,68 @@ class TestCaseMixin: self.assertEqual(actual, expected) - class ViewTestCaseMixin(TestCaseMixin): """ - TestCase extension to ease tests of kfet views. + Utilitaire pour automatiser certains tests sur les vues Django. + 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. - Urls concerns - ------------- + # 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. - # Basic usage + 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 . - 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. + 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. - View url can then be accessed at the 'url' attribute. + Test d'URLS + ------------------------ - # Advanced usage + # 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`. - 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). + # 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`. - The reversed urls can be accessed at the 't_urls' attribute. + # 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`. + 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). - 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'. + 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. + TODO (?): faire pareil pour vérifier les GET/POST classiques (code 200) """ url_name = None @@ -280,19 +273,13 @@ 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(): From bb72a16b6427c7c205123e1e1fb36491591368f2 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 10 May 2020 23:58:46 +0200 Subject: [PATCH 125/573] =?UTF-8?q?Lisibilit=C3=A9:=20t=5Furls=20->=20reve?= =?UTF-8?q?rsed=5Furls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/tests/test_views.py | 6 +++--- shared/tests/testcases.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index 37f105fd..c5cb49b7 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -228,7 +228,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): auth_forbidden = [None, "user", "member"] def test_empty(self): - r = self.client.get(self.t_urls[0]) + r = self.client.get(self.reversed_urls[0]) self.assertIn("user_form", r.context) self.assertIn("profile_form", r.context) @@ -241,7 +241,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): u.last_name = "last" u.save() - r = self.client.get(self.t_urls[1]) + r = self.client.get(self.reversed_urls[1]) self.assertIn("user_form", r.context) self.assertIn("profile_form", r.context) @@ -253,7 +253,7 @@ class RegistrationFormViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(user_form["last_name"].initial, "last") def test_clipper(self): - r = self.client.get(self.t_urls[2]) + r = self.client.get(self.reversed_urls[2]) self.assertIn("user_form", r.context) self.assertIn("profile_form", r.context) diff --git a/shared/tests/testcases.py b/shared/tests/testcases.py index ae0eeb02..2a1960fb 100644 --- a/shared/tests/testcases.py +++ b/shared/tests/testcases.py @@ -350,7 +350,7 @@ class ViewTestCaseMixin(TestCaseMixin): ] @property - def t_urls(self): + def reversed_urls(self): return [ reverse( url_conf["name"], @@ -363,16 +363,16 @@ class ViewTestCaseMixin(TestCaseMixin): @property def url(self): - return self.t_urls[0] + return self.reversed_urls[0] def test_urls(self): - for url, conf in zip(self.t_urls, self.urls_conf): + for url, conf in zip(self.reversed_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.t_urls: + for url in self.reversed_urls: self.check_forbidden(method, url, user) def check_forbidden(self, method, url, user=None): From 3b43ad84b572dc51dd1d7e1826e00c3e5698836d Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 11 May 2020 00:19:43 +0200 Subject: [PATCH 126/573] Renomme testcases.py -> mixins.py --- bda/tests/{testcases.py => mixins.py} | 2 +- bda/tests/test_views.py | 2 +- gestioncof/tests/{testcases.py => mixins.py} | 2 +- gestioncof/tests/test_views.py | 4 ++-- petitscours/tests/test_views.py | 2 +- shared/tests/{testcases.py => mixins.py} | 0 6 files changed, 6 insertions(+), 6 deletions(-) rename bda/tests/{testcases.py => mixins.py} (97%) rename gestioncof/tests/{testcases.py => mixins.py} (98%) rename shared/tests/{testcases.py => mixins.py} (100%) diff --git a/bda/tests/testcases.py b/bda/tests/mixins.py similarity index 97% rename from bda/tests/testcases.py rename to bda/tests/mixins.py index f5ac7f83..a4ba057b 100644 --- a/bda/tests/testcases.py +++ b/bda/tests/mixins.py @@ -4,7 +4,7 @@ from django.conf import settings from django.core.management import call_command from django.utils import timezone -from shared.tests.testcases import ViewTestCaseMixin +from shared.tests.mixins import ViewTestCaseMixin from ..models import CategorieSpectacle, Salle, Spectacle, Tirage from .utils import create_user diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index d13fcf6c..7082725c 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 .testcases import BdATestHelpers, BdAViewTestCaseMixin +from .mixins import BdATestHelpers, BdAViewTestCaseMixin User = get_user_model() diff --git a/gestioncof/tests/testcases.py b/gestioncof/tests/mixins.py similarity index 98% rename from gestioncof/tests/testcases.py rename to gestioncof/tests/mixins.py index 2c6cbb9d..5c8d767a 100644 --- a/gestioncof/tests/testcases.py +++ b/gestioncof/tests/mixins.py @@ -1,5 +1,5 @@ from gestioncof.models import Event -from shared.tests.testcases import ( +from shared.tests.mixins import ( CSVResponseMixin, ViewTestCaseMixin as BaseViewTestCaseMixin, ) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index c5cb49b7..e33fce03 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -16,8 +16,8 @@ from django.urls import reverse from bda.models import Salle, Tirage from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer -from gestioncof.tests.testcases import MegaHelperMixin, ViewTestCaseMixin -from shared.tests.testcases import ICalMixin, MockLDAPMixin +from gestioncof.tests.mixins import MegaHelperMixin, ViewTestCaseMixin +from shared.tests.mixins import ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper from .utils import create_member, create_root, create_user diff --git a/petitscours/tests/test_views.py b/petitscours/tests/test_views.py index fed8f0a0..3ef68a5a 100644 --- a/petitscours/tests/test_views.py +++ b/petitscours/tests/test_views.py @@ -5,7 +5,7 @@ from django.contrib.auth import get_user_model from django.test import TestCase from django.urls import reverse -from gestioncof.tests.testcases import ViewTestCaseMixin +from gestioncof.tests.mixins import ViewTestCaseMixin from .utils import ( PetitCoursTestHelpers, diff --git a/shared/tests/testcases.py b/shared/tests/mixins.py similarity index 100% rename from shared/tests/testcases.py rename to shared/tests/mixins.py From 65171d1276484bfb6c707be1b9f73e9491697203 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 11 May 2020 01:16:58 +0200 Subject: [PATCH 127/573] Fix event tests --- events/tests/test_views.py | 54 ++++++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index ee17128b..1ccd3530 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -1,4 +1,3 @@ -import csv from unittest import mock from django.contrib.auth import get_user_model @@ -14,6 +13,7 @@ from events.models import ( OptionChoice, Registration, ) +from shared.tests.mixins import CSVResponseMixin User = get_user_model() @@ -70,7 +70,7 @@ class CSVExportAccessTest(MessagePatch, TestCase): self.assertEqual(r.status_code, 403) -class CSVExportContentTest(MessagePatch, TestCase): +class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): def setUp(self): super().setUp() @@ -90,13 +90,26 @@ class CSVExportContentTest(MessagePatch, TestCase): def test_simple_event(self): self.event.subscribers.set([self.u1, self.u2]) - participants = self.client.get(self.url).content.decode("utf-8") - participants = [ - line for line in csv.reader(participants.split("\n")) if line != [] - ] - self.assertEqual(len(participants), 3) - self.assertEqual(participants[1], ["toto_foo", "toto@a.b", "toto", "foo"]) - self.assertEqual(participants[2], ["titi_bar", "titi@a.b", "titi", "bar"]) + response = self.client.get(self.url) + + content = self.load_from_csv_response(response, as_dict=True) + self.assertListEqual( + content, + [ + { + "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) @@ -127,15 +140,22 @@ class CSVExportContentTest(MessagePatch, TestCase): field=field, registration=registration, content="hello" ) - participants = self.client.get(self.url).content.decode("utf-8") - participants = list(csv.reader(participants.split("\n"))) - toto_registration = participants[1] + response = self.client.get(self.url) + content = self.load_from_csv_response(response, as_dict=True) + toto_dict = content[0] # This is not super nice, but it makes the test deterministic. - if toto_registration[5] == "f & d": - toto_registration[5] = "d & f" + toto_dict["def"] = [x.strip() for x in toto_dict["def"].split("&")] - self.assertEqual( - ["toto_foo", "toto@a.b", "toto", "foo", "a", "d & f", "hello"], - toto_registration, + self.assertDictEqual( + toto_dict, + { + "username": "toto_foo", + "prénom": "toto", + "nom de famille": "foo", + "email": "toto@a.b", + "abc": "a", + "def": ["d", "f"], + "remarks": "hello", + }, ) From 50266f2466d72cd175e771c8a3ddd1754c3e15cd Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 11 May 2020 12:44:14 +0200 Subject: [PATCH 128/573] Fix tests for python3.7 (?) --- events/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 1ccd3530..7e9b0c77 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -142,10 +142,10 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): response = self.client.get(self.url) content = self.load_from_csv_response(response, as_dict=True) - toto_dict = content[0] + toto_dict = dict(content[0]) # This is not super nice, but it makes the test deterministic. - toto_dict["def"] = [x.strip() for x in toto_dict["def"].split("&")] + toto_dict["def"] = set(x.strip() for x in toto_dict["def"].split("&")) self.assertDictEqual( toto_dict, @@ -155,7 +155,7 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): "nom de famille": "foo", "email": "toto@a.b", "abc": "a", - "def": ["d", "f"], + "def": {"d", "f"}, "remarks": "hello", }, ) From 9b0440429c435084e5b61dbb7795d092a622b720 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 12 May 2020 00:47:48 +0200 Subject: [PATCH 129/573] Fix ical tests --- shared/tests/mixins.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/shared/tests/mixins.py b/shared/tests/mixins.py index 2a1960fb..235940df 100644 --- a/shared/tests/mixins.py +++ b/shared/tests/mixins.py @@ -101,8 +101,8 @@ class ICalMixin: 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 + return i + return None def assertCalEqual(self, ical_content, expected): remaining = expected.copy() @@ -111,12 +111,15 @@ class ICalMixin: cal = icalendar.Calendar.from_ical(ical_content) for ev in cal.walk("vevent"): - found, i_found = self._find_event(ev, remaining) - if found: + 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): From 6fff995ccdcc66210a63850cedba585e0d75532b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 12 May 2020 01:11:59 +0200 Subject: [PATCH 130/573] Expand CSVResponseMixin functionality --- events/tests/test_views.py | 7 ++--- gestioncof/tests/test_views.py | 56 ++++++++++++++++------------------ shared/tests/mixins.py | 24 ++++++++++++--- 3 files changed, 49 insertions(+), 38 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 7e9b0c77..a8b4ba4a 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -92,9 +92,8 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): response = self.client.get(self.url) - content = self.load_from_csv_response(response, as_dict=True) - self.assertListEqual( - content, + self.assertCSVEqual( + response, [ { "username": "toto_foo", @@ -141,7 +140,7 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): ) response = self.client.get(self.url) - content = self.load_from_csv_response(response, as_dict=True) + content = self._load_from_csv_response(response, as_dict=True) toto_dict = dict(content[0]) # This is not super nice, but it makes the test deterministic. diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index e33fce03..d522a648 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -1,4 +1,3 @@ -import csv import os import uuid from datetime import timedelta @@ -17,7 +16,7 @@ from django.urls import reverse from bda.models import Salle, Tirage from gestioncof.models import CalendarSubscription, Club, Event, Survey, SurveyAnswer from gestioncof.tests.mixins import MegaHelperMixin, ViewTestCaseMixin -from shared.tests.mixins import ICalMixin, MockLDAPMixin +from shared.tests.mixins import CSVResponseMixin, ICalMixin, MockLDAPMixin from shared.views.autocomplete import Clipper from .utils import create_member, create_root, create_user @@ -463,7 +462,7 @@ class UserAutocompleteViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) -class ExportMembersViewTests(ViewTestCaseMixin, TestCase): +class ExportMembersViewTests(CSVResponseMixin, ViewTestCaseMixin, TestCase): url_name = "cof.membres_export" url_expected = "/export/members" @@ -483,26 +482,24 @@ class ExportMembersViewTests(ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - data = list(csv.reader(r.content.decode("utf-8").split("\n")[:-1])) - expected = [ + + self.assertCSVEqual( + r, [ - str(u1.pk), - "member", - "first", - "last", - "user@mail.net", - "0123456789", - "1A", - "Dept", - "normalien", + [ + str(u1.pk), + "member", + "first", + "last", + "user@mail.net", + "0123456789", + "1A", + "Dept", + "normalien", + ], + [str(u2.pk), "staff", "", "", "", "", "1A", "", "normalien"], ], - [str(u2.pk), "staff", "", "", "", "", "1A", "", "normalien"], - ] - # Sort before checking equality, the order of the output of csv.reader - # does not seem deterministic - expected.sort(key=lambda row: int(row[0])) - data.sort(key=lambda row: int(row[0])) - self.assertListEqual(data, expected) + ) class ExportMegaViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): @@ -516,8 +513,8 @@ class ExportMegaViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual( - self.load_from_csv_response(r), + self.assertCSVEqual( + r, [ [ "u1", @@ -546,8 +543,8 @@ class ExportMegaOrgasViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual( - self.load_from_csv_response(r), + self.assertCSVEqual( + r, [ [ "u1", @@ -574,9 +571,8 @@ class ExportMegaParticipantsViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCa r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual( - self.load_from_csv_response(r), - [["u2", "", "", "", "", str(self.u2.pk), "", ""]], + self.assertCSVEqual( + r, [["u2", "", "", "", "", str(self.u2.pk), "", ""]], ) @@ -591,8 +587,8 @@ class ExportMegaRemarksViewTests(MegaHelperMixin, ViewTestCaseMixin, TestCase): r = self.client.get(self.url) self.assertEqual(r.status_code, 200) - self.assertListEqual( - self.load_from_csv_response(r), + self.assertCSVEqual( + r, [ [ "u1", diff --git a/shared/tests/mixins.py b/shared/tests/mixins.py index 235940df..8a00480e 100644 --- a/shared/tests/mixins.py +++ b/shared/tests/mixins.py @@ -58,17 +58,33 @@ class CSVResponseMixin: par la première ligne du CSV. """ - def load_from_csv_response(self, r, as_dict=False, **reader_kwargs): + 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: - reader_class = csv.DictReader + content = csv.DictReader(content, **reader_kwargs) + # en python3.7, content est une liste d'OrderedDicts + return list(map(dict, content)) else: - reader_class = csv.reader + content = csv.reader(content, **reader_kwargs) + return list(content) - return list(reader_class(content, **reader_kwargs)) + 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: From 707b7b76dbecaeca689313d39ca86b7f1ee53e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 14 May 2020 21:23:25 +0200 Subject: [PATCH 131/573] Make events tests deterministic --- events/tests/test_views.py | 30 +++++++++++++----------------- events/views.py | 2 +- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index a8b4ba4a..3e13d8cd 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -140,21 +140,17 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): ) response = self.client.get(self.url) - content = self._load_from_csv_response(response, as_dict=True) - toto_dict = dict(content[0]) - - # This is not super nice, but it makes the test deterministic. - toto_dict["def"] = set(x.strip() for x in toto_dict["def"].split("&")) - - self.assertDictEqual( - toto_dict, - { - "username": "toto_foo", - "prénom": "toto", - "nom de famille": "foo", - "email": "toto@a.b", - "abc": "a", - "def": {"d", "f"}, - "remarks": "hello", - }, + 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 248c4284..b47ae76f 100644 --- a/events/views.py +++ b/events/views.py @@ -38,7 +38,7 @@ def participants_csv(request, event_id): # Options all_choices = registration.options_choices.values_list("choice", flat=True) options_choices = [ - " & ".join(all_choices.filter(option__id=id)) + " & ".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 From 3ca8b45014dd0a28e9de5e1c2d067cf6ebe1c399 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 20 May 2020 17:41:25 +0200 Subject: [PATCH 132/573] Migration for events app --- events/migrations/0004_unique_constraints.py | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 events/migrations/0004_unique_constraints.py diff --git a/events/migrations/0004_unique_constraints.py b/events/migrations/0004_unique_constraints.py new file mode 100644 index 00000000..3d69f99c --- /dev/null +++ b/events/migrations/0004_unique_constraints.py @@ -0,0 +1,30 @@ +# 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")}, + ), + ] From 028b6f6cb7dc3de4414bdac2b992542866c38b81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 16 Jun 2020 17:21:59 +0200 Subject: [PATCH 133/573] Switch to python-ldap (instead of ldap3) --- gestioncof/tests/test_views.py | 7 +++--- requirements-prod.txt | 2 +- shared/tests/mixins.py | 41 +++++++++++++++++----------------- shared/views/autocomplete.py | 22 ++++++++++++------ 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index d522a648..09e86860 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -317,10 +317,11 @@ class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCa self._test("aa bb", [], [], [Clipper("uid", "first last")]) - mock_ldap.search.assert_called_once_with( + mock_ldap.ldap_obj.search_s.assert_called_once_with( "dc=spi,dc=ens,dc=fr", + mock_ldap.SCOPE_SUBTREE, "(&(|(cn=*aa*)(uid=*aa*))(|(cn=*bb*)(uid=*bb*)))", - attributes=["cn", "uid"], + ["cn", "uid"], ) def test_clipper_escaped(self): @@ -328,7 +329,7 @@ class RegistrationAutocompleteViewTests(MockLDAPMixin, ViewTestCaseMixin, TestCa self._test("; & | (", [], [], []) - mock_ldap.search.assert_not_called() + mock_ldap.ldap_obj.search_s.assert_not_called() def test_clipper_no_duplicate(self): self.mockLDAP([("uid", "abc")]) diff --git a/requirements-prod.txt b/requirements-prod.txt index e08ac120..a137dd67 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -11,4 +11,4 @@ asgiref==1.1.1 daphne==1.3.0 # ldap bindings -ldap3 +python-ldap diff --git a/shared/tests/mixins.py b/shared/tests/mixins.py index 8a00480e..030b3d5c 100644 --- a/shared/tests/mixins.py +++ b/shared/tests/mixins.py @@ -22,32 +22,33 @@ class MockLDAPMixin: 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): - class Elt: - def __init__(self, value): - self.value = value + entries = [ + ("whatever", {"cn": [name.encode("utf-8")], "uid": [uid.encode("utf-8")]}) + for uid, name 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) - class Entry: - def __init__(self, **kwargs): - for k, v in kwargs.items(): - setattr(self, k, Elt(v)) + # Mock ldap module whose `initialize_method` always return the same ldap object. + mock_ldap_module = self.MockLDAPModule(mock_ldap_obj) - 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( - "shared.views.autocomplete.Connection", new=mock_context_manager - ) + patcher = mock.patch("shared.views.autocomplete.ldap", new=mock_ldap_module) patcher.start() self.addCleanup(patcher.stop) - return mock_connection + return mock_ldap_module class CSVResponseMixin: diff --git a/shared/views/autocomplete.py b/shared/views/autocomplete.py index af5e3980..168abc4b 100644 --- a/shared/views/autocomplete.py +++ b/shared/views/autocomplete.py @@ -5,11 +5,11 @@ from django.conf import settings from django.db.models import Q if getattr(settings, "LDAP_SERVER_URL", None): - from ldap3 import Connection + import ldap else: # shared.tests.testcases.TestCaseMixin.mockLDAP needs - # Connection to be defined - Connection = None + # an ldap object to be in the scope + ldap = None class SearchUnit: @@ -125,12 +125,20 @@ class LDAPSearch(SearchUnit): query = self.get_ldap_query(keywords) - if Connection is None or query == "(&)": + if ldap is None or query == "(&)": return [] - with Connection(self.ldap_server_url) as conn: - conn.search(self.domain_component, query, attributes=self.search_fields) - return [Clipper(entry.uid.value, entry.cn.value) for entry in conn.entries] + ldap_obj = ldap.initialize(self.ldap_server_url) + res = ldap_obj.search_s( + self.domain_component, ldap.SCOPE_SUBTREE, query, self.search_fields + ) + return [ + Clipper( + clipper=attrs["uid"][0].decode("utf-8"), + fullname=attrs["cn"][0].decode("utf-8"), + ) + for (_, attrs) in res + ] # --- From b9ba0a38296f8297305f6f20e9a55fe660a6f0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 20 Jun 2020 17:49:56 +0200 Subject: [PATCH 134/573] Add missing ldap system dependencies to CI config --- .gitlab-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6bb31a5f..810c1132 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,7 +22,7 @@ test: stage: test before_script: - mkdir -p vendor/{pip,apt} - - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client + - 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"/' 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 @@ -64,7 +64,7 @@ migration_checks: stage: test before_script: - mkdir -p vendor/{pip,apt} - - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client + - apt-get update -q && apt-get -o dir::cache::archives="vendor/apt" install -yqq postgresql-client libldap2-dev libsasl2-dev - cp cof/settings/secret_example.py cof/settings/secret.py - pip install --upgrade -r requirements-prod.txt - python --version From c5adc6b7d8981d1ef5272848ad0becacc2c9f54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 20 Jun 2020 19:28:48 +0200 Subject: [PATCH 135/573] Use the new shared autocomplete framework in kfet/ --- kfet/autocomplete.py | 193 +++++++----------- .../kfet/account_create_autocomplete.html | 4 +- kfet/tests/test_views.py | 4 +- kfet/urls.py | 4 +- 4 files changed, 79 insertions(+), 126 deletions(-) diff --git a/kfet/autocomplete.py b/kfet/autocomplete.py index 5b23bb1e..c4e7a766 100644 --- a/kfet/autocomplete.py +++ b/kfet/autocomplete.py @@ -1,134 +1,89 @@ -from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import Q from django.http import Http404 -from django.shortcuts import render +from django.views.generic import TemplateView -from gestioncof.models import User -from kfet.decorators import teamkfet_required -from kfet.models import Account +from shared.views import autocomplete -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 +User = get_user_model() -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 - - -@teamkfet_required -def account_create(request): - if "q" not in request.GET: - raise Http404 - q = request.GET.get("q") - - if len(q) == 0: - return render(request, "kfet/account_create_autocomplete.html") - - data = {"q": q} - - 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) - - 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) - ) - - # 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"] +class KfetAccountSearch(autocomplete.ModelSearch): + model = User + search_fields = [ + "username", + "first_name", + "last_name", + "profile__account_kfet__trigramme", ] - 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)) - - # 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) + 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 -@teamkfet_required -def account_search(request): - if "q" not in request.GET: - raise Http404 - q = request.GET.get("q") - words = q.split() +class COFMemberSearch(autocomplete.ModelSearch): + model = User + search_fields = ["username", "first_name", "last_name"] - data = {"q": 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 - 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 +class OthersSearch(autocomplete.ModelSearch): + model = User + search_fields = ["username", "first_name", "last_name"] + + 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 + + +class KfetAutocomplete(autocomplete.Compose): + search_units = [ + ("kfet", "username", KfetAccountSearch), + ("users_cof", "username", COFMemberSearch), + ("users_notcof", "username", OthersSearch), + ("clippers", "clipper", autocomplete.LDAPSearch), ] - data["accounts"] = query - return render(request, "kfet/account_search_autocomplete.html", data) + +kfet_autocomplete = KfetAutocomplete() + + +class AccountCreateAutocompleteView(PermissionRequiredMixin, TemplateView): + template_name = "kfet/account_create_autocomplete.html" + permission_required = "kfet.is_team" + + 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 + results = kfet_autocomplete.search(q.split()) + ctx["options"] = sum((len(res) for res in results.values())) + ctx.update(results) + return ctx + + +class AccountSearchAutocompleteView(PermissionRequiredMixin, TemplateView): + template_name = "kfet/account_search_autocomplete.html" + permission_required = "kfet.is_team" + + 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["accounts"] = [ + (user.profile.account_kfet.trigramme, user.get_full_name()) + for user in KfetAccountSearch().search(q.split()) + ] + return ctx diff --git a/kfet/templates/kfet/account_create_autocomplete.html b/kfet/templates/kfet/account_create_autocomplete.html index 5343b945..2f04d461 100644 --- a/kfet/templates/kfet/account_create_autocomplete.html +++ b/kfet/templates/kfet/account_create_autocomplete.html @@ -8,8 +8,8 @@
      • {% if kfet %}
      • Comptes existants
      • - {% for account, user in kfet %} -
      • {{ account }} [{{ user|highlight_user:q }}]
      • + {% for user in kfet %} +
      • {{ user.account_kfet.account }} [{{ user|highlight_user:q }}]
      • {% endfor %} {% endif %} {% if users_cof %} diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index bcd9a9b4..e411bd8d 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -183,9 +183,7 @@ class AccountCreateAutocompleteViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(r.status_code, 200) self.assertEqual(len(r.context["users_notcof"]), 0) self.assertEqual(len(r.context["users_cof"]), 0) - self.assertSetEqual( - set(r.context["kfet"]), set([(self.accounts["user"], self.users["user"])]) - ) + self.assertSetEqual(set(r.context["kfet"]), set([self.users["user"]])) class AccountSearchViewTests(ViewTestCaseMixin, TestCase): diff --git a/kfet/urls.py b/kfet/urls.py index 12c06d26..a4ce450c 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -38,13 +38,13 @@ urlpatterns = [ ), path( "autocomplete/account_new", - autocomplete.account_create, + autocomplete.AccountCreateAutocompleteView.as_view(), name="kfet.account.create.autocomplete", ), # Account - Search path( "autocomplete/account_search", - autocomplete.account_search, + autocomplete.AccountSearchAutocompleteView.as_view(), name="kfet.account.search.autocomplete", ), # Account - Read From d16bf5e6b0b6147c973bdcbb952e8d35d0f0f6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 9 May 2020 11:49:05 +0200 Subject: [PATCH 136/573] Merge local and dev settings --- cof/settings/dev.py | 55 ------------------------------------------- cof/settings/local.py | 44 +++++++++++++++++++++++++++++----- 2 files changed, 38 insertions(+), 61 deletions(-) delete mode 100644 cof/settings/dev.py diff --git a/cof/settings/dev.py b/cof/settings/dev.py deleted file mode 100644 index d287eab8..00000000 --- a/cof/settings/dev.py +++ /dev/null @@ -1,55 +0,0 @@ -""" -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 index 06cdf4a0..789ec12b 100644 --- a/cof/settings/local.py +++ b/cof/settings/local.py @@ -1,14 +1,27 @@ """ -Django local settings for the cof project. +Django local development 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 +from .common import * # NOQA +from .common import BASE_DIR, 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"] + +STATIC_URL = "/static/" +MEDIA_URL = os.path.join(BASE_DIR, "media") -# Use sqlite for local development DATABASES = { "default": { "ENGINE": "django.db.backends.sqlite3", @@ -27,5 +40,24 @@ CHANNEL_LAYERS = { } } -# No need to run collectstatic -> unset STATIC_ROOT -STATIC_ROOT = None + +# --- +# 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} From d464b69b2ea20e2c7448a1423a0a99ada4a4ed53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 9 May 2020 15:48:51 +0200 Subject: [PATCH 137/573] Split settings between COF / BDS / Local --- cof/settings/{prod.py => bds_prod.py} | 19 +-- cof/settings/cof_prod.py | 162 ++++++++++++++++++++++++++ cof/settings/common.py | 151 ++++++------------------ cof/settings/local.py | 33 ++++-- 4 files changed, 229 insertions(+), 136 deletions(-) rename cof/settings/{prod.py => bds_prod.py} (55%) create mode 100644 cof/settings/cof_prod.py diff --git a/cof/settings/prod.py b/cof/settings/bds_prod.py similarity index 55% rename from cof/settings/prod.py rename to cof/settings/bds_prod.py index 748abe73..d674a0a6 100644 --- a/cof/settings/prod.py +++ b/cof/settings/bds_prod.py @@ -6,14 +6,15 @@ 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 +from .common import BASE_DIR, INSTALLED_APPS -DEBUG = False +# --- +# BDS-only Django settings +# --- -ALLOWED_HOSTS = ["cof.ens.fr", "www.cof.ens.fr", "dev.cof.ens.fr"] +ALLOWED_HOSTS = ["bds.ens.fr", "www.bds.ens.fr", "dev.cof.ens.fr"] -if TESTING: - INSTALLED_APPS += ["events", "clubs"] +INSTALLED_APPS += ["bds", "events", "clubs"] STATIC_ROOT = os.path.join( os.path.dirname(os.path.dirname(BASE_DIR)), "public", "gestion", "static" @@ -24,5 +25,9 @@ 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") +# --- +# Auth-related stuff +# --- + +LOGIN_URL = "admin:login" +LOGIN_REDIRECT_URL = "bds:home" diff --git a/cof/settings/cof_prod.py b/cof/settings/cof_prod.py new file mode 100644 index 00000000..fe60af24 --- /dev/null +++ b/cof/settings/cof_prod.py @@ -0,0 +1,162 @@ +""" +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 ( + AUTHENTICATION_BACKENDS, + BASE_DIR, + INSTALLED_APPS, + MIDDLEWARE, + TEMPLATES, + import_secret, +) + +# --- +# COF-specific secrets +# --- + +RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") +RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") +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", + "captcha", + "kfet", + "kfet.open", + "channels", + "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"] + + MIDDLEWARE + + [ + "djconfig.middleware.DjConfigMiddleware", + "wagtail.core.middleware.SiteMiddleware", + "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") + + +# --- +# Auth-related stuff +# --- + +AUTHENTICATION_BACKENDS += [ + "gestioncof.shared.COFCASBackend", + "kfet.auth.backends.GenericBackend", +] + +LOGIN_URL = "cof-login" +LOGIN_REDIRECT_URL = "home" + + +# --- +# 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 "}, + "revente": { + "FROM": "BdA-Revente ", + "REPLYTO": "BdA-Revente ", + }, +} diff --git a/cof/settings/common.py b/cof/settings/common.py index ecf464fe..0c34bf67 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -8,6 +8,10 @@ the local development server should be here. import os import sys +# --- +# Secrets +# --- + try: from . import secret except ImportError: @@ -42,19 +46,19 @@ 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") +# --- +# 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__)))) -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", @@ -64,51 +68,15 @@ INSTALLED_APPS = [ "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", - "captcha", + "cof.apps.IgnoreSrcStaticFilesConfig", "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", @@ -116,9 +84,6 @@ MIDDLEWARE = [ "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", ] @@ -127,7 +92,6 @@ ROOT_URLCONF = "cof.urls" TEMPLATES = [ { "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], "APP_DIRS": True, "OPTIONS": { "context_processors": [ @@ -138,11 +102,6 @@ TEMPLATES = [ "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", ] }, } @@ -158,43 +117,28 @@ DATABASES = { } } - -# 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 ", - }, -} +# --- +# Internationalization +# https://docs.djangoproject.com/en/1.8/topics/i18n/ +# --- -LOGIN_URL = "cof-login" -LOGIN_REDIRECT_URL = "home" +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 = "cof.locale" + + +# --- +# Auth-related stuff +# --- + +AUTHENTICATION_BACKENDS = ["django.contrib.auth.backends.ModelBackend"] CAS_SERVER_URL = "https://cas.eleves.ens.fr/" CAS_VERSION = "2" @@ -203,37 +147,23 @@ 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( + "LOCATION": "redis://:{passwd}@{host}:{port}/{db}".format( passwd=REDIS_PASSWD, host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB ), } } +# --- # Channels settings +# --- CHANNEL_LAYERS = { "default": { @@ -253,16 +183,3 @@ CHANNEL_LAYERS = { "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/local.py b/cof/settings/local.py index 789ec12b..0ccb05dd 100644 --- a/cof/settings/local.py +++ b/cof/settings/local.py @@ -1,26 +1,35 @@ -""" -Django local development settings for the cof project. -The settings that are not listed here are imported from .common -""" +"""Django local development settings.""" import os -from .common import * # NOQA -from .common import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING +from . import bds_prod +from .cof_prod import * # NOQA +from .cof_prod import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, TESTING -EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" +# --- +# 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 = None DEBUG = True +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" 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"] - STATIC_URL = "/static/" -MEDIA_URL = os.path.join(BASE_DIR, "media") +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") DATABASES = { "default": { From 6a32a72c15eca15c6f7f246775f7e281eed7aef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 9 May 2020 16:21:40 +0200 Subject: [PATCH 138/573] One url file to rule them all, one url file to find them One url file to bring them all, and in the darkness bind them. --- bds/urls.py | 2 + cof/urls.py | 241 ++++++++++++++++++++++++++++------------------------ 2 files changed, 130 insertions(+), 113 deletions(-) create mode 100644 bds/urls.py diff --git a/bds/urls.py b/bds/urls.py new file mode 100644 index 00000000..e4487422 --- /dev/null +++ b/bds/urls.py @@ -0,0 +1,2 @@ +app_label = "bds" +urlpatterns = [] diff --git a/cof/urls.py b/cof/urls.py index 374c0f1a..12cf4f5a 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -8,129 +8,141 @@ 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)), - # 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 "gestioncof" in settings.INSTALLED_APPS: + 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, + ) + from django_js_reverse.views import urls_js + from wagtail.admin import urls as wagtailadmin_urls + from wagtail.documents import urls as wagtaildocs_urls + + # Also includes BdA, K-Fêt, etc. + urlpatterns += [ + path("admin/logout/", gestioncof_views.logout), + path( + "admin///csv/", + csv_views.admin_list_export, + {"fields": ["username"]}, + ), + # 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)), + # 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", + ), + # 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 "bds" in settings.INSTALLED_APPS: + urlpatterns.append(path("bds/", include("bds.urls"))) + 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 + # → rename this when the old events system is out urlpatterns += [path("event_v2/", include("events.urls"))] if "debug_toolbar" in settings.INSTALLED_APPS: @@ -144,6 +156,9 @@ if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) # Wagtail for uncatched -urlpatterns += i18n_patterns( - path("", include(wagtail_urls)), prefix_default_language=False -) +if "wagtail.core" in settings.INSTALLED_APPS: + from wagtail.core import urls as wagtail_urls + + urlpatterns += i18n_patterns( + path("", include(wagtail_urls)), prefix_default_language=False + ) From 9a3914ece65667dcc9054e6af020bf2a4f2db5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 9 May 2020 17:03:29 +0200 Subject: [PATCH 139/573] Add wsgi file --- cof/wsgi.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 cof/wsgi.py diff --git a/cof/wsgi.py b/cof/wsgi.py new file mode 100644 index 00000000..47285284 --- /dev/null +++ b/cof/wsgi.py @@ -0,0 +1,6 @@ +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings.bds_prod") +application = get_wsgi_application() From 7a52e841e61dc431702f14afd6cb9658f9061e31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 9 May 2020 17:52:12 +0200 Subject: [PATCH 140/573] Use the new settings in gitlab-ci --- .gitlab-ci.yml | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 810c1132..36d123ff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -2,7 +2,6 @@ image: "python:3.7" variables: # GestioCOF settings - DJANGO_SETTINGS_MODULE: "cof.settings.prod" DBHOST: "postgres" REDIS_HOST: "redis" REDIS_PASSWD: "dummy" @@ -18,8 +17,7 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD -test: - stage: test +.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 @@ -44,6 +42,19 @@ test: # Keep this disabled for now, as it may kill GitLab... # coverage: '/TOTAL.*\s(\d+\.\d+)\%$/' +coftest: + stage: test + extends: .test_template + variables: + DJANGO_SETTINGS_MODULE: "cof.settings.cof_prod" + +bdstest: + stage: test + extends: .test_template + variables: + DJANGO_SETTINGS_MODULE: "cof.settings.bds_prod" + + linters: stage: test before_script: @@ -60,8 +71,7 @@ linters: - vendor/ # Check whether there are some missing migrations. -migration_checks: - stage: test +.migration_checks_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 @@ -76,3 +86,15 @@ migration_checks: key: migration_checks paths: - vendor/ + +cof_migration_checks: + stage: test + extends: .migration_checks_template + variables: + DJANGO_SETTINGS_MODULE: "cof.settings.cof_prod" + +bds_migration_checks: + stage: test + extends: .migration_checks_template + variables: + DJANGO_SETTINGS_MODULE: "cof.settings.bds_prod" From f26d3309738c991985d538d3723500e3c7346a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 14 May 2020 23:37:47 +0200 Subject: [PATCH 141/573] Fix settings.local.ALLOWED_HOSTS --- cof/settings/local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cof/settings/local.py b/cof/settings/local.py index 0ccb05dd..bb06f006 100644 --- a/cof/settings/local.py +++ b/cof/settings/local.py @@ -19,7 +19,7 @@ for app in bds_prod.INSTALLED_APPS: # Tweaks for debug/local development # --- -ALLOWED_HOSTS = None +ALLOWED_HOSTS = [] DEBUG = True EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" From 25b603d6673fccfcbbf9dbb560034654dd1d27f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 14 May 2020 23:38:06 +0200 Subject: [PATCH 142/573] only run relevant tests in cof/bds CI --- .gitlab-ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 36d123ff..0c7c3c8f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -27,8 +27,6 @@ variables: - 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: @@ -47,13 +45,16 @@ coftest: extends: .test_template variables: DJANGO_SETTINGS_MODULE: "cof.settings.cof_prod" + script: + - coverage run manage.py test gestioncof bda kfet petitscours shared --parallel bdstest: stage: test extends: .test_template variables: DJANGO_SETTINGS_MODULE: "cof.settings.bds_prod" - + script: + - coverage run manage.py test bds clubs events --parallel linters: stage: test From 3a34ab44621f43e7d1bf9e5834f25b6389e488d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 15 May 2020 20:37:37 +0200 Subject: [PATCH 143/573] Make events tests independent of LOGIN_URL --- events/tests/test_views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 3e13d8cd..b6251ae9 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -1,5 +1,6 @@ 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 @@ -59,9 +60,8 @@ class CSVExportAccessTest(MessagePatch, TestCase): def test_anonymous(self): client = Client() r = client.get(self.url) - self.assertRedirects( - r, "/login?next={}".format(self.url), fetch_redirect_response=False - ) + login_url = "{}?next={}".format(reverse(settings.LOGIN_URL), self.url) + self.assertRedirects(r, login_url, fetch_redirect_response=False) def test_unauthorised(self): client = Client() From eadfd1d3cd5b49c1c52cda6ad6cc800f085d273f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 28 Jun 2020 18:58:45 +0200 Subject: [PATCH 144/573] Use cof.settings.local for migration checks --- .gitlab-ci.yml | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0c7c3c8f..3ef29950 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -72,12 +72,15 @@ linters: - vendor/ # Check whether there are some missing migrations. -.migration_checks_template: +migration_checks: + stage: test + variables: + DJANGO_SETTINGS_MODULE: "cof.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 cof/settings/secret_example.py cof/settings/secret.py - - pip install --upgrade -r requirements-prod.txt + - pip install --upgrade -r requirements-devel.txt - python --version script: python manage.py makemigrations --dry-run --check services: @@ -87,15 +90,3 @@ linters: key: migration_checks paths: - vendor/ - -cof_migration_checks: - stage: test - extends: .migration_checks_template - variables: - DJANGO_SETTINGS_MODULE: "cof.settings.cof_prod" - -bds_migration_checks: - stage: test - extends: .migration_checks_template - variables: - DJANGO_SETTINGS_MODULE: "cof.settings.bds_prod" From f6458074b241dfa96e03641987359c494829bb5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 28 Jun 2020 19:07:45 +0200 Subject: [PATCH 145/573] Better documentation for show_toobar --- cof/settings/local.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/cof/settings/local.py b/cof/settings/local.py index bb06f006..b0ce5ae1 100644 --- a/cof/settings/local.py +++ b/cof/settings/local.py @@ -57,10 +57,15 @@ CHANNEL_LAYERS = { def show_toolbar(request): """ - On ne veut pas la vérification de INTERNAL_IPS faite par la debug-toolbar - car cela interfère avec l'utilisation de Vagrant. En effet, l'adresse de la - machine physique n'est pas forcément connue, et peut difficilement être - mise dans les INTERNAL_IPS. + 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/") From 0789da7bed3858efd3df6e34d18c635bee7ab0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 14 May 2020 22:30:28 +0200 Subject: [PATCH 146/573] Move the 'utils' template tags to the shared app --- .../templates/gestioncof/search_results.html | 2 +- gestioncof/templatetags/utils.py | 29 ----------------- .../templatetags/__init__.py | 0 shared/templatetags/search_utils.py | 32 +++++++++++++++++++ 4 files changed, 33 insertions(+), 30 deletions(-) rename {gestioncof => shared}/templatetags/__init__.py (100%) create mode 100644 shared/templatetags/search_utils.py diff --git a/gestioncof/templates/gestioncof/search_results.html b/gestioncof/templates/gestioncof/search_results.html index ba8b6580..126649b6 100644 --- a/gestioncof/templates/gestioncof/search_results.html +++ b/gestioncof/templates/gestioncof/search_results.html @@ -1,4 +1,4 @@ -{% load utils %} +{% load search_utils %}
          {% if members %} diff --git a/gestioncof/templatetags/utils.py b/gestioncof/templatetags/utils.py index 21518614..6b2122b6 100644 --- a/gestioncof/templatetags/utils.py +++ b/gestioncof/templatetags/utils.py @@ -1,7 +1,4 @@ -import re - from django import template -from django.utils.safestring import mark_safe register = template.Library() @@ -15,29 +12,3 @@ def key(d, key_name): value = settings.TEMPLATE_STRING_IF_INVALID return value - - -def highlight_text(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) - ) - - -@register.filter -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 -def highlight_clipper(clipper, q): - if clipper.fullname: - text = "%s (%s)" % (clipper.fullname, clipper.clipper) - else: - text = clipper.clipper - return highlight_text(text, q) diff --git a/gestioncof/templatetags/__init__.py b/shared/templatetags/__init__.py similarity index 100% rename from gestioncof/templatetags/__init__.py rename to shared/templatetags/__init__.py diff --git a/shared/templatetags/search_utils.py b/shared/templatetags/search_utils.py new file mode 100644 index 00000000..28851248 --- /dev/null +++ b/shared/templatetags/search_utils.py @@ -0,0 +1,32 @@ +import re + +from django import template +from django.utils.safestring import mark_safe + +register = template.Library() + + +def highlight_text(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) + ) + + +@register.filter +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 +def highlight_clipper(clipper, q): + if clipper.fullname: + text = "%s (%s)" % (clipper.fullname, clipper.clipper) + else: + text = clipper.clipper + return highlight_text(text, q) From bca75dbf98d831561ef224395710a777f5de3200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 5 Jun 2020 17:02:16 +0200 Subject: [PATCH 147/573] Add user-search in the BDS app --- bds/autocomplete.py | 37 ++++++++++++++ bds/templates/bds/search_results.html | 73 +++++++++++++++++++++++++++ bds/urls.py | 10 +++- bds/views.py | 18 ++++++- 4 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 bds/autocomplete.py create mode 100644 bds/templates/bds/search_results.html diff --git a/bds/autocomplete.py b/bds/autocomplete.py new file mode 100644 index 00000000..0a240cea --- /dev/null +++ b/bds/autocomplete.py @@ -0,0 +1,37 @@ +from django.contrib.auth import get_user_model +from django.db.models import Q + +from shared.views import autocomplete + +User = get_user_model() + + +class BDSMemberSearch(autocomplete.ModelSearch): + model = User + search_fields = ["username", "first_name", "last_name"] + + 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 + + +class BDSOthersSearch(autocomplete.ModelSearch): + model = User + search_fields = ["username", "first_name", "last_name"] + + 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 + + +class BDSSearch(autocomplete.Compose): + search_units = [ + ("members", "username", BDSMemberSearch), + ("others", "username", BDSOthersSearch), + ("clippers", "clipper", autocomplete.LDAPSearch), + ] + + +bds_search = BDSSearch() diff --git a/bds/templates/bds/search_results.html b/bds/templates/bds/search_results.html new file mode 100644 index 00000000..b1c46622 --- /dev/null +++ b/bds/templates/bds/search_results.html @@ -0,0 +1,73 @@ +{% load i18n %} +{% load search_utils %} + +
            + {% if members %} +
          • + {% trans "Membres" %} +
          • + {% for user in members %} + {% if forloop.counter < 5 %} +
          • + + {{ user|highlight_user:q }} + +
          • + {% elif forloop.counter == 5 %} +
          • + ... +
          • + {% endif %} + {% endfor %} + {% endif %} + + {% if others %} +
          • + {% trans "Non-membres" %} +
          • + {% for user in others %} + {% if forloop.counter < 5 %} +
          • + + {{ user|highlight_user:q }} + +
          • + {% elif forloop.counter == 5 %} +
          • + ... +
          • + {% endif %} + {% endfor %} + {% endif %} + + {% if clippers %} +
          • {% trans "Utilisateurs clipper" %}
          • + {% for clipper in clippers %} + {% if forloop.counter < 5 %} +
          • + + {{ clipper|highlight_clipper:q }} + +
          • + {% elif forloop.counter == 5 %} +
          • + ... +
          • + {% endif %} + {% endfor %} + {% endif %} + + {% if total %} +
          • + {% trans "Pas dans la liste ?" %} +
          • + {% else %} +
          • + {% trans "Aucune correspondance trouvée" %} +
          • + {% endif %} + +
          • + {% trans "Créer un compte" %} +
          • +
          diff --git a/bds/urls.py b/bds/urls.py index e4487422..8e72a1c1 100644 --- a/bds/urls.py +++ b/bds/urls.py @@ -1,2 +1,8 @@ -app_label = "bds" -urlpatterns = [] +from django.urls import path + +from bds import views + +app_name = "bds" +urlpatterns = [ + path("autocomplete", views.AutocompleteView.as_view(), name="autocomplete"), +] diff --git a/bds/views.py b/bds/views.py index 60f00ef0..40670a56 100644 --- a/bds/views.py +++ b/bds/views.py @@ -1 +1,17 @@ -# Create your views here. +from django.http import Http404 +from django.views.generic import TemplateView + +from bds.autocomplete import bds_search + + +class AutocompleteView(TemplateView): + template_name = "bds/search_results.html" + + def get_context_data(self, *args, **kwargs): + ctx = super().get_context_data(*args, **kwargs) + if "q" not in self.request.GET: + raise Http404 + q = self.request.GET["q"] + ctx["q"] = q + ctx.update(bds_search.search(q.split())) + return ctx From 5d24786e20165e559357bfe7b1282f0db465f8ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Jun 2020 20:58:33 +0200 Subject: [PATCH 148/573] BDS: user search on the home page --- bds/static/bds/css/bds.css | 70 +++++++++++++++++++++++++++++++++++++ bds/templates/bds/base.html | 23 ++++++++++++ bds/templates/bds/home.html | 7 ++++ bds/templates/bds/nav.html | 29 +++++++++++++++ bds/urls.py | 1 + bds/views.py | 8 ++++- 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 bds/static/bds/css/bds.css create mode 100644 bds/templates/bds/base.html create mode 100644 bds/templates/bds/home.html create mode 100644 bds/templates/bds/nav.html diff --git a/bds/static/bds/css/bds.css b/bds/static/bds/css/bds.css new file mode 100644 index 00000000..9bce2d92 --- /dev/null +++ b/bds/static/bds/css/bds.css @@ -0,0 +1,70 @@ +html, body { + padding: 0; + margin: 0; + background: #ddcecc; + font-size: 18px; +} + +a { + text-decoration: none; + color: inherit; +} + +/* header */ + +nav { + background: #3d2464; + width: 100%; + text-align: center; + padding: 0.4em 0; +} + +input[type="text"] { + font-size: 18px; +} + +#search_autocomplete { + width: 480px; + margin: 0; + border: 0; + padding: 10px 10px; +} + +.highlight { + text-decoration: underline; + font-weight: bold; +} + +.yourlabs-autocomplete ul { + width: 500px; + list-style: none; + padding: 0; + margin: 0; +} + +.yourlabs-autocomplete ul li { + height: 2em; + line-height: 2em; + width: 500px; + padding: 0; +} + +.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: white; +} diff --git a/bds/templates/bds/base.html b/bds/templates/bds/base.html new file mode 100644 index 00000000..0bf34287 --- /dev/null +++ b/bds/templates/bds/base.html @@ -0,0 +1,23 @@ +{% load staticfiles %} + + + + + {{ site.name }} + + + + + {# CSS #} + + + {# Javascript #} + + + + + {% include "bds/nav.html" %} + + {% block content %}{% endblock %} + + diff --git a/bds/templates/bds/home.html b/bds/templates/bds/home.html new file mode 100644 index 00000000..2cf20e4a --- /dev/null +++ b/bds/templates/bds/home.html @@ -0,0 +1,7 @@ +{% extends "bds/base.html" %} + +{% block content %} +
          + Bienvenue sur le site du BDS! +
          +{% endblock %} diff --git a/bds/templates/bds/nav.html b/bds/templates/bds/nav.html new file mode 100644 index 00000000..c7dbc70a --- /dev/null +++ b/bds/templates/bds/nav.html @@ -0,0 +1,29 @@ +{% load i18n %} + + + + diff --git a/bds/urls.py b/bds/urls.py index 8e72a1c1..fbddccc6 100644 --- a/bds/urls.py +++ b/bds/urls.py @@ -4,5 +4,6 @@ from bds import views app_name = "bds" urlpatterns = [ + path("", views.Home.as_view(), name="home"), path("autocomplete", views.AutocompleteView.as_view(), name="autocomplete"), ] diff --git a/bds/views.py b/bds/views.py index 40670a56..a8d78c42 100644 --- a/bds/views.py +++ b/bds/views.py @@ -13,5 +13,11 @@ class AutocompleteView(TemplateView): raise Http404 q = self.request.GET["q"] ctx["q"] = q - ctx.update(bds_search.search(q.split())) + results = bds_search.search(q.split()) + ctx.update(results) + ctx["total"] = sum((len(r) for r in results.values())) return ctx + + +class Home(TemplateView): + template_name = "bds/home.html" From c52bac05b3425caba93296058614fcdb597c73ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Jun 2020 21:01:54 +0200 Subject: [PATCH 149/573] Restrict bds views to the staff --- bds/mixins.py | 5 +++++ bds/views.py | 5 +++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 bds/mixins.py diff --git a/bds/mixins.py b/bds/mixins.py new file mode 100644 index 00000000..14fac693 --- /dev/null +++ b/bds/mixins.py @@ -0,0 +1,5 @@ +from django.contrib.auth.mixins import PermissionRequiredMixin + + +class StaffRequiredMixin(PermissionRequiredMixin): + permission_required = "bds:is_team" diff --git a/bds/views.py b/bds/views.py index a8d78c42..a2ba3a2c 100644 --- a/bds/views.py +++ b/bds/views.py @@ -2,9 +2,10 @@ from django.http import Http404 from django.views.generic import TemplateView from bds.autocomplete import bds_search +from bds.mixins import StaffRequiredMixin -class AutocompleteView(TemplateView): +class AutocompleteView(StaffRequiredMixin, TemplateView): template_name = "bds/search_results.html" def get_context_data(self, *args, **kwargs): @@ -19,5 +20,5 @@ class AutocompleteView(TemplateView): return ctx -class Home(TemplateView): +class Home(StaffRequiredMixin, TemplateView): template_name = "bds/home.html" From 56f1edebe30887f57e67d839eca21a2905a0fcbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Jun 2020 23:15:46 +0200 Subject: [PATCH 150/573] BDS: fancier home page --- bds/static/bds/css/bds.css | 22 +++++++-- bds/static/bds/images/logo.svg | 15 ++++++ bds/static/bds/images/logout.svg | 80 ++++++++++++++++++++++++++++++++ bds/templates/bds/home.html | 19 +++++++- bds/templates/bds/nav.html | 25 +++++++--- 5 files changed, 147 insertions(+), 14 deletions(-) create mode 100644 bds/static/bds/images/logo.svg create mode 100644 bds/static/bds/images/logout.svg diff --git a/bds/static/bds/css/bds.css b/bds/static/bds/css/bds.css index 9bce2d92..fe9b2fa2 100644 --- a/bds/static/bds/css/bds.css +++ b/bds/static/bds/css/bds.css @@ -7,16 +7,23 @@ html, body { a { text-decoration: none; - color: inherit; + color: #a82305; } /* header */ nav { - background: #3d2464; - width: 100%; - text-align: center; - padding: 0.4em 0; + display: flex; + flex-flow: row wrap; + justify-content: space-between; + align-items: center; + background: #3e2263; + height: 3em; + padding: 0.4em 1em; +} + +nav a, nav a img { + height: 100%; } input[type="text"] { @@ -24,6 +31,7 @@ input[type="text"] { } #search_autocomplete { + flex: 1; width: 480px; margin: 0; border: 0; @@ -53,6 +61,10 @@ input[type="text"] { background: #e8554e; } +.yourlabs-autocomplete ul li a { + color: inherit; +} + .autocomplete-item { display: block; width: 480px; diff --git a/bds/static/bds/images/logo.svg b/bds/static/bds/images/logo.svg new file mode 100644 index 00000000..15292488 --- /dev/null +++ b/bds/static/bds/images/logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/bds/static/bds/images/logout.svg b/bds/static/bds/images/logout.svg new file mode 100644 index 00000000..12489bbd --- /dev/null +++ b/bds/static/bds/images/logout.svg @@ -0,0 +1,80 @@ + + + + + + + + + + +image/svg+xmlOpenclipart diff --git a/bds/templates/bds/home.html b/bds/templates/bds/home.html index 2cf20e4a..1ae76227 100644 --- a/bds/templates/bds/home.html +++ b/bds/templates/bds/home.html @@ -1,7 +1,22 @@ {% extends "bds/base.html" %} {% block content %} -
          - Bienvenue sur le site du BDS! +
          + Bienvenue sur GestioBDS ! + +
          +
          + + 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 %} diff --git a/bds/templates/bds/nav.html b/bds/templates/bds/nav.html index c7dbc70a..e1118caa 100644 --- a/bds/templates/bds/nav.html +++ b/bds/templates/bds/nav.html @@ -1,13 +1,24 @@ {% load i18n %} +{% load static %} - {% include "bds/nav.html" %} - - {% if messages %} - {% for message in messages %} -

          - {% if 'safe' in message.tags %} - {{ message|safe }} - {% else %} - {{ message }} - {% endif %} -

          - {% endfor %} - {% endif %} - - {% block content %}{% endblock %} + {% block body %}{% endblock %} diff --git a/bds/templates/bds/base_layout.html b/bds/templates/bds/base_layout.html new file mode 100644 index 00000000..72e18513 --- /dev/null +++ b/bds/templates/bds/base_layout.html @@ -0,0 +1,28 @@ +{% extends "bds/base.html" %} + +{% block body %} + +{% include "bds/nav.html" %} + +{% if messages %} + {% for message in messages %} +

          + {% if 'safe' in message.tags %} + {{ message|safe }} + {% else %} + {{ message }} + {% endif %} +

          + {% endfor %} +{% endif %} + +
          +
          +
          + {% block content %} + {% endblock content %} +
          +
          +
          + +{% endblock body %} diff --git a/bds/templates/bds/user_update.html b/bds/templates/bds/user_update.html index e922aa92..dfa84611 100644 --- a/bds/templates/bds/user_update.html +++ b/bds/templates/bds/user_update.html @@ -1,27 +1,31 @@ -{% extends "bds/base.html" %} +{% extends "bds/base_layout.html" %} {% load i18n %} {% block content %} - {% for error in user_form.non_field_errors %} -

          {{ error }}

          - {% endfor %} - {% for error in profile_form.non_field_errors %} -

          {{ error }}

          - {% endfor %} -
          -
          - {% csrf_token %} +{% for error in user_form.non_field_errors %} +

          {{ error }}

          +{% endfor %} +{% for error in profile_form.non_field_errors %} +

          {{ error }}

          +{% endfor %} - - - {{ user_form.as_table }} - {{ profile_form.as_table }} - -
          +

          {% trans "Modification de l'utilisateur " %}{{user_form.instance.username}}

          + +
          + + {% csrf_token %} + + {% include "bds/forms/form.html" with form=user_form errors=False %} + {% include "bds/forms/form.html" with form=profile_form errors=False %} + +
          +

          + +

          +
          + +
          - - -
          {% endblock %} From deae1c46397f6aa6436e62d826d0f7548dedada7 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 19 Jul 2020 16:46:28 +0200 Subject: [PATCH 171/573] FontAwesome : gestioncof -> shared --- bds/templates/bds/base.html | 2 +- gestioncof/templates/base.html | 2 +- .../static}/src/font-awesome/css/font-awesome.css | 0 .../static}/src/font-awesome/fonts/FontAwesome.otf | Bin .../src/font-awesome/fonts/fontawesome-webfont.eot | Bin .../src/font-awesome/fonts/fontawesome-webfont.svg | 0 .../src/font-awesome/fonts/fontawesome-webfont.ttf | Bin .../src/font-awesome/fonts/fontawesome-webfont.woff | Bin .../font-awesome/fonts/fontawesome-webfont.woff2 | Bin .../static}/src/font-awesome/images/no.png | Bin .../static}/src/font-awesome/images/none.png | Bin .../static}/src/font-awesome/images/yes.png | Bin .../vendor/font-awesome/css/font-awesome.min.css | 0 .../vendor/font-awesome/fonts/FontAwesome.otf | Bin .../font-awesome/fonts/fontawesome-webfont.eot | Bin .../font-awesome/fonts/fontawesome-webfont.svg | 0 .../font-awesome/fonts/fontawesome-webfont.ttf | Bin .../font-awesome/fonts/fontawesome-webfont.woff | Bin .../font-awesome/fonts/fontawesome-webfont.woff2 | Bin .../static}/vendor/font-awesome/images/no.png | Bin .../static}/vendor/font-awesome/images/none.png | Bin .../static}/vendor/font-awesome/images/yes.png | Bin 22 files changed, 2 insertions(+), 2 deletions(-) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/css/font-awesome.css (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/fonts/FontAwesome.otf (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/fonts/fontawesome-webfont.eot (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/fonts/fontawesome-webfont.svg (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/fonts/fontawesome-webfont.ttf (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/fonts/fontawesome-webfont.woff (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/fonts/fontawesome-webfont.woff2 (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/images/no.png (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/images/none.png (100%) rename {gestioncof/static/gestioncof => shared/static}/src/font-awesome/images/yes.png (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/css/font-awesome.min.css (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/fonts/FontAwesome.otf (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/fonts/fontawesome-webfont.eot (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/fonts/fontawesome-webfont.svg (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/fonts/fontawesome-webfont.ttf (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/fonts/fontawesome-webfont.woff (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/fonts/fontawesome-webfont.woff2 (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/images/no.png (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/images/none.png (100%) rename {gestioncof/static/gestioncof => shared/static}/vendor/font-awesome/images/yes.png (100%) diff --git a/bds/templates/bds/base.html b/bds/templates/bds/base.html index 60f58702..2000a06a 100644 --- a/bds/templates/bds/base.html +++ b/bds/templates/bds/base.html @@ -10,7 +10,7 @@ {# CSS #} - + {# Javascript #} diff --git a/gestioncof/templates/base.html b/gestioncof/templates/base.html index ae484461..d313ee9d 100644 --- a/gestioncof/templates/base.html +++ b/gestioncof/templates/base.html @@ -13,7 +13,7 @@ - + {# JS #} diff --git a/gestioncof/static/gestioncof/src/font-awesome/css/font-awesome.css b/shared/static/src/font-awesome/css/font-awesome.css similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/css/font-awesome.css rename to shared/static/src/font-awesome/css/font-awesome.css diff --git a/gestioncof/static/gestioncof/src/font-awesome/fonts/FontAwesome.otf b/shared/static/src/font-awesome/fonts/FontAwesome.otf similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/fonts/FontAwesome.otf rename to shared/static/src/font-awesome/fonts/FontAwesome.otf diff --git a/gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.eot b/shared/static/src/font-awesome/fonts/fontawesome-webfont.eot similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.eot rename to shared/static/src/font-awesome/fonts/fontawesome-webfont.eot diff --git a/gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.svg b/shared/static/src/font-awesome/fonts/fontawesome-webfont.svg similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.svg rename to shared/static/src/font-awesome/fonts/fontawesome-webfont.svg diff --git a/gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.ttf b/shared/static/src/font-awesome/fonts/fontawesome-webfont.ttf similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.ttf rename to shared/static/src/font-awesome/fonts/fontawesome-webfont.ttf diff --git a/gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.woff b/shared/static/src/font-awesome/fonts/fontawesome-webfont.woff similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.woff rename to shared/static/src/font-awesome/fonts/fontawesome-webfont.woff diff --git a/gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.woff2 b/shared/static/src/font-awesome/fonts/fontawesome-webfont.woff2 similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/fonts/fontawesome-webfont.woff2 rename to shared/static/src/font-awesome/fonts/fontawesome-webfont.woff2 diff --git a/gestioncof/static/gestioncof/src/font-awesome/images/no.png b/shared/static/src/font-awesome/images/no.png similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/images/no.png rename to shared/static/src/font-awesome/images/no.png diff --git a/gestioncof/static/gestioncof/src/font-awesome/images/none.png b/shared/static/src/font-awesome/images/none.png similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/images/none.png rename to shared/static/src/font-awesome/images/none.png diff --git a/gestioncof/static/gestioncof/src/font-awesome/images/yes.png b/shared/static/src/font-awesome/images/yes.png similarity index 100% rename from gestioncof/static/gestioncof/src/font-awesome/images/yes.png rename to shared/static/src/font-awesome/images/yes.png diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/css/font-awesome.min.css b/shared/static/vendor/font-awesome/css/font-awesome.min.css similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/css/font-awesome.min.css rename to shared/static/vendor/font-awesome/css/font-awesome.min.css diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/fonts/FontAwesome.otf b/shared/static/vendor/font-awesome/fonts/FontAwesome.otf similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/fonts/FontAwesome.otf rename to shared/static/vendor/font-awesome/fonts/FontAwesome.otf diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.eot b/shared/static/vendor/font-awesome/fonts/fontawesome-webfont.eot similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.eot rename to shared/static/vendor/font-awesome/fonts/fontawesome-webfont.eot diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.svg b/shared/static/vendor/font-awesome/fonts/fontawesome-webfont.svg similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.svg rename to shared/static/vendor/font-awesome/fonts/fontawesome-webfont.svg diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.ttf b/shared/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.ttf rename to shared/static/vendor/font-awesome/fonts/fontawesome-webfont.ttf diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.woff b/shared/static/vendor/font-awesome/fonts/fontawesome-webfont.woff similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.woff rename to shared/static/vendor/font-awesome/fonts/fontawesome-webfont.woff diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.woff2 b/shared/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/fonts/fontawesome-webfont.woff2 rename to shared/static/vendor/font-awesome/fonts/fontawesome-webfont.woff2 diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/images/no.png b/shared/static/vendor/font-awesome/images/no.png similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/images/no.png rename to shared/static/vendor/font-awesome/images/no.png diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/images/none.png b/shared/static/vendor/font-awesome/images/none.png similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/images/none.png rename to shared/static/vendor/font-awesome/images/none.png diff --git a/gestioncof/static/gestioncof/vendor/font-awesome/images/yes.png b/shared/static/vendor/font-awesome/images/yes.png similarity index 100% rename from gestioncof/static/gestioncof/vendor/font-awesome/images/yes.png rename to shared/static/vendor/font-awesome/images/yes.png From e323f2f755a758fefd9e15eef3b81bf7eadb280e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 19 Jul 2020 19:28:44 +0200 Subject: [PATCH 172/573] Bulmafy navbar --- bds/static/bds/css/bds.css | 2 +- bds/static/bds/css/bds.css.map | 2 +- bds/static/bds/images/logo_square.svg | 42 +++++++++++++++++++++++++++ bds/static/src/sass/bds.scss | 34 ++++++---------------- bds/templates/bds/home.html | 2 +- bds/templates/bds/nav.html | 25 +++++++++++----- 6 files changed, 71 insertions(+), 36 deletions(-) create mode 100644 bds/static/bds/images/logo_square.svg diff --git a/bds/static/bds/css/bds.css b/bds/static/bds/css/bds.css index 3c1ccb08..0ee13941 100644 --- a/bds/static/bds/css/bds.css +++ b/bds/static/bds/css/bds.css @@ -1 +1 @@ -/*! 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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a}.progress::-moz-progress-bar{background-color:#4a4a4a}.progress::-ms-fill{background-color:#4a4a4a;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, #4a4a4a 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:#4a4a4a;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:#4a4a4a;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:#4a4a4a}.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:#4a4a4a}.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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a}.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:#4a4a4a;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{padding:0;margin:0;background:#ddcecc;font-size:18px}a{text-decoration:none;color:#a82305}nav{display:flex;flex-flow:row wrap;justify-content:space-between;align-items:center;background:#3e2263;height:3em;padding:.4em 1em}nav a,nav a img{height:100%}#search_autocomplete{flex:1;width:480px;margin:0;padding:10px 10px}.highlight{text-decoration:underline;font-weight:bold}.yourlabs-autocomplete ul{width:500px;list-style:none;padding:0;margin:0}.yourlabs-autocomplete ul li{height:2em;line-height:2em;width:500px;padding:0}.yourlabs-autocomplete ul li.hilight{background:#e8554e}.yourlabs-autocomplete ul li a{color:inherit}.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}.error{background:red;color:#fff;width:100%;padding:.5em 0;margin:0;font-size:1.2em;text-align:center}.success{background:green;color:#fff;width:100%;padding:.5em 0;margin:0;font-size:1.2em;text-align:center}/*# sourceMappingURL=bds.css.map */ +/*! 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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a}.progress::-moz-progress-bar{background-color:#4a4a4a}.progress::-ms-fill{background-color:#4a4a4a;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, #4a4a4a 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:#4a4a4a;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:#4a4a4a;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:#4a4a4a}.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:#4a4a4a}.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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a;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:#4a4a4a}.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:#4a4a4a;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}#search-bar :first-child{justify-content:left}#search-bar :last-child{justify-content:right}.highlight{text-decoration:underline;font-weight:bold}.yourlabs-autocomplete ul{width:500px;list-style:none;padding:0;margin:0}.yourlabs-autocomplete ul li{height:2em;line-height:2em;width:500px;padding:0}.yourlabs-autocomplete ul li.hilight{background:#e8554e}.yourlabs-autocomplete ul li a{color:inherit}.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}.error{background:red;color:#fff;width:100%;padding:.5em 0;margin:0;font-size:1.2em;text-align:center}.success{background:green;color:#fff;width:100%;padding:.5em 0;margin:0;font-size:1.2em;text-align:center}/*# sourceMappingURL=bds.css.map */ diff --git a/bds/static/bds/css/bds.css.map b/bds/static/bds/css/bds.css.map index 5f54ba79..53156849 100644 --- a/bds/static/bds/css/bds.css.map +++ b/bds/static/bds/css/bds.css.map @@ -1 +1 @@ -{"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","../../../../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","../../src/sass/bds.scss"],"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,MH1Da,QG2Db,UAzDe,IA0Df,YH1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,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,UrBVK,OqBWP,gBACE,UrBdK,QqBgBH,+BACE,eACN,eACE,UrBpBK,OqBsBH,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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,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,iBpCkBJ,qCoChBE,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,kBpCnCN,2CoCqCE,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,kBpC1GN,sCoC4GE,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,kBpC/JN,sCoCiKE,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,kBpCzMJ,sCoC2MA,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,kBpCnPJ,sCoCqPA,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,mBpCnXF,2CoCsXE,0BACE,cpC3WJ,sCoC8WE,oBACE,cAGJ,qBACE,qBACA,wCACA,yCACA,6BACE,8BACA,+BAEA,0BACE,kBpC3YN,qCoC6YM,iCACE,mBpC1YR,2CoC4YM,iCACE,mBpCzYR,4DoC2YM,sCACE,mBpCxYR,sCoC0YM,gCACE,mBpCvYR,sCoCyYM,kCACE,mBpCrYN,6DoCuYI,uCACE,mBpC9XN,sCoCgYI,qCACE,mBpC5XN,6DoC8XI,0CACE,mBpCrXN,sCoCuXI,iCACE,mBA5BJ,0BACE,qBpC3YN,qCoC6YM,iCACE,sBpC1YR,2CoC4YM,iCACE,sBpCzYR,4DoC2YM,sCACE,sBpCxYR,sCoC0YM,gCACE,sBpCvYR,sCoCyYM,kCACE,sBpCrYN,6DoCuYI,uCACE,sBpC9XN,sCoCgYI,qCACE,sBpC5XN,6DoC8XI,0CACE,sBpCrXN,sCoCuXI,iCACE,sBA5BJ,0BACE,oBpC3YN,qCoC6YM,iCACE,qBpC1YR,2CoC4YM,iCACE,qBpCzYR,4DoC2YM,sCACE,qBpCxYR,sCoC0YM,gCACE,qBpCvYR,sCoCyYM,kCACE,qBpCrYN,6DoCuYI,uCACE,qBpC9XN,sCoCgYI,qCACE,qBpC5XN,6DoC8XI,0CACE,qBpCrXN,sCoCuXI,iCACE,qBA5BJ,0BACE,qBpC3YN,qCoC6YM,iCACE,sBpC1YR,2CoC4YM,iCACE,sBpCzYR,4DoC2YM,sCACE,sBpCxYR,sCoC0YM,gCACE,sBpCvYR,sCoCyYM,kCACE,sBpCrYN,6DoCuYI,uCACE,sBpC9XN,sCoCgYI,qCACE,sBpC5XN,6DoC8XI,0CACE,sBpCrXN,sCoCuXI,iCACE,sBA5BJ,0BACE,kBpC3YN,qCoC6YM,iCACE,mBpC1YR,2CoC4YM,iCACE,mBpCzYR,4DoC2YM,sCACE,mBpCxYR,sCoC0YM,gCACE,mBpCvYR,sCoCyYM,kCACE,mBpCrYN,6DoCuYI,uCACE,mBpC9XN,sCoCgYI,qCACE,mBpC5XN,6DoC8XI,0CACE,mBpCrXN,sCoCuXI,iCACE,mBA5BJ,0BACE,qBpC3YN,qCoC6YM,iCACE,sBpC1YR,2CoC4YM,iCACE,sBpCzYR,4DoC2YM,sCACE,sBpCxYR,sCoC0YM,gCACE,sBpCvYR,sCoCyYM,kCACE,sBpCrYN,6DoCuYI,uCACE,sBpC9XN,sCoCgYI,qCACE,sBpC5XN,6DoC8XI,0CACE,sBpCrXN,sCoCuXI,iCACE,sBA5BJ,0BACE,oBpC3YN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ACA,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,4B5C6EJ,qC4C9EE,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6B5CiFJ,2C4ClFE,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6B5CyFJ,sC4C1FE,iBACE,0BADF,iBACE,4BADF,iBACE,0BADF,iBACE,4BADF,iBACE,6BADF,iBACE,0BADF,iBACE,6B5C6FJ,sC4C9FE,mBACE,0BADF,mBACE,4BADF,mBACE,0BADF,mBACE,4BADF,mBACE,6BADF,mBACE,0BADF,mBACE,6B5C4GF,sC4C7GA,sBACE,0BADF,sBACE,4BADF,sBACE,0BADF,sBACE,4BADF,sBACE,6BADF,sBACE,0BADF,sBACE,6B5C2HF,sC4C5HA,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6BAyBJ,mBACE,6BADF,oBACE,8BADF,eACE,2BADF,gBACE,4B5CmDF,qC4C/CE,0BACE,8B5CkDJ,2C4ChDE,0BACE,8B5CmDJ,4D4CjDE,+BACE,8B5CoDJ,sC4ClDE,yBACE,8B5CqDJ,sC4CnDE,2BACE,8B5CuDF,6D4CrDA,gCACE,8B5C8DF,sC4C5DA,8BACE,8B5CgEF,6D4C9DA,mCACE,8B5CuEF,sC4CrEA,0BACE,8B5CsBJ,qC4C/CE,2BACE,+B5CkDJ,2C4ChDE,2BACE,+B5CmDJ,4D4CjDE,gCACE,+B5CoDJ,sC4ClDE,0BACE,+B5CqDJ,sC4CnDE,4BACE,+B5CuDF,6D4CrDA,iCACE,+B5C8DF,sC4C5DA,+BACE,+B5CgEF,6D4C9DA,oCACE,+B5CuEF,sC4CrEA,2BACE,+B5CsBJ,qC4C/CE,sBACE,4B5CkDJ,2C4ChDE,sBACE,4B5CmDJ,4D4CjDE,2BACE,4B5CoDJ,sC4ClDE,qBACE,4B5CqDJ,sC4CnDE,uBACE,4B5CuDF,6D4CrDA,4BACE,4B5C8DF,sC4C5DA,0BACE,4B5CgEF,6D4C9DA,+BACE,4B5CuEF,sC4CrEA,sBACE,4B5CsBJ,qC4C/CE,uBACE,6B5CkDJ,2C4ChDE,uBACE,6B5CmDJ,4D4CjDE,4BACE,6B5CoDJ,sC4ClDE,sBACE,6B5CqDJ,sC4CnDE,wBACE,6B5CuDF,6D4CrDA,6BACE,6B5C8DF,sC4C5DA,2BACE,6B5CgEF,6D4C9DA,gCACE,6B5CuEF,sC4CrEA,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,yB7C2EF,qC6CzEE,iBACE,0B7C4EJ,2C6C1EE,iBACE,0B7C6EJ,4D6C3EE,sBACE,0B7C8EJ,sC6C5EE,gBACE,0B7C+EJ,sC6C7EE,kBACE,0B7CiFF,6D6C/EA,uBACE,0B7CwFF,sC6CtFA,qBACE,0B7C0FF,6D6CxFA,0BACE,0B7CiGF,sC6C/FA,iBACE,0BA5BJ,SACE,wB7C2EF,qC6CzEE,gBACE,yB7C4EJ,2C6C1EE,gBACE,yB7C6EJ,4D6C3EE,qBACE,yB7C8EJ,sC6C5EE,eACE,yB7C+EJ,sC6C7EE,iBACE,yB7CiFF,6D6C/EA,sBACE,yB7CwFF,sC6CtFA,oBACE,yB7C0FF,6D6CxFA,yBACE,yB7CiGF,sC6C/FA,gBACE,yBA5BJ,WACE,0B7C2EF,qC6CzEE,kBACE,2B7C4EJ,2C6C1EE,kBACE,2B7C6EJ,4D6C3EE,uBACE,2B7C8EJ,sC6C5EE,iBACE,2B7C+EJ,sC6C7EE,mBACE,2B7CiFF,6D6C/EA,wBACE,2B7CwFF,sC6CtFA,sBACE,2B7C0FF,6D6CxFA,2BACE,2B7CiGF,sC6C/FA,kBACE,2BA5BJ,iBACE,gC7C2EF,qC6CzEE,wBACE,iC7C4EJ,2C6C1EE,wBACE,iC7C6EJ,4D6C3EE,6BACE,iC7C8EJ,sC6C5EE,uBACE,iC7C+EJ,sC6C7EE,yBACE,iC7CiFF,6D6C/EA,8BACE,iC7CwFF,sC6CtFA,4BACE,iC7C0FF,6D6CxFA,iCACE,iC7CiGF,sC6C/FA,wBACE,iCA5BJ,gBACE,+B7C2EF,qC6CzEE,uBACE,gC7C4EJ,2C6C1EE,uBACE,gC7C6EJ,4D6C3EE,4BACE,gC7C8EJ,sC6C5EE,sBACE,gC7C+EJ,sC6C7EE,wBACE,gC7CiFF,6D6C/EA,6BACE,gC7CwFF,sC6CtFA,2BACE,gC7C0FF,6D6CxFA,gCACE,gC7CiGF,sC6C/FA,uBACE,gCAEN,WACE,wBAEF,YACE,uBACA,iCACA,wBACA,2BACA,qBACA,6BACA,8BACA,uB7CmCA,qC6ChCA,kBACE,yB7CmCF,2C6ChCA,kBACE,yB7CmCF,4D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ACE,aACA,uCACE,sB9C1CN,2C8CmCF,cASI,aACA,uBACA,uC9CaA,a8CZ0B,QAI9B,sBAEE,YACA,cAEF,WACE,YACA,cACA,QAhJkB,YCIpB,SACE,QALgB,Y/CiGhB,sC+CxFE,mBACE,QATmB,YAUrB,kBACE,QAVkB,cCExB,QACE,iB/CSa,Q+CRb,QAJe,iBCMjB,UACE,UACA,SACA,WALiB,QAMjB,eAGF,EACE,qBACA,cAKF,IACE,aACA,mBACA,8BACA,mBACA,WAtBc,QAuBd,WACA,iBAGF,gBACE,YASF,qBACE,OACA,YACA,SACA,kBAGF,WACE,0BACA,iBAGF,0BACE,YACA,gBACA,UACA,SAGF,6BACE,WACA,gBACA,YACA,UAGF,qCACE,mBAGF,+BACE,cAGF,mBACE,cACA,YACA,YACA,iBACA,SAGF,qBACE,mBAGF,yDACE,gBAOF,mBACI,iBAHW,QAIX,WAEA,yBACE,iBAhGU,QAiGV,WAMN,OACE,eACA,WACA,WACA,eACA,SACA,gBACA,kBAGF,SACE,iBACA,WACA,WACA,eACA,SACA,gBACA","file":"bds.css"} \ No newline at end of file +{"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","../../../../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","../../src/sass/bds.scss"],"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,MH1Da,QG2Db,UAzDe,IA0Df,YH1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,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,UrBVK,OqBWP,gBACE,UrBdK,QqBgBH,+BACE,eACN,eACE,UrBpBK,OqBsBH,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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,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,iBpCkBJ,qCoChBE,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,kBpCnCN,2CoCqCE,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,kBpC1GN,sCoC4GE,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,kBpC/JN,sCoCiKE,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,kBpCzMJ,sCoC2MA,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,kBpCnPJ,sCoCqPA,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,mBpCnXF,2CoCsXE,0BACE,cpC3WJ,sCoC8WE,oBACE,cAGJ,qBACE,qBACA,wCACA,yCACA,6BACE,8BACA,+BAEA,0BACE,kBpC3YN,qCoC6YM,iCACE,mBpC1YR,2CoC4YM,iCACE,mBpCzYR,4DoC2YM,sCACE,mBpCxYR,sCoC0YM,gCACE,mBpCvYR,sCoCyYM,kCACE,mBpCrYN,6DoCuYI,uCACE,mBpC9XN,sCoCgYI,qCACE,mBpC5XN,6DoC8XI,0CACE,mBpCrXN,sCoCuXI,iCACE,mBA5BJ,0BACE,qBpC3YN,qCoC6YM,iCACE,sBpC1YR,2CoC4YM,iCACE,sBpCzYR,4DoC2YM,sCACE,sBpCxYR,sCoC0YM,gCACE,sBpCvYR,sCoCyYM,kCACE,sBpCrYN,6DoCuYI,uCACE,sBpC9XN,sCoCgYI,qCACE,sBpC5XN,6DoC8XI,0CACE,sBpCrXN,sCoCuXI,iCACE,sBA5BJ,0BACE,oBpC3YN,qCoC6YM,iCACE,qBpC1YR,2CoC4YM,iCACE,qBpCzYR,4DoC2YM,sCACE,qBpCxYR,sCoC0YM,gCACE,qBpCvYR,sCoCyYM,kCACE,qBpCrYN,6DoCuYI,uCACE,qBpC9XN,sCoCgYI,qCACE,qBpC5XN,6DoC8XI,0CACE,qBpCrXN,sCoCuXI,iCACE,qBA5BJ,0BACE,qBpC3YN,qCoC6YM,iCACE,sBpC1YR,2CoC4YM,iCACE,sBpCzYR,4DoC2YM,sCACE,sBpCxYR,sCoC0YM,gCACE,sBpCvYR,sCoCyYM,kCACE,sBpCrYN,6DoCuYI,uCACE,sBpC9XN,sCoCgYI,qCACE,sBpC5XN,6DoC8XI,0CACE,sBpCrXN,sCoCuXI,iCACE,sBA5BJ,0BACE,kBpC3YN,qCoC6YM,iCACE,mBpC1YR,2CoC4YM,iCACE,mBpCzYR,4DoC2YM,sCACE,mBpCxYR,sCoC0YM,gCACE,mBpCvYR,sCoCyYM,kCACE,mBpCrYN,6DoCuYI,uCACE,mBpC9XN,sCoCgYI,qCACE,mBpC5XN,6DoC8XI,0CACE,mBpCrXN,sCoCuXI,iCACE,mBA5BJ,0BACE,qBpC3YN,qCoC6YM,iCACE,sBpC1YR,2CoC4YM,iCACE,sBpCzYR,4DoC2YM,sCACE,sBpCxYR,sCoC0YM,gCACE,sBpCvYR,sCoCyYM,kCACE,sBpCrYN,6DoCuYI,uCACE,sBpC9XN,sCoCgYI,qCACE,sBpC5XN,6DoC8XI,0CACE,sBpCrXN,sCoCuXI,iCACE,sBA5BJ,0BACE,oBpC3YN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ACA,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,4B5C6EJ,qC4C9EE,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6B5CiFJ,2C4ClFE,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6B5CyFJ,sC4C1FE,iBACE,0BADF,iBACE,4BADF,iBACE,0BADF,iBACE,4BADF,iBACE,6BADF,iBACE,0BADF,iBACE,6B5C6FJ,sC4C9FE,mBACE,0BADF,mBACE,4BADF,mBACE,0BADF,mBACE,4BADF,mBACE,6BADF,mBACE,0BADF,mBACE,6B5C4GF,sC4C7GA,sBACE,0BADF,sBACE,4BADF,sBACE,0BADF,sBACE,4BADF,sBACE,6BADF,sBACE,0BADF,sBACE,6B5C2HF,sC4C5HA,kBACE,0BADF,kBACE,4BADF,kBACE,0BADF,kBACE,4BADF,kBACE,6BADF,kBACE,0BADF,kBACE,6BAyBJ,mBACE,6BADF,oBACE,8BADF,eACE,2BADF,gBACE,4B5CmDF,qC4C/CE,0BACE,8B5CkDJ,2C4ChDE,0BACE,8B5CmDJ,4D4CjDE,+BACE,8B5CoDJ,sC4ClDE,yBACE,8B5CqDJ,sC4CnDE,2BACE,8B5CuDF,6D4CrDA,gCACE,8B5C8DF,sC4C5DA,8BACE,8B5CgEF,6D4C9DA,mCACE,8B5CuEF,sC4CrEA,0BACE,8B5CsBJ,qC4C/CE,2BACE,+B5CkDJ,2C4ChDE,2BACE,+B5CmDJ,4D4CjDE,gCACE,+B5CoDJ,sC4ClDE,0BACE,+B5CqDJ,sC4CnDE,4BACE,+B5CuDF,6D4CrDA,iCACE,+B5C8DF,sC4C5DA,+BACE,+B5CgEF,6D4C9DA,oCACE,+B5CuEF,sC4CrEA,2BACE,+B5CsBJ,qC4C/CE,sBACE,4B5CkDJ,2C4ChDE,sBACE,4B5CmDJ,4D4CjDE,2BACE,4B5CoDJ,sC4ClDE,qBACE,4B5CqDJ,sC4CnDE,uBACE,4B5CuDF,6D4CrDA,4BACE,4B5C8DF,sC4C5DA,0BACE,4B5CgEF,6D4C9DA,+BACE,4B5CuEF,sC4CrEA,sBACE,4B5CsBJ,qC4C/CE,uBACE,6B5CkDJ,2C4ChDE,uBACE,6B5CmDJ,4D4CjDE,4BACE,6B5CoDJ,sC4ClDE,sBACE,6B5CqDJ,sC4CnDE,wBACE,6B5CuDF,6D4CrDA,6BACE,6B5C8DF,sC4C5DA,2BACE,6B5CgEF,6D4C9DA,gCACE,6B5CuEF,sC4CrEA,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,yB7C2EF,qC6CzEE,iBACE,0B7C4EJ,2C6C1EE,iBACE,0B7C6EJ,4D6C3EE,sBACE,0B7C8EJ,sC6C5EE,gBACE,0B7C+EJ,sC6C7EE,kBACE,0B7CiFF,6D6C/EA,uBACE,0B7CwFF,sC6CtFA,qBACE,0B7C0FF,6D6CxFA,0BACE,0B7CiGF,sC6C/FA,iBACE,0BA5BJ,SACE,wB7C2EF,qC6CzEE,gBACE,yB7C4EJ,2C6C1EE,gBACE,yB7C6EJ,4D6C3EE,qBACE,yB7C8EJ,sC6C5EE,eACE,yB7C+EJ,sC6C7EE,iBACE,yB7CiFF,6D6C/EA,sBACE,yB7CwFF,sC6CtFA,oBACE,yB7C0FF,6D6CxFA,yBACE,yB7CiGF,sC6C/FA,gBACE,yBA5BJ,WACE,0B7C2EF,qC6CzEE,kBACE,2B7C4EJ,2C6C1EE,kBACE,2B7C6EJ,4D6C3EE,uBACE,2B7C8EJ,sC6C5EE,iBACE,2B7C+EJ,sC6C7EE,mBACE,2B7CiFF,6D6C/EA,wBACE,2B7CwFF,sC6CtFA,sBACE,2B7C0FF,6D6CxFA,2BACE,2B7CiGF,sC6C/FA,kBACE,2BA5BJ,iBACE,gC7C2EF,qC6CzEE,wBACE,iC7C4EJ,2C6C1EE,wBACE,iC7C6EJ,4D6C3EE,6BACE,iC7C8EJ,sC6C5EE,uBACE,iC7C+EJ,sC6C7EE,yBACE,iC7CiFF,6D6C/EA,8BACE,iC7CwFF,sC6CtFA,4BACE,iC7C0FF,6D6CxFA,iCACE,iC7CiGF,sC6C/FA,wBACE,iCA5BJ,gBACE,+B7C2EF,qC6CzEE,uBACE,gC7C4EJ,2C6C1EE,uBACE,gC7C6EJ,4D6C3EE,4BACE,gC7C8EJ,sC6C5EE,sBACE,gC7C+EJ,sC6C7EE,wBACE,gC7CiFF,6D6C/EA,6BACE,gC7CwFF,sC6CtFA,2BACE,gC7C0FF,6D6CxFA,gCACE,gC7CiGF,sC6C/FA,uBACE,gCAEN,WACE,wBAEF,YACE,uBACA,iCACA,wBACA,2BACA,qBACA,6BACA,8BACA,uB7CmCA,qC6ChCA,kBACE,yB7CmCF,2C6ChCA,kBACE,yB7CmCF,4D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","file":"bds.css"} \ No newline at end of file diff --git a/bds/static/bds/images/logo_square.svg b/bds/static/bds/images/logo_square.svg new file mode 100644 index 00000000..25c1aefd --- /dev/null +++ b/bds/static/bds/images/logo_square.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/bds/static/src/sass/bds.scss b/bds/static/src/sass/bds.scss index 463f8bd6..22784e77 100644 --- a/bds/static/src/sass/bds.scss +++ b/bds/static/src/sass/bds.scss @@ -7,8 +7,6 @@ $primary_color: #3e2263; $background_color: #ddcecc; html, body { - padding: 0; - margin: 0; background: $background_color; font-size: 18px; } @@ -20,31 +18,17 @@ a { /* header */ -nav { - display: flex; - flex-flow: row wrap; - justify-content: space-between; - align-items: center; - background: $primary_color; - height: 3em; - padding: 0.4em 1em; -} +#search-bar { + background-color: $primary_color; + padding: 0 1em; -nav a, nav a img { - height: 100%; -} + & :first-child { + justify-content: left; + } -// input[type="text"], input[type="email"] { -// font-size: 18px; -// border: 0; -// padding: 5px 5px; -// } - -#search_autocomplete { - flex: 1; - width: 480px; - margin: 0; - padding: 10px 10px; + & :last-child { + justify-content: right; + } } .highlight { diff --git a/bds/templates/bds/home.html b/bds/templates/bds/home.html index 1ae76227..1d11c725 100644 --- a/bds/templates/bds/home.html +++ b/bds/templates/bds/home.html @@ -1,4 +1,4 @@ -{% extends "bds/base.html" %} +{% extends "bds/base_layout.html" %} {% block content %}
          diff --git a/bds/templates/bds/nav.html b/bds/templates/bds/nav.html index e1118caa..b85c5fa8 100644 --- a/bds/templates/bds/nav.html +++ b/bds/templates/bds/nav.html @@ -1,13 +1,18 @@ {% load i18n %} {% load static %} -
        + {% if perms.kfet.delete_inventory %} +
        +
        + +
        + {% csrf_token %} +
        + {% endif %} +
      @@ -64,4 +75,27 @@ + + {% endblock %} diff --git a/kfet/urls.py b/kfet/urls.py index 2548e77e..7a9498ed 100644 --- a/kfet/urls.py +++ b/kfet/urls.py @@ -270,6 +270,11 @@ urlpatterns = [ teamkfet_required(views.InventoryRead.as_view()), name="kfet.inventory.read", ), + path( + "inventaires//delete", + views.InventoryDelete.as_view(), + name="kfet.inventory.delete", + ), # ----- # Order urls # ----- From 521be6db85da2646f2dde598fddd9eb8b833173f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 11 Sep 2020 15:22:07 +0200 Subject: [PATCH 308/573] Tests --- kfet/tests/test_views.py | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 47382aa1..ecd7131e 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -4579,6 +4579,87 @@ 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/" From b9699637aa77b8ffe2eab38656de4044dee4b996 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 21 Oct 2020 15:52:50 +0200 Subject: [PATCH 309/573] Message de confirmation plus clair --- kfet/templates/kfet/inventory_read.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/kfet/templates/kfet/inventory_read.html b/kfet/templates/kfet/inventory_read.html index 143449f7..9a9275f8 100644 --- a/kfet/templates/kfet/inventory_read.html +++ b/kfet/templates/kfet/inventory_read.html @@ -84,6 +84,8 @@ $( function() { content: `
      Cette opération est irréversible ! +
      + N.B. : seuls les articles dont c'est le dernier inventaire en date seront affectés.
      `, backgroundDismiss: true, From a7cbd2d45165902d304f4e2c3b4f1506cc8674bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 4 Dec 2020 17:02:36 +0100 Subject: [PATCH 310/573] CHANGELOG --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b35d8e73..8a87eccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,12 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre Uniquement un modèle simple de clubs avec des respos. Aucune gestion des adhérents ni des cotisations. -## Version ??? - le futur +## Version ??? - dans un futur proche -... +### K-Fêt + +- On peut supprimer un inventaire. Seuls les articles dont c'est le dernier + inventaire sont affectés. ## Version 0.8 - 03/12/2020 From badee498a3209a9d5b9354066bf3df5313fa9556 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 26 Oct 2020 13:34:30 +0100 Subject: [PATCH 311/573] Use EmailField for email field --- petitscours/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/petitscours/models.py b/petitscours/models.py index b95e344c..8e5d4884 100644 --- a/petitscours/models.py +++ b/petitscours/models.py @@ -62,7 +62,7 @@ class PetitCoursAbility(models.Model): class PetitCoursDemande(models.Model): name = models.CharField(_("Nom/prénom"), max_length=200) - email = models.CharField(_("Adresse email"), max_length=300) + email = models.EmailField(_("Adresse email"), max_length=300) phone = models.CharField(_("Téléphone (facultatif)"), max_length=20, blank=True) quand = models.CharField( _("Quand ?"), From e9e0c79b4028155de8e0e1ba7a16a7f1cb1db718 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 26 Oct 2020 13:35:23 +0100 Subject: [PATCH 312/573] Migration --- .../migrations/0018_petitscours_email.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 gestioncof/migrations/0018_petitscours_email.py diff --git a/gestioncof/migrations/0018_petitscours_email.py b/gestioncof/migrations/0018_petitscours_email.py new file mode 100644 index 00000000..3fc803b3 --- /dev/null +++ b/gestioncof/migrations/0018_petitscours_email.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.12 on 2020-10-26 12:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("gestioncof", "0017_petitscours_uniqueness"), + ] + + operations = [ + migrations.AlterField( + model_name="petitcoursdemande", + name="email", + field=models.EmailField(max_length=300, verbose_name="Adresse email"), + ), + ] From ad73cc987d22465b333f58fa84669896595c2dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 4 Dec 2020 17:16:35 +0100 Subject: [PATCH 313/573] CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a87eccc..d3ca361f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ adhérents ni des cotisations. ## Version ??? - dans un futur proche +### COF + +- On s'assure que les emails dans les demandes de petits cours sont valides. + ### K-Fêt - On peut supprimer un inventaire. Seuls les articles dont c'est le dernier From 411d7e7dce235bc6f0573c7facf4ca6496890420 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 29 Oct 2020 11:13:39 +0100 Subject: [PATCH 314/573] =?UTF-8?q?On=20peut=20revendre=20une=20place=20qu?= =?UTF-8?q?'on=20a=20pay=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bda/forms.py | 2 +- bda/templates/bda/revente/notpaid.html | 6 ------ bda/views.py | 6 ------ 3 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 bda/templates/bda/revente/notpaid.html diff --git a/bda/forms.py b/bda/forms.py index bb79932e..f0cea1bd 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -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() + spectacle__date__gte=timezone.now(), paid=True ) .exclude(revente__seller=participant) .select_related("spectacle", "spectacle__location", "participant__user"), diff --git a/bda/templates/bda/revente/notpaid.html b/bda/templates/bda/revente/notpaid.html deleted file mode 100644 index 0dd4e4df..00000000 --- a/bda/templates/bda/revente/notpaid.html +++ /dev/null @@ -1,6 +0,0 @@ -{% extends "base_title.html" %} - -{% block realcontent %} -

      Nope

      -

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

      -{% endblock %} diff --git a/bda/views.py b/bda/views.py index c17b723d..a6fb1804 100644 --- a/bda/views.py +++ b/bda/views.py @@ -385,12 +385,6 @@ 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") From 8b73460165aad092f352efd3cada6b88233ee3c1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 29 Oct 2020 11:14:06 +0100 Subject: [PATCH 315/573] Make flake8 happy --- bda/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bda/forms.py b/bda/forms.py index f0cea1bd..d1d0f74f 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 Attribution, Spectacle, SpectacleRevente +from bda.models import SpectacleRevente class InscriptionInlineFormSet(BaseInlineFormSet): From f2c1ff2abd078d2760ba5326f4c8d6ebfbc54cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 4 Dec 2020 17:53:56 +0100 Subject: [PATCH 316/573] Update CHANGELOG --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ca361f..84614959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,10 @@ adhérents ni des cotisations. ## Version ??? - dans un futur proche -### COF +### COF / 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 les emails dans les demandes de petits cours sont valides. ### K-Fêt From 49fde85187a374b455bf33fcd22bea0b0512e24a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 4 Dec 2020 19:33:17 +0100 Subject: [PATCH 317/573] Admin: on utilise la recherche builtin de Django --- bda/admin.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/bda/admin.py b/bda/admin.py index 55b0475f..edba2c61 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -33,20 +33,6 @@ 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 @@ -238,7 +224,7 @@ class AttributionAdmin(ReadOnlyMixin, admin.ModelAdmin): class ChoixSpectacleAdmin(admin.ModelAdmin): - form = ChoixSpectacleAdminForm + autocomplete_fields = ["participant", "spectacle"] def tirage(self, obj): return obj.participant.tirage From 783fe1de3278dc3b19b06b0b64f5897190130ab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 7 Dec 2020 19:58:00 +0100 Subject: [PATCH 318/573] =?UTF-8?q?Liste=20des=20paquets=20dans=20un=20fic?= =?UTF-8?q?hier=20s=C3=A9par=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- provisioning/bootstrap.sh | 18 +++++++++++------- provisioning/packages.list | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 provisioning/packages.list diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 9b2bf9f2..9659f89d 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -17,16 +17,20 @@ readonly REDIS_PASSWD="dummy" readonly DJANGO_SETTINGS_MODULE="cof.settings.dev" -# Installation de paquets utiles -apt-get update +# --- +# 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 - # -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confold" \ - # upgrade -apt-get install -y python3-pip python3-dev python3-venv libpq-dev postgresql \ - postgresql-contrib libjpeg-dev nginx git redis-server \ - libldap2-dev libsasl2-dev slapd ldap-utils +get_packages_list | xargs apt-get install -y # Postgresql pg_user_exists () { diff --git a/provisioning/packages.list b/provisioning/packages.list new file mode 100644 index 00000000..34714442 --- /dev/null +++ b/provisioning/packages.list @@ -0,0 +1,25 @@ +# 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 From 0ce1e6258666c0d960e5b678f2e9765d1b79c92b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 7 Dec 2020 20:04:19 +0100 Subject: [PATCH 319/573] =?UTF-8?q?Fichier=20bootstrap.sh=20mieux=20commen?= =?UTF-8?q?t=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- provisioning/bootstrap.sh | 44 +++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index 9659f89d..d6b8f914 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -32,6 +32,11 @@ apt-get update apt-get -y upgrade get_packages_list | xargs apt-get install -y + +# --- +# Configuration de la base de données +# --- + # Postgresql pg_user_exists () { sudo -u postgres psql postgres -tAc \ @@ -51,20 +56,24 @@ 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 -# Contenu statique + +# --- +# Préparation de Django +# --- + +# Dossiers pour le contenu statique mkdir -p /srv/gestiocof/media mkdir -p /srv/gestiocof/static chown -R vagrant:www-data /srv/gestiocof -# Nginx -ln -s -f /vagrant/provisioning/nginx/gestiocof.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 @@ -82,7 +91,11 @@ sudo -H -u vagrant \ --noinput \ --settings "$DJANGO_SETTINGS_MODULE" -# Quelques units systemd: + +# --- +# Units systemd +# --- + # - Daphne fait tourner le serveur asgi # - worker = https://channels.readthedocs.io/en/stable/topics/worker.html # - Mails de rappels du BdA @@ -98,8 +111,12 @@ systemctl enable --now worker.service systemctl enable rappels.timer systemctl enable reventes.timer -# Configure le bash de l'utilisateur 'vagrant' pour utiliser le bon fichier de -# settings et et bon virtualenv. + +# --- +# 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 @@ -113,3 +130,12 @@ export DJANGO_SETTINGS_MODULE='$DJANGO_SETTINGS_MODULE' # On va dans /vagrant où se trouve le code de gestioCOF cd /vagrant EOF + + +# --- +# Configuration d'nginx +# --- + +ln -s -f /vagrant/provisioning/nginx/gestiocof.conf /etc/nginx/sites-enabled/gestiocof.conf +rm -f /etc/nginx/sites-enabled/default +systemctl reload nginx From 30ce8d13afd5a8e0049dfe1c7cab25d9ec9f1f8f Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Wed, 9 Dec 2020 21:16:49 +0100 Subject: [PATCH 320/573] Ajout date de fermeture de tirage BDA sur la page d'acceuil --- gestioncof/templates/gestioncof/home.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gestioncof/templates/gestioncof/home.html b/gestioncof/templates/gestioncof/home.html index e534f687..85dfb0f5 100644 --- a/gestioncof/templates/gestioncof/home.html +++ b/gestioncof/templates/gestioncof/home.html @@ -27,11 +27,12 @@
      {% for tirage in open_tirages %}
        -

        {{ tirage.title }}

        {% if tirage.fermeture > now %} +

        {{ tirage.title }} - Fermeture le {{ tirage.fermeture }}

      • Inscription
      • État des demandes
      • {% else %} +

        {{ tirage.title }}

      • Mes places
      • Gérer les places que je revends
      • Voir les reventes en cours
      • From 73c068055b8a7adbcfa28a6bcb7532b0c287ec14 Mon Sep 17 00:00:00 2001 From: Alseidon Date: Wed, 9 Dec 2020 21:57:40 +0100 Subject: [PATCH 321/573] =?UTF-8?q?Remise=20=C3=A0=20z=C3=A9ro=20basique?= =?UTF-8?q?=20comptes=20COF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/templates/gestioncof/reset_comptes.html | 5 +++++ gestioncof/urls.py | 3 +++ gestioncof/views.py | 13 +++++++++++++ 3 files changed, 21 insertions(+) create mode 100644 gestioncof/templates/gestioncof/reset_comptes.html diff --git a/gestioncof/templates/gestioncof/reset_comptes.html b/gestioncof/templates/gestioncof/reset_comptes.html new file mode 100644 index 00000000..78d01ae8 --- /dev/null +++ b/gestioncof/templates/gestioncof/reset_comptes.html @@ -0,0 +1,5 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

        Order 66 done

        +{% endblock %} \ No newline at end of file diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 14fb101f..a35df9ed 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -82,6 +82,9 @@ urlpatterns = [ # Misc # ----- path("", views.HomeView.as_view(), name="home"), + + path("reset_comptes/", views.ResetComptes.as_view(), name="reset_comptes"), + path( "user/autocomplete", views.UserAutocompleteView.as_view(), diff --git a/gestioncof/views.py b/gestioncof/views.py index d4b6a5be..7a19875c 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -78,6 +78,19 @@ class HomeView(LoginRequiredMixin, TemplateView): context["now"] = timezone.now() return context +class ResetComptes(LoginRequiredMixin, TemplateView): + template_name = "gestioncof/reset_comptes.html" + + def get(self, request): + for profile in CofProfile.objects.all(): + profile.is_cof = False + profile.mailing_cof = False + profile.mailing_bda = False + profile.mailing_bda_revent = False + profile.mailing_unernestaparis = False + profile.save() + return super().get(request) + def login(request): if request.user.is_authenticated: From 9d2c13e67c9bf5708b0a9afe95804a07e2c28a7d Mon Sep 17 00:00:00 2001 From: Quentin VERMANDE Date: Wed, 9 Dec 2020 22:03:54 +0100 Subject: [PATCH 322/573] kfetTriArticles --- kfet/forms.py | 2 + kfet/templates/kfet/inventory_create.html | 6 +++ kfet/templates/kfet/order_create.html | 54 +++++++++++++---------- kfet/views.py | 6 ++- 4 files changed, 43 insertions(+), 25 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index a7637551..bc98a8ce 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -558,6 +558,7 @@ 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"] # ----- @@ -584,6 +585,7 @@ class OrderArticleForm(forms.Form): self.v_et = kwargs["initial"]["v_et"] self.v_prev = kwargs["initial"]["v_prev"] self.c_rec = kwargs["initial"]["c_rec"] + self.is_sold = kwargs["initial"]["is_sold"] class OrderArticleToInventoryForm(forms.Form): diff --git a/kfet/templates/kfet/inventory_create.html b/kfet/templates/kfet/inventory_create.html index c3084e71..45bd48ed 100644 --- a/kfet/templates/kfet/inventory_create.html +++ b/kfet/templates/kfet/inventory_create.html @@ -26,6 +26,12 @@ {% for form in formset %} + {% ifchanged form.is_sold %} + + {% if form.is_sold %} Vendu {% else %} Non vendu {% endif %} + + + {% endifchanged %} {% ifchanged form.category %} {{ form.category_name }} diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index 7cb4d1cb..20ae7b69 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -58,31 +58,39 @@ {% endfor %} - {% regroup formset by category_name as category_list %} - {% for category in category_list %} - - - {{ category.grouper }} + {% regroup formset by is_sold as is_sold_list %} + {% for condition in is_sold_list %} + + + {% if condition.grouper %} Vendu {% else %} Non vendu {% endif %} - - {% for form in category.list %} - - {{ form.article }} - {{ form.name }} - {% for v_chunk in form.v_all %} - {{ v_chunk }} - {% endfor %} - {{ 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" }} - - {% endfor %} - + {% regroup condition.list by category_name as category_list %} + {% for category in category_list %} + + + {{ category.grouper }} + + + + {% for form in category.list %} + + {{ form.article }} + {{ form.name }} + {% for v_chunk in form.v_all %} + {{ v_chunk }} + {% endfor %} + {{ 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" }} + + {% endfor %} + + {% endfor %} {% endfor %}
      diff --git a/kfet/views.py b/kfet/views.py index d42c6338..136d7bd0 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1835,12 +1835,13 @@ class InventoryList(ListView): def inventory_create(request): articles = Article.objects.select_related("category").order_by( - "category__name", "name" + "-is_sold", "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, @@ -1960,7 +1961,7 @@ def order_create(request, pk): Article.objects.filter(suppliers=supplier.pk) .distinct() .select_related("category") - .order_by("category__name", "name") + .order_by("-is_sold", "category__name", "name") ) # Force hit to cache @@ -2017,6 +2018,7 @@ def order_create(request, pk): "v_et": round(v_et), "v_prev": round(v_prev), "c_rec": article.box_capacity and c_rec or round(c_rec_tot), + "is_sold": article.is_sold } ) From 319db686558017de9d0373f65b12c7ccd7ed09f2 Mon Sep 17 00:00:00 2001 From: Alseidon Date: Wed, 9 Dec 2020 22:11:21 +0100 Subject: [PATCH 323/573] Ra0 effective --- gestioncof/templates/gestioncof/reset_comptes.html | 6 +++++- gestioncof/views.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gestioncof/templates/gestioncof/reset_comptes.html b/gestioncof/templates/gestioncof/reset_comptes.html index 78d01ae8..370c83f1 100644 --- a/gestioncof/templates/gestioncof/reset_comptes.html +++ b/gestioncof/templates/gestioncof/reset_comptes.html @@ -1,5 +1,9 @@ {% extends "base_title.html" %} {% block realcontent %} -

      Order 66 done

      +

      Remise à zéro des membres COF

      +

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

      +
      + {% csrf_token %} +
      {% endblock %} \ No newline at end of file diff --git a/gestioncof/views.py b/gestioncof/views.py index 7a19875c..ca0833d8 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -81,7 +81,7 @@ class HomeView(LoginRequiredMixin, TemplateView): class ResetComptes(LoginRequiredMixin, TemplateView): template_name = "gestioncof/reset_comptes.html" - def get(self, request): + def post(self, request): for profile in CofProfile.objects.all(): profile.is_cof = False profile.mailing_cof = False From 035bbe68a5335609359d1663c52f738163370acd Mon Sep 17 00:00:00 2001 From: Quentin VERMANDE Date: Wed, 9 Dec 2020 22:22:12 +0100 Subject: [PATCH 324/573] make black happy --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index 136d7bd0..3a497111 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2018,7 +2018,7 @@ def order_create(request, pk): "v_et": round(v_et), "v_prev": round(v_prev), "c_rec": article.box_capacity and c_rec or round(c_rec_tot), - "is_sold": article.is_sold + "is_sold": article.is_sold, } ) From ba74779f95e3d4a5ce7e055cd28c5ba0c1ac69e8 Mon Sep 17 00:00:00 2001 From: Alseidon Date: Wed, 9 Dec 2020 22:40:32 +0100 Subject: [PATCH 325/573] =?UTF-8?q?Version=201.0=20remise=20=C3=A0=20z?= =?UTF-8?q?=C3=A9ro=20comptes=20COF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/templates/gestioncof/home.html | 1 + .../templates/gestioncof/reset_comptes.html | 4 ++++ gestioncof/views.py | 24 ++++++++++++------- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/gestioncof/templates/gestioncof/home.html b/gestioncof/templates/gestioncof/home.html index e534f687..2ed347ca 100644 --- a/gestioncof/templates/gestioncof/home.html +++ b/gestioncof/templates/gestioncof/home.html @@ -114,6 +114,7 @@ diff --git a/gestioncof/templates/gestioncof/reset_comptes.html b/gestioncof/templates/gestioncof/reset_comptes.html index 370c83f1..97b88998 100644 --- a/gestioncof/templates/gestioncof/reset_comptes.html +++ b/gestioncof/templates/gestioncof/reset_comptes.html @@ -2,8 +2,12 @@ {% block realcontent %}

      Remise à zéro des membres COF

      + {% if is_done%} + Order 66 done + {% else%}

      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/views.py b/gestioncof/views.py index ca0833d8..4094a183 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -78,18 +78,24 @@ class HomeView(LoginRequiredMixin, TemplateView): context["now"] = timezone.now() return context -class ResetComptes(LoginRequiredMixin, TemplateView): +class ResetComptes(BuroRequiredMixin, TemplateView): template_name = "gestioncof/reset_comptes.html" def post(self, request): - for profile in CofProfile.objects.all(): - profile.is_cof = False - profile.mailing_cof = False - profile.mailing_bda = False - profile.mailing_bda_revent = False - profile.mailing_unernestaparis = False - profile.save() - return super().get(request) + CofProfile.objects.update( + is_cof = False, + mailing_cof=False, + mailing_bda=False, + mailing_bda_revente=False, + mailing_unernestaparis=False) + context = self.get_context_data() + return render(request, self.template_name, context) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + if self.request.method == 'POST': + context['is_done'] = True + return context def login(request): From c100f2fc8dbe2c2cc48b56cb4086358df8385ca6 Mon Sep 17 00:00:00 2001 From: Alseidon Date: Wed, 9 Dec 2020 23:00:00 +0100 Subject: [PATCH 326/573] =?UTF-8?q?Version=201.1=20remise=20=C3=A0=20z?= =?UTF-8?q?=C3=A9ro=20comptes=20COF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/templates/gestioncof/reset_comptes.html | 7 ++++--- gestioncof/views.py | 11 ++++------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/gestioncof/templates/gestioncof/reset_comptes.html b/gestioncof/templates/gestioncof/reset_comptes.html index 97b88998..55d54376 100644 --- a/gestioncof/templates/gestioncof/reset_comptes.html +++ b/gestioncof/templates/gestioncof/reset_comptes.html @@ -3,11 +3,12 @@ {% block realcontent %}

      Remise à zéro des membres COF

      {% if is_done%} - Order 66 done +

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

      {% else%} -

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

      +
      ATTENTION : Cette action est irréversible.
      +

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

      - {% csrf_token %} + {% csrf_token %}
      {% endif %} {% endblock %} \ No newline at end of file diff --git a/gestioncof/views.py b/gestioncof/views.py index 4094a183..7e3c2cc4 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -82,21 +82,18 @@ class ResetComptes(BuroRequiredMixin, TemplateView): template_name = "gestioncof/reset_comptes.html" def post(self, request): + nb_adherents = CofProfile.objects.filter(is_cof=True).count() CofProfile.objects.update( is_cof = False, mailing_cof=False, mailing_bda=False, mailing_bda_revente=False, mailing_unernestaparis=False) - context = self.get_context_data() + context = super().get_context_data() + context['is_done'] = True + context['nb_adherents'] = nb_adherents return render(request, self.template_name, context) - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - if self.request.method == 'POST': - context['is_done'] = True - return context - def login(request): if request.user.is_authenticated: From 0bdbcf59fa066cdb9bfa077b26b5d4a7e5b9a6bf Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 10 Dec 2020 16:46:53 +0100 Subject: [PATCH 327/573] Add black to requirements-devel --- requirements-devel.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-devel.txt b/requirements-devel.txt index 7907bcd9..8dc49eb1 100644 --- a/requirements-devel.txt +++ b/requirements-devel.txt @@ -3,6 +3,6 @@ django-debug-toolbar ipython # Tools -# black # Uncomment when GC & most distros run with Python>=3.6 +black flake8 isort From 681507f21147711b550987b68b5ef76aba67c712 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 6 Jan 2021 21:31:47 +0100 Subject: [PATCH 328/573] Happy new year! --- kfet/migrations/0073_2021.py | 66 ++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 kfet/migrations/0073_2021.py diff --git a/kfet/migrations/0073_2021.py b/kfet/migrations/0073_2021.py new file mode 100644 index 00000000..4470b4fb --- /dev/null +++ b/kfet/migrations/0073_2021.py @@ -0,0 +1,66 @@ +# 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, + ), + ), + ] From 40391d88142abc1f1c164775b81f04f00dd89016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 6 Jan 2021 21:32:41 +0100 Subject: [PATCH 329/573] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 84614959..6b75b07a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,8 @@ adhérents ni des cotisations. ### 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. From 44b001bd3c95190b2fca6de2ba04a7d5424c61e7 Mon Sep 17 00:00:00 2001 From: Alseidon Date: Thu, 7 Jan 2021 09:19:56 +0100 Subject: [PATCH 330/573] Satisfy Lord Black --- gestioncof/urls.py | 2 -- gestioncof/views.py | 10 ++++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/gestioncof/urls.py b/gestioncof/urls.py index a35df9ed..d0ba75c7 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -82,9 +82,7 @@ urlpatterns = [ # Misc # ----- path("", views.HomeView.as_view(), name="home"), - path("reset_comptes/", views.ResetComptes.as_view(), name="reset_comptes"), - path( "user/autocomplete", views.UserAutocompleteView.as_view(), diff --git a/gestioncof/views.py b/gestioncof/views.py index 7e3c2cc4..fbe74ec7 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -78,20 +78,22 @@ class HomeView(LoginRequiredMixin, TemplateView): context["now"] = timezone.now() return context + class ResetComptes(BuroRequiredMixin, TemplateView): template_name = "gestioncof/reset_comptes.html" def post(self, request): nb_adherents = CofProfile.objects.filter(is_cof=True).count() CofProfile.objects.update( - is_cof = False, + is_cof=False, mailing_cof=False, mailing_bda=False, mailing_bda_revente=False, - mailing_unernestaparis=False) + mailing_unernestaparis=False, + ) context = super().get_context_data() - context['is_done'] = True - context['nb_adherents'] = nb_adherents + context["is_done"] = True + context["nb_adherents"] = nb_adherents return render(request, self.template_name, context) From 830aba984e145ba30e78365e141f9c7e0b721f1c Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Thu, 21 Jan 2021 20:32:36 +0100 Subject: [PATCH 331/573] Added bds/members to export members list as CSV --- bds/urls.py | 1 + bds/views.py | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/bds/urls.py b/bds/urls.py index f0877e3f..93c60b0d 100644 --- a/bds/urls.py +++ b/bds/urls.py @@ -14,4 +14,5 @@ urlpatterns = [ name="user.create.fromclipper", ), path("user/delete/", views.UserDeleteView.as_view(), name="user.delete"), + path("members", views.export_members, name="export.members"), ] diff --git a/bds/views.py b/bds/views.py index 0318d1e6..540865b1 100644 --- a/bds/views.py +++ b/bds/views.py @@ -1,5 +1,9 @@ +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.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 _ @@ -128,3 +132,21 @@ class UserDeleteView(StaffRequiredMixin, DeleteView): messages.success(request, self.success_message) return super().delete(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 From a2eed137176af9451d7be10a14b06c6cf5d79c9a Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Thu, 21 Jan 2021 20:38:15 +0100 Subject: [PATCH 332/573] Added download button to home template --- bds/templates/bds/home.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bds/templates/bds/home.html b/bds/templates/bds/home.html index 4a3e95f6..c7a5ef44 100644 --- a/bds/templates/bds/home.html +++ b/bds/templates/bds/home.html @@ -7,7 +7,7 @@
      -

      {{ member_count }}

      +

      {{ member_count }}

      adhérent·e·s
      @@ -34,6 +34,11 @@

      + Télécharger la liste des membres (CSV) + +
      +
      + Le site est encore en développement.
      Suivez notre avancement sur @@ -52,4 +57,4 @@ {% endblock layout %} - + From 79f0757e9f1349b93eb3d2b835dba50dfe30a287 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 21 Jan 2021 20:55:23 +0100 Subject: [PATCH 333/573] Fix kfet stats --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index 3a497111..e59720c0 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2507,7 +2507,7 @@ class AccountStatOperationList(UserAccountMixin, SingleResumeStat): ( "Tout le temps", MonthScale, - {"last": True, "begin": self.object.created_at}, + {"last": True, "begin": self.object.created_at.replace(tzinfo=None)}, False, ), ("1 an", MonthScale, {"last": True, "n_steps": 12}, False), From 4bc56d34e002a0c0c4dd4cf6380498b76f5ff582 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 21 Jan 2021 21:08:57 +0100 Subject: [PATCH 334/573] Fix tests --- kfet/tests/test_views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index ecd7131e..7d395e7e 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -654,7 +654,9 @@ class AccountStatOperationListViewTests(ViewTestCaseMixin, TestCase): "scale-name": ["month"], "scale-last": ["True"], "scale-begin": [ - self.accounts["user1"].created_at.isoformat(" ") + self.accounts["user1"] + .created_at.replace(tzinfo=None) + .isoformat(" ") ], }, }, From 9a78fca507777a21aa2b1ee8b3cc6cf5f566ee8b Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 29 Jan 2021 09:34:56 +0100 Subject: [PATCH 335/573] Switched to named url --- bds/templates/bds/home.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bds/templates/bds/home.html b/bds/templates/bds/home.html index c7a5ef44..9ccaa364 100644 --- a/bds/templates/bds/home.html +++ b/bds/templates/bds/home.html @@ -34,7 +34,7 @@

      - Télécharger la liste des membres (CSV) + Télécharger la liste des membres (CSV)

      From 880dc31353c342a1c122026fe9ca8a132d5e281d Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 29 Jan 2021 09:37:37 +0100 Subject: [PATCH 336/573] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b75b07a..170f92a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,10 @@ adhérents ni des cotisations. ses places pour pouvoir revendre. - On s'assure que les emails dans les demandes de petits cours sont valides. +### BDS + +- Le buro peut exporter la liste de membres avec email au format CSV depuis la page d'acceuil. + ### K-Fêt - On affiche les articles actuellement en vente en premier lors des inventaires From bf6d6d6430fb40addc6337ac2517f15f89cadba5 Mon Sep 17 00:00:00 2001 From: Alseidon Date: Wed, 27 Jan 2021 22:25:58 +0100 Subject: [PATCH 337/573] Added basic buro right handling while updating member --- bds/forms.py | 3 +++ bds/views.py | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/bds/forms.py b/bds/forms.py index 557f7c83..3869d1d7 100644 --- a/bds/forms.py +++ b/bds/forms.py @@ -1,6 +1,7 @@ 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 @@ -8,6 +9,8 @@ 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"] diff --git a/bds/views.py b/bds/views.py index 540865b1..4b279836 100644 --- a/bds/views.py +++ b/bds/views.py @@ -3,6 +3,7 @@ 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 @@ -40,6 +41,9 @@ class UserUpdateView(StaffRequiredMixin, MultipleFormView): "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) @@ -56,6 +60,11 @@ class UserUpdateView(StaffRequiredMixin, MultipleFormView): 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 !")) From 97628389214ef76df0cab10eecbfdb67c3b0ddfc Mon Sep 17 00:00:00 2001 From: Alseidon Date: Fri, 29 Jan 2021 23:53:26 +0100 Subject: [PATCH 338/573] Basic Buro right handling - minor corrections --- CHANGELOG.md | 5 ++++- bds/forms.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 170f92a5..d637e5de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,10 @@ adhérents ni des cotisations. ### BDS -- Le buro peut exporter la liste de membres avec email au format CSV depuis la page d'acceuil. +- 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 diff --git a/bds/forms.py b/bds/forms.py index 3869d1d7..4f626926 100644 --- a/bds/forms.py +++ b/bds/forms.py @@ -9,7 +9,7 @@ User = get_user_model() class UserForm(forms.ModelForm): - is_buro = forms.BooleanField(label=_("membre du Burô"), required=False) + is_buro = forms.BooleanField(label=_("Membre du Burô"), required=False) class Meta: model = User From 10746c0469ed384ac122084e4a610ab82df0dbed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Feb 2021 17:01:22 +0100 Subject: [PATCH 339/573] Version 0.9 --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d637e5de..d213bea2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,11 +23,13 @@ adhérents ni des cotisations. ## Version ??? - dans un futur proche +## Version 0.9 - 06/02/2020 + ### COF / 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 les emails dans les demandes de petits cours sont valides. +- On s'assure que l'email fourni lors d'une demande de petit cours est valide. ### BDS From 9a01d1e877f9d3871f480d685dd367359a30c966 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sat, 6 Feb 2021 17:17:47 +0100 Subject: [PATCH 340/573] CHANGELOG: add missing items in the v0.9 release --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d213bea2..b2573983 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ adhérents ni des cotisations. ### 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. From 288de95c4967ad14d1f0abc3d8046951537d8f41 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 6 Feb 2021 18:58:25 +0100 Subject: [PATCH 341/573] Checkout form is single-option now --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index e59720c0..c50fb33e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1476,7 +1476,7 @@ def history_json(request): opegroups = opegroups.filter(at__lt=end) transfergroups = transfergroups.filter(at__lt=end) if checkout: - opegroups = opegroups.filter(checkout__in=checkout) + opegroups = opegroups.filter(checkout=checkout) transfergroups = TransferGroup.objects.none() if transfers_only: opegroups = OperationGroup.objects.none() From 708138005893181bd727ff4774ddacf377f0d636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Feb 2021 16:29:21 +0100 Subject: [PATCH 342/573] =?UTF-8?q?Only=20redirect=20/=20=E2=86=92=20/gest?= =?UTF-8?q?ion=20in=20development?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cof/urls.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/cof/urls.py b/cof/urls.py index 1de437ed..0fa72c58 100644 --- a/cof/urls.py +++ b/cof/urls.py @@ -8,20 +8,22 @@ 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 = [ - # Redirection / → /gestion, only useful for developpers. - path("", RedirectView.as_view(url="gestion/")), # Website administration (independent from installed apps) path("admin/doc/", include("django.contrib.admindocs.urls")), path("admin/", admin.site.urls), ] -# App-specific urls +if not bds_is_alone: + # Redirection / → /gestion, only useful for developpers. + urlpatterns.append(path("", RedirectView.as_view(url="gestion/"))) -bds_is_alone = ( - "bds" in settings.INSTALLED_APPS and "gestioncof" not in settings.INSTALLED_APPS -) +# App-specific urls app_dict = { "bds": "" if bds_is_alone else "bds/", From 726b3f55a0f4ffc514a51db567db0c6654c96b16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Feb 2021 17:17:15 +0100 Subject: [PATCH 343/573] Rename the cof/ folder to gestioasso/ This is a much more sensible name since it contains configuration applicable to both GestioCOF and GestioBDS. The next logical step would be to rename the `gestioncof/` folder to `cof/`. --- README.md | 6 +++--- {cof => gestioasso}/__init__.py | 0 {cof => gestioasso}/apps.py | 0 {cof => gestioasso}/asgi.py | 2 +- {cof => gestioasso}/locale/__init__.py | 0 {cof => gestioasso}/locale/en/__init__.py | 0 {cof => gestioasso}/locale/en/formats.py | 0 {cof => gestioasso}/locale/fr/__init__.py | 0 {cof => gestioasso}/locale/fr/formats.py | 0 {cof => gestioasso}/routing.py | 0 {cof => gestioasso}/settings/.gitignore | 0 {cof => gestioasso}/settings/__init__.py | 0 {cof => gestioasso}/settings/bds_prod.py | 0 {cof => gestioasso}/settings/cof_prod.py | 2 +- {cof => gestioasso}/settings/common.py | 6 +++--- {cof => gestioasso}/settings/dev.py | 0 {cof => gestioasso}/settings/local.py | 2 +- {cof => gestioasso}/settings/secret_example.py | 0 {cof => gestioasso}/urls.py | 0 {cof => gestioasso}/wsgi.py | 2 +- manage.py | 2 +- provisioning/bootstrap.sh | 4 ++-- provisioning/systemd/daphne.service | 2 +- provisioning/systemd/rappels.service | 2 +- provisioning/systemd/reventes.service | 2 +- provisioning/systemd/worker.service | 2 +- 26 files changed, 17 insertions(+), 17 deletions(-) rename {cof => gestioasso}/__init__.py (100%) rename {cof => gestioasso}/apps.py (100%) rename {cof => gestioasso}/asgi.py (65%) rename {cof => gestioasso}/locale/__init__.py (100%) rename {cof => gestioasso}/locale/en/__init__.py (100%) rename {cof => gestioasso}/locale/en/formats.py (100%) rename {cof => gestioasso}/locale/fr/__init__.py (100%) rename {cof => gestioasso}/locale/fr/formats.py (100%) rename {cof => gestioasso}/routing.py (100%) rename {cof => gestioasso}/settings/.gitignore (100%) rename {cof => gestioasso}/settings/__init__.py (100%) rename {cof => gestioasso}/settings/bds_prod.py (100%) rename {cof => gestioasso}/settings/cof_prod.py (98%) rename {cof => gestioasso}/settings/common.py (96%) rename {cof => gestioasso}/settings/dev.py (100%) rename {cof => gestioasso}/settings/local.py (97%) rename {cof => gestioasso}/settings/secret_example.py (100%) rename {cof => gestioasso}/urls.py (100%) rename {cof => gestioasso}/wsgi.py (55%) diff --git a/README.md b/README.md index ffe680db..e6b5a3ee 100644 --- a/README.md +++ b/README.md @@ -38,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 `cof/settings/secret_example.py` vers -`cof/settings/secret.py`. Sous Linux ou Mac, préférez plutôt un lien symbolique +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 profiter de façon transparente des mises à jour du fichier: - ln -s secret_example.py cof/settings/secret.py + ln -s secret_example.py gestioasso/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/cof/__init__.py b/gestioasso/__init__.py similarity index 100% rename from cof/__init__.py rename to gestioasso/__init__.py diff --git a/cof/apps.py b/gestioasso/apps.py similarity index 100% rename from cof/apps.py rename to gestioasso/apps.py diff --git a/cof/asgi.py b/gestioasso/asgi.py similarity index 65% rename from cof/asgi.py rename to gestioasso/asgi.py index ab4ce291..773acaa0 100644 --- a/cof/asgi.py +++ b/gestioasso/asgi.py @@ -3,6 +3,6 @@ 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") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings") channel_layer = get_channel_layer() diff --git a/cof/locale/__init__.py b/gestioasso/locale/__init__.py similarity index 100% rename from cof/locale/__init__.py rename to gestioasso/locale/__init__.py diff --git a/cof/locale/en/__init__.py b/gestioasso/locale/en/__init__.py similarity index 100% rename from cof/locale/en/__init__.py rename to gestioasso/locale/en/__init__.py diff --git a/cof/locale/en/formats.py b/gestioasso/locale/en/formats.py similarity index 100% rename from cof/locale/en/formats.py rename to gestioasso/locale/en/formats.py diff --git a/cof/locale/fr/__init__.py b/gestioasso/locale/fr/__init__.py similarity index 100% rename from cof/locale/fr/__init__.py rename to gestioasso/locale/fr/__init__.py diff --git a/cof/locale/fr/formats.py b/gestioasso/locale/fr/formats.py similarity index 100% rename from cof/locale/fr/formats.py rename to gestioasso/locale/fr/formats.py diff --git a/cof/routing.py b/gestioasso/routing.py similarity index 100% rename from cof/routing.py rename to gestioasso/routing.py diff --git a/cof/settings/.gitignore b/gestioasso/settings/.gitignore similarity index 100% rename from cof/settings/.gitignore rename to gestioasso/settings/.gitignore diff --git a/cof/settings/__init__.py b/gestioasso/settings/__init__.py similarity index 100% rename from cof/settings/__init__.py rename to gestioasso/settings/__init__.py diff --git a/cof/settings/bds_prod.py b/gestioasso/settings/bds_prod.py similarity index 100% rename from cof/settings/bds_prod.py rename to gestioasso/settings/bds_prod.py diff --git a/cof/settings/cof_prod.py b/gestioasso/settings/cof_prod.py similarity index 98% rename from cof/settings/cof_prod.py rename to gestioasso/settings/cof_prod.py index 47fa3954..ec0694fe 100644 --- a/cof/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -149,7 +149,7 @@ CHANNEL_LAYERS = { ) ] }, - "ROUTING": "cof.routing.routing", + "ROUTING": "gestioasso.routing.routing", } } diff --git a/cof/settings/common.py b/gestioasso/settings/common.py similarity index 96% rename from cof/settings/common.py rename to gestioasso/settings/common.py index 4636ace3..88ae636a 100644 --- a/cof/settings/common.py +++ b/gestioasso/settings/common.py @@ -65,7 +65,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.admin", "django.contrib.admindocs", - "cof.apps.IgnoreSrcStaticFilesConfig", + "gestioasso.apps.IgnoreSrcStaticFilesConfig", "django_cas_ng", "bootstrapform", "widget_tweaks", @@ -82,7 +82,7 @@ MIDDLEWARE = [ "django.middleware.locale.LocaleMiddleware", ] -ROOT_URLCONF = "cof.urls" +ROOT_URLCONF = "gestioasso.urls" TEMPLATES = [ { @@ -126,7 +126,7 @@ USE_I18N = True USE_L10N = True USE_TZ = True LANGUAGES = (("fr", "Français"), ("en", "English")) -FORMAT_MODULE_PATH = "cof.locale" +FORMAT_MODULE_PATH = "gestioasso.locale" # --- diff --git a/cof/settings/dev.py b/gestioasso/settings/dev.py similarity index 100% rename from cof/settings/dev.py rename to gestioasso/settings/dev.py diff --git a/cof/settings/local.py b/gestioasso/settings/local.py similarity index 97% rename from cof/settings/local.py rename to gestioasso/settings/local.py index c3607d7f..0cc7b5e5 100644 --- a/cof/settings/local.py +++ b/gestioasso/settings/local.py @@ -43,7 +43,7 @@ CACHES = {"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache" CHANNEL_LAYERS = { "default": { "BACKEND": "asgiref.inmemory.ChannelLayer", - "ROUTING": "cof.routing.routing", + "ROUTING": "gestioasso.routing.routing", } } diff --git a/cof/settings/secret_example.py b/gestioasso/settings/secret_example.py similarity index 100% rename from cof/settings/secret_example.py rename to gestioasso/settings/secret_example.py diff --git a/cof/urls.py b/gestioasso/urls.py similarity index 100% rename from cof/urls.py rename to gestioasso/urls.py diff --git a/cof/wsgi.py b/gestioasso/wsgi.py similarity index 55% rename from cof/wsgi.py rename to gestioasso/wsgi.py index 47285284..bdd9a64c 100644 --- a/cof/wsgi.py +++ b/gestioasso/wsgi.py @@ -2,5 +2,5 @@ import os from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings.bds_prod") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.bds_prod") application = get_wsgi_application() diff --git a/manage.py b/manage.py index 094ec16f..913e4f6e 100755 --- a/manage.py +++ b/manage.py @@ -3,7 +3,7 @@ import os import sys if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cof.settings.local") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gestioasso.settings.local") from django.core.management import execute_from_command_line diff --git a/provisioning/bootstrap.sh b/provisioning/bootstrap.sh index d6b8f914..a298dfae 100644 --- a/provisioning/bootstrap.sh +++ b/provisioning/bootstrap.sh @@ -14,7 +14,7 @@ readonly DBUSER="cof_gestion" readonly DBNAME="cof_gestion" readonly DBPASSWD="4KZt3nGPLVeWSvtBZPSM3fSzXpzEU4" readonly REDIS_PASSWD="dummy" -readonly DJANGO_SETTINGS_MODULE="cof.settings.dev" +readonly DJANGO_SETTINGS_MODULE="gestioasso.settings.dev" # --- @@ -83,7 +83,7 @@ sudo -H -u vagrant ~vagrant/venv/bin/pip install \ # Préparation de Django cd /vagrant -ln -s -f secret_example.py cof/settings/secret.py +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" diff --git a/provisioning/systemd/daphne.service b/provisioning/systemd/daphne.service index a9c30008..31b31c16 100644 --- a/provisioning/systemd/daphne.service +++ b/provisioning/systemd/daphne.service @@ -8,7 +8,7 @@ User=vagrant Group=vagrant TimeoutSec=300 WorkingDirectory=/vagrant -Environment="DJANGO_SETTINGS_MODULE=cof.settings.dev" +Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" ExecStart=/home/vagrant/venv/bin/daphne \ -u /srv/gestiocof/gestiocof.sock \ cof.asgi:channel_layer diff --git a/provisioning/systemd/rappels.service b/provisioning/systemd/rappels.service index 2d407d53..0a4986f9 100644 --- a/provisioning/systemd/rappels.service +++ b/provisioning/systemd/rappels.service @@ -4,5 +4,5 @@ Description=Envoi des mails de rappel des spectales BdA [Service] Type=oneshot User=vagrant -Environment="DJANGO_SETTINGS_MODULE=cof.settings.dev" +Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" ExecStart=/home/vagrant/venv/bin/python /vagrant/manage.py sendrappels diff --git a/provisioning/systemd/reventes.service b/provisioning/systemd/reventes.service index bd1992f8..266c0646 100644 --- a/provisioning/systemd/reventes.service +++ b/provisioning/systemd/reventes.service @@ -4,5 +4,5 @@ Description=Envoi des mails de BdA-Revente [Service] Type=oneshot User=vagrant -Environment="DJANGO_SETTINGS_MODULE=cof.settings.dev" +Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" ExecStart=/home/vagrant/venv/bin/python /vagrant/manage.py manage_reventes diff --git a/provisioning/systemd/worker.service b/provisioning/systemd/worker.service index 69d742dc..a9ea733f 100644 --- a/provisioning/systemd/worker.service +++ b/provisioning/systemd/worker.service @@ -9,7 +9,7 @@ User=vagrant Group=vagrant TimeoutSec=300 WorkingDirectory=/vagrant -Environment="DJANGO_SETTINGS_MODULE=cof.settings.dev" +Environment="DJANGO_SETTINGS_MODULE=gestioasso.settings.dev" ExecStart=/home/vagrant/venv/bin/python manage.py runworker [Install] From 7c35357060f862940034f771bc4932dc833e2449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Feb 2021 16:30:02 +0100 Subject: [PATCH 344/573] Fix a reverse url resolution on the BDS home page --- bds/templates/bds/home.html | 2 +- bds/tests/test_views.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/bds/templates/bds/home.html b/bds/templates/bds/home.html index 9ccaa364..f689d2b5 100644 --- a/bds/templates/bds/home.html +++ b/bds/templates/bds/home.html @@ -34,7 +34,7 @@

      - Télécharger la liste des membres (CSV) + Télécharger la liste des membres (CSV)

      diff --git a/bds/tests/test_views.py b/bds/tests/test_views.py index 20ce02a3..a40d3d85 100644 --- a/bds/tests/test_views.py +++ b/bds/tests/test_views.py @@ -22,6 +22,16 @@ def login_url(next=None): 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) + resp = self.client.get(reverse("bds:home")) + self.assertEquals(resp.status_code, 200) + + class TestRegistrationView(TestCase): @mock.patch("gestioncof.signals.messages") def test_get_autocomplete(self, mock_messages): From aa3462aaee92260358821244b6d9af136cda1fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Feb 2021 17:19:46 +0100 Subject: [PATCH 345/573] Update the CI config wrt the new project name --- .gitlab-ci.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 28ab0748..018830a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -21,8 +21,8 @@ variables: 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"/' cof/settings/secret_example.py > cof/settings/secret.py - - sed -i.bak -E 's;^REDIS_PASSWD = .*$;REDIS_PASSWD = "";' cof/settings/secret.py + - 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 # 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 @@ -44,7 +44,7 @@ coftest: stage: test extends: .test_template variables: - DJANGO_SETTINGS_MODULE: "cof.settings.cof_prod" + DJANGO_SETTINGS_MODULE: "gestioasso.settings.cof_prod" script: - coverage run manage.py test gestioncof bda kfet petitscours shared --parallel @@ -52,7 +52,7 @@ bdstest: stage: test extends: .test_template variables: - DJANGO_SETTINGS_MODULE: "cof.settings.bds_prod" + DJANGO_SETTINGS_MODULE: "gestioasso.settings.bds_prod" script: - coverage run manage.py test bds clubs events --parallel @@ -65,7 +65,7 @@ linters: - black --check . - isort --check --diff . # Print errors only - - flake8 --exit-zero bda bds clubs cof events gestioncof kfet petitscours provisioning shared + - flake8 --exit-zero bda bds clubs gestioasso events gestioncof kfet petitscours provisioning shared cache: key: linters paths: @@ -75,11 +75,11 @@ linters: migration_checks: stage: test variables: - DJANGO_SETTINGS_MODULE: "cof.settings.local" + 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 cof/settings/secret_example.py cof/settings/secret.py + - cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py - pip install --upgrade -r requirements-devel.txt - python --version script: python manage.py makemigrations --dry-run --check From f29b3f01876dde73faeb94dabe450babee5014a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 7 Feb 2021 17:20:35 +0100 Subject: [PATCH 346/573] Make "GestioBDS" appear in the README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e6b5a3ee..28c6686d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# GestioCOF +# GestioCOF / GestioBDS [![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) From 4f60ba35eb73b2e8b54dd494a40bac86e04c274c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 8 Feb 2021 19:19:54 +0100 Subject: [PATCH 347/573] Update the settings' docstrings --- gestioasso/settings/bds_prod.py | 6 ++++-- gestioasso/settings/cof_prod.py | 6 ++++-- gestioasso/settings/common.py | 5 +---- gestioasso/settings/dev.py | 8 +++++++- gestioasso/settings/local.py | 7 ++++++- gestioasso/settings/secret_example.py | 4 ++++ 6 files changed, 26 insertions(+), 10 deletions(-) diff --git a/gestioasso/settings/bds_prod.py b/gestioasso/settings/bds_prod.py index 12f5a552..361ed7cb 100644 --- a/gestioasso/settings/bds_prod.py +++ b/gestioasso/settings/bds_prod.py @@ -1,7 +1,9 @@ """ -Django development settings for the cof project. -The settings that are not listed here are imported from .common +Settings de production de GestioBDS. + +Surcharge les settings définis dans common.py """ + from .common import * # NOQA from .common import INSTALLED_APPS diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index ec0694fe..d85e84c5 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -1,7 +1,9 @@ """ -Django development settings for the cof project. -The settings that are not listed here are imported from .common +Settings de production de GestioCOF. + +Surcharge les settings définis dans common.py """ + import os from .common import * # NOQA diff --git a/gestioasso/settings/common.py b/gestioasso/settings/common.py index 88ae636a..cabe7000 100644 --- a/gestioasso/settings/common.py +++ b/gestioasso/settings/common.py @@ -1,8 +1,5 @@ """ -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. +Settings par défaut et settings communs à GestioCOF et GestioBDS. """ import os diff --git a/gestioasso/settings/dev.py b/gestioasso/settings/dev.py index 7e1a63a8..cd254b7a 100644 --- a/gestioasso/settings/dev.py +++ b/gestioasso/settings/dev.py @@ -1,4 +1,10 @@ -"""Django local development settings.""" +""" +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 diff --git a/gestioasso/settings/local.py b/gestioasso/settings/local.py index 0cc7b5e5..5c8c2734 100644 --- a/gestioasso/settings/local.py +++ b/gestioasso/settings/local.py @@ -1,4 +1,9 @@ -"""Django local development settings.""" +""" +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 diff --git a/gestioasso/settings/secret_example.py b/gestioasso/settings/secret_example.py index 7921d467..8afce5cd 100644 --- a/gestioasso/settings/secret_example.py +++ b/gestioasso/settings/secret_example.py @@ -1,3 +1,7 @@ +""" +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" From a53bd947372b97179693b13fd54969631c2520b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Tue, 9 Feb 2021 22:42:49 +0100 Subject: [PATCH 348/573] admin: rm the login_clipper column in the user list --- gestioncof/admin.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/gestioncof/admin.py b/gestioncof/admin.py index 768cff3b..89e4160d 100644 --- a/gestioncof/admin.py +++ b/gestioncof/admin.py @@ -100,28 +100,6 @@ 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: @@ -134,7 +112,6 @@ 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") @@ -163,7 +140,6 @@ class UserProfileAdmin(UserAdmin): is_cof.boolean = True list_display = UserAdmin.list_display + ( - "profile_login_clipper", "profile_phone", "profile_occupation", "profile_mailing_cof", From fbafdb7134cb448342d6a3da7695970997ccb453 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Wed, 10 Feb 2021 21:32:44 +0100 Subject: [PATCH 349/573] Added kfet history date limit when not accessing own account --- gestioasso/settings/cof_prod.py | 4 ++++ kfet/views.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index d85e84c5..6121c98d 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -5,6 +5,7 @@ Surcharge les settings définis dans common.py """ import os +from datetime import timedelta from .common import * # NOQA from .common import ( @@ -202,3 +203,6 @@ MAIL_DATA = { "REPLYTO": "BdA-Revente ", }, } + +# Max lookback date into kfet history +KFET_HISTORY_DATE_LIMIT = timedelta(weeks=1) diff --git a/kfet/views.py b/kfet/views.py index c50fb33e..a971e155 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1,11 +1,12 @@ import heapq import statistics from collections import defaultdict -from datetime import timedelta +from datetime import datetime, timedelta from decimal import Decimal 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 @@ -1468,6 +1469,9 @@ def history_json(request): .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) @@ -1484,9 +1488,17 @@ def history_json(request): transfergroups = TransferGroup.objects.none() if account: opegroups = opegroups.filter(on_acc=account) + if account.cofprofile.user.id == request.user.id: + limit_date = False # pas de limite de date sur son propre historique # Un non-membre de l'équipe n'a que accès à son historique if not request.user.has_perm("kfet.is_team"): opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet) + limit_date = False # pas de limite de date sur son propre historique + if limit_date: + # limiter l'accès à l'historique ancien pour confidentialité + earliest_date = datetime.today() - settings.KFET_HISTORY_DATE_LIMIT + opegroups = opegroups.filter(at__gte=earliest_date) + transfergroups = transfergroups.filter(at__gte=earliest_date) # Construction de la réponse history_groups = [] From 559b36b6f080805abe437cb37aeb762884154ffd Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 10 Feb 2021 22:13:50 +0100 Subject: [PATCH 350/573] Limite le datepicker pour ne pas demander plus de temps que possible dans l'historique --- kfet/templates/kfet/history.html | 1 + kfet/views.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index c3ebc8b0..91319012 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -62,6 +62,7 @@ $(document).ready(function() { format : 'YYYY-MM-DD HH:mm', stepping : 5, locale : 'fr', + minDate : '{{ week_ago }}', showTodayButton: true, widgetPositioning: { horizontal: "left", diff --git a/kfet/views.py b/kfet/views.py index a971e155..69b9395e 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1588,7 +1588,12 @@ def kpsul_articles_data(request): @teamkfet_required def history(request): - data = {"filter_form": FilterHistoryForm()} + week_ago = timezone.now() - settings.KFET_HISTORY_DATE_LIMIT + data = { + "filter_form": FilterHistoryForm(), + "week_ago": week_ago.strftime("%Y-%m-%d %H:%M"), + } + print(data["week_ago"]) return render(request, "kfet/history.html", data) From 9303772f9a4cf3cd6044ead6bc24c831c1efc382 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Wed, 10 Feb 2021 22:19:52 +0100 Subject: [PATCH 351/573] Renamed week_ago => history_limit and removed print --- kfet/templates/kfet/history.html | 2 +- kfet/views.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/kfet/templates/kfet/history.html b/kfet/templates/kfet/history.html index 91319012..03f9bbdf 100644 --- a/kfet/templates/kfet/history.html +++ b/kfet/templates/kfet/history.html @@ -62,7 +62,7 @@ $(document).ready(function() { format : 'YYYY-MM-DD HH:mm', stepping : 5, locale : 'fr', - minDate : '{{ week_ago }}', + minDate : '{{ history_limit }}', showTodayButton: true, widgetPositioning: { horizontal: "left", diff --git a/kfet/views.py b/kfet/views.py index 69b9395e..7245f3bf 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1588,12 +1588,11 @@ def kpsul_articles_data(request): @teamkfet_required def history(request): - week_ago = timezone.now() - settings.KFET_HISTORY_DATE_LIMIT + history_limit = timezone.now() - settings.KFET_HISTORY_DATE_LIMIT data = { "filter_form": FilterHistoryForm(), - "week_ago": week_ago.strftime("%Y-%m-%d %H:%M"), + "history_limit": history_limit.strftime("%Y-%m-%d %H:%M"), } - print(data["week_ago"]) return render(request, "kfet/history.html", data) From 7297baaf7ee00e20f71420b72d793bca5d922f2e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 18 Feb 2021 17:30:28 +0100 Subject: [PATCH 352/573] Only check migrations for custom apps --- .gitlab-ci.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 018830a3..e00ccbb4 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,6 +17,9 @@ variables: # psql password authentication PGPASSWORD: $POSTGRES_PASSWORD + # apps to check migrations for + MIGRATION_APPS: "bda bds clubs events gestioncof gestioncof.cms kfet kfet.auth kfet.cms kfet.open petitscours shared" + .test_template: before_script: - mkdir -p vendor/{pip,apt} @@ -82,7 +85,7 @@ migration_checks: - cp gestioasso/settings/secret_example.py gestioasso/settings/secret.py - pip install --upgrade -r requirements-devel.txt - python --version - script: python manage.py makemigrations --dry-run --check + script: python manage.py makemigrations --dry-run --check $MIGRATION_APPS services: # this should not be necessary… - postgres:11.7 From d7367476bcc00c7a316883f809a889035dddeab2 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 18 Feb 2021 17:41:52 +0100 Subject: [PATCH 353/573] Fix app names --- .gitlab-ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e00ccbb4..b0c1f4c6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -18,7 +18,7 @@ variables: PGPASSWORD: $POSTGRES_PASSWORD # apps to check migrations for - MIGRATION_APPS: "bda bds clubs events gestioncof gestioncof.cms kfet kfet.auth kfet.cms kfet.open petitscours shared" + MIGRATION_APPS: "bda bds cofcms clubs events gestioncof kfet kfetauth kfetcms open petitscours shared" .test_template: before_script: From 89fc309c01320c848ed93c25e2357683e3863679 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 10:18:47 +0100 Subject: [PATCH 354/573] Returned 403 on dubious history request --- kfet/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 7245f3bf..efb0aed3 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1491,9 +1491,10 @@ def history_json(request): if account.cofprofile.user.id == request.user.id: limit_date = False # pas de limite de date sur son propre historique # Un non-membre de l'équipe n'a que accès à son historique - if not request.user.has_perm("kfet.is_team"): - opegroups = opegroups.filter(on_acc=request.user.profile.account_kfet) - limit_date = False # pas de limite de date sur son propre 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 = datetime.today() - settings.KFET_HISTORY_DATE_LIMIT From b97bc8bfa8375e421daef8a9752c0b41d606b00c Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 10:26:05 +0100 Subject: [PATCH 355/573] Changed accoutn comparaison from id to equality --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index efb0aed3..10576d39 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1488,7 +1488,7 @@ def history_json(request): transfergroups = TransferGroup.objects.none() if account: opegroups = opegroups.filter(on_acc=account) - if account.cofprofile.user.id == request.user.id: + if account == request.user.profile.account_kfet: limit_date = False # pas de limite de date sur son propre historique # Un non-membre de l'équipe n'a que accès à son historique elif not request.user.has_perm("kfet.is_team"): From fa8c57269cba5f68397c733960845383856eaef6 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 10:32:12 +0100 Subject: [PATCH 356/573] Added help_text to history form --- kfet/forms.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/kfet/forms.py b/kfet/forms.py index bc98a8ce..6623ad0e 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -2,6 +2,7 @@ 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 @@ -484,7 +485,14 @@ class KFetConfigForm(ConfigForm): class FilterHistoryForm(forms.Form): - start = forms.DateTimeField(label=_("De"), widget=DateTimeWidget, required=False) + start = forms.DateTimeField( + label=_("De"), + widget=DateTimeWidget, + required=False, + help_text="L'historique est limité à {} jours".format( + settings.KFET_HISTORY_DATE_LIMIT.days + ), + ) end = forms.DateTimeField(label=_("À"), widget=DateTimeWidget, required=False) checkout = forms.ModelChoiceField( label=_("Caisse"), From 46242ad2c0dd2159667b31a902e2e438fd76cffb Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 10:48:24 +0100 Subject: [PATCH 357/573] Added separate permission for chef/trez --- gestioasso/settings/cof_prod.py | 2 ++ kfet/forms.py | 5 +++-- kfet/models.py | 1 + kfet/views.py | 11 +++++++++-- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index 6121c98d..4089f8cf 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -206,3 +206,5 @@ MAIL_DATA = { # Max lookback date into kfet history KFET_HISTORY_DATE_LIMIT = timedelta(weeks=1) +# limite plus longue pour les chef/trez +KFET_HISTORY_LONG_DATE_LIMIT = timedelta(days=30) diff --git a/kfet/forms.py b/kfet/forms.py index 6623ad0e..f93ff068 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -489,8 +489,9 @@ class FilterHistoryForm(forms.Form): label=_("De"), widget=DateTimeWidget, required=False, - help_text="L'historique est limité à {} jours".format( - settings.KFET_HISTORY_DATE_LIMIT.days + 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) diff --git a/kfet/models.py b/kfet/models.py index 2eacf06f..622c0ac9 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -89,6 +89,7 @@ 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): diff --git a/kfet/views.py b/kfet/views.py index 10576d39..ca280728 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1411,6 +1411,13 @@ def cancel_operations(request): return JsonResponse(data) +def get_history_limit(user) -> timedelta: + """returns the earliest date the user can view history""" + if user.has_perm("access_old_history"): + return datetime.today() - settings.KFET_HISTORY_LONG_DATE_LIMIT + return datetime.today() - settings.KFET_HISTORY_DATE_LIMIT + + @login_required def history_json(request): # Récupération des paramètres @@ -1497,7 +1504,7 @@ def history_json(request): return JsonResponse({}, status=403) if limit_date: # limiter l'accès à l'historique ancien pour confidentialité - earliest_date = datetime.today() - settings.KFET_HISTORY_DATE_LIMIT + earliest_date = get_history_limit(request.user) opegroups = opegroups.filter(at__gte=earliest_date) transfergroups = transfergroups.filter(at__gte=earliest_date) @@ -1589,7 +1596,7 @@ def kpsul_articles_data(request): @teamkfet_required def history(request): - history_limit = timezone.now() - settings.KFET_HISTORY_DATE_LIMIT + history_limit = get_history_limit(request.user) data = { "filter_form": FilterHistoryForm(), "history_limit": history_limit.strftime("%Y-%m-%d %H:%M"), From beba3052dd01bddbed5cafd830145cbe48e41443 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 11:46:42 +0100 Subject: [PATCH 358/573] Switched from hardcoded settings to config --- gestioasso/settings/cof_prod.py | 5 ----- kfet/forms.py | 15 +++++++++++---- kfet/views.py | 4 ++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index 4089f8cf..280dab3f 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -203,8 +203,3 @@ MAIL_DATA = { "REPLYTO": "BdA-Revente ", }, } - -# Max lookback date into kfet history -KFET_HISTORY_DATE_LIMIT = timedelta(weeks=1) -# limite plus longue pour les chef/trez -KFET_HISTORY_LONG_DATE_LIMIT = timedelta(days=30) diff --git a/kfet/forms.py b/kfet/forms.py index f93ff068..aba6d7c4 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -482,6 +482,16 @@ class KFetConfigForm(ConfigForm): label="Durée pour annuler une commande sans mot de passe", initial=timedelta(minutes=5), ) + kfet_history_limit = forms.DurationField( + label="Limite de confidentialité de l'historique", + initial=timedelta(days=7), + help_text="Les éléments plus vieux que cette durée seront masqués", + ) + kfet_history_long_limit = forms.DurationField( + label="Limite de confidentialité de l'historique pour chef/trez", + initial=timedelta(days=30), + help_text="Limite plus longue en cas de problème de compta", + ) class FilterHistoryForm(forms.Form): @@ -489,10 +499,7 @@ class FilterHistoryForm(forms.Form): 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, - ), + help_text="Limité pour raisons de confidentialité", ) end = forms.DateTimeField(label=_("À"), widget=DateTimeWidget, required=False) checkout = forms.ModelChoiceField( diff --git a/kfet/views.py b/kfet/views.py index ca280728..859dc60d 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1414,8 +1414,8 @@ def cancel_operations(request): def get_history_limit(user) -> timedelta: """returns the earliest date the user can view history""" if user.has_perm("access_old_history"): - return datetime.today() - settings.KFET_HISTORY_LONG_DATE_LIMIT - return datetime.today() - settings.KFET_HISTORY_DATE_LIMIT + return datetime.today() - kfet_config.history_long_limit + return datetime.today() - kfet_config.history_limit @login_required From 884ec2535b0dfe5e1e4e6f65a5ed540bb684e6c2 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 11:51:48 +0100 Subject: [PATCH 359/573] Fixed stupid errors --- kfet/forms.py | 1 - kfet/views.py | 13 ++++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index aba6d7c4..16b4963d 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -2,7 +2,6 @@ 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 diff --git a/kfet/views.py b/kfet/views.py index 859dc60d..e45c6508 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -6,7 +6,6 @@ from decimal import Decimal 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 @@ -1411,11 +1410,15 @@ def cancel_operations(request): return JsonResponse(data) -def get_history_limit(user) -> timedelta: - """returns the earliest date the user can view history""" - if user.has_perm("access_old_history"): +def get_history_limit(user) -> datetime: + """returns the earliest date the given user can view history + according to his/her permissions""" + if user.has_perm("kfet.access_old_history"): return datetime.today() - kfet_config.history_long_limit - return datetime.today() - kfet_config.history_limit + if user.has_perm("kfet.is_team"): + return datetime.today() - kfet_config.history_limit + # should not happen - future earliest date + return datetime.today() + timedelta(days=1) @login_required From 4b95b65be2ad3d0c861f7f02f20e46166eb19c04 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 11:55:18 +0100 Subject: [PATCH 360/573] Removed unused import --- gestioasso/settings/cof_prod.py | 1 - 1 file changed, 1 deletion(-) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index 280dab3f..d85e84c5 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -5,7 +5,6 @@ Surcharge les settings définis dans common.py """ import os -from datetime import timedelta from .common import * # NOQA from .common import ( From 9a635148bbaa448ddad346369fc169bb0647f357 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 12:13:23 +0100 Subject: [PATCH 361/573] Switched from datetime.today() to timezone.now() --- kfet/views.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index e45c6508..9bf85b66 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1413,12 +1413,13 @@ def cancel_operations(request): def get_history_limit(user) -> datetime: """returns the earliest date the given user can view history according to his/her permissions""" + now = timezone.now() if user.has_perm("kfet.access_old_history"): - return datetime.today() - kfet_config.history_long_limit + return now - kfet_config.history_long_limit if user.has_perm("kfet.is_team"): - return datetime.today() - kfet_config.history_limit + return now - kfet_config.history_limit # should not happen - future earliest date - return datetime.today() + timedelta(days=1) + return now + timedelta(days=1) @login_required From 30a39ef2f695616bddd5d6690e63fdb4d31b3ef8 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 12:16:43 +0100 Subject: [PATCH 362/573] Switch from account test to user test --- kfet/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index 9bf85b66..a354cd48 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1499,7 +1499,7 @@ def history_json(request): transfergroups = TransferGroup.objects.none() if account: opegroups = opegroups.filter(on_acc=account) - if account == request.user.profile.account_kfet: + if account.user == request.user: limit_date = False # pas de limite de date sur son propre historique # Un non-membre de l'équipe n'a que accès à son historique elif not request.user.has_perm("kfet.is_team"): From a8de7e0ae00e06e35900f6ad7f7a0d8362c86a45 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 13:38:36 +0100 Subject: [PATCH 363/573] makemigrations --- kfet/migrations/0074_auto_20210219_1337.py | 36 ++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 kfet/migrations/0074_auto_20210219_1337.py diff --git a/kfet/migrations/0074_auto_20210219_1337.py b/kfet/migrations/0074_auto_20210219_1337.py new file mode 100644 index 00000000..7b4127d8 --- /dev/null +++ b/kfet/migrations/0074_auto_20210219_1337.py @@ -0,0 +1,36 @@ +# 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"), + ) + }, + ), + ] From 1183e50f60054fe3bb5d2b51c86e9621f88bfbf6 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Fri, 19 Feb 2021 13:48:12 +0100 Subject: [PATCH 364/573] Fixed tests --- kfet/tests/test_views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 7d395e7e..eb8db1f4 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -4219,8 +4219,8 @@ class HistoryJSONViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.history.json" url_expected = "/k-fet/history.json" - auth_user = "user" - auth_forbidden = [None, "noaccount"] + auth_user = "team" + auth_forbidden = [None, "user", "noaccount"] def test_ok(self): r = self.client.post(self.url) @@ -4310,6 +4310,8 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): "kfet_overdraft_duration": "2 00:00:00", "kfet_overdraft_amount": "25", "kfet_cancel_duration": "00:20:00", + "kfet_history_limit": "5 00:00:00", + "kfet_history_long_limit": "60 00:00:00", } def get_users_extra(self): @@ -4331,6 +4333,8 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): "overdraft_duration": timedelta(days=2), "overdraft_amount": Decimal("25"), "cancel_duration": timedelta(minutes=20), + "history_limit": timedelta(days=5), + "history_long_limit": timedelta(days=60), } for key, expected in expected_config.items(): From cc7c4306f466a300010ad69426982e405192a8d8 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Sat, 20 Feb 2021 19:10:49 +0100 Subject: [PATCH 365/573] Added change description to CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2573983..3bd71609 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ adhérents ni des cotisations. ## Version ??? - dans un futur proche +### K-Fêt + +- 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 limites sont configurables depuis les paramètres K-Fêt. + ## Version 0.9 - 06/02/2020 ### COF / BdA From 23f7865140d4d3b79ed4f0e013b2f2155da38e08 Mon Sep 17 00:00:00 2001 From: Dorian Lesbre Date: Sat, 20 Feb 2021 20:59:54 +0100 Subject: [PATCH 366/573] Switch back from config to settings --- CHANGELOG.md | 2 +- gestioasso/settings/cof_prod.py | 13 +++++++++++++ kfet/forms.py | 16 +++++----------- kfet/tests/test_views.py | 4 ---- kfet/views.py | 5 +++-- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bd71609..79eb297b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ adhérents ni des cotisations. ### K-Fêt -- 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 limites sont configurables depuis les paramètres K-Fêt. +- 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`. ## Version 0.9 - 06/02/2020 diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index d85e84c5..3104e5b0 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -5,6 +5,7 @@ Surcharge les settings définis dans common.py """ import os +from datetime import timedelta from .common import * # NOQA from .common import ( @@ -202,3 +203,15 @@ MAIL_DATA = { "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) diff --git a/kfet/forms.py b/kfet/forms.py index 16b4963d..f93ff068 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -2,6 +2,7 @@ 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 @@ -481,16 +482,6 @@ class KFetConfigForm(ConfigForm): label="Durée pour annuler une commande sans mot de passe", initial=timedelta(minutes=5), ) - kfet_history_limit = forms.DurationField( - label="Limite de confidentialité de l'historique", - initial=timedelta(days=7), - help_text="Les éléments plus vieux que cette durée seront masqués", - ) - kfet_history_long_limit = forms.DurationField( - label="Limite de confidentialité de l'historique pour chef/trez", - initial=timedelta(days=30), - help_text="Limite plus longue en cas de problème de compta", - ) class FilterHistoryForm(forms.Form): @@ -498,7 +489,10 @@ class FilterHistoryForm(forms.Form): label=_("De"), widget=DateTimeWidget, required=False, - help_text="Limité pour raisons de confidentialité", + 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( diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index eb8db1f4..40b9ef77 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -4310,8 +4310,6 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): "kfet_overdraft_duration": "2 00:00:00", "kfet_overdraft_amount": "25", "kfet_cancel_duration": "00:20:00", - "kfet_history_limit": "5 00:00:00", - "kfet_history_long_limit": "60 00:00:00", } def get_users_extra(self): @@ -4333,8 +4331,6 @@ class SettingsUpdateViewTests(ViewTestCaseMixin, TestCase): "overdraft_duration": timedelta(days=2), "overdraft_amount": Decimal("25"), "cancel_duration": timedelta(minutes=20), - "history_limit": timedelta(days=5), - "history_long_limit": timedelta(days=60), } for key, expected in expected_config.items(): diff --git a/kfet/views.py b/kfet/views.py index a354cd48..0fe99ea4 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -6,6 +6,7 @@ from decimal import Decimal 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 @@ -1415,9 +1416,9 @@ def get_history_limit(user) -> datetime: according to his/her permissions""" now = timezone.now() if user.has_perm("kfet.access_old_history"): - return now - kfet_config.history_long_limit + return now - settings.KFET_HISTORY_LONG_DATE_LIMIT if user.has_perm("kfet.is_team"): - return now - kfet_config.history_limit + return now - settings.KFET_HISTORY_LONG_DATE_LIMIT # should not happen - future earliest date return now + timedelta(days=1) From 4e758fbba0e09d1dd94c73960be31a0d4c15b0d0 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 18 Feb 2021 17:57:59 +0100 Subject: [PATCH 367/573] Delete `balance_offset` field --- kfet/forms.py | 1 - ...4_remove_accountnegative_balance_offset.py | 17 ++++++++ kfet/models.py | 28 ++---------- kfet/views.py | 43 ++----------------- 4 files changed, 24 insertions(+), 65 deletions(-) create mode 100644 kfet/migrations/0074_remove_accountnegative_balance_offset.py diff --git a/kfet/forms.py b/kfet/forms.py index f93ff068..4dd8a9bc 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -150,7 +150,6 @@ class AccountNegativeForm(forms.ModelForm): fields = [ "authz_overdraft_amount", "authz_overdraft_until", - "balance_offset", "comment", ] widgets = {"authz_overdraft_until": DateTimeWidget()} diff --git a/kfet/migrations/0074_remove_accountnegative_balance_offset.py b/kfet/migrations/0074_remove_accountnegative_balance_offset.py new file mode 100644 index 00000000..818adfb1 --- /dev/null +++ b/kfet/migrations/0074_remove_accountnegative_balance_offset.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.17 on 2021-02-18 16:48 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("kfet", "0073_2021"), + ] + + operations = [ + migrations.RemoveField( + model_name="accountnegative", + name="balance_offset", + ), + ] diff --git a/kfet/models.py b/kfet/models.py index 622c0ac9..7156ae52 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -129,12 +129,6 @@ class Account(models.Model): 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() @@ -275,7 +269,7 @@ class Account(models.Model): self.password = hash_password(clear_password) def update_negative(self): - if self.real_balance < 0: + if self.balance < 0: if hasattr(self, "negative") and not self.negative.start: self.negative.start = timezone.now() self.negative.save() @@ -284,15 +278,8 @@ class Account(models.Model): account=self, start=timezone.now() ) elif hasattr(self, "negative"): - # self.real_balance >= 0 - balance_offset = self.negative.balance_offset - if balance_offset: - ( - Account.objects.filter(pk=self.pk).update( - balance=F("balance") - balance_offset - ) - ) - self.refresh_from_db() + # self.balance >= 0 + # TODO: méchanisme pour éviter de contourner le délai de négatif ? self.negative.delete() class UserHasAccount(Exception): @@ -318,15 +305,6 @@ class AccountNegative(models.Model): Account, on_delete=models.CASCADE, related_name="negative" ) start = models.DateTimeField(blank=True, null=True, default=None) - 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, diff --git a/kfet/views.py b/kfet/views.py index 0fe99ea4..5322082c 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -396,29 +396,12 @@ def account_update(request, trigramme): 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( @@ -513,8 +496,8 @@ class AccountNegativeList(ListView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - real_balances = (neg.account.real_balance for neg in self.object_list) - context["negatives_sum"] = sum(real_balances) + balances = (neg.account.balance for neg in self.object_list) + context["negatives_sum"] = sum(balances) return context @@ -1716,16 +1699,7 @@ def perform_transfers(request): balance=F("balance") + to_accounts_balances[account] ) account.refresh_from_db() - 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() + account.update_negative() # Saving transfer group transfergroup.save() @@ -1827,16 +1801,7 @@ def cancel_transfers(request): balance=F("balance") + to_accounts_balances[account] ) account.refresh_from_db() - 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() + account.update_negative() transfers = ( Transfer.objects.values("id", "canceled_at", "canceled_by__trigramme") From a421bec62598c4dc56590e94d0d4a4ce83cd2fd7 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 18 Feb 2021 17:58:08 +0100 Subject: [PATCH 368/573] Fix templates --- kfet/templates/kfet/account_negative.html | 16 +++------------- kfet/templates/kfet/left_account.html | 22 ++++++++++------------ 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index fa8b508d..9ca9cd99 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -35,20 +35,16 @@ {% block main %}
      - +
      - - @@ -61,11 +57,6 @@ - @@ -73,11 +64,10 @@ - {% endfor %}
      Tri. Nom BalanceRéelle Début Dé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_until|date:'d/m/Y H:i' }} {{ neg.balance_offset|default_if_none:'' }}
      -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/kfet/templates/kfet/left_account.html b/kfet/templates/kfet/left_account.html index 716c96cc..a058abf9 100644 --- a/kfet/templates/kfet/left_account.html +++ b/kfet/templates/kfet/left_account.html @@ -39,7 +39,8 @@
    • {{ account.departement }} {{ account.promo }}
    • {% if account.is_cof %} - Adhérent COF + Adhérent COF {% else %} Non-COF {% endif %} @@ -54,9 +55,6 @@ {% 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 }} € @@ -89,20 +87,20 @@ {% endif %} + }); + \ No newline at end of file From 1cf6f6f3e71bd2b5a953b649c5e4089b15f31406 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 23 Feb 2021 22:41:04 +0100 Subject: [PATCH 369/573] Fix migration conflict --- ...t.py => 0075_remove_accountnegative_balance_offset.py} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename kfet/migrations/{0074_remove_accountnegative_balance_offset.py => 0075_remove_accountnegative_balance_offset.py} (50%) diff --git a/kfet/migrations/0074_remove_accountnegative_balance_offset.py b/kfet/migrations/0075_remove_accountnegative_balance_offset.py similarity index 50% rename from kfet/migrations/0074_remove_accountnegative_balance_offset.py rename to kfet/migrations/0075_remove_accountnegative_balance_offset.py index 818adfb1..3bf3134c 100644 --- a/kfet/migrations/0074_remove_accountnegative_balance_offset.py +++ b/kfet/migrations/0075_remove_accountnegative_balance_offset.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.17 on 2021-02-18 16:48 +# Generated by Django 2.2.17 on 2021-02-23 21:40 from django.db import migrations @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ("kfet", "0073_2021"), + ('kfet', '0074_auto_20210219_1337'), ] operations = [ migrations.RemoveField( - model_name="accountnegative", - name="balance_offset", + model_name='accountnegative', + name='balance_offset', ), ] From 1ab071d16e9569c9338ac34bc4708a0babf27256 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 23 Feb 2021 22:52:27 +0100 Subject: [PATCH 370/573] LINT --- .../0075_remove_accountnegative_balance_offset.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kfet/migrations/0075_remove_accountnegative_balance_offset.py b/kfet/migrations/0075_remove_accountnegative_balance_offset.py index 3bf3134c..bf06e9ae 100644 --- a/kfet/migrations/0075_remove_accountnegative_balance_offset.py +++ b/kfet/migrations/0075_remove_accountnegative_balance_offset.py @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('kfet', '0074_auto_20210219_1337'), + ("kfet", "0074_auto_20210219_1337"), ] operations = [ migrations.RemoveField( - model_name='accountnegative', - name='balance_offset', + model_name="accountnegative", + name="balance_offset", ), ] From b224fedf283251a329ab54b0617db15a9e4e6711 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 20 Feb 2021 15:40:52 +0100 Subject: [PATCH 371/573] Fix frozen account display --- kfet/static/kfet/css/index.css | 11 ++++++++++- kfet/static/kfet/js/account.js | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 7d4324b4..f0eaedf0 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -41,10 +41,19 @@ } .frozen-account { - background:#5072e0; + background:#000FBA; 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; diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index 5ce3c8cd..3e216155 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -69,6 +69,8 @@ var AccountView = Backbone.View.extend({ attr_data_balance: function () { if (this.model.id == 0) { return ''; + } else if (this.model.get("is_frozen")) { + return "frozen"; } else if (this.model.get("balance") < 0) { return 'neg'; } else if (this.model.get("balance") <= 5) { From 209360f535caef667480c05bf3dd69a946005efb Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 20 Feb 2021 15:42:16 +0100 Subject: [PATCH 372/573] Delete self-update form --- kfet/forms.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 4dd8a9bc..79f8bb73 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -85,11 +85,6 @@ 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", From aac94afcd0a59fdf473ca7b7fdb876437458af5b Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 20 Feb 2021 15:44:44 +0100 Subject: [PATCH 373/573] =?UTF-8?q?Am=C3=A9liore=20le=20formulaire=20de=20?= =?UTF-8?q?mdp=20K-F=C3=AAt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/forms.py | 23 +++++++++++++++++----- kfet/templates/kfet/account_update.html | 26 ++++++++++++------------- 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 79f8bb73..019a8e41 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -90,21 +90,34 @@ class AccountPwdForm(forms.Form): 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, + widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), + min_length=8, ) pwd2 = forms.CharField( - label="Confirmer le mot de passe", required=False, widget=forms.PasswordInput + label="Confirmer le mot de passe", + required=False, + widget=forms.PasswordInput(attrs={"autocomplete": "new-password"}), ) + 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: - raise ValidationError("Les mots de passes sont différents") + self.add_error("pwd2", "Les mots de passe doivent être identiques !") super().clean() + def save(self, commit=True): + password = self.cleaned_data["pwd1"] + self.account.set_password(password) + if commit: + self.account.save() + + return self.account + class CofForm(forms.ModelForm): def clean_is_cof(self): diff --git a/kfet/templates/kfet/account_update.html b/kfet/templates/kfet/account_update.html index 36b3d75d..dcb55555 100644 --- a/kfet/templates/kfet/account_update.html +++ b/kfet/templates/kfet/account_update.html @@ -6,29 +6,29 @@ {% 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 %} @@ -36,21 +36,21 @@ {% include 'kfet/form_snippet.html' with form=pwd_form %} {% include 'kfet/form_snippet.html' with form=negative_form %} {% if perms.kfet.is_team %} - {% include 'kfet/form_authentication_snippet.html' %} + {% include 'kfet/form_authentication_snippet.html' %} {% endif %} {% include 'kfet/form_submit_snippet.html' with value="Mettre à jour" %}
      -{% endblock %} +{% endblock %} \ No newline at end of file From 1450b65dcde895a37524324f55223810cf30a2d1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 20 Feb 2021 15:46:44 +0100 Subject: [PATCH 374/573] Rework complet de `account_update` --- kfet/views.py | 155 +++++++++++++++++++++++--------------------------- 1 file changed, 70 insertions(+), 85 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index 5322082c..992db0ec 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -16,7 +16,12 @@ from django.core.exceptions import SuspiciousOperation from django.db import transaction from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery, Sum from django.forms import formset_factory -from django.http import Http404, HttpResponseBadRequest, JsonResponse +from django.http import ( + Http404, + HttpResponseBadRequest, + HttpResponseForbidden, + JsonResponse, +) from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse, reverse_lazy from django.utils import timezone @@ -36,7 +41,6 @@ from kfet.forms import ( AccountNegativeForm, AccountNoTriForm, AccountPwdForm, - AccountRestrictForm, AccountStatForm, AccountTriForm, AddcostForm, @@ -332,109 +336,89 @@ def account_read(request, trigramme): # Account - Update -@login_required +@teamkfet_required @kfet_password_auth def account_update(request, trigramme): account = get_object_or_404(Account, trigramme=trigramme) # Checking permissions - if not account.editable or ( - not request.user.has_perm("kfet.is_team") and request.user != account.user - ): - raise Http404 + if not account.editable: + # Plus de leak de trigramme ! + return HttpResponseForbidden user_info_form = UserInfoForm(instance=account.user) - 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 + group_form = UserGroupForm(instance=account.user) + account_form = AccountForm(instance=account) + pwd_form = AccountPwdForm() + if hasattr(account, "negative"): + negative_form = AccountNegativeForm(instance=account.negative) else: - account_form = AccountRestrictForm(instance=account) - group_form = None negative_form = None - pwd_form = None if request.method == "POST": - # Update attempt - success = False - missing_perm = True + self_update = request.user == account.user + account_form = AccountForm(request.POST, instance=account) + group_form = UserGroupForm(request.POST, instance=account.user) + pwd_form = AccountPwdForm(request.POST, account=account) - 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 - ) + forms = [] + warnings = [] - if request.user.has_perm("kfet.change_account") and account_form.is_valid(): - missing_perm = False + if self_update or request.user.has_perm("kfet.change_account"): + forms.append(account_form) + elif account_form.has_changed(): + warnings.append("compte") - # Updating - account_form.save() + if request.user.has_perm("kfet.manage_perms"): + forms.append(group_form) + elif group_form.has_changed(): + warnings.append("statut d'équipe") - # 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") + if hasattr(account, "negative"): + negative_form = AccountNegativeForm(request.POST, instance=account.negative) - # Checking perm to manage perms - if request.user.has_perm("kfet.manage_perms") and group_form.is_valid(): - group_form.save() + if request.user.has_perm("kfet.change_accountnegative"): + forms.append(negative_form) + elif negative_form.has_changed(): + warnings.append("négatifs") - if ( - hasattr(account, "negative") - and request.user.has_perm("kfet.change_accountnegative") - and negative_form.is_valid() - ): - negative_form.save() + # 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") - 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: + # Updating account info + if forms == []: messages.error( - request, "Informations non mises à jour. Corrigez les erreurs" + request, "Informations non mises à jour : permission refusée" ) + else: + if all(form.is_valid() for form in forms): + for form in forms: + form.save() + + 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" + ) return render( request, @@ -449,7 +433,8 @@ def account_update(request, trigramme): }, ) - # Account - Delete + +# Account - Delete class AccountDelete(PermissionRequiredMixin, DeleteView): From 47f406e09e4966f1f80be40ecd37364e1cef0eea Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 20 Feb 2021 17:04:45 +0100 Subject: [PATCH 375/573] Fix tests --- kfet/auth/tests.py | 8 +++-- kfet/forms.py | 2 +- kfet/tests/test_views.py | 74 ++++++++++++++++++++-------------------- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index 32e04812..a7a0b5ad 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -284,7 +284,11 @@ class TemporaryAuthTests(TestCase): self.perm = Permission.objects.get( content_type__app_label="kfet", codename="is_team" ) - self.user2.user_permissions.add(self.perm) + 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) def test_context_processor(self): """ @@ -295,7 +299,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.is_team", r.context["perms"]) + self.assertNotIn("kfet.can_force_close", r.context["perms"]) def test_auth_not_persistent(self): """ diff --git a/kfet/forms.py b/kfet/forms.py index 019a8e41..b9adbc81 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -112,7 +112,7 @@ class AccountPwdForm(forms.Form): def save(self, commit=True): password = self.cleaned_data["pwd1"] - self.account.set_password(password) + self.account.change_pwd(password) if commit: self.account.save() diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index 40b9ef77..bc50b023 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -11,6 +11,7 @@ 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, @@ -296,8 +297,8 @@ class AccountReadViewTests(ViewTestCaseMixin, TestCase): class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): url_name = "kfet.account.update" - url_kwargs = {"trigramme": "001"} - url_expected = "/k-fet/accounts/001/edit" + url_kwargs = {"trigramme": "100"} + url_expected = "/k-fet/accounts/100/edit" http_methods = ["GET", "POST"] @@ -317,26 +318,16 @@ class AccountUpdateViewTests(ViewTestCaseMixin, TestCase): "promo": "", # 'is_frozen': not checked # Account password - "pwd1": "", - "pwd2": "", + "pwd1": "changed_pwd", + "pwd2": "changed_pwd", } 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) @@ -356,46 +347,55 @@ 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) + r = client.post(self.url, self.post_data, follow=True) self.assertRedirects(r, reverse("kfet.account.read", args=["051"])) - self.accounts["user1"].refresh_from_db() - self.users["user1"].refresh_from_db() + # Comportement attendu : compte modifié, + # utilisateur/mdp inchangé, warning pour le mdp + + self.accounts["team"].refresh_from_db() + self.users["team"].refresh_from_db() self.assertInstanceExpected( - self.accounts["user1"], - {"first_name": "first", "last_name": "last", "trigramme": "051"}, + 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"]) ) def test_post_ok_self(self): - client = Client() - client.login(username="user1", password="user1") + r = self.client.post(self.url, self.post_data, follow=True) + self.assertRedirects(r, reverse("kfet.account.read", args=["051"])) - post_data = {"first_name": "The first", "last_name": "The last"} + self.accounts["team"].refresh_from_db() + self.users["team"].refresh_from_db() - 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() + # Comportement attendu : compte/mdp modifié, utilisateur inchangé self.assertInstanceExpected( - self.accounts["user1"], {"first_name": "first", "last_name": "last"} + self.accounts["team"], + {"first_name": "team", "last_name": "member", "trigramme": "051"}, ) + self.assertEqual(self.accounts["team"].password, hash_password("changed_pwd")) def test_post_forbidden(self): - r = self.client.post(self.url, self.post_data) - self.assertForbiddenKfet(r) + 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"] + ) + ) class AccountDeleteViewTests(ViewTestCaseMixin, TestCase): From f9958e4da0248d9ce8ccf38d952616ce8227fbcd Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:35:40 +0100 Subject: [PATCH 376/573] Fix : plus de warnings chelous pendant les tests --- kfet/statistic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/statistic.py b/kfet/statistic.py index 4cf04387..4d7c86f4 100644 --- a/kfet/statistic.py +++ b/kfet/statistic.py @@ -9,7 +9,7 @@ KFET_WAKES_UP_AT = time(5, 0) # La K-Fêt ouvre à 5h (UTC) du matin def kfet_day(year, month, day, start_at=KFET_WAKES_UP_AT): """Étant donné une date, renvoie un objet `datetime` correspondant au début du 'jour K-Fêt' correspondant.""" - return datetime.combine(date(year, month, day), start_at) + return datetime.combine(date(year, month, day), start_at, tzinfo=timezone.utc) def to_kfet_day(dt, start_at=KFET_WAKES_UP_AT): From b72ea9ebf9989961c558cd096af82e7c1bede8b1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:56:12 +0100 Subject: [PATCH 377/573] Forgot a warning --- shared/views.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shared/views.py b/shared/views.py index 31523bad..a1ffb185 100644 --- a/shared/views.py +++ b/shared/views.py @@ -11,6 +11,8 @@ from shared.autocomplete import ModelSearch 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) From 472a44c30fa120a4d1425ed4d582e3efcb4a3ed7 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 3 Mar 2021 23:11:39 +0100 Subject: [PATCH 378/573] Remove useless buttons --- kfet/templates/kfet/left_account.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/kfet/templates/kfet/left_account.html b/kfet/templates/kfet/left_account.html index a058abf9..e1673d22 100644 --- a/kfet/templates/kfet/left_account.html +++ b/kfet/templates/kfet/left_account.html @@ -11,13 +11,11 @@ + {% if perms.kfet.is_team %}
      Éditer - - Créditer - {% if perms.kfet.delete_account %}
      + {% endif %}

      {{ account.name|title }}

      From 47dd078b6ae6e5c7a9ab5c28cbbabfae96ec0503 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 4 Mar 2021 17:56:42 +0100 Subject: [PATCH 379/573] Remplace recaptcha par hcaptcha --- gestioasso/settings/cof_prod.py | 2 +- petitscours/forms.py | 18 ++++++++++++++++-- requirements.txt | 2 +- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index 3104e5b0..1865e7e3 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -50,7 +50,7 @@ INSTALLED_APPS = ( + [ "bda", "petitscours", - "captcha", + "hcaptcha", "kfet", "kfet.open", "channels", diff --git a/petitscours/forms.py b/petitscours/forms.py index 01d4178a..0d9f38bc 100644 --- a/petitscours/forms.py +++ b/petitscours/forms.py @@ -1,14 +1,28 @@ -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 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 DemandeForm(ModelForm): - captcha = ReCaptchaField(attrs={"theme": "clean", "lang": "fr"}) + captcha = hCaptchaFieldWithErrors() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/requirements.txt b/requirements.txt index 565d2b71..8baaa5ed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ Django==2.2.* django-autocomplete-light==3.3.* django-cas-ng==3.6.* django-djconfig==0.8.0 -django-recaptcha==1.4.0 +django-hCaptcha==0.1.0 icalendar Pillow django-bootstrap-form==3.3 From ac8ad15ad1119d131fc2a003f60dcd60555c01b3 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 4 Mar 2021 18:30:51 +0100 Subject: [PATCH 380/573] Fix tests: mock captcha clean method --- petitscours/tests/test_views.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/petitscours/tests/test_views.py b/petitscours/tests/test_views.py index aee1f2e8..6ca97086 100644 --- a/petitscours/tests/test_views.py +++ b/petitscours/tests/test_views.py @@ -1,5 +1,5 @@ import json -import os +from unittest import mock from django.contrib.auth import get_user_model from django.test import TestCase @@ -257,18 +257,15 @@ 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) - def test_post(self): + @mock.patch("hcaptcha.fields.hCaptchaField.clean") + def test_post(self, mock_clean): data = { "name": "Le nom", "email": "lemail@mail.net", @@ -280,7 +277,7 @@ class PetitCoursDemandeViewTestCase(ViewTestCaseMixin, TestCase): "agrege_requis": "1", "niveau": "lycee", "remarques": "no comment", - "g-recaptcha-response": "PASSED", + "h-captcha-response": 1, } resp = self.client.post(self.url, data) @@ -299,18 +296,15 @@ 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) - def test_post(self): + @mock.patch("hcaptcha.fields.hCaptchaField.clean") + def test_post(self, mock_clean): data = { "name": "Le nom", "email": "lemail@mail.net", @@ -322,7 +316,7 @@ class PetitCoursDemandeRawViewTestCase(ViewTestCaseMixin, TestCase): "agrege_requis": "1", "niveau": "lycee", "remarques": "no comment", - "g-recaptcha-response": "PASSED", + "h-captcha-response": 1, } resp = self.client.post(self.url, data) From af95e64344687327678a5c8e858fb7056bf72862 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 4 Mar 2021 23:14:10 +0100 Subject: [PATCH 381/573] TODO de prod --- CHANGELOG.md | 4 ++++ gestioasso/settings/secret_example.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79eb297b..c1b3b490 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ adhérents ni des cotisations. ## Version ??? - dans un futur proche +### TODO Prod + +- Créer un compte hCaptcha (https://www.hcaptcha.com/), au COF, et remplacer les secrets associés + ### K-Fêt - 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`. diff --git a/gestioasso/settings/secret_example.py b/gestioasso/settings/secret_example.py index 8afce5cd..b93aeb4f 100644 --- a/gestioasso/settings/secret_example.py +++ b/gestioasso/settings/secret_example.py @@ -16,8 +16,8 @@ REDIS_PORT = 6379 REDIS_DB = 0 REDIS_HOST = "127.0.0.1" -RECAPTCHA_PUBLIC_KEY = "DUMMY" -RECAPTCHA_PRIVATE_KEY = "DUMMY" +HCAPTCHA_SITEKEY = "10000000-ffff-ffff-ffff-000000000001" +HCAPTCHA_SECRET = "0x0000000000000000000000000000000000000000" EMAIL_HOST = None From 4df3ef4dd954d266cc79aa6272227c1321309766 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 4 Mar 2021 23:28:55 +0100 Subject: [PATCH 382/573] Fix secret import --- gestioasso/settings/cof_prod.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index 1865e7e3..28133ebc 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -26,8 +26,8 @@ REDIS_DB = import_secret("REDIS_DB") REDIS_HOST = import_secret("REDIS_HOST") REDIS_PORT = import_secret("REDIS_PORT") -RECAPTCHA_PUBLIC_KEY = import_secret("RECAPTCHA_PUBLIC_KEY") -RECAPTCHA_PRIVATE_KEY = import_secret("RECAPTCHA_PRIVATE_KEY") +HCAPTCHA_SITEKEY = import_secret("HCAPTCHA_SITEKEY") +HCAPTCHA_SECRET = import_secret("HCAPTCHA_SECRET") KFETOPEN_TOKEN = import_secret("KFETOPEN_TOKEN") # --- From 4268a30d51f2a89028c6864dcbf6a391c2805e08 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 16 Mar 2021 22:10:33 +0100 Subject: [PATCH 383/573] CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b3b490..1ea06094 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ adhérents ni des cotisations. - 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. +- 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 ### BDS From c14c2d54a588db362c62025e7aebaa759048fc87 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sat, 20 Feb 2021 19:18:21 +0100 Subject: [PATCH 384/573] More general forbidden test --- kfet/tests/testcases.py | 11 +++++--- kfet/views.py | 58 ++++++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 18 deletions(-) diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index 4912023e..16ccb186 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -79,10 +79,15 @@ class TestCaseMixin: self.assertEqual(response.status_code, 200) try: form = response.context[form_ctx] - self.assertIn("Permission refusée", form.non_field_errors()) + 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)) except (AssertionError, AttributeError, KeyError): - messages = [str(msg) for msg in response.context["messages"]] - self.assertIn("Permission refusée", messages) + self.assertTrue( + any( + "permission-denied" in msg.tags + for msg in response.context["messages"] + ) + ) except AssertionError: request = response.wsgi_request raise AssertionError( diff --git a/kfet/views.py b/kfet/views.py index 992db0ec..0423be07 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -12,7 +12,7 @@ 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.exceptions import SuspiciousOperation, ValidationError from django.db import transaction from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery, Sum from django.forms import formset_factory @@ -160,7 +160,9 @@ def account_create(request): ): # Checking permission if not request.user.has_perm("kfet.add_account"): - messages.error(request, "Permission refusée") + messages.error( + request, "Permission refusée", extra_tags="permission-denied" + ) else: data = {} # Fill data for Account.save() @@ -393,7 +395,9 @@ def account_update(request, trigramme): # Updating account info if forms == []: messages.error( - request, "Informations non mises à jour : permission refusée" + request, + "Informations non mises à jour : permission refusée", + extra_tags="permission-denied", ) else: if all(form.is_valid() for form in forms): @@ -513,7 +517,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Creating @@ -551,7 +557,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Updating return super().form_valid(form) @@ -641,7 +649,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Creating form.instance.amount_taken = getAmountTaken(form.instance) @@ -673,7 +683,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Updating form.instance.amount_taken = getAmountTaken(form.instance) @@ -705,7 +717,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Updating @@ -754,7 +768,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Save ici pour save le manytomany suppliers @@ -820,7 +836,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Save ici pour save le manytomany suppliers @@ -1599,7 +1617,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) form.save() return super().form_valid(form) @@ -1836,7 +1856,9 @@ 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") + messages.error( + request, "Permission refusée", extra_tags="permission-denied" + ) elif formset.is_valid(): with transaction.atomic(): @@ -2007,7 +2029,9 @@ 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") + messages.error( + request, "Permission refusée", extra_tags="permission-denied" + ) elif formset.is_valid(): order = Order() order.supplier = supplier @@ -2131,7 +2155,9 @@ 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") + messages.error( + request, "Permission refusée", extra_tags="permission-denied" + ) elif formset.is_valid(): with transaction.atomic(): inventory = Inventory.objects.create( @@ -2206,7 +2232,9 @@ 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, "Permission refusée") + form.add_error( + None, ValidationError("Permission refusée", code="permission-denied") + ) return self.form_invalid(form) # Updating return super().form_valid(form) From b48d32f4bcb7fe85ea99eebdd86d063ecada9445 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Fri, 16 Apr 2021 16:42:12 +0200 Subject: [PATCH 385/573] Remove limit for purchases --- kfet/templates/kfet/kpsul.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 3d6ba2d1..7d023ed5 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -713,7 +713,7 @@ $(document).ready(function() { }); function is_nb_ok(nb) { - return /^[0-9]+$/.test(nb) && nb > 0 && nb <= 24; + return /^[0-9]+$/.test(nb) && nb > 0; } articleNb.on('keydown', function(e) { From 1f4a4ec76ffb6bf7ec48a204f0a1209ad96bb7c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 18 Apr 2021 17:46:54 +0200 Subject: [PATCH 386/573] Update CHANGELOG.md --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea06094..30a31a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,13 @@ adhérents ni des cotisations. ### K-Fêt -- 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`. +- On fait sauter la limite qui empêchait de vendre plus de 24 unités d'un item à + la fois. +- 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`. ## Version 0.9 - 06/02/2020 From 9bbe3f50cb1807e100bf009426c1f768d2b29c73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Sun, 18 Apr 2021 18:17:38 +0200 Subject: [PATCH 387/573] Update CHANGELOG.md --- CHANGELOG.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30a31a87..684f72f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre Uniquement un modèle simple de clubs avec des respos. Aucune gestion des adhérents ni des cotisations. -## Version ??? - dans un futur proche +## Version 0.10 - 18/04/2021 ### TODO Prod @@ -31,12 +31,20 @@ adhérents ni des cotisations. - 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. + 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 @@ -47,7 +55,6 @@ adhérents ni des cotisations. - 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. -- 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 ### BDS From c10e5fe45cf8625d51917de43a0bd1cd1de9d6fb Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 20:11:18 +0100 Subject: [PATCH 388/573] Refactor Account model a bit --- kfet/static/kfet/js/account.js | 83 +++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 36 deletions(-) diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index 3e216155..ac98e1dd 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -10,7 +10,6 @@ var Account = Backbone.Model.extend({ 'is_frozen': false, 'departement': '', 'nickname': '', - 'trigramme': '', }, url: function () { @@ -18,8 +17,8 @@ var Account = Backbone.Model.extend({ }, reset: function () { - // On ne veut pas trigger un `change` deux fois - this.clear({ silent: true }).set(this.defaults) + // On n'utilise pas .clear() car on ne veut pas clear le trigramme + this.set(this.defaults) }, parse: function (resp, options) { @@ -31,27 +30,37 @@ var Account = Backbone.Model.extend({ }, view: function () { - view_class = this.get("trigramme") == 'LIQ' ? LIQView : AccountView; + if (!this.is_valid()) { + view_class = EmptyAccountView + } else if (this.get("trigramme") == 'LIQ') { + view_class = LIQView + } else { + view_class = AccountView + } return new view_class({ model: this }) }, render: function () { this.view().render(); - } + }, + + is_valid: 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 @@ -67,9 +76,9 @@ var AccountView = Backbone.View.extend({ }, attr_data_balance: function () { - if (this.model.id == 0) { - return ''; - } else if (this.model.get("is_frozen")) { + // Cette fonction est utilisée uniquement sur un compte valide + + if (this.model.get("is_frozen")) { return "frozen"; } else if (this.model.get("balance") < 0) { return 'neg'; @@ -81,23 +90,9 @@ var AccountView = Backbone.View.extend({ }, get_buttons: function () { - 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 += ''; - } - } + var url = django_urls["kfet.account.read"](this.model.get("trigramme")); - return buttons + return ``; }, render: function () { @@ -108,16 +103,7 @@ var AccountView = Backbone.View.extend({ this.$el.attr("data-balance", this.attr_data_balance()); this.$(this.buttons).html(this.get_buttons()); - }, - - 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()); + $(this.id_field).val(this.get("id")); }, }) @@ -131,3 +117,28 @@ 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_tri()) { + trigramme = encodeURIComponent(trigramme); + var url_base = django_urls["kfet.account.create"](); + var url = `${url_base}?trigramme=${trigramme}`; + buttons += ``; + } + + return buttons + } +}) \ No newline at end of file From 17d96f177537006bcc55499c5fd0f0c8b3c717af Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 20:11:53 +0100 Subject: [PATCH 389/573] New account manager logic --- kfet/static/kfet/js/kfet.js | 161 ++++++++++++++++++----------------- kfet/static/kfet/js/kpsul.js | 150 ++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 76 deletions(-) create mode 100644 kfet/static/kfet/js/kpsul.js diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 1002fc32..32493088 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -1,3 +1,17 @@ +/* + * Fonctions d'aide à la gestion de trigrammes + */ + +String.prototype.format_trigramme = function () { + return this.toUpperCase().substr(0, 3) +} + +String.prototype.is_valid_tri = function () { + var pattern = /^[^a-z]{3}$/; + return pattern.test(this); +} + + /** * CSRF Token */ @@ -14,7 +28,7 @@ function csrfSafeMethod(method) { } $.ajaxSetup({ - beforeSend: function(xhr, settings) { + beforeSend: function (xhr, settings) { if (!csrfSafeMethod(settings.type) && !this.crossDomain) { xhr.setRequestHeader("X-CSRFToken", csrftoken); } @@ -23,7 +37,7 @@ $.ajaxSetup({ function add_csrf_form($form) { $form.append( - $('', {'name': 'csrfmiddlewaretoken', 'value': csrftoken}) + $('', { 'name': 'csrfmiddlewaretoken', 'value': csrftoken }) ); } @@ -66,7 +80,7 @@ class KfetWebsocket { var that = this; 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); @@ -77,26 +91,21 @@ 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 isValidTrigramme(trigramme) { - var pattern = /^[^a-z]{3}$/; - return trigramme.match(pattern); -} - function getErrorsHtml(data) { var content = ''; if (!data) @@ -113,8 +122,8 @@ function getErrorsHtml(data) { if ('missing_perms' in data['errors']) { content += 'Permissions manquantes'; content += '
        '; - for (var i=0; i'; + for (var i = 0; i < data['errors']['missing_perms'].length; i++) + content += '
      • ' + data['errors']['missing_perms'][i] + '
      • '; content += '
      '; } if ('negative' in data['errors']) { @@ -123,8 +132,8 @@ function getErrorsHtml(data) { } else { var url_base = '/k-fet/accounts/'; } - for (var i=0; iAutorisation de négatif requise pour '+data['errors']['negative'][i]+''; + for (var i = 0; i < data['errors']['negative'].length; i++) { + content += 'Autorisation de négatif requise pour ' + data['errors']['negative'][i] + ''; } } if ('addcost' in data['errors']) { @@ -138,7 +147,7 @@ function getErrorsHtml(data) { if ('account' in data['errors']) { content += 'Général'; content += '
        '; - content += '
      • Opération invalide sur le compte '+data['errors']['account']+'
      • '; + content += '
      • Opération invalide sur le compte ' + data['errors']['account'] + '
      • '; content += '
      '; } return content; @@ -147,54 +156,54 @@ function getErrorsHtml(data) { function requestAuth(data, callback, focus_next = null) { var content = getErrorsHtml(data); content += '
      ', - $.confirm({ - title: 'Authentification requise', - content: content, - backgroundDismiss: true, - animation:'top', - closeAnimation:'bottom', - keyboardEnabled: true, - confirm: function() { - var password = this.$content.find('input').val(); - callback(password); - }, - 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) { - if (e.keyCode == 13) - that.$confirmButton.click(); + $.confirm({ + title: 'Authentification requise', + content: content, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + confirm: function () { + var password = this.$content.find('input').val(); + callback(password); + }, + 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) { + 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 - (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 ; - } - if (capslock == 1) - $('.capslock .glyphicon').show() ; - else if (capslock == 0) - $('.capslock .glyphicon').hide() ; - }); - // Capslock key is not detected by keypress - this.$content.find('input').on('keydown', function(e) { - if (e.which == 20) { - capslock = 1-capslock ; - } - if (capslock == 1) - $('.capslock .glyphicon').show() ; - else if (capslock == 0) - $('.capslock .glyphicon').hide() ; - }); - }, - onClose: function() { - if (focus_next) - this._lastFocused = focus_next; - } + var s = String.fromCharCode(e.which); + 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; + } + if (capslock == 1) + $('.capslock .glyphicon').show(); + else if (capslock == 0) + $('.capslock .glyphicon').hide(); + }); + // Capslock key is not detected by keypress + this.$content.find('input').on('keydown', function (e) { + if (e.which == 20) { + capslock = 1 - capslock; + } + if (capslock == 1) + $('.capslock .glyphicon').show(); + else if (capslock == 0) + $('.capslock .glyphicon').hide(); + }); + }, + onClose: function () { + if (focus_next) + this._lastFocused = focus_next; + } - }); + }); } @@ -249,7 +258,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); @@ -270,9 +279,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'. @@ -292,16 +301,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 new file mode 100644 index 00000000..ae37939c --- /dev/null +++ b/kfet/static/kfet/js/kpsul.js @@ -0,0 +1,150 @@ +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) { + // keycode 40: Down Arrow + if (e.keyCode == 40) { + that.set("LIQ") + } + }) + + // Fonction de recherche + this._$container.on('click', '.search', function () { + that.search.open(); + }); + + this._$container.on('keydown', function (e) { + if (e.which == 70 && 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_tri()) { + 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: '.choice', + 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) { + this.manager.set(choice.find('.trigramme').text()); + this.close(); + } + + close() { + if (this._$dialog !== undefined) { + this._$dialog.close(); + } + } +} From f901ea9396b098ce64af87fd72c0b17ffe021642 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 25 Dec 2019 20:12:03 +0100 Subject: [PATCH 390/573] Remove useless kpsul.html code --- kfet/templates/kfet/kpsul.html | 156 +++++---------------------------- 1 file changed, 22 insertions(+), 134 deletions(-) diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 7d023ed5..03ade3c6 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -10,6 +10,7 @@ + {% endblock %} {% block title %}K-Psul{% endblock %} @@ -213,124 +214,8 @@ $(document).ready(function() { // Account data management // ----- - // Initializing - var account = new Account() - var account_container = $('#account'); + var account_manager = new AccountManager(); 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'); - } - }); // ----- @@ -416,7 +301,7 @@ $(document).ready(function() { // Event listener checkoutInput.on('change', function() { retrieveCheckoutData(checkoutInput.val()); - if (account_data['trigramme']) { + if (account_manager.data['trigramme']) { articleSelect.focus().select(); } else { triInput.focus().select(); @@ -752,11 +637,11 @@ $(document).ready(function() { var amount_euro = - article_data[3] * nb ; if (settings['addcost_for'] && settings['addcost_amount'] - && account_data['trigramme'] != settings['addcost_for'] + && account_manager.data['trigramme'] != settings['addcost_for'] && article_data[5]) amount_euro -= settings['addcost_amount'] * nb; var reduc_divisor = 1; - if (account_data['is_cof'] && article_data[6]) + if (account_manager.data['is_cof'] && article_data[6]) reduc_divisor = 1 + settings['subvention_cof'] / 100; return (amount_euro / reduc_divisor).toFixed(2); } @@ -779,7 +664,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_data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount_euro, account_manager.data['is_cof'], false)); basket_container.prepend(article_basket_html); if (is_low_stock(id, nb)) article_basket_html.find('.lowstock') @@ -805,7 +690,7 @@ $(document).ready(function() { .attr('data-opeindex', index) .find('.number').text(amount+"€").end() .find('.name').text('Charge').end() - .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount, account_manager.data['is_cof'], false)); basket_container.prepend(deposit_basket_html); updateBasketRel(); } @@ -818,7 +703,7 @@ $(document).ready(function() { .attr('data-opeindex', index) .find('.number').text(amount+"€").end() .find('.name').text('Édition').end() - .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount, account_manager.data['is_cof'], false)); basket_container.prepend(deposit_basket_html); updateBasketRel(); } @@ -831,7 +716,7 @@ $(document).ready(function() { .attr('data-opeindex', index) .find('.number').text(amount+"€").end() .find('.name').text('Retrait').end() - .find('.amount').text(amountToUKF(amount, account_data['is_cof'], false)); + .find('.amount').text(amountToUKF(amount, account_manager.data['is_cof'], false)); basket_container.prepend(withdraw_basket_html); updateBasketRel(); } @@ -887,7 +772,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_data['is_cof'], false)); + basket_container.find('[data-opeindex='+opeindex+'] .amount').text(amountToUKF(amount.val(), account_manager.data['is_cof'], false)); }); } @@ -895,7 +780,7 @@ $(document).ready(function() { function updateBasketRel() { var basketrel_html = ''; - if (account_data['trigramme'] == 'LIQ' && !isBasketEmpty()) { + if (account_manager.data['trigramme'] == 'LIQ' && !isBasketEmpty()) { var amount = - getAmountBasket(); basketrel_html += '
      Total: '+amount.toFixed(2)+' €
      '; if (amount < 5) @@ -904,11 +789,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_data['trigramme'] != '' && !isBasketEmpty()) { + } else if (account_manager.data['trigramme'] != '' && !isBasketEmpty()) { var amount = getAmountBasket(); - var amountUKF = amountToUKF(amount, account_data['is_cof'], false); - var newBalance = account_data['balance'] + amount; - var newBalanceUKF = amountToUKF(newBalance, account_data['is_cof'], true); + 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); basketrel_html += '
      Total: '+amountUKF+'
      '; basketrel_html += '
      Nouveau solde: '+newBalanceUKF+'
      '; if (newBalance < 0) @@ -929,7 +814,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_data['is_cof']); + var amountUKF_after = amountToUKF(amountEuro_after, account_manager.data['is_cof']); if (type == 'purchase') { if (nb_after == 0) { @@ -1151,7 +1036,7 @@ $(document).ready(function() { function updatePreviousOp() { var previousop_html = ''; - var trigramme = account_data['trigramme']; + var trigramme = account_manager.data['trigramme']; previousop_html += '
      Trigramme : '+trigramme+'
      '; previousop_html += basketrel_container.html(); previousop_container.html(previousop_html); @@ -1293,7 +1178,7 @@ $(document).ready(function() { // Reset functions function coolReset(give_tri_focus=true) { - resetAccount(); + account_manager.reset(true); resetBasket(); resetComment(); resetSelectable(); @@ -1348,7 +1233,7 @@ $(document).ready(function() { case 113: if (e.shiftKey) { // Shift+F2 - Account reset - resetAccount(); + account_manager.reset(true); triInput.focus(); } else { // F2 - Basket reset @@ -1380,6 +1265,9 @@ $(document).ready(function() { } }); + // On exporte les fonctions nécessaires dans `window` + window.updateBasketAmount = updateBasketAmount; + window.updateBasketRel = updateBasketRel; // ----- // Initiliazing all // ----- From a984d1fd6f1579f28d5909b87b2df25c65e9b38e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 26 Jul 2020 21:40:28 +0200 Subject: [PATCH 391/573] Clarity --- kfet/static/kfet/js/account.js | 9 +++++---- kfet/static/kfet/js/kfet.js | 2 +- kfet/static/kfet/js/kpsul.js | 7 +++---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index ac98e1dd..a3f23e1c 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -17,7 +17,8 @@ var Account = Backbone.Model.extend({ }, reset: function () { - // On n'utilise pas .clear() car on ne veut pas clear le trigramme + // 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) }, @@ -30,7 +31,7 @@ var Account = Backbone.Model.extend({ }, view: function () { - if (!this.is_valid()) { + if (!this.is_empty_account()) { view_class = EmptyAccountView } else if (this.get("trigramme") == 'LIQ') { view_class = LIQView @@ -44,7 +45,7 @@ var Account = Backbone.Model.extend({ this.view().render(); }, - is_valid: function () { + is_empty_account: function () { return (this.id != 0) }, }) @@ -132,7 +133,7 @@ var EmptyAccountView = AccountView.extend({ le compte est invalide */ buttons = ''; trigramme = this.model.get("trigramme") - if (trigramme.is_valid_tri()) { + if (trigramme.is_valid_trigramme()) { trigramme = encodeURIComponent(trigramme); var url_base = django_urls["kfet.account.create"](); var url = `${url_base}?trigramme=${trigramme}`; diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 32493088..1ff3c583 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -6,7 +6,7 @@ String.prototype.format_trigramme = function () { return this.toUpperCase().substr(0, 3) } -String.prototype.is_valid_tri = function () { +String.prototype.is_valid_trigramme = function () { var pattern = /^[^a-z]{3}$/; return pattern.test(this); } diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index ae37939c..cb0b9fe8 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -27,8 +27,7 @@ class AccountManager { // Raccourci LIQ this._$input.on("keydown", function (e) { - // keycode 40: Down Arrow - if (e.keyCode == 40) { + if (e.key == "ArrowDown") { that.set("LIQ") } }) @@ -39,7 +38,7 @@ class AccountManager { }); this._$container.on('keydown', function (e) { - if (e.which == 70 && e.ctrlKey) { + if (e.key == "f" && e.ctrlKey) { // Ctrl + F : universal search shortcut that.search.open(); e.preventDefault(); @@ -55,7 +54,7 @@ class AccountManager { update() { var trigramme = this._$input.val().format_trigramme(); this.account.set({ "trigramme": trigramme }) - if (trigramme.is_valid_tri()) { + if (trigramme.is_valid_trigramme()) { this.account.fetch({ "success": this._on_success.bind(this), "error": this.reset.bind(this, false), From f6c83dc692e4a8d4ea3ab4ff63f141dc3f0d9555 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 27 Jul 2020 01:26:51 +0200 Subject: [PATCH 392/573] FINALLY fix this f***ing whitespace mess --- kfet/static/kfet/css/libs/jconfirm-kfet.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kfet/static/kfet/css/libs/jconfirm-kfet.css b/kfet/static/kfet/css/libs/jconfirm-kfet.css index a50e22d6..935d4e97 100644 --- a/kfet/static/kfet/css/libs/jconfirm-kfet.css +++ b/kfet/static/kfet/css/libs/jconfirm-kfet.css @@ -25,6 +25,9 @@ .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 { @@ -51,7 +54,6 @@ } .jconfirm .jconfirm-box .buttons { - margin-top:-6px; /* j'arrive pas à voir pk y'a un espace au dessus sinon... */ padding:0; height:40px; } From d62a8d61de6e8ccca3e14ce737a956d4e731e7ac Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 27 Jul 2020 01:32:05 +0200 Subject: [PATCH 393/573] Search fix and CSS update --- kfet/static/kfet/css/index.css | 12 ++++++++++++ kfet/static/kfet/js/kpsul.js | 9 +++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index f0eaedf0..94a89a74 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -194,6 +194,18 @@ /* 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); diff --git a/kfet/static/kfet/js/kpsul.js b/kfet/static/kfet/js/kpsul.js index cb0b9fe8..a1ac8d37 100644 --- a/kfet/static/kfet/js/kpsul.js +++ b/kfet/static/kfet/js/kpsul.js @@ -119,7 +119,7 @@ class AccountSearch { url: django_urls['kfet.account.search.autocomplete'](), minimumCharacters: 2, id: 'search_autocomplete', - choiceSelector: '.choice', + choiceSelector: '.autocomplete-value', placeholder: "Chercher un utilisateur K-Fêt", container: that._$results_container, box: that._$results_container, @@ -137,7 +137,12 @@ class AccountSearch { } _on_select(e, choice, autocomplete) { - this.manager.set(choice.find('.trigramme').text()); + // 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(); } From 339223bec0a01a832c96bcf66e86503d20e0ec85 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 4 May 2021 18:12:47 +0200 Subject: [PATCH 394/573] Black --- kfet/forms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/forms.py b/kfet/forms.py index b9adbc81..d728afb1 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -640,7 +640,7 @@ class StatScaleForm(forms.Form): class AccountStatForm(forms.Form): - """ Idem, mais pour la balance d'un compte """ + """Idem, mais pour la balance d'un compte""" begin_date = forms.DateTimeField(required=False) end_date = forms.DateTimeField(required=False) From 7171a7567cf5dff791d365a28f50f563fb36b7f2 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 4 May 2021 21:43:48 +0200 Subject: [PATCH 395/573] Remove double negative --- kfet/static/kfet/js/account.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/kfet/static/kfet/js/account.js b/kfet/static/kfet/js/account.js index a3f23e1c..07fd6688 100644 --- a/kfet/static/kfet/js/account.js +++ b/kfet/static/kfet/js/account.js @@ -31,7 +31,7 @@ var Account = Backbone.Model.extend({ }, view: function () { - if (!this.is_empty_account()) { + if (this.is_empty_account()) { view_class = EmptyAccountView } else if (this.get("trigramme") == 'LIQ') { view_class = LIQView @@ -46,7 +46,7 @@ var Account = Backbone.Model.extend({ }, is_empty_account: function () { - return (this.id != 0) + return (this.id == 0) }, }) From 71878caf2c044aeaa75d62b0b0a634736dd2cca3 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 5 May 2021 00:03:52 +0200 Subject: [PATCH 396/573] On modifie le curseur quand on survole un compte dans l'autocomplete --- kfet/static/kfet/css/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index 94a89a74..aade3665 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -219,6 +219,10 @@ width:100%; } +#account_results li.autocomplete-value { + cursor: pointer; +} + #account_results .hilight { background:rgba(200,16,46,0.9); color:#fff; From dba785bf1393f9428dd3fecd3509c79899c8a67b Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 5 May 2021 00:59:47 +0200 Subject: [PATCH 397/573] Pareil, mais dans gestiocof --- gestioncof/static/gestioncof/css/cof.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/static/gestioncof/css/cof.css b/gestioncof/static/gestioncof/css/cof.css index 3bbaa539..a158476e 100644 --- a/gestioncof/static/gestioncof/css/cof.css +++ b/gestioncof/static/gestioncof/css/cof.css @@ -861,7 +861,7 @@ input[type=number][readonly]::-webkit-outer-spin-button { color: #000; } -.yourlabs-autocomplete li.autocomplete-value { +.yourlabs-autocomplete li.autocomplete-value,li.autocomplete-new { cursor: pointer; } From 7d21a5a1fc327dcec920a6a27dbc2e0829eab380 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Wed, 5 May 2021 01:57:46 +0200 Subject: [PATCH 398/573] =?UTF-8?q?On=20supprime=20des=20s=C3=A9lecteurs?= =?UTF-8?q?=20inutiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/static/gestioncof/css/cof.css | 5 +++-- kfet/static/kfet/css/index.css | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/gestioncof/static/gestioncof/css/cof.css b/gestioncof/static/gestioncof/css/cof.css index a158476e..f98313a6 100644 --- a/gestioncof/static/gestioncof/css/cof.css +++ b/gestioncof/static/gestioncof/css/cof.css @@ -855,13 +855,14 @@ input[type=number][readonly]::-webkit-outer-spin-button { font-weight: bold; } -.yourlabs-autocomplete li.autocomplete-header { +li.autocomplete-header { background-color: #FFEF9E; padding: 3px 5px; color: #000; } -.yourlabs-autocomplete li.autocomplete-value,li.autocomplete-new { +li.autocomplete-value, +li.autocomplete-new { cursor: pointer; } diff --git a/kfet/static/kfet/css/index.css b/kfet/static/kfet/css/index.css index aade3665..d81a5074 100644 --- a/kfet/static/kfet/css/index.css +++ b/kfet/static/kfet/css/index.css @@ -219,7 +219,7 @@ width:100%; } -#account_results li.autocomplete-value { +li.autocomplete-value { cursor: pointer; } From 0351f6728b9b173faebacd814fa00640e2c1dd82 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 5 May 2021 02:10:44 +0200 Subject: [PATCH 399/573] CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 684f72f1..7a56e649 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,14 @@ Liste des changements notables dans GestioCOF depuis la version 0.1 (septembre Uniquement un modèle simple de clubs avec des respos. Aucune gestion des adhérents ni des cotisations. +## Version 0.11 + +### K-Fêt + +- 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 + + ## Version 0.10 - 18/04/2021 ### TODO Prod From 99809209e098c56d3822368cecfa8bd71eb0a13a Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 23 Feb 2021 23:54:17 +0100 Subject: [PATCH 400/573] =?UTF-8?q?Change=20les=20permissions=20pour=20gel?= =?UTF-8?q?er/d=C3=A9geler=20un=20compte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/forms.py | 6 ++++++ kfet/templates/kfet/account_update.html | 1 + kfet/views.py | 8 ++++++-- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index d728afb1..418d0f0f 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -119,6 +119,12 @@ class AccountPwdForm(forms.Form): return self.account +class AccountFrozenForm(forms.ModelForm): + class Meta: + model = Account + fields = ["is_frozen"] + + class CofForm(forms.ModelForm): def clean_is_cof(self): instance = getattr(self, "instance", None) diff --git a/kfet/templates/kfet/account_update.html b/kfet/templates/kfet/account_update.html index dcb55555..2bab6c1d 100644 --- a/kfet/templates/kfet/account_update.html +++ b/kfet/templates/kfet/account_update.html @@ -32,6 +32,7 @@ Modification de mes informations {% 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=pwd_form %} {% include 'kfet/form_snippet.html' with form=negative_form %} diff --git a/kfet/views.py b/kfet/views.py index 0423be07..2d552d3c 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -38,6 +38,7 @@ from kfet.config import kfet_config from kfet.decorators import teamkfet_required from kfet.forms import ( AccountForm, + AccountFrozenForm, AccountNegativeForm, AccountNoTriForm, AccountPwdForm, @@ -349,10 +350,11 @@ def account_update(request, trigramme): return HttpResponseForbidden user_info_form = UserInfoForm(instance=account.user) - - group_form = UserGroupForm(instance=account.user) account_form = AccountForm(instance=account) + group_form = UserGroupForm(instance=account.user) + frozen_form = AccountFrozenForm(request.POST, instance=account) pwd_form = AccountPwdForm() + if hasattr(account, "negative"): negative_form = AccountNegativeForm(instance=account.negative) else: @@ -362,6 +364,7 @@ def account_update(request, trigramme): 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) pwd_form = AccountPwdForm(request.POST, account=account) forms = [] @@ -374,6 +377,7 @@ def account_update(request, trigramme): 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") From 4136cb68681dd73d697fd65539edee2216c5fa09 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 24 Feb 2021 00:23:25 +0100 Subject: [PATCH 401/573] Unfreeze every account --- kfet/migrations/0076_unfreeze_accounts.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 kfet/migrations/0076_unfreeze_accounts.py diff --git a/kfet/migrations/0076_unfreeze_accounts.py b/kfet/migrations/0076_unfreeze_accounts.py new file mode 100644 index 00000000..23901d99 --- /dev/null +++ b/kfet/migrations/0076_unfreeze_accounts.py @@ -0,0 +1,17 @@ +# 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)] From 1e44550e12bda5327a0aceb7e69c9865aae8dbfb Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 24 Feb 2021 00:25:48 +0100 Subject: [PATCH 402/573] New frozen function --- kfet/decorators.py | 3 +++ kfet/views.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/kfet/decorators.py b/kfet/decorators.py index 70848820..a01e867d 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -2,6 +2,9 @@ from django.contrib.auth.decorators import user_passes_test def kfet_is_team(user): + if hasattr(user.profile, "account_kfet") and user.profile.account_kfet.is_frozen: + return False + return user.has_perm("kfet.is_team") diff --git a/kfet/views.py b/kfet/views.py index 2d552d3c..5f44dd76 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1055,6 +1055,9 @@ def kpsul_perform_operations(request): ) need_comment = operationgroup.on_acc.need_comment + if operationgroup.on_acc.is_frozen: + data["errors"]["frozen"] = [operationgroup.on_acc.trigramme] + # Filling data of each operations # + operationgroup + calculating other stuffs for operation in operations: @@ -1676,7 +1679,11 @@ def perform_transfers(request): 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] ) @@ -1685,6 +1692,11 @@ def perform_transfers(request): if stop: negative_accounts.append(account.trigramme) + print(frozen, len(frozen)) + if len(frozen): + data["errors"]["frozen"] = list(frozen) + return JsonResponse(data, status=400) + if stop_all or not request.user.has_perms(required_perms): missing_perms = get_missing_perms(required_perms, request.user) if missing_perms: From 63738e8e02029b1ef17ef5f78b7e729c6522576d Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 24 Feb 2021 00:26:04 +0100 Subject: [PATCH 403/573] Frozen error display --- kfet/static/kfet/js/kfet.js | 17 +++++++++++++++++ kfet/templates/kfet/kpsul.html | 15 --------------- kfet/templates/kfet/transfers_create.html | 3 +++ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 1ff3c583..2030304f 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -150,6 +150,12 @@ function getErrorsHtml(data) { content += '
    • Opération invalide sur le compte ' + data['errors']['account'] + '
    • '; content += '
    '; } + if ('frozen' in data['errors']) { + content += 'Général'; + content += '
      '; + content += '
    • Les comptes suivants sont gelés : ' + data['errors']['frozen'].join(", ") + '
    • '; + content += '
    '; + } return content; } @@ -206,6 +212,17 @@ function requestAuth(data, callback, focus_next = null) { }); } +function displayErrors(html) { + $.alert({ + title: 'Erreurs', + content: html, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + }); +} + /** * Setup jquery-confirm diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 03ade3c6..ece98578 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -340,21 +340,6 @@ $(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 // ----- diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index e4fae405..52505f00 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -121,6 +121,9 @@ $(document).ready(function () { case 403: requestAuth(data, performTransfers); break; + case 400: + displayErrors(getErrorsHtml(data)); + break; } }); } From 93d283fecb105c86e6c259a916375fd9cf7db3de Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 24 Feb 2021 00:26:16 +0100 Subject: [PATCH 404/573] Remove unused permission --- .../0077_delete_frozen_permission.py | 30 +++++++++++++++++++ kfet/models.py | 4 --- 2 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 kfet/migrations/0077_delete_frozen_permission.py diff --git a/kfet/migrations/0077_delete_frozen_permission.py b/kfet/migrations/0077_delete_frozen_permission.py new file mode 100644 index 00000000..8ac297fa --- /dev/null +++ b/kfet/migrations/0077_delete_frozen_permission.py @@ -0,0 +1,30 @@ +# 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/models.py b/kfet/models.py index 7156ae52..628e5de6 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -180,9 +180,6 @@ class Account(models.Model): 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 < 0 and amount < 0: # Retrieving overdraft amount limit @@ -726,7 +723,6 @@ class Operation(models.Model): permissions = ( ("perform_deposit", "Effectuer une charge"), ("perform_negative_operations", "Enregistrer des commandes en négatif"), - ("override_frozen_protection", "Forcer le gel d'un compte"), ("cancel_old_operations", "Annuler des commandes non récentes"), ( "perform_commented_operations", From a947b9d3f28cc5c01419c0a6c739e52e6756221c Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 24 Feb 2021 00:28:17 +0100 Subject: [PATCH 405/573] Fix decorator --- kfet/decorators.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kfet/decorators.py b/kfet/decorators.py index a01e867d..0db0c2e1 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -2,7 +2,11 @@ from django.contrib.auth.decorators import user_passes_test def kfet_is_team(user): - if hasattr(user.profile, "account_kfet") and user.profile.account_kfet.is_frozen: + if ( + hasattr(user, "profile") + and hasattr(user.profile, "account_kfet") + and user.profile.account_kfet.is_frozen + ): return False return user.has_perm("kfet.is_team") From 16dee0c143506b1077d136b4f6f489856d8e17f9 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 24 Feb 2021 00:28:28 +0100 Subject: [PATCH 406/573] Remove print --- kfet/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/kfet/views.py b/kfet/views.py index 5f44dd76..c99629be 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1692,7 +1692,6 @@ def perform_transfers(request): if stop: negative_accounts.append(account.trigramme) - print(frozen, len(frozen)) if len(frozen): data["errors"]["frozen"] = list(frozen) return JsonResponse(data, status=400) From b9aaf6a19c1bfc9b7b5e3058dbbbc776e7ec8302 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 24 Feb 2021 00:31:56 +0100 Subject: [PATCH 407/573] Fix test --- kfet/tests/test_views.py | 28 +++------------------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index bc50b023..c4d31ae2 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -1926,8 +1926,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ["[kfet] Enregistrer des commandes avec commentaires"], ) - def test_group_on_acc_frozen(self): - user_add_perms(self.users["team"], ["kfet.override_frozen_protection"]) + def test_error_on_acc_frozen(self): self.account.is_frozen = True self.account.save() @@ -1944,30 +1943,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) resp = self.client.post(self.url, data) - 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) + self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["missing_perms"], ["[kfet] Forcer le gel d'un compte"] - ) + self.assertEqual(json_data["errors"]["frozen"], [self.account.trigramme]) def test_invalid_group_checkout(self): self.checkout.valid_from -= timedelta(days=300) From 7bf0c5f09e13b58dc8d474d4294b2589b309c424 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Wed, 17 Mar 2021 21:01:55 +0100 Subject: [PATCH 408/573] Fix frozen forms --- kfet/forms.py | 2 +- kfet/views.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/kfet/forms.py b/kfet/forms.py index 418d0f0f..e0d32102 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -61,7 +61,7 @@ class AccountForm(forms.ModelForm): class Meta: model = Account - fields = ["trigramme", "promo", "nickname", "is_frozen"] + fields = ["trigramme", "promo", "nickname"] widgets = {"trigramme": forms.TextInput(attrs={"autocomplete": "off"})} diff --git a/kfet/views.py b/kfet/views.py index c99629be..83bf380a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -435,6 +435,7 @@ 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, From 02584982f6174654d713aea26777fe66d6acde0a Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 15 Jun 2021 14:48:35 +0200 Subject: [PATCH 409/573] gnagnagna --- kfet/templates/kfet/transfers_create.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index 52505f00..a4a1a450 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -121,7 +121,7 @@ $(document).ready(function () { case 403: requestAuth(data, performTransfers); break; - case 400: + case 400: displayErrors(getErrorsHtml(data)); break; } From a34b83c23671d9ed0271c941cb6ac524782ad698 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 15 Jun 2021 16:52:50 +0200 Subject: [PATCH 410/573] Use backend to enforce frozen accounts --- gestioasso/settings/cof_prod.py | 16 +++++++++++----- kfet/auth/backends.py | 34 +++++++++++++++++++++++++++++++++ kfet/decorators.py | 7 ------- 3 files changed, 45 insertions(+), 12 deletions(-) diff --git a/gestioasso/settings/cof_prod.py b/gestioasso/settings/cof_prod.py index 28133ebc..b8b1c0ff 100644 --- a/gestioasso/settings/cof_prod.py +++ b/gestioasso/settings/cof_prod.py @@ -111,11 +111,17 @@ CORS_ORIGIN_WHITELIST = ("bda.ens.fr", "www.bda.ens.fr" "cof.ens.fr", "www.cof.e # Auth-related stuff # --- -AUTHENTICATION_BACKENDS += [ - "gestioncof.shared.COFCASBackend", - "kfet.auth.backends.GenericBackend", -] - +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" diff --git a/kfet/auth/backends.py b/kfet/auth/backends.py index 55e18458..0f7789a1 100644 --- a/kfet/auth/backends.py +++ b/kfet/auth/backends.py @@ -1,4 +1,5 @@ from django.contrib.auth import get_user_model +from django.core.exceptions import PermissionDenied from kfet.models import Account, GenericTeamToken @@ -37,3 +38,36 @@ 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/decorators.py b/kfet/decorators.py index 0db0c2e1..70848820 100644 --- a/kfet/decorators.py +++ b/kfet/decorators.py @@ -2,13 +2,6 @@ from django.contrib.auth.decorators import user_passes_test def kfet_is_team(user): - if ( - hasattr(user, "profile") - and hasattr(user.profile, "account_kfet") - and user.profile.account_kfet.is_frozen - ): - return False - return user.has_perm("kfet.is_team") From 6a111395885ff31b8cce0c7d0b418cfe7c69f176 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 15 Jun 2021 16:52:56 +0200 Subject: [PATCH 411/573] Fix tests --- bda/tests/test_views.py | 4 +++- bds/tests/test_views.py | 8 +++++--- events/tests/test_views.py | 10 +++++++--- gestioncof/tests/test_views.py | 4 ++-- kfet/open/tests.py | 6 ++++-- kfet/tests/testcases.py | 5 ++++- petitscours/tests/test_views.py | 4 +++- 7 files changed, 28 insertions(+), 13 deletions(-) diff --git a/bda/tests/test_views.py b/bda/tests/test_views.py index 75d01ec9..47cbd2bd 100644 --- a/bda/tests/test_views.py +++ b/bda/tests/test_views.py @@ -356,7 +356,9 @@ class TestReventeManageTest(TestCase): def test_can_get(self): client = Client() - client.force_login(self.user) + client.force_login( + self.user, backend="django.contrib.auth.backends.ModelBackend" + ) r = client.get(self.url) self.assertEqual(r.status_code, 200) diff --git a/bds/tests/test_views.py b/bds/tests/test_views.py index a40d3d85..ef6139f4 100644 --- a/bds/tests/test_views.py +++ b/bds/tests/test_views.py @@ -27,7 +27,9 @@ class TestHomeView(TestCase): def test_get(self, mock_messages): user = User.objects.create_user(username="random_user") give_bds_buro_permissions(user) - self.client.force_login(user) + self.client.force_login( + user, backend="django.contrib.auth.backends.ModelBackend" + ) resp = self.client.get(reverse("bds:home")) self.assertEquals(resp.status_code, 200) @@ -44,7 +46,7 @@ class TestRegistrationView(TestCase): self.assertRedirects(resp, login_url(next=url)) # Logged-in but unprivileged GET - client.force_login(user) + client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") resp = client.get(url) self.assertEquals(resp.status_code, 403) @@ -64,7 +66,7 @@ class TestRegistrationView(TestCase): self.assertRedirects(resp, login_url(next=url)) # Logged-in but unprivileged GET - client.force_login(user) + client.force_login(user, backend="django.contrib.auth.backends.ModelBackend") resp = client.get(url) self.assertEquals(resp.status_code, 403) diff --git a/events/tests/test_views.py b/events/tests/test_views.py index ef3eda31..611f1871 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -54,7 +54,9 @@ class CSVExportAccessTest(MessagePatch, TestCase): def test_get(self): client = Client() - client.force_login(self.staff) + client.force_login( + self.staff, backend="django.contrib.auth.backends.ModelBackend" + ) r = client.get(self.url) self.assertEqual(r.status_code, 200) @@ -66,7 +68,7 @@ class CSVExportAccessTest(MessagePatch, TestCase): def test_unauthorised(self): client = Client() - client.force_login(self.u1) + client.force_login(self.u1, backend="django.contrib.auth.backends.ModelBackend") r = client.get(self.url) self.assertEqual(r.status_code, 403) @@ -86,7 +88,9 @@ class CSVExportContentTest(MessagePatch, CSVResponseMixin, TestCase): ) self.staff = make_staff_user("staff") self.client = Client() - self.client.force_login(self.staff) + 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]) diff --git a/gestioncof/tests/test_views.py b/gestioncof/tests/test_views.py index dc9b8df0..ecbb20f6 100644 --- a/gestioncof/tests/test_views.py +++ b/gestioncof/tests/test_views.py @@ -641,7 +641,7 @@ class ClubListViewTests(ViewTestCaseMixin, TestCase): def test_as_staff(self): u = self.users["staff"] c = Client() - c.force_login(u) + c.force_login(u, backend="django.contrib.auth.backends.ModelBackend") r = c.get(self.url) @@ -686,7 +686,7 @@ class ClubMembersViewTests(ViewTestCaseMixin, TestCase): self.c.respos.add(u) c = Client() - c.force_login(u) + c.force_login(u, backend="django.contrib.auth.backends.ModelBackend") r = c.get(self.url) self.assertEqual(r.status_code, 200) diff --git a/kfet/open/tests.py b/kfet/open/tests.py index 0d527644..455f2cef 100644 --- a/kfet/open/tests.py +++ b/kfet/open/tests.py @@ -211,7 +211,7 @@ class OpenKfetConsumerTest(ChannelTestCase): ) t.user_permissions.add(is_team) c = WSClient() - c.force_login(t) + c.force_login(t, backend="django.contrib.auth.backends.ModelBackend") # connect c.send_and_consume( @@ -251,7 +251,9 @@ class OpenKfetScenarioTest(ChannelTestCase): 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.r_c_ws.force_login( + self.r, backend="django.contrib.auth.backends.ModelBackend" + ) self.kfet_open = OpenKfet( cache_prefix="test_kfetopen_%s" % random.randrange(2 ** 20) diff --git a/kfet/tests/testcases.py b/kfet/tests/testcases.py index 16ccb186..a7962f33 100644 --- a/kfet/tests/testcases.py +++ b/kfet/tests/testcases.py @@ -253,7 +253,10 @@ class ViewTestCaseMixin(TestCaseMixin): self.register_user(label, user) if self.auth_user: - self.client.force_login(self.users[self.auth_user]) + self.client.force_login( + self.users[self.auth_user], + backend="django.contrib.auth.backends.ModelBackend", + ) def tearDown(self): del self.users_base diff --git a/petitscours/tests/test_views.py b/petitscours/tests/test_views.py index 6ca97086..9367c258 100644 --- a/petitscours/tests/test_views.py +++ b/petitscours/tests/test_views.py @@ -77,7 +77,9 @@ 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"]) + self.client.force_login( + self.users["user"], backend="django.contrib.auth.backends.ModelBackend" + ) resp = self.client.get(self.url) self.assertRedirects(resp, reverse("cof-denied")) From ef8c1b8bf2377353645a9d7b57b50007764a9baa Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 01:59:43 +0100 Subject: [PATCH 412/573] =?UTF-8?q?Nouveau=20fonctionnement=20des=20n?= =?UTF-8?q?=C3=A9gatifs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kfet/forms.py | 12 ----- kfet/models.py | 66 ++++++++--------------- kfet/templates/kfet/account_negative.html | 26 ++------- kfet/templates/kfet/account_update.html | 16 +----- kfet/views.py | 23 ++------ 5 files changed, 30 insertions(+), 113 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index e0d32102..4f91680f 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -14,7 +14,6 @@ from djconfig.forms import ConfigForm from gestioncof.models import CofProfile from kfet.models import ( Account, - AccountNegative, Article, ArticleCategory, Checkout, @@ -158,17 +157,6 @@ class UserInfoForm(UserForm): fields = ["first_name", "last_name"] -class AccountNegativeForm(forms.ModelForm): - class Meta: - model = AccountNegative - fields = [ - "authz_overdraft_amount", - "authz_overdraft_until", - "comment", - ] - widgets = {"authz_overdraft_until": DateTimeWidget()} - - # ----- # Checkout forms # ----- diff --git a/kfet/models.py b/kfet/models.py index 628e5de6..887d3701 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -170,41 +170,23 @@ 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") + 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, stop_ope + + return perms, False # Surcharge Méthode save() avec gestions de User et CofProfile # Args: @@ -267,17 +249,26 @@ class Account(models.Model): def update_negative(self): if self.balance < 0: - if hasattr(self, "negative") and not self.negative.start: + # 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 + ): 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"): - # self.balance >= 0 - # TODO: méchanisme pour éviter de contourner le délai de négatif ? - self.negative.delete() + if self.negative.end is None: + self.negative.end = timezone.now() + elif timezone.now() > self.negative.end + kfet_config.cancel_duration: + # Idem: on supprime le négatif après une légère période + self.negative.delete() class UserHasAccount(Exception): def __init__(self, trigramme): @@ -302,26 +293,11 @@ class AccountNegative(models.Model): Account, on_delete=models.CASCADE, related_name="negative" ) start = models.DateTimeField(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) + end = models.DateTimeField(blank=True, null=True, default=None) class Meta: permissions = (("view_negs", "Voir la liste des négatifs"),) - @property - def until_default(self): - return self.start + kfet_config.overdraft_duration - class CheckoutQuerySet(models.QuerySet): def is_valid(self): diff --git a/kfet/templates/kfet/account_negative.html b/kfet/templates/kfet/account_negative.html index 9ca9cd99..c2390f6d 100644 --- a/kfet/templates/kfet/account_negative.html +++ b/kfet/templates/kfet/account_negative.html @@ -10,26 +10,12 @@ {{ negatives|length }} compte{{ negatives|length|pluralize }} en négatif -
    - Total: {{ negatives_sum|floatformat:2 }}€ -
    -
    - Plafond par défaut -
      -
    • Montant: {{ kfet_config.overdraft_amount }}€
    • -
    • Pendant: {{ kfet_config.overdraft_duration }}
    • -
    +
    + {{ negatives_sum|floatformat:2 }}€ + de négatif total
    -{% if perms.kfet.change_settings %} -
    -
    -
    -
    -{% endif %} - {% endblock %} {% block main %} @@ -43,8 +29,6 @@ Nom Balance Début - Découvert autorisé - Jusqu'au @@ -60,10 +44,6 @@ {{ 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' }} - {% endfor %} diff --git a/kfet/templates/kfet/account_update.html b/kfet/templates/kfet/account_update.html index 2bab6c1d..65965d83 100644 --- a/kfet/templates/kfet/account_update.html +++ b/kfet/templates/kfet/account_update.html @@ -35,23 +35,9 @@ Modification de mes informations {% 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=pwd_form %} - {% 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 %} \ No newline at end of file diff --git a/kfet/views.py b/kfet/views.py index 83bf380a..76643809 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -39,7 +39,6 @@ from kfet.decorators import teamkfet_required from kfet.forms import ( AccountForm, AccountFrozenForm, - AccountNegativeForm, AccountNoTriForm, AccountPwdForm, AccountStatForm, @@ -355,11 +354,6 @@ def account_update(request, trigramme): frozen_form = AccountFrozenForm(request.POST, instance=account) pwd_form = AccountPwdForm() - if hasattr(account, "negative"): - negative_form = AccountNegativeForm(instance=account.negative) - else: - negative_form = None - if request.method == "POST": self_update = request.user == account.user account_form = AccountForm(request.POST, instance=account) @@ -381,14 +375,6 @@ def account_update(request, trigramme): elif group_form.has_changed(): warnings.append("statut d'équipe") - if hasattr(account, "negative"): - negative_form = AccountNegativeForm(request.POST, instance=account.negative) - - if request.user.has_perm("kfet.change_accountnegative"): - forms.append(negative_form) - elif negative_form.has_changed(): - warnings.append("négatifs") - # 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"): @@ -437,7 +423,6 @@ def account_update(request, trigramme): "account_form": account_form, "frozen_form": frozen_form, "group_form": group_form, - "negative_form": negative_form, "pwd_form": pwd_form, }, ) @@ -482,9 +467,11 @@ class AccountDelete(PermissionRequiredMixin, DeleteView): class AccountNegativeList(ListView): - queryset = AccountNegative.objects.select_related( - "account", "account__cofprofile__user" - ).exclude(account__trigramme="#13") + queryset = ( + AccountNegative.objects.select_related("account", "account__cofprofile__user") + .filter(account__balance__lt=0) + .exclude(account__trigramme="#13") + ) template_name = "kfet/account_negative.html" context_object_name = "negatives" From 348881d207b5db38e53bfbf374fee1a8c542b36c Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:01:18 +0100 Subject: [PATCH 413/573] Migration --- kfet/migrations/0078_negative_end.py | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 kfet/migrations/0078_negative_end.py diff --git a/kfet/migrations/0078_negative_end.py b/kfet/migrations/0078_negative_end.py new file mode 100644 index 00000000..121a975e --- /dev/null +++ b/kfet/migrations/0078_negative_end.py @@ -0,0 +1,30 @@ +# 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), + ), + ] From 1939a54fef8637d8e20b36e4b0217da94de7fc76 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:01:45 +0100 Subject: [PATCH 414/573] Tests du nouveau comportement --- kfet/tests/test_models.py | 58 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/kfet/tests/test_models.py b/kfet/tests/test_models.py index 7ce6605c..a534493d 100644 --- a/kfet/tests/test_models.py +++ b/kfet/tests/test_models.py @@ -1,10 +1,12 @@ -import datetime +from datetime import datetime, timedelta, timezone as tz +from decimal import Decimal +from unittest import mock from django.contrib.auth import get_user_model from django.test import TestCase from django.utils import timezone -from kfet.models import Account, Checkout +from kfet.models import Account, AccountNegative, Checkout from .utils import create_user @@ -28,6 +30,56 @@ 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): @@ -39,7 +91,7 @@ class CheckoutTests(TestCase): self.c = Checkout( created_by=self.u_acc, valid_from=self.now, - valid_to=self.now + datetime.timedelta(days=1), + valid_to=self.now + timedelta(days=1), ) def test_initial_statement(self): From 29236e0b0e15cb225b5ade11f3d7dae3dafb01d2 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:02:31 +0100 Subject: [PATCH 415/573] Nouvelle gestion des erreurs JSON --- kfet/forms.py | 2 +- kfet/views.py | 275 +++++++++++++++++++++++++++++++++----------------- 2 files changed, 182 insertions(+), 95 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index 4f91680f..5cc9d83f 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -537,7 +537,7 @@ class TransferForm(forms.ModelForm): def clean_amount(self): amount = self.cleaned_data["amount"] if amount <= 0: - raise forms.ValidationError("Montant invalide") + raise forms.ValidationError("Le montant d'un transfert doit être positif") return amount class Meta: diff --git a/kfet/views.py b/kfet/views.py index 76643809..e403505a 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -15,7 +15,7 @@ from django.contrib.messages.views import SuccessMessageMixin from django.core.exceptions import SuspiciousOperation, ValidationError from django.db import transaction from django.db.models import Count, F, Max, OuterRef, Prefetch, Q, Subquery, Sum -from django.forms import formset_factory +from django.forms import ValidationError, formset_factory from django.http import ( Http404, HttpResponseBadRequest, @@ -964,15 +964,18 @@ 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(): - data = {"errors": {"addcost": list(addcost_form.errors)}} + for (field, errors) in addcost_form.errors.items(): + for error in errors: + data["errors"].append({"code": f"invalid_{field}", "message": error}) + return JsonResponse(data, status=400) + required_perms = ["kfet.manage_addcosts"] if not request.user.has_perms(required_perms): - data = { - "errors": {"missing_perms": get_missing_perms(required_perms, request.user)} - } + data["missing_perms"] = get_missing_perms(required_perms, request.user) return JsonResponse(data, status=403) trigramme = addcost_form.cleaned_data["trigramme"] @@ -987,14 +990,13 @@ def kpsul_update_addcost(request): def get_missing_perms(required_perms: List[str], user: User) -> List[str]: - def get_perm_description(app_label: str, codename: str) -> str: - name = Permission.objects.values_list("name", flat=True).get( + def get_perm_name(app_label: str, codename: str) -> str: + return 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_description(*perm.split(".")) + get_perm_name(*perm.split(".")) for perm in required_perms if not user.has_perm(perm) ] @@ -1006,17 +1008,31 @@ def get_missing_perms(required_perms: List[str], user: User) -> List[str]: @kfet_password_auth def kpsul_perform_operations(request): # Initializing response data - data = {"operationgroup": 0, "operations": [], "warnings": {}, "errors": {}} + data = {"errors": []} # Checking operationgroup operationgroup_form = KPsulOperationGroupForm(request.POST) if not operationgroup_form.is_valid(): - data["errors"]["operation_group"] = list(operationgroup_form.errors) + 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}", + } + ) # Checking operation_formset operation_formset = KPsulOperationFormSet(request.POST) if not operation_formset.is_valid(): - data["errors"]["operations"] = list(operation_formset.errors) + data["errors"].append( + { + "code": "invalid_formset", + "message": "Formulaire d'opérations vide ou invalide", + } + ) # Returning BAD REQUEST if errors if data["errors"]: @@ -1025,6 +1041,7 @@ 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 @@ -1038,13 +1055,13 @@ 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 != operationgroup.on_acc) - ) - need_comment = operationgroup.on_acc.need_comment + is_addcost = all((addcost_for, addcost_amount, addcost_for != on_acc)) + need_comment = on_acc.need_comment - if operationgroup.on_acc.is_frozen: - data["errors"]["frozen"] = [operationgroup.on_acc.trigramme] + if on_acc.is_frozen: + data["errors"].append( + {"code": "frozen_acc", "message": f"Le compte {on_acc.trigramme} est gelé"} + ) # Filling data of each operations # + operationgroup + calculating other stuffs @@ -1056,19 +1073,23 @@ 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 operationgroup.on_acc.is_cash: + if on_acc.is_cash: to_checkout_balance += -operation.amount - if ( - operationgroup.on_acc.is_cof - and operation.article.category.has_reduction - ): + if 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 to_articles_stocks[operation.article] -= operation.article_nb else: - if operationgroup.on_acc.is_cash: - data["errors"]["account"] = "LIQ" + if on_acc.is_cash: + data["errors"].append( + { + "code": "invalid_liq", + "message": ( + "Impossible de compter autre chose que des achats sur LIQ" + ), + } + ) if operation.type != Operation.EDIT: to_checkout_balance += operation.amount operationgroup.amount += operation.amount @@ -1077,41 +1098,42 @@ def kpsul_perform_operations(request): if operation.type == Operation.EDIT: required_perms.add("kfet.edit_balance_account") need_comment = True - if operationgroup.on_acc.is_cof: + if account.is_cof: to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor - (perms, stop) = operationgroup.on_acc.perms_to_perform_operation( - amount=operationgroup.amount - ) + (perms, stop) = account.perms_to_perform_operation(amount=operationgroup.amount) required_perms |= perms + if stop: + data["errors"].append( + { + "code": "negative", + "message": f"Le compte {account.trigramme} a un solde insuffisant.", + } + ) + if need_comment: operationgroup.comment = operationgroup.comment.strip() if not operationgroup.comment: - data["errors"]["need_comment"] = True + data["need_comment"] = True - if data["errors"]: + if data["errors"] or "need_comment" in data: return JsonResponse(data, status=400) - 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] + if not request.user.has_perms(required_perms): + data["missing_perms"] = get_missing_perms(required_perms, request.user) 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 = operationgroup.on_acc.is_cof + operationgroup.is_cof = 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( @@ -1135,13 +1157,10 @@ 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: @@ -1164,7 +1183,7 @@ def kpsul_perform_operations(request): "valid_by__trigramme": ( operationgroup.valid_by and operationgroup.valid_by.trigramme or None ), - "on_acc__trigramme": operationgroup.on_acc.trigramme, + "on_acc__trigramme": on_acc.trigramme, "entries": [], } ] @@ -1205,7 +1224,7 @@ def kpsul_perform_operations(request): @kfet_password_auth def 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: @@ -1214,29 +1233,41 @@ 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"]["opes_notexisting"] = opes_notexisting + data["errors"].append( + { + "code": "cancel_missing", + "message": "Opérations inexistantes : {}".format( + ", ".join(map(str, 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 - 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 + + # 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) + for ope in opes_all: if ope.canceled_at: # Opération déjà annulée, va pour un warning en Response @@ -1307,16 +1338,22 @@ 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 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 + 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) return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None @@ -1644,12 +1681,36 @@ def transfers_create(request): @teamkfet_required @kfet_password_auth def perform_transfers(request): - data = {"errors": {}, "transfers": [], "transfergroup": 0} + data = {"errors": []} # Checking transfer_formset transfer_formset = TransferFormSet(request.POST) - if not transfer_formset.is_valid(): - return JsonResponse({"errors": list(transfer_formset.errors)}, status=400) + 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) transfers = transfer_formset.save(commit=False) @@ -1657,14 +1718,12 @@ def perform_transfers(request): required_perms = set( ["kfet.add_transfer"] ) # Required perms to perform all transfers - to_accounts_balances = defaultdict(lambda: 0) # For balances of accounts + to_accounts_balances = defaultdict(int) # 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() @@ -1676,20 +1735,34 @@ def perform_transfers(request): amount=to_accounts_balances[account] ) required_perms |= perms - stop_all = stop_all or stop if stop: negative_accounts.append(account.trigramme) - if len(frozen): - data["errors"]["frozen"] = list(frozen) + 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 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 + if not request.user.has_perms(required_perms): + data["missing_perms"] = get_missing_perms(required_perms, request.user) return JsonResponse(data, status=403) # Creating transfer group @@ -1711,22 +1784,20 @@ def perform_transfers(request): # 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(data) + return JsonResponse({}) @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: @@ -1735,7 +1806,11 @@ 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) @@ -1744,17 +1819,23 @@ def cancel_transfers(request): transfer for transfer in transfers_post if transfer not in transfers_pk ] if transfers_notexisting: - data["errors"]["transfers_notexisting"] = transfers_notexisting + data["errors"].append( + { + "code": "cancel_missing", + "message": "Transferts inexistants : {}".format( + ", ".join(map(str, transfers_notexisting)) + ), + } + ) return JsonResponse(data, status=400) - transfers_already_canceled = [] # Déjà annulée - transfers = [] # Pas déjà annulée + transfers_already_canceled = [] # Déjà annulés + transfers = [] # Pas déjà annulés required_perms = set() - stop_all = False cancel_duration = kfet_config.cancel_duration - to_accounts_balances = defaultdict( - lambda: 0 - ) # Modifs à faire sur les balances des comptes + + # Modifs à faire sur les balances des comptes + to_accounts_balances = defaultdict(int) for transfer in transfers_all: if transfer.canceled_at: # Transfert déjà annulé, va pour un warning en Response @@ -1782,16 +1863,22 @@ 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 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 + 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) return JsonResponse(data, status=403) canceled_by = required_perms and request.user.profile.account_kfet or None From 964eec6ab15af15e46e8ab534395170b0afda33e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:03:56 +0100 Subject: [PATCH 416/573] Adapte le JS aux nouvelles erreurs --- kfet/static/kfet/js/history.js | 4 +- kfet/static/kfet/js/kfet.js | 161 +++++++++------------- kfet/templates/kfet/kpsul.html | 6 +- kfet/templates/kfet/transfers_create.html | 4 +- 4 files changed, 70 insertions(+), 105 deletions(-) diff --git a/kfet/static/kfet/js/history.js b/kfet/static/kfet/js/history.js index 06b10d17..4c2a2664 100644 --- a/kfet/static/kfet/js/history.js +++ b/kfet/static/kfet/js/history.js @@ -257,11 +257,11 @@ function KHistory(options = {}) { switch ($xhr.status) { case 403: requestAuth(data, function (password) { - this.cancel(opes, password); + that._cancel(type, opes, password); }); break; case 400: - displayErrors(getErrorsHtml(data)); + displayErrors(data); break; } window.lock = 0; diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 2030304f..173c8ee8 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -106,116 +106,81 @@ function amountToUKF(amount, is_cof = false, account = false) { return rounding(amount * coef_cof * 10); } -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 += '
    '; +function getErrorsHtml(data, is_error = true) { + console.log("data") + if (is_error) { + data = data.map(error => error.message) } - if ('missing_perms' in data['errors']) { - content += 'Permissions manquantes'; - content += '
      '; - for (var i = 0; i < data['errors']['missing_perms'].length; i++) - content += '
    • ' + data['errors']['missing_perms'][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; i < data['errors']['negative'].length; i++) { - content += 'Autorisation 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 += '
    '; - } - if ('frozen' in data['errors']) { - content += 'Général'; - content += '
      '; - content += '
    • Les comptes suivants sont gelés : ' + data['errors']['frozen'].join(", ") + '
    • '; - content += '
    '; + + var content = is_error ? "Général :" : "Permissions manquantes :"; + content += "
      "; + for (const message of data) { + content += '
    • ' + message + '
    • '; } + content += "
    "; + return content; } function requestAuth(data, callback, focus_next = null) { - var content = getErrorsHtml(data); - content += '
    ', - $.confirm({ - title: 'Authentification requise', - content: content, - backgroundDismiss: true, - animation: 'top', - closeAnimation: 'bottom', - keyboardEnabled: true, - confirm: function () { - var password = this.$content.find('input').val(); - callback(password); - }, - 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) { - if (e.keyCode == 13) - that.$confirmButton.click(); + var content = getErrorsHtml(data["missing_perms"], is_error = false); + content += '
    '; - var s = String.fromCharCode(e.which); - 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; - } - if (capslock == 1) - $('.capslock .glyphicon').show(); - else if (capslock == 0) - $('.capslock .glyphicon').hide(); - }); - // Capslock key is not detected by keypress - this.$content.find('input').on('keydown', function (e) { - if (e.which == 20) { - capslock = 1 - capslock; - } - if (capslock == 1) - $('.capslock .glyphicon').show(); - else if (capslock == 0) - $('.capslock .glyphicon').hide(); - }); - }, - onClose: function () { - if (focus_next) - this._lastFocused = focus_next; - } + $.confirm({ + title: 'Authentification requise', + content: content, + backgroundDismiss: true, + animation: 'top', + closeAnimation: 'bottom', + keyboardEnabled: true, + confirm: function () { + var password = this.$content.find('input').val(); + callback(password); + }, + 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) { + 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 + (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; + } + if (capslock == 1) + $('.capslock .glyphicon').show(); + else if (capslock == 0) + $('.capslock .glyphicon').hide(); + }); + // Capslock key is not detected by keypress + this.$content.find('input').on('keydown', function (e) { + if (e.which == 20) { + capslock = 1 - capslock; + } + if (capslock == 1) + $('.capslock .glyphicon').show(); + else if (capslock == 0) + $('.capslock .glyphicon').hide(); + }); + }, + onClose: function () { + if (focus_next) + this._lastFocused = focus_next; + } + + }); } -function displayErrors(html) { +function displayErrors(data) { + const content = getErrorsHtml(data["errors"], is_error = true); $.alert({ title: 'Erreurs', - content: html, + content: content, backgroundDismiss: true, animation: 'top', closeAnimation: 'bottom', diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index ece98578..8259d694 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -376,10 +376,10 @@ $(document).ready(function() { requestAuth(data, performOperations, articleSelect); break; case 400: - if ('need_comment' in data['errors']) { + if ('need_comment' in data) { askComment(performOperations); } else { - displayErrors(getErrorsHtml(data)); + displayErrors(data); } break; } @@ -1074,7 +1074,7 @@ $(document).ready(function() { }, triInput); break; case 400: - askAddcost(getErrorsHtml(data)); + askAddcost(getErrorsHtml(data["errors"], is_error=true)); break; } }); diff --git a/kfet/templates/kfet/transfers_create.html b/kfet/templates/kfet/transfers_create.html index a4a1a450..fc429d97 100644 --- a/kfet/templates/kfet/transfers_create.html +++ b/kfet/templates/kfet/transfers_create.html @@ -72,7 +72,7 @@ $(document).ready(function () { var $next = $form.next('.transfer_form').find('.from_acc input'); } var $input_id = $input.next('input'); - if (isValidTrigramme(trigramme)) { + if (trigramme.is_valid_trigramme()) { getAccountData(trigramme, function(data) { $input_id.val(data.id); $data.text(data.name); @@ -122,7 +122,7 @@ $(document).ready(function () { requestAuth(data, performTransfers); break; case 400: - displayErrors(getErrorsHtml(data)); + displayErrors(data); break; } }); From 4205e0ad0e6f3b796a3d5c51420129f403c5b965 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:05:41 +0100 Subject: [PATCH 417/573] Tests --- kfet/tests/test_views.py | 253 +++++++++++++-------------------------- 1 file changed, 84 insertions(+), 169 deletions(-) diff --git a/kfet/tests/test_views.py b/kfet/tests/test_views.py index c4d31ae2..7a7eddcb 100644 --- a/kfet/tests/test_views.py +++ b/kfet/tests/test_views.py @@ -15,7 +15,6 @@ from ..auth.utils import hash_password from ..config import kfet_config from ..models import ( Account, - AccountNegative, Article, ArticleCategory, Checkout, @@ -1856,7 +1855,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["operation_group"], ["on_acc"]) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_on_acc", "invalid_formset"], + ) def test_group_on_acc_expects_comment(self): user_add_perms(self.users["team"], ["kfet.perform_commented_operations"]) @@ -1899,7 +1901,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["need_comment"], True) + self.assertEqual(json_data["need_comment"], True) def test_invalid_group_on_acc_needs_comment_requires_perm(self): self.account.trigramme = "#13" @@ -1922,8 +1924,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["missing_perms"], - ["[kfet] Enregistrer des commandes avec commentaires"], + json_data["missing_perms"], + ["Enregistrer des commandes avec commentaires"], ) def test_error_on_acc_frozen(self): @@ -1945,7 +1947,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["frozen"], [self.account.trigramme]) + self.assertEqual([e["code"] for e in json_data["errors"]], ["frozen_acc"]) def test_invalid_group_checkout(self): self.checkout.valid_from -= timedelta(days=300) @@ -1957,7 +1959,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["operation_group"], ["checkout"]) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_checkout", "invalid_formset"], + ) def test_invalid_group_expects_one_operation(self): data = dict(self.base_post_data) @@ -1965,7 +1970,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["operations"], []) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], + ) def test_purchase_with_user_is_nof_cof(self): self.account.cofprofile.is_cof = False @@ -2023,12 +2031,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check response content self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) # Check object updates @@ -2179,9 +2182,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], - [{"__all__": ["Un achat nécessite un article et une quantité"]}], + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_purchase_expects_article_nb(self): @@ -2199,9 +2202,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], - [{"__all__": ["Un achat nécessite un article et une quantité"]}], + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_purchase_expects_article_nb_greater_than_1(self): @@ -2219,16 +2222,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - 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." - ], - } - ], + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_operation_not_purchase_with_cash(self): @@ -2247,7 +2243,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["account"], "LIQ") + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_liq"], + ) def test_deposit(self): user_add_perms(self.users["team"], ["kfet.perform_deposit"]) @@ -2300,12 +2299,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) self.account.refresh_from_db() @@ -2364,8 +2358,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_deposit_too_many_params(self): @@ -2383,8 +2378,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_deposit_expects_positive_amount(self): @@ -2402,8 +2398,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Charge non positive"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_deposit_requires_perm(self): @@ -2421,9 +2418,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["missing_perms"], ["[kfet] Effectuer une charge"] - ) + self.assertEqual(json_data["missing_perms"], ["Effectuer une charge"]) def test_withdraw(self): data = dict( @@ -2475,12 +2470,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) self.account.refresh_from_db() @@ -2539,8 +2529,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_withdraw_too_many_params(self): @@ -2558,8 +2549,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Bad request"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_invalid_withdraw_expects_negative_amount(self): @@ -2577,8 +2569,9 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual( - json_data["errors"]["operations"], [{"__all__": ["Retrait non négatif"]}] + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_formset"], ) def test_edit(self): @@ -2634,12 +2627,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation.pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) self.account.refresh_from_db() @@ -2700,8 +2688,8 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"]["missing_perms"], - ["[kfet] Modifier la balance d'un compte"], + json_data["missing_perms"], + ["Modifier la balance d'un compte"], ) def test_invalid_edit_expects_comment(self): @@ -2721,7 +2709,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"]["need_comment"], True) + self.assertEqual(json_data["need_comment"], True) def _setup_addcost(self): self.register_user("addcost", create_user("addcost", "ADD")) @@ -3008,62 +2996,10 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"], - {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, + json_data["missing_perms"], + ["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")) @@ -3083,38 +3019,13 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): ) resp = self.client.post(self.url, data) - self.assertEqual(resp.status_code, 403) + self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - 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"), + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["negative"], ) - 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", @@ -3198,12 +3109,7 @@ class KPsulPerformOperationsViewTests(ViewTestCaseMixin, TestCase): # Check response content self.assertDictEqual( json_data, - { - "operationgroup": operation_group.pk, - "operations": [operation_list[0].pk, operation_list[1].pk], - "warnings": {}, - "errors": {}, - }, + {"errors": []}, ) # Check object updates @@ -3342,7 +3248,10 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {}) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["invalid_request"], + ) def test_invalid_operation_not_exist(self): data = {"operations[]": ["1000"]} @@ -3350,7 +3259,10 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"opes_notexisting": [1000]}) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["cancel_missing"], + ) @mock.patch("django.utils.timezone.now") def test_purchase(self, now_mock): @@ -3414,7 +3326,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3602,7 +3514,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3689,7 +3601,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3776,7 +3688,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, } ], - "errors": {}, + "errors": [], "warnings": {}, "opegroups_to_update": [ { @@ -3839,8 +3751,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"], - {"missing_perms": ["[kfet] Annuler des commandes non récentes"]}, + json_data["missing_perms"], + ["Annuler des commandes non récentes"], ) def test_already_canceled(self): @@ -3964,9 +3876,12 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): data = {"operations[]": [str(operation.pk)]} resp = self.client.post(self.url, data) - self.assertEqual(resp.status_code, 403) + self.assertEqual(resp.status_code, 400) json_data = json.loads(resp.content.decode("utf-8")) - self.assertEqual(json_data["errors"], {"negative": [self.account.trigramme]}) + self.assertCountEqual( + [e["code"] for e in json_data["errors"]], + ["negative"], + ) def test_invalid_negative_requires_perms(self): kfet_config.set(overdraft_amount=Decimal("40.00")) @@ -3985,8 +3900,8 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): self.assertEqual(resp.status_code, 403) json_data = json.loads(resp.content.decode("utf-8")) self.assertEqual( - json_data["errors"], - {"missing_perms": ["[kfet] Enregistrer des commandes en négatif"]}, + json_data["missing_perms"], + ["Enregistrer des commandes en négatif"], ) def test_partial_0(self): @@ -4036,7 +3951,7 @@ class KPsulCancelOperationsViewTests(ViewTestCaseMixin, TestCase): "canceled_by__trigramme": None, }, ], - "errors": {}, + "errors": [], "warnings": {"already_canceled": [operation3.pk]}, "opegroups_to_update": [ { From 4326ba901612550276fbf0c75a7524530da5fbd1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 28 Feb 2021 02:16:40 +0100 Subject: [PATCH 418/573] Oublis de renaming --- kfet/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kfet/views.py b/kfet/views.py index e403505a..0d9f9544 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1098,17 +1098,17 @@ def kpsul_perform_operations(request): if operation.type == Operation.EDIT: required_perms.add("kfet.edit_balance_account") need_comment = True - if account.is_cof: + if on_acc.is_cof: to_addcost_for_balance = to_addcost_for_balance / cof_grant_divisor - (perms, stop) = account.perms_to_perform_operation(amount=operationgroup.amount) + (perms, stop) = on_acc.perms_to_perform_operation(amount=operationgroup.amount) required_perms |= perms if stop: data["errors"].append( { "code": "negative", - "message": f"Le compte {account.trigramme} a un solde insuffisant.", + "message": f"Le compte {on_acc.trigramme} a un solde insuffisant.", } ) From c6cfc311e088a158d732b2b88396bf620c5239a8 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 17 Jun 2021 10:45:53 +0200 Subject: [PATCH 419/573] CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a56e649..fca9682a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,11 @@ adhérents ni des cotisations. - 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 + ## Version 0.10 - 18/04/2021 From 4060730ec53bd129403c886b4e404cdb3780f6ca Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 17 Jun 2021 10:49:35 +0200 Subject: [PATCH 420/573] Remove logging --- kfet/static/kfet/js/kfet.js | 1 - 1 file changed, 1 deletion(-) diff --git a/kfet/static/kfet/js/kfet.js b/kfet/static/kfet/js/kfet.js index 173c8ee8..14c4bc40 100644 --- a/kfet/static/kfet/js/kfet.js +++ b/kfet/static/kfet/js/kfet.js @@ -107,7 +107,6 @@ function amountToUKF(amount, is_cof = false, account = false) { } function getErrorsHtml(data, is_error = true) { - console.log("data") if (is_error) { data = data.map(error => error.message) } From 6b316c482bbbf7cdef15d312e786d5d49d5ff161 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 17 Jun 2021 17:22:17 +0200 Subject: [PATCH 421/573] Remove obsolete section --- kfet/templates/kfet/left_account.html | 6 ------ 1 file changed, 6 deletions(-) diff --git a/kfet/templates/kfet/left_account.html b/kfet/templates/kfet/left_account.html index e1673d22..a68845ed 100644 --- a/kfet/templates/kfet/left_account.html +++ b/kfet/templates/kfet/left_account.html @@ -54,12 +54,6 @@ {% if account.negative.start %}
  • Depuis le {{ account.negative.start|date:"d/m/Y à H:i" }}
  • {% 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 %} From 7ca7f7298afea53c31c0ed8c78dab0223d2d43a1 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Thu, 17 Jun 2021 21:28:08 +0200 Subject: [PATCH 422/573] Update CHANGELOG --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fca9682a..b0902028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,8 +31,13 @@ adhérents ni des cotisations. - 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 From 264a0a852fb1a1d70a705c5ef24d3d39f54ed83e Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Sat, 26 Jun 2021 22:52:23 +0200 Subject: [PATCH 423/573] On utilise |richtext pour les champs RichText, ce qui permet de bien faire les rendus --- gestioncof/cms/templates/cofcms/base.html | 22 +++++++++---------- .../templates/cofcms/cof_actu_index_page.html | 22 +++++++++---------- .../cms/templates/cofcms/cof_actu_page.html | 4 ++-- .../templates/cofcms/cof_directory_page.html | 6 ++--- gestioncof/cms/templates/cofcms/cof_page.html | 6 ++--- .../cms/templates/cofcms/cof_root_page.html | 4 ++-- 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index a3c78bcb..c420115f 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -16,7 +16,7 @@ {% block extra_head %}{% endblock %} - + @@ -32,27 +32,27 @@
{% block superaside %}{% endblock %} - +
{% block content %}{% endblock %}
diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index 9ddd4550..4508a66c 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -17,17 +17,17 @@ {% block content %}

{{ page.title }}

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

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

{% else %} - {{ actu.body|safe|truncatewords_html:15 }} + {{ actu.body|richtext|truncatewords_html:15 }} {% endif %} {% trans "Lire plus" %} >
@@ -44,10 +44,10 @@ {% endfor %} {% if actus.has_previous %} - {% trans "Actualités plus récentes" %} - {% endif %} + {% trans "Actualités plus récentes" %} + {% endif %} {% if actus.has_next %} - {% trans "Actualités plus anciennes" %} - {% endif %} + {% trans "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 09e42e91..5cd88134 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_page.html @@ -1,5 +1,5 @@ {% extends "cofcms/base.html" %} -{% load wagtailimages_tags cofcms_tags i18n %} +{% load wagtailcore_tags wagtailimages_tags cofcms_tags i18n %} {% block content %}
@@ -11,7 +11,7 @@
{% image page.image width-700 %}
- {{ page.body|safe }} + {{ page.body|richtext }}
{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_directory_page.html b/gestioncof/cms/templates/cofcms/cof_directory_page.html index 28f3c4c8..da0fa3ce 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -1,5 +1,5 @@ {% extends "cofcms/base_aside.html" %} -{% load wagtailimages_tags cofcms_tags static i18n %} +{% load wagtailcore_tags wagtailimages_tags cofcms_tags static i18n %} {% block extra_head %} {{ block.super }} @@ -18,7 +18,7 @@ {% block content %}

{{ page.title }}

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

{{ entry.title }}

-
{{ entry.body|safe }}
+
{{ entry.body|richtext }}
{% if entry.links %} + {% if user.profile.is_chef and not user.profile.is_buro %} +
+

Administration

+
+ +
+
+ {% endif %} {% if user.profile.is_buro %}

Administration

diff --git a/gestioncof/templates/gestioncof/registration_kf_form.html b/gestioncof/templates/gestioncof/registration_kf_form.html new file mode 100644 index 00000000..2b0711f6 --- /dev/null +++ b/gestioncof/templates/gestioncof/registration_kf_form.html @@ -0,0 +1,21 @@ +{% 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 %} + + {{ 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 new file mode 100644 index 00000000..b5690d70 --- /dev/null +++ b/gestioncof/templates/gestioncof/registration_kf_post.html @@ -0,0 +1,8 @@ +{% extends "base_title.html" %} + +{% block realcontent %} +

Inscription d'un nouveau membre

+
+ {% include "gestioncof/registration_kf_form.html" %} +
+{% endblock %} diff --git a/gestioncof/views.py b/gestioncof/views.py index e2a3e0bd..80ed2cfd 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -28,7 +28,13 @@ from icalendar import Calendar, Event as Vevent from bda.models import Spectacle, Tirage from gestioncof.autocomplete import cof_autocomplete -from gestioncof.decorators import BuroRequiredMixin, buro_required, cof_required +from gestioncof.decorators import ( + BuroRequiredMixin, + ChefRequiredMixin, + buro_required, + chef_required, + cof_required, +) from gestioncof.forms import ( CalendarForm, ClubsForm, @@ -39,6 +45,7 @@ from gestioncof.forms import ( GestioncofConfigForm, PhoneForm, ProfileForm, + RegistrationKFProfileForm, RegistrationPassUserForm, RegistrationProfileForm, RegistrationUserForm, @@ -429,8 +436,9 @@ def registration_set_ro_fields(user_form, profile_form): profile_form.fields["login_clipper"].widget.attrs["readonly"] = True -@buro_required +@chef_required def registration_form2(request, login_clipper=None, username=None, fullname=None): + is_buro = request.user.profile.is_buro events = Event.objects.filter(old=False).all() member = None if login_clipper: @@ -452,53 +460,79 @@ def registration_form2(request, login_clipper=None, username=None, fullname=None user_form.fields["first_name"].initial = bits[0] if len(bits) > 1: user_form.fields["last_name"].initial = " ".join(bits[1:]) - # profile - profile_form = RegistrationProfileForm( - initial={"login_clipper": login_clipper} - ) + if is_buro: + # profile + profile_form = RegistrationProfileForm( + initial={"login_clipper": login_clipper} + ) + # events & clubs + event_formset = EventFormset(events=events, prefix="events") + clubs_form = ClubsForm() + else: + profile_form = RegistrationKFProfileForm( + initial={"login_clipper": login_clipper} + ) + registration_set_ro_fields(user_form, profile_form) - # events & clubs - event_formset = EventFormset(events=events, prefix="events") - clubs_form = ClubsForm() if username: member = get_object_or_404(User, username=username) (profile, _) = CofProfile.objects.get_or_create(user=member) # already existing, prefill user_form = RegistrationUserForm(instance=member) - profile_form = RegistrationProfileForm(instance=profile) + if is_buro: + profile_form = RegistrationProfileForm(instance=profile) + # events + current_registrations = [] + for event in events: + try: + current_registrations.append( + EventRegistration.objects.get(user=member, event=event) + ) + except EventRegistration.DoesNotExist: + current_registrations.append(None) + event_formset = EventFormset( + events=events, + prefix="events", + current_registrations=current_registrations, + ) + # Clubs + clubs_form = ClubsForm(initial={"clubs": member.clubs.all()}) + else: + profile_form = RegistrationKFProfileForm(instance=profile) registration_set_ro_fields(user_form, profile_form) - # events - current_registrations = [] - for event in events: - try: - current_registrations.append( - EventRegistration.objects.get(user=member, event=event) - ) - except EventRegistration.DoesNotExist: - current_registrations.append(None) - event_formset = EventFormset( - events=events, prefix="events", current_registrations=current_registrations - ) - # Clubs - clubs_form = ClubsForm(initial={"clubs": member.clubs.all()}) elif not login_clipper: # new user user_form = RegistrationPassUserForm() - profile_form = RegistrationProfileForm() - event_formset = EventFormset(events=events, prefix="events") - clubs_form = ClubsForm() - return render( - request, - "gestioncof/registration_form.html", - { - "member": member, - "login_clipper": login_clipper, - "user_form": user_form, - "profile_form": profile_form, - "event_formset": event_formset, - "clubs_form": clubs_form, - }, - ) + if is_buro: + profile_form = RegistrationProfileForm() + event_formset = EventFormset(events=events, prefix="events") + clubs_form = ClubsForm() + else: + profile_form = RegistrationKFProfileForm() + if is_buro: + return render( + request, + "gestioncof/registration_form.html", + { + "member": member, + "login_clipper": login_clipper, + "user_form": user_form, + "profile_form": profile_form, + "event_formset": event_formset, + "clubs_form": clubs_form, + }, + ) + else: + return render( + request, + "gestioncof/registration_kf_form.html", + { + "member": member, + "login_clipper": login_clipper, + "user_form": user_form, + "profile_form": profile_form, + }, + ) def notify_new_member(request, member: User): @@ -529,8 +563,9 @@ def notify_new_member(request, member: User): ) -@buro_required +@chef_required def registration(request): + is_buro = request.user.profile.is_buro if request.POST: request_dict = request.POST.copy() member = None @@ -544,10 +579,15 @@ def registration(request): user_form = RegistrationPassUserForm(request_dict) else: user_form = RegistrationUserForm(request_dict) - profile_form = RegistrationProfileForm(request_dict) - clubs_form = ClubsForm(request_dict) - events = Event.objects.filter(old=False).all() - event_formset = EventFormset(events=events, data=request_dict, prefix="events") + if is_buro: + profile_form = RegistrationProfileForm(request_dict) + clubs_form = ClubsForm(request_dict) + events = Event.objects.filter(old=False).all() + event_formset = EventFormset( + events=events, data=request_dict, prefix="events" + ) + else: + profile_form = RegistrationKFProfileForm(request_dict) if "user_exists" in request_dict and request_dict["user_exists"]: username = request_dict["username"] try: @@ -570,67 +610,74 @@ def registration(request): was_cof = profile.is_cof was_kfet = profile.is_kfet # Maintenant on remplit le formulaire de profil - profile_form = RegistrationProfileForm(request_dict, instance=profile) - if ( - profile_form.is_valid() - and event_formset.is_valid() - and clubs_form.is_valid() + if is_buro: + profile_form = RegistrationProfileForm(request_dict, instance=profile) + else: + profile_form = RegistrationKFProfileForm(request_dict, instance=profile) + if profile_form.is_valid() and ( + not is_buro or (event_formset.is_valid() and clubs_form.is_valid()) ): # Enregistrement du profil profile = profile_form.save() - if profile.is_cof and not was_cof: - notify_new_member(request, member) - profile.date_adhesion_cof = date.today() - profile.save() + if is_buro: + if profile.is_cof and not was_cof: + notify_new_member(request, member) + profile.date_adhesion_cof = date.today() + profile.save() if profile.is_kfet and not was_kfet: notify_new_member(request, member) profile.date_adhesion_kfet = date.today() profile.save() - # Enregistrement des inscriptions aux événements - for form in event_formset: - if "status" not in form.cleaned_data: - form.cleaned_data["status"] = "no" - if form.cleaned_data["status"] == "no": - try: - current_registration = EventRegistration.objects.get( - user=member, event=form.event - ) - current_registration.delete() - except EventRegistration.DoesNotExist: - pass - continue - all_choices = get_event_form_choices(form.event, form) - ( - current_registration, - created_reg, - ) = EventRegistration.objects.get_or_create( - user=member, event=form.event - ) - update_event_form_comments(form.event, form, current_registration) - current_registration.options.set(all_choices) - current_registration.paid = form.cleaned_data["status"] == "paid" - current_registration.save() - # if form.event.title == "Mega 15" and created_reg: - # field = EventCommentField.objects.get( - # event=form.event, name="Commentaires") - # try: - # comments = EventCommentValue.objects.get( - # commentfield=field, - # registration=current_registration).content - # except EventCommentValue.DoesNotExist: - # comments = field.default - # FIXME : il faut faire quelque chose de propre ici, - # par exemple écrire un mail générique pour - # l'inscription aux événements et/ou donner la - # possibilité d'associer un mail aux événements - # send_custom_mail(...) - # Enregistrement des inscriptions aux clubs - member.clubs.clear() - for club in clubs_form.cleaned_data["clubs"]: - club.membres.add(member) - club.save() + if is_buro: + # Enregistrement des inscriptions aux événements + for form in event_formset: + if "status" not in form.cleaned_data: + form.cleaned_data["status"] = "no" + if form.cleaned_data["status"] == "no": + try: + current_registration = EventRegistration.objects.get( + user=member, event=form.event + ) + current_registration.delete() + except EventRegistration.DoesNotExist: + pass + continue + all_choices = get_event_form_choices(form.event, form) + ( + current_registration, + created_reg, + ) = EventRegistration.objects.get_or_create( + user=member, event=form.event + ) + update_event_form_comments( + form.event, form, current_registration + ) + current_registration.options.set(all_choices) + current_registration.paid = ( + form.cleaned_data["status"] == "paid" + ) + current_registration.save() + # if form.event.title == "Mega 15" and created_reg: + # field = EventCommentField.objects.get( + # event=form.event, name="Commentaires") + # try: + # comments = EventCommentValue.objects.get( + # commentfield=field, + # registration=current_registration).content + # except EventCommentValue.DoesNotExist: + # comments = field.default + # FIXME : il faut faire quelque chose de propre ici, + # par exemple écrire un mail générique pour + # l'inscription aux événements et/ou donner la + # possibilité d'associer un mail aux événements + # send_custom_mail(...) + # Enregistrement des inscriptions aux clubs + member.clubs.clear() + for club in clubs_form.cleaned_data["clubs"]: + club.membres.add(member) + club.save() # --- # Success @@ -642,23 +689,35 @@ def registration(request): member.get_full_name(), member.email ) ) - if profile.is_cof: + if is_buro and profile.is_cof: msg += "\nIl est désormais membre du COF n°{:d} !".format( member.profile.id ) messages.success(request, msg, extra_tags="safe") - return render( - request, - "gestioncof/registration_post.html", - { - "user_form": user_form, - "profile_form": profile_form, - "member": member, - "login_clipper": login_clipper, - "event_formset": event_formset, - "clubs_form": clubs_form, - }, - ) + if is_buro: + return render( + request, + "gestioncof/registration_post.html", + { + "user_form": user_form, + "profile_form": profile_form, + "member": member, + "login_clipper": login_clipper, + "event_formset": event_formset, + "clubs_form": clubs_form, + }, + ) + else: + return render( + request, + "gestioncof/registration_kf_post.html", + { + "user_form": user_form, + "profile_form": profile_form, + "member": member, + "login_clipper": login_clipper, + }, + ) else: return render(request, "registration.html") @@ -986,6 +1045,6 @@ class UserAutocompleteView(BuroRequiredMixin, Select2QuerySetView): search_fields = ("username", "first_name", "last_name") -class RegistrationAutocompleteView(BuroRequiredMixin, AutocompleteView): +class RegistrationAutocompleteView(ChefRequiredMixin, AutocompleteView): template_name = "gestioncof/search_results.html" search_composer = cof_autocomplete From 897ee5dc170bdba1c4a94dcdc2f01934273c7144 Mon Sep 17 00:00:00 2001 From: catvayor Date: Thu, 27 Feb 2025 17:20:19 +0100 Subject: [PATCH 560/573] feat(kfet/subscription): allow kf team to register subscription --- gestioncof/models.py | 46 +++++++++++++++++++++++++ gestioncof/views.py | 45 +++--------------------- kfet/forms.py | 6 ++++ kfet/templates/kfet/account_update.html | 3 +- kfet/views.py | 37 +++++++++++++++----- 5 files changed, 87 insertions(+), 50 deletions(-) diff --git a/gestioncof/models.py b/gestioncof/models.py index d7a7b5a6..30ba087a 100644 --- a/gestioncof/models.py +++ b/gestioncof/models.py @@ -1,7 +1,13 @@ +from datetime import date +from smtplib import SMTPRecipientsRefused + +from django.contrib import messages from django.contrib.auth.models import User +from django.core.mail import send_mail from django.db import models from django.db.models.signals import post_delete, post_save from django.dispatch import receiver +from django.template import loader from django.utils.translation import gettext_lazy as _ from bda.models import Spectacle @@ -94,6 +100,46 @@ class CofProfile(models.Model): def __str__(self): return self.user.username + def make_adh_cof(self, request, was_cof): + if self.is_cof and not was_cof: + notify_new_member(request, self.user) + self.date_adhesion_cof = date.today() + self.save() + + def make_adh_kfet(self, request, was_kfet): + if self.is_kfet and not was_kfet: + notify_new_member(request, self.user) + self.date_adhesion_kfet = date.today() + self.save() + + +def notify_new_member(request, member: User): + if not member.email: + messages.warning( + request, + "GestioCOF n'a pas d'adresse mail pour {}, ".format(member) + + "aucun email de bienvenue n'a été envoyé", + ) + return + + # Try to send a welcome email and report SMTP errors + try: + send_mail( + "Bienvenue au COF", + loader.render_to_string( + "gestioncof/mails/welcome.txt", context={"member": member} + ), + "cof@ens.fr", + [member.email], + ) + except SMTPRecipientsRefused: + messages.error( + request, + "Error lors de l'envoi de l'email de bienvenue à {} ({})".format( + member, member.email + ), + ) + @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): diff --git a/gestioncof/views.py b/gestioncof/views.py index 80ed2cfd..112ce905 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -1,7 +1,6 @@ import csv import uuid -from datetime import date, timedelta -from smtplib import SMTPRecipientsRefused +from datetime import timedelta from urllib.parse import parse_qs, urlencode, urlparse, urlunparse from django.contrib import messages @@ -14,11 +13,9 @@ from django.contrib.auth.views import ( redirect_to_login, ) from django.contrib.sites.models import Site -from django.core.mail import send_mail from django.db.models import Q from django.http import Http404, HttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect, render -from django.template import loader from django.urls import reverse_lazy from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -535,34 +532,6 @@ def registration_form2(request, login_clipper=None, username=None, fullname=None ) -def notify_new_member(request, member: User): - if not member.email: - messages.warning( - request, - "GestioCOF n'a pas d'adresse mail pour {}, ".format(member) - + "aucun email de bienvenue n'a été envoyé", - ) - return - - # Try to send a welcome email and report SMTP errors - try: - send_mail( - "Bienvenue au COF", - loader.render_to_string( - "gestioncof/mails/welcome.txt", context={"member": member} - ), - "cof@ens.fr", - [member.email], - ) - except SMTPRecipientsRefused: - messages.error( - request, - "Error lors de l'envoi de l'email de bienvenue à {} ({})".format( - member, member.email - ), - ) - - @chef_required def registration(request): is_buro = request.user.profile.is_buro @@ -620,15 +589,11 @@ def registration(request): # Enregistrement du profil profile = profile_form.save() if is_buro: - if profile.is_cof and not was_cof: - notify_new_member(request, member) - profile.date_adhesion_cof = date.today() - profile.save() + if profile.is_cof: + profile.make_adh_cof(request, was_cof) - if profile.is_kfet and not was_kfet: - notify_new_member(request, member) - profile.date_adhesion_kfet = date.today() - profile.save() + if profile.is_kfet: + profile.make_adh_kfet(request, was_kfet) if is_buro: # Enregistrement des inscriptions aux événements diff --git a/kfet/forms.py b/kfet/forms.py index 22b74952..cf7f80d4 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -198,6 +198,12 @@ class CofForm(forms.ModelForm): fields = ["login_clipper", "is_cof", "is_kfet", "departement"] +class CofKFForm(forms.ModelForm): + class Meta: + model = CofProfile + fields = ["is_kfet"] + + class UserForm(forms.ModelForm): class Meta: model = User diff --git a/kfet/templates/kfet/account_update.html b/kfet/templates/kfet/account_update.html index 65965d83..7115b9e2 100644 --- a/kfet/templates/kfet/account_update.html +++ b/kfet/templates/kfet/account_update.html @@ -34,10 +34,11 @@ Modification de mes informations {% 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_submit_snippet.html' with value="Mettre à jour" %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/kfet/views.py b/kfet/views.py index 81498334..018d2421 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -66,6 +66,7 @@ from kfet.forms import ( CheckoutStatementCreateForm, CheckoutStatementUpdateForm, CofForm, + CofKFForm, ContactForm, DemandeSoireeForm, FilterHistoryForm, @@ -187,13 +188,17 @@ def account(request): 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, + "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), + }, + ) @login_required @@ -252,6 +257,11 @@ 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") @@ -287,7 +297,6 @@ def account_form_set_readonly_fields(user_form, cof_form): 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 - cof_form.fields["is_kfet"].widget.attrs["disabled"] = True def get_account_create_forms( @@ -364,7 +373,6 @@ def get_account_create_forms( # mais on laisse le username en écriture cof_form.fields["login_clipper"].widget.attrs["readonly"] = True cof_form.fields["is_cof"].widget.attrs["disabled"] = True - cof_form.fields["is_kfet"].widget.attrs["disabled"] = True if request: account_form = AccountNoTriForm(request.POST) @@ -434,6 +442,7 @@ def account_update(request, trigramme): 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.method == "POST": @@ -441,6 +450,7 @@ def account_update(request, trigramme): 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) forms = [] @@ -457,6 +467,11 @@ def account_update(request, trigramme): elif group_form.has_changed(): warnings.append("statut d'équipe") + if request.user.has_perm("kfet.change_account"): + 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"): @@ -473,8 +488,11 @@ def account_update(request, trigramme): ) 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( @@ -506,6 +524,7 @@ def account_update(request, trigramme): "frozen_form": frozen_form, "group_form": group_form, "pwd_form": pwd_form, + "cof_form": cof_form, }, ) From b13ed3c1697d66f4a0885c929d0d74821f202220 Mon Sep 17 00:00:00 2001 From: catvayor Date: Tue, 18 Mar 2025 21:39:58 +0100 Subject: [PATCH 561/573] feat(kfet/stats): statistic for kfet members --- kfet/migrations/0083_operationgroup_is_kfet.py | 18 ++++++++++++++++++ kfet/models.py | 1 + kfet/views.py | 4 +++- 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 kfet/migrations/0083_operationgroup_is_kfet.py diff --git a/kfet/migrations/0083_operationgroup_is_kfet.py b/kfet/migrations/0083_operationgroup_is_kfet.py new file mode 100644 index 00000000..f344d829 --- /dev/null +++ b/kfet/migrations/0083_operationgroup_is_kfet.py @@ -0,0 +1,18 @@ +# 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/models.py b/kfet/models.py index 9485b5e5..ec581550 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -687,6 +687,7 @@ 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( diff --git a/kfet/views.py b/kfet/views.py index 018d2421..acef7d72 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1259,6 +1259,7 @@ def kpsul_perform_operations(request): 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 # Starting transaction to ensure data consistency with transaction.atomic(): @@ -1309,6 +1310,7 @@ def kpsul_perform_operations(request): "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 @@ -1524,7 +1526,7 @@ 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") + OperationGroup.objects.values("id", "amount", "is_cof", "is_kfet") .filter(pk__in=opegroups_pk) .order_by("pk") ) From 2eaf3542dbcfe0e9941c831b23f83868e9f3782a Mon Sep 17 00:00:00 2001 From: catvayor Date: Thu, 3 Apr 2025 16:05:23 +0200 Subject: [PATCH 562/573] feat(kfet): carte kfet --- gestioncof/decorators.py | 27 ++++++++++++++++++- gestioncof/templates/cof-denied.html | 2 +- gestioncof/templates/gestioncof/carte_kf.html | 19 +++++++++++++ gestioncof/templates/gestioncof/home.html | 3 +++ gestioncof/templates/kfet-denied.html | 5 ++++ gestioncof/urls.py | 1 + gestioncof/views.py | 7 +++++ 7 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 gestioncof/templates/gestioncof/carte_kf.html create mode 100644 gestioncof/templates/kfet-denied.html diff --git a/gestioncof/decorators.py b/gestioncof/decorators.py index 4eb5ea4c..6a9e31ac 100644 --- a/gestioncof/decorators.py +++ b/gestioncof/decorators.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) def cof_required(view_func): - """Décorateur qui vérifie que l'utilisateur est connecté et membre du COF. + """Décorateur qui vérifie que l'utilisateur est connecté et membre COF. - Si l'utilisteur n'est pas connecté, il est redirigé vers la page de connexion @@ -33,6 +33,31 @@ def cof_required(view_func): return login_required(_wrapped_view) +def kfet_required(view_func): + """Décorateur qui vérifie que l'utilisateur est connecté et membre K-Fêt. + + - Si l'utilisteur n'est pas connecté, il est redirigé vers la page de + connexion + - Si l'utilisateur est connecté mais pas membre K-Fêt, il obtient une + page d'erreur lui demandant de s'inscrire à la K-Fêt + """ + + def is_kfet(user): + try: + return user.profile.is_cof or user.profile.is_kfet + except AttributeError: + return False + + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + if is_kfet(request.user): + return view_func(request, *args, **kwargs) + + return render(request, "kfet-denied.html", status=403) + + return login_required(_wrapped_view) + + def buro_required(view_func): """Décorateur qui vérifie que l'utilisateur est connecté et membre du burô. diff --git a/gestioncof/templates/cof-denied.html b/gestioncof/templates/cof-denied.html index b2a12717..12cfd4a7 100644 --- a/gestioncof/templates/cof-denied.html +++ b/gestioncof/templates/cof-denied.html @@ -1,5 +1,5 @@ {% extends "base_title.html" %} {% block realcontent %} -

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

+

Section réservée aux membres COF -- 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/gestioncof/carte_kf.html b/gestioncof/templates/gestioncof/carte_kf.html new file mode 100644 index 00000000..624c234e --- /dev/null +++ b/gestioncof/templates/gestioncof/carte_kf.html @@ -0,0 +1,19 @@ +{% extends "base_title.html" %} +{% load bootstrap %} + +{% block page_size %}col-sm-8{%endblock%} + +{% block realcontent %} +

Pass K-Fêt

+ +
+

Profil de {{ user.first_name }} {{ user.last_name }}

+ + {% if user.profile.is_cof %} +

Membre COF depuis le {{ user.profile.date_adhesion_cof }}

+ {% else %} +

Membre K-Fêt depuis le {{ user.profile.date_adhesion_kfet }}

+ {% endif %} +
+ +{% endblock %} diff --git a/gestioncof/templates/gestioncof/home.html b/gestioncof/templates/gestioncof/home.html index 1f754023..4fa77d96 100644 --- a/gestioncof/templates/gestioncof/home.html +++ b/gestioncof/templates/gestioncof/home.html @@ -50,6 +50,9 @@
    {# TODO: Since Django 1.9, we can store result with "as", allowing proper value management (if None) #}
  • Page d'accueil
  • + {% if user.profile.is_cof or user.profile.is_kfet %} +
  • Carte K-Fêt
  • + {% endif %}
  • Calendrier
  • {% if perms.kfet.is_team %}
  • K-Psul
  • diff --git a/gestioncof/templates/kfet-denied.html b/gestioncof/templates/kfet-denied.html new file mode 100644 index 00000000..6d18dbb5 --- /dev/null +++ b/gestioncof/templates/kfet-denied.html @@ -0,0 +1,5 @@ +{% 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/urls.py b/gestioncof/urls.py index 624f8e22..59930f03 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -99,6 +99,7 @@ urlpatterns = [ name="cof-user-autocomplete", ), path("config", views.ConfigUpdate.as_view(), name="config.edit"), + path("carte", views.carte_kf, name="profile.carte"), # ----- # Authentification # ----- diff --git a/gestioncof/views.py b/gestioncof/views.py index 112ce905..daac3b07 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -31,6 +31,7 @@ from gestioncof.decorators import ( buro_required, chef_required, cof_required, + kfet_required, ) from gestioncof.forms import ( CalendarForm, @@ -428,6 +429,12 @@ def profile(request): return render(request, "gestioncof/profile.html", context) +@kfet_required +def carte_kf(request): + user = request.user + return render(request, "gestioncof/carte_kf.html", {"user": user}) + + def registration_set_ro_fields(user_form, profile_form): user_form.fields["username"].widget.attrs["readonly"] = True profile_form.fields["login_clipper"].widget.attrs["readonly"] = True From 08adfc84048680d12cf6ef63717186533c4a8364 Mon Sep 17 00:00:00 2001 From: catvayor Date: Thu, 3 Apr 2025 16:11:45 +0200 Subject: [PATCH 563/573] feat(cof/header): k-fet status & pretty non-logged --- gestioncof/static/gestioncof/css/cof.css | 3 +++ gestioncof/templates/gestioncof/base_header.html | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gestioncof/static/gestioncof/css/cof.css b/gestioncof/static/gestioncof/css/cof.css index f98313a6..71056afd 100644 --- a/gestioncof/static/gestioncof/css/cof.css +++ b/gestioncof/static/gestioncof/css/cof.css @@ -701,6 +701,9 @@ header a:active { .user-is-cof { color : #ADE297; } +.user-is-kfet { + color : #FF8C00; +} .user-is-not-cof { color: #EE8585; } diff --git a/gestioncof/templates/gestioncof/base_header.html b/gestioncof/templates/gestioncof/base_header.html index e5f757a7..02356ba6 100644 --- a/gestioncof/templates/gestioncof/base_header.html +++ b/gestioncof/templates/gestioncof/base_header.html @@ -10,10 +10,12 @@ {% endblock %}
    + {% if user.is_authenticated %}   |  Se déconnecter  + {% endif %}
    -

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

    +

    {%if user.is_authenticated %}{% if user.first_name %}{{ user.first_name }}{% else %}{{ user.username }}{% endif %}, {% endif %}{% if user.profile.is_cof %}au COF{% elif user.profile.is_kfet %}membre K-Fêt{% else %}non-COF{% endif %}

From e308258e40196c9291524c7f8c9a92fb24d6edb3 Mon Sep 17 00:00:00 2001 From: catvayor Date: Fri, 28 Feb 2025 14:40:16 +0100 Subject: [PATCH 564/573] feat(gestionCOF/registration): allow self registration for kfet subscription --- gestioncof/forms.py | 17 ++++++ gestioncof/templates/gestioncof/home.html | 3 + .../gestioncof/self_registration.html | 28 ++++++++++ gestioncof/urls.py | 1 + gestioncof/views.py | 55 ++++++++++++++++++- 5 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 gestioncof/templates/gestioncof/self_registration.html diff --git a/gestioncof/forms.py b/gestioncof/forms.py index d7453e10..e4a2ae3b 100644 --- a/gestioncof/forms.py +++ b/gestioncof/forms.py @@ -458,3 +458,20 @@ class GestioncofConfigForm(ConfigForm): max_length=2048, required=False, ) + + +# ---- +# Formulaire pour les adhésions self-service +# ---- + + +class SubscribForm(forms.Form): + accept_ri = forms.BooleanField( + label="Lu et accepte le réglement intérieur de l'AEENS (COF).", required=True + ) + accept_status = forms.BooleanField( + label="Lu et accepte les status de l'AEENS (COF).", required=True + ) + accept_charte_kf = forms.BooleanField( + label="Lu et accepte la charte de la K-Fêt.", required=True + ) diff --git a/gestioncof/templates/gestioncof/home.html b/gestioncof/templates/gestioncof/home.html index 4fa77d96..b70277b9 100644 --- a/gestioncof/templates/gestioncof/home.html +++ b/gestioncof/templates/gestioncof/home.html @@ -71,6 +71,9 @@ {% if not user.profile.login_clipper %}
  • Changer mon mot de passe
  • {% endif %} + {% if not user.profile.is_cof and not user.profile.is_kfet %} +
  • Adhérer à la K-Fêt
  • + {% endif %} diff --git a/gestioncof/templates/gestioncof/self_registration.html b/gestioncof/templates/gestioncof/self_registration.html new file mode 100644 index 00000000..c60dce4f --- /dev/null +++ b/gestioncof/templates/gestioncof/self_registration.html @@ -0,0 +1,28 @@ +{% 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/urls.py b/gestioncof/urls.py index 59930f03..ae418c6f 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -80,6 +80,7 @@ registration_patterns = [ views.RegistrationAutocompleteView.as_view(), name="cof.registration.autocomplete", ), + path("self_kf", views.self_kf_registration, name="self.kf_registration"), ] urlpatterns = [ diff --git a/gestioncof/views.py b/gestioncof/views.py index daac3b07..e3350a37 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -16,7 +16,7 @@ from django.contrib.sites.models import Site from django.db.models import Q from django.http import Http404, HttpResponse, HttpResponseForbidden from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse_lazy +from django.urls import reverse, reverse_lazy from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.generic import FormView, TemplateView @@ -47,6 +47,7 @@ from gestioncof.forms import ( RegistrationPassUserForm, RegistrationProfileForm, RegistrationUserForm, + SubscribForm, SurveyForm, SurveyStatusFilterForm, UserForm, @@ -694,6 +695,58 @@ def registration(request): return render(request, "registration.html") +# TODO: without login +@login_required +def self_kf_registration(request): + member = request.user + (profile, _) = CofProfile.objects.get_or_create(user=member) + + if profile.is_kfet or profile.is_cof: + msg = "Vous êtes déjà adhérent du COF !" + messages.success(request, msg) + response = HttpResponse(content="", status=303) + response["Location"] = reverse("profile") + return response + + was_kfet = profile.is_kfet + if request.POST: + user_form = RegistrationUserForm(request.POST, instance=member) + profile_form = PhoneForm(request.POST, instance=profile) + agreement_form = SubscribForm(request.POST) + if ( + user_form.is_valid() + and profile_form.is_valid() + and agreement_form.is_valid() + ): + member = user_form.save() + profile = profile_form.save() + profile.is_kfet = True + profile.save() + profile.make_adh_kfet(request, was_kfet) + + msg = "Votre adhésion a été enregistrée avec succès." + messages.success(request, msg, extra_tags="safe") + response = HttpResponse(content="", status=303) + response["Location"] = reverse("profile") + return response + else: + user_form = RegistrationUserForm(instance=member) + profile_form = PhoneForm(instance=profile) + agreement_form = SubscribForm() + + user_form.fields["username"].widget.attrs["readonly"] = True + return render( + request, + "gestioncof/self_registration.html", + { + "user_form": user_form, + "profile_form": profile_form, + "agreement_form": agreement_form, + "member": member, + }, + ) + + # ----- # Clubs # ----- From 415c0055bf0655ae2df4399d1bdb59c289ab4b15 Mon Sep 17 00:00:00 2001 From: catvayor Date: Sat, 3 May 2025 23:27:37 +0200 Subject: [PATCH 565/573] feat(kfet/subscription): special permission to manage is_kfet --- kfet/migrations/0084_alter_account_options.py | 37 +++++++++++++++++++ kfet/models.py | 1 + kfet/views.py | 2 +- 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 kfet/migrations/0084_alter_account_options.py diff --git a/kfet/migrations/0084_alter_account_options.py b/kfet/migrations/0084_alter_account_options.py new file mode 100644 index 00000000..5174c000 --- /dev/null +++ b/kfet/migrations/0084_alter_account_options.py @@ -0,0 +1,37 @@ +# 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/models.py b/kfet/models.py index ec581550..aa5be022 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -73,6 +73,7 @@ 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"), diff --git a/kfet/views.py b/kfet/views.py index acef7d72..66220602 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -467,7 +467,7 @@ def account_update(request, trigramme): elif group_form.has_changed(): warnings.append("statut d'équipe") - if request.user.has_perm("kfet.change_account"): + if request.user.has_perm("kfet.change_adh"): forms.append(cof_form) elif cof_form.has_changed(): warnings.append("adhésion kfet") From d0941c8dca142f9df9f238dd3db32c147063556b Mon Sep 17 00:00:00 2001 From: Alice Andres Date: Sun, 11 May 2025 13:16:56 +0200 Subject: [PATCH 566/573] =?UTF-8?q?feat:=20inclusive=20"adh=C3=A9rent?= =?UTF-8?q?=E2=8B=85e=E2=8B=85s"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/templates/gestioncof/home.html | 2 +- ...operation_options_alter_article_no_exte.py | 41 +++++++++++++++++++ kfet/models.py | 4 +- kfet/templates/kfet/article.html | 4 +- kfet/templates/kfet/article_read.html | 2 +- kfet/templates/kfet/kpsul.html | 2 +- kfet/views.py | 2 +- 7 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 kfet/migrations/0085_alter_operation_options_alter_article_no_exte.py diff --git a/gestioncof/templates/gestioncof/home.html b/gestioncof/templates/gestioncof/home.html index b70277b9..d7df5b44 100644 --- a/gestioncof/templates/gestioncof/home.html +++ b/gestioncof/templates/gestioncof/home.html @@ -132,7 +132,7 @@ 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 new file mode 100644 index 00000000..f99320df --- /dev/null +++ b/kfet/migrations/0085_alter_operation_options_alter_article_no_exte.py @@ -0,0 +1,41 @@ +# 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 aa5be022..8851d9f8 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -499,7 +499,7 @@ 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érents", default=False) + no_exte = models.BooleanField("Réservé au adhérent⋅e⋅s", default=False) hidden = models.BooleanField( "caché", default=False, @@ -763,7 +763,7 @@ class Operation(models.Model): ("perform_negative_operations", "Enregistrer des commandes en négatif"), ( "perform_liq_reserved", - "Effectuer une opération réservé aux adhérents sur LIQ", + "Effectuer une opération réservé aux adhérent⋅e⋅s sur LIQ", ), ("cancel_old_operations", "Annuler des commandes non récentes"), ( diff --git a/kfet/templates/kfet/article.html b/kfet/templates/kfet/article.html index fd51b8ef..cb52a5ea 100644 --- a/kfet/templates/kfet/article.html +++ b/kfet/templates/kfet/article.html @@ -40,7 +40,7 @@ Prix Stock En vente - Reservé aux adhérents + Reservé aux adhérent⋅e⋅s Affiché Dernier inventaire @@ -90,7 +90,7 @@ Prix Stock En vente - Reservé aux adhérents + Reservé aux adhérent⋅e⋅s Affiché Dernier inventaire diff --git a/kfet/templates/kfet/article_read.html b/kfet/templates/kfet/article_read.html index 4d2cb436..52032099 100644 --- a/kfet/templates/kfet/article_read.html +++ b/kfet/templates/kfet/article_read.html @@ -39,7 +39,7 @@
  • Stock: {{ article.stock }}
  • En vente: {{ article.is_sold|yesno|title }}
  • Affiché: {{ article.hidden|yesno|title }}
  • -
  • Réservé aux adhérents: {{ article.no_exte|yesno|title }}
  • +
  • Réservé aux adhérent⋅e⋅s: {{ article.no_exte|yesno|title }}
  • diff --git a/kfet/templates/kfet/kpsul.html b/kfet/templates/kfet/kpsul.html index 1c8bc8c5..b44f1a25 100644 --- a/kfet/templates/kfet/kpsul.html +++ b/kfet/templates/kfet/kpsul.html @@ -411,7 +411,7 @@ $(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érents" : ""); + 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) { diff --git a/kfet/views.py b/kfet/views.py index 66220602..fb9db4b2 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -1196,7 +1196,7 @@ def kpsul_perform_operations(request): "message": ( "L'article " + operation.article.name - + " est réservé aux adhérents du COF, or " + + " est réservé aux adhérent⋅e⋅s du COF, or " + on_acc.trigramme + " ne l'est pas" ), From 342ab6f14163f5898cc1057ed36d796f26cf7038 Mon Sep 17 00:00:00 2001 From: catvayor Date: Mon, 5 May 2025 14:01:40 +0200 Subject: [PATCH 567/573] chore(gestion/welcome): update welcome mail --- gestioncof/templates/gestioncof/mails/welcome.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gestioncof/templates/gestioncof/mails/welcome.txt b/gestioncof/templates/gestioncof/mails/welcome.txt index aa7ff326..61244bb2 100644 --- a/gestioncof/templates/gestioncof/mails/welcome.txt +++ b/gestioncof/templates/gestioncof/mails/welcome.txt @@ -1,11 +1,11 @@ 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 page Facebook : https://www.facebook.com/cof.ulm +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ô du lundi au vendredi de 12h à 14h et de 18h à 20h. +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 les évènements de rentrée pour les conscrit.e.s et les vieux/vieilles organisés par le COF et ses clubs ici : https://cof.ens.fr/planningrentree. +Retrouvez tout les évènements organisés par le COF et ses clubs ici : https://calendrier.dgnum.eu/. Amicalement, -Ton COF qui t'aime. \ No newline at end of file +Ton COF qui t'aime. From 6fa7b88b5da93f6f32cde945b5bf8368ed046ba2 Mon Sep 17 00:00:00 2001 From: catvayor Date: Thu, 29 May 2025 11:38:09 +0200 Subject: [PATCH 568/573] fix(async): synchronous only operation from update --- kfet/config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kfet/config.py b/kfet/config.py index a8f7f0eb..7280afc6 100644 --- a/kfet/config.py +++ b/kfet/config.py @@ -1,4 +1,5 @@ import djconfig +from asgiref.sync import sync_to_async from django.core.exceptions import ValidationError from django.db import models @@ -23,7 +24,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: - djconfig.reload_maybe() + sync_to_async(djconfig.reload_maybe)() self._conf_init = True def __getattr__(self, key): From 12b48dc8f65742e5d8113bfdc57135da5b192b02 Mon Sep 17 00:00:00 2001 From: Alice Andres Date: Sun, 11 May 2025 16:23:05 +0200 Subject: [PATCH 569/573] feat: new formula for recommended order quantity for 1 week --- kfet/forms.py | 2 +- kfet/views.py | 18 ++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/kfet/forms.py b/kfet/forms.py index cf7f80d4..2d0808ef 100644 --- a/kfet/forms.py +++ b/kfet/forms.py @@ -666,7 +666,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 = kwargs["initial"]["c_rec"] + self.c_rec_1w = kwargs["initial"]["c_rec_1w"] self.is_sold = kwargs["initial"]["is_sold"] diff --git a/kfet/views.py b/kfet/views.py index fb9db4b2..f85639b5 100644 --- a/kfet/views.py +++ b/kfet/views.py @@ -2258,21 +2258,11 @@ def order_create(request, pk): v_et = statistics.pstdev(v_3max, v_moy) # Expected sales for next week v_prev = v_moy + v_et - # We want to have 1.5 * the expected sales in stock - # (because sometimes some articles are not delivered) - c_rec_tot = max(v_prev * 1.5 - article.stock, 0) - # If ordered quantity is close enough to a level which can led to free - # boxes, we increase it to this level. + + c_rec_tot = max(v_prev - max(article.stock, 0), 0) if article.box_capacity: c_rec_temp = c_rec_tot / article.box_capacity - 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) + c_rec = round(c_rec_temp) initial.append( { "article": article.pk, @@ -2285,7 +2275,7 @@ def order_create(request, pk): "v_moy": round(v_moy), "v_et": round(v_et), "v_prev": round(v_prev), - "c_rec": article.box_capacity and c_rec or round(c_rec_tot), + "c_rec_1w": article.box_capacity and c_rec or round(c_rec_tot), "is_sold": article.is_sold, } ) From fa427c1c8dbdfdd33df207a28c0dc68a54e84e91 Mon Sep 17 00:00:00 2001 From: Alice Andres Date: Sun, 11 May 2025 16:23:28 +0200 Subject: [PATCH 570/573] feat: dynamic order qty recommendation by week --- kfet/templates/kfet/order_create.html | 55 ++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index 20ae7b69..eb18c666 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -7,11 +7,20 @@ {% block main-size %}col-lg-8 col-lg-offset-2{% endblock %} {% block main %} +
    +
    + +
    + +
    +
    +
    {% csrf_token %}
    @@ -74,7 +83,7 @@ {% for form in category.list %} - + {{ form.article }} {% for v_chunk in form.v_all %} @@ -82,10 +91,10 @@ {% endfor %} - - - - + + + + {% endfor %} @@ -107,6 +116,42 @@ $(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 %} From 5bc57493f7f844ed27c29c7dadb56ddf33130774 Mon Sep 17 00:00:00 2001 From: Alice Andres Date: Sun, 11 May 2025 16:28:47 +0200 Subject: [PATCH 571/573] feat: clarify columns in orders --- kfet/templates/kfet/order_create.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index eb18c666..7e24d1c0 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -34,7 +34,7 @@
    {{ form.name }}{{ form.v_moy }} {{ form.v_et }}{{ form.v_prev }}{{ form.stock }}{{ form.box_capacity|default:"" }}{{ form.c_rec }}{{ form.v_prev }}{{ form.stock }}{{ form.box_capacity|default:"" }} {{ form.quantity_ordered|add_class:"form-control" }}
    V. moy.
    - +
    E.T. @@ -44,7 +44,7 @@ Prév.
    - +
    Stock @@ -55,7 +55,7 @@ Rec.
    - +
    Commande From 0cebc8828b02e0e7c651ac76ed984cacdf32ce77 Mon Sep 17 00:00:00 2001 From: Tom Hubrecht Date: Fri, 4 Jul 2025 15:49:47 +0200 Subject: [PATCH 572/573] fix(channels): Declare a custom serializer instead of recreating the whole layer C.f. https://github.com/django/channels_redis/tree/main?tab=readme-ov-file#serializer_format which was added in the 4.2 release --- shared/channels.py | 32 ++++++++++++-------------------- 1 file changed, 12 insertions(+), 20 deletions(-) diff --git a/shared/channels.py b/shared/channels.py index ae8c1248..fe31b859 100644 --- a/shared/channels.py +++ b/shared/channels.py @@ -1,9 +1,9 @@ import datetime -import random from decimal import Decimal import msgpack -from channels_redis.core import RedisChannelLayer +from channels_redis.core import BaseMessageSerializer +from channels_redis.serializers import registry def encode_kf(obj): @@ -22,24 +22,16 @@ def decode_kf(obj): 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) +class KfetMessageSerializer(BaseMessageSerializer): + """ + Customize the serializer class to allow encoding Decimal and Datetime objects. + """ - if self.crypter: - value = self.crypter.encrypt(value) + def as_bytes(self, message): + return msgpack.packb(message, default=encode_kf, use_bin_type=True) - # 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) + def from_bytes(self, message): return msgpack.unpackb(message, object_hook=decode_kf, raw=False) + + +registry.register_serializer("kfet", KfetMessageSerializer) From 1677e4088e259bd4513e4be7ca353058c6ed44a2 Mon Sep 17 00:00:00 2001 From: catvayor Date: Fri, 4 Jul 2025 17:40:46 +0200 Subject: [PATCH 573/573] Revert: fix(channels): Declare a custom serializer instead of recreating the whole layer This reverts commit 0cebc8828b02e0e7c651ac76ed984cacdf32ce77. --- shared/channels.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/shared/channels.py b/shared/channels.py index fe31b859..ae8c1248 100644 --- a/shared/channels.py +++ b/shared/channels.py @@ -1,9 +1,9 @@ import datetime +import random from decimal import Decimal import msgpack -from channels_redis.core import BaseMessageSerializer -from channels_redis.serializers import registry +from channels_redis.core import RedisChannelLayer def encode_kf(obj): @@ -22,16 +22,24 @@ def decode_kf(obj): return obj -class KfetMessageSerializer(BaseMessageSerializer): - """ - Customize the serializer class to allow encoding Decimal and Datetime objects. - """ +class ChannelLayer(RedisChannelLayer): + def serialize(self, message): + """Serializes to a byte string.""" + value = msgpack.packb(message, default=encode_kf, use_bin_type=True) - def as_bytes(self, message): - return msgpack.packb(message, default=encode_kf, use_bin_type=True) + if self.crypter: + value = self.crypter.encrypt(value) - def from_bytes(self, message): + # 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) - - -registry.register_serializer("kfet", KfetMessageSerializer)