From 65d7a66eb8db2a5b90d804686541dba645217032 Mon Sep 17 00:00:00 2001 From: Evarin Date: Mon, 7 Aug 2017 23:31:27 +0200 Subject: [PATCH 001/211] =?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/211] 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/211] 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/211] =?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/211] 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/211] 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 012/211] 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 013/211] 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 732e47707e96bb7047438edb1791511e89918fa1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 17:25:58 +0200 Subject: [PATCH 014/211] Add unsubscribe option + list of current draws --- bda/forms.py | 41 +++++++++++++++++++-- bda/templates/bda/revente-tirages.html | 28 +++++++++++++++ bda/urls.py | 11 +++--- bda/views.py | 50 +++++++++++++++++++++++++- gestioncof/templates/home.html | 7 ++-- 5 files changed, 126 insertions(+), 11 deletions(-) create mode 100644 bda/templates/bda/revente-tirages.html diff --git a/bda/forms.py b/bda/forms.py index c0417d1e..139ef45d 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -4,7 +4,7 @@ from django import forms from django.forms.models import BaseInlineFormSet from django.utils import timezone -from bda.models import Attribution, Spectacle +from bda.models import Attribution, Spectacle, SpectacleRevente class InscriptionInlineFormSet(BaseInlineFormSet): @@ -45,6 +45,9 @@ class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): return "%s" % str(obj.spectacle) +class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): + def label_from_instance(self, obj): + return "%s" % str(obj.attribution.spectacle) class ResellForm(forms.Form): attributions = AttributionModelMultipleChoiceField( @@ -63,7 +66,6 @@ class ResellForm(forms.Form): 'participant__user') ) - class AnnulForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', @@ -83,7 +85,6 @@ class AnnulForm(forms.Form): 'participant__user') ) - class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( queryset=Spectacle.objects.none(), @@ -98,6 +99,40 @@ class InscriptionReventeForm(forms.Form): .filter(date__gte=timezone.now()) ) +class ReventeTirageAnnulForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + participant.wanted.filter(soldTo__isnull=True) + .select_related('attribution__spectacle') + ) + + +class ReventeTirageForm(forms.Form): + reventes = ReventeModelMultipleChoiceField( + label='', + queryset=SpectacleRevente.objects.none(), + widget=forms.CheckboxSelectMultiple, + required=False + ) + + def __init__(self, participant, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['reventes'].queryset = ( + SpectacleRevente.objects.filter( + notif_sent=True, + shotgun=False, + tirage_done=False + ).exclude(answered_mail=participant) + .select_related('attribution__spectacle') + ) class SoldForm(forms.Form): attributions = AttributionModelMultipleChoiceField( diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente-tirages.html new file mode 100644 index 00000000..bd738673 --- /dev/null +++ b/bda/templates/bda/revente-tirages.html @@ -0,0 +1,28 @@ +{% extends "base_title.html" %} +{% load bootstrap %} + +{% block realcontent %} + +

    Tirages au sort de reventes

    +{% if annulform.reventes %} +

    Mes inscriptions

    +
    + {% csrf_token %} + {{annulform|bootstrap}} +
    + +
    +
    +{% endif %} +
    +{% if subform.reventes %} +

    Tirages en cours

    +
    + {% csrf_token %} + {{subform|bootstrap}} +
    + +
    +
    +{% endif %} +{% endblock %} diff --git a/bda/urls.py b/bda/urls.py index 876c84ea..51dd4235 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -32,16 +32,19 @@ urlpatterns = [ url(r'^spectacles/unpaid/(?P\d+)$', views.unpaid, name="bda-unpaid"), - url(r'^liste-revente/(?P\d+)$', + url(r'^revente/(?P\d+)/list$', views.list_revente, name="bda-liste-revente"), - url(r'^buy-revente/(?P\d+)$', + url(r'^revente/(?P\d+)/tirages$', + views.revente_tirages, + name="bda-revente-tirages"), + url(r'^revente/(?P\d+)/buy$', views.buy_revente, name="bda-buy-revente"), - url(r'^revente-interested/(?P\d+)$', + url(r'^revente/(?P\d+)/interested$', views.revente_interested, name='bda-revente-interested'), - url(r'^revente-immediat/(?P\d+)$', + url(r'^revente/(?P\d+)/immediat$', views.revente_shotgun, name="bda-shotgun"), url(r'^mails-rappel/(?P\d+)$', diff --git a/bda/views.py b/bda/views.py index 84b6c9d3..4b75c116 100644 --- a/bda/views.py +++ b/bda/views.py @@ -30,7 +30,7 @@ from bda.models import ( from bda.algorithm import Algorithm from bda.forms import ( TokenForm, ResellForm, AnnulForm, InscriptionReventeForm, SoldForm, - InscriptionInlineFormSet, + InscriptionInlineFormSet, ReventeTirageForm, ReventeTirageAnnulForm ) @@ -377,6 +377,7 @@ def revente(request, tirage_id): if not created: revente.seller = participant revente.date = timezone.now() + revente.wanted = Participant.objects.none() revente.soldTo = None revente.notif_sent = False revente.tirage_done = False @@ -442,6 +443,53 @@ def revente(request, tirage_id): "annulform": annulform, "resellform": resellform}) +@login_required +def revente_tirages(request, tirage_id): + tirage = get_object_or_404(Tirage, id=tirage_id) + participant, _ = Participant.objects.get_or_create( + user=request.user, tirage=tirage) + unsub = 0 + subform = ReventeTirageForm(participant, prefix="subscribe") + annulform = ReventeTirageAnnulForm(participant, prefix="annul") + + if request.method == 'POST': + if "subscribe" in request.POST: + subform = ReventeTirageForm(participant, request.POST, + prefix="subscribe") + if subform.is_valid(): + sub = 0 + reventes = subform.cleaned_data['reventes'] + for revente in reventes: + revente.answered_mail.add(participant) + sub += 1 + if sub > 0: + plural = "s" if sub > 1 else "" + messages.success( + request, + "Tu as bien été inscrit à {} revente{}" + .format(sub, plural) + ) + elif "annul" in request.POST: + annulform = ReventeTirageAnnulForm(participant, request.POST, + prefix="annul") + if annulform.is_valid(): + unsub = 0 + reventes = annulform.cleaned_data['reventes'] + for revente in reventes: + revente.answered_mail.remove(participant) + unsub += 1 + if unsub > 0: + plural = "s" if unsub > 1 else "" + messages.success( + request, + "Tu as bien été désinscrit de {} revente{}" + .format(unsub, plural) + ) + + return render(request, "bda/revente-tirages.html", + {"annulform": annulform, "subform": subform}) + + @login_required def revente_interested(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index acc04f30..f7ca57b5 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -43,9 +43,10 @@
  • État des demandes
  • {% else %}
  • Mes places
  • -
  • Revendre une place
  • -
  • S'inscrire à BdA-Revente
  • -
  • Places disponibles immédiatement
  • +
  • Gestion de mes reventes
  • +
  • Reventes en cours
  • +
  • S'inscrire à BdA-Revente
  • +
  • Places disponibles immédiatement
  • {% endif %}
{% endfor %} From e74dbb11f1556a4ffb3cc42f83686a3a27cf5f45 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 18:39:45 +0200 Subject: [PATCH 015/211] Organize revente files and function names --- .../revente/confirm-shotgun.html} | 0 .../revente/confirmed.html} | 0 .../revente/mail-success.html} | 0 .../{reventes.html => revente/manage.html} | 0 .../revente/none.html} | 0 .../revente/notpaid.html} | 0 .../revente/shotgun.html} | 2 +- .../subscribe.html} | 0 .../tirages.html} | 0 .../revente/wrongtime.html} | 2 +- bda/urls.py | 30 +++++++++++-------- bda/views.py | 30 +++++++++---------- gestioncof/management/data/custommail.json | 2 +- gestioncof/templates/home.html | 6 ++-- 14 files changed, 38 insertions(+), 34 deletions(-) rename bda/templates/{revente-confirm.html => bda/revente/confirm-shotgun.html} (100%) rename bda/templates/{bda-interested.html => bda/revente/confirmed.html} (100%) rename bda/templates/{bda-success.html => bda/revente/mail-success.html} (100%) rename bda/templates/bda/{reventes.html => revente/manage.html} (100%) rename bda/templates/{bda-no-revente.html => bda/revente/none.html} (100%) rename bda/templates/{bda-notpaid.html => bda/revente/notpaid.html} (100%) rename bda/templates/{bda-shotgun.html => bda/revente/shotgun.html} (83%) rename bda/templates/bda/{liste-reventes.html => revente/subscribe.html} (100%) rename bda/templates/bda/{revente-tirages.html => revente/tirages.html} (100%) rename bda/templates/{bda-wrongtime.html => bda/revente/wrongtime.html} (86%) diff --git a/bda/templates/revente-confirm.html b/bda/templates/bda/revente/confirm-shotgun.html similarity index 100% rename from bda/templates/revente-confirm.html rename to bda/templates/bda/revente/confirm-shotgun.html diff --git a/bda/templates/bda-interested.html b/bda/templates/bda/revente/confirmed.html similarity index 100% rename from bda/templates/bda-interested.html rename to bda/templates/bda/revente/confirmed.html diff --git a/bda/templates/bda-success.html b/bda/templates/bda/revente/mail-success.html similarity index 100% rename from bda/templates/bda-success.html rename to bda/templates/bda/revente/mail-success.html diff --git a/bda/templates/bda/reventes.html b/bda/templates/bda/revente/manage.html similarity index 100% rename from bda/templates/bda/reventes.html rename to bda/templates/bda/revente/manage.html diff --git a/bda/templates/bda-no-revente.html b/bda/templates/bda/revente/none.html similarity index 100% rename from bda/templates/bda-no-revente.html rename to bda/templates/bda/revente/none.html diff --git a/bda/templates/bda-notpaid.html b/bda/templates/bda/revente/notpaid.html similarity index 100% rename from bda/templates/bda-notpaid.html rename to bda/templates/bda/revente/notpaid.html diff --git a/bda/templates/bda-shotgun.html b/bda/templates/bda/revente/shotgun.html similarity index 83% rename from bda/templates/bda-shotgun.html rename to bda/templates/bda/revente/shotgun.html index e10fae00..fae36c04 100644 --- a/bda/templates/bda-shotgun.html +++ b/bda/templates/bda/revente/shotgun.html @@ -5,7 +5,7 @@ {% if shotgun %}
    {% for spectacle in shotgun %} -
  • {{spectacle}}
  • +
  • {{spectacle}}
  • {% endfor %} {% else %}

    Pas de places disponibles immédiatement, désolé !

    diff --git a/bda/templates/bda/liste-reventes.html b/bda/templates/bda/revente/subscribe.html similarity index 100% rename from bda/templates/bda/liste-reventes.html rename to bda/templates/bda/revente/subscribe.html diff --git a/bda/templates/bda/revente-tirages.html b/bda/templates/bda/revente/tirages.html similarity index 100% rename from bda/templates/bda/revente-tirages.html rename to bda/templates/bda/revente/tirages.html diff --git a/bda/templates/bda-wrongtime.html b/bda/templates/bda/revente/wrongtime.html similarity index 86% rename from bda/templates/bda-wrongtime.html rename to bda/templates/bda/revente/wrongtime.html index dfafb05f..18c417a2 100644 --- a/bda/templates/bda-wrongtime.html +++ b/bda/templates/bda/revente/wrongtime.html @@ -6,7 +6,7 @@

    Le tirage au sort de cette revente a déjà été effectué !

    Si personne n'était intéressé, elle est maintenant disponible - ici.

    + ici.

    {% else %}

    Il n'est pas encore possible de s'inscrire à cette revente, réessaie dans quelque temps !

    {% endif %} diff --git a/bda/urls.py b/bda/urls.py index 51dd4235..7588187c 100644 --- a/bda/urls.py +++ b/bda/urls.py @@ -16,9 +16,6 @@ urlpatterns = [ url(r'^places/(?P\d+)$', views.places, name="bda-places-attribuees"), - url(r'^revente/(?P\d+)$', - views.revente, - name='bda-revente'), url(r'^etat-places/(?P\d+)$', views.etat_places, name='bda-etat-places'), @@ -32,21 +29,28 @@ urlpatterns = [ url(r'^spectacles/unpaid/(?P\d+)$', views.unpaid, name="bda-unpaid"), - url(r'^revente/(?P\d+)/list$', - views.list_revente, - name="bda-liste-revente"), + + # Urls BdA-Revente + + url(r'^revente/(?P\d+)/manage$', + views.revente_manage, + name='bda-revente-manage'), + url(r'^revente/(?P\d+)/subscribe$', + views.revente_subscribe, + name="bda-revente-subscribe"), url(r'^revente/(?P\d+)/tirages$', views.revente_tirages, name="bda-revente-tirages"), url(r'^revente/(?P\d+)/buy$', - views.buy_revente, - name="bda-buy-revente"), - url(r'^revente/(?P\d+)/interested$', - views.revente_interested, - name='bda-revente-interested'), - url(r'^revente/(?P\d+)/immediat$', + views.revente_buy, + name="bda-revente-buy"), + url(r'^revente/(?P\d+)/confirm$', + views.revente_confirm, + name='bda-revente-confirm'), + url(r'^revente/(?P\d+)/shotgun$', views.revente_shotgun, - name="bda-shotgun"), + name="bda-revente-shotgun"), + url(r'^mails-rappel/(?P\d+)$', views.send_rappel, name="bda-rappels" diff --git a/bda/views.py b/bda/views.py index 4b75c116..c0e64230 100644 --- a/bda/views.py +++ b/bda/views.py @@ -349,13 +349,13 @@ def tirage(request, tirage_id): @login_required -def revente(request, tirage_id): +def revente_manage(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, created = Participant.objects.get_or_create( user=request.user, tirage=tirage) if not participant.paid: - return render(request, "bda-notpaid.html", {}) + return render(request, "bda/revente/notpaid.html", {}) resellform = ResellForm(participant, prefix='resell') annulform = AnnulForm(participant, prefix='annul') @@ -438,7 +438,7 @@ def revente(request, tirage_id): .filter( Q(revente__soldTo__isnull=True) | Q(revente__soldTo=participant)) - return render(request, "bda/reventes.html", + return render(request, "bda/revente/manage.html", {'tirage': tirage, 'overdue': overdue, "soldform": soldform, "annulform": annulform, "resellform": resellform}) @@ -486,27 +486,27 @@ def revente_tirages(request, tirage_id): .format(unsub, plural) ) - return render(request, "bda/revente-tirages.html", + return render(request, "bda/revente/tirages.html", {"annulform": annulform, "subform": subform}) @login_required -def revente_interested(request, revente_id): +def revente_confirm(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=revente.attribution.spectacle.tirage) if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: - return render(request, "bda-wrongtime.html", + return render(request, "bda/revente/wrongtime.html", {"revente": revente}) revente.answered_mail.add(participant) - return render(request, "bda-interested.html", + return render(request, "bda/revente/confirmed.html", {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}) @login_required -def list_revente(request, tirage_id): +def revente_subscribe(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=tirage) @@ -560,11 +560,11 @@ def list_revente(request, tirage_id): ) messages.info(request, msg, extra_tags="safe") - return render(request, "bda/liste-reventes.html", {"form": form}) + return render(request, "bda/revente/subscribe.html", {"form": form}) @login_required -def buy_revente(request, spectacle_id): +def revente_buy(request, spectacle_id): spectacle = get_object_or_404(Spectacle, id=spectacle_id) tirage = spectacle.tirage participant, _ = Participant.objects.get_or_create( @@ -578,13 +578,13 @@ def buy_revente(request, spectacle_id): own_reventes = reventes.filter(seller=participant) if len(own_reventes) > 0: own_reventes[0].delete() - return HttpResponseRedirect(reverse("bda-shotgun", + return HttpResponseRedirect(reverse("bda-revente-shotgun", args=[tirage.id])) reventes_shotgun = reventes.filter(shotgun=True) if not reventes_shotgun: - return render(request, "bda-no-revente.html", {}) + return render(request, "bda/revente/none.html", {}) if request.POST: revente = random.choice(reventes_shotgun) @@ -601,11 +601,11 @@ def buy_revente(request, spectacle_id): [revente.seller.user.email], context=context, ) - return render(request, "bda-success.html", + return render(request, "bda/revente/mail-success.html", {"seller": revente.attribution.participant.user, "spectacle": spectacle}) - return render(request, "revente-confirm.html", + return render(request, "bda/revente/confirm-shotgun.html", {"spectacle": spectacle, "user": request.user}) @@ -629,7 +629,7 @@ def revente_shotgun(request, tirage_id): ) shotgun = [sp for sp in spectacles if len(sp.shotguns) > 0] - return render(request, "bda-shotgun.html", + return render(request, "bda/revente/shotgun.html", {"shotgun": shotgun}) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index 9ee9b1ea..bf59e5f6 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -151,7 +151,7 @@ "shortname": "bda-revente", "subject": "{{ show }}", "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-confirm\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" } }, { diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index f7ca57b5..943ef780 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -43,10 +43,10 @@
  • État des demandes
  • {% else %}
  • Mes places
  • -
  • Gestion de mes reventes
  • +
  • Gestion de mes reventes
  • Reventes en cours
  • -
  • S'inscrire à BdA-Revente
  • -
  • Places disponibles immédiatement
  • +
  • S'inscrire à BdA-Revente
  • +
  • Places disponibles immédiatement
  • {% endif %}
{% endfor %} From 919bcd197d077767bdc07355a70a84d39f8ebecf Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 18:59:30 +0200 Subject: [PATCH 016/211] Small code QoL improvements --- bda/models.py | 10 ++++++++++ bda/views.py | 10 ++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/bda/models.py b/bda/models.py index 41462d70..5533e3bb 100644 --- a/bda/models.py +++ b/bda/models.py @@ -252,6 +252,16 @@ class SpectacleRevente(models.Model): class Meta: verbose_name = "Revente" + def reset(self): + """Réinitialise la revente pour permettre une remise sur le marché""" + self.seller = self.attribution.participant + self.date = timezone.now() + self.answered_mail.clear() + self.soldTo = None + self.notif_sent = False + self.tirage_done = False + self.shotgun = False + def send_notif(self): """ Envoie une notification pour indiquer la mise en vente d'une place sur diff --git a/bda/views.py b/bda/views.py index c0e64230..311d530a 100644 --- a/bda/views.py +++ b/bda/views.py @@ -375,13 +375,7 @@ def revente_manage(request, tirage_id): attribution=attribution, defaults={'seller': participant}) if not created: - revente.seller = participant - revente.date = timezone.now() - revente.wanted = Participant.objects.none() - revente.soldTo = None - revente.notif_sent = False - revente.tirage_done = False - revente.shotgun = False + revente.reset() context = { 'vendeur': participant.user, 'show': attribution.spectacle, @@ -495,7 +489,7 @@ def revente_confirm(request, revente_id): revente = get_object_or_404(SpectacleRevente, id=revente_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=revente.attribution.spectacle.tirage) - if (timezone.now() < revente.date + timedelta(hours=1)) or revente.shotgun: + if not revente.notif_sent or revente.shotgun: return render(request, "bda/revente/wrongtime.html", {"revente": revente}) From 1b0e4285ecbc7ea224cb7fad9c725365d4a9ba01 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:26:07 +0200 Subject: [PATCH 017/211] Reverse match fix --- gestioncof/management/data/custommail.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index bf59e5f6..029c03e0 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -161,7 +161,7 @@ "shortname": "bda-shotgun", "subject": "{{ show }}", "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-revente-buy\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" } }, { From 684603709e90e471f5528ab35f1efc2011145fb5 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:30:11 +0200 Subject: [PATCH 018/211] Class attributes and properties + more verbose log SpectacleRevente gets brand new properties and attributes to simplify code ; also, manage_reventes command output is more verbose --- bda/management/commands/manage_reventes.py | 48 ++++++++++++++-------- bda/models.py | 45 +++++++++++++++++--- 2 files changed, 69 insertions(+), 24 deletions(-) diff --git a/bda/management/commands/manage_reventes.py b/bda/management/commands/manage_reventes.py index 0302ec4b..5a767604 100644 --- a/bda/management/commands/manage_reventes.py +++ b/bda/management/commands/manage_reventes.py @@ -6,7 +6,6 @@ Gestion en ligne de commande des reventes. from __future__ import unicode_literals -from datetime import timedelta from django.core.management import BaseCommand from django.utils import timezone from bda.models import SpectacleRevente @@ -21,23 +20,36 @@ class Command(BaseCommand): now = timezone.now() reventes = SpectacleRevente.objects.all() for revente in reventes: - # Check si < 24h - if (revente.attribution.spectacle.date <= - revente.date + timedelta(days=1)) and \ - now >= revente.date + timedelta(minutes=15) and \ - not revente.notif_sent: - self.stdout.write(str(now)) - revente.mail_shotgun() - self.stdout.write("Mail de disponibilité immédiate envoyé") - # Check si délai de retrait dépassé - elif (now >= revente.date + timedelta(hours=1) and - not revente.notif_sent): + # Le spectacle est bientôt et on a pas encore envoyé de mail : + # on met la place au shotgun et on prévient. + if revente.is_urgent and not revente.notif_sent: + if revente.can_notif: + self.stdout.write(str(now)) + revente.mail_shotgun() + self.stdout.write( + "Mails de disponibilité immédiate envoyés " + "pour la revente [%s]" % revente + ) + + # Le spectacle est dans plus longtemps : on prévient + elif (revente.can_notif and not revente.notif_sent): self.stdout.write(str(now)) revente.send_notif() - self.stdout.write("Mail d'inscription à une revente envoyé") - # Check si tirage à faire - elif (now >= revente.date_tirage and - not revente.tirage_done): + self.stdout.write( + "Mails d'inscription à la revente [%s] envoyés" + % revente + ) + + # On fait le tirage + elif (now >= revente.date_tirage and not revente.tirage_done): self.stdout.write(str(now)) - revente.tirage() - self.stdout.write("Tirage effectué, mails envoyés") + winner = revente.tirage() + self.stdout.write( + "Tirage effectué pour la revente [%s]" + % revente + ) + + if winner: + self.stdout.write("Gagnant : %s" % winner.user) + else: + self.stdout.write("Pas de gagnant ; place au shotgun") diff --git a/bda/models.py b/bda/models.py index 5533e3bb..b2882900 100644 --- a/bda/models.py +++ b/bda/models.py @@ -233,17 +233,46 @@ class SpectacleRevente(models.Model): default=False) shotgun = models.BooleanField("Disponible immédiatement", default=False) + #### + # Some class attributes + ### + # TODO : settings ? + + # Temps minimum entre le tirage et le spectacle + min_margin = timedelta(days=5) + + # Temps entre la création d'une revente et l'envoi du mail + remorse_time = timedelta(hours=1) + + # Temps min/max d'attente avant le tirage + max_wait_time = timedelta(days=3) + min_wait_time = timedelta(days=1) @property def date_tirage(self): """Renvoie la date du tirage au sort de la revente.""" - # L'acheteur doit être connu au plus 12h avant le spectacle + notif_time = self.date + self.remorse_time + remaining_time = (self.attribution.spectacle.date - - self.date - timedelta(hours=13)) - # Au minimum, on attend 2 jours avant le tirage - delay = min(remaining_time, timedelta(days=2)) - # Le vendeur a aussi 1h pour changer d'avis - return self.date + delay + timedelta(hours=1) + - notif_time - self.min_margin) + + delay = min(remaining_time, self.max_wait_time) + + return notif_time + delay + + @property + def is_urgent(self): + """ + Renvoie True iff la revente doit être mise au shotgun directement. + Plus précisément, on doit avoir min_margin + min_wait_time de marge. + """ + spectacle_date = self.attribution.spectacle.date + return (spectacle_date <= timezone.now() + self.min_margin + + self.min_wait_time) + + @property + def can_notif(self): + return (timezone.now() >= self.date + self.remorse_time) def __str__(self): return "%s -- %s" % (self.seller, @@ -353,8 +382,12 @@ class SpectacleRevente(models.Model): [inscrit.user.email] )) send_mass_custom_mail(datatuple) + + return winner + # Si personne ne veut de la place, elle part au shotgun else: self.shotgun = True + return None self.tirage_done = True self.save() From 6a6549e0d72937d9f5adaf7bf43ff090150b4891 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Mon, 23 Oct 2017 20:52:25 +0200 Subject: [PATCH 019/211] Add notif time In case of a gestioCOF bug, we keep the notification time in memory to still do the drawing 1-3 days after. --- bda/admin.py | 6 +++--- bda/forms.py | 4 ++-- bda/migrations/0012_notif_time.py | 28 ++++++++++++++++++++++++++++ bda/models.py | 29 +++++++++++++++++++++-------- bda/views.py | 14 +++++++------- 5 files changed, 61 insertions(+), 20 deletions(-) create mode 100644 bda/migrations/0012_notif_time.py diff --git a/bda/admin.py b/bda/admin.py index 60d3c1ba..4f5d821a 100644 --- a/bda/admin.py +++ b/bda/admin.py @@ -225,7 +225,7 @@ class SpectacleReventeAdminForm(forms.ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields['answered_mail'].queryset = ( + self.fields['confirmed_entry'].queryset = ( Participant.objects .select_related('user', 'tirage') ) @@ -292,8 +292,8 @@ class SpectacleReventeAdmin(admin.ModelAdmin): revente.soldTo = None revente.notif_sent = False revente.tirage_done = False - if revente.answered_mail: - revente.answered_mail.clear() + if revente.confirmed_entry: + revente.confirmed_entry.clear() revente.save() self.message_user( request, diff --git a/bda/forms.py b/bda/forms.py index 139ef45d..11d05b0e 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -110,7 +110,7 @@ class ReventeTirageAnnulForm(forms.Form): def __init__(self, participant, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['reventes'].queryset = ( - participant.wanted.filter(soldTo__isnull=True) + participant.entered.filter(soldTo__isnull=True) .select_related('attribution__spectacle') ) @@ -130,7 +130,7 @@ class ReventeTirageForm(forms.Form): notif_sent=True, shotgun=False, tirage_done=False - ).exclude(answered_mail=participant) + ).exclude(confirmed_entry=participant) .select_related('attribution__spectacle') ) diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py new file mode 100644 index 00000000..be66efd1 --- /dev/null +++ b/bda/migrations/0012_notif_time.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bda', '0011_tirage_appear_catalogue'), + ] + + operations = [ + migrations.RemoveField( + model_name='spectaclerevente', + name='answered_mail', + ), + migrations.AddField( + model_name='spectaclerevente', + name='confirmed_entry', + field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), + ), + migrations.AddField( + model_name='spectaclerevente', + name='notif_time', + field=models.DateTimeField(blank=True, verbose_name="Moment d'envoi de la notification", null=True), + ), + ] diff --git a/bda/models.py b/bda/models.py index b2882900..2ad47dbf 100644 --- a/bda/models.py +++ b/bda/models.py @@ -218,9 +218,9 @@ class SpectacleRevente(models.Model): related_name="revente") date = models.DateTimeField("Date de mise en vente", default=timezone.now) - answered_mail = models.ManyToManyField(Participant, - related_name="wanted", - blank=True) + confirmed_entry = models.ManyToManyField(Participant, + related_name="entered", + blank=True) seller = models.ForeignKey(Participant, related_name="original_shows", verbose_name="Vendeur") @@ -229,8 +229,13 @@ class SpectacleRevente(models.Model): notif_sent = models.BooleanField("Notification envoyée", default=False) + + notif_time = models.DateTimeField("Moment d'envoi de la notification", + blank=True, null=True) + tirage_done = models.BooleanField("Tirage effectué", default=False) + shotgun = models.BooleanField("Disponible immédiatement", default=False) #### @@ -248,17 +253,23 @@ class SpectacleRevente(models.Model): max_wait_time = timedelta(days=3) min_wait_time = timedelta(days=1) + @property + def real_notif_time(self): + if self.notif_time: + return self.notif_time + else: + return self.date + self.remorse_time + @property def date_tirage(self): """Renvoie la date du tirage au sort de la revente.""" - notif_time = self.date + self.remorse_time remaining_time = (self.attribution.spectacle.date - - notif_time - self.min_margin) + - self.real_notif_time - self.min_margin) delay = min(remaining_time, self.max_wait_time) - return notif_time + delay + return self.real_notif_time + delay @property def is_urgent(self): @@ -285,7 +296,7 @@ class SpectacleRevente(models.Model): """Réinitialise la revente pour permettre une remise sur le marché""" self.seller = self.attribution.participant self.date = timezone.now() - self.answered_mail.clear() + self.confirmed_entry.clear() self.soldTo = None self.notif_sent = False self.tirage_done = False @@ -311,6 +322,7 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() self.save() def mail_shotgun(self): @@ -332,6 +344,7 @@ class SpectacleRevente(models.Model): ] send_mass_custom_mail(datatuple) self.notif_sent = True + self.notif_time = timezone.now() # Flag inutile, sauf si l'horloge interne merde self.tirage_done = True self.shotgun = True @@ -343,7 +356,7 @@ class SpectacleRevente(models.Model): parmis les personnes intéressées par le spectacle. Les personnes sont ensuites prévenues par mail du résultat du tirage. """ - inscrits = list(self.answered_mail.all()) + inscrits = list(self.confirmed_entry.all()) spectacle = self.attribution.spectacle seller = self.seller diff --git a/bda/views.py b/bda/views.py index 311d530a..6ed22b21 100644 --- a/bda/views.py +++ b/bda/views.py @@ -420,8 +420,8 @@ def revente_manage(request, tirage_id): revente.notif_sent = False revente.tirage_done = False revente.shotgun = False - if revente.answered_mail: - revente.answered_mail.clear() + if revente.confirmed_entry: + revente.confirmed_entry.clear() revente.save() overdue = participant.attribution_set.filter( @@ -454,7 +454,7 @@ def revente_tirages(request, tirage_id): sub = 0 reventes = subform.cleaned_data['reventes'] for revente in reventes: - revente.answered_mail.add(participant) + revente.confirmed_entry.add(participant) sub += 1 if sub > 0: plural = "s" if sub > 1 else "" @@ -470,7 +470,7 @@ def revente_tirages(request, tirage_id): unsub = 0 reventes = annulform.cleaned_data['reventes'] for revente in reventes: - revente.answered_mail.remove(participant) + revente.confirmed_entry.remove(participant) unsub += 1 if unsub > 0: plural = "s" if unsub > 1 else "" @@ -493,7 +493,7 @@ def revente_confirm(request, revente_id): return render(request, "bda/revente/wrongtime.html", {"revente": revente}) - revente.answered_mail.add(participant) + revente.confirmed_entry.add(participant) return render(request, "bda/revente/confirmed.html", {"spectacle": revente.attribution.spectacle, "date": revente.date_tirage}) @@ -526,12 +526,12 @@ def revente_subscribe(request, tirage_id): # la revente ayant le moins d'inscrits min_resell = ( qset.filter(shotgun=False) - .annotate(nb_subscribers=Count('answered_mail')) + .annotate(nb_subscribers=Count('confirmed_entry')) .order_by('nb_subscribers') .first() ) if min_resell is not None: - min_resell.answered_mail.add(participant) + min_resell.confirmed_entry.add(participant) inscrit_revente.append(spectacle) success = True else: From a07b5308a322ac03b3f252b4a74b025ed1f0c1d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 25 Oct 2017 22:01:58 +0200 Subject: [PATCH 020/211] PetitCoursAttributionCounter defaults to 0 --- gestioncof/petits_cours_models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gestioncof/petits_cours_models.py b/gestioncof/petits_cours_models.py index 753e8674..d9ea9668 100644 --- a/gestioncof/petits_cours_models.py +++ b/gestioncof/petits_cours_models.py @@ -157,14 +157,16 @@ class PetitCoursAttributionCounter(models.Model): compteurs de tout le monde. """ counter, created = cls.objects.get_or_create( - user=user, matiere=matiere) + user=user, + matiere=matiere, + ) if created: mincount = ( cls.objects.filter(matiere=matiere).exclude(user=user) .aggregate(Min('count')) ['count__min'] ) - counter.count = mincount + counter.count = mincount or 0 counter.save() return counter From 40abe27e8185f843ed2d313c978701832fe0543b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 25 Oct 2017 22:05:14 +0200 Subject: [PATCH 021/211] EMAIL_HOST needs to be set but as a secret --- cof/settings/common.py | 1 + cof/settings/secret_example.py | 1 + 2 files changed, 2 insertions(+) diff --git a/cof/settings/common.py b/cof/settings/common.py index f92dc83b..a2ea3f5e 100644 --- a/cof/settings/common.py +++ b/cof/settings/common.py @@ -31,6 +31,7 @@ def import_secret(name): SECRET_KEY = import_secret("SECRET_KEY") ADMINS = import_secret("ADMINS") SERVER_EMAIL = import_secret("SERVER_EMAIL") +EMAIL_HOST = import_secret("EMAIL_HOST") DBNAME = import_secret("DBNAME") DBUSER = import_secret("DBUSER") diff --git a/cof/settings/secret_example.py b/cof/settings/secret_example.py index e9c0e63c..e966565a 100644 --- a/cof/settings/secret_example.py +++ b/cof/settings/secret_example.py @@ -1,6 +1,7 @@ SECRET_KEY = 'q()(zn4m63i%5cp4)f+ww4-28_w+ly3q9=6imw2ciu&_(5_4ah' ADMINS = None SERVER_EMAIL = "root@vagrant" +EMAIL_HOST = "localhost" DBUSER = "cof_gestion" DBNAME = "cof_gestion" From 1a136088bfa8892eebde77cb5aa8619dd342b3ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 25 Oct 2017 22:08:29 +0200 Subject: [PATCH 022/211] Add missing type in custommail (dev only) --- gestioncof/management/data/custommail.json | 655 +++++++++++---------- 1 file changed, 334 insertions(+), 321 deletions(-) diff --git a/gestioncof/management/data/custommail.json b/gestioncof/management/data/custommail.json index 9ee9b1ea..590ebf18 100644 --- a/gestioncof/management/data/custommail.json +++ b/gestioncof/management/data/custommail.json @@ -1,587 +1,600 @@ [ { - "model": "custommail.variabletype", - "pk": 1, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "auth", "user" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 1 }, { - "model": "custommail.variabletype", - "pk": 2, + "model": "custommail.type", "fields": { + "kind": "int", "content_type": null, "inner1": null, - "kind": "int", "inner2": null - } + }, + "pk": 2 }, { - "model": "custommail.variabletype", - "pk": 3, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "bda", "spectacle" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 3 }, { - "model": "custommail.variabletype", - "pk": 4, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "bda", "spectaclerevente" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 4 }, { - "model": "custommail.variabletype", - "pk": 5, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "sites", "site" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 5 }, { - "model": "custommail.variabletype", - "pk": 6, + "model": "custommail.type", "fields": { + "kind": "model", "content_type": [ "gestioncof", "petitcoursdemande" ], "inner1": null, - "kind": "model", "inner2": null - } + }, + "pk": 6 }, { - "model": "custommail.variabletype", - "pk": 7, + "model": "custommail.type", "fields": { - "content_type": null, - "inner1": null, "kind": "list", + "content_type": null, + "inner1": 12, "inner2": null - } + }, + "pk": 7 }, { - "model": "custommail.variabletype", - "pk": 8, + "model": "custommail.type", "fields": { + "kind": "list", "content_type": null, "inner1": 1, - "kind": "list", "inner2": null - } + }, + "pk": 8 }, { - "model": "custommail.variabletype", - "pk": 9, + "model": "custommail.type", "fields": { - "content_type": null, - "inner1": null, "kind": "pair", + "content_type": null, + "inner1": 12, "inner2": 8 - } + }, + "pk": 9 }, { - "model": "custommail.variabletype", - "pk": 10, + "model": "custommail.type", "fields": { + "kind": "list", "content_type": null, "inner1": 9, - "kind": "list", "inner2": null - } + }, + "pk": 10 }, { - "model": "custommail.variabletype", - "pk": 11, + "model": "custommail.type", "fields": { + "kind": "list", "content_type": null, "inner1": 3, - "kind": "list", "inner2": null - } + }, + "pk": 11 +}, +{ + "model": "custommail.type", + "fields": { + "kind": "model", + "content_type": [ + "gestioncof", + "petitcourssubject" + ], + "inner1": null, + "inner2": null + }, + "pk": 12 }, { "model": "custommail.custommail", - "pk": 1, "fields": { "shortname": "welcome", "subject": "Bienvenue au COF", - "description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre", - "body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime." - } + "body": "Bonjour {{ member.first_name }} et bienvenue au COF !\r\n\r\nTu trouveras plein de trucs cool sur le site du COF : https://www.cof.ens.fr/ et notre page Facebook : https://www.facebook.com/cof.ulm\r\nEt n'oublie pas d'aller d\u00e9couvrir GestioCOF, la plateforme de gestion du COF !\r\nSi tu as des questions, tu peux nous envoyer un mail \u00e0 cof@ens.fr (on aime le spam), ou passer nous voir au Bur\u00f4 pr\u00e8s de la Cour\u00f4 du lundi au vendredi de 12h \u00e0 14h et de 18h \u00e0 20h.\r\n\r\nRetrouvez les \u00e9v\u00e8nements de rentr\u00e9e pour les conscrit.e.s et les vieux/vieilles organis\u00e9s par le COF et ses clubs ici : http://www.cof.ens.fr/depot/Rentree.pdf \r\n\r\nAmicalement,\r\n\r\nTon COF qui t'aime.", + "description": "Mail de bienvenue au COF envoy\u00e9 automatiquement \u00e0 l'inscription d'un nouveau membre" + }, + "pk": 1 }, { "model": "custommail.custommail", - "pk": 2, "fields": { "shortname": "bda-rappel", "subject": "{{ show }}", - "description": "Mail de rappel pour les spectacles BdA", - "body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts" - } + "body": "Bonjour {{ member.first_name }},\r\n\r\nNous te rappellons que tu as eu la chance d'obtenir {{ nb_attr|pluralize:\"une place,deux places\" }}\r\npour {{ show.title }}, le {{ show.date }} au {{ show.location }}. N'oublie pas de t'y rendre !\r\n{% if nb_attr == 2 %}\r\nTu as obtenu deux places pour ce spectacle. Nous te rappelons que\r\nces places sont strictement r\u00e9serv\u00e9es aux personnes de moins de 28 ans.\r\n{% endif %}\r\n{% if show.listing %}Pour ce spectacle, tu as re\u00e7u des places sur\r\nlisting. Il te faudra donc te rendre 15 minutes en avance sur les lieux de la repr\u00e9sentation\r\npour retirer {{ nb_attr|pluralize:\"ta place,tes places\" }}.\r\n{% else %}Pour assister \u00e0 ce spectacle, tu dois pr\u00e9senter les billets qui ont\r\n\u00e9t\u00e9 distribu\u00e9s au bur\u00f4.\r\n{% endif %}\r\n\r\nSi tu ne peux plus assister \u00e0 cette repr\u00e9sentation, tu peux\r\nrevendre ta place via BdA-revente, accessible directement sur\r\nGestioCOF (lien \"revendre une place du premier tirage\" sur la page\r\nd'accueil https://www.cof.ens.fr/gestion/).\r\n\r\nEn te souhaitant un excellent spectacle,\r\n\r\nLe Bureau des Arts", + "description": "Mail de rappel pour les spectacles BdA" + }, + "pk": 2 }, { "model": "custommail.custommail", - "pk": 3, "fields": { "shortname": "bda-revente", "subject": "{{ show }}", - "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nSi ce spectacle t'int\u00e9resse toujours, merci de nous le signaler en cliquant\r\nsur ce lien : http://{{ site }}{% url \"bda-revente-interested\" revente.id %}.\r\nDans le cas o\u00f9 plusieurs personnes seraient int\u00e9ress\u00e9es, nous proc\u00e8derons \u00e0\r\nun tirage au sort le {{ revente.date_tirage|date:\"DATE_FORMAT\" }}.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Notification envoy\u00e9e \u00e0 toutes les personnes int\u00e9ress\u00e9es par un spectacle pour le signaler qu'une place vient d'\u00eatre mise en vente." + }, + "pk": 3 }, { "model": "custommail.custommail", - "pk": 4, "fields": { "shortname": "bda-shotgun", "subject": "{{ show }}", - "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es.", - "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ member.first_name }}\r\n\r\nUne place pour le spectacle {{ show.title }} ({{ show.date }})\r\na \u00e9t\u00e9 post\u00e9e sur BdA-Revente.\r\n\r\nPuisque ce spectacle a lieu dans moins de 24h, il n'y a pas de tirage au sort pour\r\ncette place : elle est disponible imm\u00e9diatement \u00e0 l'adresse\r\nhttp://{{ site }}{% url \"bda-buy-revente\" show.id %}, \u00e0 la disposition de tous.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Notification signalant qu'une place est au shotgun aux personnes int\u00e9ress\u00e9es." + }, + "pk": 4 }, { "model": "custommail.custommail", - "pk": 5, "fields": { "shortname": "bda-revente-winner", "subject": "BdA-Revente : {{ show.title }}", - "description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente", - "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu as \u00e9t\u00e9 tir\u00e9-e au sort pour racheter une place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nTu peux contacter le/la vendeur-se \u00e0 l'adresse {{ vendeur.email }}.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Mail envoy\u00e9 au gagnant d'un tirage BdA-Revente" + }, + "pk": 5 }, { "model": "custommail.custommail", - "pk": 6, "fields": { "shortname": "bda-revente-loser", "subject": "BdA-Revente : {{ show.title }}", - "description": "Notification envoy\u00e9e aux perdants d'un tirage de revente.", - "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts" - } + "body": "Bonjour {{ acheteur.first_name }},\r\n\r\nTu t'\u00e9tais inscrit-e pour la revente de la place de {{ vendeur.get_full_name }}\r\npour {{ show.title }}.\r\nMalheureusement, une autre personne a \u00e9t\u00e9 tir\u00e9e au sort pour racheter la place.\r\nTu pourras certainement retenter ta chance pour une autre revente !\r\n\r\n\u00c0 tr\u00e8s bient\u00f4t,\r\nLe Bureau des Arts", + "description": "Notification envoy\u00e9e aux perdants d'un tirage de revente." + }, + "pk": 6 }, { "model": "custommail.custommail", - "pk": 7, "fields": { "shortname": "bda-revente-seller", "subject": "BdA-Revente : {{ show.title }}", - "description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e", - "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA" - } + "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nLa personne tir\u00e9e au sort pour racheter ta place pour {{ show.title }} est {{ acheteur.get_full_name }}.\r\nTu peux le/la contacter \u00e0 l'adresse {{ acheteur.email }}, ou en r\u00e9pondant \u00e0 ce mail.\r\n\r\nChaleureusement,\r\nLe BdA", + "description": "Notification envoy\u00e9e au vendeur d'une place pour lui indiquer qu'elle vient d'\u00eatre attribu\u00e9e" + }, + "pk": 7 }, { "model": "custommail.custommail", - "pk": 8, "fields": { "shortname": "bda-revente-new", "subject": "BdA-Revente : {{ show.title }}", - "description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires.", - "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts" - } + "body": "Bonjour {{ vendeur.first_name }},\r\n\r\nTu t\u2019es bien inscrit-e pour la revente de {{ show.title }}.\r\n\r\n{% with revente.date_tirage as time %}\r\nLe tirage au sort entre tout-e-s les racheteuse-eur-s potentiel-le-s aura lieu\r\nle {{ time|date:\"DATE_FORMAT\" }} \u00e0 {{ time|time:\"TIME_FORMAT\" }} (dans {{time|timeuntil }}).\r\nSi personne ne s\u2019est inscrit pour racheter la place, celle-ci apparaitra parmi\r\nles \u00ab Places disponibles imm\u00e9diatement \u00e0 la revente \u00bb sur GestioCOF.\r\n{% endwith %}\r\n\r\nBonne revente !\r\nLe Bureau des Arts", + "description": "Notification signalant au vendeur d'une place que sa mise en vente a bien eu lieu et lui donnant quelques informations compl\u00e9mentaires." + }, + "pk": 8 }, { "model": "custommail.custommail", - "pk": 9, "fields": { "shortname": "bda-buy-shotgun", "subject": "BdA-Revente : {{ show.title }}", - "description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun.", - "body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})" - } + "body": "Bonjour {{ vendeur.first_name }} !\r\n\r\nJe souhaiterais racheter ta place pour {{ show.title }} le {{ show.date }} ({{ show.location }}) \u00e0 {{ show.price|floatformat:2 }}\u20ac.\r\nContacte-moi si tu es toujours int\u00e9ress\u00e9\u00b7e !\r\n\r\n{{ acheteur.get_full_name }} ({{ acheteur.email }})", + "description": "Mail envoy\u00e9 au revendeur lors d'un achat au shotgun." + }, + "pk": 9 }, { "model": "custommail.custommail", - "pk": 10, "fields": { "shortname": "petit-cours-mail-eleve", "subject": "Petits cours ENS par le COF", - "description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours", - "body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours" - } + "body": "Salut,\r\n\r\nLe COF a re\u00e7u une demande de petit cours qui te correspond. Tu es en haut de la liste d'attente donc on a transmis tes coordonn\u00e9es, ainsi que celles de 2 autres qui correspondaient aussi (c'est la vie, on donne les num\u00e9ros 3 par 3 pour que ce soit plus souple). Voici quelques infos sur l'annonce en question :\r\n\r\n\u00a4 Nom : {{ demande.name }}\r\n\r\n\u00a4 P\u00e9riode : {{ demande.quand }}\r\n\r\n\u00a4 Fr\u00e9quence : {{ demande.freq }}\r\n\r\n\u00a4 Lieu (si pr\u00e9f\u00e9r\u00e9) : {{ demande.lieu }}\r\n\r\n\u00a4 Niveau : {{ demande.get_niveau_display }}\r\n\r\n\u00a4 Remarques diverses (d\u00e9sol\u00e9 pour les balises HTML) : {{ demande.remarques }}\r\n\r\n{% if matieres|length > 1 %}\u00a4 Mati\u00e8res :\r\n{% for matiere in matieres %} \u00a4 {{ matiere }}\r\n{% endfor %}{% else %}\u00a4 Mati\u00e8re : {% for matiere in matieres %}{{ matiere }}\r\n{% endfor %}{% endif %}\r\nVoil\u00e0, cette personne te contactera peut-\u00eatre sous peu, tu pourras voir les d\u00e9tails directement avec elle (prix, modalit\u00e9s, ...). Pour indication, 30 Euro/h semble \u00eatre la moyenne.\r\n\r\nSi tu te rends compte qu'en fait tu ne peux pas/plus donner de cours en ce moment, \u00e7a serait cool que tu d\u00e9coches la case \"Recevoir des propositions de petits cours\" sur GestioCOF. Ensuite d\u00e8s que tu voudras r\u00e9appara\u00eetre tu pourras recocher la case et tu seras \u00e0 nouveau sur la liste.\r\n\r\n\u00c0 bient\u00f4t,\r\n\r\n--\r\nLe COF, pour les petits cours", + "description": "Mail envoy\u00e9 aux personnes dont ont a donn\u00e9 les contacts \u00e0 des demandeurs de petits cours" + }, + "pk": 10 }, { "model": "custommail.custommail", - "pk": 11, "fields": { "shortname": "petits-cours-mail-demandeur", "subject": "Cours particuliers ENS", - "description": "Mail envoy\u00e9 aux personnent qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})", - "body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS" - } + "body": "Bonjour,\r\n\r\nJe vous contacte au sujet de votre annonce pass\u00e9e sur le site du COF pour rentrer en contact avec un \u00e9l\u00e8ve normalien pour des cours particuliers. Voici les coordonn\u00e9es d'\u00e9l\u00e8ves qui sont motiv\u00e9s par de tels cours et correspondent aux crit\u00e8res que vous nous aviez transmis :\r\n\r\n{% for matiere, proposed in proposals %}\u00a4 {{ matiere }} :{% for user in proposed %}\r\n \u00a4 {{ user.get_full_name }}{% if user.profile.phone %}, {{ user.profile.phone }}{% endif %}{% if user.email %}, {{ user.email }}{% endif %}{% endfor %}\r\n\r\n{% endfor %}{% if unsatisfied %}Nous n'avons cependant pas pu trouver d'\u00e9l\u00e8ve disponible pour des cours de {% for matiere in unsatisfied %}{% if forloop.counter0 > 0 %}, {% endif %}{{ matiere }}{% endfor %}.\r\n\r\n{% endif %}Si pour une raison ou une autre ces num\u00e9ros ne suffisaient pas, n'h\u00e9sitez pas \u00e0 r\u00e9pondre \u00e0 cet e-mail et je vous en ferai parvenir d'autres sans probl\u00e8me.\r\n{% if extra|length > 0 %}\r\n{{ extra|safe }}\r\n{% endif %}\r\nCordialement,\r\n\r\n--\r\nLe COF, BdE de l'ENS", + "description": "Mail envoy\u00e9 aux personnes qui demandent des petits cours lorsque leur demande est trait\u00e9e.\r\n\r\n(Ne pas toucher \u00e0 {{ extra|safe }})" + }, + "pk": 11 }, { "model": "custommail.custommail", - "pk": 12, "fields": { "shortname": "bda-attributions", "subject": "R\u00e9sultats du tirage au sort", - "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places", - "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts" - } + "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Tu as \u00e9t\u00e9 s\u00e9lectionn\u00e9-e\r\npour les spectacles suivants :\r\n{% for place in places %}\r\n- 1 place pour {{ place }}{% endfor %}\r\n\r\n*Paiement*\r\nL'int\u00e9gralit\u00e9 de ces places de spectacles est \u00e0 r\u00e9gler d\u00e8s maintenant et AVANT\r\nvendredi prochain, au bureau du COF pendant les heures de permanences (du lundi au vendredi\r\nentre 12h et 14h, et entre 18h et 20h). Des facilit\u00e9s de paiement sont bien\r\n\u00e9videmment possibles : nous pouvons ne pas encaisser le ch\u00e8que imm\u00e9diatement,\r\nou bien d\u00e9couper votre paiement en deux fois. Pour ceux qui ne pourraient pas\r\nvenir payer au bureau, merci de nous contacter par mail.\r\n\r\n*Mode de retrait des places*\r\nAu moment du paiement, certaines places vous seront remises directement,\r\nd'autres seront \u00e0 r\u00e9cup\u00e9rer au cours de l'ann\u00e9e, d'autres encore seront\r\nnominatives et \u00e0 retirer le soir m\u00eame dans les the\u00e2tres correspondants.\r\nPour chaque spectacle, vous recevrez un mail quelques jours avant la\r\nrepr\u00e9sentation vous indiquant le mode de retrait.\r\n\r\nNous vous rappelons que l'obtention de places du BdA vous engage \u00e0\r\nrespecter les r\u00e8gles de fonctionnement :\r\nhttp://www.cof.ens.fr/bda/?page_id=1370\r\nUn syst\u00e8me de revente des places via les mails BdA-revente disponible\r\ndirectement sur votre compte GestioCOF.\r\n\r\nEn vous souhaitant de tr\u00e8s beaux spectacles tout au long de l'ann\u00e9e,\r\n--\r\nLe Bureau des Arts", + "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux gagnants d'une ou plusieurs places" + }, + "pk": 12 }, { "model": "custommail.custommail", - "pk": 13, "fields": { "shortname": "bda-attributions-decus", "subject": "R\u00e9sultats du tirage au sort", - "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place", - "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts" - } + "body": "Cher-e {{ member.first_name }},\r\n\r\nTu t'es inscrit-e pour le tirage au sort du BdA. Malheureusement, tu n'as\r\nobtenu aucune place.\r\n\r\nNous proposons cependant de nombreuses offres hors-tirage tout au long de\r\nl'ann\u00e9e, et nous t'invitons \u00e0 nous contacter si l'une d'entre elles\r\nt'int\u00e9resse !\r\n--\r\nLe Bureau des Arts", + "description": "Mail annon\u00e7ant les r\u00e9sultats du tirage au sort du BdA aux personnes n'ayant pas obtenu de place" + }, + "pk": 13 }, { - "model": "custommail.custommailvariable", - "pk": 1, + "model": "custommail.variable", "fields": { - "name": "member", - "description": "Utilisateur de GestioCOF", "custommail": 1, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 2, - "fields": { + "type": 1, "name": "member", - "description": "Utilisateur ayant eu une place pour ce spectacle", - "custommail": 2, - "type": 1 - } + "description": "Utilisateur de GestioCOF" + }, + "pk": 1 }, { - "model": "custommail.custommailvariable", - "pk": 3, + "model": "custommail.variable", "fields": { + "custommail": 2, + "type": 1, + "name": "member", + "description": "Utilisateur ayant eu une place pour ce spectacle" + }, + "pk": 2 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 2, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 2, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 3 }, { - "model": "custommail.custommailvariable", - "pk": 4, + "model": "custommail.variable", "fields": { + "custommail": 2, + "type": 2, "name": "nb_attr", - "description": "Nombre de places obtenues", - "custommail": 2, - "type": 2 - } + "description": "Nombre de places obtenues" + }, + "pk": 4 }, { - "model": "custommail.custommailvariable", - "pk": 5, + "model": "custommail.variable", "fields": { + "custommail": 3, + "type": 4, "name": "revente", - "description": "Revente mentionn\u00e9e dans le mail", - "custommail": 3, - "type": 4 - } + "description": "Revente mentionn\u00e9e dans le mail" + }, + "pk": 5 }, { - "model": "custommail.custommailvariable", - "pk": 6, + "model": "custommail.variable", "fields": { + "custommail": 3, + "type": 1, "name": "member", - "description": "Personne int\u00e9ress\u00e9e par la place", - "custommail": 3, - "type": 1 - } + "description": "Personne int\u00e9ress\u00e9e par la place" + }, + "pk": 6 }, { - "model": "custommail.custommailvariable", - "pk": 7, + "model": "custommail.variable", "fields": { + "custommail": 3, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 3, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 7 }, { - "model": "custommail.custommailvariable", - "pk": 8, + "model": "custommail.variable", "fields": { - "name": "site", - "description": "Site web (gestioCOF)", "custommail": 3, - "type": 5 - } + "type": 5, + "name": "site", + "description": "Site web (gestioCOF)" + }, + "pk": 8 }, { - "model": "custommail.custommailvariable", - "pk": 9, + "model": "custommail.variable", "fields": { - "name": "site", - "description": "Site web (gestioCOF)", "custommail": 4, - "type": 5 - } + "type": 5, + "name": "site", + "description": "Site web (gestioCOF)" + }, + "pk": 9 }, { - "model": "custommail.custommailvariable", - "pk": 10, + "model": "custommail.variable", "fields": { + "custommail": 4, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 4, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 10 }, { - "model": "custommail.custommailvariable", - "pk": 11, + "model": "custommail.variable", "fields": { + "custommail": 4, + "type": 1, "name": "member", - "description": "Personne int\u00e9ress\u00e9e par la place", - "custommail": 4, - "type": 1 - } + "description": "Personne int\u00e9ress\u00e9e par la place" + }, + "pk": 11 }, { - "model": "custommail.custommailvariable", - "pk": 12, + "model": "custommail.variable", "fields": { - "name": "acheteur", - "description": "Gagnant-e du tirage", "custommail": 5, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 13, - "fields": { - "name": "vendeur", - "description": "Personne qui vend une place", - "custommail": 5, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 14, - "fields": { - "name": "show", - "description": "Spectacle", - "custommail": 5, - "type": 3 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 15, - "fields": { - "name": "show", - "description": "Spectacle", - "custommail": 6, - "type": 3 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 16, - "fields": { - "name": "vendeur", - "description": "Personne qui vend une place", - "custommail": 6, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 17, - "fields": { + "type": 1, "name": "acheteur", - "description": "Personne inscrite au tirage qui n'a pas eu la place", - "custommail": 6, - "type": 1 - } + "description": "Gagnant-e du tirage" + }, + "pk": 12 }, { - "model": "custommail.custommailvariable", - "pk": 18, - "fields": { - "name": "acheteur", - "description": "Gagnant-e du tirage", - "custommail": 7, - "type": 1 - } -}, -{ - "model": "custommail.custommailvariable", - "pk": 19, + "model": "custommail.variable", "fields": { + "custommail": 5, + "type": 1, "name": "vendeur", - "description": "Personne qui vend une place", - "custommail": 7, - "type": 1 - } + "description": "Personne qui vend une place" + }, + "pk": 13 }, { - "model": "custommail.custommailvariable", - "pk": 20, + "model": "custommail.variable", "fields": { + "custommail": 5, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 7, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 14 }, { - "model": "custommail.custommailvariable", - "pk": 21, + "model": "custommail.variable", "fields": { + "custommail": 6, + "type": 3, "name": "show", - "description": "Spectacle", + "description": "Spectacle" + }, + "pk": 15 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 6, + "type": 1, + "name": "vendeur", + "description": "Personne qui vend une place" + }, + "pk": 16 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 6, + "type": 1, + "name": "acheteur", + "description": "Personne inscrite au tirage qui n'a pas eu la place" + }, + "pk": 17 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 7, + "type": 1, + "name": "acheteur", + "description": "Gagnant-e du tirage" + }, + "pk": 18 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 7, + "type": 1, + "name": "vendeur", + "description": "Personne qui vend une place" + }, + "pk": 19 +}, +{ + "model": "custommail.variable", + "fields": { + "custommail": 7, + "type": 3, + "name": "show", + "description": "Spectacle" + }, + "pk": 20 +}, +{ + "model": "custommail.variable", + "fields": { "custommail": 8, - "type": 3 - } + "type": 3, + "name": "show", + "description": "Spectacle" + }, + "pk": 21 }, { - "model": "custommail.custommailvariable", - "pk": 22, + "model": "custommail.variable", "fields": { - "name": "vendeur", - "description": "Personne qui vend la place", "custommail": 8, - "type": 1 - } + "type": 1, + "name": "vendeur", + "description": "Personne qui vend la place" + }, + "pk": 22 }, { - "model": "custommail.custommailvariable", - "pk": 23, + "model": "custommail.variable", "fields": { + "custommail": 8, + "type": 4, "name": "revente", - "description": "Revente mentionn\u00e9e dans le mail", - "custommail": 8, - "type": 4 - } + "description": "Revente mentionn\u00e9e dans le mail" + }, + "pk": 23 }, { - "model": "custommail.custommailvariable", - "pk": 24, + "model": "custommail.variable", "fields": { + "custommail": 9, + "type": 1, "name": "vendeur", - "description": "Personne qui vend la place", - "custommail": 9, - "type": 1 - } + "description": "Personne qui vend la place" + }, + "pk": 24 }, { - "model": "custommail.custommailvariable", - "pk": 25, + "model": "custommail.variable", "fields": { + "custommail": 9, + "type": 3, "name": "show", - "description": "Spectacle", - "custommail": 9, - "type": 3 - } + "description": "Spectacle" + }, + "pk": 25 }, { - "model": "custommail.custommailvariable", - "pk": 26, + "model": "custommail.variable", "fields": { + "custommail": 9, + "type": 1, "name": "acheteur", - "description": "Personne qui prend la place au shotgun", - "custommail": 9, - "type": 1 - } + "description": "Personne qui prend la place au shotgun" + }, + "pk": 26 }, { - "model": "custommail.custommailvariable", - "pk": 27, + "model": "custommail.variable", "fields": { + "custommail": 10, + "type": 6, "name": "demande", - "description": "Demande de petit cours", - "custommail": 10, - "type": 6 - } + "description": "Demande de petit cours" + }, + "pk": 27 }, { - "model": "custommail.custommailvariable", - "pk": 28, + "model": "custommail.variable", "fields": { + "custommail": 10, + "type": 7, "name": "matieres", - "description": "Liste des mati\u00e8res concern\u00e9es par la demande", - "custommail": 10, - "type": 7 - } + "description": "Liste des mati\u00e8res concern\u00e9es par la demande" + }, + "pk": 28 }, { - "model": "custommail.custommailvariable", - "pk": 29, + "model": "custommail.variable", "fields": { + "custommail": 11, + "type": 10, "name": "proposals", - "description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re", - "custommail": 11, - "type": 10 - } + "description": "Liste associant une liste d'enseignants \u00e0 chaque mati\u00e8re" + }, + "pk": 29 }, { - "model": "custommail.custommailvariable", - "pk": 30, + "model": "custommail.variable", "fields": { + "custommail": 11, + "type": 7, "name": "unsatisfied", - "description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer", - "custommail": 11, - "type": 7 - } + "description": "Liste des mati\u00e8res pour lesquelles on n'a pas d'enseigant \u00e0 proposer" + }, + "pk": 30 }, { - "model": "custommail.custommailvariable", - "pk": 31, + "model": "custommail.variable", "fields": { + "custommail": 12, + "type": 11, "name": "places", - "description": "Places de spectacle du participant", - "custommail": 12, - "type": 11 - } + "description": "Places de spectacle du participant" + }, + "pk": 31 }, { - "model": "custommail.custommailvariable", - "pk": 32, + "model": "custommail.variable", "fields": { - "name": "member", - "description": "Participant du tirage au sort", "custommail": 12, - "type": 1 - } + "type": 1, + "name": "member", + "description": "Participant du tirage au sort" + }, + "pk": 32 }, { - "model": "custommail.custommailvariable", - "pk": 33, + "model": "custommail.variable", "fields": { - "name": "member", - "description": "Participant du tirage au sort", "custommail": 13, - "type": 1 - } + "type": 1, + "name": "member", + "description": "Participant du tirage au sort" + }, + "pk": 33 } ] From 785555c05cc874dfc2a9542608c0e94baffccc2e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Thu, 26 Oct 2017 12:40:11 +0200 Subject: [PATCH 023/211] Misc fixes --- bda/forms.py | 10 ++++++++-- bda/migrations/0012_notif_time.py | 7 ++++--- bda/models.py | 11 ++++++----- bda/views.py | 33 +++++++++++-------------------- 4 files changed, 30 insertions(+), 31 deletions(-) diff --git a/bda/forms.py b/bda/forms.py index 11d05b0e..2929f771 100644 --- a/bda/forms.py +++ b/bda/forms.py @@ -43,11 +43,13 @@ class TokenForm(forms.Form): class AttributionModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s" % str(obj.spectacle) + return str(obj.spectacle) + class ReventeModelMultipleChoiceField(forms.ModelMultipleChoiceField): def label_from_instance(self, obj): - return "%s" % str(obj.attribution.spectacle) + return str(obj.attribution.spectacle) + class ResellForm(forms.Form): attributions = AttributionModelMultipleChoiceField( @@ -66,6 +68,7 @@ class ResellForm(forms.Form): 'participant__user') ) + class AnnulForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', @@ -85,6 +88,7 @@ class AnnulForm(forms.Form): 'participant__user') ) + class InscriptionReventeForm(forms.Form): spectacles = forms.ModelMultipleChoiceField( queryset=Spectacle.objects.none(), @@ -99,6 +103,7 @@ class InscriptionReventeForm(forms.Form): .filter(date__gte=timezone.now()) ) + class ReventeTirageAnnulForm(forms.Form): reventes = ReventeModelMultipleChoiceField( label='', @@ -134,6 +139,7 @@ class ReventeTirageForm(forms.Form): .select_related('attribution__spectacle') ) + class SoldForm(forms.Form): attributions = AttributionModelMultipleChoiceField( label='', diff --git a/bda/migrations/0012_notif_time.py b/bda/migrations/0012_notif_time.py index be66efd1..ee777e35 100644 --- a/bda/migrations/0012_notif_time.py +++ b/bda/migrations/0012_notif_time.py @@ -11,11 +11,12 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( + migrations.RenameField( model_name='spectaclerevente', - name='answered_mail', + old_name='answered_mail', + new_name='confirmed_entry', ), - migrations.AddField( + migrations.AlterField( model_name='spectaclerevente', name='confirmed_entry', field=models.ManyToManyField(blank=True, related_name='entered', to='bda.Participant'), diff --git a/bda/models.py b/bda/models.py index 2ad47dbf..59827621 100644 --- a/bda/models.py +++ b/bda/models.py @@ -168,6 +168,7 @@ class Participant(models.Model): def __str__(self): return "%s - %s" % (self.user, self.tirage.title) + DOUBLE_CHOICES = ( ("1", "1 place"), ("autoquit", "2 places si possible, 1 sinon"), @@ -292,15 +293,16 @@ class SpectacleRevente(models.Model): class Meta: verbose_name = "Revente" - def reset(self): + def reset(self, new_date=timezone.now()): """Réinitialise la revente pour permettre une remise sur le marché""" self.seller = self.attribution.participant - self.date = timezone.now() + self.date = new_date self.confirmed_entry.clear() self.soldTo = None self.notif_sent = False self.tirage_done = False self.shotgun = False + self.save() def send_notif(self): """ @@ -396,11 +398,10 @@ class SpectacleRevente(models.Model): )) send_mass_custom_mail(datatuple) - return winner - # Si personne ne veut de la place, elle part au shotgun else: + winner = None self.shotgun = True - return None self.tirage_done = True self.save() + return winner diff --git a/bda/views.py b/bda/views.py index 6ed22b21..fb1a2e82 100644 --- a/bda/views.py +++ b/bda/views.py @@ -5,7 +5,6 @@ import random import hashlib import time import json -from datetime import timedelta from custommail.shortcuts import send_mass_custom_mail, send_custom_mail from custommail.models import CustomMail from django.shortcuts import render, get_object_or_404 @@ -14,6 +13,7 @@ from django.contrib import messages from django.db import transaction from django.core import serializers from django.db.models import Count, Q, Prefetch +from django.template.defaultfilters import pluralize from django.forms.models import inlineformset_factory from django.http import ( HttpResponseBadRequest, HttpResponseRedirect, JsonResponse @@ -376,6 +376,7 @@ def revente_manage(request, tirage_id): defaults={'seller': participant}) if not created: revente.reset() + context = { 'vendeur': participant.user, 'show': attribution.spectacle, @@ -414,15 +415,10 @@ def revente_manage(request, tirage_id): attributions = soldform.cleaned_data['attributions'] for attribution in attributions: if attribution.spectacle.date > timezone.now(): - revente = attribution.revente - revente.date = timezone.now() - timedelta(minutes=65) - revente.soldTo = None - revente.notif_sent = False - revente.tirage_done = False - revente.shotgun = False - if revente.confirmed_entry: - revente.confirmed_entry.clear() - revente.save() + # On antidate pour envoyer le mail plus vite + new_date = (timezone.now() + - SpectacleRevente.remorse_time) + revente.reset(new_date=new_date) overdue = participant.attribution_set.filter( spectacle__date__gte=timezone.now(), @@ -442,7 +438,6 @@ def revente_tirages(request, tirage_id): tirage = get_object_or_404(Tirage, id=tirage_id) participant, _ = Participant.objects.get_or_create( user=request.user, tirage=tirage) - unsub = 0 subform = ReventeTirageForm(participant, prefix="subscribe") annulform = ReventeTirageAnnulForm(participant, prefix="annul") @@ -451,33 +446,29 @@ def revente_tirages(request, tirage_id): subform = ReventeTirageForm(participant, request.POST, prefix="subscribe") if subform.is_valid(): - sub = 0 reventes = subform.cleaned_data['reventes'] + count = reventes.count() for revente in reventes: revente.confirmed_entry.add(participant) - sub += 1 - if sub > 0: - plural = "s" if sub > 1 else "" + if count > 0: messages.success( request, "Tu as bien été inscrit à {} revente{}" - .format(sub, plural) + .format(count, pluralize(count)) ) elif "annul" in request.POST: annulform = ReventeTirageAnnulForm(participant, request.POST, prefix="annul") if annulform.is_valid(): - unsub = 0 reventes = annulform.cleaned_data['reventes'] + count = reventes.count() for revente in reventes: revente.confirmed_entry.remove(participant) - unsub += 1 - if unsub > 0: - plural = "s" if unsub > 1 else "" + if count > 0: messages.success( request, "Tu as bien été désinscrit de {} revente{}" - .format(unsub, plural) + .format(count, pluralize(count)) ) return render(request, "bda/revente/tirages.html", From 1c90d067fa38542ca0877e8ba22dcd2a6108ad8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Thu, 26 Oct 2017 18:13:09 +0200 Subject: [PATCH 024/211] Make cof.settings a module --- cof/settings/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cof/settings/__init__.py diff --git a/cof/settings/__init__.py b/cof/settings/__init__.py new file mode 100644 index 00000000..e69de29b From 895f7e062cc9da5d590f18c6ed36fa1f1e6738cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Delobelle?= Date: Fri, 27 Oct 2017 03:00:33 +0200 Subject: [PATCH 025/211] Delete GlobalPermissions model (migrations) It is an old model which doesn't exist anymore in kfet.models module. This adds its missing DeleteModel in migrations. --- kfet/migrations/0062_delete_globalpermissions.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 kfet/migrations/0062_delete_globalpermissions.py diff --git a/kfet/migrations/0062_delete_globalpermissions.py b/kfet/migrations/0062_delete_globalpermissions.py new file mode 100644 index 00000000..ee245412 --- /dev/null +++ b/kfet/migrations/0062_delete_globalpermissions.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('kfet', '0061_add_perms_config'), + ] + + operations = [ + migrations.DeleteModel( + name='GlobalPermissions', + ), + ] From 93fa79128cd4688e8fbe1adb07db49409cc2d9d8 Mon Sep 17 00:00:00 2001 From: Qwann Date: Tue, 31 Oct 2017 15:10:21 +0100 Subject: [PATCH 026/211] order table striped --- kfet/templates/kfet/order_create.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kfet/templates/kfet/order_create.html b/kfet/templates/kfet/order_create.html index 6ff9b824..d95cafe3 100644 --- a/kfet/templates/kfet/order_create.html +++ b/kfet/templates/kfet/order_create.html @@ -11,7 +11,7 @@
{% csrf_token %}
- +
From 273e6374ef072c6b589ddb46268a8085a2085359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 1 Nov 2017 11:09:16 +0100 Subject: [PATCH 027/211] Pluralization in bda -> participant list --- bda/templates/bda/participants.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bda/templates/bda/participants.html b/bda/templates/bda/participants.html index 85af4a2e..c3ff31d6 100644 --- a/bda/templates/bda/participants.html +++ b/bda/templates/bda/participants.html @@ -47,11 +47,11 @@
- + From f18959c0a1d643fab3b08921a004f158d4ba4720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Wed, 1 Nov 2017 17:26:40 +0100 Subject: [PATCH 028/211] BdA-Revente: meaningful names, some help tests --- bda/templates/bda/revente/manage.html | 58 +++++++++++++++++++----- bda/templates/bda/revente/subscribe.html | 39 ++++++++++------ bda/templates/bda/revente/tirages.html | 40 ++++++++++++---- bda/views.py | 18 ++++++++ gestioncof/static/css/cof.css | 11 +++++ gestioncof/templates/home.html | 6 +-- 6 files changed, 136 insertions(+), 36 deletions(-) diff --git a/bda/templates/bda/revente/manage.html b/bda/templates/bda/revente/manage.html index 0912babb..8162d55d 100644 --- a/bda/templates/bda/revente/manage.html +++ b/bda/templates/bda/revente/manage.html @@ -3,50 +3,84 @@ {% block realcontent %} -

Revente de place

+

Gestion des places que je revends

{% with resell_attributions=resellform.attributions annul_attributions=annulform.attributions sold_attributions=soldform.attributions %} {% if resellform.attributions %} +
+

Places non revendues

- {% csrf_token %} - {{resellform|bootstrap}} +
+ + Cochez les places que vous souhaitez revendre, et validez. Vous aurez + ensuite 1h pour changer d'avis avant que la revente soit confirmée et + que les notifications soient envoyées aux intéressé·e·s. +
+
+ {% csrf_token %} + {{ resellform|bootstrap }} +
+ +
{% endif %} -
+ {% if annul_attributions or overdue %}

Places en cours de revente

+ {% if annul_attributions %} +
+ + Vous pouvez annuler les places mises en vente il y a moins d'une heure. +
+ {% endif %} {% csrf_token %}
    {% for attrib in annul_attributions %} -
  • {{attrib.tag}} {{attrib.choice_label}}
  • +
  • {{ attrib.tag }} {{ attrib.choice_label }}
  • {% endfor %} {% for attrib in overdue %}
  • - {{attrib.spectacle}} + {{ attrib.spectacle }}
  • {% endfor %} +
+
+
{% if annul_attributions %} {% endif %} + +
{% endif %} -
+ {% if sold_attributions %}

Places revendues

-
+ +
+ + Pour chaque revente, vous devez soit l'annuler soit la confirmer pour + transférer la place la place à la personne tirée au sort. + + L'annulation sert par exemple à pouvoir remettre la place en jeu si + vous ne parvenez pas à entrer en contact avec la personne tirée au + sort. +
+
{% csrf_token %} - {{soldform|bootstrap}} - - - + {{ soldform|bootstrap }} +
+ + + {% endif %} {% if not resell_attributions and not annul_attributions and not overdue and not sold_attributions %}

Plus de reventes possibles !

diff --git a/bda/templates/bda/revente/subscribe.html b/bda/templates/bda/revente/subscribe.html index fcf57345..9a193908 100644 --- a/bda/templates/bda/revente/subscribe.html +++ b/bda/templates/bda/revente/subscribe.html @@ -4,28 +4,41 @@ {% block realcontent %}

Inscriptions pour BdA-Revente

+
+ + Cochez les spectacles pour lesquels vous souhaitez recevoir un + notification quand une place est disponible en revente.
+ Lorsque vous validez vos choix, si un tirage au sort est en cours pour + un des spectacles que vous avez sélectionné, vous serez automatiquement + inscrit à ce tirage. +
+
{% csrf_token %}
-

Spectacles

-
- - + + -
-
    - {% for checkbox in form.spectacles %} -
  • {{checkbox}}
  • - {%endfor%} -
-
+
+
    + {% for checkbox in form.spectacles %} +
  • {{ checkbox }}
  • + {% endfor %} +
+
- + - {% include 'autocomplete_light/static.html' %} -{% endblock %} diff --git a/gestioncof/templates/admin/index.html b/gestioncof/templates/admin/index.html deleted file mode 100644 index 965c71fa..00000000 --- a/gestioncof/templates/admin/index.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends "admin/base_site.html" %} - - -{% load i18n grp_tags log %} - - -{% block javascripts %} - {{ block.super }} -{% endblock %} - - -{% block breadcrumbs %} -
    -
  • {% trans "Home" %}
  • -
-{% endblock %} -{% block content_title %} - {% if title %} -

{{ title }}

- {% endif %} -{% endblock %} - - -{% block content %} -
-
- - {% for app in app_list %} -
-

{% trans app.name %}

- {% for model in app.models %} -
- {% if model.perms.change %}{{ model.name }}{% else %}{{ model.name }}{% endif %} - {% if model.perms.add or model.perms.change %} - - {% endif %} -
- {% endfor %} -
- {% empty %} -

{% trans "You don´t have permission to edit anything." %}

- {% endfor %} -
-
-
-

{% trans 'Recent Actions' %}

-
-

{% trans 'My Actions' %}

- {% get_admin_log 20 as admin_log for_user user %} - {% if not admin_log %} -

{% trans 'None available' %}

- {% else %} -
    - {% for entry in admin_log %} -
  • - {% if entry.is_deletion %} - {{ entry.object_repr }} - {% else %} - {{ entry.object_repr }} - {% endif %} - {% filter capfirst %}{% trans entry.content_type.name %}{% endfilter %} -
  • - {% endfor %} -
- {% endif %} -
-
-
-
-{% endblock %} - diff --git a/gestioncof/templates/gestioncof/base_header.html b/gestioncof/templates/gestioncof/base_header.html index 21441875..e5f757a7 100644 --- a/gestioncof/templates/gestioncof/base_header.html +++ b/gestioncof/templates/gestioncof/base_header.html @@ -3,7 +3,7 @@ {% block content %}
diff --git a/gestioncof/templates/gestioncof/event.html b/gestioncof/templates/gestioncof/event.html index 52f893db..f388bc25 100644 --- a/gestioncof/templates/gestioncof/event.html +++ b/gestioncof/templates/gestioncof/event.html @@ -5,7 +5,7 @@ {% if event.details %}

{{ event.details }}

{% endif %} -
+ {% csrf_token %} {{ form.as_p }} diff --git a/gestioncof/templates/home.html b/gestioncof/templates/home.html index acc04f30..65c4ba5e 100644 --- a/gestioncof/templates/home.html +++ b/gestioncof/templates/home.html @@ -14,7 +14,7 @@
@@ -24,7 +24,7 @@
@@ -69,11 +69,11 @@

Divers

{% endif %} @@ -86,16 +86,16 @@

Général

  • Administration générale
  • Demandes de petits cours
  • -
  • Inscription d'un nouveau membre
  • +
  • Inscription d'un nouveau membre
  • Gestion des clubs
  • @@ -120,8 +120,8 @@

    Liens utiles

    diff --git a/gestioncof/templates/login.html b/gestioncof/templates/login.html index 1cd1d25d..bfc2dbb8 100644 --- a/gestioncof/templates/login.html +++ b/gestioncof/templates/login.html @@ -15,7 +15,7 @@

    Identifiants incorrects.

    {% endif %} + action="{% url 'ext_login_view' %}?next={{ next|urlencode }}"> {% csrf_token %}
    diff --git a/gestioncof/templates/login_switch.html b/gestioncof/templates/login_switch.html index aa8a68c6..d361493b 100644 --- a/gestioncof/templates/login_switch.html +++ b/gestioncof/templates/login_switch.html @@ -12,13 +12,13 @@
    + href="{% url 'cas_login_view' %}?next={{ next|urlencode }}">
    Compte clipper
    + href="{% url 'ext_login_view' %}?next={{ next|urlencode }}">
    Extérieur
    diff --git a/gestioncof/templates/registration/password_change_done.html b/gestioncof/templates/registration/password_change_done.html index f83a781b..9f2c4a60 100644 --- a/gestioncof/templates/registration/password_change_done.html +++ b/gestioncof/templates/registration/password_change_done.html @@ -5,5 +5,5 @@ {% block realcontent %}

    Mot de passe modifié avec succès !

    -

    Retour au menu principal

    +

    Retour au menu principal

    {% endblock %} diff --git a/gestioncof/templates/registration/password_change_form.html b/gestioncof/templates/registration/password_change_form.html index f579fb31..d9a3f66a 100644 --- a/gestioncof/templates/registration/password_change_form.html +++ b/gestioncof/templates/registration/password_change_form.html @@ -5,7 +5,7 @@ {% block realcontent %}

    Changement de mot de passe

    - + {% csrf_token %} {{ form | bootstrap }} diff --git a/gestioncof/urls.py b/gestioncof/urls.py index 57c2e8f2..2be609b3 100644 --- a/gestioncof/urls.py +++ b/gestioncof/urls.py @@ -36,19 +36,23 @@ petitcours_patterns = [ ] surveys_patterns = [ - url(r'^(?P\d+)/status$', views.survey_status), - url(r'^(?P\d+)$', views.survey), + url(r'^(?P\d+)/status$', views.survey_status, + name='survey.details.status'), + url(r'^(?P\d+)$', views.survey, + name='survey.details'), ] events_patterns = [ - url(r'^(?P\d+)$', views.event), - url(r'^(?P\d+)/status$', views.event_status), + url(r'^(?P\d+)$', views.event, + name='event.details'), + url(r'^(?P\d+)/status$', views.event_status, + name='event.details.status'), ] calendar_patterns = [ - url(r'^subscription$', 'gestioncof.views.calendar'), - url(r'^(?P[a-z0-9-]+)/calendar.ics$', - 'gestioncof.views.calendar_ics') + url(r'^subscription$', views.calendar, + name='calendar'), + url(r'^(?P[a-z0-9-]+)/calendar.ics$', views.calendar_ics) ] clubs_patterns = [ diff --git a/gestioncof/views.py b/gestioncof/views.py index ec9f6efd..5dfee83f 100644 --- a/gestioncof/views.py +++ b/gestioncof/views.py @@ -20,6 +20,8 @@ from django.contrib import messages from django_cas_ng.views import logout as cas_logout_view +from utils.views.autocomplete import Select2QuerySetView + from gestioncof.models import Survey, SurveyAnswer, SurveyQuestion, \ SurveyQuestionAnswer from gestioncof.models import Event, EventRegistration, EventOption, \ @@ -54,8 +56,8 @@ def home(request): def login(request): - if request.user.is_authenticated(): - return redirect("gestioncof.views.home") + if request.user.is_authenticated: + return redirect("home") context = {} if request.method == "GET" and 'next' in request.GET: context['next'] = request.GET['next'] @@ -786,3 +788,18 @@ class ConfigUpdate(FormView): def form_valid(self, form): form.save() return super().form_valid(form) + + +## +# Autocomplete views +# +# https://django-autocomplete-light.readthedocs.io/en/master/tutorial.html#create-an-autocomplete-view +## + + +class UserAutocomplete(Select2QuerySetView): + model = User + search_fields = ('username', 'first_name', 'last_name') + + +user_autocomplete = buro_required(UserAutocomplete.as_view()) diff --git a/kfet/auth/middleware.py b/kfet/auth/middleware.py index 748ce4dd..48d9c4ee 100644 --- a/kfet/auth/middleware.py +++ b/kfet/auth/middleware.py @@ -13,8 +13,11 @@ class TemporaryAuthMiddleware: values from CofProfile and Account of this user. """ - def process_request(self, request): - if request.user.is_authenticated(): + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + if request.user.is_authenticated: # avoid multiple db accesses in views and templates request.user = ( User.objects @@ -31,6 +34,8 @@ class TemporaryAuthMiddleware: request.real_user = request.user request.user = temp_request_user + return self.get_response(request) + def get_kfet_password(self, request): return ( request.META.get('HTTP_KFETPASSWORD') or diff --git a/kfet/auth/tests.py b/kfet/auth/tests.py index c2f183cd..0c8b25d3 100644 --- a/kfet/auth/tests.py +++ b/kfet/auth/tests.py @@ -286,6 +286,8 @@ class TemporaryAuthTests(TestCase): self.factory = RequestFactory() + self.middleware = TemporaryAuthMiddleware(mock.Mock()) + user1_acc = Account(trigramme='000') user1_acc.change_pwd('kfet_user1') user1_acc.save({'username': 'user1'}) @@ -312,7 +314,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.get('/', HTTP_KFETPASSWORD='kfet_user2') request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user2) self.assertEqual(request.real_user, self.user1) @@ -325,7 +327,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.post('/', {'KFETPASSWORD': 'kfet_user2'}) request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user2) self.assertEqual(request.real_user, self.user1) @@ -337,7 +339,7 @@ class TemporaryAuthTests(TestCase): request = self.factory.post('/', {'KFETPASSWORD': 'invalid'}) request.user = self.user1 - TemporaryAuthMiddleware().process_request(request) + self.middleware(request) self.assertEqual(request.user, self.user1) self.assertFalse(hasattr(request, 'real_user')) diff --git a/kfet/cms/migrations/0001_initial.py b/kfet/cms/migrations/0001_initial.py index 951637c7..ed0b0948 100644 --- a/kfet/cms/migrations/0001_initial.py +++ b/kfet/cms/migrations/0001_initial.py @@ -20,7 +20,7 @@ class Migration(migrations.Migration): migrations.CreateModel( name='KFetPage', fields=[ - ('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page')), + ('page_ptr', models.OneToOneField(serialize=False, primary_key=True, parent_link=True, auto_created=True, to='wagtailcore.Page', on_delete=models.CASCADE)), ('no_header', models.BooleanField(verbose_name='Sans en-tête', help_text="Coché, l'en-tête (avec le titre) de la page n'est pas affiché.", default=False)), ('content', wagtail.wagtailcore.fields.StreamField((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses'))))), ('group', wagtail.wagtailcore.blocks.StreamBlock((('rich', wagtail.wagtailcore.blocks.RichTextBlock(label='Éditeur')), ('carte', kfet.cms.models.MenuBlock()), ('group_team', wagtail.wagtailcore.blocks.StructBlock((('show_only', wagtail.wagtailcore.blocks.IntegerBlock(help_text='Nombre initial de membres affichés. Laisser vide pour tou-te-s les afficher.', required=False, label='Montrer seulement')), ('members', wagtail.wagtailcore.blocks.ListBlock(wagtail.wagtailsnippets.blocks.SnippetChooserBlock(kfet.cms.models.MemberTeam), classname='team-group', label='K-Fêt-eux-ses')))))), label='Contenu groupé'))), verbose_name='Contenu')), ('layout', models.CharField(max_length=255, choices=[('kfet/base_col_1.html', 'Une colonne : centrée sur la page'), ('kfet/base_col_2.html', 'Deux colonnes : fixe à gauche, contenu à droite'), ('kfet/base_col_mult.html', 'Contenu scindé sur plusieurs colonnes')], help_text='Comment cette page devrait être affichée ?', verbose_name='Template', default='kfet/base_col_mult.html')), diff --git a/kfet/models.py b/kfet/models.py index b1e351d5..deee76eb 100644 --- a/kfet/models.py +++ b/kfet/models.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- from django.db import models -from django.core.urlresolvers import reverse from django.core.validators import RegexValidator from django.contrib.auth.models import User from gestioncof.models import CofProfile +from django.urls import reverse from django.utils.six.moves import reduce from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible diff --git a/kfet/templates/kfet/base_nav.html b/kfet/templates/kfet/base_nav.html index dda6c1ef..f4c07e05 100644 --- a/kfet/templates/kfet/base_nav.html +++ b/kfet/templates/kfet/base_nav.html @@ -1,10 +1,12 @@ {% load i18n static %} {% load wagtailcore_tags %} +{% slugurl "kfet" as kfet_home_url %} +
    0 ) { + cacheIndex += span; + } + cacheIndex++; + } + // add the row data to the end + cells[ c.columns ] = rowData; + // update cache + c.cache[ tbodyIndex ].normalized[ order ] = cells; + } + // resort using current settings + ts.checkResort( c, resort, callback ); + } + }, + + updateCache : function( c, callback, $tbodies ) { + // rebuild parsers + if ( !( c.parsers && c.parsers.length ) ) { + ts.setupParsers( c, $tbodies ); + } + // rebuild the cache map + ts.buildCache( c, callback, $tbodies ); + }, + + // init flag (true) used by pager plugin to prevent widget application + // renamed from appendToTable + appendCache : function( c, init ) { + var parsed, totalRows, $tbody, $curTbody, rowIndex, tbodyIndex, appendTime, + table = c.table, + wo = c.widgetOptions, + $tbodies = c.$tbodies, + rows = [], + cache = c.cache; + // empty table - fixes #206/#346 + if ( ts.isEmptyObject( cache ) ) { + // run pager appender in case the table was just emptied + return c.appender ? c.appender( table, rows ) : + table.isUpdating ? c.$table.triggerHandler( 'updateComplete', table ) : ''; // Fixes #532 + } + if ( c.debug ) { + appendTime = new Date(); + } + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = $tbodies.eq( tbodyIndex ); + if ( $tbody.length ) { + // detach tbody for manipulation + $curTbody = ts.processTbody( table, $tbody, true ); + parsed = cache[ tbodyIndex ].normalized; + totalRows = parsed.length; + for ( rowIndex = 0; rowIndex < totalRows; rowIndex++ ) { + rows[rows.length] = parsed[ rowIndex ][ c.columns ].$row; + // removeRows used by the pager plugin; don't render if using ajax - fixes #411 + if ( !c.appender || ( c.pager && ( !c.pager.removeRows || !wo.pager_removeRows ) && !c.pager.ajax ) ) { + $curTbody.append( parsed[ rowIndex ][ c.columns ].$row ); + } + } + // restore tbody + ts.processTbody( table, $curTbody, false ); + } + } + if ( c.appender ) { + c.appender( table, rows ); + } + if ( c.debug ) { + console.log( 'Rebuilt table' + ts.benchmark( appendTime ) ); + } + // apply table widgets; but not before ajax completes + if ( !init && !c.appender ) { + ts.applyWidget( table ); + } + if ( table.isUpdating ) { + c.$table.triggerHandler( 'updateComplete', table ); + } + }, + + commonUpdate : function( c, resort, callback ) { + // remove rows/elements before update + c.$table.find( c.selectorRemove ).remove(); + // rebuild parsers + ts.setupParsers( c ); + // rebuild the cache map + ts.buildCache( c ); + ts.checkResort( c, resort, callback ); + }, + + /* + ▄█████ ▄████▄ █████▄ ██████ ██ █████▄ ▄████▄ + ▀█▄ ██ ██ ██▄▄██ ██ ██ ██ ██ ██ ▄▄▄ + ▀█▄ ██ ██ ██▀██ ██ ██ ██ ██ ██ ▀██ + █████▀ ▀████▀ ██ ██ ██ ██ ██ ██ ▀████▀ + */ + initSort : function( c, cell, event ) { + if ( c.table.isUpdating ) { + // let any updates complete before initializing a sort + return setTimeout( function(){ + ts.initSort( c, cell, event ); + }, 50 ); + } + + var arry, indx, headerIndx, dir, temp, tmp, $header, + notMultiSort = !event[ c.sortMultiSortKey ], + table = c.table, + len = c.$headers.length, + // get current column index; *always* stored on th/td + $th = ts.getHeaderCell( $( cell ) ), + col = parseInt( $th.attr( 'data-column' ), 10 ), + order = c.sortVars[ col ].order; + // Only call sortStart if sorting is enabled + c.$table.triggerHandler( 'sortStart', table ); + // get current column sort order + tmp = ( c.sortVars[ col ].count + 1 ) % order.length; + c.sortVars[ col ].count = event[ c.sortResetKey ] ? 2 : tmp; + // reset all sorts on non-current column - issue #30 + if ( c.sortRestart ) { + for ( headerIndx = 0; headerIndx < len; headerIndx++ ) { + $header = c.$headers.eq( headerIndx ); + tmp = parseInt( $header.attr( 'data-column' ), 10 ); + // only reset counts on columns that weren't just clicked on and if not included in a multisort + if ( col !== tmp && ( notMultiSort || $header.hasClass( ts.css.sortNone ) ) ) { + c.sortVars[ tmp ].count = -1; + } + } + } + // user only wants to sort on one column + if ( notMultiSort ) { + // flush the sort list + c.sortList = []; + c.last.sortList = []; + if ( c.sortForce !== null ) { + arry = c.sortForce; + for ( indx = 0; indx < arry.length; indx++ ) { + if ( arry[ indx ][ 0 ] !== col ) { + c.sortList[ c.sortList.length ] = arry[ indx ]; + } + } + } + // add column to sort list + dir = order[ c.sortVars[ col ].count ]; + if ( dir < 2 ) { + c.sortList[ c.sortList.length ] = [ col, dir ]; + // add other columns if header spans across multiple + if ( cell.colSpan > 1 ) { + for ( indx = 1; indx < cell.colSpan; indx++ ) { + c.sortList[ c.sortList.length ] = [ col + indx, dir ]; + // update count on columns in colSpan + c.sortVars[ col + indx ].count = $.inArray( dir, order ); + } + } + } + // multi column sorting + } else { + // get rid of the sortAppend before adding more - fixes issue #115 & #523 + c.sortList = $.extend( [], c.last.sortList ); + + // the user has clicked on an already sorted column + if ( ts.isValueInArray( col, c.sortList ) >= 0 ) { + // reverse the sorting direction + for ( indx = 0; indx < c.sortList.length; indx++ ) { + tmp = c.sortList[ indx ]; + if ( tmp[ 0 ] === col ) { + // order.count seems to be incorrect when compared to cell.count + tmp[ 1 ] = order[ c.sortVars[ col ].count ]; + if ( tmp[1] === 2 ) { + c.sortList.splice( indx, 1 ); + c.sortVars[ col ].count = -1; + } + } + } + } else { + // add column to sort list array + dir = order[ c.sortVars[ col ].count ]; + if ( dir < 2 ) { + c.sortList[ c.sortList.length ] = [ col, dir ]; + // add other columns if header spans across multiple + if ( cell.colSpan > 1 ) { + for ( indx = 1; indx < cell.colSpan; indx++ ) { + c.sortList[ c.sortList.length ] = [ col + indx, dir ]; + // update count on columns in colSpan + c.sortVars[ col + indx ].count = $.inArray( dir, order ); + } + } + } + } + } + // save sort before applying sortAppend + c.last.sortList = $.extend( [], c.sortList ); + if ( c.sortList.length && c.sortAppend ) { + arry = $.isArray( c.sortAppend ) ? c.sortAppend : c.sortAppend[ c.sortList[ 0 ][ 0 ] ]; + if ( !ts.isEmptyObject( arry ) ) { + for ( indx = 0; indx < arry.length; indx++ ) { + if ( arry[ indx ][ 0 ] !== col && ts.isValueInArray( arry[ indx ][ 0 ], c.sortList ) < 0 ) { + dir = arry[ indx ][ 1 ]; + temp = ( '' + dir ).match( /^(a|d|s|o|n)/ ); + if ( temp ) { + tmp = c.sortList[ 0 ][ 1 ]; + switch ( temp[ 0 ] ) { + case 'd' : + dir = 1; + break; + case 's' : + dir = tmp; + break; + case 'o' : + dir = tmp === 0 ? 1 : 0; + break; + case 'n' : + dir = ( tmp + 1 ) % order.length; + break; + default: + dir = 0; + break; + } + } + c.sortList[ c.sortList.length ] = [ arry[ indx ][ 0 ], dir ]; + } + } + } + } + // sortBegin event triggered immediately before the sort + c.$table.triggerHandler( 'sortBegin', table ); + // setTimeout needed so the processing icon shows up + setTimeout( function() { + // set css for headers + ts.setHeadersCss( c ); + ts.multisort( c ); + ts.appendCache( c ); + c.$table.triggerHandler( 'sortBeforeEnd', table ); + c.$table.triggerHandler( 'sortEnd', table ); + }, 1 ); + }, + + // sort multiple columns + multisort : function( c ) { /*jshint loopfunc:true */ + var tbodyIndex, sortTime, colMax, rows, tmp, + table = c.table, + sorter = [], + dir = 0, + textSorter = c.textSorter || '', + sortList = c.sortList, + sortLen = sortList.length, + len = c.$tbodies.length; + if ( c.serverSideSorting || ts.isEmptyObject( c.cache ) ) { + // empty table - fixes #206/#346 + return; + } + if ( c.debug ) { sortTime = new Date(); } + // cache textSorter to optimize speed + if ( typeof textSorter === 'object' ) { + colMax = c.columns; + while ( colMax-- ) { + tmp = ts.getColumnData( table, textSorter, colMax ); + if ( typeof tmp === 'function' ) { + sorter[ colMax ] = tmp; + } + } + } + for ( tbodyIndex = 0; tbodyIndex < len; tbodyIndex++ ) { + colMax = c.cache[ tbodyIndex ].colMax; + rows = c.cache[ tbodyIndex ].normalized; + + rows.sort( function( a, b ) { + var sortIndex, num, col, order, sort, x, y; + // rows is undefined here in IE, so don't use it! + for ( sortIndex = 0; sortIndex < sortLen; sortIndex++ ) { + col = sortList[ sortIndex ][ 0 ]; + order = sortList[ sortIndex ][ 1 ]; + // sort direction, true = asc, false = desc + dir = order === 0; + + if ( c.sortStable && a[ col ] === b[ col ] && sortLen === 1 ) { + return a[ c.columns ].order - b[ c.columns ].order; + } + + // fallback to natural sort since it is more robust + num = /n/i.test( ts.getSortType( c.parsers, col ) ); + if ( num && c.strings[ col ] ) { + // sort strings in numerical columns + if ( typeof ( ts.string[ c.strings[ col ] ] ) === 'boolean' ) { + num = ( dir ? 1 : -1 ) * ( ts.string[ c.strings[ col ] ] ? -1 : 1 ); + } else { + num = ( c.strings[ col ] ) ? ts.string[ c.strings[ col ] ] || 0 : 0; + } + // fall back to built-in numeric sort + // var sort = $.tablesorter['sort' + s]( a[col], b[col], dir, colMax[col], table ); + sort = c.numberSorter ? c.numberSorter( a[ col ], b[ col ], dir, colMax[ col ], table ) : + ts[ 'sortNumeric' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], num, colMax[ col ], col, c ); + } else { + // set a & b depending on sort direction + x = dir ? a : b; + y = dir ? b : a; + // text sort function + if ( typeof textSorter === 'function' ) { + // custom OVERALL text sorter + sort = textSorter( x[ col ], y[ col ], dir, col, table ); + } else if ( typeof sorter[ col ] === 'function' ) { + // custom text sorter for a SPECIFIC COLUMN + sort = sorter[ col ]( x[ col ], y[ col ], dir, col, table ); + } else { + // fall back to natural sort + sort = ts[ 'sortNatural' + ( dir ? 'Asc' : 'Desc' ) ]( a[ col ], b[ col ], col, c ); + } + } + if ( sort ) { return sort; } + } + return a[ c.columns ].order - b[ c.columns ].order; + }); + } + if ( c.debug ) { + console.log( 'Applying sort ' + sortList.toString() + ts.benchmark( sortTime ) ); + } + }, + + resortComplete : function( c, callback ) { + if ( c.table.isUpdating ) { + c.$table.triggerHandler( 'updateComplete', c.table ); + } + if ( $.isFunction( callback ) ) { + callback( c.table ); + } + }, + + checkResort : function( c, resort, callback ) { + var sortList = $.isArray( resort ) ? resort : c.sortList, + // if no resort parameter is passed, fallback to config.resort (true by default) + resrt = typeof resort === 'undefined' ? c.resort : resort; + // don't try to resort if the table is still processing + // this will catch spamming of the updateCell method + if ( resrt !== false && !c.serverSideSorting && !c.table.isProcessing ) { + if ( sortList.length ) { + ts.sortOn( c, sortList, function() { + ts.resortComplete( c, callback ); + }, true ); + } else { + ts.sortReset( c, function() { + ts.resortComplete( c, callback ); + ts.applyWidget( c.table, false ); + } ); + } + } else { + ts.resortComplete( c, callback ); + ts.applyWidget( c.table, false ); + } + }, + + sortOn : function( c, list, callback, init ) { + var table = c.table; + c.$table.triggerHandler( 'sortStart', table ); + // update header count index + ts.updateHeaderSortCount( c, list ); + // set css for headers + ts.setHeadersCss( c ); + // fixes #346 + if ( c.delayInit && ts.isEmptyObject( c.cache ) ) { + ts.buildCache( c ); + } + c.$table.triggerHandler( 'sortBegin', table ); + // sort the table and append it to the dom + ts.multisort( c ); + ts.appendCache( c, init ); + c.$table.triggerHandler( 'sortBeforeEnd', table ); + c.$table.triggerHandler( 'sortEnd', table ); + ts.applyWidget( table ); + if ( $.isFunction( callback ) ) { + callback( table ); + } + }, + + sortReset : function( c, callback ) { + c.sortList = []; + ts.setHeadersCss( c ); + ts.multisort( c ); + ts.appendCache( c ); + var indx; + for (indx = 0; indx < c.columns; indx++) { + c.sortVars[ indx ].count = -1; + } + if ( $.isFunction( callback ) ) { + callback( c.table ); + } + }, + + getSortType : function( parsers, column ) { + return ( parsers && parsers[ column ] ) ? parsers[ column ].type || '' : ''; + }, + + getOrder : function( val ) { + // look for 'd' in 'desc' order; return true + return ( /^d/i.test( val ) || val === 1 ); + }, + + // Natural sort - https://github.com/overset/javascript-natural-sort (date sorting removed) + sortNatural : function( a, b ) { + if ( a === b ) { return 0; } + a = a.toString(); + b = b.toString(); + var aNum, bNum, aFloat, bFloat, indx, max, + regex = ts.regex; + // first try and sort Hex codes + if ( regex.hex.test( b ) ) { + aNum = parseInt( ( a || '' ).match( regex.hex ), 16 ); + bNum = parseInt( ( b || '' ).match( regex.hex ), 16 ); + if ( aNum < bNum ) { return -1; } + if ( aNum > bNum ) { return 1; } + } + // chunk/tokenize + aNum = ( a || '' ).replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); + bNum = ( b || '' ).replace( regex.chunk, '\\0$1\\0' ).replace( regex.chunks, '' ).split( '\\0' ); + max = Math.max( aNum.length, bNum.length ); + // natural sorting through split numeric strings and default strings + for ( indx = 0; indx < max; indx++ ) { + // find floats not starting with '0', string or 0 if not defined + aFloat = isNaN( aNum[ indx ] ) ? aNum[ indx ] || 0 : parseFloat( aNum[ indx ] ) || 0; + bFloat = isNaN( bNum[ indx ] ) ? bNum[ indx ] || 0 : parseFloat( bNum[ indx ] ) || 0; + // handle numeric vs string comparison - number < string - (Kyle Adams) + if ( isNaN( aFloat ) !== isNaN( bFloat ) ) { return isNaN( aFloat ) ? 1 : -1; } + // rely on string comparison if different types - i.e. '02' < 2 != '02' < '2' + if ( typeof aFloat !== typeof bFloat ) { + aFloat += ''; + bFloat += ''; + } + if ( aFloat < bFloat ) { return -1; } + if ( aFloat > bFloat ) { return 1; } + } + return 0; + }, + + sortNaturalAsc : function( a, b, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } + return ts.sortNatural( a, b ); + }, + + sortNaturalDesc : function( a, b, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } + return ts.sortNatural( b, a ); + }, + + // basic alphabetical sort + sortText : function( a, b ) { + return a > b ? 1 : ( a < b ? -1 : 0 ); + }, + + // return text string value by adding up ascii value + // so the text is somewhat sorted when using a digital sort + // this is NOT an alphanumeric sort + getTextValue : function( val, num, max ) { + if ( max ) { + // make sure the text value is greater than the max numerical value (max) + var indx, + len = val ? val.length : 0, + n = max + num; + for ( indx = 0; indx < len; indx++ ) { + n += val.charCodeAt( indx ); + } + return num * n; + } + return 0; + }, + + sortNumericAsc : function( a, b, num, max, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : -empty || -1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : empty || 1; } + if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } + if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } + return a - b; + }, + + sortNumericDesc : function( a, b, num, max, col, c ) { + if ( a === b ) { return 0; } + var empty = ts.string[ ( c.empties[ col ] || c.emptyTo ) ]; + if ( a === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? -1 : 1 ) : empty || 1; } + if ( b === '' && empty !== 0 ) { return typeof empty === 'boolean' ? ( empty ? 1 : -1 ) : -empty || -1; } + if ( isNaN( a ) ) { a = ts.getTextValue( a, num, max ); } + if ( isNaN( b ) ) { b = ts.getTextValue( b, num, max ); } + return b - a; + }, + + sortNumeric : function( a, b ) { + return a - b; + }, + + /* + ██ ██ ██ ██ █████▄ ▄████▄ ██████ ██████ ▄█████ + ██ ██ ██ ██ ██ ██ ██ ▄▄▄ ██▄▄ ██ ▀█▄ + ██ ██ ██ ██ ██ ██ ██ ▀██ ██▀▀ ██ ▀█▄ + ███████▀ ██ █████▀ ▀████▀ ██████ ██ █████▀ + */ + addWidget : function( widget ) { + if ( widget.id && !ts.isEmptyObject( ts.getWidgetById( widget.id ) ) ) { + console.warn( '"' + widget.id + '" widget was loaded more than once!' ); + } + ts.widgets[ ts.widgets.length ] = widget; + }, + + hasWidget : function( $table, name ) { + $table = $( $table ); + return $table.length && $table[ 0 ].config && $table[ 0 ].config.widgetInit[ name ] || false; + }, + + getWidgetById : function( name ) { + var indx, widget, + len = ts.widgets.length; + for ( indx = 0; indx < len; indx++ ) { + widget = ts.widgets[ indx ]; + if ( widget && widget.id && widget.id.toLowerCase() === name.toLowerCase() ) { + return widget; + } + } + }, + + applyWidgetOptions : function( table ) { + var indx, widget, wo, + c = table.config, + len = c.widgets.length; + if ( len ) { + for ( indx = 0; indx < len; indx++ ) { + widget = ts.getWidgetById( c.widgets[ indx ] ); + if ( widget && widget.options ) { + wo = $.extend( true, {}, widget.options ); + c.widgetOptions = $.extend( true, wo, c.widgetOptions ); + // add widgetOptions to defaults for option validator + $.extend( true, ts.defaults.widgetOptions, widget.options ); + } + } + } + }, + + addWidgetFromClass : function( table ) { + var len, indx, + c = table.config, + // look for widgets to apply from table class + // don't match from 'ui-widget-content'; use \S instead of \w to include widgets + // with dashes in the name, e.g. "widget-test-2" extracts out "test-2" + regex = '^' + c.widgetClass.replace( ts.regex.templateName, '(\\S+)+' ) + '$', + widgetClass = new RegExp( regex, 'g' ), + // split up table class (widget id's can include dashes) - stop using match + // otherwise only one widget gets extracted, see #1109 + widgets = ( table.className || '' ).split( ts.regex.spaces ); + if ( widgets.length ) { + len = widgets.length; + for ( indx = 0; indx < len; indx++ ) { + if ( widgets[ indx ].match( widgetClass ) ) { + c.widgets[ c.widgets.length ] = widgets[ indx ].replace( widgetClass, '$1' ); + } + } + } + }, + + applyWidgetId : function( table, id, init ) { + table = $(table)[0]; + var applied, time, name, + c = table.config, + wo = c.widgetOptions, + widget = ts.getWidgetById( id ); + if ( widget ) { + name = widget.id; + applied = false; + // add widget name to option list so it gets reapplied after sorting, filtering, etc + if ( $.inArray( name, c.widgets ) < 0 ) { + c.widgets[ c.widgets.length ] = name; + } + if ( c.debug ) { time = new Date(); } + + if ( init || !( c.widgetInit[ name ] ) ) { + // set init flag first to prevent calling init more than once (e.g. pager) + c.widgetInit[ name ] = true; + if ( table.hasInitialized ) { + // don't reapply widget options on tablesorter init + ts.applyWidgetOptions( table ); + } + if ( typeof widget.init === 'function' ) { + applied = true; + if ( c.debug ) { + console[ console.group ? 'group' : 'log' ]( 'Initializing ' + name + ' widget' ); + } + widget.init( table, widget, c, wo ); + } + } + if ( !init && typeof widget.format === 'function' ) { + applied = true; + if ( c.debug ) { + console[ console.group ? 'group' : 'log' ]( 'Updating ' + name + ' widget' ); + } + widget.format( table, c, wo, false ); + } + if ( c.debug ) { + if ( applied ) { + console.log( 'Completed ' + ( init ? 'initializing ' : 'applying ' ) + name + ' widget' + ts.benchmark( time ) ); + if ( console.groupEnd ) { console.groupEnd(); } + } + } + } + }, + + applyWidget : function( table, init, callback ) { + table = $( table )[ 0 ]; // in case this is called externally + var indx, len, names, widget, time, + c = table.config, + widgets = []; + // prevent numerous consecutive widget applications + if ( init !== false && table.hasInitialized && ( table.isApplyingWidgets || table.isUpdating ) ) { + return; + } + if ( c.debug ) { time = new Date(); } + ts.addWidgetFromClass( table ); + // prevent "tablesorter-ready" from firing multiple times in a row + clearTimeout( c.timerReady ); + if ( c.widgets.length ) { + table.isApplyingWidgets = true; + // ensure unique widget ids + c.widgets = $.grep( c.widgets, function( val, index ) { + return $.inArray( val, c.widgets ) === index; + }); + names = c.widgets || []; + len = names.length; + // build widget array & add priority as needed + for ( indx = 0; indx < len; indx++ ) { + widget = ts.getWidgetById( names[ indx ] ); + if ( widget && widget.id ) { + // set priority to 10 if not defined + if ( !widget.priority ) { widget.priority = 10; } + widgets[ indx ] = widget; + } else if ( c.debug ) { + console.warn( '"' + names[ indx ] + '" was enabled, but the widget code has not been loaded!' ); + } + } + // sort widgets by priority + widgets.sort( function( a, b ) { + return a.priority < b.priority ? -1 : a.priority === b.priority ? 0 : 1; + }); + // add/update selected widgets + len = widgets.length; + if ( c.debug ) { + console[ console.group ? 'group' : 'log' ]( 'Start ' + ( init ? 'initializing' : 'applying' ) + ' widgets' ); + } + for ( indx = 0; indx < len; indx++ ) { + widget = widgets[ indx ]; + if ( widget && widget.id ) { + ts.applyWidgetId( table, widget.id, init ); + } + } + if ( c.debug && console.groupEnd ) { console.groupEnd(); } + } + c.timerReady = setTimeout( function() { + table.isApplyingWidgets = false; + $.data( table, 'lastWidgetApplication', new Date() ); + c.$table.triggerHandler( 'tablesorter-ready' ); + // callback executed on init only + if ( !init && typeof callback === 'function' ) { + callback( table ); + } + if ( c.debug ) { + widget = c.widgets.length; + console.log( 'Completed ' + + ( init === true ? 'initializing ' : 'applying ' ) + widget + + ' widget' + ( widget !== 1 ? 's' : '' ) + ts.benchmark( time ) ); + } + }, 10 ); + }, + + removeWidget : function( table, name, refreshing ) { + table = $( table )[ 0 ]; + var index, widget, indx, len, + c = table.config; + // if name === true, add all widgets from $.tablesorter.widgets + if ( name === true ) { + name = []; + len = ts.widgets.length; + for ( indx = 0; indx < len; indx++ ) { + widget = ts.widgets[ indx ]; + if ( widget && widget.id ) { + name[ name.length ] = widget.id; + } + } + } else { + // name can be either an array of widgets names, + // or a space/comma separated list of widget names + name = ( $.isArray( name ) ? name.join( ',' ) : name || '' ).toLowerCase().split( /[\s,]+/ ); + } + len = name.length; + for ( index = 0; index < len; index++ ) { + widget = ts.getWidgetById( name[ index ] ); + indx = $.inArray( name[ index ], c.widgets ); + // don't remove the widget from config.widget if refreshing + if ( indx >= 0 && refreshing !== true ) { + c.widgets.splice( indx, 1 ); + } + if ( widget && widget.remove ) { + if ( c.debug ) { + console.log( ( refreshing ? 'Refreshing' : 'Removing' ) + ' "' + name[ index ] + '" widget' ); + } + widget.remove( table, c, c.widgetOptions, refreshing ); + c.widgetInit[ name[ index ] ] = false; + } + } + c.$table.triggerHandler( 'widgetRemoveEnd', table ); + }, + + refreshWidgets : function( table, doAll, dontapply ) { + table = $( table )[ 0 ]; // see issue #243 + var indx, widget, + c = table.config, + curWidgets = c.widgets, + widgets = ts.widgets, + len = widgets.length, + list = [], + callback = function( table ) { + $( table ).triggerHandler( 'refreshComplete' ); + }; + // remove widgets not defined in config.widgets, unless doAll is true + for ( indx = 0; indx < len; indx++ ) { + widget = widgets[ indx ]; + if ( widget && widget.id && ( doAll || $.inArray( widget.id, curWidgets ) < 0 ) ) { + list[ list.length ] = widget.id; + } + } + ts.removeWidget( table, list.join( ',' ), true ); + if ( dontapply !== true ) { + // call widget init if + ts.applyWidget( table, doAll || false, callback ); + if ( doAll ) { + // apply widget format + ts.applyWidget( table, false, callback ); + } + } else { + callback( table ); + } + }, + + /* + ██ ██ ██████ ██ ██ ██ ██████ ██ ██████ ▄█████ + ██ ██ ██ ██ ██ ██ ██ ██ ██▄▄ ▀█▄ + ██ ██ ██ ██ ██ ██ ██ ██ ██▀▀ ▀█▄ + ▀████▀ ██ ██ ██████ ██ ██ ██ ██████ █████▀ + */ + benchmark : function( diff ) { + return ( ' (' + ( new Date().getTime() - diff.getTime() ) + ' ms)' ); + }, + // deprecated ts.log + log : function() { + console.log( arguments ); + }, + + // $.isEmptyObject from jQuery v1.4 + isEmptyObject : function( obj ) { + /*jshint forin: false */ + for ( var name in obj ) { + return false; + } + return true; + }, + + isValueInArray : function( column, arry ) { + var indx, + len = arry && arry.length || 0; + for ( indx = 0; indx < len; indx++ ) { + if ( arry[ indx ][ 0 ] === column ) { + return indx; + } + } + return -1; + }, + + formatFloat : function( str, table ) { + if ( typeof str !== 'string' || str === '' ) { return str; } + // allow using formatFloat without a table; defaults to US number format + var num, + usFormat = table && table.config ? table.config.usNumberFormat !== false : + typeof table !== 'undefined' ? table : true; + if ( usFormat ) { + // US Format - 1,234,567.89 -> 1234567.89 + str = str.replace( ts.regex.comma, '' ); + } else { + // German Format = 1.234.567,89 -> 1234567.89 + // French Format = 1 234 567,89 -> 1234567.89 + str = str.replace( ts.regex.digitNonUS, '' ).replace( ts.regex.comma, '.' ); + } + if ( ts.regex.digitNegativeTest.test( str ) ) { + // make (#) into a negative number -> (10) = -10 + str = str.replace( ts.regex.digitNegativeReplace, '-$1' ); + } + num = parseFloat( str ); + // return the text instead of zero + return isNaN( num ) ? $.trim( str ) : num; + }, + + isDigit : function( str ) { + // replace all unwanted chars and match + return isNaN( str ) ? + ts.regex.digitTest.test( str.toString().replace( ts.regex.digitReplace, '' ) ) : + str !== ''; + }, + + // computeTableHeaderCellIndexes from: + // http://www.javascripttoolbox.com/lib/table/examples.php + // http://www.javascripttoolbox.com/temp/table_cellindex.html + computeColumnIndex : function( $rows, c ) { + var i, j, k, l, cell, cells, rowIndex, rowSpan, colSpan, firstAvailCol, + // total columns has been calculated, use it to set the matrixrow + columns = c && c.columns || 0, + matrix = [], + matrixrow = new Array( columns ); + for ( i = 0; i < $rows.length; i++ ) { + cells = $rows[ i ].cells; + for ( j = 0; j < cells.length; j++ ) { + cell = cells[ j ]; + rowIndex = i; + rowSpan = cell.rowSpan || 1; + colSpan = cell.colSpan || 1; + if ( typeof matrix[ rowIndex ] === 'undefined' ) { + matrix[ rowIndex ] = []; + } + // Find first available column in the first row + for ( k = 0; k < matrix[ rowIndex ].length + 1; k++ ) { + if ( typeof matrix[ rowIndex ][ k ] === 'undefined' ) { + firstAvailCol = k; + break; + } + } + // jscs:disable disallowEmptyBlocks + if ( columns && cell.cellIndex === firstAvailCol ) { + // don't to anything + } else if ( cell.setAttribute ) { + // jscs:enable disallowEmptyBlocks + // add data-column (setAttribute = IE8+) + cell.setAttribute( 'data-column', firstAvailCol ); + } else { + // remove once we drop support for IE7 - 1/12/2016 + $( cell ).attr( 'data-column', firstAvailCol ); + } + for ( k = rowIndex; k < rowIndex + rowSpan; k++ ) { + if ( typeof matrix[ k ] === 'undefined' ) { + matrix[ k ] = []; + } + matrixrow = matrix[ k ]; + for ( l = firstAvailCol; l < firstAvailCol + colSpan; l++ ) { + matrixrow[ l ] = 'x'; + } + } + } + } + ts.checkColumnCount($rows, matrix, matrixrow.length); + return matrixrow.length; + }, + + checkColumnCount : function($rows, matrix, columns) { + // this DOES NOT report any tbody column issues, except for the math and + // and column selector widgets + var i, len, + valid = true, + cells = []; + for ( i = 0; i < matrix.length; i++ ) { + // some matrix entries are undefined when testing the footer because + // it is using the rowIndex property + if ( matrix[i] ) { + len = matrix[i].length; + if ( matrix[i].length !== columns ) { + valid = false; + break; + } + } + } + if ( !valid ) { + $rows.each( function( indx, el ) { + var cell = el.parentElement.nodeName; + if ( cells.indexOf( cell ) < 0 ) { + cells.push( cell ); + } + }); + console.error( + 'Invalid or incorrect number of columns in the ' + + cells.join( ' or ' ) + '; expected ' + columns + + ', but found ' + len + ' columns' + ); + } + }, + + // automatically add a colgroup with col elements set to a percentage width + fixColumnWidth : function( table ) { + table = $( table )[ 0 ]; + var overallWidth, percent, $tbodies, len, index, + c = table.config, + $colgroup = c.$table.children( 'colgroup' ); + // remove plugin-added colgroup, in case we need to refresh the widths + if ( $colgroup.length && $colgroup.hasClass( ts.css.colgroup ) ) { + $colgroup.remove(); + } + if ( c.widthFixed && c.$table.children( 'colgroup' ).length === 0 ) { + $colgroup = $( '' ); + overallWidth = c.$table.width(); + // only add col for visible columns - fixes #371 + $tbodies = c.$tbodies.find( 'tr:first' ).children( ':visible' ); + len = $tbodies.length; + for ( index = 0; index < len; index++ ) { + percent = parseInt( ( $tbodies.eq( index ).width() / overallWidth ) * 1000, 10 ) / 10 + '%'; + $colgroup.append( $( '' ).css( 'width', percent ) ); + } + c.$table.prepend( $colgroup ); + } + }, + + // get sorter, string, empty, etc options for each column from + // jQuery data, metadata, header option or header class name ('sorter-false') + // priority = jQuery data > meta > headers option > header class name + getData : function( header, configHeader, key ) { + var meta, cl4ss, + val = '', + $header = $( header ); + if ( !$header.length ) { return ''; } + meta = $.metadata ? $header.metadata() : false; + cl4ss = ' ' + ( $header.attr( 'class' ) || '' ); + if ( typeof $header.data( key ) !== 'undefined' || + typeof $header.data( key.toLowerCase() ) !== 'undefined' ) { + // 'data-lockedOrder' is assigned to 'lockedorder'; but 'data-locked-order' is assigned to 'lockedOrder' + // 'data-sort-initial-order' is assigned to 'sortInitialOrder' + val += $header.data( key ) || $header.data( key.toLowerCase() ); + } else if ( meta && typeof meta[ key ] !== 'undefined' ) { + val += meta[ key ]; + } else if ( configHeader && typeof configHeader[ key ] !== 'undefined' ) { + val += configHeader[ key ]; + } else if ( cl4ss !== ' ' && cl4ss.match( ' ' + key + '-' ) ) { + // include sorter class name 'sorter-text', etc; now works with 'sorter-my-custom-parser' + val = cl4ss.match( new RegExp( '\\s' + key + '-([\\w-]+)' ) )[ 1 ] || ''; + } + return $.trim( val ); + }, + + getColumnData : function( table, obj, indx, getCell, $headers ) { + if ( typeof obj !== 'object' || obj === null ) { + return obj; + } + table = $( table )[ 0 ]; + var $header, key, + c = table.config, + $cells = ( $headers || c.$headers ), + // c.$headerIndexed is not defined initially + $cell = c.$headerIndexed && c.$headerIndexed[ indx ] || + $cells.filter( '[data-column="' + indx + '"]:last' ); + if ( typeof obj[ indx ] !== 'undefined' ) { + return getCell ? obj[ indx ] : obj[ $cells.index( $cell ) ]; + } + for ( key in obj ) { + if ( typeof key === 'string' ) { + $header = $cell + // header cell with class/id + .filter( key ) + // find elements within the header cell with cell/id + .add( $cell.find( key ) ); + if ( $header.length ) { + return obj[ key ]; + } + } + } + return; + }, + + // *** Process table *** + // add processing indicator + isProcessing : function( $table, toggle, $headers ) { + $table = $( $table ); + var c = $table[ 0 ].config, + // default to all headers + $header = $headers || $table.find( '.' + ts.css.header ); + if ( toggle ) { + // don't use sortList if custom $headers used + if ( typeof $headers !== 'undefined' && c.sortList.length > 0 ) { + // get headers from the sortList + $header = $header.filter( function() { + // get data-column from attr to keep compatibility with jQuery 1.2.6 + return this.sortDisabled ? + false : + ts.isValueInArray( parseFloat( $( this ).attr( 'data-column' ) ), c.sortList ) >= 0; + }); + } + $table.add( $header ).addClass( ts.css.processing + ' ' + c.cssProcessing ); + } else { + $table.add( $header ).removeClass( ts.css.processing + ' ' + c.cssProcessing ); + } + }, + + // detach tbody but save the position + // don't use tbody because there are portions that look for a tbody index (updateCell) + processTbody : function( table, $tb, getIt ) { + table = $( table )[ 0 ]; + if ( getIt ) { + table.isProcessing = true; + $tb.before( '' ); + return $.fn.detach ? $tb.detach() : $tb.remove(); + } + var holdr = $( table ).find( 'colgroup.tablesorter-savemyplace' ); + $tb.insertAfter( holdr ); + holdr.remove(); + table.isProcessing = false; + }, + + clearTableBody : function( table ) { + $( table )[ 0 ].config.$tbodies.children().detach(); + }, + + // used when replacing accented characters during sorting + characterEquivalents : { + 'a' : '\u00e1\u00e0\u00e2\u00e3\u00e4\u0105\u00e5', // áàâãäąå + 'A' : '\u00c1\u00c0\u00c2\u00c3\u00c4\u0104\u00c5', // ÁÀÂÃÄĄÅ + 'c' : '\u00e7\u0107\u010d', // çćč + 'C' : '\u00c7\u0106\u010c', // ÇĆČ + 'e' : '\u00e9\u00e8\u00ea\u00eb\u011b\u0119', // éèêëěę + 'E' : '\u00c9\u00c8\u00ca\u00cb\u011a\u0118', // ÉÈÊËĚĘ + 'i' : '\u00ed\u00ec\u0130\u00ee\u00ef\u0131', // íìİîïı + 'I' : '\u00cd\u00cc\u0130\u00ce\u00cf', // ÍÌİÎÏ + 'o' : '\u00f3\u00f2\u00f4\u00f5\u00f6\u014d', // óòôõöō + 'O' : '\u00d3\u00d2\u00d4\u00d5\u00d6\u014c', // ÓÒÔÕÖŌ + 'ss': '\u00df', // ß (s sharp) + 'SS': '\u1e9e', // ẞ (Capital sharp s) + 'u' : '\u00fa\u00f9\u00fb\u00fc\u016f', // úùûüů + 'U' : '\u00da\u00d9\u00db\u00dc\u016e' // ÚÙÛÜŮ + }, + + replaceAccents : function( str ) { + var chr, + acc = '[', + eq = ts.characterEquivalents; + if ( !ts.characterRegex ) { + ts.characterRegexArray = {}; + for ( chr in eq ) { + if ( typeof chr === 'string' ) { + acc += eq[ chr ]; + ts.characterRegexArray[ chr ] = new RegExp( '[' + eq[ chr ] + ']', 'g' ); + } + } + ts.characterRegex = new RegExp( acc + ']' ); + } + if ( ts.characterRegex.test( str ) ) { + for ( chr in eq ) { + if ( typeof chr === 'string' ) { + str = str.replace( ts.characterRegexArray[ chr ], chr ); + } + } + } + return str; + }, + + validateOptions : function( c ) { + var setting, setting2, typ, timer, + // ignore options containing an array + ignore = 'headers sortForce sortList sortAppend widgets'.split( ' ' ), + orig = c.originalSettings; + if ( orig ) { + if ( c.debug ) { + timer = new Date(); + } + for ( setting in orig ) { + typ = typeof ts.defaults[setting]; + if ( typ === 'undefined' ) { + console.warn( 'Tablesorter Warning! "table.config.' + setting + '" option not recognized' ); + } else if ( typ === 'object' ) { + for ( setting2 in orig[setting] ) { + typ = ts.defaults[setting] && typeof ts.defaults[setting][setting2]; + if ( $.inArray( setting, ignore ) < 0 && typ === 'undefined' ) { + console.warn( 'Tablesorter Warning! "table.config.' + setting + '.' + setting2 + '" option not recognized' ); + } + } + } + } + if ( c.debug ) { + console.log( 'validate options time:' + ts.benchmark( timer ) ); + } + } + }, + + // restore headers + restoreHeaders : function( table ) { + var index, $cell, + c = $( table )[ 0 ].config, + $headers = c.$table.find( c.selectorHeaders ), + len = $headers.length; + // don't use c.$headers here in case header cells were swapped + for ( index = 0; index < len; index++ ) { + $cell = $headers.eq( index ); + // only restore header cells if it is wrapped + // because this is also used by the updateAll method + if ( $cell.find( '.' + ts.css.headerIn ).length ) { + $cell.html( c.headerContent[ index ] ); + } + } + }, + + destroy : function( table, removeClasses, callback ) { + table = $( table )[ 0 ]; + if ( !table.hasInitialized ) { return; } + // remove all widgets + ts.removeWidget( table, true, false ); + var events, + $t = $( table ), + c = table.config, + debug = c.debug, + $h = $t.find( 'thead:first' ), + $r = $h.find( 'tr.' + ts.css.headerRow ).removeClass( ts.css.headerRow + ' ' + c.cssHeaderRow ), + $f = $t.find( 'tfoot:first > tr' ).children( 'th, td' ); + if ( removeClasses === false && $.inArray( 'uitheme', c.widgets ) >= 0 ) { + // reapply uitheme classes, in case we want to maintain appearance + $t.triggerHandler( 'applyWidgetId', [ 'uitheme' ] ); + $t.triggerHandler( 'applyWidgetId', [ 'zebra' ] ); + } + // remove widget added rows, just in case + $h.find( 'tr' ).not( $r ).remove(); + // disable tablesorter - not using .unbind( namespace ) because namespacing was + // added in jQuery v1.4.3 - see http://api.jquery.com/event.namespace/ + events = 'sortReset update updateRows updateAll updateHeaders updateCell addRows updateComplete sorton ' + + 'appendCache updateCache applyWidgetId applyWidgets refreshWidgets removeWidget destroy mouseup mouseleave ' + + 'keypress sortBegin sortEnd resetToLoadState '.split( ' ' ) + .join( c.namespace + ' ' ); + $t + .removeData( 'tablesorter' ) + .unbind( events.replace( ts.regex.spaces, ' ' ) ); + c.$headers + .add( $f ) + .removeClass( [ ts.css.header, c.cssHeader, c.cssAsc, c.cssDesc, ts.css.sortAsc, ts.css.sortDesc, ts.css.sortNone ].join( ' ' ) ) + .removeAttr( 'data-column' ) + .removeAttr( 'aria-label' ) + .attr( 'aria-disabled', 'true' ); + $r + .find( c.selectorSort ) + .unbind( ( 'mousedown mouseup keypress '.split( ' ' ).join( c.namespace + ' ' ) ).replace( ts.regex.spaces, ' ' ) ); + ts.restoreHeaders( table ); + $t.toggleClass( ts.css.table + ' ' + c.tableClass + ' tablesorter-' + c.theme, removeClasses === false ); + $t.removeClass(c.namespace.slice(1)); + // clear flag in case the plugin is initialized again + table.hasInitialized = false; + delete table.config.cache; + if ( typeof callback === 'function' ) { + callback( table ); + } + if ( debug ) { + console.log( 'tablesorter has been removed' ); + } + } + + }; + + $.fn.tablesorter = function( settings ) { + return this.each( function() { + var table = this, + // merge & extend config options + c = $.extend( true, {}, ts.defaults, settings, ts.instanceMethods ); + // save initial settings + c.originalSettings = settings; + // create a table from data (build table widget) + if ( !table.hasInitialized && ts.buildTable && this.nodeName !== 'TABLE' ) { + // return the table (in case the original target is the table's container) + ts.buildTable( table, c ); + } else { + ts.setup( table, c ); + } + }); + }; + + // set up debug logs + if ( !( window.console && window.console.log ) ) { + // access $.tablesorter.logs for browsers that don't have a console... + ts.logs = []; + /*jshint -W020 */ + console = {}; + console.log = console.warn = console.error = console.table = function() { + var arg = arguments.length > 1 ? arguments : arguments[0]; + ts.logs[ ts.logs.length ] = { date: Date.now(), log: arg }; + }; + } + + // add default parsers + ts.addParser({ + id : 'no-parser', + is : function() { + return false; + }, + format : function() { + return ''; + }, + type : 'text' + }); + + ts.addParser({ + id : 'text', + is : function() { + return true; + }, + format : function( str, table ) { + var c = table.config; + if ( str ) { + str = $.trim( c.ignoreCase ? str.toLocaleLowerCase() : str ); + str = c.sortLocaleCompare ? ts.replaceAccents( str ) : str; + } + return str; + }, + type : 'text' + }); + + ts.regex.nondigit = /[^\w,. \-()]/g; + ts.addParser({ + id : 'digit', + is : function( str ) { + return ts.isDigit( str ); + }, + format : function( str, table ) { + var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); + return str && typeof num === 'number' ? num : + str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; + }, + type : 'numeric' + }); + + ts.regex.currencyReplace = /[+\-,. ]/g; + ts.regex.currencyTest = /^\(?\d+[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]|[\u00a3$\u20ac\u00a4\u00a5\u00a2?.]\d+\)?$/; + ts.addParser({ + id : 'currency', + is : function( str ) { + str = ( str || '' ).replace( ts.regex.currencyReplace, '' ); + // test for £$€¤¥¢ + return ts.regex.currencyTest.test( str ); + }, + format : function( str, table ) { + var num = ts.formatFloat( ( str || '' ).replace( ts.regex.nondigit, '' ), table ); + return str && typeof num === 'number' ? num : + str ? $.trim( str && table.config.ignoreCase ? str.toLocaleLowerCase() : str ) : str; + }, + type : 'numeric' + }); + + // too many protocols to add them all https://en.wikipedia.org/wiki/URI_scheme + // now, this regex can be updated before initialization + ts.regex.urlProtocolTest = /^(https?|ftp|file):\/\//; + ts.regex.urlProtocolReplace = /(https?|ftp|file):\/\/(www\.)?/; + ts.addParser({ + id : 'url', + is : function( str ) { + return ts.regex.urlProtocolTest.test( str ); + }, + format : function( str ) { + return str ? $.trim( str.replace( ts.regex.urlProtocolReplace, '' ) ) : str; + }, + type : 'text' + }); + + ts.regex.dash = /-/g; + ts.regex.isoDate = /^\d{4}[\/\-]\d{1,2}[\/\-]\d{1,2}/; + ts.addParser({ + id : 'isoDate', + is : function( str ) { + return ts.regex.isoDate.test( str ); + }, + format : function( str, table ) { + var date = str ? new Date( str.replace( ts.regex.dash, '/' ) ) : str; + return date instanceof Date && isFinite( date ) ? date.getTime() : str; + }, + type : 'numeric' + }); + + ts.regex.percent = /%/g; + ts.regex.percentTest = /(\d\s*?%|%\s*?\d)/; + ts.addParser({ + id : 'percent', + is : function( str ) { + return ts.regex.percentTest.test( str ) && str.length < 15; + }, + format : function( str, table ) { + return str ? ts.formatFloat( str.replace( ts.regex.percent, '' ), table ) : str; + }, + type : 'numeric' + }); + + // added image parser to core v2.17.9 + ts.addParser({ + id : 'image', + is : function( str, table, node, $node ) { + return $node.find( 'img' ).length > 0; + }, + format : function( str, table, cell ) { + return $( cell ).find( 'img' ).attr( table.config.imgAttr || 'alt' ) || str; + }, + parsed : true, // filter widget flag + type : 'text' + }); + + ts.regex.dateReplace = /(\S)([AP]M)$/i; // used by usLongDate & time parser + ts.regex.usLongDateTest1 = /^[A-Z]{3,10}\.?\s+\d{1,2},?\s+(\d{4})(\s+\d{1,2}:\d{2}(:\d{2})?(\s+[AP]M)?)?$/i; + ts.regex.usLongDateTest2 = /^\d{1,2}\s+[A-Z]{3,10}\s+\d{4}/i; + ts.addParser({ + id : 'usLongDate', + is : function( str ) { + // two digit years are not allowed cross-browser + // Jan 01, 2013 12:34:56 PM or 01 Jan 2013 + return ts.regex.usLongDateTest1.test( str ) || ts.regex.usLongDateTest2.test( str ); + }, + format : function( str, table ) { + var date = str ? new Date( str.replace( ts.regex.dateReplace, '$1 $2' ) ) : str; + return date instanceof Date && isFinite( date ) ? date.getTime() : str; + }, + type : 'numeric' + }); + + // testing for ##-##-#### or ####-##-##, so it's not perfect; time can be included + ts.regex.shortDateTest = /(^\d{1,2}[\/\s]\d{1,2}[\/\s]\d{4})|(^\d{4}[\/\s]\d{1,2}[\/\s]\d{1,2})/; + // escaped "-" because JSHint in Firefox was showing it as an error + ts.regex.shortDateReplace = /[\-.,]/g; + // XXY covers MDY & DMY formats + ts.regex.shortDateXXY = /(\d{1,2})[\/\s](\d{1,2})[\/\s](\d{4})/; + ts.regex.shortDateYMD = /(\d{4})[\/\s](\d{1,2})[\/\s](\d{1,2})/; + ts.convertFormat = function( dateString, format ) { + dateString = ( dateString || '' ) + .replace( ts.regex.spaces, ' ' ) + .replace( ts.regex.shortDateReplace, '/' ); + if ( format === 'mmddyyyy' ) { + dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$1/$2' ); + } else if ( format === 'ddmmyyyy' ) { + dateString = dateString.replace( ts.regex.shortDateXXY, '$3/$2/$1' ); + } else if ( format === 'yyyymmdd' ) { + dateString = dateString.replace( ts.regex.shortDateYMD, '$1/$2/$3' ); + } + var date = new Date( dateString ); + return date instanceof Date && isFinite( date ) ? date.getTime() : ''; + }; + + ts.addParser({ + id : 'shortDate', // 'mmddyyyy', 'ddmmyyyy' or 'yyyymmdd' + is : function( str ) { + str = ( str || '' ).replace( ts.regex.spaces, ' ' ).replace( ts.regex.shortDateReplace, '/' ); + return ts.regex.shortDateTest.test( str ); + }, + format : function( str, table, cell, cellIndex ) { + if ( str ) { + var c = table.config, + $header = c.$headerIndexed[ cellIndex ], + format = $header.length && $header.data( 'dateFormat' ) || + ts.getData( $header, ts.getColumnData( table, c.headers, cellIndex ), 'dateFormat' ) || + c.dateFormat; + // save format because getData can be slow... + if ( $header.length ) { + $header.data( 'dateFormat', format ); + } + return ts.convertFormat( str, format ) || str; + } + return str; + }, + type : 'numeric' + }); + + // match 24 hour time & 12 hours time + am/pm - see http://regexr.com/3c3tk + ts.regex.timeTest = /^(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)$|^((?:[01]\d|[2][0-4]):[0-5]\d)$/i; + ts.regex.timeMatch = /(0?[1-9]|1[0-2]):([0-5]\d)(\s[AP]M)|((?:[01]\d|[2][0-4]):[0-5]\d)/i; + ts.addParser({ + id : 'time', + is : function( str ) { + return ts.regex.timeTest.test( str ); + }, + format : function( str, table ) { + // isolate time... ignore month, day and year + var temp, + timePart = ( str || '' ).match( ts.regex.timeMatch ), + orig = new Date( str ), + // no time component? default to 00:00 by leaving it out, but only if str is defined + time = str && ( timePart !== null ? timePart[ 0 ] : '00:00 AM' ), + date = time ? new Date( '2000/01/01 ' + time.replace( ts.regex.dateReplace, '$1 $2' ) ) : time; + if ( date instanceof Date && isFinite( date ) ) { + temp = orig instanceof Date && isFinite( orig ) ? orig.getTime() : 0; + // if original string was a valid date, add it to the decimal so the column sorts in some kind of order + // luckily new Date() ignores the decimals + return temp ? parseFloat( date.getTime() + '.' + orig.getTime() ) : date.getTime(); + } + return str; + }, + type : 'numeric' + }); + + ts.addParser({ + id : 'metadata', + is : function() { + return false; + }, + format : function( str, table, cell ) { + var c = table.config, + p = ( !c.parserMetadataName ) ? 'sortValue' : c.parserMetadataName; + return $( cell ).metadata()[ p ]; + }, + type : 'numeric' + }); + + /* + ██████ ██████ █████▄ █████▄ ▄████▄ + ▄█▀ ██▄▄ ██▄▄██ ██▄▄██ ██▄▄██ + ▄█▀ ██▀▀ ██▀▀██ ██▀▀█ ██▀▀██ + ██████ ██████ █████▀ ██ ██ ██ ██ + */ + // add default widgets + ts.addWidget({ + id : 'zebra', + priority : 90, + format : function( table, c, wo ) { + var $visibleRows, $row, count, isEven, tbodyIndex, rowIndex, len, + child = new RegExp( c.cssChildRow, 'i' ), + $tbodies = c.$tbodies.add( $( c.namespace + '_extra_table' ).children( 'tbody:not(.' + c.cssInfoBlock + ')' ) ); + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + // loop through the visible rows + count = 0; + $visibleRows = $tbodies.eq( tbodyIndex ).children( 'tr:visible' ).not( c.selectorRemove ); + len = $visibleRows.length; + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + $row = $visibleRows.eq( rowIndex ); + // style child rows the same way the parent row was styled + if ( !child.test( $row[ 0 ].className ) ) { count++; } + isEven = ( count % 2 === 0 ); + $row + .removeClass( wo.zebra[ isEven ? 1 : 0 ] ) + .addClass( wo.zebra[ isEven ? 0 : 1 ] ); + } + } + }, + remove : function( table, c, wo, refreshing ) { + if ( refreshing ) { return; } + var tbodyIndex, $tbody, + $tbodies = c.$tbodies, + toRemove = ( wo.zebra || [ 'even', 'odd' ] ).join( ' ' ); + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ){ + $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody + $tbody.children().removeClass( toRemove ); + ts.processTbody( table, $tbody, false ); // restore tbody + } + } + }); + +})( jQuery ); + +/*! Widget: storage - updated 4/18/2017 (v2.28.8) */ +/*global JSON:false */ +;(function ($, window, document) { + 'use strict'; + + var ts = $.tablesorter || {}; + + // update defaults for validator; these values must be falsy! + $.extend(true, ts.defaults, { + fixedUrl: '', + widgetOptions: { + storage_fixedUrl: '', + storage_group: '', + storage_page: '', + storage_storageType: '', + storage_tableId: '', + storage_useSessionStorage: '' + } + }); + + // *** Store data in local storage, with a cookie fallback *** + /* IE7 needs JSON library for JSON.stringify - (http://caniuse.com/#search=json) + if you need it, then include https://github.com/douglascrockford/JSON-js + + $.parseJSON is not available is jQuery versions older than 1.4.1, using older + versions will only allow storing information for one page at a time + + // *** Save data (JSON format only) *** + // val must be valid JSON... use http://jsonlint.com/ to ensure it is valid + var val = { "mywidget" : "data1" }; // valid JSON uses double quotes + // $.tablesorter.storage(table, key, val); + $.tablesorter.storage(table, 'tablesorter-mywidget', val); + + // *** Get data: $.tablesorter.storage(table, key); *** + v = $.tablesorter.storage(table, 'tablesorter-mywidget'); + // val may be empty, so also check for your data + val = (v && v.hasOwnProperty('mywidget')) ? v.mywidget : ''; + alert(val); // 'data1' if saved, or '' if not + */ + ts.storage = function(table, key, value, options) { + table = $(table)[0]; + var cookieIndex, cookies, date, + hasStorage = false, + values = {}, + c = table.config, + wo = c && c.widgetOptions, + storageType = ( + ( options && options.storageType ) || ( wo && wo.storage_storageType ) + ).toString().charAt(0).toLowerCase(), + // deprecating "useSessionStorage"; any storageType setting overrides it + session = storageType ? '' : + ( options && options.useSessionStorage ) || ( wo && wo.storage_useSessionStorage ), + $table = $(table), + // id from (1) options ID, (2) table 'data-table-group' attribute, (3) widgetOptions.storage_tableId, + // (4) table ID, then (5) table index + id = options && options.id || + $table.attr( options && options.group || wo && wo.storage_group || 'data-table-group') || + wo && wo.storage_tableId || table.id || $('.tablesorter').index( $table ), + // url from (1) options url, (2) table 'data-table-page' attribute, (3) widgetOptions.storage_fixedUrl, + // (4) table.config.fixedUrl (deprecated), then (5) window location path + url = options && options.url || + $table.attr(options && options.page || wo && wo.storage_page || 'data-table-page') || + wo && wo.storage_fixedUrl || c && c.fixedUrl || window.location.pathname; + + // skip if using cookies + if (storageType !== 'c') { + storageType = (storageType === 's' || session) ? 'sessionStorage' : 'localStorage'; + // https://gist.github.com/paulirish/5558557 + if (storageType in window) { + try { + window[storageType].setItem('_tmptest', 'temp'); + hasStorage = true; + window[storageType].removeItem('_tmptest'); + } catch (error) { + if (c && c.debug) { + console.warn( storageType + ' is not supported in this browser' ); + } + } + } + } + if (c.debug) { + console.log('Storage widget using', hasStorage ? storageType : 'cookies'); + } + // *** get value *** + if ($.parseJSON) { + if (hasStorage) { + values = $.parseJSON( window[storageType][key] || 'null' ) || {}; + } else { + // old browser, using cookies + cookies = document.cookie.split(/[;\s|=]/); + // add one to get from the key to the value + cookieIndex = $.inArray(key, cookies) + 1; + values = (cookieIndex !== 0) ? $.parseJSON(cookies[cookieIndex] || 'null') || {} : {}; + } + } + // allow value to be an empty string too + if (typeof value !== 'undefined' && window.JSON && JSON.hasOwnProperty('stringify')) { + // add unique identifiers = url pathname > table ID/index on page > data + if (!values[url]) { + values[url] = {}; + } + values[url][id] = value; + // *** set value *** + if (hasStorage) { + window[storageType][key] = JSON.stringify(values); + } else { + date = new Date(); + date.setTime(date.getTime() + (31536e+6)); // 365 days + document.cookie = key + '=' + (JSON.stringify(values)).replace(/\"/g, '\"') + '; expires=' + date.toGMTString() + '; path=/'; + } + } else { + return values && values[url] ? values[url][id] : ''; + } + }; + +})(jQuery, window, document); + +/*! Widget: uitheme - updated 9/27/2017 (v2.29.0) */ +;(function ($) { + 'use strict'; + var ts = $.tablesorter || {}; + + ts.themes = { + 'bootstrap' : { + table : 'table table-bordered table-striped', + caption : 'caption', + // header class names + header : 'bootstrap-header', // give the header a gradient background (theme.bootstrap_2.css) + sortNone : '', + sortAsc : '', + sortDesc : '', + active : '', // applied when column is sorted + hover : '', // custom css required - a defined bootstrap style may not override other classes + // icon class names + icons : '', // add 'bootstrap-icon-white' to make them white; this icon class is added to the in the header + iconSortNone : 'bootstrap-icon-unsorted', // class name added to icon when column is not sorted + iconSortAsc : 'glyphicon glyphicon-chevron-up', // class name added to icon when column has ascending sort + iconSortDesc : 'glyphicon glyphicon-chevron-down', // class name added to icon when column has descending sort + filterRow : '', // filter row class + footerRow : '', + footerCells : '', + even : '', // even row zebra striping + odd : '' // odd row zebra striping + }, + 'jui' : { + table : 'ui-widget ui-widget-content ui-corner-all', // table classes + caption : 'ui-widget-content', + // header class names + header : 'ui-widget-header ui-corner-all ui-state-default', // header classes + sortNone : '', + sortAsc : '', + sortDesc : '', + active : 'ui-state-active', // applied when column is sorted + hover : 'ui-state-hover', // hover class + // icon class names + icons : 'ui-icon', // icon class added to the in the header + iconSortNone : 'ui-icon-carat-2-n-s ui-icon-caret-2-n-s', // class name added to icon when column is not sorted + iconSortAsc : 'ui-icon-carat-1-n ui-icon-caret-1-n', // class name added to icon when column has ascending sort + iconSortDesc : 'ui-icon-carat-1-s ui-icon-caret-1-s', // class name added to icon when column has descending sort + filterRow : '', + footerRow : '', + footerCells : '', + even : 'ui-widget-content', // even row zebra striping + odd : 'ui-state-default' // odd row zebra striping + } + }; + + $.extend(ts.css, { + wrapper : 'tablesorter-wrapper' // ui theme & resizable + }); + + ts.addWidget({ + id: 'uitheme', + priority: 10, + format: function(table, c, wo) { + var i, tmp, hdr, icon, time, $header, $icon, $tfoot, $h, oldtheme, oldremove, oldIconRmv, hasOldTheme, + themesAll = ts.themes, + $table = c.$table.add( $( c.namespace + '_extra_table' ) ), + $headers = c.$headers.add( $( c.namespace + '_extra_headers' ) ), + theme = c.theme || 'jui', + themes = themesAll[theme] || {}, + remove = $.trim( [ themes.sortNone, themes.sortDesc, themes.sortAsc, themes.active ].join( ' ' ) ), + iconRmv = $.trim( [ themes.iconSortNone, themes.iconSortDesc, themes.iconSortAsc ].join( ' ' ) ); + if (c.debug) { time = new Date(); } + // initialization code - run once + if (!$table.hasClass('tablesorter-' + theme) || c.theme !== c.appliedTheme || !wo.uitheme_applied) { + wo.uitheme_applied = true; + oldtheme = themesAll[c.appliedTheme] || {}; + hasOldTheme = !$.isEmptyObject(oldtheme); + oldremove = hasOldTheme ? [ oldtheme.sortNone, oldtheme.sortDesc, oldtheme.sortAsc, oldtheme.active ].join( ' ' ) : ''; + oldIconRmv = hasOldTheme ? [ oldtheme.iconSortNone, oldtheme.iconSortDesc, oldtheme.iconSortAsc ].join( ' ' ) : ''; + if (hasOldTheme) { + wo.zebra[0] = $.trim( ' ' + wo.zebra[0].replace(' ' + oldtheme.even, '') ); + wo.zebra[1] = $.trim( ' ' + wo.zebra[1].replace(' ' + oldtheme.odd, '') ); + c.$tbodies.children().removeClass( [ oldtheme.even, oldtheme.odd ].join(' ') ); + } + // update zebra stripes + if (themes.even) { wo.zebra[0] += ' ' + themes.even; } + if (themes.odd) { wo.zebra[1] += ' ' + themes.odd; } + // add caption style + $table.children('caption') + .removeClass(oldtheme.caption || '') + .addClass(themes.caption); + // add table/footer class names + $tfoot = $table + // remove other selected themes + .removeClass( (c.appliedTheme ? 'tablesorter-' + (c.appliedTheme || '') : '') + ' ' + (oldtheme.table || '') ) + .addClass('tablesorter-' + theme + ' ' + (themes.table || '')) // add theme widget class name + .children('tfoot'); + c.appliedTheme = c.theme; + + if ($tfoot.length) { + $tfoot + // if oldtheme.footerRow or oldtheme.footerCells are undefined, all class names are removed + .children('tr').removeClass(oldtheme.footerRow || '').addClass(themes.footerRow) + .children('th, td').removeClass(oldtheme.footerCells || '').addClass(themes.footerCells); + } + // update header classes + $headers + .removeClass( (hasOldTheme ? [ oldtheme.header, oldtheme.hover, oldremove ].join(' ') : '') || '' ) + .addClass(themes.header) + .not('.sorter-false') + .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') + .bind('mouseenter.tsuitheme mouseleave.tsuitheme', function(event) { + // toggleClass with switch added in jQuery 1.3 + $(this)[ event.type === 'mouseenter' ? 'addClass' : 'removeClass' ](themes.hover || ''); + }); + + $headers.each(function(){ + var $this = $(this); + if (!$this.find('.' + ts.css.wrapper).length) { + // Firefox needs this inner div to position the icon & resizer correctly + $this.wrapInner('
    '); + } + }); + if (c.cssIcon) { + // if c.cssIcon is '', then no is added to the header + $headers + .find('.' + ts.css.icon) + .removeClass(hasOldTheme ? [ oldtheme.icons, oldIconRmv ].join(' ') : '') + .addClass(themes.icons || ''); + } + // filter widget initializes after uitheme + if (ts.hasWidget( c.table, 'filter' )) { + tmp = function() { + $table.children('thead').children('.' + ts.css.filterRow) + .removeClass(hasOldTheme ? oldtheme.filterRow || '' : '') + .addClass(themes.filterRow || ''); + }; + if (wo.filter_initialized) { + tmp(); + } else { + $table.one('filterInit', function() { + tmp(); + }); + } + } + } + for (i = 0; i < c.columns; i++) { + $header = c.$headers + .add($(c.namespace + '_extra_headers')) + .not('.sorter-false') + .filter('[data-column="' + i + '"]'); + $icon = (ts.css.icon) ? $header.find('.' + ts.css.icon) : $(); + $h = $headers.not('.sorter-false').filter('[data-column="' + i + '"]:last'); + if ($h.length) { + $header.removeClass(remove); + $icon.removeClass(iconRmv); + if ($h[0].sortDisabled) { + // no sort arrows for disabled columns! + $icon.removeClass(themes.icons || ''); + } else { + hdr = themes.sortNone; + icon = themes.iconSortNone; + if ($h.hasClass(ts.css.sortAsc)) { + hdr = [ themes.sortAsc, themes.active ].join(' '); + icon = themes.iconSortAsc; + } else if ($h.hasClass(ts.css.sortDesc)) { + hdr = [ themes.sortDesc, themes.active ].join(' '); + icon = themes.iconSortDesc; + } + $header.addClass(hdr); + $icon.addClass(icon || ''); + } + } + } + if (c.debug) { + console.log('Applying ' + theme + ' theme' + ts.benchmark(time)); + } + }, + remove: function(table, c, wo, refreshing) { + if (!wo.uitheme_applied) { return; } + var $table = c.$table, + theme = c.appliedTheme || 'jui', + themes = ts.themes[ theme ] || ts.themes.jui, + $headers = $table.children('thead').children(), + remove = themes.sortNone + ' ' + themes.sortDesc + ' ' + themes.sortAsc, + iconRmv = themes.iconSortNone + ' ' + themes.iconSortDesc + ' ' + themes.iconSortAsc; + $table.removeClass('tablesorter-' + theme + ' ' + themes.table); + wo.uitheme_applied = false; + if (refreshing) { return; } + $table.find(ts.css.header).removeClass(themes.header); + $headers + .unbind('mouseenter.tsuitheme mouseleave.tsuitheme') // remove hover + .removeClass(themes.hover + ' ' + remove + ' ' + themes.active) + .filter('.' + ts.css.filterRow) + .removeClass(themes.filterRow); + $headers.find('.' + ts.css.icon).removeClass(themes.icons + ' ' + iconRmv); + } + }); + +})(jQuery); + +/*! Widget: columns - updated 5/24/2017 (v2.28.11) */ +;(function ($) { + 'use strict'; + var ts = $.tablesorter || {}; + + ts.addWidget({ + id: 'columns', + priority: 65, + options : { + columns : [ 'primary', 'secondary', 'tertiary' ] + }, + format: function(table, c, wo) { + var $tbody, tbodyIndex, $rows, rows, $row, $cells, remove, indx, + $table = c.$table, + $tbodies = c.$tbodies, + sortList = c.sortList, + len = sortList.length, + // removed c.widgetColumns support + css = wo && wo.columns || [ 'primary', 'secondary', 'tertiary' ], + last = css.length - 1; + remove = css.join(' '); + // check if there is a sort (on initialization there may not be one) + for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // detach tbody + $rows = $tbody.children('tr'); + // loop through the visible rows + $rows.each(function() { + $row = $(this); + if (this.style.display !== 'none') { + // remove all columns class names + $cells = $row.children().removeClass(remove); + // add appropriate column class names + if (sortList && sortList[0]) { + // primary sort column class + $cells.eq(sortList[0][0]).addClass(css[0]); + if (len > 1) { + for (indx = 1; indx < len; indx++) { + // secondary, tertiary, etc sort column classes + $cells.eq(sortList[indx][0]).addClass( css[indx] || css[last] ); + } + } + } + } + }); + ts.processTbody(table, $tbody, false); + } + // add classes to thead and tfoot + rows = wo.columns_thead !== false ? [ 'thead tr' ] : []; + if (wo.columns_tfoot !== false) { + rows.push('tfoot tr'); + } + if (rows.length) { + $rows = $table.find( rows.join(',') ).children().removeClass(remove); + if (len) { + for (indx = 0; indx < len; indx++) { + // add primary. secondary, tertiary, etc sort column classes + $rows.filter('[data-column="' + sortList[indx][0] + '"]').addClass(css[indx] || css[last]); + } + } + } + }, + remove: function(table, c, wo) { + var tbodyIndex, $tbody, + $tbodies = c.$tbodies, + remove = (wo.columns || [ 'primary', 'secondary', 'tertiary' ]).join(' '); + c.$headers.removeClass(remove); + c.$table.children('tfoot').children('tr').children('th, td').removeClass(remove); + for (tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody(table, $tbodies.eq(tbodyIndex), true); // remove tbody + $tbody.children('tr').each(function() { + $(this).children().removeClass(remove); + }); + ts.processTbody(table, $tbody, false); // restore tbody + } + } + }); + +})(jQuery); + +/*! Widget: filter - updated 7/4/2017 (v2.28.15) *//* + * Requires tablesorter v2.8+ and jQuery 1.7+ + * by Rob Garrison + */ +;( function ( $ ) { + 'use strict'; + var tsf, tsfRegex, + ts = $.tablesorter || {}, + tscss = ts.css, + tskeyCodes = ts.keyCodes; + + $.extend( tscss, { + filterRow : 'tablesorter-filter-row', + filter : 'tablesorter-filter', + filterDisabled : 'disabled', + filterRowHide : 'hideme' + }); + + $.extend( tskeyCodes, { + backSpace : 8, + escape : 27, + space : 32, + left : 37, + down : 40 + }); + + ts.addWidget({ + id: 'filter', + priority: 50, + options : { + filter_cellFilter : '', // css class name added to the filter cell ( string or array ) + filter_childRows : false, // if true, filter includes child row content in the search + filter_childByColumn : false, // ( filter_childRows must be true ) if true = search child rows by column; false = search all child row text grouped + filter_childWithSibs : true, // if true, include matching child row siblings + filter_columnAnyMatch: true, // if true, allows using '#:{query}' in AnyMatch searches ( column:query ) + filter_columnFilters : true, // if true, a filter will be added to the top of each table column + filter_cssFilter : '', // css class name added to the filter row & each input in the row ( tablesorter-filter is ALWAYS added ) + filter_defaultAttrib : 'data-value', // data attribute in the header cell that contains the default filter value + filter_defaultFilter : {}, // add a default column filter type '~{query}' to make fuzzy searches default; '{q1} AND {q2}' to make all searches use a logical AND. + filter_excludeFilter : {}, // filters to exclude, per column + filter_external : '', // jQuery selector string ( or jQuery object ) of external filters + filter_filteredRow : 'filtered', // class added to filtered rows; define in css with "display:none" to hide the filtered-out rows + filter_formatter : null, // add custom filter elements to the filter row + filter_functions : null, // add custom filter functions using this option + filter_hideEmpty : true, // hide filter row when table is empty + filter_hideFilters : false, // collapse filter row when mouse leaves the area + filter_ignoreCase : true, // if true, make all searches case-insensitive + filter_liveSearch : true, // if true, search column content while the user types ( with a delay ) + filter_matchType : { 'input': 'exact', 'select': 'exact' }, // global query settings ('exact' or 'match'); overridden by "filter-match" or "filter-exact" class + filter_onlyAvail : 'filter-onlyAvail', // a header with a select dropdown & this class name will only show available ( visible ) options within the drop down + filter_placeholder : { search : '', select : '' }, // default placeholder text ( overridden by any header 'data-placeholder' setting ) + filter_reset : null, // jQuery selector string of an element used to reset the filters + filter_resetOnEsc : true, // Reset filter input when the user presses escape - normalized across browsers + filter_saveFilters : false, // Use the $.tablesorter.storage utility to save the most recent filters + filter_searchDelay : 300, // typing delay in milliseconds before starting a search + filter_searchFiltered: true, // allow searching through already filtered rows in special circumstances; will speed up searching in large tables if true + filter_selectSource : null, // include a function to return an array of values to be added to the column filter select + filter_selectSourceSeparator : '|', // filter_selectSource array text left of the separator is added to the option value, right into the option text + filter_serversideFiltering : false, // if true, must perform server-side filtering b/c client-side filtering is disabled, but the ui and events will still be used. + filter_startsWith : false, // if true, filter start from the beginning of the cell contents + filter_useParsedData : false // filter all data using parsed content + }, + format: function( table, c, wo ) { + if ( !c.$table.hasClass( 'hasFilters' ) ) { + tsf.init( table, c, wo ); + } + }, + remove: function( table, c, wo, refreshing ) { + var tbodyIndex, $tbody, + $table = c.$table, + $tbodies = c.$tbodies, + events = ( + 'addRows updateCell update updateRows updateComplete appendCache filterReset ' + + 'filterAndSortReset filterFomatterUpdate filterEnd search stickyHeadersInit ' + ).split( ' ' ).join( c.namespace + 'filter ' ); + $table + .removeClass( 'hasFilters' ) + // add filter namespace to all BUT search + .unbind( events.replace( ts.regex.spaces, ' ' ) ) + // remove the filter row even if refreshing, because the column might have been moved + .find( '.' + tscss.filterRow ).remove(); + wo.filter_initialized = false; + if ( refreshing ) { return; } + for ( tbodyIndex = 0; tbodyIndex < $tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( table, $tbodies.eq( tbodyIndex ), true ); // remove tbody + $tbody.children().removeClass( wo.filter_filteredRow ).show(); + ts.processTbody( table, $tbody, false ); // restore tbody + } + if ( wo.filter_reset ) { + $( document ).undelegate( wo.filter_reset, 'click' + c.namespace + 'filter' ); + } + } + }); + + tsf = ts.filter = { + + // regex used in filter 'check' functions - not for general use and not documented + regex: { + regex : /^\/((?:\\\/|[^\/])+)\/([migyu]{0,5})?$/, // regex to test for regex + child : /tablesorter-childRow/, // child row class name; this gets updated in the script + filtered : /filtered/, // filtered (hidden) row class name; updated in the script + type : /undefined|number/, // check type + exact : /(^[\"\'=]+)|([\"\'=]+$)/g, // exact match (allow '==') + operators : /[<>=]/g, // replace operators + query : '(q|query)', // replace filter queries + wild01 : /\?/g, // wild card match 0 or 1 + wild0More : /\*/g, // wild care match 0 or more + quote : /\"/g, + isNeg1 : /(>=?\s*-\d)/, + isNeg2 : /(<=?\s*\d)/ + }, + // function( c, data ) { } + // c = table.config + // data.$row = jQuery object of the row currently being processed + // data.$cells = jQuery object of all cells within the current row + // data.filters = array of filters for all columns ( some may be undefined ) + // data.filter = filter for the current column + // data.iFilter = same as data.filter, except lowercase ( if wo.filter_ignoreCase is true ) + // data.exact = table cell text ( or parsed data if column parser enabled; may be a number & not a string ) + // data.iExact = same as data.exact, except lowercase ( if wo.filter_ignoreCase is true; may be a number & not a string ) + // data.cache = table cell text from cache, so it has been parsed ( & in all lower case if c.ignoreCase is true ) + // data.cacheArray = An array of parsed content from each table cell in the row being processed + // data.index = column index; table = table element ( DOM ) + // data.parsed = array ( by column ) of boolean values ( from filter_useParsedData or 'filter-parsed' class ) + types: { + or : function( c, data, vars ) { + // look for "|", but not if it is inside of a regular expression + if ( ( tsfRegex.orTest.test( data.iFilter ) || tsfRegex.orSplit.test( data.filter ) ) && + // this test for regex has potential to slow down the overall search + !tsfRegex.regex.test( data.filter ) ) { + var indx, filterMatched, query, regex, + // duplicate data but split filter + data2 = $.extend( {}, data ), + filter = data.filter.split( tsfRegex.orSplit ), + iFilter = data.iFilter.split( tsfRegex.orSplit ), + len = filter.length; + for ( indx = 0; indx < len; indx++ ) { + data2.nestedFilters = true; + data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); + data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); + query = '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')'; + try { + // use try/catch, because query may not be a valid regex if "|" is contained within a partial regex search, + // e.g "/(Alex|Aar" -> Uncaught SyntaxError: Invalid regular expression: /(/(Alex)/: Unterminated group + regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); + // filterMatched = data2.filter === '' && indx > 0 ? true + // look for an exact match with the 'or' unless the 'filter-match' class is found + filterMatched = regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ); + if ( filterMatched ) { + return filterMatched; + } + } catch ( error ) { + return null; + } + } + // may be null from processing types + return filterMatched || false; + } + return null; + }, + // Look for an AND or && operator ( logical and ) + and : function( c, data, vars ) { + if ( tsfRegex.andTest.test( data.filter ) ) { + var indx, filterMatched, result, query, regex, + // duplicate data but split filter + data2 = $.extend( {}, data ), + filter = data.filter.split( tsfRegex.andSplit ), + iFilter = data.iFilter.split( tsfRegex.andSplit ), + len = filter.length; + for ( indx = 0; indx < len; indx++ ) { + data2.nestedFilters = true; + data2.filter = '' + ( tsf.parseFilter( c, filter[ indx ], data ) || '' ); + data2.iFilter = '' + ( tsf.parseFilter( c, iFilter[ indx ], data ) || '' ); + query = ( '(' + ( tsf.parseFilter( c, data2.filter, data ) || '' ) + ')' ) + // replace wild cards since /(a*)/i will match anything + .replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ); + try { + // use try/catch just in case RegExp is invalid + regex = new RegExp( data.isMatch ? query : '^' + query + '$', c.widgetOptions.filter_ignoreCase ? 'i' : '' ); + // look for an exact match with the 'and' unless the 'filter-match' class is found + result = ( regex.test( data2.exact ) || tsf.processTypes( c, data2, vars ) ); + if ( indx === 0 ) { + filterMatched = result; + } else { + filterMatched = filterMatched && result; + } + } catch ( error ) { + return null; + } + } + // may be null from processing types + return filterMatched || false; + } + return null; + }, + // Look for regex + regex: function( c, data ) { + if ( tsfRegex.regex.test( data.filter ) ) { + var matches, + // cache regex per column for optimal speed + regex = data.filter_regexCache[ data.index ] || tsfRegex.regex.exec( data.filter ), + isRegex = regex instanceof RegExp; + try { + if ( !isRegex ) { + // force case insensitive search if ignoreCase option set? + // if ( c.ignoreCase && !regex[2] ) { regex[2] = 'i'; } + data.filter_regexCache[ data.index ] = regex = new RegExp( regex[1], regex[2] ); + } + matches = regex.test( data.exact ); + } catch ( error ) { + matches = false; + } + return matches; + } + return null; + }, + // Look for operators >, >=, < or <= + operators: function( c, data ) { + // ignore empty strings... because '' < 10 is true + if ( tsfRegex.operTest.test( data.iFilter ) && data.iExact !== '' ) { + var cachedValue, result, txt, + table = c.table, + parsed = data.parsed[ data.index ], + query = ts.formatFloat( data.iFilter.replace( tsfRegex.operators, '' ), table ), + parser = c.parsers[ data.index ] || {}, + savedSearch = query; + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || parser.type === 'numeric' ) { + txt = $.trim( '' + data.iFilter.replace( tsfRegex.operators, '' ) ); + result = tsf.parseFilter( c, txt, data, true ); + query = ( typeof result === 'number' && result !== '' && !isNaN( result ) ) ? result : query; + } + // iExact may be numeric - see issue #149; + // check if cached is defined, because sometimes j goes out of range? ( numeric columns ) + if ( ( parsed || parser.type === 'numeric' ) && !isNaN( query ) && + typeof data.cache !== 'undefined' ) { + cachedValue = data.cache; + } else { + txt = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + cachedValue = ts.formatFloat( txt, table ); + } + if ( tsfRegex.gtTest.test( data.iFilter ) ) { + result = tsfRegex.gteTest.test( data.iFilter ) ? cachedValue >= query : cachedValue > query; + } else if ( tsfRegex.ltTest.test( data.iFilter ) ) { + result = tsfRegex.lteTest.test( data.iFilter ) ? cachedValue <= query : cachedValue < query; + } + // keep showing all rows if nothing follows the operator + if ( !result && savedSearch === '' ) { + result = true; + } + return result; + } + return null; + }, + // Look for a not match + notMatch: function( c, data ) { + if ( tsfRegex.notTest.test( data.iFilter ) ) { + var indx, + txt = data.iFilter.replace( '!', '' ), + filter = tsf.parseFilter( c, txt, data ) || ''; + if ( tsfRegex.exact.test( filter ) ) { + // look for exact not matches - see #628 + filter = filter.replace( tsfRegex.exact, '' ); + return filter === '' ? true : $.trim( filter ) !== data.iExact; + } else { + indx = data.iExact.search( $.trim( filter ) ); + return filter === '' ? true : + // return true if not found + data.anyMatch ? indx < 0 : + // return false if found + !( c.widgetOptions.filter_startsWith ? indx === 0 : indx >= 0 ); + } + } + return null; + }, + // Look for quotes or equals to get an exact match; ignore type since iExact could be numeric + exact: function( c, data ) { + /*jshint eqeqeq:false */ + if ( tsfRegex.exact.test( data.iFilter ) ) { + var txt = data.iFilter.replace( tsfRegex.exact, '' ), + filter = tsf.parseFilter( c, txt, data ) || ''; + return data.anyMatch ? $.inArray( filter, data.rowArray ) >= 0 : filter == data.iExact; + } + return null; + }, + // Look for a range ( using ' to ' or ' - ' ) - see issue #166; thanks matzhu! + range : function( c, data ) { + if ( tsfRegex.toTest.test( data.iFilter ) ) { + var result, tmp, range1, range2, + table = c.table, + index = data.index, + parsed = data.parsed[index], + // make sure the dash is for a range and not indicating a negative number + query = data.iFilter.split( tsfRegex.toSplit ); + + tmp = query[0].replace( ts.regex.nondigit, '' ) || ''; + range1 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + tmp = query[1].replace( ts.regex.nondigit, '' ) || ''; + range2 = ts.formatFloat( tsf.parseFilter( c, tmp, data ), table ); + // parse filter value in case we're comparing numbers ( dates ) + if ( parsed || c.parsers[ index ].type === 'numeric' ) { + result = c.parsers[ index ].format( '' + query[0], table, c.$headers.eq( index ), index ); + range1 = ( result !== '' && !isNaN( result ) ) ? result : range1; + result = c.parsers[ index ].format( '' + query[1], table, c.$headers.eq( index ), index ); + range2 = ( result !== '' && !isNaN( result ) ) ? result : range2; + } + if ( ( parsed || c.parsers[ index ].type === 'numeric' ) && !isNaN( range1 ) && !isNaN( range2 ) ) { + result = data.cache; + } else { + tmp = isNaN( data.iExact ) ? data.iExact.replace( ts.regex.nondigit, '' ) : data.iExact; + result = ts.formatFloat( tmp, table ); + } + if ( range1 > range2 ) { + tmp = range1; range1 = range2; range2 = tmp; // swap + } + return ( result >= range1 && result <= range2 ) || ( range1 === '' || range2 === '' ); + } + return null; + }, + // Look for wild card: ? = single, * = multiple, or | = logical OR + wild : function( c, data ) { + if ( tsfRegex.wildOrTest.test( data.iFilter ) ) { + var query = '' + ( tsf.parseFilter( c, data.iFilter, data ) || '' ); + // look for an exact match with the 'or' unless the 'filter-match' class is found + if ( !tsfRegex.wildTest.test( query ) && data.nestedFilters ) { + query = data.isMatch ? query : '^(' + query + ')$'; + } + // parsing the filter may not work properly when using wildcards =/ + try { + return new RegExp( + query.replace( tsfRegex.wild01, '\\S{1}' ).replace( tsfRegex.wild0More, '\\S*' ), + c.widgetOptions.filter_ignoreCase ? 'i' : '' + ) + .test( data.exact ); + } catch ( error ) { + return null; + } + } + return null; + }, + // fuzzy text search; modified from https://github.com/mattyork/fuzzy ( MIT license ) + fuzzy: function( c, data ) { + if ( tsfRegex.fuzzyTest.test( data.iFilter ) ) { + var indx, + patternIndx = 0, + len = data.iExact.length, + txt = data.iFilter.slice( 1 ), + pattern = tsf.parseFilter( c, txt, data ) || ''; + for ( indx = 0; indx < len; indx++ ) { + if ( data.iExact[ indx ] === pattern[ patternIndx ] ) { + patternIndx += 1; + } + } + return patternIndx === pattern.length; + } + return null; + } + }, + init: function( table ) { + // filter language options + ts.language = $.extend( true, {}, { + to : 'to', + or : 'or', + and : 'and' + }, ts.language ); + + var options, string, txt, $header, column, val, fxn, noSelect, + c = table.config, + wo = c.widgetOptions; + c.$table.addClass( 'hasFilters' ); + c.lastSearch = []; + + // define timers so using clearTimeout won't cause an undefined error + wo.filter_searchTimer = null; + wo.filter_initTimer = null; + wo.filter_formatterCount = 0; + wo.filter_formatterInit = []; + wo.filter_anyColumnSelector = '[data-column="all"],[data-column="any"]'; + wo.filter_multipleColumnSelector = '[data-column*="-"],[data-column*=","]'; + + val = '\\{' + tsfRegex.query + '\\}'; + $.extend( tsfRegex, { + child : new RegExp( c.cssChildRow ), + filtered : new RegExp( wo.filter_filteredRow ), + alreadyFiltered : new RegExp( '(\\s+(' + ts.language.or + '|-|' + ts.language.to + ')\\s+)', 'i' ), + toTest : new RegExp( '\\s+(-|' + ts.language.to + ')\\s+', 'i' ), + toSplit : new RegExp( '(?:\\s+(?:-|' + ts.language.to + ')\\s+)', 'gi' ), + andTest : new RegExp( '\\s+(' + ts.language.and + '|&&)\\s+', 'i' ), + andSplit : new RegExp( '(?:\\s+(?:' + ts.language.and + '|&&)\\s+)', 'gi' ), + orTest : new RegExp( '(\\||\\s+' + ts.language.or + '\\s+)', 'i' ), + orSplit : new RegExp( '(?:\\s+(?:' + ts.language.or + ')\\s+|\\|)', 'gi' ), + iQuery : new RegExp( val, 'i' ), + igQuery : new RegExp( val, 'ig' ), + operTest : /^[<>]=?/, + gtTest : />/, + gteTest : />=/, + ltTest : /' + + ( $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.select || + '' + ) + + '' : ''; + val = string; + txt = string; + if ( string.indexOf( wo.filter_selectSourceSeparator ) >= 0 ) { + val = string.split( wo.filter_selectSourceSeparator ); + txt = val[1]; + val = val[0]; + } + options += ''; + } + } + c.$table + .find( 'thead' ) + .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) + .append( options ); + txt = wo.filter_selectSource; + fxn = typeof txt === 'function' ? true : ts.getColumnData( table, txt, column ); + if ( fxn ) { + // updating so the extra options are appended + tsf.buildSelect( c.table, column, '', true, $header.hasClass( wo.filter_onlyAvail ) ); + } + } + } + } + } + // not really updating, but if the column has both the 'filter-select' class & + // filter_functions set to true, it would append the same options twice. + tsf.buildDefault( table, true ); + + tsf.bindSearch( table, c.$table.find( '.' + tscss.filter ), true ); + if ( wo.filter_external ) { + tsf.bindSearch( table, wo.filter_external ); + } + + if ( wo.filter_hideFilters ) { + tsf.hideFilters( c ); + } + + // show processing icon + if ( c.showProcessing ) { + txt = 'filterStart filterEnd '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table + .unbind( txt.replace( ts.regex.spaces, ' ' ) ) + .bind( txt, function( event, columns ) { + // only add processing to certain columns to all columns + $header = ( columns ) ? + c.$table + .find( '.' + tscss.header ) + .filter( '[data-column]' ) + .filter( function() { + return columns[ $( this ).data( 'column' ) ] !== ''; + }) : ''; + ts.isProcessing( table, event.type === 'filterStart', columns ? $header : '' ); + }); + } + + // set filtered rows count ( intially unfiltered ) + c.filteredRows = c.totalRows; + + // add default values + txt = 'tablesorter-initialized pagerBeforeInitialized '.split( ' ' ).join( c.namespace + 'filter ' ); + c.$table + .unbind( txt.replace( ts.regex.spaces, ' ' ) ) + .bind( txt, function() { + tsf.completeInit( this ); + }); + // if filter widget is added after pager has initialized; then set filter init flag + if ( c.pager && c.pager.initialized && !wo.filter_initialized ) { + c.$table.triggerHandler( 'filterFomatterUpdate' ); + setTimeout( function() { + tsf.filterInitComplete( c ); + }, 100 ); + } else if ( !wo.filter_initialized ) { + tsf.completeInit( table ); + } + }, + completeInit: function( table ) { + // redefine 'c' & 'wo' so they update properly inside this callback + var c = table.config, + wo = c.widgetOptions, + filters = tsf.setDefaults( table, c, wo ) || []; + if ( filters.length ) { + // prevent delayInit from triggering a cache build if filters are empty + if ( !( c.delayInit && filters.join( '' ) === '' ) ) { + ts.setFilters( table, filters, true ); + } + } + c.$table.triggerHandler( 'filterFomatterUpdate' ); + // trigger init after setTimeout to prevent multiple filterStart/End/Init triggers + setTimeout( function() { + if ( !wo.filter_initialized ) { + tsf.filterInitComplete( c ); + } + }, 100 ); + }, + + // $cell parameter, but not the config, is passed to the filter_formatters, + // so we have to work with it instead + formatterUpdated: function( $cell, column ) { + // prevent error if $cell is undefined - see #1056 + var $table = $cell && $cell.closest( 'table' ); + var config = $table.length && $table[0].config, + wo = config && config.widgetOptions; + if ( wo && !wo.filter_initialized ) { + // add updates by column since this function + // may be called numerous times before initialization + wo.filter_formatterInit[ column ] = 1; + } + }, + filterInitComplete: function( c ) { + var indx, len, + wo = c.widgetOptions, + count = 0, + completed = function() { + wo.filter_initialized = true; + // update lastSearch - it gets cleared often + c.lastSearch = c.$table.data( 'lastSearch' ); + c.$table.triggerHandler( 'filterInit', c ); + tsf.findRows( c.table, c.lastSearch || [] ); + }; + if ( $.isEmptyObject( wo.filter_formatter ) ) { + completed(); + } else { + len = wo.filter_formatterInit.length; + for ( indx = 0; indx < len; indx++ ) { + if ( wo.filter_formatterInit[ indx ] === 1 ) { + count++; + } + } + clearTimeout( wo.filter_initTimer ); + if ( !wo.filter_initialized && count === wo.filter_formatterCount ) { + // filter widget initialized + completed(); + } else if ( !wo.filter_initialized ) { + // fall back in case a filter_formatter doesn't call + // $.tablesorter.filter.formatterUpdated( $cell, column ), and the count is off + wo.filter_initTimer = setTimeout( function() { + completed(); + }, 500 ); + } + } + }, + // encode or decode filters for storage; see #1026 + processFilters: function( filters, encode ) { + var indx, + // fixes #1237; previously returning an encoded "filters" value + result = [], + mode = encode ? encodeURIComponent : decodeURIComponent, + len = filters.length; + for ( indx = 0; indx < len; indx++ ) { + if ( filters[ indx ] ) { + result[ indx ] = mode( filters[ indx ] ); + } + } + return result; + }, + setDefaults: function( table, c, wo ) { + var isArray, saved, indx, col, $filters, + // get current ( default ) filters + filters = ts.getFilters( table ) || []; + if ( wo.filter_saveFilters && ts.storage ) { + saved = ts.storage( table, 'tablesorter-filters' ) || []; + isArray = $.isArray( saved ); + // make sure we're not just getting an empty array + if ( !( isArray && saved.join( '' ) === '' || !isArray ) ) { + filters = tsf.processFilters( saved ); + } + } + // if no filters saved, then check default settings + if ( filters.join( '' ) === '' ) { + // allow adding default setting to external filters + $filters = c.$headers.add( wo.filter_$externalFilters ) + .filter( '[' + wo.filter_defaultAttrib + ']' ); + for ( indx = 0; indx <= c.columns; indx++ ) { + // include data-column='all' external filters + col = indx === c.columns ? 'all' : indx; + filters[ indx ] = $filters + .filter( '[data-column="' + col + '"]' ) + .attr( wo.filter_defaultAttrib ) || filters[indx] || ''; + } + } + c.$table.data( 'lastSearch', filters ); + return filters; + }, + parseFilter: function( c, filter, data, parsed ) { + return parsed || data.parsed[ data.index ] ? + c.parsers[ data.index ].format( filter, c.table, [], data.index ) : + filter; + }, + buildRow: function( table, c, wo ) { + var $filter, col, column, $header, makeSelect, disabled, name, ffxn, tmp, + // c.columns defined in computeThIndexes() + cellFilter = wo.filter_cellFilter, + columns = c.columns, + arry = $.isArray( cellFilter ), + buildFilter = '
    '; + for ( column = 0; column < columns; column++ ) { + if ( c.$headerIndexed[ column ].length ) { + // account for entire column set with colspan. See #1047 + tmp = c.$headerIndexed[ column ] && c.$headerIndexed[ column ][0].colSpan || 0; + if ( tmp > 1 ) { + buildFilter += '
    Article
    ' ).appendTo( $filter ); + } else { + ffxn = ts.getColumnData( table, wo.filter_formatter, column ); + if ( ffxn ) { + wo.filter_formatterCount++; + buildFilter = ffxn( $filter, column ); + // no element returned, so lets go find it + if ( buildFilter && buildFilter.length === 0 ) { + buildFilter = $filter.children( 'input' ); + } + // element not in DOM, so lets attach it + if ( buildFilter && ( buildFilter.parent().length === 0 || + ( buildFilter.parent().length && buildFilter.parent()[0] !== $filter[0] ) ) ) { + $filter.append( buildFilter ); + } + } else { + buildFilter = $( '' ).appendTo( $filter ); + } + if ( buildFilter ) { + tmp = $header.data( 'placeholder' ) || + $header.attr( 'data-placeholder' ) || + wo.filter_placeholder.search || ''; + buildFilter.attr( 'placeholder', tmp ); + } + } + if ( buildFilter ) { + // add filter class name + name = ( $.isArray( wo.filter_cssFilter ) ? + ( typeof wo.filter_cssFilter[column] !== 'undefined' ? wo.filter_cssFilter[column] || '' : '' ) : + wo.filter_cssFilter ) || ''; + // copy data-column from table cell (it will include colspan) + buildFilter.addClass( tscss.filter + ' ' + name ).attr( 'data-column', $filter.attr( 'data-column' ) ); + if ( disabled ) { + buildFilter.attr( 'placeholder', '' ).addClass( tscss.filterDisabled )[0].disabled = true; + } + } + } + } + }, + bindSearch: function( table, $el, internal ) { + table = $( table )[0]; + $el = $( $el ); // allow passing a selector string + if ( !$el.length ) { return; } + var tmp, + c = table.config, + wo = c.widgetOptions, + namespace = c.namespace + 'filter', + $ext = wo.filter_$externalFilters; + if ( internal !== true ) { + // save anyMatch element + tmp = wo.filter_anyColumnSelector + ',' + wo.filter_multipleColumnSelector; + wo.filter_$anyMatch = $el.filter( tmp ); + if ( $ext && $ext.length ) { + wo.filter_$externalFilters = wo.filter_$externalFilters.add( $el ); + } else { + wo.filter_$externalFilters = $el; + } + // update values ( external filters added after table initialization ) + ts.setFilters( table, c.$table.data( 'lastSearch' ) || [], internal === false ); + } + // unbind events + tmp = ( 'keypress keyup keydown search change input '.split( ' ' ).join( namespace + ' ' ) ); + $el + // use data attribute instead of jQuery data since the head is cloned without including + // the data/binding + .attr( 'data-lastSearchTime', new Date().getTime() ) + .unbind( tmp.replace( ts.regex.spaces, ' ' ) ) + .bind( 'keydown' + namespace, function( event ) { + if ( event.which === tskeyCodes.escape && !table.config.widgetOptions.filter_resetOnEsc ) { + // prevent keypress event + return false; + } + }) + .bind( 'keyup' + namespace, function( event ) { + wo = table.config.widgetOptions; // make sure "wo" isn't cached + var column = parseInt( $( this ).attr( 'data-column' ), 10 ), + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? wo.filter_liveSearch : + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( typeof liveSearch === 'undefined' ) { + liveSearch = wo.filter_liveSearch.fallback || false; + } + $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); + // emulate what webkit does.... escape clears the filter + if ( event.which === tskeyCodes.escape ) { + // make sure to restore the last value on escape + this.value = wo.filter_resetOnEsc ? '' : c.lastSearch[column]; + // don't return if the search value is empty ( all rows need to be revealed ) + } else if ( this.value !== '' && ( + // liveSearch can contain a min value length; ignore arrow and meta keys, but allow backspace + ( typeof liveSearch === 'number' && this.value.length < liveSearch ) || + // let return & backspace continue on, but ignore arrows & non-valid characters + ( event.which !== tskeyCodes.enter && event.which !== tskeyCodes.backSpace && + ( event.which < tskeyCodes.space || ( event.which >= tskeyCodes.left && event.which <= tskeyCodes.down ) ) ) ) ) { + return; + // live search + } else if ( liveSearch === false ) { + if ( this.value !== '' && event.which !== tskeyCodes.enter ) { + return; + } + } + // change event = no delay; last true flag tells getFilters to skip newest timed input + tsf.searching( table, true, true, column ); + }) + // include change for select - fixes #473 + .bind( 'search change keypress input blur '.split( ' ' ).join( namespace + ' ' ), function( event ) { + // don't get cached data, in case data-column changes dynamically + var column = parseInt( $( this ).attr( 'data-column' ), 10 ), + eventType = event.type, + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? + wo.filter_liveSearch : + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( table.config.widgetOptions.filter_initialized && + // immediate search if user presses enter + ( event.which === tskeyCodes.enter || + // immediate search if a "search" or "blur" is triggered on the input + ( eventType === 'search' || eventType === 'blur' ) || + // change & input events must be ignored if liveSearch !== true + ( eventType === 'change' || eventType === 'input' ) && + // prevent search if liveSearch is a number + ( liveSearch === true || liveSearch !== true && event.target.nodeName !== 'INPUT' ) && + // don't allow 'change' or 'input' event to process if the input value + // is the same - fixes #685 + this.value !== c.lastSearch[column] + ) + ) { + event.preventDefault(); + // init search with no delay + $( this ).attr( 'data-lastSearchTime', new Date().getTime() ); + tsf.searching( table, eventType !== 'keypress', true, column ); + } + }); + }, + searching: function( table, filter, skipFirst, column ) { + var liveSearch, + wo = table.config.widgetOptions; + if (typeof column === 'undefined') { + // no delay + liveSearch = false; + } else { + liveSearch = typeof wo.filter_liveSearch === 'boolean' ? + wo.filter_liveSearch : + // get column setting, or set to fallback value, or default to false + ts.getColumnData( table, wo.filter_liveSearch, column ); + if ( typeof liveSearch === 'undefined' ) { + liveSearch = wo.filter_liveSearch.fallback || false; + } + } + clearTimeout( wo.filter_searchTimer ); + if ( typeof filter === 'undefined' || filter === true ) { + // delay filtering + wo.filter_searchTimer = setTimeout( function() { + tsf.checkFilters( table, filter, skipFirst ); + }, liveSearch ? wo.filter_searchDelay : 10 ); + } else { + // skip delay + tsf.checkFilters( table, filter, skipFirst ); + } + }, + equalFilters: function (c, filter1, filter2) { + var indx, + f1 = [], + f2 = [], + len = c.columns + 1; // add one to include anyMatch filter + filter1 = $.isArray(filter1) ? filter1 : []; + filter2 = $.isArray(filter2) ? filter2 : []; + for (indx = 0; indx < len; indx++) { + f1[indx] = filter1[indx] || ''; + f2[indx] = filter2[indx] || ''; + } + return f1.join(',') === f2.join(','); + }, + checkFilters: function( table, filter, skipFirst ) { + var c = table.config, + wo = c.widgetOptions, + filterArray = $.isArray( filter ), + filters = ( filterArray ) ? filter : ts.getFilters( table, true ), + currentFilters = filters || []; // current filter values + // prevent errors if delay init is set + if ( $.isEmptyObject( c.cache ) ) { + // update cache if delayInit set & pager has initialized ( after user initiates a search ) + if ( c.delayInit && ( !c.pager || c.pager && c.pager.initialized ) ) { + ts.updateCache( c, function() { + tsf.checkFilters( table, false, skipFirst ); + }); + } + return; + } + // add filter array back into inputs + if ( filterArray ) { + ts.setFilters( table, filters, false, skipFirst !== true ); + if ( !wo.filter_initialized ) { + c.lastSearch = []; + c.lastCombinedFilter = ''; + } + } + if ( wo.filter_hideFilters ) { + // show/hide filter row as needed + c.$table + .find( '.' + tscss.filterRow ) + .triggerHandler( tsf.hideFiltersCheck( c ) ? 'mouseleave' : 'mouseenter' ); + } + // return if the last search is the same; but filter === false when updating the search + // see example-widget-filter.html filter toggle buttons + if ( tsf.equalFilters(c, c.lastSearch, currentFilters) && filter !== false ) { + return; + } else if ( filter === false ) { + // force filter refresh + c.lastCombinedFilter = ''; + c.lastSearch = []; + } + // define filter inside it is false + filters = filters || []; + // convert filters to strings - see #1070 + filters = Array.prototype.map ? + filters.map( String ) : + // for IE8 & older browsers - maybe not the best method + filters.join( '\ufffd' ).split( '\ufffd' ); + + if ( wo.filter_initialized ) { + c.$table.triggerHandler( 'filterStart', [ filters ] ); + } + if ( c.showProcessing ) { + // give it time for the processing icon to kick in + setTimeout( function() { + tsf.findRows( table, filters, currentFilters ); + return false; + }, 30 ); + } else { + tsf.findRows( table, filters, currentFilters ); + return false; + } + }, + hideFiltersCheck: function( c ) { + if (typeof c.widgetOptions.filter_hideFilters === 'function') { + var val = c.widgetOptions.filter_hideFilters( c ); + if (typeof val === 'boolean') { + return val; + } + } + return ts.getFilters( c.$table ).join( '' ) === ''; + }, + hideFilters: function( c, $table ) { + var timer; + ( $table || c.$table ) + .find( '.' + tscss.filterRow ) + .addClass( tscss.filterRowHide ) + .bind( 'mouseenter mouseleave', function( e ) { + // save event object - http://bugs.jquery.com/ticket/12140 + var event = e, + $row = $( this ); + clearTimeout( timer ); + timer = setTimeout( function() { + if ( /enter|over/.test( event.type ) ) { + $row.removeClass( tscss.filterRowHide ); + } else { + // don't hide if input has focus + // $( ':focus' ) needs jQuery 1.6+ + if ( $( document.activeElement ).closest( 'tr' )[0] !== $row[0] ) { + // don't hide row if any filter has a value + $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) ); + } + } + }, 200 ); + }) + .find( 'input, select' ).bind( 'focus blur', function( e ) { + var event = e, + $row = $( this ).closest( 'tr' ); + clearTimeout( timer ); + timer = setTimeout( function() { + clearTimeout( timer ); + // don't hide row if any filter has a value + $row.toggleClass( tscss.filterRowHide, tsf.hideFiltersCheck( c ) && event.type !== 'focus' ); + }, 200 ); + }); + }, + defaultFilter: function( filter, mask ) { + if ( filter === '' ) { return filter; } + var regex = tsfRegex.iQuery, + maskLen = mask.match( tsfRegex.igQuery ).length, + query = maskLen > 1 ? $.trim( filter ).split( /\s/ ) : [ $.trim( filter ) ], + len = query.length - 1, + indx = 0, + val = mask; + if ( len < 1 && maskLen > 1 ) { + // only one 'word' in query but mask has >1 slots + query[1] = query[0]; + } + // replace all {query} with query words... + // if query = 'Bob', then convert mask from '!{query}' to '!Bob' + // if query = 'Bob Joe Frank', then convert mask '{q} OR {q}' to 'Bob OR Joe OR Frank' + while ( regex.test( val ) ) { + val = val.replace( regex, query[indx++] || '' ); + if ( regex.test( val ) && indx < len && ( query[indx] || '' ) !== '' ) { + val = mask.replace( regex, val ); + } + } + return val; + }, + getLatestSearch: function( $input ) { + if ( $input ) { + return $input.sort( function( a, b ) { + return $( b ).attr( 'data-lastSearchTime' ) - $( a ).attr( 'data-lastSearchTime' ); + }); + } + return $input || $(); + }, + findRange: function( c, val, ignoreRanges ) { + // look for multiple columns '1-3,4-6,8' in data-column + var temp, ranges, range, start, end, singles, i, indx, len, + columns = []; + if ( /^[0-9]+$/.test( val ) ) { + // always return an array + return [ parseInt( val, 10 ) ]; + } + // process column range + if ( !ignoreRanges && /-/.test( val ) ) { + ranges = val.match( /(\d+)\s*-\s*(\d+)/g ); + len = ranges ? ranges.length : 0; + for ( indx = 0; indx < len; indx++ ) { + range = ranges[indx].split( /\s*-\s*/ ); + start = parseInt( range[0], 10 ) || 0; + end = parseInt( range[1], 10 ) || ( c.columns - 1 ); + if ( start > end ) { + temp = start; start = end; end = temp; // swap + } + if ( end >= c.columns ) { + end = c.columns - 1; + } + for ( ; start <= end; start++ ) { + columns[ columns.length ] = start; + } + // remove processed range from val + val = val.replace( ranges[ indx ], '' ); + } + } + // process single columns + if ( !ignoreRanges && /,/.test( val ) ) { + singles = val.split( /\s*,\s*/ ); + len = singles.length; + for ( i = 0; i < len; i++ ) { + if ( singles[ i ] !== '' ) { + indx = parseInt( singles[ i ], 10 ); + if ( indx < c.columns ) { + columns[ columns.length ] = indx; + } + } + } + } + // return all columns + if ( !columns.length ) { + for ( indx = 0; indx < c.columns; indx++ ) { + columns[ columns.length ] = indx; + } + } + return columns; + }, + getColumnElm: function( c, $elements, column ) { + // data-column may contain multiple columns '1-3,5-6,8' + // replaces: c.$filters.filter( '[data-column="' + column + '"]' ); + return $elements.filter( function() { + var cols = tsf.findRange( c, $( this ).attr( 'data-column' ) ); + return $.inArray( column, cols ) > -1; + }); + }, + multipleColumns: function( c, $input ) { + // look for multiple columns '1-3,4-6,8' in data-column + var wo = c.widgetOptions, + // only target 'all' column inputs on initialization + // & don't target 'all' column inputs if they don't exist + targets = wo.filter_initialized || !$input.filter( wo.filter_anyColumnSelector ).length, + val = $.trim( tsf.getLatestSearch( $input ).attr( 'data-column' ) || '' ); + return tsf.findRange( c, val, !targets ); + }, + processTypes: function( c, data, vars ) { + var ffxn, + filterMatched = null, + matches = null; + for ( ffxn in tsf.types ) { + if ( $.inArray( ffxn, vars.excludeMatch ) < 0 && matches === null ) { + matches = tsf.types[ffxn]( c, data, vars ); + if ( matches !== null ) { + filterMatched = matches; + } + } + } + return filterMatched; + }, + matchType: function( c, columnIndex ) { + var isMatch, + wo = c.widgetOptions, + $el = c.$headerIndexed[ columnIndex ]; + // filter-exact > filter-match > filter_matchType for type + if ( $el.hasClass( 'filter-exact' ) ) { + isMatch = false; + } else if ( $el.hasClass( 'filter-match' ) ) { + isMatch = true; + } else { + // filter-select is not applied when filter_functions are used, so look for a select + if ( wo.filter_columnFilters ) { + $el = c.$filters + .find( '.' + tscss.filter ) + .add( wo.filter_$externalFilters ) + .filter( '[data-column="' + columnIndex + '"]' ); + } else if ( wo.filter_$externalFilters ) { + $el = wo.filter_$externalFilters.filter( '[data-column="' + columnIndex + '"]' ); + } + isMatch = $el.length ? + c.widgetOptions.filter_matchType[ ( $el[ 0 ].nodeName || '' ).toLowerCase() ] === 'match' : + // default to exact, if no inputs found + false; + } + return isMatch; + }, + processRow: function( c, data, vars ) { + var result, filterMatched, + fxn, ffxn, txt, + wo = c.widgetOptions, + showRow = true, + hasAnyMatchInput = wo.filter_$anyMatch && wo.filter_$anyMatch.length, + + // if wo.filter_$anyMatch data-column attribute is changed dynamically + // we don't want to do an "anyMatch" search on one column using data + // for the entire row - see #998 + columnIndex = wo.filter_$anyMatch && wo.filter_$anyMatch.length ? + // look for multiple columns '1-3,4-6,8' + tsf.multipleColumns( c, wo.filter_$anyMatch ) : + []; + data.$cells = data.$row.children(); + if ( data.anyMatchFlag && columnIndex.length > 1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { + data.anyMatch = true; + data.isMatch = true; + data.rowArray = data.$cells.map( function( i ) { + if ( $.inArray( i, columnIndex ) > -1 || ( data.anyMatchFilter && !hasAnyMatchInput ) ) { + if ( data.parsed[ i ] ) { + txt = data.cacheArray[ i ]; + } else { + txt = data.rawArray[ i ]; + txt = $.trim( wo.filter_ignoreCase ? txt.toLowerCase() : txt ); + if ( c.sortLocaleCompare ) { + txt = ts.replaceAccents( txt ); + } + } + return txt; + } + }).get(); + data.filter = data.anyMatchFilter; + data.iFilter = data.iAnyMatchFilter; + data.exact = data.rowArray.join( ' ' ); + data.iExact = wo.filter_ignoreCase ? data.exact.toLowerCase() : data.exact; + data.cache = data.cacheArray.slice( 0, -1 ).join( ' ' ); + vars.excludeMatch = vars.noAnyMatch; + filterMatched = tsf.processTypes( c, data, vars ); + if ( filterMatched !== null ) { + showRow = filterMatched; + } else { + if ( wo.filter_startsWith ) { + showRow = false; + // data.rowArray may not contain all columns + columnIndex = Math.min( c.columns, data.rowArray.length ); + while ( !showRow && columnIndex > 0 ) { + columnIndex--; + showRow = showRow || data.rowArray[ columnIndex ].indexOf( data.iFilter ) === 0; + } + } else { + showRow = ( data.iExact + data.childRowText ).indexOf( data.iFilter ) >= 0; + } + } + data.anyMatch = false; + // no other filters to process + if ( data.filters.join( '' ) === data.filter ) { + return showRow; + } + } + + for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { + data.filter = data.filters[ columnIndex ]; + data.index = columnIndex; + + // filter types to exclude, per column + vars.excludeMatch = vars.excludeFilter[ columnIndex ]; + + // ignore if filter is empty or disabled + if ( data.filter ) { + data.cache = data.cacheArray[ columnIndex ]; + result = data.parsed[ columnIndex ] ? data.cache : data.rawArray[ columnIndex ] || ''; + data.exact = c.sortLocaleCompare ? ts.replaceAccents( result ) : result; // issue #405 + data.iExact = !tsfRegex.type.test( typeof data.exact ) && wo.filter_ignoreCase ? + data.exact.toLowerCase() : data.exact; + data.isMatch = tsf.matchType( c, columnIndex ); + + result = showRow; // if showRow is true, show that row + + // in case select filter option has a different value vs text 'a - z|A through Z' + ffxn = wo.filter_columnFilters ? + c.$filters.add( wo.filter_$externalFilters ) + .filter( '[data-column="' + columnIndex + '"]' ) + .find( 'select option:selected' ) + .attr( 'data-function-name' ) || '' : ''; + // replace accents - see #357 + if ( c.sortLocaleCompare ) { + data.filter = ts.replaceAccents( data.filter ); + } + + // replace column specific default filters - see #1088 + if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultColFilter[ columnIndex ] ) ) { + data.filter = tsf.defaultFilter( data.filter, vars.defaultColFilter[ columnIndex ] ); + } + + // data.iFilter = case insensitive ( if wo.filter_ignoreCase is true ), + // data.filter = case sensitive + data.iFilter = wo.filter_ignoreCase ? ( data.filter || '' ).toLowerCase() : data.filter; + fxn = vars.functions[ columnIndex ]; + filterMatched = null; + if ( fxn ) { + if ( typeof fxn === 'function' ) { + // filter callback( exact cell content, parser normalized content, + // filter input value, column index, jQuery row object ) + filterMatched = fxn( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); + } else if ( typeof fxn[ ffxn || data.filter ] === 'function' ) { + // selector option function + txt = ffxn || data.filter; + filterMatched = + fxn[ txt ]( data.exact, data.cache, data.filter, columnIndex, data.$row, c, data ); + } + } + if ( filterMatched === null ) { + // cycle through the different filters + // filters return a boolean or null if nothing matches + filterMatched = tsf.processTypes( c, data, vars ); + if ( filterMatched !== null ) { + result = filterMatched; + // Look for match, and add child row data for matching + } else { + // check fxn (filter-select in header) after filter types are checked + // without this, the filter + jQuery UI selectmenu demo was breaking + if ( fxn === true ) { + // default selector uses exact match unless 'filter-match' class is found + result = data.isMatch ? + // data.iExact may be a number + ( '' + data.iExact ).search( data.iFilter ) >= 0 : + data.filter === data.exact; + } else { + txt = ( data.iExact + data.childRowText ).indexOf( tsf.parseFilter( c, data.iFilter, data ) ); + result = ( ( !wo.filter_startsWith && txt >= 0 ) || ( wo.filter_startsWith && txt === 0 ) ); + } + } + } else { + result = filterMatched; + } + showRow = ( result ) ? showRow : false; + } + } + return showRow; + }, + findRows: function( table, filters, currentFilters ) { + if ( + tsf.equalFilters(table.config, table.config.lastSearch, currentFilters) || + !table.config.widgetOptions.filter_initialized + ) { + return; + } + var len, norm_rows, rowData, $rows, $row, rowIndex, tbodyIndex, $tbody, columnIndex, + isChild, childRow, lastSearch, showRow, showParent, time, val, indx, + notFiltered, searchFiltered, query, injected, res, id, txt, + storedFilters = $.extend( [], filters ), + c = table.config, + wo = c.widgetOptions, + // data object passed to filters; anyMatch is a flag for the filters + data = { + anyMatch: false, + filters: filters, + // regex filter type cache + filter_regexCache : [] + }, + vars = { + // anyMatch really screws up with these types of filters + noAnyMatch: [ 'range', 'operators' ], + // cache filter variables that use ts.getColumnData in the main loop + functions : [], + excludeFilter : [], + defaultColFilter : [], + defaultAnyFilter : ts.getColumnData( table, wo.filter_defaultFilter, c.columns, true ) || '' + }; + + // parse columns after formatter, in case the class is added at that point + data.parsed = []; + for ( columnIndex = 0; columnIndex < c.columns; columnIndex++ ) { + data.parsed[ columnIndex ] = wo.filter_useParsedData || + // parser has a "parsed" parameter + ( c.parsers && c.parsers[ columnIndex ] && c.parsers[ columnIndex ].parsed || + // getData may not return 'parsed' if other 'filter-' class names exist + // ( e.g. ) + ts.getData && ts.getData( c.$headerIndexed[ columnIndex ], + ts.getColumnData( table, c.headers, columnIndex ), 'filter' ) === 'parsed' || + c.$headerIndexed[ columnIndex ].hasClass( 'filter-parsed' ) ); + + vars.functions[ columnIndex ] = + ts.getColumnData( table, wo.filter_functions, columnIndex ) || + c.$headerIndexed[ columnIndex ].hasClass( 'filter-select' ); + vars.defaultColFilter[ columnIndex ] = + ts.getColumnData( table, wo.filter_defaultFilter, columnIndex ) || ''; + vars.excludeFilter[ columnIndex ] = + ( ts.getColumnData( table, wo.filter_excludeFilter, columnIndex, true ) || '' ).split( /\s+/ ); + } + + if ( c.debug ) { + console.log( 'Filter: Starting filter widget search', filters ); + time = new Date(); + } + // filtered rows count + c.filteredRows = 0; + c.totalRows = 0; + currentFilters = ( storedFilters || [] ); + + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + $tbody = ts.processTbody( table, c.$tbodies.eq( tbodyIndex ), true ); + // skip child rows & widget added ( removable ) rows - fixes #448 thanks to @hempel! + // $rows = $tbody.children( 'tr' ).not( c.selectorRemove ); + columnIndex = c.columns; + // convert stored rows into a jQuery object + norm_rows = c.cache[ tbodyIndex ].normalized; + $rows = $( $.map( norm_rows, function( el ) { + return el[ columnIndex ].$row.get(); + }) ); + + if ( currentFilters.join('') === '' || wo.filter_serversideFiltering ) { + $rows + .removeClass( wo.filter_filteredRow ) + .not( '.' + c.cssChildRow ) + .css( 'display', '' ); + } else { + // filter out child rows + $rows = $rows.not( '.' + c.cssChildRow ); + len = $rows.length; + + if ( ( wo.filter_$anyMatch && wo.filter_$anyMatch.length ) || + typeof filters[c.columns] !== 'undefined' ) { + data.anyMatchFlag = true; + data.anyMatchFilter = '' + ( + filters[ c.columns ] || + wo.filter_$anyMatch && tsf.getLatestSearch( wo.filter_$anyMatch ).val() || + '' + ); + if ( wo.filter_columnAnyMatch ) { + // specific columns search + query = data.anyMatchFilter.split( tsfRegex.andSplit ); + injected = false; + for ( indx = 0; indx < query.length; indx++ ) { + res = query[ indx ].split( ':' ); + if ( res.length > 1 ) { + // make the column a one-based index ( non-developers start counting from one :P ) + if ( isNaN( res[0] ) ) { + $.each( c.headerContent, function( i, txt ) { + // multiple matches are possible + if ( txt.toLowerCase().indexOf( res[0] ) > -1 ) { + id = i; + filters[ id ] = res[1]; + } + }); + } else { + id = parseInt( res[0], 10 ) - 1; + } + if ( id >= 0 && id < c.columns ) { // if id is an integer + filters[ id ] = res[1]; + query.splice( indx, 1 ); + indx--; + injected = true; + } + } + } + if ( injected ) { + data.anyMatchFilter = query.join( ' && ' ); + } + } + } + + // optimize searching only through already filtered rows - see #313 + searchFiltered = wo.filter_searchFiltered; + lastSearch = c.lastSearch || c.$table.data( 'lastSearch' ) || []; + if ( searchFiltered ) { + // cycle through all filters; include last ( columnIndex + 1 = match any column ). Fixes #669 + for ( indx = 0; indx < columnIndex + 1; indx++ ) { + val = filters[indx] || ''; + // break out of loop if we've already determined not to search filtered rows + if ( !searchFiltered ) { indx = columnIndex; } + // search already filtered rows if... + searchFiltered = searchFiltered && lastSearch.length && + // there are no changes from beginning of filter + val.indexOf( lastSearch[indx] || '' ) === 0 && + // if there is NOT a logical 'or', or range ( 'to' or '-' ) in the string + !tsfRegex.alreadyFiltered.test( val ) && + // if we are not doing exact matches, using '|' ( logical or ) or not '!' + !tsfRegex.exactTest.test( val ) && + // don't search only filtered if the value is negative + // ( '> -10' => '> -100' will ignore hidden rows ) + !( tsfRegex.isNeg1.test( val ) || tsfRegex.isNeg2.test( val ) ) && + // if filtering using a select without a 'filter-match' class ( exact match ) - fixes #593 + !( val !== '' && c.$filters && c.$filters.filter( '[data-column="' + indx + '"]' ).find( 'select' ).length && + !tsf.matchType( c, indx ) ); + } + } + notFiltered = $rows.not( '.' + wo.filter_filteredRow ).length; + // can't search when all rows are hidden - this happens when looking for exact matches + if ( searchFiltered && notFiltered === 0 ) { searchFiltered = false; } + if ( c.debug ) { + console.log( 'Filter: Searching through ' + + ( searchFiltered && notFiltered < len ? notFiltered : 'all' ) + ' rows' ); + } + if ( data.anyMatchFlag ) { + if ( c.sortLocaleCompare ) { + // replace accents + data.anyMatchFilter = ts.replaceAccents( data.anyMatchFilter ); + } + if ( wo.filter_defaultFilter && tsfRegex.iQuery.test( vars.defaultAnyFilter ) ) { + data.anyMatchFilter = tsf.defaultFilter( data.anyMatchFilter, vars.defaultAnyFilter ); + // clear search filtered flag because default filters are not saved to the last search + searchFiltered = false; + } + // make iAnyMatchFilter lowercase unless both filter widget & core ignoreCase options are true + // when c.ignoreCase is true, the cache contains all lower case data + data.iAnyMatchFilter = !( wo.filter_ignoreCase && c.ignoreCase ) ? + data.anyMatchFilter : + data.anyMatchFilter.toLowerCase(); + } + + // loop through the rows + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + + txt = $rows[ rowIndex ].className; + // the first row can never be a child row + isChild = rowIndex && tsfRegex.child.test( txt ); + // skip child rows & already filtered rows + if ( isChild || ( searchFiltered && tsfRegex.filtered.test( txt ) ) ) { + continue; + } + + data.$row = $rows.eq( rowIndex ); + data.rowIndex = rowIndex; + data.cacheArray = norm_rows[ rowIndex ]; + rowData = data.cacheArray[ c.columns ]; + data.rawArray = rowData.raw; + data.childRowText = ''; + + if ( !wo.filter_childByColumn ) { + txt = ''; + // child row cached text + childRow = rowData.child; + // so, if 'table.config.widgetOptions.filter_childRows' is true and there is + // a match anywhere in the child row, then it will make the row visible + // checked here so the option can be changed dynamically + for ( indx = 0; indx < childRow.length; indx++ ) { + txt += ' ' + childRow[indx].join( ' ' ) || ''; + } + data.childRowText = wo.filter_childRows ? + ( wo.filter_ignoreCase ? txt.toLowerCase() : txt ) : + ''; + } + + showRow = false; + showParent = tsf.processRow( c, data, vars ); + $row = rowData.$row; + + // don't pass reference to val + val = showParent ? true : false; + childRow = rowData.$row.filter( ':gt(0)' ); + if ( wo.filter_childRows && childRow.length ) { + if ( wo.filter_childByColumn ) { + if ( !wo.filter_childWithSibs ) { + // hide all child rows + childRow.addClass( wo.filter_filteredRow ); + // if only showing resulting child row, only include parent + $row = $row.eq( 0 ); + } + // cycle through each child row + for ( indx = 0; indx < childRow.length; indx++ ) { + data.$row = childRow.eq( indx ); + data.cacheArray = rowData.child[ indx ]; + data.rawArray = data.cacheArray; + val = tsf.processRow( c, data, vars ); + // use OR comparison on child rows + showRow = showRow || val; + if ( !wo.filter_childWithSibs && val ) { + childRow.eq( indx ).removeClass( wo.filter_filteredRow ); + } + } + } + // keep parent row match even if no child matches... see #1020 + showRow = showRow || showParent; + } else { + showRow = val; + } + $row + .toggleClass( wo.filter_filteredRow, !showRow )[0] + .display = showRow ? '' : 'none'; + } + } + c.filteredRows += $rows.not( '.' + wo.filter_filteredRow ).length; + c.totalRows += $rows.length; + ts.processTbody( table, $tbody, false ); + } + // lastCombinedFilter is no longer used internally + c.lastCombinedFilter = storedFilters.join(''); // save last search + // don't save 'filters' directly since it may have altered ( AnyMatch column searches ) + c.lastSearch = storedFilters; + c.$table.data( 'lastSearch', storedFilters ); + if ( wo.filter_saveFilters && ts.storage ) { + ts.storage( table, 'tablesorter-filters', tsf.processFilters( storedFilters, true ) ); + } + if ( c.debug ) { + console.log( 'Completed filter widget search' + ts.benchmark(time) ); + } + if ( wo.filter_initialized ) { + c.$table.triggerHandler( 'filterBeforeEnd', c ); + c.$table.triggerHandler( 'filterEnd', c ); + } + setTimeout( function() { + ts.applyWidget( c.table ); // make sure zebra widget is applied + }, 0 ); + }, + getOptionSource: function( table, column, onlyAvail ) { + table = $( table )[0]; + var c = table.config, + wo = c.widgetOptions, + arry = false, + source = wo.filter_selectSource, + last = c.$table.data( 'lastSearch' ) || [], + fxn = typeof source === 'function' ? true : ts.getColumnData( table, source, column ); + + if ( onlyAvail && last[column] !== '' ) { + onlyAvail = false; + } + + // filter select source option + if ( fxn === true ) { + // OVERALL source + arry = source( table, column, onlyAvail ); + } else if ( fxn instanceof $ || ( $.type( fxn ) === 'string' && fxn.indexOf( '' ) >= 0 ) ) { + // selectSource is a jQuery object or string of options + return fxn; + } else if ( $.isArray( fxn ) ) { + arry = fxn; + } else if ( $.type( source ) === 'object' && fxn ) { + // custom select source function for a SPECIFIC COLUMN + arry = fxn( table, column, onlyAvail ); + // abort - updating the selects from an external method + if (arry === null) { + return null; + } + } + if ( arry === false ) { + // fall back to original method + arry = tsf.getOptions( table, column, onlyAvail ); + } + + return tsf.processOptions( table, column, arry ); + + }, + processOptions: function( table, column, arry ) { + if ( !$.isArray( arry ) ) { + return false; + } + table = $( table )[0]; + var cts, txt, indx, len, parsedTxt, str, + c = table.config, + validColumn = typeof column !== 'undefined' && column !== null && column >= 0 && column < c.columns, + direction = validColumn ? c.$headerIndexed[ column ].hasClass( 'filter-select-sort-desc' ) : false, + parsed = []; + // get unique elements and sort the list + // if $.tablesorter.sortText exists ( not in the original tablesorter ), + // then natural sort the list otherwise use a basic sort + arry = $.grep( arry, function( value, indx ) { + if ( value.text ) { + return true; + } + return $.inArray( value, arry ) === indx; + }); + if ( validColumn && c.$headerIndexed[ column ].hasClass( 'filter-select-nosort' ) ) { + // unsorted select options + return arry; + } else { + len = arry.length; + // parse select option values + for ( indx = 0; indx < len; indx++ ) { + txt = arry[ indx ]; + // check for object + str = txt.text ? txt.text : txt; + // sortNatural breaks if you don't pass it strings + parsedTxt = ( validColumn && c.parsers && c.parsers.length && + c.parsers[ column ].format( str, table, [], column ) || str ).toString(); + parsedTxt = c.widgetOptions.filter_ignoreCase ? parsedTxt.toLowerCase() : parsedTxt; + // parse array data using set column parser; this DOES NOT pass the original + // table cell to the parser format function + if ( txt.text ) { + txt.parsed = parsedTxt; + parsed[ parsed.length ] = txt; + } else { + parsed[ parsed.length ] = { + text : txt, + // check parser length - fixes #934 + parsed : parsedTxt + }; + } + } + // sort parsed select options + cts = c.textSorter || ''; + parsed.sort( function( a, b ) { + var x = direction ? b.parsed : a.parsed, + y = direction ? a.parsed : b.parsed; + if ( validColumn && typeof cts === 'function' ) { + // custom OVERALL text sorter + return cts( x, y, true, column, table ); + } else if ( validColumn && typeof cts === 'object' && cts.hasOwnProperty( column ) ) { + // custom text sorter for a SPECIFIC COLUMN + return cts[column]( x, y, true, column, table ); + } else if ( ts.sortNatural ) { + // fall back to natural sort + return ts.sortNatural( x, y ); + } + // using an older version! do a basic sort + return true; + }); + // rebuild arry from sorted parsed data + arry = []; + len = parsed.length; + for ( indx = 0; indx < len; indx++ ) { + arry[ arry.length ] = parsed[indx]; + } + return arry; + } + }, + getOptions: function( table, column, onlyAvail ) { + table = $( table )[0]; + var rowIndex, tbodyIndex, len, row, cache, indx, child, childLen, + c = table.config, + wo = c.widgetOptions, + arry = []; + for ( tbodyIndex = 0; tbodyIndex < c.$tbodies.length; tbodyIndex++ ) { + cache = c.cache[tbodyIndex]; + len = c.cache[tbodyIndex].normalized.length; + // loop through the rows + for ( rowIndex = 0; rowIndex < len; rowIndex++ ) { + // get cached row from cache.row ( old ) or row data object + // ( new; last item in normalized array ) + row = cache.row ? + cache.row[ rowIndex ] : + cache.normalized[ rowIndex ][ c.columns ].$row[0]; + // check if has class filtered + if ( onlyAvail && row.className.match( wo.filter_filteredRow ) ) { + continue; + } + // get non-normalized cell content + if ( wo.filter_useParsedData || + c.parsers[column].parsed || + c.$headerIndexed[column].hasClass( 'filter-parsed' ) ) { + arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ column ]; + // child row parsed data + if ( wo.filter_childRows && wo.filter_childByColumn ) { + childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length - 1; + for ( indx = 0; indx < childLen; indx++ ) { + arry[ arry.length ] = '' + cache.normalized[ rowIndex ][ c.columns ].child[ indx ][ column ]; + } + } + } else { + // get raw cached data instead of content directly from the cells + arry[ arry.length ] = cache.normalized[ rowIndex ][ c.columns ].raw[ column ]; + // child row unparsed data + if ( wo.filter_childRows && wo.filter_childByColumn ) { + childLen = cache.normalized[ rowIndex ][ c.columns ].$row.length; + for ( indx = 1; indx < childLen; indx++ ) { + child = cache.normalized[ rowIndex ][ c.columns ].$row.eq( indx ).children().eq( column ); + arry[ arry.length ] = '' + ts.getElementText( c, child, column ); + } + } + } + } + } + return arry; + }, + buildSelect: function( table, column, arry, updating, onlyAvail ) { + table = $( table )[0]; + column = parseInt( column, 10 ); + if ( !table.config.cache || $.isEmptyObject( table.config.cache ) ) { + return; + } + + var indx, val, txt, t, $filters, $filter, option, + c = table.config, + wo = c.widgetOptions, + node = c.$headerIndexed[ column ], + // t.data( 'placeholder' ) won't work in jQuery older than 1.4.3 + options = '', + // Get curent filter value + currentValue = c.$table + .find( 'thead' ) + .find( 'select.' + tscss.filter + '[data-column="' + column + '"]' ) + .val(); + + // nothing included in arry ( external source ), so get the options from + // filter_selectSource or column data + if ( typeof arry === 'undefined' || arry === '' ) { + arry = tsf.getOptionSource( table, column, onlyAvail ); + // abort, selects are updated by an external method + if (arry === null) { + return; + } + } + + if ( $.isArray( arry ) ) { + // build option list + for ( indx = 0; indx < arry.length; indx++ ) { + option = arry[ indx ]; + if ( option.text ) { + // OBJECT!! add data-function-name in case the value is set in filter_functions + option['data-function-name'] = typeof option.value === 'undefined' ? option.text : option.value; + + // support jQuery < v1.8, otherwise the below code could be shortened to + // options += $( '