From 65d7a66eb8db2a5b90d804686541dba645217032 Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 7 Aug 2017 23:31:27 +0200 Subject: [PATCH 001/773] =?UTF-8?q?D=C3=A9but=20nouveau=20site=20cof?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cof/settings/common.py | 8 +++++ gestioncof/cms/__init__.py | 0 gestioncof/cms/admin.py | 3 ++ gestioncof/cms/migrations/0001_initial.py | 38 +++++++++++++++++++++++ gestioncof/cms/migrations/__init__.py | 0 gestioncof/cms/models.py | 11 +++++++ gestioncof/cms/tests.py | 3 ++ gestioncof/cms/translation.py | 11 +++++++ gestioncof/cms/urls.py | 0 gestioncof/cms/views.py | 3 ++ requirements.txt | 1 + 11 files changed, 78 insertions(+) create mode 100644 gestioncof/cms/__init__.py create mode 100644 gestioncof/cms/admin.py create mode 100644 gestioncof/cms/migrations/0001_initial.py create mode 100644 gestioncof/cms/migrations/__init__.py create mode 100644 gestioncof/cms/models.py create mode 100644 gestioncof/cms/tests.py create mode 100644 gestioncof/cms/translation.py create mode 100644 gestioncof/cms/urls.py create mode 100644 gestioncof/cms/views.py diff --git a/cof/settings/common.py b/cof/settings/common.py index ffcb8ee5..0117c662 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -73,7 +73,9 @@ INSTALLED_APPS = [ 'wagtailmenus', 'modelcluster', 'taggit', + 'wagtail_modeltranslation', 'kfet.cms', + 'gestioncof.cms', ] MIDDLEWARE_CLASSES = [ @@ -89,6 +91,7 @@ MIDDLEWARE_CLASSES = [ 'djconfig.middleware.DjConfigMiddleware', 'wagtail.wagtailcore.middleware.SiteMiddleware', 'wagtail.wagtailredirects.middleware.RedirectMiddleware', + 'django.middleware.locale.LocaleMiddleware', ] ROOT_URLCONF = 'cof.urls' @@ -141,6 +144,11 @@ USE_L10N = True USE_TZ = True +LANGUAGES = ( + ('fr', u'Français'), + ('en', u'English'), +) + # Various additional settings SITE_ID = 1 diff --git a/gestioncof/cms/__init__.py b/gestioncof/cms/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gestioncof/cms/admin.py b/gestioncof/cms/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/gestioncof/cms/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/gestioncof/cms/migrations/0001_initial.py b/gestioncof/cms/migrations/0001_initial.py new file mode 100644 index 00000000..5b48c5ec --- /dev/null +++ b/gestioncof/cms/migrations/0001_initial.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models +import wagtail.wagtailcore.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('wagtailcore', '0033_remove_golive_expiry_help_text'), + ] + + operations = [ + migrations.CreateModel( + name='COFPage', + fields=[ + ('page_ptr', models.OneToOneField(primary_key=True, to='wagtailcore.Page', parent_link=True, auto_created=True, serialize=False)), + ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", null=True, max_length=255, verbose_name='title')), + ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", null=True, max_length=255, verbose_name='title')), + ('slug_fr', models.SlugField(help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', null=True, max_length=255, verbose_name='slug')), + ('slug_en', models.SlugField(help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', null=True, max_length=255, verbose_name='slug')), + ('url_path_fr', models.TextField(blank=True, null=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(blank=True, null=True, editable=False, verbose_name='URL path')), + ('seo_title_fr', models.CharField(help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", null=True, blank=True, max_length=255, verbose_name='page title')), + ('seo_title_en', models.CharField(help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", null=True, blank=True, max_length=255, verbose_name='page title')), + ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), + ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), + ('corps', wagtail.wagtailcore.fields.RichTextField()), + ('corps_fr', wagtail.wagtailcore.fields.RichTextField(null=True)), + ('corps_en', wagtail.wagtailcore.fields.RichTextField(null=True)), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + ] diff --git a/gestioncof/cms/migrations/__init__.py b/gestioncof/cms/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py new file mode 100644 index 00000000..17492f08 --- /dev/null +++ b/gestioncof/cms/models.py @@ -0,0 +1,11 @@ +from django.db import models + +# Create your models here. +from wagtail.wagtailcore.models import Page, Orderable + +from wagtail.wagtailcore.fields import RichTextField, StreamField +from wagtail.wagtailcore import blocks + + +class COFPage(Page): + corps = RichTextField() diff --git a/gestioncof/cms/tests.py b/gestioncof/cms/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/gestioncof/cms/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/gestioncof/cms/translation.py b/gestioncof/cms/translation.py new file mode 100644 index 00000000..b705f6ed --- /dev/null +++ b/gestioncof/cms/translation.py @@ -0,0 +1,11 @@ +from .models import COFPage + +from wagtail_modeltranslation.translator import WagtailTranslationOptions +from modeltranslation.decorators import register + +@register(COFPage) +class COFPageTr(WagtailTranslationOptions): + fields = ( + 'corps', + ) + diff --git a/gestioncof/cms/urls.py b/gestioncof/cms/urls.py new file mode 100644 index 00000000..e69de29b diff --git a/gestioncof/cms/views.py b/gestioncof/cms/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/gestioncof/cms/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/requirements.txt b/requirements.txt index 1da8c361..91b13db4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,3 +25,4 @@ channels==1.1.5 python-dateutil wagtail==1.10.* wagtailmenus==2.2.* +wagtail-modeltranslation==0.6.0rc2 From 6023211ab09a46a8c9c873def0fec847ac46af1e Mon Sep 17 00:00:00 2001 From: Evarin Date: Wed, 9 Aug 2017 00:07:56 +0200 Subject: [PATCH 002/773] Models des pages et traductions --- gestioncof/cms/__init__.py | 1 + gestioncof/cms/apps.py | 7 + gestioncof/cms/migrations/0001_initial.py | 165 ++++++++++++++++++++-- gestioncof/cms/models.py | 101 ++++++++++++- gestioncof/cms/translation.py | 40 +++++- 5 files changed, 296 insertions(+), 18 deletions(-) create mode 100644 gestioncof/cms/apps.py diff --git a/gestioncof/cms/__init__.py b/gestioncof/cms/__init__.py index e69de29b..9892db53 100644 --- a/gestioncof/cms/__init__.py +++ b/gestioncof/cms/__init__.py @@ -0,0 +1 @@ +default_app_config = 'gestioncof.cms.apps.COFCMSAppConfig' diff --git a/gestioncof/cms/apps.py b/gestioncof/cms/apps.py new file mode 100644 index 00000000..cbc58688 --- /dev/null +++ b/gestioncof/cms/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class COFCMSAppConfig(AppConfig): + name = 'gestioncof.cms' + label = 'cofcms' + verbose_name = 'CMS COF' diff --git a/gestioncof/cms/migrations/0001_initial.py b/gestioncof/cms/migrations/0001_initial.py index 5b48c5ec..19cf61c7 100644 --- a/gestioncof/cms/migrations/0001_initial.py +++ b/gestioncof/cms/migrations/0001_initial.py @@ -2,33 +2,170 @@ from __future__ import unicode_literals from django.db import migrations, models +import django.db.models.deletion +import wagtail.wagtailimages.blocks +import wagtail.wagtailcore.blocks import wagtail.wagtailcore.fields class Migration(migrations.Migration): dependencies = [ + ('wagtailimages', '0019_delete_filter'), ('wagtailcore', '0033_remove_golive_expiry_help_text'), ] operations = [ + migrations.CreateModel( + name='COFActuIndexPage', + fields=[ + ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), + ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='COFActuPage', + fields=[ + ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), + ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('body', wagtail.wagtailcore.fields.RichTextField(verbose_name='Contenu')), + ('body_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Contenu')), + ('body_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Contenu')), + ('date', models.DateField(verbose_name='Date du post')), + ('Image à la Une', models.ForeignKey(null=True, to='wagtailimages.Image', on_delete=django.db.models.deletion.SET_NULL, blank=True, related_name='+')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='COFDirectoryEntryPage', + fields=[ + ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), + ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('body', wagtail.wagtailcore.fields.RichTextField(verbose_name='Description')), + ('body_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description')), + ('body_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description')), + ('links', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))))), + ('links_fr', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))), null=True)), + ('links_en', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))), null=True)), + ('Image', models.ForeignKey(null=True, to='wagtailimages.Image', on_delete=django.db.models.deletion.SET_NULL, blank=True, related_name='+')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='COFDirectoryPage', + fields=[ + ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), + ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('introduction', wagtail.wagtailcore.fields.RichTextField(verbose_name='Introduction')), + ('introduction_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), + ('introduction_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='COFEvent', + fields=[ + ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), + ('title', models.TextField(verbose_name='Titre')), + ('title_fr', models.TextField(null=True, verbose_name='Titre')), + ('title_en', models.TextField(null=True, verbose_name='Titre')), + ('description', wagtail.wagtailcore.fields.RichTextField(verbose_name='Description (concise)')), + ('description_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description (concise)')), + ('description_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description (concise)')), + ('date_start', models.DateTimeField(verbose_name='Date et heure de début')), + ('date_end', models.DateTimeField(null=True, verbose_name='Date et heure de fin')), + ('all_day', models.BooleanField(verbose_name='Toute la journée', default=False)), + ], + ), migrations.CreateModel( name='COFPage', fields=[ - ('page_ptr', models.OneToOneField(primary_key=True, to='wagtailcore.Page', parent_link=True, auto_created=True, serialize=False)), - ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", null=True, max_length=255, verbose_name='title')), - ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", null=True, max_length=255, verbose_name='title')), - ('slug_fr', models.SlugField(help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', null=True, max_length=255, verbose_name='slug')), - ('slug_en', models.SlugField(help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', null=True, max_length=255, verbose_name='slug')), - ('url_path_fr', models.TextField(blank=True, null=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(blank=True, null=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", null=True, blank=True, max_length=255, verbose_name='page title')), - ('seo_title_en', models.CharField(help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", null=True, blank=True, max_length=255, verbose_name='page title')), - ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), - ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), - ('corps', wagtail.wagtailcore.fields.RichTextField()), - ('corps_fr', wagtail.wagtailcore.fields.RichTextField(null=True)), - ('corps_en', wagtail.wagtailcore.fields.RichTextField(null=True)), + ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), + ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('body', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock())))), + ('body_fr', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock())), null=True)), + ('body_en', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock())), null=True)), + ], + options={ + 'abstract': False, + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='COFRootPage', + fields=[ + ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), + ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('introduction', wagtail.wagtailcore.fields.RichTextField(verbose_name='Introduction')), + ('introduction_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), + ('introduction_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), ], options={ 'abstract': False, diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index 17492f08..eb50df48 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -1,11 +1,108 @@ from django.db import models -# Create your models here. from wagtail.wagtailcore.models import Page, Orderable from wagtail.wagtailcore.fields import RichTextField, StreamField from wagtail.wagtailcore import blocks +from wagtail.wagtailimages.edit_handlers import ImageChooserPanel +from wagtail.wagtailimages.blocks import ImageChooserBlock +from wagtail.wagtailadmin.edit_handlers import FieldPanel, StreamFieldPanel +from wagtail.wagtailsnippets.models import register_snippet +# Racine du site du COF +class COFRootPage(Page): + introduction = RichTextField("Introduction") + + content_panels = Page.content_panels + [ + FieldPanel('introduction', classname="full"), + ] + + subpage_types = ['COFActuIndexPage', 'COFPage', 'COFDirectoryPage'] + +# Page lambda du site class COFPage(Page): - corps = RichTextField() + body = StreamField([ + ('heading', blocks.CharBlock(classname="full title")), + ('paragraph', blocks.RichTextBlock()), + ('image', ImageChooserBlock()), + ]) + + content_panels = Page.content_panels + [ + StreamFieldPanel('body'), + ] + + subpages_types = ['COFDirectoryPage', 'COFPage'] + parent_page_types = ['COFPage', 'COFRootPage'] + +# Évènements +@register_snippet +class COFEvent(models.Model): + title = models.TextField("Titre") + description = RichTextField("Description (concise)") + + date_start = models.DateTimeField("Date et heure de début") + date_end = models.DateTimeField("Date et heure de fin", null=True) + all_day = models.BooleanField("Toute la journée", default=False, blank=True) + + panels = [ + FieldPanel("title"), + FieldPanel("description"), + FieldPanel("date_start"), + FieldPanel("date_end"), + FieldPanel("all_day"), + ] + +# Actualités +class COFActuIndexPage(Page): + subpages_types = ['COFActuPage'] + parent_page_types = ['COFRootPage'] + +class COFActuPage(Page): + body = RichTextField("Contenu") + date = models.DateField("Date du post") + image = models.ForeignKey( + 'wagtailimages.Image', name="Image à la Une", + null=True, blank=True, + on_delete=models.SET_NULL, related_name='+' + ) + + content_panels = Page.content_panels + [ + FieldPanel('date'), +# ImageChooserPanel('image'), + FieldPanel('body', classname="full"), + ] + + + subpages_types = [] + parent_page_types = ['COFActuIndexPage'] + +# Annuaires (Clubs, partenaires, bonnes adresses) +class COFDirectoryPage(Page): + introduction = RichTextField("Introduction") + + subpages_types = ['COFActuPage', 'COFDirectoryEntryPage'] + parent_page_types = ['COFRootPage', 'COFPage'] + +class COFDirectoryEntryPage(Page): + body = RichTextField("Description") + links = StreamField([ + ('lien', blocks.StructBlock([ + ('url', blocks.URLBlock(required=True)), + ('texte', blocks.CharBlock()), + ])), + ('contact', blocks.StructBlock([ + ('email', blocks.EmailBlock(required=True)), + ('texte', blocks.CharBlock()), + ])), + ]) + + image = models.ForeignKey( + 'wagtailimages.Image', name="Image", + null=True, blank=True, + on_delete=models.SET_NULL, related_name='+' + ) + + subpages_types = [] + parent_page_types = ['COFDirectoryPage'] + diff --git a/gestioncof/cms/translation.py b/gestioncof/cms/translation.py index b705f6ed..705a9c1f 100644 --- a/gestioncof/cms/translation.py +++ b/gestioncof/cms/translation.py @@ -1,11 +1,47 @@ -from .models import COFPage +from .models import COFRootPage, COFPage, COFEvent, COFActuIndexPage, COFActuPage, COFDirectoryPage, COFDirectoryEntryPage from wagtail_modeltranslation.translator import WagtailTranslationOptions from modeltranslation.decorators import register +@register(COFRootPage) +class COFPageTr(WagtailTranslationOptions): + fields = ( + 'introduction', + ) + @register(COFPage) class COFPageTr(WagtailTranslationOptions): fields = ( - 'corps', + 'body', + ) + +@register(COFEvent) +class COFEventTr(WagtailTranslationOptions): + fields = ( + 'title', + 'description', + ) + +@register(COFActuIndexPage) +class COFActuIndexPageTr(WagtailTranslationOptions): + fields = ( + ) + +@register(COFActuPage) +class COFActuPageTr(WagtailTranslationOptions): + fields = ( + 'body', + ) + +@register(COFDirectoryPage) +class COFDirectoryPageTr(WagtailTranslationOptions): + fields = ( + 'introduction', ) +@register(COFDirectoryEntryPage) +class COFDirectoryEntryPageTr(WagtailTranslationOptions): + fields = ( + 'body', + 'links', + ) From 66fc36473925b27d36e3183d22e88cae2e8b2a43 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sat, 19 Aug 2017 01:29:45 +0200 Subject: [PATCH 003/773] Ignore sass cache --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index f12190af..0e991fe8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ venv/ /src media/ *.log +.sass-cache/ \ No newline at end of file From f5778fed2a5ecf6115c32b6730d24a00700a289a Mon Sep 17 00:00:00 2001 From: Evarin Date: Sat, 19 Aug 2017 01:32:26 +0200 Subject: [PATCH 004/773] =?UTF-8?q?Mod=C3=A8les=20plus=20cleans=20et=20tem?= =?UTF-8?q?plates=20principaux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/cms/migrations/0001_initial.py | 143 ++++++++++-------- gestioncof/cms/models.py | 117 ++++++++++---- gestioncof/cms/static/cofcms/config.rb | 25 +++ gestioncof/cms/static/cofcms/css/ie.css | 5 + gestioncof/cms/static/cofcms/css/print.css | 3 + gestioncof/cms/static/cofcms/css/screen.css | 122 +++++++++++++++ .../cms/static/cofcms/sass/_colors.scss | 4 + gestioncof/cms/static/cofcms/sass/screen.scss | 58 +++++++ gestioncof/cms/templates/cofcms/base.html | 31 ++++ gestioncof/cms/templates/cofcms/base_nav.html | 9 ++ .../templates/cofcms/cof_actu_index_page.html | 18 +++ .../templates/cofcms/cof_directory_page.html | 34 +++++ gestioncof/cms/templates/cofcms/cof_page.html | 23 +++ .../cms/templates/cofcms/cof_root_page.html | 2 + gestioncof/cms/templatetags/__init__.py | 0 gestioncof/cms/templatetags/cofcms_tags.py | 12 ++ gestioncof/cms/translation.py | 10 +- 17 files changed, 522 insertions(+), 94 deletions(-) create mode 100644 gestioncof/cms/static/cofcms/config.rb create mode 100644 gestioncof/cms/static/cofcms/css/ie.css create mode 100644 gestioncof/cms/static/cofcms/css/print.css create mode 100644 gestioncof/cms/static/cofcms/css/screen.css create mode 100644 gestioncof/cms/static/cofcms/sass/_colors.scss create mode 100644 gestioncof/cms/static/cofcms/sass/screen.scss create mode 100644 gestioncof/cms/templates/cofcms/base.html create mode 100644 gestioncof/cms/templates/cofcms/base_nav.html create mode 100644 gestioncof/cms/templates/cofcms/cof_actu_index_page.html create mode 100644 gestioncof/cms/templates/cofcms/cof_directory_page.html create mode 100644 gestioncof/cms/templates/cofcms/cof_page.html create mode 100644 gestioncof/cms/templates/cofcms/cof_root_page.html create mode 100644 gestioncof/cms/templatetags/__init__.py create mode 100644 gestioncof/cms/templatetags/cofcms_tags.py diff --git a/gestioncof/cms/migrations/0001_initial.py b/gestioncof/cms/migrations/0001_initial.py index 19cf61c7..e9abe2fb 100644 --- a/gestioncof/cms/migrations/0001_initial.py +++ b/gestioncof/cms/migrations/0001_initial.py @@ -2,50 +2,83 @@ from __future__ import unicode_literals from django.db import migrations, models -import django.db.models.deletion -import wagtail.wagtailimages.blocks -import wagtail.wagtailcore.blocks import wagtail.wagtailcore.fields +import wagtail.wagtailimages.blocks +import gestioncof.cms.models +import wagtail.wagtailcore.blocks +import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('wagtailimages', '0019_delete_filter'), ('wagtailcore', '0033_remove_golive_expiry_help_text'), + ('wagtailimages', '0019_delete_filter'), ] operations = [ migrations.CreateModel( - name='COFActuIndexPage', + name='COFActuEventPage', fields=[ - ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), - ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), + ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), + ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('chapo', models.TextField(verbose_name='Description rapide')), + ('chapo_fr', models.TextField(null=True, verbose_name='Description rapide')), + ('chapo_en', models.TextField(null=True, verbose_name='Description rapide')), + ('body', wagtail.wagtailcore.fields.RichTextField(verbose_name='Description longue')), + ('body_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description longue')), + ('body_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description longue')), + ('date_start', models.DateTimeField(verbose_name='Date et heure de début')), + ('date_end', models.DateTimeField(null=True, blank=True, default=None, verbose_name='Date et heure de fin')), + ('all_day', models.BooleanField(default=False, verbose_name='Toute la journée')), + ('image', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailimages.Image', related_name='+', verbose_name='Image à la Une')), + ], + options={ + 'verbose_name_plural': 'Actus liées à des évènements', + 'verbose_name': 'Actu liée à un évènement', + }, + bases=('wagtailcore.page',), + ), + migrations.CreateModel( + name='COFActuIndexPage', + fields=[ + ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), + ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), + ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), ], options={ - 'abstract': False, + 'verbose_name_plural': 'Indexs des actualités', + 'verbose_name': 'Index des actualités', }, - bases=('wagtailcore.page',), + bases=('wagtailcore.page', gestioncof.cms.models.COFActuIndexMixin), ), migrations.CreateModel( name='COFActuPage', fields=[ - ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), - ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), @@ -54,23 +87,24 @@ class Migration(migrations.Migration): ('body_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Contenu')), ('body_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Contenu')), ('date', models.DateField(verbose_name='Date du post')), - ('Image à la Une', models.ForeignKey(null=True, to='wagtailimages.Image', on_delete=django.db.models.deletion.SET_NULL, blank=True, related_name='+')), + ('image', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailimages.Image', related_name='+', verbose_name='Image à la Une')), ], options={ - 'abstract': False, + 'verbose_name_plural': 'Actualités simples', + 'verbose_name': 'Actualité simple', }, bases=('wagtailcore.page',), ), migrations.CreateModel( name='COFDirectoryEntryPage', fields=[ - ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), - ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), @@ -81,23 +115,24 @@ class Migration(migrations.Migration): ('links', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))))), ('links_fr', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))), null=True)), ('links_en', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))), null=True)), - ('Image', models.ForeignKey(null=True, to='wagtailimages.Image', on_delete=django.db.models.deletion.SET_NULL, blank=True, related_name='+')), + ('image', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailimages.Image', related_name='+', verbose_name='Image')), ], options={ - 'abstract': False, + 'verbose_name_plural': "Éntrées d'annuaire", + 'verbose_name': "Éntrée d'annuaire", }, bases=('wagtailcore.page',), ), migrations.CreateModel( name='COFDirectoryPage', fields=[ - ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), - ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), @@ -107,35 +142,21 @@ class Migration(migrations.Migration): ('introduction_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), ], options={ - 'abstract': False, + 'verbose_name_plural': 'Annuaires', + 'verbose_name': 'Annuaire (clubs, partenaires, bons plans...)', }, bases=('wagtailcore.page',), ), - migrations.CreateModel( - name='COFEvent', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('title', models.TextField(verbose_name='Titre')), - ('title_fr', models.TextField(null=True, verbose_name='Titre')), - ('title_en', models.TextField(null=True, verbose_name='Titre')), - ('description', wagtail.wagtailcore.fields.RichTextField(verbose_name='Description (concise)')), - ('description_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description (concise)')), - ('description_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description (concise)')), - ('date_start', models.DateTimeField(verbose_name='Date et heure de début')), - ('date_end', models.DateTimeField(null=True, verbose_name='Date et heure de fin')), - ('all_day', models.BooleanField(verbose_name='Toute la journée', default=False)), - ], - ), migrations.CreateModel( name='COFPage', fields=[ - ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), - ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), @@ -145,20 +166,21 @@ class Migration(migrations.Migration): ('body_en', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock())), null=True)), ], options={ - 'abstract': False, + 'verbose_name_plural': 'Pages normales COF', + 'verbose_name': 'Page normale COF', }, bases=('wagtailcore.page',), ), migrations.CreateModel( name='COFRootPage', fields=[ - ('page_ptr', models.OneToOneField(primary_key=True, parent_link=True, serialize=False, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('slug_en', models.SlugField(null=True, verbose_name='slug', help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), - ('url_path_en', models.TextField(null=True, blank=True, verbose_name='URL path', editable=False)), + ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), + ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), + ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), @@ -168,8 +190,9 @@ class Migration(migrations.Migration): ('introduction_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), ], options={ - 'abstract': False, + 'verbose_name_plural': 'Racines site du COF', + 'verbose_name': 'Racine site du COF', }, - bases=('wagtailcore.page',), + bases=('wagtailcore.page', gestioncof.cms.models.COFActuIndexMixin), ), ] diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index eb50df48..ef957b5c 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -10,8 +10,17 @@ from wagtail.wagtailimages.blocks import ImageChooserBlock from wagtail.wagtailadmin.edit_handlers import FieldPanel, StreamFieldPanel from wagtail.wagtailsnippets.models import register_snippet +# Page pouvant afficher des actualités +class COFActuIndexMixin(): + @property + def actus(self): + actus = COFActuPage.objects.live().descendant_of(self) + events = COFActuEventPage.objects.live().descendant_of(self) + genactus = list(actus) + list(events) + return genactus + # Racine du site du COF -class COFRootPage(Page): +class COFRootPage(Page, COFActuIndexMixin): introduction = RichTextField("Introduction") content_panels = Page.content_panels + [ @@ -19,6 +28,10 @@ class COFRootPage(Page): ] subpage_types = ['COFActuIndexPage', 'COFPage', 'COFDirectoryPage'] + + class Meta: + verbose_name = "Racine site du COF" + verbose_name_plural = "Racines site du COF" # Page lambda du site class COFPage(Page): @@ -32,58 +45,95 @@ class COFPage(Page): StreamFieldPanel('body'), ] - subpages_types = ['COFDirectoryPage', 'COFPage'] + subpage_types = ['COFDirectoryPage', 'COFPage'] parent_page_types = ['COFPage', 'COFRootPage'] - -# Évènements -@register_snippet -class COFEvent(models.Model): - title = models.TextField("Titre") - description = RichTextField("Description (concise)") - date_start = models.DateTimeField("Date et heure de début") - date_end = models.DateTimeField("Date et heure de fin", null=True) - all_day = models.BooleanField("Toute la journée", default=False, blank=True) - - panels = [ - FieldPanel("title"), - FieldPanel("description"), - FieldPanel("date_start"), - FieldPanel("date_end"), - FieldPanel("all_day"), - ] + class Meta: + verbose_name = "Page normale COF" + verbose_name_plural = "Pages normales COF" # Actualités -class COFActuIndexPage(Page): - subpages_types = ['COFActuPage'] +class COFActuIndexPage(Page, COFActuIndexMixin): + subpage_types = ['COFActuPage', 'COFActuEventPage'] parent_page_types = ['COFRootPage'] + + class Meta: + verbose_name = "Index des actualités" + verbose_name_plural = "Indexs des actualités" class COFActuPage(Page): body = RichTextField("Contenu") date = models.DateField("Date du post") image = models.ForeignKey( - 'wagtailimages.Image', name="Image à la Une", + 'wagtailimages.Image', verbose_name="Image à la Une", null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) content_panels = Page.content_panels + [ FieldPanel('date'), -# ImageChooserPanel('image'), + ImageChooserPanel('image'), FieldPanel('body', classname="full"), ] - - subpages_types = [] + subpage_types = [] parent_page_types = ['COFActuIndexPage'] + class Meta: + verbose_name = "Actualité simple" + verbose_name_plural = "Actualités simples" + +# Évènements +class COFActuEventPage(Page): + chapo = models.TextField("Description rapide") + body = RichTextField("Description longue") + image = models.ForeignKey( + 'wagtailimages.Image', verbose_name="Image à la Une", + null=True, blank=True, + on_delete=models.SET_NULL, related_name='+' + ) + + date_start = models.DateTimeField("Date et heure de début") + date_end = models.DateTimeField("Date et heure de fin", blank=True, default=None, null=True) + all_day = models.BooleanField("Toute la journée", default=False, blank=True) + + content_panels = Page.content_panels + [ + ImageChooserPanel('image'), + FieldPanel('chapo'), + FieldPanel('body', classname="full"), + FieldPanel("date_start"), + FieldPanel("date_end"), + FieldPanel("all_day"), + ] + + subpage_types = [] + parent_page_types = ['COFActuIndexPage'] + + class Meta: + verbose_name = "Actu liée à un évènement" + verbose_name_plural = "Actus liées à des évènements" + # Annuaires (Clubs, partenaires, bonnes adresses) class COFDirectoryPage(Page): introduction = RichTextField("Introduction") - subpages_types = ['COFActuPage', 'COFDirectoryEntryPage'] + content_panels = Page.content_panels + [ + FieldPanel('introduction'), + ] + + subpage_types = ['COFActuPage', 'COFDirectoryEntryPage'] parent_page_types = ['COFRootPage', 'COFPage'] + @property + def entries(self): + entries = COFDirectoryEntryPage.objects.live().descendant_of(self) + return entries + + class Meta: + verbose_name = "Annuaire (clubs, partenaires, bons plans...)" + verbose_name_plural = "Annuaires" + + class COFDirectoryEntryPage(Page): body = RichTextField("Description") links = StreamField([ @@ -96,13 +146,22 @@ class COFDirectoryEntryPage(Page): ('texte', blocks.CharBlock()), ])), ]) - + image = models.ForeignKey( - 'wagtailimages.Image', name="Image", + 'wagtailimages.Image', verbose_name="Image", null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) - subpages_types = [] + content_panels = Page.content_panels + [ + ImageChooserPanel('image'), + FieldPanel('body', classname="full"), + StreamFieldPanel("links"), + ] + + subpage_types = [] parent_page_types = ['COFDirectoryPage'] + class Meta: + verbose_name = "Éntrée d'annuaire" + verbose_name_plural = "Éntrées d'annuaire" diff --git a/gestioncof/cms/static/cofcms/config.rb b/gestioncof/cms/static/cofcms/config.rb new file mode 100644 index 00000000..826a3727 --- /dev/null +++ b/gestioncof/cms/static/cofcms/config.rb @@ -0,0 +1,25 @@ +require 'compass/import-once/activate' +# Require any additional compass plugins here. + +# Set this to the root of your project when deployed: +http_path = "/" +css_dir = "css" +sass_dir = "sass" +images_dir = "images" +javascripts_dir = "js" + +# You can select your preferred output style here (can be overridden via the command line): +# output_style = :expanded or :nested or :compact or :compressed + +# To enable relative paths to assets via compass helper functions. Uncomment: +# relative_assets = true + +# To disable debugging comments that display the original location of your selectors. Uncomment: +# line_comments = false + + +# If you prefer the indented syntax, you might want to regenerate this +# project again passing --syntax sass, or you can uncomment this: +# preferred_syntax = :sass +# and then run: +# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass diff --git a/gestioncof/cms/static/cofcms/css/ie.css b/gestioncof/cms/static/cofcms/css/ie.css new file mode 100644 index 00000000..5cd5b6c5 --- /dev/null +++ b/gestioncof/cms/static/cofcms/css/ie.css @@ -0,0 +1,5 @@ +/* Welcome to Compass. Use this file to write IE specific override styles. + * Import this file using the following HTML or equivalent: + * */ diff --git a/gestioncof/cms/static/cofcms/css/print.css b/gestioncof/cms/static/cofcms/css/print.css new file mode 100644 index 00000000..b0e9e456 --- /dev/null +++ b/gestioncof/cms/static/cofcms/css/print.css @@ -0,0 +1,3 @@ +/* Welcome to Compass. Use this file to define print styles. + * Import this file using the following HTML or equivalent: + * */ diff --git a/gestioncof/cms/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css new file mode 100644 index 00000000..a2497901 --- /dev/null +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -0,0 +1,122 @@ +/* Welcome to Compass. + * 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 */ +html, body, div, span, applet, object, iframe, +h1, h2, h3, h4, h5, h6, p, blockquote, pre, +a, abbr, acronym, address, big, cite, code, +del, dfn, em, img, ins, kbd, q, s, samp, +small, strike, strong, sub, sup, tt, var, +b, u, i, center, +dl, dt, dd, ol, ul, li, +fieldset, form, label, legend, +table, caption, tbody, tfoot, thead, tr, th, td, +article, aside, canvas, details, embed, +figure, figcaption, footer, header, hgroup, +menu, nav, output, ruby, section, summary, +time, mark, audio, video { + margin: 0; + padding: 0; + border: 0; + font: inherit; + font-size: 100%; + vertical-align: baseline; +} + +/* 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.3.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 */ +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 */ +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 */ +q, blockquote { + quotes: none; +} +/* 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.3.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 */ +article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { + display: block; +} + +/* line 12, ../sass/screen.scss */ +body { + background: #ff7869; + font: 17px "Source Sans Pro", "sans-serif"; +} + +/* line 17, ../sass/screen.scss */ +header { + background: #02c082; +} + +/* line 21, ../sass/screen.scss */ +h1, h2 { + font-family: "Carter One"; +} + +/* line 25, ../sass/screen.scss */ +h1 { + font-size: 2.3em; +} + +/* line 29, ../sass/screen.scss */ +a { + color: #fff; + text-decoration: none; +} + +/* line 36, ../sass/screen.scss */ +header nav ul { + display: flex; +} +/* line 38, ../sass/screen.scss */ +header nav ul li { + display: inline-block; +} +/* line 40, ../sass/screen.scss */ +header nav ul li > * { + display: block; + padding: 10px 15px; + font-weight: bold; +} +/* line 45, ../sass/screen.scss */ +header nav ul li > *:hover { + background: #018e60; +} +/* line 52, ../sass/screen.scss */ +header section { + display: flex; + width: 100%; + justify-content: space-between; + align-items: stretch; +} diff --git a/gestioncof/cms/static/cofcms/sass/_colors.scss b/gestioncof/cms/static/cofcms/sass/_colors.scss new file mode 100644 index 00000000..a2bc8fc2 --- /dev/null +++ b/gestioncof/cms/static/cofcms/sass/_colors.scss @@ -0,0 +1,4 @@ +$fond: #ff7869; +$bandeau: #02c082; +$aside: #ffe896; +$titre: #e23427; diff --git a/gestioncof/cms/static/cofcms/sass/screen.scss b/gestioncof/cms/static/cofcms/sass/screen.scss new file mode 100644 index 00000000..76d2ef20 --- /dev/null +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -0,0 +1,58 @@ +/* Welcome to Compass. + * 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'); + +@import "compass/reset"; + +@import "_colors"; + +body { + background: $fond; + font: 17px "Source Sans Pro", "sans-serif"; +} + +header { + background: $bandeau; +} + +h1, h2 { + font-family: "Carter One"; +} + +h1 { + font-size: 2.3em; +} + +a { + color: #fff; + text-decoration: none; +} + +header { + nav { + ul { + display: flex; + li { + display: inline-block; + > * { + display: block; + padding: 10px 15px; + font-weight: bold; + + &:hover { + background: darken($bandeau, 10%); + } + } + } + } + } + section { + display: flex; + width: 100%; + justify-content: space-between; + align-items: stretch; + } +} diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html new file mode 100644 index 00000000..e6aea017 --- /dev/null +++ b/gestioncof/cms/templates/cofcms/base.html @@ -0,0 +1,31 @@ +{% load static menu_tags wagtailuserbar %} + + + + + + {% block title %}Association des élèves de l'ENS Ulm{% endblock %} + + + + +
+
+

COF

+ +
+
+ +
+
+ +
+ {% block content %}{% endblock %} +
+ {% wagtailuserbar %} + + diff --git a/gestioncof/cms/templates/cofcms/base_nav.html b/gestioncof/cms/templates/cofcms/base_nav.html new file mode 100644 index 00000000..6ec8a8ed --- /dev/null +++ b/gestioncof/cms/templates/cofcms/base_nav.html @@ -0,0 +1,9 @@ + diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html new file mode 100644 index 00000000..3146e6cd --- /dev/null +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -0,0 +1,18 @@ +{% extends "cofcms/base.html" %} +{% load wagtailimages_tags cofcms_tags %} + +{% block content %} +
+

{{ page.title }}

+
{{ page.introduction|safe }}
+
+ +
+ {% for actu in page.actus %} +
+

{{ actu.title }}

+ {{ actu.body|safe }} +
+ {% endfor %} +
+{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_directory_page.html b/gestioncof/cms/templates/cofcms/cof_directory_page.html new file mode 100644 index 00000000..8f0e6746 --- /dev/null +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -0,0 +1,34 @@ +{% extends "cofcms/base.html" %} +{% load wagtailimages_tags cofcms_tags %} + +{% block content %} +
+

{{ page.title }}

+
{{ page.introduction|safe }}
+
+ +
+ {% for entry in page.entries %} +
+

{{ entry.title }}

+ {% if entry.image %} + {% image entry.image width-400 class="entry-img" %} + {% endif %} +
{{ entry.body|safe }}
+ {% if entry.links %} + + {% endif %} +
+ {% endfor %} +
+{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_page.html b/gestioncof/cms/templates/cofcms/cof_page.html new file mode 100644 index 00000000..d6e64e56 --- /dev/null +++ b/gestioncof/cms/templates/cofcms/cof_page.html @@ -0,0 +1,23 @@ +{% extends "cofcms/base.html" %} +{% load wagtailimages_tags cofcms_tags %} + +{% block content %} +
+

{{ page.title }}

+
{{ page.introduction|safe }}
+
+ +
+ {% for block in page.body %} + {% if block.block_type == "heading" %} +

{{ block.value }}

+ {% else %}{% if block.block_type == "paragraph" %} +
+ {{ block.value|safe }} +
+ {% else %}{% if block.block_type == "image" %} + {% image block.value width-400 %} + {% endif %}{% endif %}{% endif %} + {% endfor %} +
+{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_root_page.html b/gestioncof/cms/templates/cofcms/cof_root_page.html new file mode 100644 index 00000000..a730ec7a --- /dev/null +++ b/gestioncof/cms/templates/cofcms/cof_root_page.html @@ -0,0 +1,2 @@ +{% extends "cofcms/base.html" %} + diff --git a/gestioncof/cms/templatetags/__init__.py b/gestioncof/cms/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/gestioncof/cms/templatetags/cofcms_tags.py b/gestioncof/cms/templatetags/cofcms_tags.py new file mode 100644 index 00000000..223d9b81 --- /dev/null +++ b/gestioncof/cms/templatetags/cofcms_tags.py @@ -0,0 +1,12 @@ +from datetime import date +from django import template +from django.conf import settings + +import re + +register = template.Library() + +@register.filter() +def obfuscate_mail(value): + val = value.replace('', '/').replace('@', 'arbse').replace('.', 'pnt') + return val diff --git a/gestioncof/cms/translation.py b/gestioncof/cms/translation.py index 705a9c1f..2be97221 100644 --- a/gestioncof/cms/translation.py +++ b/gestioncof/cms/translation.py @@ -1,4 +1,4 @@ -from .models import COFRootPage, COFPage, COFEvent, COFActuIndexPage, COFActuPage, COFDirectoryPage, COFDirectoryEntryPage +from .models import COFRootPage, COFPage, COFActuEventPage, COFActuIndexPage, COFActuPage, COFDirectoryPage, COFDirectoryEntryPage from wagtail_modeltranslation.translator import WagtailTranslationOptions from modeltranslation.decorators import register @@ -15,11 +15,11 @@ class COFPageTr(WagtailTranslationOptions): 'body', ) -@register(COFEvent) -class COFEventTr(WagtailTranslationOptions): +@register(COFActuEventPage) +class COFActuEventPageTr(WagtailTranslationOptions): fields = ( - 'title', - 'description', + 'chapo', + 'body', ) @register(COFActuIndexPage) From 53658589f89e3883567b9ca6c23cdee9cb62eea2 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 20 Aug 2017 00:39:19 +0200 Subject: [PATCH 005/773] Nouvelles couleurs, Plus de templates, Calendrier (sommaire) --- gestioncof/cms/models.py | 5 +- gestioncof/cms/static/cofcms/css/screen.css | 141 ++++++++++++++---- .../cms/static/cofcms/sass/_colors.scss | 10 +- gestioncof/cms/static/cofcms/sass/screen.scss | 103 ++++++++++++- gestioncof/cms/templates/cofcms/base.html | 6 +- .../cms/templates/cofcms/base_aside.html | 9 ++ gestioncof/cms/templates/cofcms/calendar.html | 14 ++ .../templates/cofcms/cof_actu_index_page.html | 6 +- .../templates/cofcms/cof_directory_page.html | 11 +- .../cms/templates/cofcms/cof_root_page.html | 26 +++- gestioncof/cms/templatetags/cofcms_tags.py | 43 +++++- 11 files changed, 330 insertions(+), 44 deletions(-) create mode 100644 gestioncof/cms/templates/cofcms/base_aside.html create mode 100644 gestioncof/cms/templates/cofcms/calendar.html diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index ef957b5c..69f56ae0 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -96,6 +96,7 @@ class COFActuEventPage(Page): date_start = models.DateTimeField("Date et heure de début") date_end = models.DateTimeField("Date et heure de fin", blank=True, default=None, null=True) all_day = models.BooleanField("Toute la journée", default=False, blank=True) + is_event = True content_panels = Page.content_panels + [ ImageChooserPanel('image'), @@ -108,11 +109,11 @@ class COFActuEventPage(Page): subpage_types = [] parent_page_types = ['COFActuIndexPage'] - + class Meta: verbose_name = "Actu liée à un évènement" verbose_name_plural = "Actus liées à des évènements" - + # Annuaires (Clubs, partenaires, bonnes adresses) class COFDirectoryPage(Page): introduction = RichTextField("Introduction") diff --git a/gestioncof/cms/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css index a2497901..ed2eb06f 100644 --- a/gestioncof/cms/static/cofcms/css/screen.css +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -69,54 +69,143 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, } /* line 12, ../sass/screen.scss */ +*, *:after, *:before { + box-sizing: content-box; +} + +/* line 16, ../sass/screen.scss */ body { - background: #ff7869; + background: #ffcc6f; font: 17px "Source Sans Pro", "sans-serif"; } -/* line 17, ../sass/screen.scss */ +/* line 21, ../sass/screen.scss */ header { - background: #02c082; + background: #30355a; } -/* line 21, ../sass/screen.scss */ +/* line 25, ../sass/screen.scss */ h1, h2 { font-family: "Carter One"; } -/* line 25, ../sass/screen.scss */ +/* line 29, ../sass/screen.scss */ h1 { font-size: 2.3em; } -/* line 29, ../sass/screen.scss */ +/* line 33, ../sass/screen.scss */ +h2 { + font-size: 1.6em; +} + +/* line 37, ../sass/screen.scss */ a { - color: #fff; + color: #f9752b; text-decoration: none; } -/* line 36, ../sass/screen.scss */ -header nav ul { - display: flex; -} -/* line 38, ../sass/screen.scss */ -header nav ul li { - display: inline-block; -} -/* line 40, ../sass/screen.scss */ -header nav ul li > * { - display: block; - padding: 10px 15px; - font-weight: bold; -} -/* line 45, ../sass/screen.scss */ -header nav ul li > *:hover { - background: #018e60; -} -/* line 52, ../sass/screen.scss */ +/* line 43, ../sass/screen.scss */ header section { display: flex; width: 100%; justify-content: space-between; align-items: stretch; } +/* line 49, ../sass/screen.scss */ +header section.bottom-menu { + justify-content: space-around; + text-align: center; + background: #47395e; +} +/* line 55, ../sass/screen.scss */ +header h1 { + padding: 0 15px; +} +/* line 59, ../sass/screen.scss */ +header nav ul { + display: flex; +} +/* line 61, ../sass/screen.scss */ +header nav ul li { + display: inline-block; +} +/* line 63, ../sass/screen.scss */ +header nav ul li > * { + display: block; + padding: 10px 15px; + font-weight: bold; +} +/* line 68, ../sass/screen.scss */ +header nav ul li > *:hover { + background: #1e2139; +} + +/* line 77, ../sass/screen.scss */ +.container { + max-width: 1000px; + margin: 0 auto; + position: relative; +} +/* line 82, ../sass/screen.scss */ +.container .aside-wrap { + position: absolute; + top: 30px; + height: 100%; + width: 250px; +} +/* line 88, ../sass/screen.scss */ +.container .aside-wrap .aside { + position: fixed; + position: sticky; + top: 5px; + width: 100%; + background: #7a504c; + padding: 15px; + box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.3); +} +/* line 97, ../sass/screen.scss */ +.container .aside-wrap .aside .calendar { + margin: 0 auto; + display: block; +} +/* line 104, ../sass/screen.scss */ +.container .content { + max-width: 700px; + margin-left: auto; + margin-right: 0; +} +/* line 111, ../sass/screen.scss */ +.container .content section article { + background: #fff; + padding: 30px; + box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.3); +} +/* line 115, ../sass/screen.scss */ +.container .content section article a { + color: #30355a; +} +/* line 120, ../sass/screen.scss */ +.container .content section article + h2 { + margin-top: 15px; +} + +/* line 128, ../sass/screen.scss */ +.calendar td, .calendar th { + text-align: center; + vertical-align: center; + border: 2px solid transparent; + padding: 1px; +} +/* line 135, ../sass/screen.scss */ +.calendar th { + font-weight: bold; +} +/* line 140, ../sass/screen.scss */ +.calendar td.out { + opacity: 0.3; +} +/* line 143, ../sass/screen.scss */ +.calendar td.today { + border-bottom-color: #000; +} diff --git a/gestioncof/cms/static/cofcms/sass/_colors.scss b/gestioncof/cms/static/cofcms/sass/_colors.scss index a2bc8fc2..32e8088c 100644 --- a/gestioncof/cms/static/cofcms/sass/_colors.scss +++ b/gestioncof/cms/static/cofcms/sass/_colors.scss @@ -1,4 +1,6 @@ -$fond: #ff7869; -$bandeau: #02c082; -$aside: #ffe896; -$titre: #e23427; +$fond: #ffcc6f; +$bandeau: #30355a; +$sousbandeau: #47395e; +$aside: #7a504c; +$titre: #31597e; +$lien: #f9752b; diff --git a/gestioncof/cms/static/cofcms/sass/screen.scss b/gestioncof/cms/static/cofcms/sass/screen.scss index 76d2ef20..42373298 100644 --- a/gestioncof/cms/static/cofcms/sass/screen.scss +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -9,6 +9,10 @@ @import "_colors"; +*, *:after, *:before { + box-sizing: content-box; +} + body { background: $fond; font: 17px "Source Sans Pro", "sans-serif"; @@ -26,18 +30,37 @@ h1 { font-size: 2.3em; } +h2 { + font-size: 1.6em; +} + a { - color: #fff; + color: $lien; text-decoration: none; } header { + section { + display: flex; + width: 100%; + justify-content: space-between; + align-items: stretch; + + &.bottom-menu { + justify-content: space-around; + text-align: center; + background: $sousbandeau; + } + } + h1 { + padding: 0 15px; + } nav { ul { display: flex; li { display: inline-block; - > * { + & > * { display: block; padding: 10px 15px; font-weight: bold; @@ -49,10 +72,76 @@ header { } } } - section { - display: flex; - width: 100%; - justify-content: space-between; - align-items: stretch; +} + +.container { + max-width: 1000px; + margin: 0 auto; + position: relative; + + .aside-wrap { + position: absolute; + top: 30px; + height: 100%; + width: 250px; + + .aside { + position: fixed; + position: sticky; + top: 5px; + width: 100%; + background: $aside; + padding: 15px; + box-shadow: -4px 4px 1px rgba(#000, 0.3); + + .calendar { + margin: 0 auto; + display: block; + } + } + } + + .content { + max-width: 700px; + margin-left: auto; + margin-right: 0; + + + section { + article { + background: #fff; + padding: 30px; + box-shadow: -4px 4px 1px rgba(#000, 0.3); + a { + color: $bandeau; + } + } + + article + h2 { + margin-top: 15px; + } + } + } +} + +.calendar { + td, th { + text-align: center; + vertical-align: center; + border: 2px solid transparent; + padding: 1px; + } + + th { + font-weight: bold; + } + + td { + &.out { + opacity: 0.3; + } + &.today { + border-bottom-color: #000; + } } } diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index e6aea017..dbf0faa4 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -24,7 +24,11 @@
- {% block content %}{% endblock %} + {% block superaside %}{% endblock %} + +
+ {% block content %}{% endblock %} +
{% wagtailuserbar %} diff --git a/gestioncof/cms/templates/cofcms/base_aside.html b/gestioncof/cms/templates/cofcms/base_aside.html new file mode 100644 index 00000000..49404432 --- /dev/null +++ b/gestioncof/cms/templates/cofcms/base_aside.html @@ -0,0 +1,9 @@ +{% extends "cofcms/base.html" %} + +{% block superaside %} +
+
+ {% block aside %}{% endblock %} +
+
+{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/calendar.html b/gestioncof/cms/templates/cofcms/calendar.html new file mode 100644 index 00000000..98eacdab --- /dev/null +++ b/gestioncof/cms/templates/cofcms/calendar.html @@ -0,0 +1,14 @@ + + + + {% for week in weeks %} + + {% for day in week %} + + {% endfor %} + + {% endfor %} + +
LMMJVSD
+ {% if day.events %}{{ day.day }}{% else %}{{ day.day }}{% endif %} +
diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index 3146e6cd..d0f15ae4 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -1,6 +1,10 @@ -{% extends "cofcms/base.html" %} +{% extends "cofcms/base_aside.html" %} {% load wagtailimages_tags cofcms_tags %} +{% block aside %} + {% calendar %} +{% endblock %} + {% block content %}

{{ page.title }}

diff --git a/gestioncof/cms/templates/cofcms/cof_directory_page.html b/gestioncof/cms/templates/cofcms/cof_directory_page.html index 8f0e6746..62d6eff9 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -1,6 +1,15 @@ -{% extends "cofcms/base.html" %} +{% extends "cofcms/base_aside.html" %} {% load wagtailimages_tags cofcms_tags %} +{% block aside %} +

Accès rapide

+ +{% endblock %} + {% block content %}

{{ page.title }}

diff --git a/gestioncof/cms/templates/cofcms/cof_root_page.html b/gestioncof/cms/templates/cofcms/cof_root_page.html index a730ec7a..a734abe0 100644 --- a/gestioncof/cms/templates/cofcms/cof_root_page.html +++ b/gestioncof/cms/templates/cofcms/cof_root_page.html @@ -1,2 +1,26 @@ -{% extends "cofcms/base.html" %} +{% extends "cofcms/base_aside.html" %} +{% load static cofcms_tags wagtailimages_tags %} +{% block aside %} + {% calendar %} +{% endblock %} + +{% block content %} +
+

{{ page.title }}

+
{{ page.introduction|safe }}
+
+ +
+ {% for actu in page.actus %} + {% if actu.is_event %} +
+

{{ actu.title }}

+ {% if actu.image %} + {% image actu.image fill-400x200 class="actu-img" %} + {% endif %} +
+ {% endif %} + {% endfor %} +
+{% endblock %} diff --git a/gestioncof/cms/templatetags/cofcms_tags.py b/gestioncof/cms/templatetags/cofcms_tags.py index 223d9b81..ef8cdd41 100644 --- a/gestioncof/cms/templatetags/cofcms_tags.py +++ b/gestioncof/cms/templatetags/cofcms_tags.py @@ -1,6 +1,9 @@ -from datetime import date +from datetime import timedelta, date from django import template from django.conf import settings +from django.utils import timezone + +from ..models import COFActuEventPage import re @@ -10,3 +13,41 @@ register = template.Library() def obfuscate_mail(value): val = value.replace('', '/').replace('@', 'arbse').replace('.', 'pnt') return val + +@register.inclusion_tag("cofcms/calendar.html") +def calendar(): + now = timezone.now() + month_start = date(now.year, now.month, 1) + next_month = month_start + timedelta(days=32) + next_month = date(next_month.year, next_month.month, 1) + month_prestart = month_start - timedelta(days=(month_start.weekday()+7)%7) + month_postend = next_month + timedelta(days=(next_month.weekday()+6)%7) + events = COFActuEventPage.objects.live()\ + .filter(date_start__range=[month_prestart, + month_postend])\ + .order_by('-date_start') + events = list(events) + weeks = [] + curday = month_prestart + deltaday = timedelta(days=1) + while curday < next_month and len(weeks)<10: + week = [] + for k in range(7): + curevents = [] + for k in range(len(events)-1, -1, -1): + e = events[k] + if e.date_start.date() > curday: break + if (e.date_start if e.date_end is None else e.date_end).date() < curday: + del events[k] + else: + curevents.append(e) + print(curevents) + day = {'day': curday.day, + 'class': (('today ' if (curday.day == now.day + and curday.month == now.month) else '') + + ('in' if curday.month == now.month else 'out')), + 'events': curevents} + week.append(day) + curday += deltaday + weeks.append(week) + return {"events": events, "weeks": weeks} From 09e63bf00cb0e39bc92f91082d3b5ab3ef37f0fe Mon Sep 17 00:00:00 2001 From: Evarin Date: Tue, 22 Aug 2017 00:58:18 +0200 Subject: [PATCH 006/773] Actus et listes de clubs plus jolies et fonctionnelles, calendriers (beta) --- gestioncof/cms/models.py | 39 ++++++++++- gestioncof/cms/static/cofcms/css/screen.css | 69 +++++++++++++++++-- gestioncof/cms/static/cofcms/js/script.js | 13 ++++ gestioncof/cms/static/cofcms/sass/screen.scss | 68 +++++++++++++++++- gestioncof/cms/templates/cofcms/base.html | 1 + .../templates/cofcms/cof_directory_page.html | 13 ++-- .../cms/templates/cofcms/cof_root_page.html | 15 ++-- .../cms/templates/cofcms/mini_calendar.html | 11 +++ gestioncof/cms/templatetags/cofcms_tags.py | 19 ++++- 9 files changed, 230 insertions(+), 18 deletions(-) create mode 100644 gestioncof/cms/static/cofcms/js/script.js create mode 100644 gestioncof/cms/templates/cofcms/mini_calendar.html diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index 69f56ae0..9b72568e 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -109,7 +109,44 @@ class COFActuEventPage(Page): subpage_types = [] parent_page_types = ['COFActuIndexPage'] - + + @property + def dates(self): + if self.date_end: + if self.date_end.date() == self.date_start.date(): + if self.all_day: + return self.date_start.strftime("le %A %w %B %Y") + else: + return "le %s à %s" % \ + (self.date_start.strftime("%A %w %B %Y de %Hh%M"), + self.date_end.strftime("%Hh%M")) + else: + tmpl = "%A %w %B %Y" + diff_i = len(tmpl) + if self.date_end.year != self.date_start.year: + diff_i = len(tmpl) + elif self.date_end.month != self.date_start.month: + diff_i = len(tmpl) - 3 + elif self.date_end.day != self.date_start.day: + diff_i = len(tmpl) - 6 + common = tmpl[diff_i:] + diff = tmpl[:diff_i] + if self.all_day: + return "du %s au %s %s" % (self.date_start.strftime(diff), + self.date_end.strftime(diff), + self.date_end.strftime(common)) + else: + return "du %s %s %s au %s %s" % \ + (self.date_start.strftime(diff), + self.date_start.strftime(common), + self.date_start.strftime("%Hh%M"), + self.date_end.strftime(diff), + self.date_end.strftime("%Hh%M")) + else: + if self.all_day: + return self.date_start.strftime("le %A %w %B %Y") + else: + return self.date_start.strftime("le %A %w %B %Y à %Hh%M") class Meta: verbose_name = "Actu liée à un évènement" verbose_name_plural = "Actus liées à des évènements" diff --git a/gestioncof/cms/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css index ed2eb06f..5d41db14 100644 --- a/gestioncof/cms/static/cofcms/css/screen.css +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -70,7 +70,7 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, /* line 12, ../sass/screen.scss */ *, *:after, *:before { - box-sizing: content-box; + box-sizing: border-box; } /* line 16, ../sass/screen.scss */ @@ -189,23 +189,82 @@ header nav ul li > *:hover { .container .content section article + h2 { margin-top: 15px; } +/* line 125, ../sass/screen.scss */ +.container .content section.directory article.entry { + width: 80%; + max-width: 600px; + max-height: 100%; + position: relative; + padding-right: 120px; +} +/* line 132, ../sass/screen.scss */ +.container .content section.directory article.entry .entry-image { + display: block; + position: absolute; + width: 150px; + background: #fff; + box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.2); + padding: 1px; + overflow: hidden; + right: 100px; + transform: translateX(90%); + top: -15px; +} +/* line 144, ../sass/screen.scss */ +.container .content section.directory article.entry .entry-image img { + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; +} +/* line 155, ../sass/screen.scss */ +.container .content section.actuhome article.actu { + background: none; + box-shadow: none; + max-width: 400px; +} +/* line 160, ../sass/screen.scss */ +.container .content section.actuhome article.actu .actu-header { + position: relative; + box-shadow: -4px 5px 1px rgba(0, 0, 0, 0.3); + padding: 0; + margin: 0; + overflow: hidden; +} +/* line 167, ../sass/screen.scss */ +.container .content section.actuhome article.actu .actu-header img { + position: absolute; + top: 0; + left: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + z-index: -1; +} +/* line 177, ../sass/screen.scss */ +.container .content section.actuhome article.actu .actu-header h2 { + width: 100%; + height: 100%; + padding-top: 150px; +} -/* line 128, ../sass/screen.scss */ +/* line 194, ../sass/screen.scss */ .calendar td, .calendar th { text-align: center; vertical-align: center; border: 2px solid transparent; padding: 1px; } -/* line 135, ../sass/screen.scss */ +/* line 201, ../sass/screen.scss */ .calendar th { font-weight: bold; } -/* line 140, ../sass/screen.scss */ +/* line 206, ../sass/screen.scss */ .calendar td.out { opacity: 0.3; } -/* line 143, ../sass/screen.scss */ +/* line 209, ../sass/screen.scss */ .calendar td.today { border-bottom-color: #000; } diff --git a/gestioncof/cms/static/cofcms/js/script.js b/gestioncof/cms/static/cofcms/js/script.js new file mode 100644 index 00000000..c7693a6d --- /dev/null +++ b/gestioncof/cms/static/cofcms/js/script.js @@ -0,0 +1,13 @@ +$(function() { + $(".facteur").on("click", function(){ + var $this = $(this); + var sticker = $this.attr('data-mref') + .replace('pont', '.') + .replace('arbre', '@') + .replace(/(.)-/g, '$1'); + + var boite = $("", {href:"ma"+"il"+"to:"+sticker}).text(sticker); + $(this).before(boite) + .remove(); + }) +}); diff --git a/gestioncof/cms/static/cofcms/sass/screen.scss b/gestioncof/cms/static/cofcms/sass/screen.scss index 42373298..ab374da7 100644 --- a/gestioncof/cms/static/cofcms/sass/screen.scss +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -10,7 +10,7 @@ @import "_colors"; *, *:after, *:before { - box-sizing: content-box; + box-sizing: border-box; } body { @@ -120,6 +120,72 @@ header { article + h2 { margin-top: 15px; } + + &.directory { + article.entry { + width: 80%; + max-width: 600px; + max-height: 100%; + position: relative; + padding-right: 120px; + + .entry-image { + display: block; + position: absolute; + width: 150px; + background: #fff; + box-shadow: -4px 4px 1px rgba(#000, 0.2); + padding: 1px; + overflow: hidden; + right: 100px; + transform: translateX(90%); + top: -15px; + + img { + width: auto; + height: auto; + max-width: 100%; + max-height: 100%; + } + } + } + } + + &.actuhome { + article.actu { + background: none; + box-shadow: none; + max-width: 400px; + + .actu-header { + position: relative; + box-shadow: -4px 5px 1px rgba(#000, 0.3); + padding: 0; + margin: 0; + overflow: hidden; + + img { + position: absolute; + top: 0; + left: 0; + min-height: 100%; + min-width: 100%; + height: auto; + width: auto; + z-index: -1; + } + h2 { + width: 100%; + height: 100%; + padding-top: 150px; + } + } + + .actu-misc { + + } + } + } } } } diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index dbf0faa4..05119dbd 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -6,6 +6,7 @@ {% block title %}Association des élèves de l'ENS Ulm{% endblock %} + {% 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 62d6eff9..0c0b84da 100644 --- a/gestioncof/cms/templates/cofcms/cof_directory_page.html +++ b/gestioncof/cms/templates/cofcms/cof_directory_page.html @@ -1,6 +1,11 @@ {% extends "cofcms/base_aside.html" %} -{% load wagtailimages_tags cofcms_tags %} +{% load wagtailimages_tags cofcms_tags static %} +{% block extra_head %} + {{ block.super }} + + +{% endblock %} {% block aside %}

Accès rapide

    @@ -19,10 +24,10 @@
    {% for entry in page.entries %}
    -

    {{ entry.title }}

    {% if entry.image %} - {% image entry.image width-400 class="entry-img" %} +
    {% image entry.image width-150 class="entry-img" %}
    {% endif %} +

    {{ entry.title }}

    {{ entry.body|safe }}
    {% if entry.links %}
    {% endfor %}
    diff --git a/gestioncof/cms/templatetags/cofcms_tags.py b/gestioncof/cms/templatetags/cofcms_tags.py index 004fd21c..7d426e46 100644 --- a/gestioncof/cms/templatetags/cofcms_tags.py +++ b/gestioncof/cms/templatetags/cofcms_tags.py @@ -42,7 +42,6 @@ def calendar(): del events[k] else: curevents.append(e) - print(curevents) day = {'day': curday.day, 'class': (('today ' if (curday.day == now.day and curday.month == now.month) else '') From 5a22b1cd372c12d447db7610fa591143ead201d6 Mon Sep 17 00:00:00 2001 From: Evarin Date: Tue, 10 Oct 2017 11:22:02 +0200 Subject: [PATCH 010/773] Affichage des actus --- gestioncof/cms/static/cofcms/css/screen.css | 93 ++++++++++++------- gestioncof/cms/static/cofcms/sass/screen.scss | 31 +++++++ .../templates/cofcms/cof_actu_index_page.html | 17 ++-- 3 files changed, 103 insertions(+), 38 deletions(-) diff --git a/gestioncof/cms/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css index 5f543bb4..965206c3 100644 --- a/gestioncof/cms/static/cofcms/css/screen.css +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -169,21 +169,25 @@ article ul { article ul li { list-style: outside; } +/* line 98, ../sass/screen.scss */ +article:last-child { + margin-bottom: 30px; +} -/* line 100, ../sass/screen.scss */ +/* line 103, ../sass/screen.scss */ .container { max-width: 1000px; margin: 0 auto; position: relative; } -/* line 105, ../sass/screen.scss */ +/* line 108, ../sass/screen.scss */ .container .aside-wrap { position: absolute; top: 30px; height: 100%; width: 250px; } -/* line 111, ../sass/screen.scss */ +/* line 114, ../sass/screen.scss */ .container .aside-wrap .aside { color: #fff; position: fixed; @@ -194,43 +198,47 @@ article ul li { padding: 15px; box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.3); } -/* line 121, ../sass/screen.scss */ +/* line 124, ../sass/screen.scss */ +.container .aside-wrap .aside h2 { + color: #fff; +} +/* line 128, ../sass/screen.scss */ .container .aside-wrap .aside .calendar { margin: 0 auto; display: block; } -/* line 128, ../sass/screen.scss */ +/* line 135, ../sass/screen.scss */ .container .content { max-width: 700px; margin-left: auto; margin-right: 0; } -/* line 133, ../sass/screen.scss */ +/* line 140, ../sass/screen.scss */ .container .content .intro { border-bottom: 3px solid #a3d200; margin: 20px -10px; margin-top: 5px; padding: 15px 5px; } -/* line 143, ../sass/screen.scss */ +/* line 150, ../sass/screen.scss */ .container .content section article { background: #fff; padding: 30px; box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.3); } -/* line 147, ../sass/screen.scss */ +/* line 154, ../sass/screen.scss */ .container .content section article a { color: #3cb3a6; } -/* line 152, ../sass/screen.scss */ +/* line 159, ../sass/screen.scss */ .container .content section article + h2 { margin-top: 15px; } -/* line 156, ../sass/screen.scss */ +/* line 163, ../sass/screen.scss */ .container .content section article + article { margin-top: 25px; } -/* line 161, ../sass/screen.scss */ +/* line 168, ../sass/screen.scss */ .container .content section.directory article.entry { width: 80%; max-width: 600px; @@ -238,7 +246,7 @@ article ul li { position: relative; padding-right: 120px; } -/* line 168, ../sass/screen.scss */ +/* line 175, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image { display: block; position: absolute; @@ -251,25 +259,25 @@ article ul li { transform: translateX(90%); top: -15px; } -/* line 180, ../sass/screen.scss */ +/* line 187, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image img { width: auto; height: auto; max-width: 100%; max-height: 100%; } -/* line 190, ../sass/screen.scss */ +/* line 197, ../sass/screen.scss */ .container .content section.actuhome { display: flex; flex-wrap: wrap; justify-content: space-around; align-items: top; } -/* line 196, ../sass/screen.scss */ +/* line 203, ../sass/screen.scss */ .container .content section.actuhome article + article { margin: 0; } -/* line 200, ../sass/screen.scss */ +/* line 207, ../sass/screen.scss */ .container .content section.actuhome article.actu { position: relative; background: none; @@ -278,7 +286,7 @@ article ul li { min-width: 300px; flex: 1; } -/* line 208, ../sass/screen.scss */ +/* line 215, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header { position: relative; box-shadow: -4px 5px 1px rgba(0, 0, 0, 0.3); @@ -289,7 +297,7 @@ article ul li { background-size: cover; background-position: center center; } -/* line 218, ../sass/screen.scss */ +/* line 225, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 { position: absolute; width: 100%; @@ -299,11 +307,11 @@ article ul li { text-shadow: 0 0 5px rgba(0, 0, 0, 0.8); background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); } -/* line 226, ../sass/screen.scss */ +/* line 233, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 a { color: #fff; } -/* line 232, ../sass/screen.scss */ +/* line 239, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc { background: white; box-shadow: -2px 2px 1px rgba(0, 0, 0, 0.2); @@ -311,17 +319,17 @@ article ul li { padding: 15px; padding-top: 5px; } -/* line 239, ../sass/screen.scss */ +/* line 246, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-minical { display: block; } -/* line 242, ../sass/screen.scss */ +/* line 249, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-dates { display: block; text-align: right; font-size: 0.9em; } -/* line 249, ../sass/screen.scss */ +/* line 256, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-overlay { display: block; background: none; @@ -333,41 +341,64 @@ article ul li { z-index: 5; opacity: 0; } +/* line 272, ../sass/screen.scss */ +.container .content section.actulist article.actu { + display: flex; + width: 100%; + padding: 0; +} +/* line 277, ../sass/screen.scss */ +.container .content section.actulist article.actu .actu-image { + width: 30%; + max-width: 200px; + background-size: cover; + background-position: center center; +} +/* line 283, ../sass/screen.scss */ +.container .content section.actulist article.actu .actu-infos { + padding: 15px; + flex: 1; +} +/* line 287, ../sass/screen.scss */ +.container .content section.actulist article.actu .actu-infos .actu-dates { + font-weight: bold; + font-size: 0.9em; +} -/* line 268, ../sass/screen.scss */ +/* line 299, ../sass/screen.scss */ .calendar td, .calendar th { text-align: center; vertical-align: center; border: 2px solid transparent; padding: 1px; } -/* line 275, ../sass/screen.scss */ +/* line 306, ../sass/screen.scss */ .calendar th { font-weight: bold; } -/* line 279, ../sass/screen.scss */ +/* line 310, ../sass/screen.scss */ .calendar td { font-size: 0.8em; width: 25px; height: 30px; } -/* line 284, ../sass/screen.scss */ +/* line 315, ../sass/screen.scss */ .calendar td.out { opacity: 0.3; } -/* line 287, ../sass/screen.scss */ +/* line 318, ../sass/screen.scss */ .calendar td.today { border-bottom-color: #000; } -/* line 290, ../sass/screen.scss */ +/* line 321, ../sass/screen.scss */ .calendar td:nth-child(7) { background: rgba(0, 0, 0, 0.3); } -/* line 293, ../sass/screen.scss */ +/* line 324, ../sass/screen.scss */ .calendar td:nth-child(6) { background: rgba(0, 0, 0, 0.2); } -/* line 296, ../sass/screen.scss */ +/* line 327, ../sass/screen.scss */ .calendar td.hasevent { font-weight: bold; color: #3cb3a6; diff --git a/gestioncof/cms/static/cofcms/sass/screen.scss b/gestioncof/cms/static/cofcms/sass/screen.scss index aa278464..7a69cf5b 100644 --- a/gestioncof/cms/static/cofcms/sass/screen.scss +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -95,6 +95,9 @@ article { list-style: outside; } } + &:last-child { + margin-bottom: 30px; + } } .container { @@ -118,6 +121,10 @@ article { padding: 15px; box-shadow: -4px 4px 1px rgba(#000, 0.3); + h2 { + color: #fff; + } + .calendar { margin: 0 auto; display: block; @@ -260,6 +267,30 @@ article { } } + + &.actulist { + article.actu { + display:flex; + width: 100%; + padding: 0; + + .actu-image { + width: 30%; + max-width: 200px; + background-size: cover; + background-position: center center; + } + .actu-infos { + padding: 15px; + flex: 1; + + .actu-dates { + font-weight: bold; + font-size: 0.9em; + } + } + } + } } } } diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index bc2ecc2a..affd433d 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -14,13 +14,16 @@
    {% for actu in page.actus %}
    -

    {{ actu.title }}

    - {% if actu.is_event %} -

    {{ actu.chapo }}

    - {% else %} - {{ actu.body|safe|truncatewords_html:25 }} - {% endif %} - Lire plus > +
    +
    +

    {{ actu.title }}

    + {% if actu.is_event %} +

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

    + {% else %} + {{ actu.body|safe|truncatewords_html:25 }} + {% endif %} + Lire plus > +
    {% endfor %}
    From adf43889e1975d9e33f4580d6f19a9c5968ebbf5 Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 23 Oct 2017 11:05:49 +0200 Subject: [PATCH 011/773] Fixtures site cof --- gestioncof/cms/fixtures/wagtail_cof_cms.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 gestioncof/cms/fixtures/wagtail_cof_cms.json diff --git a/gestioncof/cms/fixtures/wagtail_cof_cms.json b/gestioncof/cms/fixtures/wagtail_cof_cms.json new file mode 100644 index 00000000..f0d74dcc --- /dev/null +++ b/gestioncof/cms/fixtures/wagtail_cof_cms.json @@ -0,0 +1 @@ +[{"pk": 11, "fields": {"url_path_fr": "/global/site-du-cof/", "title_fr": "Site du COF", "url_path_en": "/global/site-du-cof/", "seo_title_fr": "", "introduction_fr": "

    Bienvenue sur le site du COF

    ", "search_description_en": "", "title_en": "", "seo_title_en": "", "introduction_en": "", "search_description_fr": "", "slug_fr": "site-du-cof", "slug_en": "", "introduction": "

    Bienvenue sur le site du COF

    "}, "model": "cofcms.cofrootpage"}, {"pk": 15, "fields": {"body_fr": "[{\"type\": \"heading\", \"value\": \"Quoi ?\"}, {\"type\": \"paragraph\", \"value\": \"

    Le COF (Comit\\u00e9 d\\u2019Organisation des F\\u00eates), c\\u2019est le petit nom de \\nl\\u2019AEENS, l\\u2019Association des \\u00c9l\\u00e8ves de l\\u2019ENS (association de loi 1901). \\nC\\u2019est lui qui organise les\\u00a0\\u00e9v\\u00e8nements\\u00a0culturels, associatifs et bien s\\u00fbr\\n festifs, de l\\u2019\\u00c9cole normale.

    \\n

    Ses principales responsabilit\\u00e9s sont entre autres :

    \\n
    • L\\u2019organisation du week-end d\\u2019int\\u00e9gration, plus connu sous le nom de Mega
    • L\\u2019organisation du Gala : la Nuit de la rue d\\u2019Ulm, h\\u00e9riti\\u00e8re du Bal de l\\u2019\\u00c9cole
    • L\\u2019organisation de grands rendez-vous avec les autres \\u00e9coles comme les InterENS culturelles
    • L\\u2019\\u00e9dition du BOcal, le journal du COF
    • La coordination et le financement des activit\\u00e9s d\\u2019une quarantaine de clubs
    • La gestion d\\u2019un syst\\u00e8me de petits cours dispens\\u00e9s par les \\u00e9l\\u00e8ves de l\\u2019\\u00e9cole
    • L\\u2019organisation des soir\\u00e9es \\u00e9tudiantes, qui ont souvent lieu dans le bar de l\\u2019\\u00e9cole, la \\u2018K-F\\u00eat\\u2018
    • L\\u2019\\u00e9tablissement de nombreux partenariats culturels avec les grandes salles parisiennes (voir le site du Bureau des Arts), l\\u2019organisation de voyages, \\u2026
    \\n

    Il est bien s\\u00fbr tr\\u00e8s li\\u00e9 au BDS (Bureau des Sports) avec qui il pr\\u00e9pare les InterENS sportives, mais qui est\\u00a0n\\u00e9anmoins\\u00a0une entit\\u00e9 distincte du COF.

    \"}, {\"type\": \"heading\", \"value\": \"Qui ?\"}, {\"type\": \"paragraph\", \"value\": \"

    Le COF c\\u2019est avant tout ses membres (environ 700 chaque ann\\u00e9e) et ses\\n clubs (entre 20 et 40 selon les ann\\u00e9es). Chaque club est g\\u00e9r\\u00e9 par un \\nresponsable (voir les pages des clubs).

    \\n

    Comme dans toute association il y a un bureau \\u2013 compos\\u00e9 de 12 personnes r\\u00e9\\u00e9lues tous les 6 mois.

    \\n

    Le COF organise au moins 3 Assembl\\u00e9es G\\u00e9n\\u00e9rales par an, une en \\noctobre pour attribuer les budgets annuels, une en f\\u00e9vrier pour \\nr\\u00e9ajuster les budgets, discuter des projets, des affaires courantes ; et\\n la derni\\u00e8re en juin pour faire un bilan de l\\u2019ann\\u00e9e, voter les \\ncotisations et les partenariats. C\\u2019est l\\u2019occasion pour tous les membres \\nde se rassembler et de faire entendre leur voix, pour les clubs de se \\npr\\u00e9senter et pour le Bur\\u00f4\\u2026 et de vous rendre des comptes ! Il y en a \\naussi une avant chaque \\u00e9lection afin d\\u2019\\u00e9couter la pr\\u00e9sentation des \\ncandidats au Bur\\u00f4.

    \"}]", "url_path_fr": "/global/site-du-cof/prsentation/", "title_fr": "Pr\u00e9sentation", "url_path_en": "/global/site-du-cof/presentation/", "seo_title_fr": "", "search_description_en": "", "title_en": "Presentation", "seo_title_en": "", "body_en": "[]", "search_description_fr": "", "slug_fr": "prsentation", "slug_en": "presentation", "body": "[{\"type\": \"heading\", \"value\": \"Quoi ?\"}, {\"type\": \"paragraph\", \"value\": \"

    Le COF (Comit\\u00e9 d\\u2019Organisation des F\\u00eates), c\\u2019est le petit nom de \\nl\\u2019AEENS, l\\u2019Association des \\u00c9l\\u00e8ves de l\\u2019ENS (association de loi 1901). \\nC\\u2019est lui qui organise les\\u00a0\\u00e9v\\u00e8nements\\u00a0culturels, associatifs et bien s\\u00fbr\\n festifs, de l\\u2019\\u00c9cole normale.

    \\n

    Ses principales responsabilit\\u00e9s sont entre autres :

    \\n
    • L\\u2019organisation du week-end d\\u2019int\\u00e9gration, plus connu sous le nom de Mega
    • L\\u2019organisation du Gala : la Nuit de la rue d\\u2019Ulm, h\\u00e9riti\\u00e8re du Bal de l\\u2019\\u00c9cole
    • L\\u2019organisation de grands rendez-vous avec les autres \\u00e9coles comme les InterENS culturelles
    • L\\u2019\\u00e9dition du BOcal, le journal du COF
    • La coordination et le financement des activit\\u00e9s d\\u2019une quarantaine de clubs
    • La gestion d\\u2019un syst\\u00e8me de petits cours dispens\\u00e9s par les \\u00e9l\\u00e8ves de l\\u2019\\u00e9cole
    • L\\u2019organisation des soir\\u00e9es \\u00e9tudiantes, qui ont souvent lieu dans le bar de l\\u2019\\u00e9cole, la \\u2018K-F\\u00eat\\u2018
    • L\\u2019\\u00e9tablissement de nombreux partenariats culturels avec les grandes salles parisiennes (voir le site du Bureau des Arts), l\\u2019organisation de voyages, \\u2026
    \\n

    Il est bien s\\u00fbr tr\\u00e8s li\\u00e9 au BDS (Bureau des Sports) avec qui il pr\\u00e9pare les InterENS sportives, mais qui est\\u00a0n\\u00e9anmoins\\u00a0une entit\\u00e9 distincte du COF.

    \"}, {\"type\": \"heading\", \"value\": \"Qui ?\"}, {\"type\": \"paragraph\", \"value\": \"

    Le COF c\\u2019est avant tout ses membres (environ 700 chaque ann\\u00e9e) et ses\\n clubs (entre 20 et 40 selon les ann\\u00e9es). Chaque club est g\\u00e9r\\u00e9 par un \\nresponsable (voir les pages des clubs).

    \\n

    Comme dans toute association il y a un bureau \\u2013 compos\\u00e9 de 12 personnes r\\u00e9\\u00e9lues tous les 6 mois.

    \\n

    Le COF organise au moins 3 Assembl\\u00e9es G\\u00e9n\\u00e9rales par an, une en \\noctobre pour attribuer les budgets annuels, une en f\\u00e9vrier pour \\nr\\u00e9ajuster les budgets, discuter des projets, des affaires courantes ; et\\n la derni\\u00e8re en juin pour faire un bilan de l\\u2019ann\\u00e9e, voter les \\ncotisations et les partenariats. C\\u2019est l\\u2019occasion pour tous les membres \\nde se rassembler et de faire entendre leur voix, pour les clubs de se \\npr\\u00e9senter et pour le Bur\\u00f4\\u2026 et de vous rendre des comptes ! Il y en a \\naussi une avant chaque \\u00e9lection afin d\\u2019\\u00e9couter la pr\\u00e9sentation des \\ncandidats au Bur\\u00f4.

    \"}]"}, "model": "cofcms.cofpage"}, {"pk": 12, "fields": {"search_description_fr": "", "title_en": "News", "url_path_fr": "/global/site-du-cof/actualites/", "title_fr": "Actualit\u00e9s", "url_path_en": "/global/site-du-cof/news/", "seo_title_fr": "", "slug_fr": "actualites", "slug_en": "news", "search_description_en": "", "seo_title_en": ""}, "model": "cofcms.cofactuindexpage"}, {"pk": 18, "fields": {"date": "2017-08-26", "body_fr": "

    Venez faire la f\u00eate en K-F\u00eat.

    C'est une bonne id\u00e9e pour r\u00e9ussir ses oraux !

    ", "url_path_fr": "/global/site-du-cof/actualites/accueil-des-admissibles/", "title_fr": "Accueil des admissibles", "url_path_en": "/global/site-du-cof/news/accueil-des-admissibles/", "image": 36, "seo_title_fr": "", "search_description_en": "", "title_en": "Welcoming the conscrits-to-come", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "accueil-des-admissibles", "slug_en": "", "body": "

    Venez faire la f\u00eate en K-F\u00eat.

    C'est une bonne id\u00e9e pour r\u00e9ussir ses oraux !

    "}, "model": "cofcms.cofactupage"}, {"pk": 13, "fields": {"body_fr": "

    H\u00e9 les gars viendez on va se tr\u00e9mousser en K-F\u00eat !

    ", "url_path_fr": "/global/site-du-cof/actualites/soire-en-k-ft/", "title_fr": "Soir\u00e9e en K-F\u00eat", "date_start": "2017-08-18T20:00:00Z", "url_path_en": "/global/site-du-cof/news/party-in-k-ft/", "chapo_en": "Big party", "seo_title_fr": "", "chapo_fr": "Grosse soir\u00e9e", "image": 34, "all_day": false, "chapo": "Grosse soir\u00e9e", "search_description_en": "", "title_en": "Party in K-F\u00eat", "seo_title_en": "", "body_en": "

    Hey guys come on, let's be wasted in K-F\u00eat!

    ", "search_description_fr": "", "slug_fr": "soire-en-k-ft", "slug_en": "party-in-k-ft", "body": "

    H\u00e9 les gars viendez on va se tr\u00e9mousser en K-F\u00eat !

    ", "date_end": null}, "model": "cofcms.cofactueventpage"}, {"pk": 17, "fields": {"body_fr": "

    Rendez vous au 45 rue d'Ulm pour la plus grosse soir\u00e9e de l'ann\u00e9e.

    ", "url_path_fr": "/global/site-du-cof/actualites/soire-de-nol/", "title_fr": "Soir\u00e9e de No\u00ebl", "date_start": "2017-08-30T15:00:00Z", "url_path_en": "/global/site-du-cof/news/soire-de-nol/", "chapo_en": "", "seo_title_fr": "", "chapo_fr": "Grosse soir\u00e9e en bo\u00eete pour f\u00eater la fin de l'ann\u00e9e !", "image": 35, "all_day": false, "chapo": "Grosse soir\u00e9e en bo\u00eete pour f\u00eater la fin de l'ann\u00e9e !", "search_description_en": "", "title_en": "", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "soire-de-nol", "slug_en": "", "body": "

    Rendez vous au 45 rue d'Ulm pour la plus grosse soir\u00e9e de l'ann\u00e9e.

    ", "date_end": "2017-08-31T16:00:00Z"}, "model": "cofcms.cofactueventpage"}, {"pk": 14, "fields": {"url_path_fr": "/global/site-du-cof/clubs/", "title_fr": "Clubs", "url_path_en": "/global/site-du-cof/clubs/", "seo_title_fr": "", "introduction_fr": "

    Tous les clubs de l'ENS

    ", "search_description_en": "", "title_en": "", "seo_title_en": "", "introduction_en": "

    All the clubs in the ENS

    ", "search_description_fr": "", "slug_fr": "clubs", "slug_en": "", "introduction": "

    Tous les clubs de l'ENS

    "}, "model": "cofcms.cofdirectorypage"}, {"pk": 16, "fields": {"body_fr": "

    Des jolies affiches dans l'ENS

    ", "url_path_fr": "/global/site-du-cof/clubs/graphiche/", "title_fr": "Graph'iche", "url_path_en": "/global/site-du-cof/clubs/graphiche/", "links_en": "[]", "seo_title_fr": "", "links_fr": "[{\"type\": \"lien\", \"value\": {\"url\": \"http://evarin.fr\", \"texte\": \"Site\"}}, {\"type\": \"contact\", \"value\": {\"email\": \"graphiche@ens.fr\", \"texte\": \"Mailing-list\"}}]", "image": 33, "search_description_en": "", "title_en": "", "links": "[{\"type\": \"lien\", \"value\": {\"url\": \"http://evarin.fr\", \"texte\": \"Site\"}}, {\"type\": \"contact\", \"value\": {\"email\": \"graphiche@ens.fr\", \"texte\": \"Mailing-list\"}}]", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "graphiche", "slug_en": "", "body": "

    Des jolies affiches dans l'ENS

    "}, "model": "cofcms.cofdirectoryentrypage"}, {"pk": 19, "fields": {"body_fr": "

    Le club de d\u00e9bat

    ", "url_path_fr": "/global/site-du-cof/clubs/eloquens/", "title_fr": "Eloqu'ENS", "url_path_en": "/global/site-du-cof/clubs/eloquens/", "links_en": "[]", "seo_title_fr": "", "links_fr": "[{\"type\": \"contact\", \"value\": {\"email\": \"eloquens@ens.fr\", \"texte\": \"Mailing-liste\"}}]", "image": 37, "search_description_en": "", "title_en": "", "links": "[{\"type\": \"contact\", \"value\": {\"email\": \"eloquens@ens.fr\", \"texte\": \"Mailing-liste\"}}]", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "eloquens", "slug_en": "", "body": "

    Le club de d\u00e9bat

    "}, "model": "cofcms.cofdirectoryentrypage"}] \ No newline at end of file From ea495e8f290571bb1922b916c6e2a944b720f1d1 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sat, 20 Jan 2018 19:33:50 +0100 Subject: [PATCH 012/773] Archives beta --- gestioncof/cms/models.py | 16 ++++++++++++++++ .../templates/cofcms/cof_actu_index_page.html | 14 ++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index 71e76bb8..e63acde7 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -62,6 +62,22 @@ class COFActuIndexPage(Page, COFActuIndexMixin): verbose_name = "Index des actualités" verbose_name_plural = "Indexs des actualités" + def get_context(self, request): + context = super(COFActuIndexPage, self).get_context(request) + actus = COFActuPage.objects.live().descendant_of(self).order_by('-date') + + page = request.GET.get('page') + paginator = Paginator(actus, 5) + try: + actus = paginator.page(page) + except PageNotAnInteger: + actus = paginator.page(1) + except EmptyPage: + actus = paginator.page(paginator.num_pages) + + context['actus'] = actus + return context + class COFActuPage(Page): body = RichTextField("Contenu") date = models.DateField("Date du post") diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index affd433d..975e520d 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -12,6 +12,13 @@
+ {% if actus.has_previous %} + Actualités plus récentes + {% endif %} + {% if actus.has_next %} + Actualités plus anciennes + {% endif %} + {% for actu in page.actus %}
@@ -26,5 +33,12 @@
{% endfor %} + + {% if actus.has_previous %} + Actualités plus récentes + {% endif %} + {% if actus.has_next %} + Actualités plus anciennes + {% endif %}
{% endblock %} From 8488beeb4ef3fefc274bb10735bd2bc3041f7172 Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 22 Jan 2018 21:24:20 +0100 Subject: [PATCH 013/773] =?UTF-8?q?Un=20seul=20mod=C3=A8le=20pour=20les=20?= =?UTF-8?q?actus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gestioncof/cms/fixtures/cofcms.json | 1 + gestioncof/cms/fixtures/wagtail_cof_cms.json | 1 - gestioncof/cms/migrations/0001_initial.py | 198 ++++++++---------- gestioncof/cms/models.py | 59 ++---- .../templates/cofcms/cof_actu_event_page.html | 17 -- .../templates/cofcms/cof_actu_index_page.html | 2 +- .../cms/templates/cofcms/cof_actu_page.html | 3 +- .../cms/templates/cofcms/cof_root_page.html | 2 +- gestioncof/cms/templatetags/cofcms_tags.py | 10 +- gestioncof/cms/translation.py | 10 +- 10 files changed, 120 insertions(+), 183 deletions(-) create mode 100644 gestioncof/cms/fixtures/cofcms.json delete mode 100644 gestioncof/cms/fixtures/wagtail_cof_cms.json delete mode 100644 gestioncof/cms/templates/cofcms/cof_actu_event_page.html diff --git a/gestioncof/cms/fixtures/cofcms.json b/gestioncof/cms/fixtures/cofcms.json new file mode 100644 index 00000000..82114ff3 --- /dev/null +++ b/gestioncof/cms/fixtures/cofcms.json @@ -0,0 +1 @@ +[{"model": "cofcms.cofrootpage", "pk": 11, "fields": {"title_fr": "Site du COF", "title_en": "COF's website", "slug_fr": "site", "slug_en": "news", "url_path_fr": "/global/site/", "url_path_en": "/global/news/", "seo_title_fr": "Accueil", "seo_title_en": "Home", "search_description_fr": "", "search_description_en": "", "introduction": "

Bienvenue sur le site du COF

", "introduction_fr": "

Bienvenue sur le site du COF

", "introduction_en": "

Welcome to the COF's website

"}}, {"model": "cofcms.cofpage", "pk": 13, "fields": {"title_fr": "Pr\u00e9sentation", "title_en": "Presentation", "slug_fr": "pr\u00e9sentation", "slug_en": "presentation", "url_path_fr": "/global/site/pr\u00e9sentation/", "url_path_en": "/global/news/presentation/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "body": "[{\"type\": \"heading\", \"value\": \"Quoi ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF (Comit\\u00e9 d\\u2019Organisation des F\\u00eates), c\\u2019est le petit nom de \\nl\\u2019AEENS, l\\u2019Association des \\u00c9l\\u00e8ves de l\\u2019ENS (association de loi 1901). \\nC\\u2019est lui qui organise les\\u00a0\\u00e9v\\u00e8nements\\u00a0culturels, associatifs et bien s\\u00fbr\\n festifs, de l\\u2019\\u00c9cole normale.

\\n

Ses principales responsabilit\\u00e9s sont entre autres :

\\n
  • L\\u2019organisation du week-end d\\u2019int\\u00e9gration, plus connu sous le nom de Mega
  • L\\u2019organisation du Gala : la Nuit de la rue d\\u2019Ulm, h\\u00e9riti\\u00e8re du Bal de l\\u2019\\u00c9cole
  • L\\u2019organisation de grands rendez-vous avec les autres \\u00e9coles comme les InterENS culturelles
  • L\\u2019\\u00e9dition du BOcal, le journal du COF
  • La coordination et le financement des activit\\u00e9s d\\u2019une quarantaine de clubs
  • La gestion d\\u2019un syst\\u00e8me de petits cours dispens\\u00e9s par les \\u00e9l\\u00e8ves de l\\u2019\\u00e9cole
  • L\\u2019organisation des soir\\u00e9es \\u00e9tudiantes, qui ont souvent lieu dans le bar de l\\u2019\\u00e9cole, la \\u2018K-F\\u00eat\\u2018
  • L\\u2019\\u00e9tablissement de nombreux partenariats culturels avec les grandes salles parisiennes (voir le site du Bureau des Arts), l\\u2019organisation de voyages, \\u2026
\\n

Il est bien s\\u00fbr tr\\u00e8s li\\u00e9 au BDS (Bureau des Sports) avec qui il pr\\u00e9pare les InterENS sportives, mais qui est\\u00a0n\\u00e9anmoins\\u00a0une entit\\u00e9 distincte du COF.

\"}, {\"type\": \"heading\", \"value\": \"Qui ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF c\\u2019est avant tout ses membres (environ 700 chaque ann\\u00e9e) et ses\\n clubs (entre 20 et 40 selon les ann\\u00e9es). Chaque club est g\\u00e9r\\u00e9 par un \\nresponsable (voir les pages des clubs).

\\n

Comme dans toute association il y a un bureau \\u2013 compos\\u00e9 de 12 personnes r\\u00e9\\u00e9lues tous les 6 mois.

\\n

Le COF organise au moins 3 Assembl\\u00e9es G\\u00e9n\\u00e9rales par an, une en \\noctobre pour attribuer les budgets annuels, une en f\\u00e9vrier pour \\nr\\u00e9ajuster les budgets, discuter des projets, des affaires courantes ; et\\n la derni\\u00e8re en juin pour faire un bilan de l\\u2019ann\\u00e9e, voter les \\ncotisations et les partenariats. C\\u2019est l\\u2019occasion pour tous les membres \\nde se rassembler et de faire entendre leur voix, pour les clubs de se \\npr\\u00e9senter et pour le Bur\\u00f4\\u2026 et de vous rendre des comptes ! Il y en a \\naussi une avant chaque \\u00e9lection afin d\\u2019\\u00e9couter la pr\\u00e9sentation des \\ncandidats au Bur\\u00f4.

\"}, {\"type\": \"image\", \"value\": 33}, {\"type\": \"paragraph\", \"value\": \"Poste / Nom\\n\\n\\n\\n\\n\\t
Pr\\u00e9sident : Valentin Cocco
Vice-pr\\u00e9sident : Th\\u00e9o Mathevet
Tr\\u00e9sorier : Mathias Penot\\n\\n\\n\\t
Sous-tr\\u00e9sorier : Octave Tessiot
Secr\\u00e9taire : L\\u00e9na Gurriaran
Charg\\u00e9 de comm' : Cl\\u00e9ment de Mecquenem
Charg\\u00e9 de comm' adjoint : Charles Giroudot
Pr\\u00e9sidente du Bureau des Arts : Caroline Delattre
Bureau des Arts : Bryan Rimbault
Bureau des Arts : Philippe Danr\\u00e9
Bureau des Arts : Louise Garrigou
Bureau des Arts : Emile Laymand

Bureau des Arts : Cl\\u00e9mence Elmira

\"}, {\"type\": \"heading\", \"value\": \"O\\u00f9 ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF dispose d\\u2019un local dans l\\u2019\\u00e9cole, au 45 rue d\\u2019Ulm. Il suffit de\\n traverser la cour aux Ernest puis de tourner tout de suite \\u00e0 gauche.

\"}, {\"type\": \"heading\", \"value\": \"Quand ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF assure deux permanences tous les jours (sauf samedi et dimanche) : une de 12h \\u00e0 14h, et une de 18h \\u00e0 20h.

\"}, {\"type\": \"heading\", \"value\": \"Des questions ?\"}, {\"type\": \"paragraph\", \"value\": \"

Vous pouvez nous contacter facilement !

\\n
  • Adresse email : cof (arobase) ens (point) fr
  • T\\u00e9l\\u00e9phone : 01 44 32 28 80
  • Adresse postale :\\u00a0
    \\nCOF
    \\n45 rue d\\u2019Ulm
    \\n75005 Paris
\"}]", "body_fr": "[{\"type\": \"heading\", \"value\": \"Quoi ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF (Comit\\u00e9 d\\u2019Organisation des F\\u00eates), c\\u2019est le petit nom de \\nl\\u2019AEENS, l\\u2019Association des \\u00c9l\\u00e8ves de l\\u2019ENS (association de loi 1901). \\nC\\u2019est lui qui organise les\\u00a0\\u00e9v\\u00e8nements\\u00a0culturels, associatifs et bien s\\u00fbr\\n festifs, de l\\u2019\\u00c9cole normale.

\\n

Ses principales responsabilit\\u00e9s sont entre autres :

\\n
  • L\\u2019organisation du week-end d\\u2019int\\u00e9gration, plus connu sous le nom de Mega
  • L\\u2019organisation du Gala : la Nuit de la rue d\\u2019Ulm, h\\u00e9riti\\u00e8re du Bal de l\\u2019\\u00c9cole
  • L\\u2019organisation de grands rendez-vous avec les autres \\u00e9coles comme les InterENS culturelles
  • L\\u2019\\u00e9dition du BOcal, le journal du COF
  • La coordination et le financement des activit\\u00e9s d\\u2019une quarantaine de clubs
  • La gestion d\\u2019un syst\\u00e8me de petits cours dispens\\u00e9s par les \\u00e9l\\u00e8ves de l\\u2019\\u00e9cole
  • L\\u2019organisation des soir\\u00e9es \\u00e9tudiantes, qui ont souvent lieu dans le bar de l\\u2019\\u00e9cole, la \\u2018K-F\\u00eat\\u2018
  • L\\u2019\\u00e9tablissement de nombreux partenariats culturels avec les grandes salles parisiennes (voir le site du Bureau des Arts), l\\u2019organisation de voyages, \\u2026
\\n

Il est bien s\\u00fbr tr\\u00e8s li\\u00e9 au BDS (Bureau des Sports) avec qui il pr\\u00e9pare les InterENS sportives, mais qui est\\u00a0n\\u00e9anmoins\\u00a0une entit\\u00e9 distincte du COF.

\"}, {\"type\": \"heading\", \"value\": \"Qui ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF c\\u2019est avant tout ses membres (environ 700 chaque ann\\u00e9e) et ses\\n clubs (entre 20 et 40 selon les ann\\u00e9es). Chaque club est g\\u00e9r\\u00e9 par un \\nresponsable (voir les pages des clubs).

\\n

Comme dans toute association il y a un bureau \\u2013 compos\\u00e9 de 12 personnes r\\u00e9\\u00e9lues tous les 6 mois.

\\n

Le COF organise au moins 3 Assembl\\u00e9es G\\u00e9n\\u00e9rales par an, une en \\noctobre pour attribuer les budgets annuels, une en f\\u00e9vrier pour \\nr\\u00e9ajuster les budgets, discuter des projets, des affaires courantes ; et\\n la derni\\u00e8re en juin pour faire un bilan de l\\u2019ann\\u00e9e, voter les \\ncotisations et les partenariats. C\\u2019est l\\u2019occasion pour tous les membres \\nde se rassembler et de faire entendre leur voix, pour les clubs de se \\npr\\u00e9senter et pour le Bur\\u00f4\\u2026 et de vous rendre des comptes ! Il y en a \\naussi une avant chaque \\u00e9lection afin d\\u2019\\u00e9couter la pr\\u00e9sentation des \\ncandidats au Bur\\u00f4.

\"}, {\"type\": \"image\", \"value\": 33}, {\"type\": \"paragraph\", \"value\": \"Poste / Nom\\n\\n\\n\\n\\n\\t
Pr\\u00e9sident : Valentin Cocco
Vice-pr\\u00e9sident : Th\\u00e9o Mathevet
Tr\\u00e9sorier : Mathias Penot\\n\\n\\n\\t
Sous-tr\\u00e9sorier : Octave Tessiot
Secr\\u00e9taire : L\\u00e9na Gurriaran
Charg\\u00e9 de comm' : Cl\\u00e9ment de Mecquenem
Charg\\u00e9 de comm' adjoint : Charles Giroudot
Pr\\u00e9sidente du Bureau des Arts : Caroline Delattre
Bureau des Arts : Bryan Rimbault
Bureau des Arts : Philippe Danr\\u00e9
Bureau des Arts : Louise Garrigou
Bureau des Arts : Emile Laymand

Bureau des Arts : Cl\\u00e9mence Elmira

\"}, {\"type\": \"heading\", \"value\": \"O\\u00f9 ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF dispose d\\u2019un local dans l\\u2019\\u00e9cole, au 45 rue d\\u2019Ulm. Il suffit de\\n traverser la cour aux Ernest puis de tourner tout de suite \\u00e0 gauche.

\"}, {\"type\": \"heading\", \"value\": \"Quand ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF assure deux permanences tous les jours (sauf samedi et dimanche) : une de 12h \\u00e0 14h, et une de 18h \\u00e0 20h.

\"}, {\"type\": \"heading\", \"value\": \"Des questions ?\"}, {\"type\": \"paragraph\", \"value\": \"

Vous pouvez nous contacter facilement !

\\n
  • Adresse email : cof (arobase) ens (point) fr
  • T\\u00e9l\\u00e9phone : 01 44 32 28 80
  • Adresse postale :\\u00a0
    \\nCOF
    \\n45 rue d\\u2019Ulm
    \\n75005 Paris
\"}]", "body_en": "[]"}}, {"model": "cofcms.cofpage", "pk": 16, "fields": {"title_fr": "Cours Particuliers", "title_en": null, "slug_fr": "cours-particuliers", "slug_en": null, "url_path_fr": "/global/site/cours-particuliers/", "url_path_en": "/global/news/cours-particuliers/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "body": "[{\"type\": \"paragraph\", \"value\": \"

Les \\u00e9l\\u00e8ves de l'ENS peuvent donner des cours particuliers. Si vous \\n\\u00eates int\\u00e9ress\\u00e9 pour en prendre, merci de faire une demande de petits \\ncours sur cette page.

\\n \\n \\n\\t\\n\\t
\\n\\tSi vous \\u00eates \\u00e9l\\u00e8ve de l'\\u00e9cole, vous pouvez g\\u00e9rer vos petits cours sur GestioCOF.\"}]", "body_fr": "[{\"type\": \"paragraph\", \"value\": \"

Les \\u00e9l\\u00e8ves de l'ENS peuvent donner des cours particuliers. Si vous \\n\\u00eates int\\u00e9ress\\u00e9 pour en prendre, merci de faire une demande de petits \\ncours sur cette page.

\\n \\n \\n\\t\\n\\t
\\n\\tSi vous \\u00eates \\u00e9l\\u00e8ve de l'\\u00e9cole, vous pouvez g\\u00e9rer vos petits cours sur GestioCOF.\"}]", "body_en": "[]"}}, {"model": "cofcms.cofactuindexpage", "pk": 12, "fields": {"title_fr": "Actualit\u00e9s", "title_en": "News", "slug_fr": "actualites", "slug_en": "news", "url_path_fr": "/global/site/actualites/", "url_path_en": "/global/news/news/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": ""}}, {"model": "cofcms.cofactupage", "pk": 17, "fields": {"title_fr": "Singin' in the R'ENS", "title_en": null, "slug_fr": "singin-in-the-rens", "slug_en": null, "url_path_fr": "/global/site/actualites/singin-in-the-rens/", "url_path_en": "/global/news/news/singin-in-the-rens/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "chapo": "Soir\u00e9e com\u00e9die musicale", "chapo_fr": "Soir\u00e9e com\u00e9die musicale", "chapo_en": "", "body": "Je chante dans l'ENS
Je chante dans l'ENS
Cette glorieuse soir\u00e9e
Me rendra le sourire
Je tournoie sur la piste
Lumi\u00e8re noire au plafond
Vibrations dans mon coeur
Et j'suis pr\u00eat\u00b7e \u00e0 chanter
Que la foule endiabl\u00e9e
Chasse tous mes tracassins
Viens \u00e0 notre soir\u00e9e
On va bien s'amuser
Oui descends en K-F\u00eat
Avec le sourire aux l\u00e8vres
Et puis chante
Chante dans l'ENS !

\n Une ambiance de com\u00e9die musicale, de la danse, du chant, et beaucoup de\n bonne humeur : venez nombreux\u00b7ses jeudi 25 janvier pour notre soir\u00e9e \nSingin' in the R'ENS, qui aura lieu en K-F\u00eat d\u00e8s 23h !

", "body_fr": "Je chante dans l'ENS
Je chante dans l'ENS
Cette glorieuse soir\u00e9e
Me rendra le sourire
Je tournoie sur la piste
Lumi\u00e8re noire au plafond
Vibrations dans mon coeur
Et j'suis pr\u00eat\u00b7e \u00e0 chanter
Que la foule endiabl\u00e9e
Chasse tous mes tracassins
Viens \u00e0 notre soir\u00e9e
On va bien s'amuser
Oui descends en K-F\u00eat
Avec le sourire aux l\u00e8vres
Et puis chante
Chante dans l'ENS !

\n Une ambiance de com\u00e9die musicale, de la danse, du chant, et beaucoup de\n bonne humeur : venez nombreux\u00b7ses jeudi 25 janvier pour notre soir\u00e9e \nSingin' in the R'ENS, qui aura lieu en K-F\u00eat d\u00e8s 23h !

", "body_en": "", "image": 34, "is_event": true, "date_start": "2018-01-25T21:00:00Z", "date_end": null, "all_day": false}}, {"model": "cofcms.cofactupage", "pk": 18, "fields": {"title_fr": "Le Retour du Bur\u00f4", "title_en": null, "slug_fr": "le-retour-du-bur\u00f4", "slug_en": null, "url_path_fr": "/global/site/actualites/le-retour-du-bur\u00f4/", "url_path_en": "/global/news/news/le-retour-du-bur\u00f4/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "chapo": "Premi\u00e8re soir\u00e9e du nouveau COF", "chapo_fr": "Premi\u00e8re soir\u00e9e du nouveau COF", "chapo_en": "", "body": "Le Retour du \nBur\u00f4, qu'est-ce que c'est donc ? La premi\u00e8re soir\u00e9e du nouveau COF pardi\n ! Le th\u00e8me ? Top : je suis une saga cin\u00e9matographique \u00e0 grand succ\u00e8s, \nje compte actuellement huit \u00e9pisodes (et demi) \u00e0 mon actif, j'ai fait \nr\u00eaver des g\u00e9n\u00e9rations enti\u00e8res depuis 1977, m\u00eame sans m'avoir vu vous me\n connaissez sans doute pour une c\u00e9l\u00e8bre r\u00e9plique, je suis, je suis... Je\n suis ton p\u00e8re ! Hum hum, je suis : Star Wars !
Au programme, bracelets-lasers, \u00e9toiles dans les yeux, et voyage musical interstellaire.

Rendez-vous jeudi 18 janvier en K-F\u00eat, \u00e0 partir de 22h ! Venez du COFt\u00e9 obscur, on a des cookies !

", "body_fr": "Le Retour du \nBur\u00f4, qu'est-ce que c'est donc ? La premi\u00e8re soir\u00e9e du nouveau COF pardi\n ! Le th\u00e8me ? Top : je suis une saga cin\u00e9matographique \u00e0 grand succ\u00e8s, \nje compte actuellement huit \u00e9pisodes (et demi) \u00e0 mon actif, j'ai fait \nr\u00eaver des g\u00e9n\u00e9rations enti\u00e8res depuis 1977, m\u00eame sans m'avoir vu vous me\n connaissez sans doute pour une c\u00e9l\u00e8bre r\u00e9plique, je suis, je suis... Je\n suis ton p\u00e8re ! Hum hum, je suis : Star Wars !
Au programme, bracelets-lasers, \u00e9toiles dans les yeux, et voyage musical interstellaire.

Rendez-vous jeudi 18 janvier en K-F\u00eat, \u00e0 partir de 22h ! Venez du COFt\u00e9 obscur, on a des cookies !

", "body_en": "", "image": 35, "is_event": true, "date_start": "2018-01-18T21:00:00Z", "date_end": null, "all_day": false}}, {"model": "cofcms.cofactupage", "pk": 19, "fields": {"title_fr": "\u00c9lection du Bur\u00f4 2018", "title_en": null, "slug_fr": "\u00e9lection-du-bur\u00f4-2018", "slug_en": null, "url_path_fr": "/global/site/actualites/\u00e9lection-du-bur\u00f4-2018/", "url_path_en": "/global/news/news/\u00e9lection-du-bur\u00f4-2018/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "chapo": "", "chapo_fr": "", "chapo_en": "", "body": "Les campagnes battent leurs pleins rythm\u00e9es par de nombreux petits \nd\u00e9jeuners, soir\u00e9es et autres \u00e9v\u00e9nements organis\u00e9s par les diff\u00e9rentes \nlistes et ces deux semaines se finiront par l\u2019\u00e9lection du nouveau Bur\u00f4.\n

\u00a0

\n

\u00a0

\n

Passez donc en aquarium pour le premier tour des \u00e9lections du Bur\u00f4 du\n COF 2018 ! Si vous ne pouvez pas venir en personne, un vote \n\u00e9lectronique sera mis en place !

", "body_fr": "Les campagnes battent leurs pleins rythm\u00e9es par de nombreux petits \nd\u00e9jeuners, soir\u00e9es et autres \u00e9v\u00e9nements organis\u00e9s par les diff\u00e9rentes \nlistes et ces deux semaines se finiront par l\u2019\u00e9lection du nouveau Bur\u00f4.\n

\u00a0

\n

\u00a0

\n

Passez donc en aquarium pour le premier tour des \u00e9lections du Bur\u00f4 du\n COF 2018 ! Si vous ne pouvez pas venir en personne, un vote \n\u00e9lectronique sera mis en place !

", "body_en": "", "image": 36, "is_event": false, "date_start": "2017-12-18T19:22:00Z", "date_end": "2018-01-25T19:22:00Z", "all_day": true}}, {"model": "cofcms.cofdirectorypage", "pk": 14, "fields": {"title_fr": "Clubs", "title_en": null, "slug_fr": "clubs", "slug_en": null, "url_path_fr": "/global/site/clubs/", "url_path_en": "/global/news/clubs/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "introduction": "
\nVoici tous les clubs du COF !\n

La plupart de ces clubs ont des mailing lists, auxquelles il est souvent possible de s\u2019inscrire via le serveur mail sympa.

", "introduction_fr": "
\nVoici tous les clubs du COF !\n

La plupart de ces clubs ont des mailing lists, auxquelles il est souvent possible de s\u2019inscrire via le serveur mail sympa.

", "introduction_en": ""}}, {"model": "cofcms.cofdirectorypage", "pk": 15, "fields": {"title_fr": "Partenaires", "title_en": null, "slug_fr": "partenaires", "slug_en": null, "url_path_fr": "/global/site/partenaires/", "url_path_en": "/global/news/partenaires/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "introduction": "

Le COF a n\u00e9goci\u00e9 pour vous de nombreux partenariats ! Bien s\u00fbr, il faut \u00eatre membre du COF pour en b\u00e9n\u00e9ficier.

", "introduction_fr": "

Le COF a n\u00e9goci\u00e9 pour vous de nombreux partenariats ! Bien s\u00fbr, il faut \u00eatre membre du COF pour en b\u00e9n\u00e9ficier.

", "introduction_en": ""}}, {"model": "cofcms.cofdirectoryentrypage", "pk": 20, "fields": {"title_fr": "Arts Plastiques", "title_en": null, "slug_fr": "arts-plastiques", "slug_en": null, "url_path_fr": "/global/site/clubs/arts-plastiques/", "url_path_en": "/global/news/clubs/arts-plastiques/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "body": "Le club Arts Plastiques te propose un lieu de rencontre entre \npersonnes int\u00e9ress\u00e9es par les arts plastiques, le dessin ou la peinture.
\nMais, que faisons-nous au club Arts Plastiques ? D\u2019abord, des s\u00e9ances \nd\u2019initiation; et puis des s\u00e9ances \u00e0 th\u00e8me, avec des intervenants; des \nprojets communs; des sorties croquis\u2026

\nQue tu n\u2019aies jamais touch\u00e9 \u00e0 un pinceau, ou que tu sois d\u00e9j\u00e0 un-e grand-e artiste, n\u2019h\u00e9site pas \u00e0 venir nous rejoindre !

", "body_fr": "Le club Arts Plastiques te propose un lieu de rencontre entre \npersonnes int\u00e9ress\u00e9es par les arts plastiques, le dessin ou la peinture.
\nMais, que faisons-nous au club Arts Plastiques ? D\u2019abord, des s\u00e9ances \nd\u2019initiation; et puis des s\u00e9ances \u00e0 th\u00e8me, avec des intervenants; des \nprojets communs; des sorties croquis\u2026

\nQue tu n\u2019aies jamais touch\u00e9 \u00e0 un pinceau, ou que tu sois d\u00e9j\u00e0 un-e grand-e artiste, n\u2019h\u00e9site pas \u00e0 venir nous rejoindre !

", "body_en": "", "links": "[{\"type\": \"contact\", \"value\": {\"texte\": \"Mailing-liste\", \"email\": \"artsplastiques@ens.fr\"}}]", "links_fr": "[{\"type\": \"contact\", \"value\": {\"texte\": \"Mailing-liste\", \"email\": \"artsplastiques@ens.fr\"}}]", "links_en": "[]", "image": 37}}, {"model": "cofcms.cofdirectoryentrypage", "pk": 21, "fields": {"title_fr": "B\u00e9d\u00e9th\u00e8que", "title_en": null, "slug_fr": "b\u00e9d\u00e9th\u00e8que", "slug_en": null, "url_path_fr": "/global/site/clubs/b\u00e9d\u00e9th\u00e8que/", "url_path_en": "/global/news/clubs/b\u00e9d\u00e9th\u00e8que/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "body": "De Riad Sattouf \u00e0 Katsuhiro Otomo, en passant par Andr\u00e9 Franquin, \nJacques Tardi, Didier Tarquin et Georges Wolinski, la BDth\u00e8que poss\u00e8de \nune collection de quatre mille bandes dessin\u00e9es sur une cinquantaine \nd\u2019\u00e9tag\u00e8res, en constante croissance. Class\u00e9es par dessinateur et \ncatalogu\u00e9es, toutes ces oeuvres offrent un large panorama du XXe si\u00e8cle \net sont librement consultables sur place sans mod\u00e9ration ! \u00c0 cela \ns\u2019ajoutent des milliers de p\u00e9riodiques sp\u00e9cialis\u00e9s datant de l\u2019\u00e2ge d\u2019or \nde la BD franco-belge, un Enfer cach\u00e9, et quelques conseils de lecture \n\u00e9crits par des fans.

\nLe club organise ausis des \u00e9v\u00e9nements divers par exemple un s\u00e9minaire \nsur la bande dessin\u00e9e, qui ne demande qu\u2019\u00e0 \u00eatre relanc\u00e9. \u00c0 l\u2019ordre du \njour, la mise en place d\u2019une exp\u00e9dition \u00e0 Angoul\u00eame avec le BDA en \njanvier 2017 pourrait m\u00eame se faire avec ton aide. Tous les \ndons et suggestions d\u2019achats sont les bienvenus. Pour participer aux \nd\u00e9cisions d\u2019achats, \u00e9crire des suggesions de lecture dans le BOcal, \nchoisir les bandes dessin\u00e9es \u00e0 mettre en valeur, n\u2019h\u00e9site pas \u00e0 \nrejoindre le club!

", "body_fr": "De Riad Sattouf \u00e0 Katsuhiro Otomo, en passant par Andr\u00e9 Franquin, \nJacques Tardi, Didier Tarquin et Georges Wolinski, la BDth\u00e8que poss\u00e8de \nune collection de quatre mille bandes dessin\u00e9es sur une cinquantaine \nd\u2019\u00e9tag\u00e8res, en constante croissance. Class\u00e9es par dessinateur et \ncatalogu\u00e9es, toutes ces oeuvres offrent un large panorama du XXe si\u00e8cle \net sont librement consultables sur place sans mod\u00e9ration ! \u00c0 cela \ns\u2019ajoutent des milliers de p\u00e9riodiques sp\u00e9cialis\u00e9s datant de l\u2019\u00e2ge d\u2019or \nde la BD franco-belge, un Enfer cach\u00e9, et quelques conseils de lecture \n\u00e9crits par des fans.

\nLe club organise ausis des \u00e9v\u00e9nements divers par exemple un s\u00e9minaire \nsur la bande dessin\u00e9e, qui ne demande qu\u2019\u00e0 \u00eatre relanc\u00e9. \u00c0 l\u2019ordre du \njour, la mise en place d\u2019une exp\u00e9dition \u00e0 Angoul\u00eame avec le BDA en \njanvier 2017 pourrait m\u00eame se faire avec ton aide. Tous les \ndons et suggestions d\u2019achats sont les bienvenus. Pour participer aux \nd\u00e9cisions d\u2019achats, \u00e9crire des suggesions de lecture dans le BOcal, \nchoisir les bandes dessin\u00e9es \u00e0 mettre en valeur, n\u2019h\u00e9site pas \u00e0 \nrejoindre le club!

", "body_en": "", "links": "[{\"type\": \"contact\", \"value\": {\"texte\": \"Mailing-liste\", \"email\": \"bdtheque@ens.fr\"}}]", "links_fr": "[{\"type\": \"contact\", \"value\": {\"texte\": \"Mailing-liste\", \"email\": \"bdtheque@ens.fr\"}}]", "links_en": "[]", "image": null}}, {"model": "cofcms.cofdirectoryentrypage", "pk": 22, "fields": {"title_fr": "MGEN", "title_en": null, "slug_fr": "mgen", "slug_en": null, "url_path_fr": "/global/site/partenaires/mgen/", "url_path_en": "/global/news/partenaires/mgen/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "body": "

La MGEN est un des \nprincipaux partenaires du COF. Elle\u00a0participe au financement des \nprincipaux \u00e9v\u00e9nements (Jour le plus court, 48h des Arts) et \u00e0 \nl\u2019impression de la Plakette Alpha.

\n

Elle dispose \u00e9galement d\u2019un stand chaque ann\u00e9e au moment de la \nrentr\u00e9e, afin de proposer ses prestations aux pensionnaires de l\u2019Ecole.

", "body_fr": "

La MGEN est un des \nprincipaux partenaires du COF. Elle\u00a0participe au financement des \nprincipaux \u00e9v\u00e9nements (Jour le plus court, 48h des Arts) et \u00e0 \nl\u2019impression de la Plakette Alpha.

\n

Elle dispose \u00e9galement d\u2019un stand chaque ann\u00e9e au moment de la \nrentr\u00e9e, afin de proposer ses prestations aux pensionnaires de l\u2019Ecole.

", "body_en": "", "links": "[{\"type\": \"lien\", \"value\": {\"texte\": \"Site internet\", \"url\": \"https://www.mgen.fr/accueil/\"}}]", "links_fr": "[{\"type\": \"lien\", \"value\": {\"texte\": \"Site internet\", \"url\": \"https://www.mgen.fr/accueil/\"}}]", "links_en": "[]", "image": 38}}, {"model": "cofcms.cofdirectoryentrypage", "pk": 23, "fields": {"title_fr": "Soci\u00e9t\u00e9 G\u00e9n\u00e9rale", "title_en": null, "slug_fr": "soci\u00e9t\u00e9-g\u00e9n\u00e9rale", "slug_en": null, "url_path_fr": "/global/site/partenaires/soci\u00e9t\u00e9-g\u00e9n\u00e9rale/", "url_path_en": "/global/news/partenaires/soci\u00e9t\u00e9-g\u00e9n\u00e9rale/", "seo_title_fr": null, "seo_title_en": null, "search_description_fr": "", "search_description_en": "", "body": "

Si vous ouvrez un compte \u00e0 la SoG\u00e9, ils vous versent 140\u20ac et versent \naussi une somme au bureau, nous permettant de financer vos clubs et \n\u00e9v\u00e8nements. Vous pouvez donc rembourser votre cotisation au COF (voire \nplus) rien qu\u2019en ouvrant un compte !

\n

Vous devez ouvrir ce compte lors des journ\u00e9es de rentr\u00e9e, lorsque la \nSoG\u00e9 a un stand \u00e0 l\u2019Ecole, ou toute l\u2019ann\u00e9e \u00e0 leur agence au 38 rue \nGay-Lussac.

", "body_fr": "

Si vous ouvrez un compte \u00e0 la SoG\u00e9, ils vous versent 140\u20ac et versent \naussi une somme au bureau, nous permettant de financer vos clubs et \n\u00e9v\u00e8nements. Vous pouvez donc rembourser votre cotisation au COF (voire \nplus) rien qu\u2019en ouvrant un compte !

\n

Vous devez ouvrir ce compte lors des journ\u00e9es de rentr\u00e9e, lorsque la \nSoG\u00e9 a un stand \u00e0 l\u2019Ecole, ou toute l\u2019ann\u00e9e \u00e0 leur agence au 38 rue \nGay-Lussac.

", "body_en": "", "links": "[]", "links_fr": "[]", "links_en": "[]", "image": null}}] \ No newline at end of file diff --git a/gestioncof/cms/fixtures/wagtail_cof_cms.json b/gestioncof/cms/fixtures/wagtail_cof_cms.json deleted file mode 100644 index f0d74dcc..00000000 --- a/gestioncof/cms/fixtures/wagtail_cof_cms.json +++ /dev/null @@ -1 +0,0 @@ -[{"pk": 11, "fields": {"url_path_fr": "/global/site-du-cof/", "title_fr": "Site du COF", "url_path_en": "/global/site-du-cof/", "seo_title_fr": "", "introduction_fr": "

Bienvenue sur le site du COF

", "search_description_en": "", "title_en": "", "seo_title_en": "", "introduction_en": "", "search_description_fr": "", "slug_fr": "site-du-cof", "slug_en": "", "introduction": "

Bienvenue sur le site du COF

"}, "model": "cofcms.cofrootpage"}, {"pk": 15, "fields": {"body_fr": "[{\"type\": \"heading\", \"value\": \"Quoi ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF (Comit\\u00e9 d\\u2019Organisation des F\\u00eates), c\\u2019est le petit nom de \\nl\\u2019AEENS, l\\u2019Association des \\u00c9l\\u00e8ves de l\\u2019ENS (association de loi 1901). \\nC\\u2019est lui qui organise les\\u00a0\\u00e9v\\u00e8nements\\u00a0culturels, associatifs et bien s\\u00fbr\\n festifs, de l\\u2019\\u00c9cole normale.

\\n

Ses principales responsabilit\\u00e9s sont entre autres :

\\n
  • L\\u2019organisation du week-end d\\u2019int\\u00e9gration, plus connu sous le nom de Mega
  • L\\u2019organisation du Gala : la Nuit de la rue d\\u2019Ulm, h\\u00e9riti\\u00e8re du Bal de l\\u2019\\u00c9cole
  • L\\u2019organisation de grands rendez-vous avec les autres \\u00e9coles comme les InterENS culturelles
  • L\\u2019\\u00e9dition du BOcal, le journal du COF
  • La coordination et le financement des activit\\u00e9s d\\u2019une quarantaine de clubs
  • La gestion d\\u2019un syst\\u00e8me de petits cours dispens\\u00e9s par les \\u00e9l\\u00e8ves de l\\u2019\\u00e9cole
  • L\\u2019organisation des soir\\u00e9es \\u00e9tudiantes, qui ont souvent lieu dans le bar de l\\u2019\\u00e9cole, la \\u2018K-F\\u00eat\\u2018
  • L\\u2019\\u00e9tablissement de nombreux partenariats culturels avec les grandes salles parisiennes (voir le site du Bureau des Arts), l\\u2019organisation de voyages, \\u2026
\\n

Il est bien s\\u00fbr tr\\u00e8s li\\u00e9 au BDS (Bureau des Sports) avec qui il pr\\u00e9pare les InterENS sportives, mais qui est\\u00a0n\\u00e9anmoins\\u00a0une entit\\u00e9 distincte du COF.

\"}, {\"type\": \"heading\", \"value\": \"Qui ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF c\\u2019est avant tout ses membres (environ 700 chaque ann\\u00e9e) et ses\\n clubs (entre 20 et 40 selon les ann\\u00e9es). Chaque club est g\\u00e9r\\u00e9 par un \\nresponsable (voir les pages des clubs).

\\n

Comme dans toute association il y a un bureau \\u2013 compos\\u00e9 de 12 personnes r\\u00e9\\u00e9lues tous les 6 mois.

\\n

Le COF organise au moins 3 Assembl\\u00e9es G\\u00e9n\\u00e9rales par an, une en \\noctobre pour attribuer les budgets annuels, une en f\\u00e9vrier pour \\nr\\u00e9ajuster les budgets, discuter des projets, des affaires courantes ; et\\n la derni\\u00e8re en juin pour faire un bilan de l\\u2019ann\\u00e9e, voter les \\ncotisations et les partenariats. C\\u2019est l\\u2019occasion pour tous les membres \\nde se rassembler et de faire entendre leur voix, pour les clubs de se \\npr\\u00e9senter et pour le Bur\\u00f4\\u2026 et de vous rendre des comptes ! Il y en a \\naussi une avant chaque \\u00e9lection afin d\\u2019\\u00e9couter la pr\\u00e9sentation des \\ncandidats au Bur\\u00f4.

\"}]", "url_path_fr": "/global/site-du-cof/prsentation/", "title_fr": "Pr\u00e9sentation", "url_path_en": "/global/site-du-cof/presentation/", "seo_title_fr": "", "search_description_en": "", "title_en": "Presentation", "seo_title_en": "", "body_en": "[]", "search_description_fr": "", "slug_fr": "prsentation", "slug_en": "presentation", "body": "[{\"type\": \"heading\", \"value\": \"Quoi ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF (Comit\\u00e9 d\\u2019Organisation des F\\u00eates), c\\u2019est le petit nom de \\nl\\u2019AEENS, l\\u2019Association des \\u00c9l\\u00e8ves de l\\u2019ENS (association de loi 1901). \\nC\\u2019est lui qui organise les\\u00a0\\u00e9v\\u00e8nements\\u00a0culturels, associatifs et bien s\\u00fbr\\n festifs, de l\\u2019\\u00c9cole normale.

\\n

Ses principales responsabilit\\u00e9s sont entre autres :

\\n
  • L\\u2019organisation du week-end d\\u2019int\\u00e9gration, plus connu sous le nom de Mega
  • L\\u2019organisation du Gala : la Nuit de la rue d\\u2019Ulm, h\\u00e9riti\\u00e8re du Bal de l\\u2019\\u00c9cole
  • L\\u2019organisation de grands rendez-vous avec les autres \\u00e9coles comme les InterENS culturelles
  • L\\u2019\\u00e9dition du BOcal, le journal du COF
  • La coordination et le financement des activit\\u00e9s d\\u2019une quarantaine de clubs
  • La gestion d\\u2019un syst\\u00e8me de petits cours dispens\\u00e9s par les \\u00e9l\\u00e8ves de l\\u2019\\u00e9cole
  • L\\u2019organisation des soir\\u00e9es \\u00e9tudiantes, qui ont souvent lieu dans le bar de l\\u2019\\u00e9cole, la \\u2018K-F\\u00eat\\u2018
  • L\\u2019\\u00e9tablissement de nombreux partenariats culturels avec les grandes salles parisiennes (voir le site du Bureau des Arts), l\\u2019organisation de voyages, \\u2026
\\n

Il est bien s\\u00fbr tr\\u00e8s li\\u00e9 au BDS (Bureau des Sports) avec qui il pr\\u00e9pare les InterENS sportives, mais qui est\\u00a0n\\u00e9anmoins\\u00a0une entit\\u00e9 distincte du COF.

\"}, {\"type\": \"heading\", \"value\": \"Qui ?\"}, {\"type\": \"paragraph\", \"value\": \"

Le COF c\\u2019est avant tout ses membres (environ 700 chaque ann\\u00e9e) et ses\\n clubs (entre 20 et 40 selon les ann\\u00e9es). Chaque club est g\\u00e9r\\u00e9 par un \\nresponsable (voir les pages des clubs).

\\n

Comme dans toute association il y a un bureau \\u2013 compos\\u00e9 de 12 personnes r\\u00e9\\u00e9lues tous les 6 mois.

\\n

Le COF organise au moins 3 Assembl\\u00e9es G\\u00e9n\\u00e9rales par an, une en \\noctobre pour attribuer les budgets annuels, une en f\\u00e9vrier pour \\nr\\u00e9ajuster les budgets, discuter des projets, des affaires courantes ; et\\n la derni\\u00e8re en juin pour faire un bilan de l\\u2019ann\\u00e9e, voter les \\ncotisations et les partenariats. C\\u2019est l\\u2019occasion pour tous les membres \\nde se rassembler et de faire entendre leur voix, pour les clubs de se \\npr\\u00e9senter et pour le Bur\\u00f4\\u2026 et de vous rendre des comptes ! Il y en a \\naussi une avant chaque \\u00e9lection afin d\\u2019\\u00e9couter la pr\\u00e9sentation des \\ncandidats au Bur\\u00f4.

\"}]"}, "model": "cofcms.cofpage"}, {"pk": 12, "fields": {"search_description_fr": "", "title_en": "News", "url_path_fr": "/global/site-du-cof/actualites/", "title_fr": "Actualit\u00e9s", "url_path_en": "/global/site-du-cof/news/", "seo_title_fr": "", "slug_fr": "actualites", "slug_en": "news", "search_description_en": "", "seo_title_en": ""}, "model": "cofcms.cofactuindexpage"}, {"pk": 18, "fields": {"date": "2017-08-26", "body_fr": "

Venez faire la f\u00eate en K-F\u00eat.

C'est une bonne id\u00e9e pour r\u00e9ussir ses oraux !

", "url_path_fr": "/global/site-du-cof/actualites/accueil-des-admissibles/", "title_fr": "Accueil des admissibles", "url_path_en": "/global/site-du-cof/news/accueil-des-admissibles/", "image": 36, "seo_title_fr": "", "search_description_en": "", "title_en": "Welcoming the conscrits-to-come", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "accueil-des-admissibles", "slug_en": "", "body": "

Venez faire la f\u00eate en K-F\u00eat.

C'est une bonne id\u00e9e pour r\u00e9ussir ses oraux !

"}, "model": "cofcms.cofactupage"}, {"pk": 13, "fields": {"body_fr": "

H\u00e9 les gars viendez on va se tr\u00e9mousser en K-F\u00eat !

", "url_path_fr": "/global/site-du-cof/actualites/soire-en-k-ft/", "title_fr": "Soir\u00e9e en K-F\u00eat", "date_start": "2017-08-18T20:00:00Z", "url_path_en": "/global/site-du-cof/news/party-in-k-ft/", "chapo_en": "Big party", "seo_title_fr": "", "chapo_fr": "Grosse soir\u00e9e", "image": 34, "all_day": false, "chapo": "Grosse soir\u00e9e", "search_description_en": "", "title_en": "Party in K-F\u00eat", "seo_title_en": "", "body_en": "

Hey guys come on, let's be wasted in K-F\u00eat!

", "search_description_fr": "", "slug_fr": "soire-en-k-ft", "slug_en": "party-in-k-ft", "body": "

H\u00e9 les gars viendez on va se tr\u00e9mousser en K-F\u00eat !

", "date_end": null}, "model": "cofcms.cofactueventpage"}, {"pk": 17, "fields": {"body_fr": "

Rendez vous au 45 rue d'Ulm pour la plus grosse soir\u00e9e de l'ann\u00e9e.

", "url_path_fr": "/global/site-du-cof/actualites/soire-de-nol/", "title_fr": "Soir\u00e9e de No\u00ebl", "date_start": "2017-08-30T15:00:00Z", "url_path_en": "/global/site-du-cof/news/soire-de-nol/", "chapo_en": "", "seo_title_fr": "", "chapo_fr": "Grosse soir\u00e9e en bo\u00eete pour f\u00eater la fin de l'ann\u00e9e !", "image": 35, "all_day": false, "chapo": "Grosse soir\u00e9e en bo\u00eete pour f\u00eater la fin de l'ann\u00e9e !", "search_description_en": "", "title_en": "", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "soire-de-nol", "slug_en": "", "body": "

Rendez vous au 45 rue d'Ulm pour la plus grosse soir\u00e9e de l'ann\u00e9e.

", "date_end": "2017-08-31T16:00:00Z"}, "model": "cofcms.cofactueventpage"}, {"pk": 14, "fields": {"url_path_fr": "/global/site-du-cof/clubs/", "title_fr": "Clubs", "url_path_en": "/global/site-du-cof/clubs/", "seo_title_fr": "", "introduction_fr": "

Tous les clubs de l'ENS

", "search_description_en": "", "title_en": "", "seo_title_en": "", "introduction_en": "

All the clubs in the ENS

", "search_description_fr": "", "slug_fr": "clubs", "slug_en": "", "introduction": "

Tous les clubs de l'ENS

"}, "model": "cofcms.cofdirectorypage"}, {"pk": 16, "fields": {"body_fr": "

Des jolies affiches dans l'ENS

", "url_path_fr": "/global/site-du-cof/clubs/graphiche/", "title_fr": "Graph'iche", "url_path_en": "/global/site-du-cof/clubs/graphiche/", "links_en": "[]", "seo_title_fr": "", "links_fr": "[{\"type\": \"lien\", \"value\": {\"url\": \"http://evarin.fr\", \"texte\": \"Site\"}}, {\"type\": \"contact\", \"value\": {\"email\": \"graphiche@ens.fr\", \"texte\": \"Mailing-list\"}}]", "image": 33, "search_description_en": "", "title_en": "", "links": "[{\"type\": \"lien\", \"value\": {\"url\": \"http://evarin.fr\", \"texte\": \"Site\"}}, {\"type\": \"contact\", \"value\": {\"email\": \"graphiche@ens.fr\", \"texte\": \"Mailing-list\"}}]", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "graphiche", "slug_en": "", "body": "

Des jolies affiches dans l'ENS

"}, "model": "cofcms.cofdirectoryentrypage"}, {"pk": 19, "fields": {"body_fr": "

Le club de d\u00e9bat

", "url_path_fr": "/global/site-du-cof/clubs/eloquens/", "title_fr": "Eloqu'ENS", "url_path_en": "/global/site-du-cof/clubs/eloquens/", "links_en": "[]", "seo_title_fr": "", "links_fr": "[{\"type\": \"contact\", \"value\": {\"email\": \"eloquens@ens.fr\", \"texte\": \"Mailing-liste\"}}]", "image": 37, "search_description_en": "", "title_en": "", "links": "[{\"type\": \"contact\", \"value\": {\"email\": \"eloquens@ens.fr\", \"texte\": \"Mailing-liste\"}}]", "seo_title_en": "", "body_en": "", "search_description_fr": "", "slug_fr": "eloquens", "slug_en": "", "body": "

Le club de d\u00e9bat

"}, "model": "cofcms.cofdirectoryentrypage"}] \ No newline at end of file diff --git a/gestioncof/cms/migrations/0001_initial.py b/gestioncof/cms/migrations/0001_initial.py index e9abe2fb..77b6d4a5 100644 --- a/gestioncof/cms/migrations/0001_initial.py +++ b/gestioncof/cms/migrations/0001_initial.py @@ -1,197 +1,175 @@ # -*- coding: utf-8 -*- +# Generated by Django 1.11.9 on 2018-01-20 19:10 from __future__ import unicode_literals from django.db import migrations, models -import wagtail.wagtailcore.fields -import wagtail.wagtailimages.blocks +import django.db.models.deletion import gestioncof.cms.models import wagtail.wagtailcore.blocks -import django.db.models.deletion +import wagtail.wagtailcore.fields +import wagtail.wagtailimages.blocks class Migration(migrations.Migration): + initial = True + dependencies = [ ('wagtailcore', '0033_remove_golive_expiry_help_text'), ('wagtailimages', '0019_delete_filter'), ] operations = [ - migrations.CreateModel( - name='COFActuEventPage', - fields=[ - ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), - ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), - ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), - ('chapo', models.TextField(verbose_name='Description rapide')), - ('chapo_fr', models.TextField(null=True, verbose_name='Description rapide')), - ('chapo_en', models.TextField(null=True, verbose_name='Description rapide')), - ('body', wagtail.wagtailcore.fields.RichTextField(verbose_name='Description longue')), - ('body_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description longue')), - ('body_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description longue')), - ('date_start', models.DateTimeField(verbose_name='Date et heure de début')), - ('date_end', models.DateTimeField(null=True, blank=True, default=None, verbose_name='Date et heure de fin')), - ('all_day', models.BooleanField(default=False, verbose_name='Toute la journée')), - ('image', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailimages.Image', related_name='+', verbose_name='Image à la Une')), - ], - options={ - 'verbose_name_plural': 'Actus liées à des évènements', - 'verbose_name': 'Actu liée à un évènement', - }, - bases=('wagtailcore.page',), - ), migrations.CreateModel( name='COFActuIndexPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), - ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), - ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('slug_fr', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('slug_en', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('url_path_fr', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('url_path_en', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('seo_title_fr', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('seo_title_en', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), + ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), ], options={ - 'verbose_name_plural': 'Indexs des actualités', 'verbose_name': 'Index des actualités', + 'verbose_name_plural': 'Indexs des actualités', }, bases=('wagtailcore.page', gestioncof.cms.models.COFActuIndexMixin), ), migrations.CreateModel( name='COFActuPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), - ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), - ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('slug_fr', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('slug_en', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('url_path_fr', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('url_path_en', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('seo_title_fr', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('seo_title_en', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), + ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), + ('chapo', models.TextField(blank=True, verbose_name='Description rapide')), + ('chapo_fr', models.TextField(blank=True, null=True, verbose_name='Description rapide')), + ('chapo_en', models.TextField(blank=True, null=True, verbose_name='Description rapide')), ('body', wagtail.wagtailcore.fields.RichTextField(verbose_name='Contenu')), ('body_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Contenu')), ('body_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Contenu')), - ('date', models.DateField(verbose_name='Date du post')), - ('image', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailimages.Image', related_name='+', verbose_name='Image à la Une')), + ('is_event', models.BooleanField(default=True, verbose_name='Évènement')), + ('date_start', models.DateTimeField(verbose_name='Date et heure de début')), + ('date_end', models.DateTimeField(blank=True, default=None, null=True, verbose_name='Date et heure de fin')), + ('all_day', models.BooleanField(default=False, verbose_name='Toute la journée')), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.Image', verbose_name='Image à la Une')), ], options={ - 'verbose_name_plural': 'Actualités simples', - 'verbose_name': 'Actualité simple', + 'verbose_name': 'Actualité', + 'verbose_name_plural': 'Actualités', }, bases=('wagtailcore.page',), ), migrations.CreateModel( name='COFDirectoryEntryPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), - ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), - ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('slug_fr', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('slug_en', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('url_path_fr', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('url_path_en', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('seo_title_fr', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('seo_title_en', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), + ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), ('body', wagtail.wagtailcore.fields.RichTextField(verbose_name='Description')), ('body_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description')), ('body_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Description')), ('links', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))))), ('links_fr', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))), null=True)), ('links_en', wagtail.wagtailcore.fields.StreamField((('lien', wagtail.wagtailcore.blocks.StructBlock((('url', wagtail.wagtailcore.blocks.URLBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock())))), ('contact', wagtail.wagtailcore.blocks.StructBlock((('email', wagtail.wagtailcore.blocks.EmailBlock(required=True)), ('texte', wagtail.wagtailcore.blocks.CharBlock()))))), null=True)), - ('image', models.ForeignKey(null=True, blank=True, on_delete=django.db.models.deletion.SET_NULL, to='wagtailimages.Image', related_name='+', verbose_name='Image')), + ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.Image', verbose_name='Image')), ], options={ - 'verbose_name_plural': "Éntrées d'annuaire", 'verbose_name': "Éntrée d'annuaire", + 'verbose_name_plural': "Éntrées d'annuaire", }, bases=('wagtailcore.page',), ), migrations.CreateModel( name='COFDirectoryPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), - ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), - ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('slug_fr', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('slug_en', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('url_path_fr', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('url_path_en', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('seo_title_fr', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('seo_title_en', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), + ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), ('introduction', wagtail.wagtailcore.fields.RichTextField(verbose_name='Introduction')), ('introduction_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), ('introduction_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), ], options={ - 'verbose_name_plural': 'Annuaires', 'verbose_name': 'Annuaire (clubs, partenaires, bons plans...)', + 'verbose_name_plural': 'Annuaires', }, bases=('wagtailcore.page',), ), migrations.CreateModel( name='COFPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), - ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), - ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('slug_fr', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('slug_en', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('url_path_fr', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('url_path_en', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('seo_title_fr', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('seo_title_en', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), + ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), ('body', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock())))), ('body_fr', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock())), null=True)), ('body_en', wagtail.wagtailcore.fields.StreamField((('heading', wagtail.wagtailcore.blocks.CharBlock(classname='full title')), ('paragraph', wagtail.wagtailcore.blocks.RichTextBlock()), ('image', wagtail.wagtailimages.blocks.ImageChooserBlock())), null=True)), ], options={ - 'verbose_name_plural': 'Pages normales COF', 'verbose_name': 'Page normale COF', + 'verbose_name_plural': 'Pages normales COF', }, bases=('wagtailcore.page',), ), migrations.CreateModel( name='COFRootPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, parent_link=True, to='wagtailcore.Page', primary_key=True, auto_created=True)), - ('title_fr', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('title_en', models.CharField(null=True, verbose_name='title', help_text="The page title as you'd like it to be seen by the public", max_length=255)), - ('slug_fr', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('slug_en', models.SlugField(null=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', verbose_name='slug', max_length=255)), - ('url_path_fr', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('url_path_en', models.TextField(null=True, blank=True, editable=False, verbose_name='URL path')), - ('seo_title_fr', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('seo_title_en', models.CharField(null=True, blank=True, verbose_name='page title', help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255)), - ('search_description_fr', models.TextField(null=True, blank=True, verbose_name='search description')), - ('search_description_en', models.TextField(null=True, blank=True, verbose_name='search description')), + ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.Page')), + ('title_fr', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('title_en', models.CharField(help_text="The page title as you'd like it to be seen by the public", max_length=255, null=True, verbose_name='title')), + ('slug_fr', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('slug_en', models.SlugField(allow_unicode=True, help_text='The name of the page as it will appear in URLs e.g http://domain.com/blog/[my-slug]/', max_length=255, null=True, verbose_name='slug')), + ('url_path_fr', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('url_path_en', models.TextField(blank=True, editable=False, null=True, verbose_name='URL path')), + ('seo_title_fr', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('seo_title_en', models.CharField(blank=True, help_text="Optional. 'Search Engine Friendly' title. This will appear at the top of the browser window.", max_length=255, null=True, verbose_name='page title')), + ('search_description_fr', models.TextField(blank=True, null=True, verbose_name='search description')), + ('search_description_en', models.TextField(blank=True, null=True, verbose_name='search description')), ('introduction', wagtail.wagtailcore.fields.RichTextField(verbose_name='Introduction')), ('introduction_fr', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), ('introduction_en', wagtail.wagtailcore.fields.RichTextField(null=True, verbose_name='Introduction')), ], options={ - 'verbose_name_plural': 'Racines site du COF', 'verbose_name': 'Racine site du COF', + 'verbose_name_plural': 'Racines site du COF', }, bases=('wagtailcore.page', gestioncof.cms.models.COFActuIndexMixin), ), diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index e63acde7..508c9743 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -2,6 +2,7 @@ from django.db import models from wagtail.wagtailcore.models import Page, Orderable +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from wagtail.wagtailcore.fields import RichTextField, StreamField from wagtail.wagtailcore import blocks from wagtail.wagtailimages.edit_handlers import ImageChooserPanel @@ -15,10 +16,8 @@ from wagtail.wagtailsnippets.models import register_snippet class COFActuIndexMixin(): @property def actus(self): - actus = COFActuPage.objects.live().descendant_of(self) - events = COFActuEventPage.objects.live().descendant_of(self) - genactus = list(actus) + list(events) - return genactus + actus = COFActuPage.objects.live().order_by('-date_start').descendant_of(self) + return actus # Racine du site du COF class COFRootPage(Page, COFActuIndexMixin): @@ -55,7 +54,7 @@ class COFPage(Page): # Actualités class COFActuIndexPage(Page, COFActuIndexMixin): - subpage_types = ['COFActuPage', 'COFActuEventPage'] + subpage_types = ['COFActuPage'] parent_page_types = ['COFRootPage'] class Meta: @@ -64,7 +63,7 @@ class COFActuIndexPage(Page, COFActuIndexMixin): def get_context(self, request): context = super(COFActuIndexPage, self).get_context(request) - actus = COFActuPage.objects.live().descendant_of(self).order_by('-date') + actus = COFActuPage.objects.live().descendant_of(self).order_by('-date_end') page = request.GET.get('page') paginator = Paginator(actus, 5) @@ -79,18 +78,27 @@ class COFActuIndexPage(Page, COFActuIndexMixin): return context class COFActuPage(Page): + chapo = models.TextField("Description rapide", blank=True) body = RichTextField("Contenu") - date = models.DateField("Date du post") image = models.ForeignKey( 'wagtailimages.Image', verbose_name="Image à la Une", null=True, blank=True, on_delete=models.SET_NULL, related_name='+' ) + is_event = models.BooleanField("Évènement", default=True, blank=True) + date_start = models.DateTimeField("Date et heure de début") + date_end = models.DateTimeField("Date et heure de fin", blank=True, default=None, null=True) + all_day = models.BooleanField("Toute la journée", default=False, blank=True) + content_panels = Page.content_panels + [ - FieldPanel('date'), ImageChooserPanel('image'), + FieldPanel('chapo'), FieldPanel('body', classname="full"), + FieldPanel("is_event"), + FieldPanel("date_start"), + FieldPanel("date_end"), + FieldPanel("all_day"), ] subpage_types = [] @@ -100,33 +108,6 @@ class COFActuPage(Page): verbose_name = "Actualité simple" verbose_name_plural = "Actualités simples" -# Évènements -class COFActuEventPage(Page): - chapo = models.TextField("Description rapide") - body = RichTextField("Description longue") - image = models.ForeignKey( - 'wagtailimages.Image', verbose_name="Image à la Une", - null=True, blank=True, - on_delete=models.SET_NULL, related_name='+' - ) - - date_start = models.DateTimeField("Date et heure de début") - date_end = models.DateTimeField("Date et heure de fin", blank=True, default=None, null=True) - all_day = models.BooleanField("Toute la journée", default=False, blank=True) - is_event = True - - content_panels = Page.content_panels + [ - ImageChooserPanel('image'), - FieldPanel('chapo'), - FieldPanel('body', classname="full"), - FieldPanel("date_start"), - FieldPanel("date_end"), - FieldPanel("all_day"), - ] - - subpage_types = [] - parent_page_types = ['COFActuIndexPage'] - @property def dates(self): if self.date_end: @@ -147,8 +128,8 @@ class COFActuEventPage(Page): diff_i = len(tmpl) - 3 elif self.date_end.day != self.date_start.day: diff_i = len(tmpl) - 6 - common = tmpl[diff_i:] - diff = tmpl[:diff_i] + common = tmpl[diff_i:] + diff = tmpl[:diff_i] if self.all_day: return _("du %s au %s %s") % \ (self.date_start.strftime(diff), @@ -167,8 +148,8 @@ class COFActuEventPage(Page): else: return self.date_start.strftime(_("le %A %d %B %Y à %Hh%M")) class Meta: - verbose_name = "Actu liée à un évènement" - verbose_name_plural = "Actus liées à des évènements" + verbose_name = "Actualité" + verbose_name_plural = "Actualités" # Annuaires (Clubs, partenaires, bonnes adresses) class COFDirectoryPage(Page): diff --git a/gestioncof/cms/templates/cofcms/cof_actu_event_page.html b/gestioncof/cms/templates/cofcms/cof_actu_event_page.html deleted file mode 100644 index 3640ece8..00000000 --- a/gestioncof/cms/templates/cofcms/cof_actu_event_page.html +++ /dev/null @@ -1,17 +0,0 @@ -{% extends "cofcms/base.html" %} -{% load wagtailimages_tags cofcms_tags %} - -{% block content %} -
-

{{ page.title }}

-

A lieu {{ page.dates }}

-

{{ page.chapo }}

-
- -
- {% image page.image width-700 %} -
- {{ page.body|safe }} -
-
-{% endblock %} diff --git a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html index 975e520d..ae1a6f8a 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_index_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_index_page.html @@ -27,7 +27,7 @@ {% if actu.is_event %}

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

{% else %} - {{ actu.body|safe|truncatewords_html:25 }} + {{ actu.body|safe|truncatewords_html:15 }} {% endif %} Lire plus > diff --git a/gestioncof/cms/templates/cofcms/cof_actu_page.html b/gestioncof/cms/templates/cofcms/cof_actu_page.html index ef4830be..3640ece8 100644 --- a/gestioncof/cms/templates/cofcms/cof_actu_page.html +++ b/gestioncof/cms/templates/cofcms/cof_actu_page.html @@ -4,7 +4,8 @@ {% block content %}

{{ page.title }}

-

Publié le {{ page.date }}

+

A lieu {{ page.dates }}

+

{{ page.chapo }}

diff --git a/gestioncof/cms/templates/cofcms/cof_root_page.html b/gestioncof/cms/templates/cofcms/cof_root_page.html index 84b60b80..75acc2fa 100644 --- a/gestioncof/cms/templates/cofcms/cof_root_page.html +++ b/gestioncof/cms/templates/cofcms/cof_root_page.html @@ -22,7 +22,7 @@ {% get_current_language as curlang %} {% mini_calendar actu curlang %}{{ actu.dates }} {% else %} - {{ actu.body|safe|truncatewords_html:25 }} + {{ actu.body|safe|truncatewords_html:10 }} {% endif %} diff --git a/gestioncof/cms/templatetags/cofcms_tags.py b/gestioncof/cms/templatetags/cofcms_tags.py index 7d426e46..e226d33b 100644 --- a/gestioncof/cms/templatetags/cofcms_tags.py +++ b/gestioncof/cms/templatetags/cofcms_tags.py @@ -4,7 +4,7 @@ from django.conf import settings from django.utils import timezone import locale -from ..models import COFActuEventPage +from ..models import COFActuPage import re @@ -23,10 +23,10 @@ def calendar(): next_month = date(next_month.year, next_month.month, 1) month_prestart = month_start - timedelta(days=month_start.weekday()) month_postend = next_month + timedelta(days=(next_month.weekday()+6)%7) - events = COFActuEventPage.objects.live()\ - .filter(date_start__range=[month_prestart, - month_postend])\ - .order_by('-date_start') + events = COFActuPage.objects.live()\ + .filter(date_start__range=[month_prestart, + month_postend])\ + .order_by('-date_start') events = list(events) weeks = [] curday = month_prestart diff --git a/gestioncof/cms/translation.py b/gestioncof/cms/translation.py index 2be97221..ef7bd77d 100644 --- a/gestioncof/cms/translation.py +++ b/gestioncof/cms/translation.py @@ -1,4 +1,4 @@ -from .models import COFRootPage, COFPage, COFActuEventPage, COFActuIndexPage, COFActuPage, COFDirectoryPage, COFDirectoryEntryPage +from .models import COFRootPage, COFPage, COFActuIndexPage, COFActuPage, COFDirectoryPage, COFDirectoryEntryPage from wagtail_modeltranslation.translator import WagtailTranslationOptions from modeltranslation.decorators import register @@ -15,13 +15,6 @@ class COFPageTr(WagtailTranslationOptions): 'body', ) -@register(COFActuEventPage) -class COFActuEventPageTr(WagtailTranslationOptions): - fields = ( - 'chapo', - 'body', - ) - @register(COFActuIndexPage) class COFActuIndexPageTr(WagtailTranslationOptions): fields = ( @@ -30,6 +23,7 @@ class COFActuIndexPageTr(WagtailTranslationOptions): @register(COFActuPage) class COFActuPageTr(WagtailTranslationOptions): fields = ( + 'chapo', 'body', ) From c11ccf2ecc96d09809b30ee3b21c2740e0aaaa96 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 28 Jan 2018 19:09:35 +0100 Subject: [PATCH 014/773] Tri des annuaires --- .../cms/migrations/0002_auto_20180128_1717.py | 25 +++++++++++++++++++ gestioncof/cms/models.py | 12 ++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 gestioncof/cms/migrations/0002_auto_20180128_1717.py diff --git a/gestioncof/cms/migrations/0002_auto_20180128_1717.py b/gestioncof/cms/migrations/0002_auto_20180128_1717.py new file mode 100644 index 00000000..9b32ab15 --- /dev/null +++ b/gestioncof/cms/migrations/0002_auto_20180128_1717.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.9 on 2018-01-28 16:17 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cofcms', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='cofdirectoryentrypage', + name='sort_order', + field=models.IntegerField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name='cofdirectorypage', + name='alphabetique', + field=models.BooleanField(default=True, verbose_name='Tri par ordre alphabétique ?'), + ), + ] diff --git a/gestioncof/cms/models.py b/gestioncof/cms/models.py index 508c9743..da6f6a8b 100644 --- a/gestioncof/cms/models.py +++ b/gestioncof/cms/models.py @@ -154,9 +154,12 @@ class COFActuPage(Page): # Annuaires (Clubs, partenaires, bonnes adresses) class COFDirectoryPage(Page): introduction = RichTextField("Introduction") - + alphabetique = models.BooleanField("Tri par ordre alphabétique ?", + default=True, blank=True) + content_panels = Page.content_panels + [ FieldPanel('introduction'), + FieldPanel('alphabetique'), ] subpage_types = ['COFActuPage', 'COFDirectoryEntryPage'] @@ -165,8 +168,9 @@ class COFDirectoryPage(Page): @property def entries(self): entries = COFDirectoryEntryPage.objects.live()\ - .descendant_of(self)\ - .order_by("title") + .descendant_of(self) + if self.alphabetique: + entries = entries.order_by("title") return entries class Meta: @@ -174,7 +178,7 @@ class COFDirectoryPage(Page): verbose_name_plural = "Annuaires" -class COFDirectoryEntryPage(Page): +class COFDirectoryEntryPage(Page, Orderable): body = RichTextField("Description") links = StreamField([ ('lien', blocks.StructBlock([ From f8952225d69e70aa348c175b99453687ff10bcc7 Mon Sep 17 00:00:00 2001 From: Evarin Date: Sun, 28 Jan 2018 19:09:56 +0100 Subject: [PATCH 015/773] Apparence et Responsiveness --- gestioncof/cms/static/cofcms/css/screen.css | 297 ++++++++++++++---- .../cms/static/cofcms/images/minimenu.svg | 11 + .../cms/static/cofcms/sass/_colors.scss | 16 +- .../cms/static/cofcms/sass/_responsive.scss | 124 ++++++++ gestioncof/cms/static/cofcms/sass/screen.scss | 84 +++-- gestioncof/cms/templates/cofcms/base.html | 3 +- .../cms/templates/cofcms/base_aside.html | 7 +- .../templates/cofcms/cof_actu_index_page.html | 1 + .../cms/templates/cofcms/cof_actu_page.html | 2 +- .../templates/cofcms/cof_directory_page.html | 2 +- gestioncof/cms/templates/cofcms/cof_page.html | 4 +- .../cms/templates/cofcms/cof_root_page.html | 1 + 12 files changed, 453 insertions(+), 99 deletions(-) create mode 100644 gestioncof/cms/static/cofcms/images/minimenu.svg create mode 100644 gestioncof/cms/static/cofcms/sass/_responsive.scss diff --git a/gestioncof/cms/static/cofcms/css/screen.css b/gestioncof/cms/static/cofcms/css/screen.css index 965206c3..0f82ce97 100644 --- a/gestioncof/cms/static/cofcms/css/screen.css +++ b/gestioncof/cms/static/cofcms/css/screen.css @@ -75,19 +75,19 @@ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, /* line 16, ../sass/screen.scss */ body { - background: #f5ffd2; + background: #fefefe; font: 17px "Source Sans Pro", "sans-serif"; } /* line 21, ../sass/screen.scss */ header { - background: #400530; + background: #5B0012; } /* line 25, ../sass/screen.scss */ h1, h2 { - font-family: "Carter One"; - color: #2c3900; + font-family: "Carter One", "serif"; + color: #90001C; } /* line 30, ../sass/screen.scss */ @@ -102,7 +102,7 @@ h2 { /* line 38, ../sass/screen.scss */ a { - color: #3cb3a6; + color: #CC9500; text-decoration: none; font-weight: bold; } @@ -115,7 +115,7 @@ h2 a { /* line 50, ../sass/screen.scss */ header a { - color: #f5ffd2; + color: #fefefe; } /* line 53, ../sass/screen.scss */ header section { @@ -128,7 +128,7 @@ header section { header section.bottom-menu { justify-content: space-around; text-align: center; - background: #5a004f; + background: #90001C; } /* line 65, ../sass/screen.scss */ header h1 { @@ -150,7 +150,7 @@ header nav ul li > * { } /* line 78, ../sass/screen.scss */ header nav ul li > *:hover { - background: #11010d; + background: #280008; } /* line 87, ../sass/screen.scss */ @@ -185,151 +185,184 @@ article:last-child { position: absolute; top: 30px; height: 100%; - width: 250px; + width: 25%; + left: 6px; } -/* line 114, ../sass/screen.scss */ +/* line 115, ../sass/screen.scss */ .container .aside-wrap .aside { - color: #fff; + color: #222; position: fixed; position: sticky; top: 5px; width: 100%; - background: #5a004f; + background: #FFC500; padding: 15px; - box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.3); + box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.3); } -/* line 124, ../sass/screen.scss */ +/* line 125, ../sass/screen.scss */ .container .aside-wrap .aside h2 { color: #fff; } -/* line 128, ../sass/screen.scss */ +/* line 129, ../sass/screen.scss */ .container .aside-wrap .aside .calendar { margin: 0 auto; display: block; } -/* line 135, ../sass/screen.scss */ -.container .content { - max-width: 700px; - margin-left: auto; - margin-right: 0; +/* line 134, ../sass/screen.scss */ +.container .aside-wrap .aside a { + color: #997000; } /* line 140, ../sass/screen.scss */ +.container .content { + max-width: 900px; + margin-left: auto; + margin-right: 6px; +} +/* line 145, ../sass/screen.scss */ .container .content .intro { - border-bottom: 3px solid #a3d200; - margin: 20px -10px; + border-bottom: 3px solid #7f7f7f; + margin: 20px 0; margin-top: 5px; padding: 15px 5px; } -/* line 150, ../sass/screen.scss */ +/* line 154, ../sass/screen.scss */ .container .content section article { background: #fff; - padding: 30px; - box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.3); + padding: 20px 30px; + box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.3); + border: 1px solid rgba(153, 118, 0, 0.1); + border-radius: 2px; } -/* line 154, ../sass/screen.scss */ +/* line 160, ../sass/screen.scss */ .container .content section article a { - color: #3cb3a6; + color: #CC9500; } -/* line 159, ../sass/screen.scss */ +/* line 165, ../sass/screen.scss */ .container .content section article + h2 { margin-top: 15px; } -/* line 163, ../sass/screen.scss */ +/* line 169, ../sass/screen.scss */ .container .content section article + article { margin-top: 25px; } -/* line 168, ../sass/screen.scss */ +/* line 173, ../sass/screen.scss */ +.container .content section .image { + margin: 15px 0; + text-align: center; + padding: 20px; +} +/* line 178, ../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 186, ../sass/screen.scss */ .container .content section.directory article.entry { width: 80%; max-width: 600px; max-height: 100%; position: relative; - padding-right: 120px; + margin-left: 6%; } -/* line 175, ../sass/screen.scss */ +/* line 193, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image { display: block; - position: absolute; + float: right; width: 150px; background: #fff; - box-shadow: -4px 4px 1px rgba(0, 0, 0, 0.2); + box-shadow: -4px 4px 1px rgba(153, 118, 0, 0.2); + border-right: 1px solid rgba(153, 118, 0, 0.2); + border-top: 1px solid rgba(153, 118, 0, 0.2); padding: 1px; overflow: hidden; - right: 100px; - transform: translateX(90%); - top: -15px; + margin-left: 10px; + margin-bottom: 10px; + transform: translateX(10px); } -/* line 187, ../sass/screen.scss */ +/* line 207, ../sass/screen.scss */ .container .content section.directory article.entry .entry-image img { width: auto; height: auto; max-width: 100%; max-height: 100%; } -/* line 197, ../sass/screen.scss */ +/* line 215, ../sass/screen.scss */ +.container .content section.directory article.entry ul.links { + margin-top: 10px; + border-top: 1px solid #90001C; + padding-top: 10px; +} +/* line 223, ../sass/screen.scss */ .container .content section.actuhome { display: flex; flex-wrap: wrap; justify-content: space-around; align-items: top; } -/* line 203, ../sass/screen.scss */ +/* line 229, ../sass/screen.scss */ .container .content section.actuhome article + article { margin: 0; } -/* line 207, ../sass/screen.scss */ +/* line 233, ../sass/screen.scss */ .container .content section.actuhome article.actu { position: relative; background: none; box-shadow: none; + border: none; max-width: 400px; min-width: 300px; flex: 1; } -/* line 215, ../sass/screen.scss */ +/* line 242, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header { position: relative; - box-shadow: -4px 5px 1px rgba(0, 0, 0, 0.3); + box-shadow: -4px 5px 1px rgba(153, 118, 0, 0.3); + border-right: 1px solid rgba(153, 118, 0, 0.2); + border-top: 1px solid rgba(153, 118, 0, 0.2); min-height: 180px; padding: 0; margin: 0; overflow: hidden; background-size: cover; background-position: center center; + background-repeat: no-repeat; } -/* line 225, ../sass/screen.scss */ +/* line 255, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 { position: absolute; width: 100%; bottom: 0; left: 0; padding: 5px; - text-shadow: 0 0 5px rgba(0, 0, 0, 0.8); + text-shadow: 0 0 5px rgba(153, 118, 0, 0.8); background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent); } -/* line 233, ../sass/screen.scss */ +/* line 263, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-header h2 a { color: #fff; } -/* line 239, ../sass/screen.scss */ +/* line 269, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc { background: white; - box-shadow: -2px 2px 1px rgba(0, 0, 0, 0.2); + box-shadow: -2px 2px 1px rgba(153, 118, 0, 0.2); + border: 1px solid rgba(153, 118, 0, 0.2); + border-radius: 2px; margin: 0 10px; padding: 15px; padding-top: 5px; } -/* line 246, ../sass/screen.scss */ +/* line 278, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-minical { display: block; } -/* line 249, ../sass/screen.scss */ +/* line 281, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-misc .actu-dates { display: block; text-align: right; font-size: 0.9em; } -/* line 256, ../sass/screen.scss */ +/* line 288, ../sass/screen.scss */ .container .content section.actuhome article.actu .actu-overlay { display: block; background: none; @@ -341,66 +374,202 @@ article:last-child { z-index: 5; opacity: 0; } -/* line 272, ../sass/screen.scss */ +/* line 304, ../sass/screen.scss */ .container .content section.actulist article.actu { display: flex; width: 100%; padding: 0; } -/* line 277, ../sass/screen.scss */ +/* line 309, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-image { width: 30%; max-width: 200px; background-size: cover; background-position: center center; } -/* line 283, ../sass/screen.scss */ +/* line 315, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos { padding: 15px; flex: 1; } -/* line 287, ../sass/screen.scss */ +/* line 319, ../sass/screen.scss */ .container .content section.actulist article.actu .actu-infos .actu-dates { font-weight: bold; font-size: 0.9em; } +/* line 329, ../sass/screen.scss */ +.container .aside-wrap + .content { + max-width: 70%; +} -/* line 299, ../sass/screen.scss */ +/* line 335, ../sass/screen.scss */ .calendar td, .calendar th { text-align: center; vertical-align: center; border: 2px solid transparent; padding: 1px; } -/* line 306, ../sass/screen.scss */ +/* line 342, ../sass/screen.scss */ .calendar th { font-weight: bold; } -/* line 310, ../sass/screen.scss */ +/* line 346, ../sass/screen.scss */ .calendar td { font-size: 0.8em; width: 25px; height: 30px; } -/* line 315, ../sass/screen.scss */ +/* line 351, ../sass/screen.scss */ .calendar td.out { opacity: 0.3; } -/* line 318, ../sass/screen.scss */ +/* line 354, ../sass/screen.scss */ .calendar td.today { border-bottom-color: #000; } -/* line 321, ../sass/screen.scss */ +/* line 357, ../sass/screen.scss */ .calendar td:nth-child(7) { background: rgba(0, 0, 0, 0.3); } -/* line 324, ../sass/screen.scss */ +/* line 360, ../sass/screen.scss */ .calendar td:nth-child(6) { background: rgba(0, 0, 0, 0.2); } -/* line 327, ../sass/screen.scss */ +/* line 363, ../sass/screen.scss */ .calendar td.hasevent { font-weight: bold; - color: #3cb3a6; + color: #997000; font-size: 1em; } + +/* line 1, ../sass/_responsive.scss */ +header .minimenu { + display: none; +} + +@media only screen and (max-width: 600px) { + /* line 6, ../sass/_responsive.scss */ + header { + position: fixed; + top: 0; + left: 0; + z-index: 10; + width: 100%; + max-height: 100vh; + height: 60px; + overflow: hidden; + } + /* line 16, ../sass/_responsive.scss */ + header .minimenu { + display: block; + position: absolute; + right: 3px; + top: 3px; + } + /* line 23, ../sass/_responsive.scss */ + header section { + display: block; + } + /* line 25, ../sass/_responsive.scss */ + header section nav { + display: none; + } + + /* line 31, ../sass/_responsive.scss */ + header.expanded { + overflow: auto; + height: auto; + } + /* line 35, ../sass/_responsive.scss */ + header.expanded nav { + display: block; + text-align: center; + } + /* line 38, ../sass/_responsive.scss */ + header.expanded nav ul { + flex-wrap: wrap; + justify-content: right; + } + /* line 41, ../sass/_responsive.scss */ + header.expanded nav ul li > * { + padding: 18px; + } + + /* line 48, ../sass/_responsive.scss */ + .container { + margin-top: 65px; + } + /* line 51, ../sass/_responsive.scss */ + .container .content { + max-width: unset; + margin: 6px; + } + /* line 56, ../sass/_responsive.scss */ + .container .content section article { + padding: 10px; + } + /* line 60, ../sass/_responsive.scss */ + .container .content section .image { + padding: 0; + margin: 10px -6px; + } + /* line 65, ../sass/_responsive.scss */ + .container .content section.directory article.entry { + width: 100%; + margin-left: 0; + } + /* line 72, ../sass/_responsive.scss */ + .container .aside-wrap + .content { + max-width: unset; + margin-top: 120px; + } + /* line 77, ../sass/_responsive.scss */ + .container .aside-wrap { + z-index: 3; + top: 60px; + position: fixed; + width: 100%; + margin: 0; + height: auto; + left: 0; + } + /* line 86, ../sass/_responsive.scss */ + .container .aside-wrap .aside { + margin: 0; + padding: 0; + top: 0; + position: absolute; + } + /* line 92, ../sass/_responsive.scss */ + .container .aside-wrap .aside > h2 { + position: relative; + cursor: pointer; + padding: 5px 10px; + } + /* line 96, ../sass/_responsive.scss */ + .container .aside-wrap .aside > h2:after { + content: "v"; + font-family: "Source Sans Pro", "sans-serif"; + font-weight: bold; + color: #CC9500; + position: absolute; + right: 10px; + } + /* line 106, ../sass/_responsive.scss */ + .container .aside-wrap .aside:not(.expanded) .aside-content { + display: none; + } + /* line 111, ../sass/_responsive.scss */ + .container .aside-wrap .aside ul { + text-align: center; + } + /* line 113, ../sass/_responsive.scss */ + .container .aside-wrap .aside ul li { + display: inline-block; + } + /* line 115, ../sass/_responsive.scss */ + .container .aside-wrap .aside ul li > * { + display: block; + padding: 15px; + } +} diff --git a/gestioncof/cms/static/cofcms/images/minimenu.svg b/gestioncof/cms/static/cofcms/images/minimenu.svg new file mode 100644 index 00000000..46a31695 --- /dev/null +++ b/gestioncof/cms/static/cofcms/images/minimenu.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/gestioncof/cms/static/cofcms/sass/_colors.scss b/gestioncof/cms/static/cofcms/sass/_colors.scss index bd93019b..2d295b98 100644 --- a/gestioncof/cms/static/cofcms/sass/_colors.scss +++ b/gestioncof/cms/static/cofcms/sass/_colors.scss @@ -1,7 +1,11 @@ -$fond: #f5ffd2; -$bandeau: #400530; -$sousbandeau: #5a004f; -$aside: $sousbandeau; -$titre: darken($fond, 80%); -$lien: #3cb3a6; +$fond: #fefefe; +$bandeau: #5B0012; +$sousbandeau: #90001C; +$aside: #FFC500; +$titre: $sousbandeau; +$lien: #CC9500; $headerlien: $fond; +$ombres: darken($aside, 20%); + +$bodyfont: "Source Sans Pro", "sans-serif"; +$headfont: "Carter One", "serif"; diff --git a/gestioncof/cms/static/cofcms/sass/_responsive.scss b/gestioncof/cms/static/cofcms/sass/_responsive.scss new file mode 100644 index 00000000..26e835fd --- /dev/null +++ b/gestioncof/cms/static/cofcms/sass/_responsive.scss @@ -0,0 +1,124 @@ +header .minimenu { + display: none; +} + +@media only screen and (max-width: 600px) { + header { + position: fixed; + top: 0; + left: 0; + z-index: 10; + width: 100%; + max-height: 100vh; + height: 60px; + overflow: hidden; + + .minimenu { + display: block; + position: absolute; + right: 3px; + top: 3px; + } + + section { + display: block; + nav { + display: none; + } + } + } + + header.expanded { + overflow: auto; + height: auto; + + nav { + display: block; + text-align: center; + ul { + flex-wrap: wrap; + justify-content: right; + li > * { + padding: 18px; + } + } + } + } + + .container { + margin-top: 65px; + + .content { + max-width: unset; + margin: 6px; + + section { + article { + padding: 10px; + } + + .image { + padding: 0; + margin: 10px -6px; + } + + &.directory article.entry { + width: 100%; + margin-left: 0; + } + } + } + + .aside-wrap + .content { + max-width: unset; + margin-top: 120px; + } + + .aside-wrap { + z-index: 3; + top: 60px; + position: fixed; + width: 100%; + margin: 0; + height: auto; + left: 0; + + .aside { + margin: 0; + padding: 0; + top: 0; + position: absolute; + + & > h2 { + position: relative; + cursor: pointer; + padding: 5px 10px; + &:after { + content: "v"; + font-family: $bodyfont; + font-weight: bold; + color: $lien; + position: absolute; + right: 10px; + } + } + &:not(.expanded) { + .aside-content { + display: none; + } + } + + ul { + text-align: center; + li { + display: inline-block; + & > * { + display: block; + padding: 15px; + } + } + } + } + } + } +} diff --git a/gestioncof/cms/static/cofcms/sass/screen.scss b/gestioncof/cms/static/cofcms/sass/screen.scss index 7a69cf5b..a389822b 100644 --- a/gestioncof/cms/static/cofcms/sass/screen.scss +++ b/gestioncof/cms/static/cofcms/sass/screen.scss @@ -15,7 +15,7 @@ body { background: $fond; - font: 17px "Source Sans Pro", "sans-serif"; + font: 17px $bodyfont; } header { @@ -23,7 +23,7 @@ header { } h1, h2 { - font-family: "Carter One"; + font-family: $headfont; color: $titre; } @@ -109,17 +109,18 @@ article { position: absolute; top: 30px; height: 100%; - width: 250px; + width: 25%; + left: 6px; .aside { - color: #fff; + color: #222; position: fixed; position: sticky; top: 5px; width: 100%; background: $aside; padding: 15px; - box-shadow: -4px 4px 1px rgba(#000, 0.3); + box-shadow: -4px 4px 1px rgba($ombres, 0.3); h2 { color: #fff; @@ -129,28 +130,33 @@ article { margin: 0 auto; display: block; } + + a { + color: darken($lien, 10%); + } } } - + .content { - max-width: 700px; + max-width: 900px; margin-left: auto; - margin-right: 0; + margin-right: 6px; .intro { border-bottom: 3px solid darken($fond, 50%); - margin: 20px -10px; + margin: 20px 0; margin-top: 5px; padding: 15px 5px; - } section { article { background: #fff; - padding: 30px; - box-shadow: -4px 4px 1px rgba(#000, 0.3); + padding: 20px 30px;; + box-shadow: -4px 4px 1px rgba($ombres, 0.3); + border: 1px solid rgba($ombres, 0.1); + border-radius: 2px; a { color: $lien; } @@ -164,25 +170,39 @@ article { margin-top: 25px; } + .image { + margin: 15px 0; + text-align: center; + padding: 20px; + + img { + max-width: 100%; + height: auto; + box-shadow: -7px 7px 1px rgba($ombres, 0.2); + } + } + &.directory { article.entry { width: 80%; max-width: 600px; max-height: 100%; position: relative; - padding-right: 120px; - + margin-left: 6%; + .entry-image { display: block; - position: absolute; + float: right; width: 150px; background: #fff; - box-shadow: -4px 4px 1px rgba(#000, 0.2); + box-shadow: -4px 4px 1px rgba($ombres, 0.2); + border-right: 1px solid rgba($ombres, 0.2); + border-top: 1px solid rgba($ombres, 0.2); padding: 1px; overflow: hidden; - right: 100px; - transform: translateX(90%); - top: -15px; + margin-left: 10px; + margin-bottom: 10px; + transform: translateX(10px); img { width: auto; @@ -191,6 +211,12 @@ article { max-height: 100%; } } + + ul.links { + margin-top: 10px; + border-top: 1px solid $sousbandeau; + padding-top: 10px; + } } } @@ -208,19 +234,23 @@ article { position: relative; background: none; box-shadow: none; + border: none; max-width: 400px; min-width: 300px; flex: 1; .actu-header { position: relative; - box-shadow: -4px 5px 1px rgba(#000, 0.3); + box-shadow: -4px 5px 1px rgba($ombres, 0.3); + border-right: 1px solid rgba($ombres, 0.2); + border-top: 1px solid rgba($ombres, 0.2); min-height: 180px; padding: 0; margin: 0; overflow: hidden; background-size: cover; background-position: center center; + background-repeat: no-repeat; h2 { position: absolute; @@ -228,7 +258,7 @@ article { bottom: 0; left: 0; padding: 5px; - text-shadow: 0 0 5px rgba(#000, 0.8); + text-shadow: 0 0 5px rgba($ombres, 0.8); background: linear-gradient(to top, rgba(#000, 0.7), rgba(#000, 0)); a { color: #fff; @@ -238,7 +268,9 @@ article { .actu-misc { background: lighten($fond, 15%); - box-shadow: -2px 2px 1px rgba(#000, 0.2); + box-shadow: -2px 2px 1px rgba($ombres, 0.2); + border: 1px solid rgba($ombres, 0.2); + border-radius: 2px; margin: 0 10px; padding: 15px; padding-top: 5px; @@ -293,6 +325,10 @@ article { } } } + + .aside-wrap + .content { + max-width: 70%; + } } .calendar { @@ -326,8 +362,10 @@ article { } &.hasevent { font-weight: bold; - color: $lien; + color: darken($lien, 10%); font-size: 1em; } } } + +@import "_responsive"; diff --git a/gestioncof/cms/templates/cofcms/base.html b/gestioncof/cms/templates/cofcms/base.html index 05119dbd..056e37ec 100644 --- a/gestioncof/cms/templates/cofcms/base.html +++ b/gestioncof/cms/templates/cofcms/base.html @@ -10,9 +10,10 @@ -
+
{% 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 329/773] 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 330/773] 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 331/773] 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 332/773] =?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 333/773] =?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 334/773] 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 335/773] 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 336/773] 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 337/773] 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 338/773] =?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 339/773] 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 340/773] 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 341/773] 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 342/773] 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 343/773] 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 344/773] 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 345/773] 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 346/773] 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 347/773] 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 348/773] 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 349/773] 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 350/773] 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 351/773] 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 352/773] 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 353/773] 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 354/773] 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 355/773] 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 356/773] 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 357/773] 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 358/773] 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 359/773] 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 360/773] 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 361/773] 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 362/773] 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 363/773] 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 364/773] 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 365/773] 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 366/773] 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 367/773] 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 368/773] 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 369/773] 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 370/773] =?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 371/773] 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 372/773] =?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 373/773] =?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 374/773] 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 375/773] 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 376/773] 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 377/773] 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 378/773] 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 379/773] 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 380/773] 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 381/773] 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 382/773] 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 383/773] 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 384/773] 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 385/773] 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 386/773] 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 387/773] 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 388/773] 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 389/773] 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 390/773] 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 391/773] 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 392/773] 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 393/773] 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 394/773] 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 395/773] 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 396/773] 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 397/773] 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 398/773] 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 419/773] 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 420/773] 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 556/773] 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 557/773] 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 558/773] 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 559/773] 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 560/773] 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 561/773] 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 562/773] =?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 563/773] 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 564/773] 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 565/773] 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 566/773] =?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 567/773] =?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 568/773] 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 569/773] =?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 570/773] 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 571/773] 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 572/773] 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 573/773] =?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 574/773] =?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 575/773] 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 576/773] 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 577/773] 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 578/773] 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 579/773] 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 580/773] 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 581/773] 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 582/773] 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 583/773] 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 584/773] 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 585/773] 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 586/773] 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 587/773] 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 588/773] 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 589/773] 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 590/773] =?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 591/773] 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 592/773] 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 593/773] 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 594/773] 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 595/773] 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 596/773] 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 597/773] 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 598/773] 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 599/773] 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 600/773] 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 601/773] 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 602/773] 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 603/773] 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 604/773] 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 605/773] 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 606/773] 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 607/773] 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 608/773] 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 609/773] 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 610/773] 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 611/773] 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 612/773] 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 613/773] 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 614/773] 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 615/773] 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 616/773] 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 617/773] 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 618/773] 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 619/773] 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 620/773] 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 621/773] =?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 622/773] 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 623/773] 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 624/773] 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 625/773] 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 626/773] 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 627/773] 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 628/773] 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 629/773] 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 630/773] 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 631/773] 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 632/773] 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 633/773] 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 634/773] 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 635/773] 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 636/773] 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 637/773] 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 638/773] 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 639/773] 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 640/773] 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 641/773] 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 642/773] 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 643/773] 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 644/773] 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 645/773] 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 646/773] =?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 647/773] 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 648/773] =?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 649/773] 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 650/773] 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 651/773] 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 652/773] 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 653/773] 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 654/773] 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 655/773] 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 656/773] 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 657/773] 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 658/773] 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 659/773] 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 660/773] =?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 661/773] 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 662/773] 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 663/773] 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 664/773] 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 665/773] 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 666/773] 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 667/773] 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 668/773] 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 669/773] 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 670/773] 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 671/773] 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 %}