diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index ed8ca510..00000000 --- a/.coveragerc +++ /dev/null @@ -1,12 +0,0 @@ -[run] -source = - activity - member - note -omit = - activity/tests/*.py - activity/migrations/*.py - member/tests/*.py - member/migrations/*.py - note/tests/*.py - note/migrations/*.py \ No newline at end of file diff --git a/.env_example b/.env_example index 5aba0d14..d903b724 100644 --- a/.env_example +++ b/.env_example @@ -1,13 +1,13 @@ -DJANGO_APP_STAGE="dev" +DJANGO_APP_STAGE=dev # Only used in dev mode, change to "postgresql" if you want to use PostgreSQL in dev -DJANGO_DEV_STORE_METHOD="sqllite" -DJANGO_DB_HOST="localhost" -DJANGO_DB_NAME="note_db" -DJANGO_DB_USER="note" -DJANGO_DB_PASSWORD="CHANGE_ME" -DJANGO_DB_PORT="" -DJANGO_SECRET_KEY="CHANGE_ME" -DJANGO_SETTINGS_MODULE="note_kfet.settings" -DOMAIN="localhost" -CONTACT_EMAIL="tresorerie.bde@localhost" -NOTE_URL="localhost" +DJANGO_DEV_STORE_METHOD=sqllite +DJANGO_DB_HOST=localhost +DJANGO_DB_NAME=note_db +DJANGO_DB_USER=note +DJANGO_DB_PASSWORD=CHANGE_ME +DJANGO_DB_PORT= +DJANGO_SECRET_KEY=CHANGE_ME +DJANGO_SETTINGS_MODULE=note_kfet.settings +DOMAIN=localhost +CONTACT_EMAIL=tresorerie.bde@localhost +NOTE_URL=localhost diff --git a/Dockerfile b/Dockerfile index dfc49d04..80f2c773 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,6 @@ COPY . /code/ # Comment what is not needed RUN pip install -r requirements/base.txt -RUN pip install -r requirements/api.txt RUN pip install -r requirements/cas.txt RUN pip install -r requirements/production.txt diff --git a/README.md b/README.md index 9b0c927e..0c0cfb2e 100644 --- a/README.md +++ b/README.md @@ -106,18 +106,18 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n On copie le fichier `.env_example` vers le fichier `.env` à la racine du projet et on renseigne des secrets et des paramètres : - DJANGO_APP_STAGE="dev" # ou "prod" - DJANGO_DEV_STORE_METHOD="sqllite" # ou "postgres" - DJANGO_DB_HOST="localhost" - DJANGO_DB_NAME="note_db" - DJANGO_DB_USER="note" - DJANGO_DB_PASSWORD="CHANGE_ME" - DJANGO_DB_PORT="" - DJANGO_SECRET_KEY="CHANGE_ME" - DJANGO_SETTINGS_MODULE="note_kfet.settings" - DOMAIN="localhost" # note.example.com - CONTACT_EMAIL="tresorerie.bde@localhost" - NOTE_URL="localhost" # serveur cas note.example.com si auto-hébergé. + DJANGO_APP_STAGE=dev # ou "prod" + DJANGO_DEV_STORE_METHOD=sqllite # ou "postgres" + DJANGO_DB_HOST=localhost + DJANGO_DB_NAME=note_db + DJANGO_DB_USER=note + DJANGO_DB_PASSWORD=CHANGE_ME + DJANGO_DB_PORT= + DJANGO_SECRET_KEY=CHANGE_ME + DJANGO_SETTINGS_MODULE="note_kfet.settings + DOMAIN=localhost # note.example.com + CONTACT_EMAIL=tresorerie.bde@localhost + NOTE_URL=localhost # serveur cas note.example.com si auto-hébergé. Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations diff --git a/apps/activity/api/serializers.py b/apps/activity/api/serializers.py index 514515ef..2f257de0 100644 --- a/apps/activity/api/serializers.py +++ b/apps/activity/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from ..models import ActivityType, Activity, Guest +from ..models import ActivityType, Activity, Guest, Entry, GuestTransaction class ActivityTypeSerializer(serializers.ModelSerializer): @@ -37,3 +37,25 @@ class GuestSerializer(serializers.ModelSerializer): class Meta: model = Guest fields = '__all__' + + +class EntrySerializer(serializers.ModelSerializer): + """ + REST API Serializer for Entries. + The djangorestframework plugin will analyse the model `Entry` and parse all fields in the API. + """ + + class Meta: + model = Entry + fields = '__all__' + + +class GuestTransactionSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Special transactions. + The djangorestframework plugin will analyse the model `GuestTransaction` and parse all fields in the API. + """ + + class Meta: + model = GuestTransaction + fields = '__all__' diff --git a/apps/activity/api/urls.py b/apps/activity/api/urls.py index 79e0ba30..3a2495fb 100644 --- a/apps/activity/api/urls.py +++ b/apps/activity/api/urls.py @@ -1,7 +1,7 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet +from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet, EntryViewSet def register_activity_urls(router, path): @@ -11,3 +11,4 @@ def register_activity_urls(router, path): router.register(path + '/activity', ActivityViewSet) router.register(path + '/type', ActivityTypeViewSet) router.register(path + '/guest', GuestViewSet) + router.register(path + '/entry', EntryViewSet) diff --git a/apps/activity/api/views.py b/apps/activity/api/views.py index 76b2b333..764f2ac3 100644 --- a/apps/activity/api/views.py +++ b/apps/activity/api/views.py @@ -5,8 +5,8 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter from api.viewsets import ReadProtectedModelViewSet -from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer -from ..models import ActivityType, Activity, Guest +from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer, EntrySerializer +from ..models import ActivityType, Activity, Guest, Entry class ActivityTypeViewSet(ReadProtectedModelViewSet): @@ -42,4 +42,16 @@ class GuestViewSet(ReadProtectedModelViewSet): queryset = Guest.objects.all() serializer_class = GuestSerializer filter_backends = [SearchFilter] - search_fields = ['$name', ] + search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] + + +class EntryViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Entry` objects, serialize it to JSON with the given serializer, + then render it on /api/activity/entry/ + """ + queryset = Entry.objects.all() + serializer_class = EntrySerializer + filter_backends = [SearchFilter] + search_fields = ['$last_name', '$first_name', '$inviter__alias__name', '$inviter__alias__normalized_name', ] diff --git a/apps/activity/fixtures/initial.json b/apps/activity/fixtures/initial.json new file mode 100644 index 00000000..1856bce4 --- /dev/null +++ b/apps/activity/fixtures/initial.json @@ -0,0 +1,20 @@ +[ + { + "model": "activity.activitytype", + "pk": 1, + "fields": { + "name": "Pot", + "can_invite": true, + "guest_entry_fee": 500 + } + }, + { + "model": "activity.activitytype", + "pk": 2, + "fields": { + "name": "Soir\u00e9e de club", + "can_invite": false, + "guest_entry_fee": 0 + } + } +] \ No newline at end of file diff --git a/apps/activity/forms.py b/apps/activity/forms.py new file mode 100644 index 00000000..dcbd3c9d --- /dev/null +++ b/apps/activity/forms.py @@ -0,0 +1,84 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later +from datetime import timedelta, datetime + +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import gettext as _ +from member.models import Club +from note.models import NoteUser, Note +from note_kfet.inputs import DateTimePickerInput, Autocomplete + +from .models import Activity, Guest + + +class ActivityForm(forms.ModelForm): + class Meta: + model = Activity + exclude = ('creater', 'valid', 'open', ) + widgets = { + "organizer": Autocomplete( + model=Club, + attrs={"api_url": "/api/members/club/"}, + ), + "note": Autocomplete( + model=Note, + attrs={ + "api_url": "/api/note/note/", + 'placeholder': 'Note de l\'événement sur laquelle envoyer les crédits d\'invitation ...' + }, + ), + "attendees_club": Autocomplete( + model=Club, + attrs={"api_url": "/api/members/club/"}, + ), + "date_start": DateTimePickerInput(), + "date_end": DateTimePickerInput(), + } + + +class GuestForm(forms.ModelForm): + def clean(self): + cleaned_data = super().clean() + + if self.activity.date_start > datetime.now(): + self.add_error("inviter", _("You can't invite someone once the activity is started.")) + + if not self.activity.valid: + self.add_error("inviter", _("This activity is not validated yet.")) + + one_year = timedelta(days=365) + + qs = Guest.objects.filter( + first_name=cleaned_data["first_name"], + last_name=cleaned_data["last_name"], + activity__date_start__gte=self.activity.date_start - one_year, + ) + if len(qs) >= 5: + self.add_error("last_name", _("This person has been already invited 5 times this year.")) + + qs = qs.filter(activity=self.activity) + if qs.exists(): + self.add_error("last_name", _("This person is already invited.")) + + qs = Guest.objects.filter(inviter=cleaned_data["inviter"], activity=self.activity) + if len(qs) >= 3: + self.add_error("inviter", _("You can't invite more than 3 people to this activity.")) + + return cleaned_data + + class Meta: + model = Guest + fields = ('last_name', 'first_name', 'inviter', ) + widgets = { + "inviter": Autocomplete( + NoteUser, + attrs={ + 'api_url': '/api/note/note/', + # We don't evaluate the content type at launch because the DB might be not initialized + 'api_url_suffix': + lambda: '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteUser).pk), + 'placeholder': 'Note ...', + }, + ), + } diff --git a/apps/activity/models.py b/apps/activity/models.py index 8f23060c..29f04b39 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -1,9 +1,13 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import timedelta, datetime -from django.conf import settings +from django.contrib.auth.models import User from django.db import models +from django.db.models import Q from django.utils.translation import gettext_lazy as _ +from rest_framework.exceptions import ValidationError +from note.models import NoteUser, Transaction class ActivityType(models.Model): @@ -44,39 +48,127 @@ class Activity(models.Model): verbose_name=_('name'), max_length=255, ) + description = models.TextField( verbose_name=_('description'), ) + activity_type = models.ForeignKey( ActivityType, on_delete=models.PROTECT, related_name='+', verbose_name=_('type'), ) + + creater = models.ForeignKey( + User, + on_delete=models.PROTECT, + verbose_name=_("user"), + ) + organizer = models.ForeignKey( 'member.Club', on_delete=models.PROTECT, related_name='+', verbose_name=_('organizer'), ) + attendees_club = models.ForeignKey( 'member.Club', on_delete=models.PROTECT, related_name='+', verbose_name=_('attendees club'), ) + date_start = models.DateTimeField( verbose_name=_('start date'), ) + date_end = models.DateTimeField( verbose_name=_('end date'), ) + valid = models.BooleanField( + default=False, + verbose_name=_('valid'), + ) + + open = models.BooleanField( + default=False, + verbose_name=_('open'), + ) + class Meta: verbose_name = _("activity") verbose_name_plural = _("activities") +class Entry(models.Model): + """ + Register the entry of someone: + - a member with a :model:`note.NoteUser` + - or a :model:`activity.Guest` + In the case of a Guest Entry, the inviter note is also save. + """ + activity = models.ForeignKey( + Activity, + on_delete=models.PROTECT, + related_name="entries", + verbose_name=_("activity"), + ) + + time = models.DateTimeField( + auto_now_add=True, + verbose_name=_("entry time"), + ) + + note = models.ForeignKey( + NoteUser, + on_delete=models.PROTECT, + verbose_name=_("note"), + ) + + guest = models.OneToOneField( + 'activity.Guest', + on_delete=models.PROTECT, + null=True, + ) + + class Meta: + unique_together = (('activity', 'note', 'guest', ), ) + verbose_name = _("entry") + verbose_name_plural = _("entries") + + def save(self, *args,**kwargs): + + qs = Entry.objects.filter(~Q(pk=self.pk), activity=self.activity, note=self.note, guest=self.guest) + if qs.exists(): + raise ValidationError(_("Already entered on ") + _("{:%Y-%m-%d %H:%M:%S}").format(qs.get().time, )) + + if self.guest: + self.note = self.guest.inviter + + insert = not self.pk + if insert: + if self.note.balance < 0: + raise ValidationError(_("The balance is negative.")) + + ret = super().save(*args,**kwargs) + + if insert and self.guest: + GuestTransaction.objects.create( + source=self.note, + destination=self.activity.organizer.note, + quantity=1, + amount=self.activity.activity_type.guest_entry_fee, + reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name, + valid=True, + guest=self.guest, + ).save() + + return ret + + class Guest(models.Model): """ People who are not current members of any clubs, and are invited by someone who is a current member. @@ -86,24 +178,73 @@ class Guest(models.Model): on_delete=models.PROTECT, related_name='+', ) - name = models.CharField( + + last_name = models.CharField( max_length=255, + verbose_name=_("last name"), ) + + first_name = models.CharField( + max_length=255, + verbose_name=_("first name"), + ) + inviter = models.ForeignKey( - settings.AUTH_USER_MODEL, + NoteUser, on_delete=models.PROTECT, - related_name='+', - ) - entry = models.DateTimeField( - null=True, - ) - entry_transaction = models.ForeignKey( - 'note.Transaction', - on_delete=models.PROTECT, - blank=True, - null=True, + related_name='guests', + verbose_name=_("inviter"), ) + @property + def has_entry(self): + try: + if self.entry: + return True + return False + except AttributeError: + return False + + def save(self, force_insert=False, force_update=False, using=None, update_fields=None): + one_year = timedelta(days=365) + + if not force_insert: + if self.activity.date_start > datetime.now(): + raise ValidationError(_("You can't invite someone once the activity is started.")) + + if not self.activity.valid: + raise ValidationError(_("This activity is not validated yet.")) + + qs = Guest.objects.filter( + first_name=self.first_name, + last_name=self.last_name, + activity__date_start__gte=self.activity.date_start - one_year, + ) + if len(qs) >= 5: + raise ValidationError(_("This person has been already invited 5 times this year.")) + + qs = qs.filter(activity=self.activity) + if qs.exists(): + raise ValidationError(_("This person is already invited.")) + + qs = Guest.objects.filter(inviter=self.inviter, activity=self.activity) + if len(qs) >= 3: + raise ValidationError(_("You can't invite more than 3 people to this activity.")) + + return super().save(force_insert, force_update, using, update_fields) + class Meta: verbose_name = _("guest") verbose_name_plural = _("guests") + unique_together = ("activity", "last_name", "first_name", ) + + +class GuestTransaction(Transaction): + guest = models.OneToOneField( + Guest, + on_delete=models.PROTECT, + ) + + @property + def type(self): + return _('Invitation') diff --git a/apps/activity/tables.py b/apps/activity/tables.py new file mode 100644 index 00000000..d6e566d3 --- /dev/null +++ b/apps/activity/tables.py @@ -0,0 +1,108 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ +import django_tables2 as tables +from django_tables2 import A +from note.templatetags.pretty_money import pretty_money + +from .models import Activity, Guest, Entry + + +class ActivityTable(tables.Table): + name = tables.LinkColumn( + 'activity:activity_detail', + args=[A('pk'), ], + ) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Activity + template_name = 'django_tables2/bootstrap4.html' + fields = ('name', 'activity_type', 'organizer', 'attendees_club', 'date_start', 'date_end', ) + + +class GuestTable(tables.Table): + inviter = tables.LinkColumn( + 'member:user_detail', + args=[A('inviter.user.pk'), ], + ) + + entry = tables.Column( + empty_values=(), + attrs={ + "td": { + "class": lambda record: "" if record.has_entry else "validate btn btn-danger", + "onclick": lambda record: "" if record.has_entry else "remove_guest(" + str(record.pk) + ")" + } + } + ) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Guest + template_name = 'django_tables2/bootstrap4.html' + fields = ("last_name", "first_name", "inviter", ) + + def render_entry(self, record): + if record.has_entry: + return str(_("Entered on ") + str(_("{:%Y-%m-%d %H:%M:%S}").format(record.entry.time, ))) + return _("remove").capitalize() + + +def get_row_class(record): + c = "table-row" + if isinstance(record, Guest): + if record.has_entry: + c += " table-success" + else: + c += " table-warning" + else: + qs = Entry.objects.filter(note=record.note, activity=record.activity, guest=None) + if qs.exists(): + c += " table-success" + elif record.note.balance < 0: + c += " table-danger" + return c + + +class EntryTable(tables.Table): + type = tables.Column(verbose_name=_("Type")) + + last_name = tables.Column(verbose_name=_("Last name")) + + first_name = tables.Column(verbose_name=_("First name")) + + note_name = tables.Column(verbose_name=_("Note")) + + balance = tables.Column(verbose_name=_("Balance")) + + def render_note_name(self, value, record): + if hasattr(record, 'username'): + username = record.username + if username != value: + return format_html(value + " aka. " + username) + return value + + def render_balance(self, value): + return pretty_money(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + template_name = 'django_tables2/bootstrap4.html' + row_attrs = { + 'class': lambda record: get_row_class(record), + 'id': lambda record: "row-" + ("guest-" if isinstance(record, Guest) else "membership-") + str(record.pk), + 'data-type': lambda record: "guest" if isinstance(record, Guest) else "membership", + 'data-id': lambda record: record.pk if isinstance(record, Guest) else record.note.pk, + 'data-inviter': lambda record: record.inviter.pk if isinstance(record, Guest) else "", + 'data-last-name': lambda record: record.last_name, + 'data-first-name': lambda record: record.first_name, + } diff --git a/apps/activity/urls.py b/apps/activity/urls.py new file mode 100644 index 00000000..f074e8f7 --- /dev/null +++ b/apps/activity/urls.py @@ -0,0 +1,17 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from . import views + +app_name = 'activity' + +urlpatterns = [ + path('', views.ActivityListView.as_view(), name='activity_list'), + path('/', views.ActivityDetailView.as_view(), name='activity_detail'), + path('/invite/', views.ActivityInviteView.as_view(), name='activity_invite'), + path('/entry/', views.ActivityEntryView.as_view(), name='activity_entry'), + path('/update/', views.ActivityUpdateView.as_view(), name='activity_update'), + path('new/', views.ActivityCreateView.as_view(), name='activity_create'), +] diff --git a/apps/activity/views.py b/apps/activity/views.py new file mode 100644 index 00000000..14746929 --- /dev/null +++ b/apps/activity/views.py @@ -0,0 +1,161 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from datetime import datetime, timezone + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.contenttypes.models import ContentType +from django.db.models import F, Q +from django.urls import reverse_lazy +from django.views.generic import CreateView, DetailView, UpdateView, TemplateView +from django.utils.translation import gettext_lazy as _ +from django_tables2.views import SingleTableView +from note.models import NoteUser, Alias, NoteSpecial +from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin + +from .forms import ActivityForm, GuestForm +from .models import Activity, Guest, Entry +from .tables import ActivityTable, GuestTable, EntryTable + + +class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + model = Activity + form_class = ActivityForm + + def form_valid(self, form): + form.instance.creater = self.request.user + return super().form_valid(form) + + def get_success_url(self, **kwargs): + self.object.refresh_from_db() + return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk}) + + +class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + model = Activity + table_class = ActivityTable + + def get_queryset(self): + return super().get_queryset().reverse() + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context['title'] = _("Activities") + + upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now()) + context['upcoming'] = ActivityTable(data=upcoming_activities + .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))) + + return context + + +class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + model = Activity + context_object_name = "activity" + + def get_context_data(self, **kwargs): + context = super().get_context_data() + + table = GuestTable(data=Guest.objects.filter(activity=self.object) + .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))) + context["guests"] = table + + context["activity_started"] = datetime.now(timezone.utc) > self.object.date_start + + return context + + +class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + model = Activity + form_class = ActivityForm + + def get_success_url(self, **kwargs): + return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) + + +class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + model = Guest + form_class = GuestForm + template_name = "activity/activity_invite.html" + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ + .get(pk=self.kwargs["pk"]) + return form + + def form_valid(self, form): + form.instance.activity = Activity.objects\ + .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"]) + return super().form_valid(form) + + def get_success_url(self, **kwargs): + return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) + + +class ActivityEntryView(LoginRequiredMixin, TemplateView): + template_name = "activity/activity_entry.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ + .get(pk=self.kwargs["pk"]) + context["activity"] = activity + + matched = [] + + pattern = "^$" + if "search" in self.request.GET: + pattern = self.request.GET["search"] + + if not pattern: + pattern = "^$" + + if pattern[0] != "^": + pattern = "^" + pattern + + guest_qs = Guest.objects\ + .annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\ + .filter(Q(first_name__regex=pattern) | Q(last_name__regex=pattern) + | Q(inviter__alias__name__regex=pattern) + | Q(inviter__alias__normalized_name__regex=Alias.normalize(pattern))) \ + .filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\ + .distinct()[:20] + for guest in guest_qs: + guest.type = "Invité" + matched.append(guest) + + note_qs = Alias.objects.annotate(last_name=F("note__noteuser__user__last_name"), + first_name=F("note__noteuser__user__first_name"), + username=F("note__noteuser__user__username"), + note_name=F("name"), + balance=F("note__balance"))\ + .filter(Q(note__polymorphic_ctype__model="noteuser") + & (Q(note__noteuser__user__first_name__regex=pattern) + | Q(note__noteuser__user__last_name__regex=pattern) + | Q(name__regex=pattern) + | Q(normalized_name__regex=Alias.normalize(pattern)))) \ + .filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))\ + .distinct()[:20] + for note in note_qs: + note.type = "Adhérent" + note.activity = activity + matched.append(note) + + table = EntryTable(data=matched) + context["table"] = table + + context["entries"] = Entry.objects.filter(activity=activity) + + context["title"] = _('Entry for activity "{}"').format(activity.name) + context["noteuser_ctype"] = ContentType.objects.get_for_model(NoteUser).pk + context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk + + context["activities_open"] = Activity.objects.filter(open=True).filter( + PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter( + PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all() + + return context \ No newline at end of file diff --git a/apps/logs/models.py b/apps/logs/models.py index 10e2651f..94e2b4ce 100644 --- a/apps/logs/models.py +++ b/apps/logs/models.py @@ -75,3 +75,7 @@ class Changelog(models.Model): def delete(self, using=None, keep_parents=False): raise ValidationError(_("Logs cannot be destroyed.")) + + class Meta: + verbose_name = _("changelog") + verbose_name_plural = _("changelogs") diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 43fc1e13..68bf95c0 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_no_log"): + return + # noinspection PyProtectedMember previous = instance._previous @@ -106,6 +109,9 @@ def delete_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_no_log"): + return + # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP user, ip = get_current_authenticated_user(), get_current_ip() diff --git a/apps/member/filters.py b/apps/member/filters.py deleted file mode 100644 index 951723e8..00000000 --- a/apps/member/filters.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Submit -from django.contrib.auth.models import User -from django.db.models import CharField -from django_filters import FilterSet, CharFilter - - -class UserFilter(FilterSet): - class Meta: - model = User - fields = ['last_name', 'first_name', 'username', 'profile__section'] - filter_overrides = { - CharField: { - 'filter_class': CharFilter, - 'extra': lambda f: { - 'lookup_expr': 'icontains' - } - } - } - - -class UserFilterFormHelper(FormHelper): - form_method = 'GET' - layout = Layout( - 'last_name', - 'first_name', - 'username', - 'profile__section', - Submit('Submit', 'Apply Filter'), - ) diff --git a/apps/member/fixtures/initial.json b/apps/member/fixtures/initial.json index bba1e7ac..e27eb72d 100644 --- a/apps/member/fixtures/initial.json +++ b/apps/member/fixtures/initial.json @@ -5,10 +5,12 @@ "fields": { "name": "BDE", "email": "tresorerie.bde@example.com", - "membership_fee": 500, - "membership_duration": "396 00:00:00", - "membership_start": "213 00:00:00", - "membership_end": "273 00:00:00" + "require_memberships": true, + "membership_fee_paid": 500, + "membership_fee_unpaid": 500, + "membership_duration": 396, + "membership_start": "2019-08-31", + "membership_end": "2020-09-30" } }, { @@ -17,10 +19,13 @@ "fields": { "name": "Kfet", "email": "tresorerie.bde@example.com", - "membership_fee": 3500, - "membership_duration": "396 00:00:00", - "membership_start": "213 00:00:00", - "membership_end": "273 00:00:00" + "parent_club": 1, + "require_memberships": true, + "membership_fee_paid": 3500, + "membership_fee_unpaid": 3500, + "membership_duration": 396, + "membership_start": "2019-08-31", + "membership_end": "2020-09-30" } } ] diff --git a/apps/member/forms.py b/apps/member/forms.py index 5f2d5838..6fe95f5a 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -1,13 +1,12 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from crispy_forms.bootstrap import Div -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout -from dal import autocomplete from django import forms -from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ +from note.models import NoteSpecial +from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput from permission.models import PermissionMask from .models import Profile, Club, Membership @@ -21,17 +20,6 @@ class CustomAuthenticationForm(AuthenticationForm): ) -class SignUpForm(UserCreationForm): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.fields['username'].widget.attrs.pop("autofocus", None) - self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"}) - - class Meta: - model = User - fields = ['first_name', 'last_name', 'username', 'email'] - - class ProfileForm(forms.ModelForm): """ A form for the extras field provided by the :model:`member.Profile` model. @@ -40,21 +28,64 @@ class ProfileForm(forms.ModelForm): class Meta: model = Profile fields = '__all__' - exclude = ['user'] + exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', ) class ClubForm(forms.ModelForm): class Meta: model = Club fields = '__all__' - - -class AddMembersForm(forms.Form): - class Meta: - fields = ('',) + widgets = { + "membership_fee_paid": AmountInput(), + "membership_fee_unpaid": AmountInput(), + "parent_club": Autocomplete( + Club, + attrs={ + 'api_url': '/api/members/club/', + } + ), + "membership_start": DatePickerInput(), + "membership_end": DatePickerInput(), + } class MembershipForm(forms.ModelForm): + soge = forms.BooleanField( + label=_("Inscription paid by Société Générale"), + required=False, + help_text=_("Check this case is the Société Générale paid the inscription."), + ) + + credit_type = forms.ModelChoiceField( + queryset=NoteSpecial.objects, + label=_("Credit type"), + empty_label=_("No credit"), + required=False, + help_text=_("You can credit the note of the user."), + ) + + credit_amount = forms.IntegerField( + label=_("Credit amount"), + required=False, + initial=0, + widget=AmountInput(), + ) + + last_name = forms.CharField( + label=_("Last name"), + required=False, + ) + + first_name = forms.CharField( + label=_("First name"), + required=False, + ) + + bank = forms.CharField( + label=_("Bank"), + required=False, + ) + class Meta: model = Membership fields = ('user', 'roles', 'date_start') @@ -63,35 +94,13 @@ class MembershipForm(forms.ModelForm): # et récupère les noms d'utilisateur valides widgets = { 'user': - autocomplete.ModelSelect2( - url='member:user_autocomplete', + Autocomplete( + User, attrs={ - 'data-placeholder': 'Nom ...', - 'data-minimum-input-length': 1, + 'api_url': '/api/user/', + 'name_field': 'username', + 'placeholder': 'Nom ...', }, ), + 'date_start': DatePickerInput(), } - - -MemberFormSet = forms.modelformset_factory( - Membership, - form=MembershipForm, - extra=2, - can_delete=True, -) - - -class FormSetHelper(FormHelper): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_tag = False - self.form_method = 'POST' - self.form_class = 'form-inline' - # self.template = 'bootstrap/table_inline_formset.html' - self.layout = Layout( - Div( - Div('user', css_class='col-sm-2'), - Div('roles', css_class='col-sm-2'), - Div('date_start', css_class='col-sm-2'), - css_class="row formset-row", - )) diff --git a/apps/member/models.py b/apps/member/models.py index d0051e59..3a022434 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -2,12 +2,19 @@ # SPDX-License-Identifier: GPL-3.0-or-later import datetime +import os from django.conf import settings +from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models +from django.template import loader from django.urls import reverse, reverse_lazy +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode from django.utils.translation import gettext_lazy as _ +from registration.tokens import email_validation_token +from note.models import MembershipTransaction class Profile(models.Model): @@ -43,6 +50,23 @@ class Profile(models.Model): ) paid = models.BooleanField( verbose_name=_("paid"), + help_text=_("Tells if the user receive a salary."), + default=False, + ) + + email_confirmed = models.BooleanField( + verbose_name=_("email confirmed"), + default=False, + ) + + registration_valid = models.BooleanField( + verbose_name=_("registration valid"), + default=False, + ) + + soge = models.BooleanField( + verbose_name=_("Société générale"), + help_text=_("Has the user ever be paid by the Société générale?"), default=False, ) @@ -54,6 +78,17 @@ class Profile(models.Model): def get_absolute_url(self): return reverse('user_detail', args=(self.pk,)) + def send_email_validation_link(self): + subject = "Activate your Note Kfet account" + message = loader.render_to_string('registration/mails/email_validation_email.html', + { + 'user': self.user, + 'domain': os.getenv("NOTE_URL", "note.example.com"), + 'token': email_validation_token.make_token(self.user), + 'uid': urlsafe_base64_encode(force_bytes(self.user.pk)).decode('UTF-8'), + }) + self.user.email_user(subject, message) + class Club(models.Model): """ @@ -77,22 +112,43 @@ class Club(models.Model): ) # Memberships - membership_fee = models.PositiveIntegerField( - verbose_name=_('membership fee'), + + # When set to False, the membership system won't be used. + # Useful to create notes for activities or departments. + require_memberships = models.BooleanField( + default=True, + verbose_name=_("require memberships"), + help_text=_("Uncheck if this club don't require memberships."), ) - membership_duration = models.DurationField( + + membership_fee_paid = models.PositiveIntegerField( + default=0, + verbose_name=_('membership fee (paid students)'), + ) + + membership_fee_unpaid = models.PositiveIntegerField( + default=0, + verbose_name=_('membership fee (unpaid students)'), + ) + + membership_duration = models.PositiveIntegerField( + blank=True, null=True, verbose_name=_('membership duration'), - help_text=_('The longest time a membership can last ' + help_text=_('The longest time (in days) a membership can last ' '(NULL = infinite).'), ) - membership_start = models.DurationField( + + membership_start = models.DateField( + blank=True, null=True, verbose_name=_('membership start'), help_text=_('How long after January 1st the members can renew ' 'their membership.'), ) - membership_end = models.DurationField( + + membership_end = models.DateField( + blank=True, null=True, verbose_name=_('membership end'), help_text=_('How long the membership can last after January 1st ' @@ -100,6 +156,33 @@ class Club(models.Model): 'membership.'), ) + def update_membership_dates(self): + """ + This function is called each time the club detail view is displayed. + Update the year of the membership dates. + """ + if not self.membership_start: + return + + today = datetime.date.today() + + if (today - self.membership_start).days >= 365: + self.membership_start = datetime.date(self.membership_start.year + 1, + self.membership_start.month, self.membership_start.day) + self.membership_end = datetime.date(self.membership_end.year + 1, + self.membership_end.month, self.membership_end.day) + self.save(force_update=True) + + def save(self, force_insert=False, force_update=False, using=None, + update_fields=None): + if not self.require_memberships: + self.membership_fee_paid = 0 + self.membership_fee_unpaid = 0 + self.membership_duration = None + self.membership_start = None + self.membership_end = None + super().save(force_insert, force_update, update_fields) + class Meta: verbose_name = _("club") verbose_name_plural = _("clubs") @@ -114,9 +197,6 @@ class Club(models.Model): class Role(models.Model): """ Role that an :model:`auth.User` can have in a :model:`member.Club` - - TODO: Integrate the right management, and create some standard Roles at the - creation of the club. """ name = models.CharField( verbose_name=_('name'), @@ -138,40 +218,101 @@ class Membership(models.Model): """ user = models.ForeignKey( - settings.AUTH_USER_MODEL, + User, on_delete=models.PROTECT, + verbose_name=_("user"), ) + club = models.ForeignKey( Club, on_delete=models.PROTECT, + verbose_name=_("club"), ) - roles = models.ForeignKey( + + roles = models.ManyToManyField( Role, - on_delete=models.PROTECT, + verbose_name=_("roles"), ) + date_start = models.DateField( + default=datetime.date.today, verbose_name=_('membership starts on'), ) + date_end = models.DateField( verbose_name=_('membership ends on'), null=True, ) + fee = models.PositiveIntegerField( verbose_name=_('fee'), ) def valid(self): + """ + A membership is valid if today is between the start and the end date. + """ if self.date_end is not None: return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal() else: return self.date_start.toordinal() <= datetime.datetime.now().toordinal() def save(self, *args, **kwargs): + """ + Calculate fee and end date before saving the membership and creating the transaction if needed. + """ if self.club.parent_club is not None: - if not Membership.objects.filter(user=self.user, club=self.club.parent_club): - raise ValidationError(_('User is not a member of the parent club')) + if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists(): + raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name) + + created = not self.pk + if created: + if Membership.objects.filter( + user=self.user, + club=self.club, + date_start__lte=self.date_start, + date_end__gte=self.date_start, + ).exists(): + raise ValidationError(_('User is already a member of the club')) + + if self.user.profile.paid: + self.fee = self.club.membership_fee_paid + else: + self.fee = self.club.membership_fee_unpaid + + if self.club.membership_duration is not None: + self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) + else: + self.date_end = self.date_start + datetime.timedelta(days=424242) + if self.club.membership_end is not None and self.date_end > self.club.membership_end: + self.date_end = self.club.membership_end + super().save(*args, **kwargs) + self.make_transaction() + + def make_transaction(self): + """ + Create Membership transaction associated to this membership. + """ + if not self.fee or MembershipTransaction.objects.filter(membership=self).exists(): + return + + if self.fee: + transaction = MembershipTransaction( + membership=self, + source=self.user.note, + destination=self.club.note, + quantity=1, + amount=self.fee, + reason="Adhésion " + self.club.name, + ) + transaction._force_save = True + transaction.save(force_insert=True) + + def __str__(self): + return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, ) + class Meta: verbose_name = _('membership') verbose_name_plural = _('memberships') diff --git a/apps/member/signals.py b/apps/member/signals.py index 2b03e3ce..fbb66c1f 100644 --- a/apps/member/signals.py +++ b/apps/member/signals.py @@ -10,7 +10,7 @@ def save_user_profile(instance, created, raw, **_kwargs): # When provisionning data, do not try to autocreate return - if created: + if created and instance.is_active: from .models import Profile Profile.objects.get_or_create(user=instance) - instance.profile.save() + instance.profile.save() diff --git a/apps/member/tables.py b/apps/member/tables.py index d0c37a6e..515d7836 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -1,13 +1,23 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import datetime import django_tables2 as tables from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ +from django.urls import reverse_lazy +from django.utils.html import format_html +from note.templatetags.pretty_money import pretty_money +from note_kfet.middlewares import get_current_authenticated_user +from permission.backends import PermissionBackend -from .models import Club +from .models import Club, Membership class ClubTable(tables.Table): + """ + List all clubs. + """ class Meta: attrs = { 'class': 'table table-condensed table-striped table-hover' @@ -23,8 +33,15 @@ class ClubTable(tables.Table): class UserTable(tables.Table): + """ + List all users. + """ section = tables.Column(accessor='profile.section') - solde = tables.Column(accessor='note.balance') + + balance = tables.Column(accessor='note.balance', verbose_name=_("Balance")) + + def render_balance(self, value): + return pretty_money(value) class Meta: attrs = { @@ -33,3 +50,82 @@ class UserTable(tables.Table): template_name = 'django_tables2/bootstrap4.html' fields = ('last_name', 'first_name', 'username', 'email') model = User + row_attrs = { + 'class': 'table-row', + 'data-href': lambda record: record.pk + } + + +class MembershipTable(tables.Table): + """ + List all memberships. + """ + roles = tables.Column( + attrs={ + "td": { + "class": "text-truncate", + } + } + ) + + def render_user(self, value): + # If the user has the right, link the displayed user with the page of its detail. + s = value.username + if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value): + s = format_html("{name}", + url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s) + + return s + + def render_club(self, value): + # If the user has the right, link the displayed club with the page of its detail. + s = value.name + if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value): + s = format_html("{name}", + url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s) + + return s + + def render_fee(self, value, record): + t = pretty_money(value) + + # If it is required and if the user has the right, the renew button is displayed. + if record.club.membership_start is not None: + if record.date_start < record.club.membership_start: # If the renew is available + if not Membership.objects.filter( + club=record.club, + user=record.user, + date_start__gte=record.club.membership_start, + date_end__lte=record.club.membership_end, + ).exists(): # If the renew is not yet performed + empty_membership = Membership( + club=record.club, + user=record.user, + date_start=datetime.now().date(), + date_end=datetime.now().date(), + fee=0, + ) + if PermissionBackend.check_perm(get_current_authenticated_user(), + "member:add_membership", empty_membership): # If the user has right + t = format_html(t + ' {text}', + url=reverse_lazy('member:club_renew_membership', + kwargs={"pk": record.pk}), text=_("Renew")) + return t + + def render_roles(self, record): + # If the user has the right to manage the roles, display the link to manage them + roles = record.roles.all() + s = ", ".join(str(role) for role in roles) + if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record): + s = format_html("" + s + "") + return s + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover', + 'style': 'table-layout: fixed;' + } + template_name = 'django_tables2/bootstrap4.html' + fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', ) + model = Membership diff --git a/apps/member/urls.py b/apps/member/urls.py index 0b705bfd..4be4ceae 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -7,20 +7,20 @@ from . import views app_name = 'member' urlpatterns = [ - path('signup/', views.UserCreateView.as_view(), name="signup"), path('club/', views.ClubListView.as_view(), name="club_list"), - path('club//', views.ClubDetailView.as_view(), name="club_detail"), - path('club//add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), path('club/create/', views.ClubCreateView.as_view(), name="club_create"), - path('club//update', views.ClubUpdateView.as_view(), name="club_update"), - path('club//update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), - path('club//aliases', views.ClubAliasView.as_view(), name="club_alias"), + path('club//', views.ClubDetailView.as_view(), name="club_detail"), + path('club//add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), + path('club/manage_roles//', views.ClubManageRolesView.as_view(), name="club_manage_roles"), + path('club/renew_membership//', views.ClubAddMemberView.as_view(), name="club_renew_membership"), + path('club//update/', views.ClubUpdateView.as_view(), name="club_update"), + path('club//update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), + path('club//aliases/', views.ClubAliasView.as_view(), name="club_alias"), + path('user/', views.UserListView.as_view(), name="user_list"), - path('user/', views.UserDetailView.as_view(), name="user_detail"), - path('user//update', views.UserUpdateView.as_view(), name="user_update_profile"), - path('user//update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), - path('user//aliases', views.ProfileAliasView.as_view(), name="user_alias"), + path('user//', views.UserDetailView.as_view(), name="user_detail"), + path('user//update/', views.UserUpdateView.as_view(), name="user_update_profile"), + path('user//update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), + path('user//aliases/', views.ProfileAliasView.as_view(), name="user_alias"), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), - # API for the user autocompleter - path('user/user-autocomplete', views.UserAutocomplete.as_view(), name="user_autocomplete"), ] diff --git a/apps/member/views.py b/apps/member/views.py index 2992f76e..381314b2 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -2,39 +2,37 @@ # SPDX-License-Identifier: GPL-3.0-or-later import io +from datetime import datetime, timedelta from PIL import Image -from dal import autocomplete from django.conf import settings -from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.models import User from django.contrib.auth.views import LoginView -from django.core.exceptions import ValidationError from django.db.models import Q -from django.http import HttpResponseRedirect from django.shortcuts import redirect from django.urls import reverse_lazy from django.utils.translation import gettext_lazy as _ -from django.views.generic import CreateView, DetailView, UpdateView, TemplateView, DeleteView +from django.views.generic import CreateView, DetailView, UpdateView, TemplateView from django.views.generic.edit import FormMixin from django_tables2.views import SingleTableView from rest_framework.authtoken.models import Token from note.forms import ImageForm -#from note.forms import AliasForm, ImageForm -from note.models import Alias, NoteUser -from note.models.transactions import Transaction +from note.models import Alias, NoteUser, NoteSpecial +from note.models.transactions import Transaction, SpecialTransaction from note.tables import HistoryTable, AliasTable from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin -from .filters import UserFilter, UserFilterFormHelper -from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \ - CustomAuthenticationForm -from .models import Club, Membership -from .tables import ClubTable, UserTable +from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm +from .models import Club, Membership, Role +from .tables import ClubTable, UserTable, MembershipTable class CustomLoginView(LoginView): + """ + Login view, where the user can select its permission mask. + """ form_class = CustomAuthenticationForm def form_valid(self, form): @@ -42,33 +40,10 @@ class CustomLoginView(LoginView): return super().form_valid(form) -class UserCreateView(CreateView): +class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ - Une vue pour inscrire un utilisateur et lui créer un profile + Update the user information. """ - - form_class = SignUpForm - success_url = reverse_lazy('login') - template_name = 'member/signup.html' - second_form = ProfileForm - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["profile_form"] = self.second_form() - - return context - - def form_valid(self, form): - profile_form = ProfileForm(self.request.POST) - if form.is_valid() and profile_form.is_valid(): - user = form.save(commit=False) - user.profile = profile_form.save(commit=False) - user.save() - user.profile.save() - return super().form_valid(form) - - -class UserUpdateView(LoginRequiredMixin, UpdateView): model = User fields = ['first_name', 'last_name', 'username', 'email'] template_name = 'member/profile_update.html' @@ -77,14 +52,20 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + + form = context['form'] + form.fields['username'].widget.attrs.pop("autofocus", None) + form.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"}) + form.fields['first_name'].required = True + form.fields['last_name'].required = True + form.fields['email'].required = True + form.fields['email'].help_text = _("This address must be valid.") + context['profile_form'] = self.profile_form(instance=context['user_object'].profile) context['title'] = _("Update Profile") return context - def get_form(self, form_class=None): - form = super().get_form(form_class) - if 'username' not in form.data: - return form + def form_valid(self, form): new_username = form.data['username'] # Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant note = NoteUser.objects.filter( @@ -92,9 +73,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): if note.exists() and note.get().user != self.object: form.add_error('username', _("An alias with a similar name already exists.")) - return form + return super().form_invalid(form) - def form_valid(self, form): profile_form = ProfileForm( data=self.request.POST, instance=self.object.profile, @@ -102,29 +82,35 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): if form.is_valid() and profile_form.is_valid(): new_username = form.data['username'] alias = Alias.objects.filter(name=new_username) - # Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer + # Si le nouveau pseudo n'est pas un de nos alias, + # on supprime éventuellement un alias similaire pour le remplacer if not alias.exists(): similar = Alias.objects.filter( normalized_name=Alias.normalize(new_username)) if similar.exists(): similar.delete() + olduser = User.objects.get(pk=form.instance.pk) + user = form.save(commit=False) profile = profile_form.save(commit=False) profile.user = user profile.save() user.save() + + if olduser.email != user.email: + # If the user changed her/his email, then it is unvalidated and a confirmation link is sent. + user.profile.email_confirmed = False + user.profile.send_email_validation_link() + return super().form_valid(form) def get_success_url(self, **kwargs): - if kwargs: - return reverse_lazy('member:user_detail', - kwargs={'pk': kwargs['id']}) - else: - return reverse_lazy('member:user_detail', args=(self.object.id,)) + url = 'member:user_detail' if self.object.profile.registration_valid else 'registration:future_user_detail' + return reverse_lazy(url, args=(self.object.id,)) -class UserDetailView(LoginRequiredMixin, DetailView): +class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): """ Affiche les informations sur un utilisateur, sa note, ses clubs... """ @@ -133,47 +119,77 @@ class UserDetailView(LoginRequiredMixin, DetailView): template_name = "member/profile_detail.html" def get_queryset(self, **kwargs): - return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) + """ + We can't display information of a not registered user. + """ + return super().get_queryset().filter(profile__registration_valid=True) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = context['user_object'] history_list = \ - Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id") - context['history_list'] = HistoryTable(history_list) - club_list = \ - Membership.objects.all().filter(user=user).only("club") - context['club_list'] = ClubTable(club_list) + Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\ + .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) + history_table = HistoryTable(history_list, prefix='transaction-') + history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1)) + context['history_list'] = history_table + + club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\ + .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) + membership_table = MembershipTable(data=club_list, prefix='membership-') + membership_table.paginate(per_page=10, page=self.request.GET.get("membership-page", 1)) + context['club_list'] = membership_table return context -class UserListView(LoginRequiredMixin, SingleTableView): +class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ - Affiche la liste des utilisateurs, avec une fonction de recherche statique + Display user list, with a search bar """ model = User table_class = UserTable template_name = 'member/user_list.html' - filter_class = UserFilter - formhelper_class = UserFilterFormHelper def get_queryset(self, **kwargs): - qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) - self.filter = self.filter_class(self.request.GET, queryset=qs) - self.filter.form.helper = self.formhelper_class() - return self.filter.qs + """ + Filter the user list with the given pattern. + """ + qs = super().get_queryset().filter(profile__registration_valid=True) + if "search" in self.request.GET: + pattern = self.request.GET["search"] + + if not pattern: + return qs.none() + + qs = qs.filter( + Q(first_name__iregex=pattern) + | Q(last_name__iregex=pattern) + | Q(profile__section__iregex=pattern) + | Q(profile__username__iregex="^" + pattern) + | Q(note__alias__name__iregex="^" + pattern) + | Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern)) + ) + else: + qs = qs.none() + + return qs[:20] def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["filter"] = self.filter + + context["title"] = _("Search user") + return context - -class ProfileAliasView(LoginRequiredMixin, DetailView): + +class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + View and manage user aliases. + """ model = User template_name = 'member/profile_alias.html' context_object_name = 'user_object' - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) note = context['object'].note @@ -181,11 +197,14 @@ class ProfileAliasView(LoginRequiredMixin, DetailView): return context -class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): +class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): + """ + Update profile picture of the user note. + """ form_class = ImageForm - def get_context_data(self, *args, **kwargs): - context = super().get_context_data(*args, **kwargs) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) context['form'] = self.form_class(self.request.POST, self.request.FILES) return context @@ -242,8 +261,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): template_name = "member/manage_auth_tokens.html" def get(self, request, *args, **kwargs): - if 'regenerate' in request.GET and Token.objects.filter( - user=request.user).exists(): + if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists(): Token.objects.get(user=self.request.user).delete() return redirect(reverse_lazy('member:auth_token') + "?show", permanent=True) @@ -252,39 +270,16 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['token'] = Token.objects.get_or_create( - user=self.request.user)[0] + context['token'] = Token.objects.get_or_create(user=self.request.user)[0] return context -class UserAutocomplete(autocomplete.Select2QuerySetView): - """ - Auto complete users by usernames - """ - - def get_queryset(self): - """ - Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion. - Cette fonction récupère la requête, et renvoie la liste filtrée des utilisateurs par pseudos. - """ - # Un utilisateur non connecté n'a accès à aucune information - if not self.request.user.is_authenticated: - return User.objects.none() - - qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all() - - if self.q: - qs = qs.filter(username__regex="^" + self.q) - - return qs - - # ******************************* # # CLUB # # ******************************* # -class ClubCreateView(LoginRequiredMixin, CreateView): +class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create Club """ @@ -294,43 +289,66 @@ class ClubCreateView(LoginRequiredMixin, CreateView): def form_valid(self, form): return super().form_valid(form) - -class ClubListView(LoginRequiredMixin, SingleTableView): + +class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List existing Clubs """ model = Club table_class = ClubTable - def get_queryset(self, **kwargs): - return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) - -class ClubDetailView(LoginRequiredMixin, DetailView): +class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + Display details of a club + """ model = Club context_object_name = "club" - def get_queryset(self, **kwargs): - return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) - def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) + club = context["club"] - club_transactions = \ - Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) - context['history_list'] = HistoryTable(club_transactions) - club_member = \ - Membership.objects.all().filter(club=club) - # TODO: consider only valid Membership - context['member_list'] = club_member + if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club): + club.update_membership_dates() + + club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ + .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id') + history_table = HistoryTable(club_transactions, prefix="history-") + history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1)) + context['history_list'] = history_table + club_member = Membership.objects.filter( + club=club, + date_end__gte=datetime.today(), + ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) + + membership_table = MembershipTable(data=club_member, prefix="membership-") + membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1)) + context['member_list'] = membership_table + + # Check if the user has the right to create a membership, to display the button. + empty_membership = Membership( + club=club, + user=User.objects.first(), + date_start=datetime.now().date(), + date_end=datetime.now().date(), + fee=0, + ) + context["can_add_members"] = PermissionBackend()\ + .has_perm(self.request.user, "member.add_membership", empty_membership) + return context -class ClubAliasView(LoginRequiredMixin, DetailView): + +class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + Manage aliases of a club. + """ model = Club template_name = 'member/club_alias.html' context_object_name = 'club' - + def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) note = context['object'].note @@ -338,15 +356,23 @@ class ClubAliasView(LoginRequiredMixin, DetailView): return context -class ClubUpdateView(LoginRequiredMixin, UpdateView): +class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Update the information of a club. + """ model = Club context_object_name = "club" form_class = ClubForm template_name = "member/club_form.html" - success_url = reverse_lazy("member:club_detail") + + def get_success_url(self): + return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk}) class ClubPictureUpdateView(PictureUpdateView): + """ + Update the profile picture of a club. + """ model = Club template_name = 'member/club_picture_update.html' context_object_name = 'club' @@ -355,34 +381,229 @@ class ClubPictureUpdateView(PictureUpdateView): return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id}) -class ClubAddMemberView(LoginRequiredMixin, CreateView): +class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + """ + Add a membership to a club. + """ model = Membership form_class = MembershipForm template_name = 'member/add_members.html' - def get_queryset(self, **kwargs): - return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view") - | PermissionBackend.filter_queryset(self.request.user, Membership, - "change")) def get_context_data(self, **kwargs): - club = Club.objects.get(pk=self.kwargs["pk"]) context = super().get_context_data(**kwargs) - context['formset'] = MemberFormSet() - context['helper'] = FormSetHelper() + form = context['form'] + + if "club_pk" in self.kwargs: + # We create a new membership. + club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ + .get(pk=self.kwargs["club_pk"]) + form.fields['credit_amount'].initial = club.membership_fee_paid + form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all() + + # If the concerned club is the BDE, then we add the option that Société générale pays the membership. + if club.name != "BDE": + del form.fields['soge'] + else: + fee = 0 + bde = Club.objects.get(name="BDE") + fee += bde.membership_fee_paid + kfet = Club.objects.get(name="Kfet") + fee += kfet.membership_fee_paid + context["total_fee"] = "{:.02f}".format(fee / 100, ) + else: + # This is a renewal. Fields can be pre-completed. + old_membership = self.get_queryset().get(pk=self.kwargs["pk"]) + club = old_membership.club + user = old_membership.user + form.fields['user'].initial = user + form.fields['user'].disabled = True + form.fields['roles'].initial = old_membership.roles.all() + form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1) + form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \ + else club.membership_fee_unpaid + form.fields['last_name'].initial = user.last_name + form.fields['first_name'].initial = user.first_name + + # If this is a renewal of a BDE membership, Société générale can pays, if it is not yet done + if club.name != "BDE" or user.profile.soge: + del form.fields['soge'] + else: + fee = 0 + bde = Club.objects.get(name="BDE") + fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid + kfet = Club.objects.get(name="Kfet") + fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid + context["total_fee"] = "{:.02f}".format(fee / 100, ) + context['club'] = club - context['no_cache'] = True return context - def post(self, request, *args, **kwargs): - return - # TODO: Implement POST - # formset = MembershipFormset(request.POST) - # if formset.is_valid(): - # return self.form_valid(formset) - # else: - # return self.form_invalid(formset) + def form_valid(self, form): + """ + Create membership, check that all is good, make transactions + """ + # Get the club that is concerned by the membership + if "club_pk" in self.kwargs: + club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \ + .get(pk=self.kwargs["club_pk"]) + user = form.instance.user + else: + old_membership = self.get_queryset().get(pk=self.kwargs["pk"]) + club = old_membership.club + user = old_membership.user - def form_valid(self, formset): - formset.save() - return super().form_valid(formset) + form.instance.club = club + + # Get form data + credit_type = form.cleaned_data["credit_type"] + credit_amount = form.cleaned_data["credit_amount"] + last_name = form.cleaned_data["last_name"] + first_name = form.cleaned_data["first_name"] + bank = form.cleaned_data["bank"] + soge = form.cleaned_data["soge"] and not user.profile.soge and club.name == "BDE" + + # If Société générale pays, then we auto-fill some data + if soge: + credit_type = NoteSpecial.objects.get(special_type="Virement bancaire") + bde = club + kfet = Club.objects.get(name="Kfet") + if user.profile.paid: + fee = bde.membership_fee_paid + kfet.membership_fee_paid + else: + fee = bde.membership_fee_unpaid + kfet.membership_fee_unpaid + credit_amount = fee + bank = "Société générale" + + if credit_type is None: + credit_amount = 0 + + if user.profile.paid: + fee = club.membership_fee_paid + else: + fee = club.membership_fee_unpaid + if user.note.balance + credit_amount < fee and not Membership.objects.filter( + club__name="Kfet", + user=user, + date_start__lte=datetime.now().date(), + date_end__gte=datetime.now().date(), + ).exists(): + # Users without a valid Kfet membership can't have a negative balance. + # Club 2 = Kfet (hard-code :'( ) + # TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note + form.add_error('user', + _("This user don't have enough money to join this club, and can't have a negative balance.")) + return super().form_invalid(form) + + if club.parent_club is not None: + if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists(): + form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name) + return super().form_invalid(form) + + if Membership.objects.filter( + user=form.instance.user, + club=club, + date_start__lte=form.instance.date_start, + date_end__gte=form.instance.date_start, + ).exists(): + form.add_error('user', _('User is already a member of the club')) + return super().form_invalid(form) + + if club.membership_start and form.instance.date_start < club.membership_start: + form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") + .format(form.instance.club.membership_start)) + return super().form_invalid(form) + + if club.membership_end and form.instance.date_start > club.membership_end: + form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.") + .format(form.instance.club.membership_start)) + return super().form_invalid(form) + + # Now, all is fine, the membership can be created. + + # Credit note before the membership is created. + if credit_amount > 0: + if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"): + if not last_name: + form.add_error('last_name', _("This field is required.")) + if not first_name: + form.add_error('first_name', _("This field is required.")) + if not bank and credit_type.special_type == "Chèque": + form.add_error('bank', _("This field is required.")) + return self.form_invalid(form) + + SpecialTransaction.objects.create( + source=credit_type, + destination=user.note, + quantity=1, + amount=credit_amount, + reason="Crédit " + credit_type.special_type + " (Adhésion " + club.name + ")", + last_name=last_name, + first_name=first_name, + bank=bank, + valid=True, + ) + + # If Société générale pays, then we store the information: the bank can't pay twice to a same person. + if soge: + user.profile.soge = True + user.profile.save() + + kfet = Club.objects.get(name="Kfet") + kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid + + # Get current membership, to get the end date + old_membership = Membership.objects.filter( + club__name="Kfet", + user=user, + date_start__lte=datetime.today(), + date_end__gte=datetime.today(), + ) + + membership = Membership.objects.create( + club=kfet, + user=user, + fee=kfet_fee, + date_start=old_membership.get().date_end + timedelta(days=1) + if old_membership.exists() else form.instance.date_start, + ) + if old_membership.exists(): + membership.roles.set(old_membership.get().roles.all()) + else: + membership.roles.add(Role.objects.get(name="Adhérent Kfet")) + membership.save() + + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) + + +class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Manage the roles of a user in a club + """ + model = Membership + form_class = MembershipForm + template_name = 'member/add_members.html' + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + club = self.object.club + context['club'] = club + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + # We don't create a full membership, we only update one field + form.fields['user'].disabled = True + del form.fields['date_start'] + del form.fields['credit_type'] + del form.fields['credit_amount'] + del form.fields['last_name'] + del form.fields['first_name'] + del form.fields['bank'] + return form + + def get_success_url(self): + return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) diff --git a/apps/note/admin.py b/apps/note/admin.py index 702d3350..dc6470d2 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \ from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ - RecurrentTransaction, MembershipTransaction + RecurrentTransaction, MembershipTransaction, SpecialTransaction class AliasInlines(admin.TabularInline): @@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin): """ Admin customisation for Transaction """ - child_models = (RecurrentTransaction, MembershipTransaction) + child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction) list_display = ('created_at', 'poly_source', 'poly_destination', 'quantity', 'amount', 'valid') list_filter = ('valid',) @@ -138,6 +138,20 @@ class TransactionAdmin(PolymorphicParentModelAdmin): return [] +@admin.register(MembershipTransaction) +class MembershipTransactionAdmin(PolymorphicChildModelAdmin): + """ + Admin customisation for MembershipTransaction + """ + + +@admin.register(SpecialTransaction) +class SpecialTransactionAdmin(PolymorphicChildModelAdmin): + """ + Admin customisation for SpecialTransaction + """ + + @admin.register(TransactionTemplate) class TransactionTemplateAdmin(admin.ModelAdmin): """ diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 02379b04..7005ce16 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -90,7 +90,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer): Note: NoteSerializer, NoteUser: NoteUserSerializer, NoteClub: NoteClubSerializer, - NoteSpecial: NoteSpecialSerializer + NoteSpecial: NoteSpecialSerializer, } class Meta: @@ -177,6 +177,7 @@ class SpecialTransactionSerializer(serializers.ModelSerializer): fields = '__all__' +# noinspection PyUnresolvedReferences class TransactionPolymorphicSerializer(PolymorphicSerializer): model_serializer_mapping = { Transaction: TransactionSerializer, @@ -185,5 +186,12 @@ class TransactionPolymorphicSerializer(PolymorphicSerializer): SpecialTransaction: SpecialTransactionSerializer, } + try: + from activity.models import GuestTransaction + from activity.api.serializers import GuestTransactionSerializer + model_serializer_mapping[GuestTransaction] = GuestTransactionSerializer + except ImportError: # Activity app is not loaded + pass + class Meta: model = Transaction diff --git a/apps/note/api/views.py b/apps/note/api/views.py index b5064ae4..61ef19eb 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -8,7 +8,6 @@ from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework import viewsets from rest_framework.response import Response from rest_framework import status - from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\ @@ -25,7 +24,8 @@ class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet): """ queryset = Note.objects.all() serializer_class = NotePolymorphicSerializer - filter_backends = [SearchFilter, OrderingFilter] + filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] + filterset_fields = ['polymorphic_ctype', 'is_active', ] search_fields = ['$alias__normalized_name', '$alias__name', '$polymorphic_ctype__model', ] ordering_fields = ['alias__name', 'alias__normalized_name'] @@ -60,19 +60,19 @@ class AliasViewSet(ReadProtectedModelViewSet): def get_serializer_class(self): serializer_class = self.serializer_class if self.request.method in ['PUT', 'PATCH']: - #alias owner cannot be change once establish + # alias owner cannot be change once establish setattr(serializer_class.Meta, 'read_only_fields', ('note',)) return serializer_class - + def destroy(self, request, *args, **kwargs): instance = self.get_object() try: self.perform_destroy(instance) except ValidationError as e: print(e) - return Response({e.code:e.message},status.HTTP_400_BAD_REQUEST) + return Response({e.code: e.message}, status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_204_NO_CONTENT) - + def get_queryset(self): """ Parse query and apply filters. diff --git a/apps/note/forms.py b/apps/note/forms.py index 60252ad5..50f226f2 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -1,12 +1,12 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from dal import autocomplete from django import forms +from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ +from note_kfet.inputs import Autocomplete -from .models import Alias -from .models import TransactionTemplate +from .models import TransactionTemplate, NoteClub class ImageForm(forms.Form): @@ -31,11 +31,14 @@ class TransactionTemplateForm(forms.ModelForm): # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special} widgets = { 'destination': - autocomplete.ModelSelect2( - url='note:note_autocomplete', + Autocomplete( + NoteClub, attrs={ - 'data-placeholder': 'Note ...', - 'data-minimum-input-length': 1, + 'api_url': '/api/note/note/', + # We don't evaluate the content type at launch because the DB might be not initialized + 'api_url_suffix': + lambda: '&polymorphic_ctype=' + str(ContentType.objects.get_for_model(NoteClub).pk), + 'placeholder': 'Note ...', }, ), } diff --git a/apps/note/models/notes.py b/apps/note/models/notes.py index 43faabfe..9282bde9 100644 --- a/apps/note/models/notes.py +++ b/apps/note/models/notes.py @@ -242,10 +242,10 @@ class Alias(models.Model): pass self.normalized_name = normalized_name - def save(self,*args,**kwargs): + def save(self, *args, **kwargs): self.normalized_name = self.normalize(self.name) - super().save(*args,**kwargs) - + super().save(*args, **kwargs) + def delete(self, using=None, keep_parents=False): if self.name == str(self.note): raise ValidationError(_("You can't delete your main alias."), diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index c6b8baa6..83f8f914 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -2,7 +2,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.db import models -from django.db.models import F from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -47,12 +46,14 @@ class TransactionTemplate(models.Model): unique=True, error_messages={'unique': _("A template with this name already exist")}, ) + destination = models.ForeignKey( NoteClub, on_delete=models.PROTECT, related_name='+', # no reverse verbose_name=_('destination'), ) + amount = models.PositiveIntegerField( verbose_name=_('amount'), help_text=_('in centimes'), @@ -63,9 +64,12 @@ class TransactionTemplate(models.Model): verbose_name=_('type'), max_length=31, ) + display = models.BooleanField( default=True, + verbose_name=_("display"), ) + description = models.CharField( verbose_name=_('description'), max_length=255, @@ -141,6 +145,7 @@ class Transaction(PolymorphicModel): max_length=255, default=None, null=True, + blank=True, ) class Meta: diff --git a/apps/note/signals.py b/apps/note/signals.py index e62115b3..37737a45 100644 --- a/apps/note/signals.py +++ b/apps/note/signals.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later -def save_user_note(instance, created, raw, **_kwargs): +def save_user_note(instance, raw, **_kwargs): """ Hook to create and save a note when an user is updated """ @@ -10,10 +10,11 @@ def save_user_note(instance, created, raw, **_kwargs): # When provisionning data, do not try to autocreate return - if created: - from .models import NoteUser - NoteUser.objects.create(user=instance) - instance.note.save() + if (instance.is_superuser or instance.profile.registration_valid) and instance.is_active: + # Create note only when the registration is validated + from note.models import NoteUser + NoteUser.objects.get_or_create(user=instance) + instance.note.save() def save_club_note(instance, created, raw, **_kwargs): diff --git a/apps/note/tables.py b/apps/note/tables.py index 201b6c43..a38beb9a 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -106,9 +106,8 @@ DELETE_TEMPLATE = """ class AliasTable(tables.Table): class Meta: attrs = { - 'class': - 'table table condensed table-striped table-hover', - 'id':"alias_table" + 'class': 'table table condensed table-striped table-hover', + 'id': "alias_table" } model = Alias fields = ('name',) @@ -118,9 +117,9 @@ class AliasTable(tables.Table): name = tables.Column(attrs={'td': {'class': 'text-center'}}) delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, - extra_context={"delete_trans": _('delete')}, - attrs={'td': {'class': 'col-sm-1'}}) - + extra_context={"delete_trans": _('delete')}, + attrs={'td': {'class': 'col-sm-1'}}, + verbose_name=_("Delete"),) class ButtonTable(tables.Table): @@ -136,17 +135,20 @@ class ButtonTable(tables.Table): } model = TransactionTemplate + exclude = ('id',) edit = tables.LinkColumn('note:template_update', args=[A('pk')], attrs={'td': {'class': 'col-sm-1'}, 'a': {'class': 'btn btn-sm btn-primary'}}, text=_('edit'), - accessor='pk') + accessor='pk', + verbose_name=_("Edit"),) delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, - extra_context={"delete_trans": _('delete')}, - attrs={'td': {'class': 'col-sm-1'}}) + extra_context={"delete_trans": _('delete')}, + attrs={'td': {'class': 'col-sm-1'}}, + verbose_name=_("Delete"),) def render_amount(self, value): return pretty_money(value) diff --git a/apps/note/templatetags/pretty_money.py b/apps/note/templatetags/pretty_money.py index ba527f9b..265870a8 100644 --- a/apps/note/templatetags/pretty_money.py +++ b/apps/note/templatetags/pretty_money.py @@ -18,10 +18,5 @@ def pretty_money(value): ) -def cents_to_euros(value): - return "{:.02f}".format(value / 100) if value else "" - - register = template.Library() register.filter('pretty_money', pretty_money) -register.filter('cents_to_euros', cents_to_euros) diff --git a/apps/note/urls.py b/apps/note/urls.py index 59316069..9d6af317 100644 --- a/apps/note/urls.py +++ b/apps/note/urls.py @@ -4,7 +4,6 @@ from django.urls import path from . import views -from .models import Note app_name = 'note' urlpatterns = [ @@ -13,7 +12,4 @@ urlpatterns = [ path('buttons/update//', views.TransactionTemplateUpdateView.as_view(), name='template_update'), path('buttons/', views.TransactionTemplateListView.as_view(), name='template_list'), path('consos/', views.ConsoView.as_view(), name='consos'), - - # API for the note autocompleter - path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note), name='note_autocomplete'), ] diff --git a/apps/note/views.py b/apps/note/views.py index ddf5ee6f..88d47847 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -1,23 +1,24 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from dal import autocomplete +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType -from django.db.models import Q from django.utils.translation import gettext_lazy as _ from django.views.generic import CreateView, UpdateView from django_tables2 import SingleTableView from django.urls import reverse_lazy +from note_kfet.inputs import AmountInput from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin from .forms import TransactionTemplateForm -from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial +from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial from .models.transactions import SpecialTransaction from .tables import HistoryTable, ButtonTable -class TransactionCreateView(LoginRequiredMixin, SingleTableView): +class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`. e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial` @@ -27,12 +28,9 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView): model = Transaction # Transaction history table table_class = HistoryTable - table_pagination = {"per_page": 50} - def get_queryset(self): - return Transaction.objects.filter(PermissionBackend.filter_queryset( - self.request.user, Transaction, "view") - ).order_by("-id").all()[:50] + def get_queryset(self, **kwargs): + return super().get_queryset(**kwargs).order_by("-id").all()[:20] def get_context_data(self, **kwargs): """ @@ -40,109 +38,62 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView): """ context = super().get_context_data(**kwargs) context['title'] = _('Transfer money') + context['amount_widget'] = AmountInput(attrs={"id": "amount"}) context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk - context['special_types'] = NoteSpecial.objects.order_by("special_type").all() + context['special_types'] = NoteSpecial.objects\ + .filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\ + .order_by("special_type").all() + + # Add a shortcut for entry page for open activities + if "activity" in settings.INSTALLED_APPS: + from activity.models import Activity + context["activities_open"] = Activity.objects.filter(open=True).filter( + PermissionBackend.filter_queryset(self.request.user, Activity, "view")).filter( + PermissionBackend.filter_queryset(self.request.user, Activity, "change")).all() return context -class NoteAutocomplete(autocomplete.Select2QuerySetView): +class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ - Auto complete note by aliases. Used in every search field for note - ex: :view:`ConsoView`, :view:`TransactionCreateView` - """ - - def get_queryset(self): - """ - When someone look for an :models:`note.Alias`, a query is sent to the dedicated API. - This function handles the result and return a filtered list of aliases. - """ - # Un utilisateur non connecté n'a accès à aucune information - if not self.request.user.is_authenticated: - return Alias.objects.none() - - qs = Alias.objects.all() - - # self.q est le paramètre de la recherche - if self.q: - qs = qs.filter(Q(name__regex="^" + self.q) | Q(normalized_name__regex="^" + Alias.normalize(self.q))) \ - .order_by('normalized_name').distinct() - - # Filtrage par type de note (user, club, special) - note_type = self.forwarded.get("note_type", None) - if note_type: - types = str(note_type).lower() - if "user" in types: - qs = qs.filter(note__polymorphic_ctype__model="noteuser") - elif "club" in types: - qs = qs.filter(note__polymorphic_ctype__model="noteclub") - elif "special" in types: - qs = qs.filter(note__polymorphic_ctype__model="notespecial") - else: - qs = qs.none() - - return qs - - def get_result_label(self, result): - """ - Show the selected alias and the username associated - (aka. ) - """ - # Gère l'affichage de l'alias dans la recherche - res = result.name - note_name = str(result.note) - if res != note_name: - res += " (aka. " + note_name + ")" - return res - - def get_result_value(self, result): - """ - The value used for the transactions will be the id of the Note. - """ - return str(result.note.pk) - - -class TransactionTemplateCreateView(LoginRequiredMixin, CreateView): - """ - Create TransactionTemplate + Create Transaction template """ model = TransactionTemplate form_class = TransactionTemplateForm success_url = reverse_lazy('note:template_list') -class TransactionTemplateListView(LoginRequiredMixin, SingleTableView): +class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ - List TransactionsTemplates + List Transaction templates """ model = TransactionTemplate table_class = ButtonTable -class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): +class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ + Update Transaction template """ model = TransactionTemplate form_class = TransactionTemplateForm success_url = reverse_lazy('note:template_list') -class ConsoView(LoginRequiredMixin, SingleTableView): +class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ The Magic View that make people pay their beer and burgers. (Most of the magic happens in the dark world of Javascript see consos.js) """ + model = Transaction template_name = "note/conso_form.html" # Transaction history table table_class = HistoryTable - table_pagination = {"per_page": 50} - def get_queryset(self): - return Transaction.objects.filter( - PermissionBackend.filter_queryset(self.request.user, Transaction, "view") - ).order_by("-id").all()[:50] + def get_queryset(self, **kwargs): + return super().get_queryset(**kwargs).order_by("-id").all()[:20] def get_context_data(self, **kwargs): """ diff --git a/apps/permission/api/serializers.py b/apps/permission/api/serializers.py index 0a52f4fe..e30ed7dc 100644 --- a/apps/permission/api/serializers.py +++ b/apps/permission/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from ..models import Permission +from ..models import Permission, RolePermissions class PermissionSerializer(serializers.ModelSerializer): @@ -15,3 +15,14 @@ class PermissionSerializer(serializers.ModelSerializer): class Meta: model = Permission fields = '__all__' + + +class RolePermissionsSerializer(serializers.ModelSerializer): + """ + REST API Serializer for RolePermissions types. + The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API. + """ + + class Meta: + model = RolePermissions + fields = '__all__' diff --git a/apps/permission/api/urls.py b/apps/permission/api/urls.py index d50344ea..b5d53466 100644 --- a/apps/permission/api/urls.py +++ b/apps/permission/api/urls.py @@ -1,11 +1,12 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .views import PermissionViewSet +from .views import PermissionViewSet, RolePermissionsViewSet def register_permission_urls(router, path): """ Configure router for permission REST API. """ - router.register(path, PermissionViewSet) + router.register(path + "/permission", PermissionViewSet) + router.register(path + "/roles", RolePermissionsViewSet) diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py index 965e82c9..6a068225 100644 --- a/apps/permission/api/views.py +++ b/apps/permission/api/views.py @@ -4,17 +4,29 @@ from django_filters.rest_framework import DjangoFilterBackend from api.viewsets import ReadOnlyProtectedModelViewSet -from .serializers import PermissionSerializer -from ..models import Permission +from .serializers import PermissionSerializer, RolePermissionsSerializer +from ..models import Permission, RolePermissions class PermissionViewSet(ReadOnlyProtectedModelViewSet): """ REST API View set. - The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, - then render it on /api/logs/ + The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer, + then render it on /api/permission/permission/ """ queryset = Permission.objects.all() serializer_class = PermissionSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ['model', 'type', ] + + +class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer + then render it on /api/permission/roles/ + """ + queryset = RolePermissions.objects.all() + serializer_class = RolePermissionsSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['role', ] diff --git a/apps/permission/backends.py b/apps/permission/backends.py index e61b0719..04d93528 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -1,6 +1,8 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import datetime + from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, AnonymousUser from django.contrib.contenttypes.models import ContentType @@ -9,6 +11,7 @@ from note.models import Note, NoteUser, NoteClub, NoteSpecial from note_kfet.middlewares import get_current_session from member.models import Membership, Club +from .decorators import memoize from .models import Permission @@ -20,6 +23,28 @@ class PermissionBackend(ModelBackend): supports_anonymous_user = False supports_inactive_user = False + @staticmethod + @memoize + def get_raw_permissions(user, t): + """ + Query permissions of a certain type for a user, then memoize it. + :param user: The owner of the permissions + :param t: The type of the permissions: view, change, add or delete + :return: The queryset of the permissions of the user (memoized) grouped by clubs + """ + if isinstance(user, AnonymousUser): + # Unauthenticated users have no permissions + return Permission.objects.none() + + return Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ + .filter( + rolepermissions__role__membership__user=user, + rolepermissions__role__membership__date_start__lte=datetime.date.today(), + rolepermissions__role__membership__date_end__gte=datetime.date.today(), + type=t, + mask__rank__lte=get_current_session().get("permission_mask", 0), + ).distinct() + @staticmethod def permissions(user, model, type): """ @@ -29,16 +54,16 @@ class PermissionBackend(ModelBackend): :param type: The type of the permissions: view, change, add or delete :return: A generator of the requested permissions """ - for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ - .filter( - rolepermissions__role__membership__user=user, - model__app_label=model.app_label, # For polymorphic models, we don't filter on model type - type=type, - ).all(): - if not isinstance(model, permission.model.__class__): + clubs = {} + + for permission in PermissionBackend.get_raw_permissions(user, type): + if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club: continue - club = Club.objects.get(pk=permission.club) + if permission.club not in clubs: + clubs[permission.club] = club = Club.objects.get(pk=permission.club) + else: + club = clubs[permission.club] permission = permission.about( user=user, club=club, @@ -52,10 +77,10 @@ class PermissionBackend(ModelBackend): F=F, Q=Q ) - if permission.mask.rank <= get_current_session().get("permission_mask", 0): - yield permission + yield permission @staticmethod + @memoize def filter_queryset(user, model, t, field=None): """ Filter a queryset by considering the permissions of a given user. @@ -89,10 +114,23 @@ class PermissionBackend(ModelBackend): query = query | perm.query return query - def has_perm(self, user_obj, perm, obj=None): + @staticmethod + @memoize + def check_perm(user_obj, perm, obj=None): + """ + Check is the given user has the permission over a given object. + The result is then memoized. + Exception: for add permissions, since the object is not hashable since it doesn't have any + primary key, the result is not memoized. Moreover, the right could change + (e.g. for a transaction, the balance of the user could change) + """ if user_obj is None or isinstance(user_obj, AnonymousUser): return False + sess = get_current_session() + if sess is not None and sess.session_key is None: + return Permission.objects.none() + if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42: return True @@ -104,10 +142,13 @@ class PermissionBackend(ModelBackend): perm_field = perm[2] if len(perm) == 3 else None ct = ContentType.objects.get_for_model(obj) if any(permission.applies(obj, perm_type, perm_field) - for permission in self.permissions(user_obj, ct, perm_type)): + for permission in PermissionBackend.permissions(user_obj, ct, perm_type)): return True return False + def has_perm(self, user_obj, perm, obj=None): + return PermissionBackend.check_perm(user_obj, perm, obj) + def has_module_perms(self, user_obj, app_label): return False diff --git a/apps/permission/decorators.py b/apps/permission/decorators.py new file mode 100644 index 00000000..f144935a --- /dev/null +++ b/apps/permission/decorators.py @@ -0,0 +1,59 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from functools import lru_cache +from time import time + +from django.contrib.sessions.models import Session +from note_kfet.middlewares import get_current_session + + +def memoize(f): + """ + Memoize results and store in sessions + + This decorator is useful for permissions: they are loaded once needed, then stored for next calls. + The storage is contained with sessions since it depends on the selected mask. + """ + sess_funs = {} + last_collect = time() + + def collect(): + """ + Clear cache of results when sessions are invalid, to flush useless data. + This function is called every minute. + """ + nonlocal sess_funs + + new_sess_funs = {} + for sess_key in sess_funs: + if Session.objects.filter(session_key=sess_key).exists(): + new_sess_funs[sess_key] = sess_funs[sess_key] + sess_funs = new_sess_funs + + def func(*args, **kwargs): + nonlocal last_collect + + if time() - last_collect > 60: + # Clear cache + collect() + last_collect = time() + + # If there is no session, then we don't memoize anything. + sess = get_current_session() + if sess is None or sess.session_key is None: + return f(*args, **kwargs) + + sess_key = sess.session_key + if sess_key not in sess_funs: + # lru_cache makes the job of memoization + # We store only the 512 latest data per session. It has to be enough. + sess_funs[sess_key] = lru_cache(512)(f) + try: + return sess_funs[sess_key](*args, **kwargs) + except TypeError: # For add permissions, objects are not hashable (not yet created). Don't memoize this case. + return f(*args, **kwargs) + + func.func_name = f.__name__ + + return func diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 43d39a36..d7eca508 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -17,42 +17,63 @@ "model": "member.role", "pk": 3, "fields": { - "name": "Pr\u00e9sident\u00b7e BDE" + "name": "Membre de club" } }, { "model": "member.role", "pk": 4, "fields": { - "name": "Tr\u00e9sorier\u00b7\u00e8re BDE" + "name": "Bureau de club" } }, { "model": "member.role", "pk": 5, "fields": { - "name": "Respo info" + "name": "Pr\u00e9sident\u00b7e de club" } }, { "model": "member.role", "pk": 6, "fields": { - "name": "GC Kfet" + "name": "Tr\u00e9sorier\u00b7\u00e8re de club" } }, { "model": "member.role", "pk": 7, "fields": { - "name": "Pr\u00e9sident\u00b7e de club" + "name": "Pr\u00e9sident\u00b7e BDE" } }, { "model": "member.role", "pk": 8, "fields": { - "name": "Tr\u00e9sorier\u00b7\u00e8re de club" + "name": "Tr\u00e9sorier\u00b7\u00e8re BDE" + } + }, + { + "model": "member.role", + "pk": 9, + "fields": { + "name": "Respo info" + } + }, + { + "model": "member.role", + "pk": 10, + "fields": { + "name": "GC Kfet" + } + }, + { + "model": "member.role", + "pk": 11, + "fields": { + "name": "Res[pot]" } }, { @@ -83,10 +104,7 @@ "model": "permission.permission", "pk": 1, "fields": { - "model": [ - "auth", - "user" - ], + "model": 4, "query": "{\"pk\": [\"user\", \"pk\"]}", "type": "view", "mask": 1, @@ -98,10 +116,7 @@ "model": "permission.permission", "pk": 2, "fields": { - "model": [ - "member", - "profile" - ], + "model": 17, "query": "{\"user\": [\"user\"]}", "type": "view", "mask": 1, @@ -113,10 +128,7 @@ "model": "permission.permission", "pk": 3, "fields": { - "model": [ - "note", - "noteuser" - ], + "model": 27, "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", "type": "view", "mask": 1, @@ -128,10 +140,7 @@ "model": "permission.permission", "pk": 4, "fields": { - "model": [ - "authtoken", - "token" - ], + "model": 8, "query": "{\"user\": [\"user\"]}", "type": "view", "mask": 1, @@ -143,10 +152,7 @@ "model": "permission.permission", "pk": 5, "fields": { - "model": [ - "note", - "transaction" - ], + "model": 22, "query": "[\"OR\", {\"source\": [\"user\", \"note\"]}, {\"destination\": [\"user\", \"note\"]}]", "type": "view", "mask": 1, @@ -158,10 +164,7 @@ "model": "permission.permission", "pk": 6, "fields": { - "model": [ - "note", - "alias" - ], + "model": 19, "query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]", "type": "view", "mask": 1, @@ -173,10 +176,7 @@ "model": "permission.permission", "pk": 7, "fields": { - "model": [ - "auth", - "user" - ], + "model": 4, "query": "{\"pk\": [\"user\", \"pk\"]}", "type": "change", "mask": 1, @@ -188,10 +188,7 @@ "model": "permission.permission", "pk": 8, "fields": { - "model": [ - "auth", - "user" - ], + "model": 4, "query": "{\"pk\": [\"user\", \"pk\"]}", "type": "change", "mask": 1, @@ -203,10 +200,7 @@ "model": "permission.permission", "pk": 9, "fields": { - "model": [ - "auth", - "user" - ], + "model": 4, "query": "{\"pk\": [\"user\", \"pk\"]}", "type": "change", "mask": 1, @@ -218,10 +212,7 @@ "model": "permission.permission", "pk": 10, "fields": { - "model": [ - "auth", - "user" - ], + "model": 4, "query": "{\"pk\": [\"user\", \"pk\"]}", "type": "change", "mask": 1, @@ -233,10 +224,7 @@ "model": "permission.permission", "pk": 11, "fields": { - "model": [ - "auth", - "user" - ], + "model": 4, "query": "{\"pk\": [\"user\", \"pk\"]}", "type": "change", "mask": 1, @@ -248,10 +236,7 @@ "model": "permission.permission", "pk": 12, "fields": { - "model": [ - "authtoken", - "token" - ], + "model": 8, "query": "{\"user\": [\"user\"]}", "type": "delete", "mask": 1, @@ -263,10 +248,7 @@ "model": "permission.permission", "pk": 13, "fields": { - "model": [ - "authtoken", - "token" - ], + "model": 8, "query": "{\"user\": [\"user\"]}", "type": "add", "mask": 1, @@ -278,10 +260,7 @@ "model": "permission.permission", "pk": 14, "fields": { - "model": [ - "note", - "alias" - ], + "model": 19, "query": "{\"note\": [\"user\", \"note\"]}", "type": "delete", "mask": 1, @@ -293,10 +272,7 @@ "model": "permission.permission", "pk": 15, "fields": { - "model": [ - "note", - "alias" - ], + "model": 19, "query": "{\"note\": [\"user\", \"note\"]}", "type": "add", "mask": 1, @@ -308,10 +284,7 @@ "model": "permission.permission", "pk": 16, "fields": { - "model": [ - "note", - "noteuser" - ], + "model": 27, "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", "type": "change", "mask": 1, @@ -323,10 +296,7 @@ "model": "permission.permission", "pk": 17, "fields": { - "model": [ - "note", - "transaction" - ], + "model": 22, "query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"amount__lte\": [\"user\", \"note\", \"balance\"]}, {\"valid\": false}]]", "type": "add", "mask": 1, @@ -338,10 +308,7 @@ "model": "permission.permission", "pk": 18, "fields": { - "model": [ - "note", - "note" - ], + "model": 20, "query": "{}", "type": "change", "mask": 1, @@ -353,10 +320,7 @@ "model": "permission.permission", "pk": 19, "fields": { - "model": [ - "note", - "note" - ], + "model": 20, "query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"pk__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club\": [\"club\"]}], [\"all\"]]}]", "type": "view", "mask": 2, @@ -368,11 +332,8 @@ "model": "permission.permission", "pk": 20, "fields": { - "model": [ - "note", - "transaction" - ], - "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]", + "model": 22, + "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]", "type": "add", "mask": 2, "field": "", @@ -383,10 +344,7 @@ "model": "permission.permission", "pk": 21, "fields": { - "model": [ - "note", - "recurrenttransaction" - ], + "model": 28, "query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]", "type": "add", "mask": 2, @@ -398,10 +356,7 @@ "model": "permission.permission", "pk": 22, "fields": { - "model": [ - "member", - "club" - ], + "model": 15, "query": "{\"pk\": [\"club\", \"pk\"]}", "type": "view", "mask": 1, @@ -413,10 +368,7 @@ "model": "permission.permission", "pk": 23, "fields": { - "model": [ - "note", - "transaction" - ], + "model": 22, "query": "{}", "type": "change", "mask": 1, @@ -428,10 +380,7 @@ "model": "permission.permission", "pk": 24, "fields": { - "model": [ - "note", - "transaction" - ], + "model": 22, "query": "{}", "type": "view", "mask": 2, @@ -443,10 +392,7 @@ "model": "permission.permission", "pk": 25, "fields": { - "model": [ - "note", - "notespecial" - ], + "model": 26, "query": "{}", "type": "view", "mask": 2, @@ -458,10 +404,7 @@ "model": "permission.permission", "pk": 26, "fields": { - "model": [ - "note", - "specialtransaction" - ], + "model": 29, "query": "{}", "type": "add", "mask": 2, @@ -473,10 +416,7 @@ "model": "permission.permission", "pk": 27, "fields": { - "model": [ - "note", - "templatecategory" - ], + "model": 21, "query": "{}", "type": "view", "mask": 2, @@ -488,10 +428,7 @@ "model": "permission.permission", "pk": 28, "fields": { - "model": [ - "note", - "templatecategory" - ], + "model": 21, "query": "{}", "type": "change", "mask": 3, @@ -503,10 +440,7 @@ "model": "permission.permission", "pk": 29, "fields": { - "model": [ - "note", - "templatecategory" - ], + "model": 21, "query": "{}", "type": "add", "mask": 3, @@ -518,10 +452,7 @@ "model": "permission.permission", "pk": 30, "fields": { - "model": [ - "note", - "transactiontemplate" - ], + "model": 23, "query": "{}", "type": "view", "mask": 2, @@ -533,10 +464,7 @@ "model": "permission.permission", "pk": 31, "fields": { - "model": [ - "note", - "transactiontemplate" - ], + "model": 23, "query": "{}", "type": "add", "mask": 3, @@ -548,10 +476,7 @@ "model": "permission.permission", "pk": 32, "fields": { - "model": [ - "note", - "transactiontemplate" - ], + "model": 23, "query": "{}", "type": "change", "mask": 3, @@ -563,10 +488,7 @@ "model": "permission.permission", "pk": 33, "fields": { - "model": [ - "note", - "transaction" - ], + "model": 22, "query": "{}", "type": "add", "mask": 2, @@ -574,6 +496,438 @@ "description": "Create any transaction" } }, + { + "model": "permission.permission", + "pk": 34, + "fields": { + "model": 9, + "query": "[\"OR\", {\"valid\": true}, {\"creater\": [\"user\"]}]", + "type": "view", + "mask": 1, + "field": "", + "description": "View valid activites" + } + }, + { + "model": "permission.permission", + "pk": 35, + "fields": { + "model": 9, + "query": "[\"AND\", {\"valid\": false}, {\"creater\": [\"user\"]}]", + "type": "change", + "mask": 1, + "field": "", + "description": "Change our activities" + } + }, + { + "model": "permission.permission", + "pk": 36, + "fields": { + "model": 9, + "query": "{\"creater\": [\"user\"], \"valid\": false}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add activities" + } + }, + { + "model": "permission.permission", + "pk": 37, + "fields": { + "model": 9, + "query": "{}", + "type": "change", + "mask": 2, + "field": "valid", + "description": "Validate activities" + } + }, + { + "model": "permission.permission", + "pk": 38, + "fields": { + "model": 9, + "query": "{}", + "type": "change", + "mask": 2, + "field": "open", + "description": "Open activities" + } + }, + { + "model": "permission.permission", + "pk": 39, + "fields": { + "model": 12, + "query": "{\"inviter\": [\"user\", \"note\"], \"activity__activity_type__can_invite\": true}", + "type": "add", + "mask": 1, + "field": "", + "description": "Invite people to activities" + } + }, + { + "model": "permission.permission", + "pk": 40, + "fields": { + "model": 12, + "query": "{\"inviter\": [\"user\", \"note\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View invited people" + } + }, + { + "model": "permission.permission", + "pk": 41, + "fields": { + "model": 9, + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View all activities" + } + }, + { + "model": "permission.permission", + "pk": 42, + "fields": { + "model": 12, + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View all invited people" + } + }, + { + "model": "permission.permission", + "pk": 43, + "fields": { + "model": 11, + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Manage entries" + } + }, + { + "model": "permission.permission", + "pk": 44, + "fields": { + "model": 13, + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Add invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 45, + "fields": { + "model": 13, + "query": "{}", + "type": "view", + "mask": 1, + "field": "", + "description": "View invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 46, + "fields": { + "model": 13, + "query": "{}", + "type": "change", + "mask": 2, + "field": "valid", + "description": "Validate invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 47, + "fields": { + "model": 15, + "query": "{\"pk\": [\"club\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "", + "description": "Update club" + } + }, + { + "model": "permission.permission", + "pk": 48, + "fields": { + "model": 16, + "query": "{\"user\": [\"user\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View our memberships" + } + }, + { + "model": "permission.permission", + "pk": 49, + "fields": { + "model": 16, + "query": "{\"club\": [\"club\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View club's memberships" + } + }, + { + "model": "permission.permission", + "pk": 50, + "fields": { + "model": 16, + "query": "{\"club\": [\"club\"]}", + "type": "add", + "mask": 2, + "field": "", + "description": "Add a membership to a club" + } + }, + { + "model": "permission.permission", + "pk": 51, + "fields": { + "model": 16, + "query": "{\"club\": [\"club\"]}", + "type": "change", + "mask": 2, + "field": "roles", + "description": "Update user roles" + } + }, + { + "model": "permission.permission", + "pk": 52, + "fields": { + "model": 17, + "query": "{\"user\": [\"user\"]}", + "type": "change", + "mask": 1, + "field": "", + "description": "Change own profile" + } + }, + { + "model": "permission.permission", + "pk": 53, + "fields": { + "model": 17, + "query": "{}", + "type": "change", + "mask": 2, + "field": "", + "description": "Change any profile" + } + }, + { + "model": "permission.permission", + "pk": 54, + "fields": { + "model": 4, + "query": "{}", + "type": "change", + "mask": 2, + "field": "", + "description": "Change any user" + } + }, + { + "model": "permission.permission", + "pk": 55, + "fields": { + "model": 4, + "query": "{}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add user" + } + }, + { + "model": "permission.permission", + "pk": 56, + "fields": { + "model": 17, + "query": "{\"email_confirmed\": false, \"registration_valid\": false}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add profile" + } + }, + { + "model": "permission.permission", + "pk": 57, + "fields": { + "model": 4, + "query": "{\"profile__registration_valid\": false}", + "type": "delete", + "mask": 2, + "field": "", + "description": "Delete pre-registered user" + } + }, + { + "model": "permission.permission", + "pk": 58, + "fields": { + "model": 17, + "query": "{\"registration_valid\": false}", + "type": "delete", + "mask": 2, + "field": "", + "description": "Delete pre-registered user profile" + } + }, + { + "model": "permission.permission", + "pk": 59, + "fields": { + "model": 23, + "query": "{\"destination\": [\"club\", \"note\"]}", + "type": "view", + "mask": 2, + "field": "", + "description": "New club button" + } + }, + { + "model": "permission.permission", + "pk": 60, + "fields": { + "model": 23, + "query": "{\"destination\": [\"club\", \"note\"]}", + "type": "add", + "mask": 2, + "field": "", + "description": "Create club button" + } + }, + { + "model": "permission.permission", + "pk": 61, + "fields": { + "model": 23, + "query": "{\"destination\": [\"club\", \"note\"]}", + "type": "change", + "mask": 2, + "field": "", + "description": "Update club button" + } + }, + { + "model": "permission.permission", + "pk": 62, + "fields": { + "model": 22, + "query": "[\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}]", + "type": "view", + "mask": 1, + "field": "", + "description": "View transactions of a club" + } + }, + { + "model": "permission.permission", + "pk": 63, + "fields": { + "model": 33, + "query": "{}", + "type": "view", + "mask": 3, + "field": "", + "description": "View invoices" + } + }, + { + "model": "permission.permission", + "pk": 64, + "fields": { + "model": 33, + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add invoice" + } + }, + { + "model": "permission.permission", + "pk": 65, + "fields": { + "model": 33, + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Change invoice" + } + }, + { + "model": "permission.permission", + "pk": 66, + "fields": { + "model": 34, + "query": "{}", + "type": "view", + "mask": 3, + "field": "", + "description": "View products" + } + }, + { + "model": "permission.permission", + "pk": 67, + "fields": { + "model": 34, + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add products" + } + }, + { + "model": "permission.permission", + "pk": 68, + "fields": { + "model": 34, + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Change product" + } + }, + { + "model": "permission.permission", + "pk": 69, + "fields": { + "model": 34, + "query": "{}", + "type": "delete", + "mask": 3, + "field": "", + "description": "Delete product" + } + }, { "model": "permission.rolepermissions", "pk": 1, @@ -582,11 +936,18 @@ "permissions": [ 1, 2, + 3, + 4, + 5, 7, 8, 9, 10, - 11 + 11, + 12, + 13, + 48, + 52 ] } }, @@ -595,6 +956,114 @@ "pk": 2, "fields": { "role": 2, + "permissions": [ + 6, + 14, + 15, + 16, + 17, + 18, + 34, + 35, + 36, + 39, + 40 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 4, + "fields": { + "role": 4, + "permissions": [ + 22, + 47, + 49 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 5, + "fields": { + "role": 5, + "permissions": [ + 50, + 51, + 62 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 6, + "fields": { + "role": 6, + "permissions": [ + 19, + 21, + 27, + 59, + 60, + 61, + 20, + 62 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 7, + "fields": { + "role": 7, + "permissions": [ + 33, + 24, + 25, + 26, + 27 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 8, + "fields": { + "role": 8, + "permissions": [ + 32, + 33, + 56, + 58, + 55, + 57, + 53, + 54, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 64, + 65, + 66, + 67, + 68, + 69, + 63 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 9, + "fields": { + "role": 9, "permissions": [ 1, 2, @@ -613,28 +1082,66 @@ 15, 16, 17, - 18 - ] - } - }, - { - "model": "permission.rolepermissions", - "pk": 3, - "fields": { - "role": 8, - "permissions": [ + 18, 19, - 20, 21, - 22 + 22, + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 34, + 35, + 36, + 37, + 38, + 39, + 40, + 41, + 42, + 43, + 44, + 45, + 46, + 47, + 48, + 49, + 50, + 51, + 52, + 53, + 54, + 55, + 56, + 57, + 58, + 59, + 60, + 61, + 20, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69 ] } }, { "model": "permission.rolepermissions", - "pk": 4, + "pk": 10, "fields": { - "role": 4, + "role": 10, "permissions": [ 23, 24, @@ -646,7 +1153,31 @@ 30, 31, 32, - 33 + 33, + 52, + 53, + 54, + 55, + 56, + 57, + 58 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 11, + "fields": { + "role": 11, + "permissions": [ + 37, + 38, + 41, + 42, + 43, + 44, + 45, + 46 ] } } diff --git a/apps/permission/models.py b/apps/permission/models.py index 205f5b41..81174389 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -38,20 +38,33 @@ class InstancedPermission: if permission_type == self.type: self.update_query() - # Don't increase indexes - obj.pk = 0 + # Don't increase indexes, if the primary key is an AutoField + if not hasattr(obj, "pk") or not obj.pk: + obj.pk = 0 + oldpk = None + else: + oldpk = obj.pk + # Ensure previous models are deleted + self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete() # Force insertion, no data verification, no trigger + obj._force_save = True Model.save(obj, force_insert=True) - ret = obj in self.model.model_class().objects.filter(self.query).all() + # We don't want log anything + obj._no_log = True + ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() # Delete testing object + obj._force_delete = True Model.delete(obj) + + # If the primary key was specified, we restore it + obj.pk = oldpk return ret if permission_type == self.type: if self.field and field_name != self.field: return False self.update_query() - return obj in self.model.model_class().objects.filter(self.query).all() + return self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() else: return False @@ -93,6 +106,10 @@ class PermissionMask(models.Model): def __str__(self): return self.description + class Meta: + verbose_name = _("permission mask") + verbose_name_plural = _("permission masks") + class Permission(models.Model): @@ -140,6 +157,8 @@ class Permission(models.Model): class Meta: unique_together = ('model', 'query', 'type', 'field') + verbose_name = _("permission") + verbose_name_plural = _("permissions") def clean(self): self.query = json.dumps(json.loads(self.query)) @@ -280,3 +299,7 @@ class RolePermissions(models.Model): def __str__(self): return str(self.role) + + class Meta: + verbose_name = _("role permissions") + verbose_name_plural = _("role permissions") diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py index 7097085f..40321567 100644 --- a/apps/permission/permissions.py +++ b/apps/permission/permissions.py @@ -44,7 +44,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions): perms = self.get_required_object_permissions(request.method, model_cls) # if not user.has_perms(perms, obj): - if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms): + if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms): # If the user does not have permissions we need to determine if # they have read permissions to see 403, or not, and simply see # a 404 response. diff --git a/apps/permission/signals.py b/apps/permission/signals.py index 1e30f56f..bf54b72f 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -2,8 +2,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.core.exceptions import PermissionDenied -from django.db.models.signals import pre_save, pre_delete, post_save, post_delete -from logs import signals as logs_signals from note_kfet.middlewares import get_current_authenticated_user from permission.backends import PermissionBackend @@ -29,6 +27,9 @@ def pre_save_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_force_save"): + return + user = get_current_authenticated_user() if user is None: # Action performed on shell is always granted @@ -43,7 +44,7 @@ def pre_save_object(sender, instance, **kwargs): # We check if the user can change the model # If the user has all right on a model, then OK - if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance): + if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance): return # In the other case, we check if he/she has the right to change one field @@ -55,35 +56,17 @@ def pre_save_object(sender, instance, **kwargs): # If the field wasn't modified, no need to check the permissions if old_value == new_value: continue - if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): + if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): raise PermissionDenied else: - # We check if the user can add the model - - # While checking permissions, the object will be inserted in the DB, then removed. - # We disable temporary the connectors - pre_save.disconnect(pre_save_object) - pre_delete.disconnect(pre_delete_object) - # We disable also logs connectors - pre_save.disconnect(logs_signals.pre_save_object) - post_save.disconnect(logs_signals.save_object) - post_delete.disconnect(logs_signals.delete_object) - # We check if the user has right to add the object - has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance) - - # Then we reconnect all - pre_save.connect(pre_save_object) - pre_delete.connect(pre_delete_object) - pre_save.connect(logs_signals.pre_save_object) - post_save.connect(logs_signals.save_object) - post_delete.connect(logs_signals.delete_object) + has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance) if not has_perm: raise PermissionDenied -def pre_delete_object(sender, instance, **kwargs): +def pre_delete_object(instance, **kwargs): """ Before a model get deleted, we check the permissions """ @@ -91,6 +74,9 @@ def pre_delete_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_force_delete"): + return + user = get_current_authenticated_user() if user is None: # Action performed on shell is always granted @@ -101,5 +87,5 @@ def pre_delete_object(sender, instance, **kwargs): model_name = model_name_full[1] # We check if the user has rights to delete the object - if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance): + if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance): raise PermissionDenied diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index 8bcd3597..a89c7f49 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import stringfilter from django import template +from note.models import Transaction from note_kfet.middlewares import get_current_authenticated_user, get_current_session from permission.backends import PermissionBackend @@ -19,13 +20,8 @@ def not_empty_model_list(model_name): return False elif user.is_superuser and session.get("permission_mask", 0) >= 42: return True - if session.get("not_empty_model_list_" + model_name, None): - return session.get("not_empty_model_list_" + model_name, None) == 1 - spl = model_name.split(".") - ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) - qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")).all() - session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2 - return session.get("not_empty_model_list_" + model_name) == 1 + qs = model_list(model_name) + return qs.exists() @stringfilter @@ -39,15 +35,54 @@ def not_empty_model_change_list(model_name): return False elif user.is_superuser and session.get("permission_mask", 0) >= 42: return True - if session.get("not_empty_model_change_list_" + model_name, None): - return session.get("not_empty_model_change_list_" + model_name, None) == 1 + qs = model_list(model_name, "change") + return qs.exists() + + +@stringfilter +def model_list(model_name, t="view"): + """ + Return the queryset of all visible instances of the given model. + """ + user = get_current_authenticated_user() + if user is None: + return False spl = model_name.split(".") ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) - qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change")) - session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2 - return session.get("not_empty_model_change_list_" + model_name) == 1 + qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all() + return qs + + +def has_perm(perm, obj): + return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj) + + +def can_create_transaction(): + """ + :return: True iff the authenticated user can create a transaction. + """ + user = get_current_authenticated_user() + session = get_current_session() + if user is None: + return False + elif user.is_superuser and session.get("permission_mask", 0) >= 42: + return True + if session.get("can_create_transaction", None): + return session.get("can_create_transaction", None) == 1 + + empty_transaction = Transaction( + source=user.note, + destination=user.note, + quantity=1, + amount=0, + reason="Check permissions", + ) + session["can_create_transaction"] = PermissionBackend.check_perm(user, "note.add_transaction", empty_transaction) + return session.get("can_create_transaction") == 1 register = template.Library() register.filter('not_empty_model_list', not_empty_model_list) register.filter('not_empty_model_change_list', not_empty_model_change_list) +register.filter('model_list', model_list) +register.filter('has_perm', has_perm) diff --git a/apps/permission/views.py b/apps/permission/views.py new file mode 100644 index 00000000..bbd9872f --- /dev/null +++ b/apps/permission/views.py @@ -0,0 +1,11 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from permission.backends import PermissionBackend + + +class ProtectQuerysetMixin: + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + + return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")) diff --git a/apps/registration/__init__.py b/apps/registration/__init__.py new file mode 100644 index 00000000..700d9f00 --- /dev/null +++ b/apps/registration/__init__.py @@ -0,0 +1,4 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +default_app_config = 'registration.apps.RegistrationConfig' diff --git a/apps/registration/apps.py b/apps/registration/apps.py new file mode 100644 index 00000000..dec89274 --- /dev/null +++ b/apps/registration/apps.py @@ -0,0 +1,10 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class RegistrationConfig(AppConfig): + name = 'registration' + verbose_name = _('registration') diff --git a/apps/registration/forms.py b/apps/registration/forms.py new file mode 100644 index 00000000..cba5c2ae --- /dev/null +++ b/apps/registration/forms.py @@ -0,0 +1,80 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django import forms +from django.contrib.auth.forms import UserCreationForm +from django.contrib.auth.models import User +from django.utils.translation import gettext_lazy as _ +from note.models import NoteSpecial +from note_kfet.inputs import AmountInput + + +class SignUpForm(UserCreationForm): + """ + Pre-register users with all information + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['username'].widget.attrs.pop("autofocus", None) + self.fields['first_name'].widget.attrs.update({"autofocus": "autofocus"}) + self.fields['first_name'].required = True + self.fields['last_name'].required = True + self.fields['email'].required = True + self.fields['email'].help_text = _("This address must be valid.") + + class Meta: + model = User + fields = ('first_name', 'last_name', 'username', 'email', ) + + +class ValidationForm(forms.Form): + """ + Validate the inscription of the new users and pay memberships. + """ + soge = forms.BooleanField( + label=_("Inscription paid by Société Générale"), + required=False, + help_text=_("Check this case is the Société Générale paid the inscription."), + ) + + credit_type = forms.ModelChoiceField( + queryset=NoteSpecial.objects, + label=_("Credit type"), + empty_label=_("No credit"), + required=False, + ) + + credit_amount = forms.IntegerField( + label=_("Credit amount"), + required=False, + initial=0, + widget=AmountInput(), + ) + + last_name = forms.CharField( + label=_("Last name"), + required=False, + ) + + first_name = forms.CharField( + label=_("First name"), + required=False, + ) + + bank = forms.CharField( + label=_("Bank"), + required=False, + ) + + join_BDE = forms.BooleanField( + label=_("Join BDE Club"), + required=False, + initial=True, + ) + + # The user can join the Kfet club at the inscription + join_Kfet = forms.BooleanField( + label=_("Join Kfet Club"), + required=False, + initial=True, + ) diff --git a/apps/registration/migrations/__init__.py b/apps/registration/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/registration/tables.py b/apps/registration/tables.py new file mode 100644 index 00000000..7068f6ca --- /dev/null +++ b/apps/registration/tables.py @@ -0,0 +1,26 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import django_tables2 as tables +from django.contrib.auth.models import User + + +class FutureUserTable(tables.Table): + """ + Display the list of pre-registered users + """ + phone_number = tables.Column(accessor='profile.phone_number') + + section = tables.Column(accessor='profile.section') + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + template_name = 'django_tables2/bootstrap4.html' + fields = ('last_name', 'first_name', 'username', 'email', ) + model = User + row_attrs = { + 'class': 'table-row', + 'data-href': lambda record: record.pk + } diff --git a/apps/registration/tokens.py b/apps/registration/tokens.py new file mode 100644 index 00000000..c5ddc82b --- /dev/null +++ b/apps/registration/tokens.py @@ -0,0 +1,30 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later +# Copied from https://gitlab.crans.org/bombar/codeflix/-/blob/master/codeflix/codeflix/tokens.py + +from django.contrib.auth.tokens import PasswordResetTokenGenerator + + +class AccountActivationTokenGenerator(PasswordResetTokenGenerator): + """ + Create a unique token generator to confirm email addresses. + """ + def _make_hash_value(self, user, timestamp): + """ + Hash the user's primary key and some user state that's sure to change + after an account validation to produce a token that invalidated when + it's used: + 1. The user.profile.email_confirmed field will change upon an account + validation. + 2. The last_login field will usually be updated very shortly after + an account validation. + Failing those things, settings.PASSWORD_RESET_TIMEOUT_DAYS eventually + invalidates the token. + """ + # Truncate microseconds so that tokens are consistent even if the + # database doesn't support microseconds. + login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None) + return str(user.pk) + str(user.profile.email_confirmed) + str(login_timestamp) + str(timestamp) + + +email_validation_token = AccountActivationTokenGenerator() diff --git a/apps/registration/urls.py b/apps/registration/urls.py new file mode 100644 index 00000000..14678cbb --- /dev/null +++ b/apps/registration/urls.py @@ -0,0 +1,18 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from . import views + +app_name = 'registration' +urlpatterns = [ + path('signup/', views.UserCreateView.as_view(), name="signup"), + path('validate_email/sent/', views.UserValidationEmailSentView.as_view(), name='email_validation_sent'), + path('validate_email/resend//', views.UserResendValidationEmailView.as_view(), + name='email_validation_resend'), + path('validate_email///', views.UserValidateView.as_view(), name='email_validation'), + path('validate_user/', views.FutureUserListView.as_view(), name="future_user_list"), + path('validate_user//', views.FutureUserDetailView.as_view(), name="future_user_detail"), + path('validate_user//invalidate/', views.FutureUserInvalidateView.as_view(), name="future_user_invalidate"), +] diff --git a/apps/registration/views.py b/apps/registration/views.py new file mode 100644 index 00000000..35391b05 --- /dev/null +++ b/apps/registration/views.py @@ -0,0 +1,358 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.conf import settings +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.db.models import Q +from django.shortcuts import resolve_url, redirect +from django.urls import reverse_lazy +from django.utils.http import urlsafe_base64_decode +from django.utils.translation import gettext_lazy as _ +from django.views import View +from django.views.generic import CreateView, TemplateView, DetailView, FormView +from django.views.generic.edit import FormMixin +from django_tables2 import SingleTableView +from member.forms import ProfileForm +from member.models import Membership, Club, Role +from note.models import SpecialTransaction, NoteSpecial +from note.templatetags.pretty_money import pretty_money +from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin + +from .forms import SignUpForm, ValidationForm +from .tables import FutureUserTable +from .tokens import email_validation_token + + +class UserCreateView(CreateView): + """ + Une vue pour inscrire un utilisateur et lui créer un profil + """ + + form_class = SignUpForm + success_url = reverse_lazy('registration:email_validation_sent') + template_name = 'registration/signup.html' + second_form = ProfileForm + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["profile_form"] = self.second_form() + + return context + + def form_valid(self, form): + """ + If the form is valid, then the user is created with is_active set to False + so that the user cannot log in until the email has been validated. + The user must also wait that someone validate her/his account. + """ + profile_form = ProfileForm(data=self.request.POST) + if not profile_form.is_valid(): + return self.form_invalid(form) + + # Save the user and the profile + user = form.save(commit=False) + user.is_active = False + profile_form.instance.user = user + profile = profile_form.save(commit=False) + user.profile = profile + user.save() + user.refresh_from_db() + profile.user = user + profile.save() + + user.profile.send_email_validation_link() + + return super().form_valid(form) + + +class UserValidateView(TemplateView): + """ + A view to validate the email address. + """ + title = _("Email validation") + template_name = 'registration/email_validation_complete.html' + + def get(self, *args, **kwargs): + """ + With a given token and user id (in params), validate the email address. + """ + assert 'uidb64' in kwargs and 'token' in kwargs + + self.validlink = False + user = self.get_user(kwargs['uidb64']) + token = kwargs['token'] + + # Validate the token + if user is not None and email_validation_token.check_token(user, token): + self.validlink = True + # The user must wait that someone validates the account before the user can be active and login. + user.is_active = user.profile.registration_valid or user.is_superuser + user.profile.email_confirmed = True + user.save() + user.profile.save() + return super().dispatch(*args, **kwargs) + else: + # Display the "Email validation unsuccessful" page. + return self.render_to_response(self.get_context_data()) + + def get_user(self, uidb64): + """ + Get user from the base64-encoded string. + """ + try: + # urlsafe_base64_decode() decodes to bytestring + uid = urlsafe_base64_decode(uidb64).decode() + user = User.objects.get(pk=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist, ValidationError): + user = None + return user + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['user'] = self.get_user(self.kwargs["uidb64"]) + context['login_url'] = resolve_url(settings.LOGIN_URL) + if self.validlink: + context['validlink'] = True + else: + context.update({ + 'title': _('Email validation unsuccessful'), + 'validlink': False, + }) + return context + + +class UserValidationEmailSentView(TemplateView): + """ + Display the information that the validation link has been sent. + """ + template_name = 'registration/email_validation_email_sent.html' + title = _('Email validation email sent') + + +class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView): + """ + Rensend the email validation link. + """ + model = User + + def get(self, request, *args, **kwargs): + user = self.get_object() + + user.profile.send_email_validation_link() + + url = 'member:user_detail' if user.profile.registration_valid else 'registration:future_user_detail' + return redirect(url, user.id) + + +class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + Display pre-registered users, with a search bar + """ + model = User + table_class = FutureUserTable + template_name = 'registration/future_user_list.html' + + def get_queryset(self, **kwargs): + """ + Filter the table with the given parameter. + :param kwargs: + :return: + """ + qs = super().get_queryset().filter(profile__registration_valid=False) + if "search" in self.request.GET: + pattern = self.request.GET["search"] + + if not pattern: + return qs.none() + + qs = qs.filter( + Q(first_name__iregex=pattern) + | Q(last_name__iregex=pattern) + | Q(profile__section__iregex=pattern) + | Q(username__iregex="^" + pattern) + ) + else: + qs = qs.none() + + return qs[:20] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context["title"] = _("Unregistered users") + + return context + + +class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): + """ + Display information about a pre-registered user, in order to complete the registration. + """ + model = User + form_class = ValidationForm + context_object_name = "user_object" + template_name = "registration/future_profile_detail.html" + + def post(self, request, *args, **kwargs): + form = self.get_form() + self.object = self.get_object() + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_invalid(form) + + def get_queryset(self, **kwargs): + """ + We only display information of a not registered user. + """ + return super().get_queryset().filter(profile__registration_valid=False) + + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + user = self.get_object() + fee = 0 + bde = Club.objects.get(name="BDE") + fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid + kfet = Club.objects.get(name="Kfet") + fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid + ctx["total_fee"] = "{:.02f}".format(fee / 100, ) + + return ctx + + def get_form(self, form_class=None): + form = super().get_form(form_class) + user = self.get_object() + form.fields["last_name"].initial = user.last_name + form.fields["first_name"].initial = user.first_name + return form + + def form_valid(self, form): + user = self.get_object() + + # Get form data + soge = form.cleaned_data["soge"] + credit_type = form.cleaned_data["credit_type"] + credit_amount = form.cleaned_data["credit_amount"] + last_name = form.cleaned_data["last_name"] + first_name = form.cleaned_data["first_name"] + bank = form.cleaned_data["bank"] + join_BDE = form.cleaned_data["join_BDE"] + join_Kfet = form.cleaned_data["join_Kfet"] + + if soge: + # If Société Générale pays the inscription, the user joins the two clubs + join_BDE = True + join_Kfet = True + + if not join_BDE: + form.add_error('join_BDE', _("You must join the BDE.")) + return super().form_invalid(form) + + fee = 0 + bde = Club.objects.get(name="BDE") + bde_fee = bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid + if join_BDE: + fee += bde_fee + kfet = Club.objects.get(name="Kfet") + kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid + if join_Kfet: + fee += kfet_fee + + if soge: + # Fill payment information if Société Générale pays the inscription + credit_type = NoteSpecial.objects.get(special_type="Virement bancaire") + credit_amount = fee + bank = "Société générale" + + print("OK") + + if join_Kfet and not join_BDE: + form.add_error('join_Kfet', _("You must join BDE club before joining Kfet club.")) + + if fee > credit_amount: + # Check if the user credits enough money + form.add_error('credit_type', + _("The entered amount is not enough for the memberships, should be at least {}") + .format(pretty_money(fee))) + return self.form_invalid(form) + + if credit_type is not None and credit_amount > 0: + if not last_name or not first_name or (not bank and credit_type.special_type == "Chèque"): + if not last_name: + form.add_error('last_name', _("This field is required.")) + if not first_name: + form.add_error('first_name', _("This field is required.")) + if not bank and credit_type.special_type == "Chèque": + form.add_error('bank', _("This field is required.")) + return self.form_invalid(form) + + # Save the user and finally validate the registration + # Saving the user creates the associated note + ret = super().form_valid(form) + user.is_active = user.profile.email_confirmed or user.is_superuser + user.profile.registration_valid = True + # Store if Société générale paid for next years + user.profile.soge = soge + user.save() + user.profile.save() + + if credit_type is not None and credit_amount > 0: + # Credit the note + SpecialTransaction.objects.create( + source=credit_type, + destination=user.note, + quantity=1, + amount=credit_amount, + reason="Crédit " + ("Société générale" if soge else credit_type.special_type) + " (Inscription)", + last_name=last_name, + first_name=first_name, + bank=bank, + valid=True, + ) + + if join_BDE: + # Create membership for the user to the BDE starting today + membership = Membership.objects.create( + club=bde, + user=user, + fee=bde_fee, + ) + membership.roles.add(Role.objects.get(name="Adhérent BDE")) + membership.save() + + if join_Kfet: + # Create membership for the user to the Kfet starting today + membership = Membership.objects.create( + club=kfet, + user=user, + fee=kfet_fee, + ) + membership.roles.add(Role.objects.get(name="Adhérent Kfet")) + membership.save() + + return ret + + def get_success_url(self): + return reverse_lazy('member:user_detail', args=(self.get_object().pk, )) + + +class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View): + """ + Delete a pre-registered user. + """ + + def get(self, request, *args, **kwargs): + """ + Delete the pre-registered user which id is given in the URL. + """ + user = User.objects.filter(profile__registration_valid=False)\ + .filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\ + .get(pk=self.kwargs["pk"]) + + user.delete() + + return redirect('registration:future_user_list') diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py index caaa365f..b09a46c7 100644 --- a/apps/treasury/forms.py +++ b/apps/treasury/forms.py @@ -7,6 +7,8 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit from django import forms from django.utils.translation import gettext_lazy as _ +from note_kfet.inputs import DatePickerInput, AmountInput +from permission.backends import PermissionBackend from .models import Invoice, Product, Remittance, SpecialTransactionProxy @@ -19,7 +21,7 @@ class InvoiceForm(forms.ModelForm): # Django forms don't support date fields. We have to add it manually date = forms.DateField( initial=datetime.date.today, - widget=forms.TextInput(attrs={'type': 'date'}) + widget=DatePickerInput() ) def clean_date(self): @@ -30,19 +32,28 @@ class InvoiceForm(forms.ModelForm): exclude = ('bde', ) +class ProductForm(forms.ModelForm): + class Meta: + model = Product + fields = '__all__' + widgets = { + "amount": AmountInput() + } + + # Add a subform per product in the invoice form, and manage correctly the link between the invoice and # its products. The FormSet will search automatically the ForeignKey in the Product model. ProductFormSet = forms.inlineformset_factory( Invoice, Product, - fields='__all__', + form=ProductForm, extra=1, ) class ProductFormSetHelper(FormHelper): """ - Specify some template informations for the product form. + Specify some template information for the product form. """ def __init__(self, form=None): @@ -121,7 +132,8 @@ class LinkTransactionToRemittanceForm(forms.ModelForm): # Add submit button self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) - self.fields["remittance"].queryset = Remittance.objects.filter(closed=False) + self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\ + .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) def clean_last_name(self): """ diff --git a/apps/treasury/models.py b/apps/treasury/models.py index bcd89db9..ca1da3a4 100644 --- a/apps/treasury/models.py +++ b/apps/treasury/models.py @@ -59,6 +59,10 @@ class Invoice(models.Model): verbose_name=_("Acquitted"), ) + class Meta: + verbose_name = _("invoice") + verbose_name_plural = _("invoices") + class Product(models.Model): """ @@ -95,6 +99,10 @@ class Product(models.Model): def total_euros(self): return self.total / 100 + class Meta: + verbose_name = _("product") + verbose_name_plural = _("products") + class RemittanceType(models.Model): """ @@ -109,6 +117,10 @@ class RemittanceType(models.Model): def __str__(self): return str(self.note) + class Meta: + verbose_name = _("remittance type") + verbose_name_plural = _("remittance types") + class Remittance(models.Model): """ @@ -136,6 +148,10 @@ class Remittance(models.Model): verbose_name=_("Closed"), ) + class Meta: + verbose_name = _("remittance") + verbose_name_plural = _("remittances") + @property def transactions(self): """ @@ -187,3 +203,7 @@ class SpecialTransactionProxy(models.Model): null=True, verbose_name=_("Remittance"), ) + + class Meta: + verbose_name = _("special transaction proxy") + verbose_name_plural = _("special transaction proxies") diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 90440566..7361d1d2 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -19,13 +19,15 @@ from django.views.generic.base import View, TemplateView from django_tables2 import SingleTableView from note.models import SpecialTransaction, NoteSpecial from note_kfet.settings.base import BASE_DIR +from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm from .models import Invoice, Product, Remittance, SpecialTransactionProxy from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable -class InvoiceCreateView(LoginRequiredMixin, CreateView): +class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create Invoice """ @@ -50,18 +52,8 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView): def form_valid(self, form): ret = super().form_valid(form) - kwargs = {} - - # The user type amounts in cents. We convert it in euros. - for key in self.request.POST: - value = self.request.POST[key] - if key.endswith("amount") and value: - kwargs[key] = str(int(100 * float(value))) - elif value: - kwargs[key] = value - # For each product, we save it - formset = ProductFormSet(kwargs, instance=form.instance) + formset = ProductFormSet(self.request.POST, instance=form.instance) if formset.is_valid(): for f in formset: # We don't save the product if the designation is not entered, ie. if the line is empty @@ -77,7 +69,7 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView): return reverse_lazy('treasury:invoice_list') -class InvoiceListView(LoginRequiredMixin, SingleTableView): +class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ List existing Invoices """ @@ -85,7 +77,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView): table_class = InvoiceTable -class InvoiceUpdateView(LoginRequiredMixin, UpdateView): +class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Create Invoice """ @@ -112,16 +104,7 @@ class InvoiceUpdateView(LoginRequiredMixin, UpdateView): def form_valid(self, form): ret = super().form_valid(form) - kwargs = {} - # The user type amounts in cents. We convert it in euros. - for key in self.request.POST: - value = self.request.POST[key] - if key.endswith("amount") and value: - kwargs[key] = str(int(100 * float(value))) - elif value: - kwargs[key] = value - - formset = ProductFormSet(kwargs, instance=form.instance) + formset = ProductFormSet(self.request.POST, instance=form.instance) saved = [] # For each product, we save it if formset.is_valid(): @@ -149,7 +132,7 @@ class InvoiceRenderView(LoginRequiredMixin, View): def get(self, request, **kwargs): pk = kwargs["pk"] - invoice = Invoice.objects.get(pk=pk) + invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk) products = Product.objects.filter(invoice=invoice).all() # Informations of the BDE. Should be updated when the school will move. @@ -207,7 +190,7 @@ class InvoiceRenderView(LoginRequiredMixin, View): return response -class RemittanceCreateView(LoginRequiredMixin, CreateView): +class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create Remittance """ @@ -218,12 +201,14 @@ class RemittanceCreateView(LoginRequiredMixin, CreateView): return reverse_lazy('treasury:remittance_list') def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) + context = super().get_context_data(**kwargs) - ctx["table"] = RemittanceTable(data=Remittance.objects.all()) - ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) + context["table"] = RemittanceTable(data=Remittance.objects + .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) + .all()) + context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) - return ctx + return context class RemittanceListView(LoginRequiredMixin, TemplateView): @@ -233,24 +218,30 @@ class RemittanceListView(LoginRequiredMixin, TemplateView): template_name = "treasury/remittance_list.html" def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) + context = super().get_context_data(**kwargs) - ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all()) - ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all()) + context["opened_remittances"] = RemittanceTable( + data=Remittance.objects.filter(closed=False).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) + context["closed_remittances"] = RemittanceTable( + data=Remittance.objects.filter(closed=True).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all()) - ctx["special_transactions_no_remittance"] = SpecialTransactionTable( + context["special_transactions_no_remittance"] = SpecialTransactionTable( data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), - specialtransactionproxy__remittance=None).all(), + specialtransactionproxy__remittance=None).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), exclude=('remittance_remove', )) - ctx["special_transactions_with_remittance"] = SpecialTransactionTable( + context["special_transactions_with_remittance"] = SpecialTransactionTable( data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), - specialtransactionproxy__remittance__closed=False).all(), + specialtransactionproxy__remittance__closed=False).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), exclude=('remittance_add', )) - return ctx + return context -class RemittanceUpdateView(LoginRequiredMixin, UpdateView): +class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Update Remittance """ @@ -261,18 +252,20 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView): return reverse_lazy('treasury:remittance_list') def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) + context = super().get_context_data(**kwargs) - ctx["table"] = RemittanceTable(data=Remittance.objects.all()) - data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all() - ctx["special_transactions"] = SpecialTransactionTable( + context["table"] = RemittanceTable(data=Remittance.objects.filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) + data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter( + PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all() + context["special_transactions"] = SpecialTransactionTable( data=data, exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )) - return ctx + return context -class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): +class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): """ Attach a special transaction to a remittance """ @@ -284,9 +277,9 @@ class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): return reverse_lazy('treasury:remittance_list') def get_context_data(self, **kwargs): - ctx = super().get_context_data(**kwargs) + context = super().get_context_data(**kwargs) - form = ctx["form"] + form = context["form"] form.fields["last_name"].initial = self.object.transaction.last_name form.fields["first_name"].initial = self.object.transaction.first_name form.fields["bank"].initial = self.object.transaction.bank @@ -294,7 +287,7 @@ class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): form.fields["remittance"].queryset = form.fields["remittance"] \ .queryset.filter(remittance_type__note=self.object.transaction.source) - return ctx + return context class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View): diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index c9eda5aa..f616ffd6 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-26 14:40+0100\n" +"POT-Creation-Date: 2020-04-09 21:59+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,72 +18,194 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: apps/activity/apps.py:10 apps/activity/models.py:76 +#: apps/activity/apps.py:10 apps/activity/models.py:102 +#: apps/activity/models.py:111 msgid "activity" msgstr "" -#: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:63 apps/member/models.py:114 -#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:232 -#: templates/member/profile_detail.html:15 +#: apps/activity/forms.py:45 apps/activity/models.py:208 +msgid "You can't invite someone once the activity is started." +msgstr "" + +#: apps/activity/forms.py:48 apps/activity/models.py:211 +msgid "This activity is not validated yet." +msgstr "" + +#: apps/activity/forms.py:58 apps/activity/models.py:219 +msgid "This person has been already invited 5 times this year." +msgstr "" + +#: apps/activity/forms.py:62 apps/activity/models.py:223 +msgid "This person is already invited." +msgstr "" + +#: apps/activity/forms.py:66 apps/activity/models.py:227 +msgid "You can't invite more than 3 people to this activity." +msgstr "" + +#: apps/activity/models.py:23 apps/activity/models.py:48 +#: apps/member/models.py:99 apps/member/models.py:202 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:237 +#: templates/member/club_info.html:13 templates/member/profile_info.html:14 +#: templates/registration/future_profile_detail.html:16 msgid "name" msgstr "" -#: apps/activity/models.py:23 +#: apps/activity/models.py:27 templates/activity/activity_detail.html:39 msgid "can invite" msgstr "" -#: apps/activity/models.py:26 +#: apps/activity/models.py:30 templates/activity/activity_detail.html:43 msgid "guest entry fee" msgstr "" -#: apps/activity/models.py:30 +#: apps/activity/models.py:34 msgid "activity type" msgstr "" -#: apps/activity/models.py:31 +#: apps/activity/models.py:35 msgid "activity types" msgstr "" -#: apps/activity/models.py:48 apps/note/models/transactions.py:70 -#: apps/permission/models.py:91 +#: apps/activity/models.py:53 apps/note/models/transactions.py:74 +#: apps/permission/models.py:103 templates/activity/activity_detail.html:16 msgid "description" msgstr "" -#: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:63 +#: apps/activity/models.py:60 apps/note/models/notes.py:164 +#: apps/note/models/transactions.py:64 +#: templates/activity/activity_detail.html:19 msgid "type" msgstr "" -#: apps/activity/models.py:60 +#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:223 +#: apps/note/models/notes.py:117 +msgid "user" +msgstr "" + +#: apps/activity/models.py:73 templates/activity/activity_detail.html:33 msgid "organizer" msgstr "" -#: apps/activity/models.py:66 +#: apps/activity/models.py:80 templates/activity/activity_detail.html:36 msgid "attendees club" msgstr "" -#: apps/activity/models.py:69 +#: apps/activity/models.py:84 templates/activity/activity_detail.html:22 msgid "start date" msgstr "" -#: apps/activity/models.py:72 +#: apps/activity/models.py:88 templates/activity/activity_detail.html:25 msgid "end date" msgstr "" -#: apps/activity/models.py:77 +#: apps/activity/models.py:93 apps/note/models/transactions.py:139 +#: templates/activity/activity_detail.html:47 +msgid "valid" +msgstr "" + +#: apps/activity/models.py:98 templates/activity/activity_detail.html:61 +msgid "open" +msgstr "" + +#: apps/activity/models.py:103 msgid "activities" msgstr "" -#: apps/activity/models.py:108 +#: apps/activity/models.py:116 +msgid "entry time" +msgstr "" + +#: apps/activity/models.py:122 apps/note/apps.py:14 +#: apps/note/models/notes.py:58 +msgid "note" +msgstr "" + +#: apps/activity/models.py:133 templates/activity/activity_entry.html:38 +msgid "entry" +msgstr "" + +#: apps/activity/models.py:134 templates/activity/activity_entry.html:38 +msgid "entries" +msgstr "" + +#: apps/activity/models.py:141 +msgid "Already entered on " +msgstr "" + +#: apps/activity/models.py:141 apps/activity/tables.py:54 +msgid "{:%Y-%m-%d %H:%M:%S}" +msgstr "" + +#: apps/activity/models.py:149 +msgid "The balance is negative." +msgstr "" + +#: apps/activity/models.py:179 +msgid "last name" +msgstr "" + +#: apps/activity/models.py:184 templates/member/profile_info.html:14 +#: templates/registration/future_profile_detail.html:16 +msgid "first name" +msgstr "" + +#: apps/activity/models.py:191 +msgid "inviter" +msgstr "" + +#: apps/activity/models.py:232 msgid "guest" msgstr "" -#: apps/activity/models.py:109 +#: apps/activity/models.py:233 msgid "guests" msgstr "" +#: apps/activity/models.py:245 +msgid "Invitation" +msgstr "" + +#: apps/activity/tables.py:54 +msgid "Entered on " +msgstr "" + +#: apps/activity/tables.py:55 +msgid "remove" +msgstr "" + +#: apps/activity/tables.py:75 apps/treasury/models.py:138 +msgid "Type" +msgstr "" + +#: apps/activity/tables.py:77 apps/member/forms.py:75 +#: apps/registration/forms.py:55 apps/treasury/forms.py:121 +msgid "Last name" +msgstr "" + +#: apps/activity/tables.py:79 apps/member/forms.py:80 +#: apps/registration/forms.py:60 apps/treasury/forms.py:123 +#: templates/note/transaction_form.html:97 +msgid "First name" +msgstr "" + +#: apps/activity/tables.py:81 apps/note/models/notes.py:67 +msgid "Note" +msgstr "" + +#: apps/activity/tables.py:83 apps/member/tables.py:41 +msgid "Balance" +msgstr "" + +#: apps/activity/views.py:45 templates/base.html:106 +msgid "Activities" +msgstr "" + +#: apps/activity/views.py:154 +msgid "Entry for activity \"{}\"" +msgstr "" + #: apps/api/apps.py:10 msgid "API" msgstr "" @@ -92,10 +214,6 @@ msgstr "" msgid "Logs" msgstr "" -#: apps/logs/models.py:21 apps/note/models/notes.py:117 -msgid "user" -msgstr "" - #: apps/logs/models.py:27 msgid "IP Address" msgstr "" @@ -120,11 +238,12 @@ msgstr "" msgid "create" msgstr "" -#: apps/logs/models.py:61 apps/note/tables.py:147 +#: apps/logs/models.py:61 apps/note/tables.py:144 +#: templates/activity/activity_detail.html:67 msgid "edit" msgstr "" -#: apps/logs/models.py:62 apps/note/tables.py:151 +#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:149 msgid "delete" msgstr "" @@ -140,143 +259,251 @@ msgstr "" msgid "Logs cannot be destroyed." msgstr "" +#: apps/logs/models.py:80 +msgid "changelog" +msgstr "" + +#: apps/logs/models.py:81 +msgid "changelogs" +msgstr "" + #: apps/member/apps.py:14 msgid "member" msgstr "" -#: apps/member/models.py:25 +#: apps/member/forms.py:54 apps/registration/forms.py:35 +msgid "Inscription paid by Société Générale" +msgstr "" + +#: apps/member/forms.py:56 apps/registration/forms.py:37 +msgid "Check this case is the Société Générale paid the inscription." +msgstr "" + +#: apps/member/forms.py:61 apps/registration/forms.py:42 +msgid "Credit type" +msgstr "" + +#: apps/member/forms.py:62 apps/registration/forms.py:43 +msgid "No credit" +msgstr "" + +#: apps/member/forms.py:64 +msgid "You can credit the note of the user." +msgstr "" + +#: apps/member/forms.py:68 apps/registration/forms.py:48 +msgid "Credit amount" +msgstr "" + +#: apps/member/forms.py:85 apps/registration/forms.py:65 +#: apps/treasury/forms.py:125 templates/note/transaction_form.html:103 +msgid "Bank" +msgstr "" + +#: apps/member/models.py:33 +#: templates/registration/future_profile_detail.html:47 msgid "phone number" msgstr "" -#: apps/member/models.py:31 templates/member/profile_detail.html:28 +#: apps/member/models.py:39 templates/member/profile_info.html:27 +#: templates/registration/future_profile_detail.html:41 msgid "section" msgstr "" -#: apps/member/models.py:32 +#: apps/member/models.py:40 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "" -#: apps/member/models.py:38 templates/member/profile_detail.html:31 +#: apps/member/models.py:46 templates/member/profile_info.html:30 +#: templates/registration/future_profile_detail.html:44 msgid "address" msgstr "" -#: apps/member/models.py:44 +#: apps/member/models.py:52 +#: templates/registration/future_profile_detail.html:50 msgid "paid" msgstr "" -#: apps/member/models.py:49 apps/member/models.py:50 -msgid "user profile" +#: apps/member/models.py:53 +msgid "Tells if the user receive a salary." +msgstr "" + +#: apps/member/models.py:58 +msgid "email confirmed" +msgstr "" + +#: apps/member/models.py:63 +msgid "registration valid" msgstr "" #: apps/member/models.py:68 +msgid "Société générale" +msgstr "" + +#: apps/member/models.py:69 +msgid "Has the user ever be paid by the Société générale?" +msgstr "" + +#: apps/member/models.py:74 apps/member/models.py:75 +msgid "user profile" +msgstr "" + +#: apps/member/models.py:104 templates/member/club_info.html:46 +#: templates/registration/future_profile_detail.html:22 msgid "email" msgstr "" -#: apps/member/models.py:73 -msgid "membership fee" +#: apps/member/models.py:111 +msgid "parent club" msgstr "" -#: apps/member/models.py:77 +#: apps/member/models.py:120 +msgid "require memberships" +msgstr "" + +#: apps/member/models.py:121 +msgid "Uncheck if this club don't require memberships." +msgstr "" + +#: apps/member/models.py:126 templates/member/club_info.html:35 +msgid "membership fee (paid students)" +msgstr "" + +#: apps/member/models.py:131 templates/member/club_info.html:38 +msgid "membership fee (unpaid students)" +msgstr "" + +#: apps/member/models.py:137 templates/member/club_info.html:28 msgid "membership duration" msgstr "" -#: apps/member/models.py:78 -msgid "The longest time a membership can last (NULL = infinite)." +#: apps/member/models.py:138 +msgid "The longest time (in days) a membership can last (NULL = infinite)." msgstr "" -#: apps/member/models.py:83 +#: apps/member/models.py:145 templates/member/club_info.html:22 msgid "membership start" msgstr "" -#: apps/member/models.py:84 +#: apps/member/models.py:146 msgid "How long after January 1st the members can renew their membership." msgstr "" -#: apps/member/models.py:89 +#: apps/member/models.py:153 templates/member/club_info.html:25 msgid "membership end" msgstr "" -#: apps/member/models.py:90 +#: apps/member/models.py:154 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." msgstr "" -#: apps/member/models.py:96 apps/note/models/notes.py:139 +#: apps/member/models.py:187 apps/member/models.py:229 +#: apps/note/models/notes.py:139 msgid "club" msgstr "" -#: apps/member/models.py:97 +#: apps/member/models.py:188 msgid "clubs" msgstr "" -#: apps/member/models.py:120 apps/permission/models.py:276 +#: apps/member/models.py:208 apps/permission/models.py:294 msgid "role" msgstr "" -#: apps/member/models.py:121 +#: apps/member/models.py:209 apps/member/models.py:234 msgid "roles" msgstr "" -#: apps/member/models.py:145 +#: apps/member/models.py:239 msgid "membership starts on" msgstr "" -#: apps/member/models.py:148 +#: apps/member/models.py:243 msgid "membership ends on" msgstr "" -#: apps/member/models.py:152 +#: apps/member/models.py:248 msgid "fee" msgstr "" -#: apps/member/models.py:162 +#: apps/member/models.py:266 apps/member/views.py:500 +msgid "User is not a member of the parent club" +msgstr "" + +#: apps/member/models.py:276 apps/member/views.py:509 +msgid "User is already a member of the club" +msgstr "" + +#: apps/member/models.py:314 +#, python-brace-format +msgid "Membership of {user} for the club {club}" +msgstr "" + +#: apps/member/models.py:317 msgid "membership" msgstr "" -#: apps/member/models.py:163 +#: apps/member/models.py:318 msgid "memberships" msgstr "" -#: apps/member/views.py:80 templates/member/profile_detail.html:46 +#: apps/member/tables.py:112 +msgid "Renew" +msgstr "" + +#: apps/member/views.py:62 apps/registration/forms.py:23 +msgid "This address must be valid." +msgstr "" + +#: apps/member/views.py:65 templates/member/profile_info.html:45 +#: templates/registration/future_profile_detail.html:55 msgid "Update Profile" msgstr "" -#: apps/member/views.py:93 +#: apps/member/views.py:75 msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/views.py:146 -#, python-format -msgid "Account #%(id)s: %(username)s" +#: apps/member/views.py:180 +msgid "Search user" msgstr "" -#: apps/member/views.py:216 -msgid "Alias successfully deleted" +#: apps/member/views.py:495 +msgid "" +"This user don't have enough money to join this club, and can't have a " +"negative balance." msgstr "" -#: apps/note/admin.py:120 apps/note/models/transactions.py:95 +#: apps/member/views.py:513 +msgid "The membership must start after {:%m-%d-%Y}." +msgstr "" + +#: apps/member/views.py:518 +msgid "The membership must begin before {:%m-%d-%Y}." +msgstr "" + +#: apps/member/views.py:528 apps/member/views.py:530 apps/member/views.py:532 +#: apps/registration/views.py:286 apps/registration/views.py:288 +#: apps/registration/views.py:290 +msgid "This field is required." +msgstr "" + +#: apps/note/admin.py:120 apps/note/models/transactions.py:99 msgid "source" msgstr "" -#: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:108 +#: apps/note/admin.py:128 apps/note/admin.py:170 +#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:112 msgid "destination" msgstr "" -#: apps/note/apps.py:14 apps/note/models/notes.py:58 -msgid "note" -msgstr "" - -#: apps/note/forms.py:20 -msgid "New Alias" -msgstr "" - -#: apps/note/forms.py:25 +#: apps/note/forms.py:14 msgid "select an image" msgstr "" -#: apps/note/forms.py:26 +#: apps/note/forms.py:15 msgid "Maximal size: 2MB" msgstr "" @@ -310,7 +537,7 @@ msgstr "" msgid "display image" msgstr "" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:118 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:122 msgid "created at" msgstr "" @@ -318,10 +545,6 @@ msgstr "" msgid "notes" msgstr "" -#: apps/note/models/notes.py:67 -msgid "Note" -msgstr "" - #: apps/note/models/notes.py:77 apps/note/models/notes.py:101 msgid "This alias is already taken." msgstr "" @@ -368,7 +591,8 @@ msgstr "" msgid "alias" msgstr "" -#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 +#: apps/note/models/notes.py:211 templates/member/club_info.html:43 +#: templates/member/profile_info.html:36 msgid "aliases" msgstr "" @@ -380,163 +604,239 @@ msgstr "" msgid "An alias with a similar name already exists: {} " msgstr "" -#: apps/note/models/notes.py:247 +#: apps/note/models/notes.py:251 msgid "You can't delete your main alias." msgstr "" -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:30 msgid "transaction category" msgstr "" -#: apps/note/models/transactions.py:32 +#: apps/note/models/transactions.py:31 msgid "transaction categories" msgstr "" -#: apps/note/models/transactions.py:48 +#: apps/note/models/transactions.py:47 msgid "A template with this name already exist" msgstr "" -#: apps/note/models/transactions.py:57 apps/note/models/transactions.py:126 +#: apps/note/models/transactions.py:58 apps/note/models/transactions.py:130 msgid "amount" msgstr "" -#: apps/note/models/transactions.py:58 +#: apps/note/models/transactions.py:59 msgid "in centimes" msgstr "" -#: apps/note/models/transactions.py:76 +#: apps/note/models/transactions.py:70 +msgid "display" +msgstr "" + +#: apps/note/models/transactions.py:80 msgid "transaction template" msgstr "" -#: apps/note/models/transactions.py:77 +#: apps/note/models/transactions.py:81 msgid "transaction templates" msgstr "" -#: apps/note/models/transactions.py:101 apps/note/models/transactions.py:114 +#: apps/note/models/transactions.py:105 apps/note/models/transactions.py:118 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "" -#: apps/note/models/transactions.py:122 +#: apps/note/models/transactions.py:126 msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:130 +#: apps/note/models/transactions.py:134 msgid "reason" msgstr "" -#: apps/note/models/transactions.py:135 -msgid "valid" -msgstr "" - -#: apps/note/models/transactions.py:140 apps/note/tables.py:95 +#: apps/note/models/transactions.py:144 apps/note/tables.py:95 msgid "invalidity reason" msgstr "" -#: apps/note/models/transactions.py:147 +#: apps/note/models/transactions.py:152 msgid "transaction" msgstr "" -#: apps/note/models/transactions.py:148 +#: apps/note/models/transactions.py:153 msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:202 templates/base.html:83 +#: apps/note/models/transactions.py:207 +#: templates/activity/activity_entry.html:13 templates/base.html:84 #: templates/note/transaction_form.html:19 #: templates/note/transaction_form.html:145 msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:188 +#: apps/note/models/transactions.py:227 msgid "Template" msgstr "" -#: apps/note/models/transactions.py:203 +#: apps/note/models/transactions.py:242 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:208 +#: apps/note/models/transactions.py:247 msgid "bank" msgstr "" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:253 +#: templates/activity/activity_entry.html:17 +#: templates/note/transaction_form.html:24 msgid "Credit" msgstr "" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:253 templates/note/transaction_form.html:28 msgid "Debit" msgstr "" -#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +#: apps/note/models/transactions.py:269 apps/note/models/transactions.py:274 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:231 +#: apps/note/models/transactions.py:270 msgid "membership transactions" msgstr "" -#: apps/note/views.py:39 +#: apps/note/tables.py:57 +msgid "Click to invalidate" +msgstr "" + +#: apps/note/tables.py:57 +msgid "Click to validate" +msgstr "" + +#: apps/note/tables.py:93 +msgid "No reason specified" +msgstr "" + +#: apps/note/tables.py:122 apps/note/tables.py:151 +msgid "Delete" +msgstr "" + +#: apps/note/tables.py:146 templates/member/club_info.html:55 +#: templates/note/conso_form.html:121 +msgid "Edit" +msgstr "" + +#: apps/note/views.py:40 msgid "Transfer money" msgstr "" -#: apps/note/views.py:145 templates/base.html:79 +#: apps/note/views.py:109 templates/base.html:79 msgid "Consumptions" msgstr "" -#: apps/permission/models.py:69 apps/permission/models.py:262 +#: apps/permission/models.py:82 apps/permission/models.py:281 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "" -#: apps/permission/models.py:71 apps/permission/models.py:264 +#: apps/permission/models.py:84 apps/permission/models.py:283 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" -#: apps/permission/models.py:84 +#: apps/permission/models.py:97 msgid "rank" msgstr "" -#: apps/permission/models.py:147 +#: apps/permission/models.py:110 +msgid "permission mask" +msgstr "" + +#: apps/permission/models.py:111 +msgid "permission masks" +msgstr "" + +#: apps/permission/models.py:160 +msgid "permission" +msgstr "" + +#: apps/permission/models.py:161 +msgid "permissions" +msgstr "" + +#: apps/permission/models.py:166 msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/treasury/apps.py:11 templates/base.html:102 +#: apps/permission/models.py:304 apps/permission/models.py:305 +msgid "role permissions" +msgstr "" + +#: apps/registration/apps.py:10 +msgid "registration" +msgstr "" + +#: apps/registration/forms.py:70 +msgid "Join BDE Club" +msgstr "" + +#: apps/registration/forms.py:77 +msgid "Join Kfet Club" +msgstr "" + +#: apps/registration/views.py:75 +msgid "Email validation" +msgstr "" + +#: apps/registration/views.py:121 +msgid "Email validation unsuccessful" +msgstr "" + +#: apps/registration/views.py:132 +msgid "Email validation email sent" +msgstr "" + +#: apps/registration/views.py:185 +msgid "Unregistered users" +msgstr "" + +#: apps/registration/views.py:252 +msgid "You must join the BDE." +msgstr "" + +#: apps/registration/views.py:274 +msgid "You must join BDE club before joining Kfet club." +msgstr "" + +#: apps/registration/views.py:279 +msgid "" +"The entered amount is not enough for the memberships, should be at least {}" +msgstr "" + +#: apps/treasury/apps.py:12 templates/base.html:111 msgid "Treasury" msgstr "" -#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: apps/treasury/forms.py:85 apps/treasury/forms.py:133 +#: templates/activity/activity_form.html:9 +#: templates/activity/activity_invite.html:8 #: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +#: templates/member/add_members.html:14 templates/member/club_form.html:9 +#: templates/treasury/invoice_form.html:46 msgid "Submit" msgstr "" -#: apps/treasury/forms.py:58 +#: apps/treasury/forms.py:87 msgid "Close" msgstr "" -#: apps/treasury/forms.py:65 +#: apps/treasury/forms.py:96 msgid "Remittance is already closed." msgstr "" -#: apps/treasury/forms.py:70 +#: apps/treasury/forms.py:101 msgid "You can't change the type of the remittance." msgstr "" -#: apps/treasury/forms.py:84 -msgid "Last name" -msgstr "" - -#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 -msgid "First name" -msgstr "" - -#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 -msgid "Bank" -msgstr "" - -#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 -#: templates/note/transaction_form.html:128 +#: apps/treasury/forms.py:127 apps/treasury/tables.py:47 +#: templates/note/transaction_form.html:133 #: templates/treasury/remittance_form.html:18 msgid "Amount" msgstr "" @@ -557,7 +857,7 @@ msgstr "" msgid "Description" msgstr "" -#: apps/treasury/models.py:46 templates/note/transaction_form.html:86 +#: apps/treasury/models.py:46 templates/note/transaction_form.html:91 msgid "Name" msgstr "" @@ -573,66 +873,102 @@ msgstr "" msgid "Acquitted" msgstr "" -#: apps/treasury/models.py:75 -msgid "Designation" +#: apps/treasury/models.py:63 +msgid "invoice" +msgstr "" + +#: apps/treasury/models.py:64 +msgid "invoices" msgstr "" #: apps/treasury/models.py:79 -msgid "Quantity" +msgid "Designation" msgstr "" #: apps/treasury/models.py:83 +msgid "Quantity" +msgstr "" + +#: apps/treasury/models.py:87 msgid "Unit price" msgstr "" -#: apps/treasury/models.py:120 +#: apps/treasury/models.py:103 +msgid "product" +msgstr "" + +#: apps/treasury/models.py:104 +msgid "products" +msgstr "" + +#: apps/treasury/models.py:121 +msgid "remittance type" +msgstr "" + +#: apps/treasury/models.py:122 +msgid "remittance types" +msgstr "" + +#: apps/treasury/models.py:132 msgid "Date" msgstr "" -#: apps/treasury/models.py:126 -msgid "Type" -msgstr "" - -#: apps/treasury/models.py:131 +#: apps/treasury/models.py:143 msgid "Comment" msgstr "" -#: apps/treasury/models.py:136 +#: apps/treasury/models.py:148 msgid "Closed" msgstr "" -#: apps/treasury/models.py:159 +#: apps/treasury/models.py:152 +msgid "remittance" +msgstr "" + +#: apps/treasury/models.py:153 +msgid "remittances" +msgstr "" + +#: apps/treasury/models.py:185 msgid "Remittance #{:d}: {}" msgstr "" -#: apps/treasury/models.py:178 apps/treasury/tables.py:64 -#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: apps/treasury/models.py:204 apps/treasury/tables.py:76 +#: apps/treasury/tables.py:84 templates/treasury/invoice_list.html:13 #: templates/treasury/remittance_list.html:13 msgid "Remittance" msgstr "" -#: apps/treasury/tables.py:16 +#: apps/treasury/models.py:208 +msgid "special transaction proxy" +msgstr "" + +#: apps/treasury/models.py:209 +msgid "special transaction proxies" +msgstr "" + +#: apps/treasury/tables.py:19 msgid "Invoice #{:d}" msgstr "" -#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: apps/treasury/tables.py:22 templates/treasury/invoice_list.html:10 #: templates/treasury/remittance_list.html:10 msgid "Invoice" msgstr "" -#: apps/treasury/tables.py:38 +#: apps/treasury/tables.py:45 msgid "Transaction count" msgstr "" -#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +#: apps/treasury/tables.py:50 apps/treasury/tables.py:52 msgid "View" msgstr "" -#: apps/treasury/tables.py:66 +#: apps/treasury/tables.py:78 msgid "Add" msgstr "" -#: apps/treasury/tables.py:74 +#: apps/treasury/tables.py:86 msgid "Remove" msgstr "" @@ -643,28 +979,95 @@ msgid "" "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:151 +#: note_kfet/settings/base.py:153 msgid "German" msgstr "" -#: note_kfet/settings/base.py:152 +#: note_kfet/settings/base.py:154 msgid "English" msgstr "" -#: note_kfet/settings/base.py:153 +#: note_kfet/settings/base.py:155 msgid "French" msgstr "" +#: templates/activity/activity_detail.html:29 +msgid "creater" +msgstr "" + +#: templates/activity/activity_detail.html:50 +msgid "opened" +msgstr "" + +#: templates/activity/activity_detail.html:57 +msgid "Entry page" +msgstr "" + +#: templates/activity/activity_detail.html:61 +msgid "close" +msgstr "" + +#: templates/activity/activity_detail.html:64 +msgid "invalidate" +msgstr "" + +#: templates/activity/activity_detail.html:64 +msgid "validate" +msgstr "" + +#: templates/activity/activity_detail.html:70 +msgid "Invite" +msgstr "" + +#: templates/activity/activity_detail.html:77 +msgid "Guests list" +msgstr "" + +#: templates/activity/activity_entry.html:22 +#: templates/note/transaction_form.html:33 +msgid "Entries" +msgstr "" + +#: templates/activity/activity_entry.html:30 +msgid "Return to activity page" +msgstr "" + +#: templates/activity/activity_list.html:5 +msgid "Upcoming activities" +msgstr "" + +#: templates/activity/activity_list.html:10 +msgid "There is no planned activity." +msgstr "" + +#: templates/activity/activity_list.html:14 +msgid "New activity" +msgstr "" + +#: templates/activity/activity_list.html:18 +msgid "All activities" +msgstr "" + #: templates/base.html:13 msgid "The ENS Paris-Saclay BDE note." msgstr "" -#: templates/base.html:87 +#: templates/base.html:89 +msgid "Users" +msgstr "" + +#: templates/base.html:94 msgid "Clubs" msgstr "" -#: templates/base.html:92 -msgid "Activities" +#: templates/base.html:100 +msgid "Registrations" +msgstr "" + +#: templates/base.html:150 +msgid "" +"Your e-mail address is not validated. Please check your mail inbox and click " +"on the validation link." msgstr "" #: templates/cas_server/base.html:7 @@ -722,32 +1125,48 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/member/club_detail.html:10 -msgid "Membership starts on" +#: templates/member/alias_update.html:5 +msgid "Add alias" msgstr "" -#: templates/member/club_detail.html:12 -msgid "Membership ends on" +#: templates/member/club_info.html:17 +msgid "Club Parent" msgstr "" -#: templates/member/club_detail.html:14 -msgid "Membership duration" +#: templates/member/club_info.html:29 +msgid "days" msgstr "" -#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34 -msgid "balance" +#: templates/member/club_info.html:32 +msgid "membership fee" msgstr "" -#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75 -msgid "Transaction history" +#: templates/member/club_info.html:52 +msgid "Add member" msgstr "" -#: templates/member/club_form.html:6 -msgid "Clubs list" +#: templates/member/club_info.html:59 templates/member/profile_info.html:48 +msgid "View Profile" msgstr "" #: templates/member/club_list.html:8 -msgid "New club" +msgid "search clubs" +msgstr "" + +#: templates/member/club_list.html:12 +msgid "Create club" +msgstr "" + +#: templates/member/club_list.html:19 +msgid "club listing " +msgstr "" + +#: templates/member/club_tables.html:6 +msgid "Member of the Club" +msgstr "" + +#: templates/member/club_tables.html:17 templates/member/profile_tables.html:28 +msgid "Transaction history" msgstr "" #: templates/member/manage_auth_tokens.html:16 @@ -762,35 +1181,45 @@ msgstr "" msgid "Regenerate token" msgstr "" -#: templates/member/profile_alias.html:10 -msgid "Add alias" +#: templates/member/profile_info.html:5 +#: templates/registration/future_profile_detail.html:12 +msgid "Account #" msgstr "" -#: templates/member/profile_detail.html:15 -msgid "first name" -msgstr "" - -#: templates/member/profile_detail.html:18 +#: templates/member/profile_info.html:17 +#: templates/registration/future_profile_detail.html:19 msgid "username" msgstr "" -#: templates/member/profile_detail.html:21 +#: templates/member/profile_info.html:20 +#: templates/registration/future_profile_detail.html:34 msgid "password" msgstr "" -#: templates/member/profile_detail.html:24 +#: templates/member/profile_info.html:23 +#: templates/registration/future_profile_detail.html:37 msgid "Change password" msgstr "" -#: templates/member/profile_detail.html:42 +#: templates/member/profile_info.html:33 +msgid "balance" +msgstr "" + +#: templates/member/profile_info.html:41 msgid "Manage auth token" msgstr "" -#: templates/member/profile_detail.html:49 -msgid "View Profile" +#: templates/member/profile_tables.html:7 +#: templates/registration/future_profile_detail.html:28 +msgid "This user doesn't have confirmed his/her e-mail address." msgstr "" -#: templates/member/profile_detail.html:62 +#: templates/member/profile_tables.html:8 +#: templates/registration/future_profile_detail.html:29 +msgid "Click here to resend a validation link." +msgstr "" + +#: templates/member/profile_tables.html:15 msgid "View my memberships" msgstr "" @@ -798,12 +1227,12 @@ msgstr "" msgid "Save Changes" msgstr "" -#: templates/member/signup.html:5 templates/member/signup.html:8 -#: templates/member/signup.html:14 -msgid "Sign up" +#: templates/member/user_list.html:14 +#: templates/registration/future_user_list.html:17 +msgid "There is no pending user with this pattern." msgstr "" -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:50 +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:55 msgid "Select emitters" msgstr "" @@ -819,10 +1248,6 @@ msgstr "" msgid "Most used buttons" msgstr "" -#: templates/note/conso_form.html:121 -msgid "Edit" -msgstr "" - #: templates/note/conso_form.html:126 msgid "Single consumptions" msgstr "" @@ -839,36 +1264,20 @@ msgstr "" msgid "Gift" msgstr "" -#: templates/note/transaction_form.html:68 +#: templates/note/transaction_form.html:73 msgid "External payment" msgstr "" -#: templates/note/transaction_form.html:76 +#: templates/note/transaction_form.html:81 msgid "Transfer type" msgstr "" -#: templates/note/transaction_form.html:86 -msgid "Name" -msgstr "" - -#: templates/note/transaction_form.html:92 -msgid "First name" -msgstr "" - -#: templates/note/transaction_form.html:98 -msgid "Bank" -msgstr "" - -#: templates/note/transaction_form.html:111 +#: templates/note/transaction_form.html:116 #: templates/note/transaction_form.html:169 #: templates/note/transaction_form.html:176 msgid "Select receivers" msgstr "" -#: templates/note/transaction_form.html:128 -msgid "Amount" -msgstr "" - #: templates/note/transaction_form.html:138 msgid "Reason" msgstr "" @@ -889,18 +1298,58 @@ msgstr "" msgid "search button" msgstr "" +#: templates/note/transactiontemplate_list.html:13 +msgid "New button" +msgstr "" + #: templates/note/transactiontemplate_list.html:20 msgid "buttons listing " msgstr "" -#: templates/note/transactiontemplate_list.html:71 +#: templates/note/transactiontemplate_list.html:70 msgid "button successfully deleted " msgstr "" -#: templates/note/transactiontemplate_list.html:75 +#: templates/note/transactiontemplate_list.html:74 msgid "Unable to delete button " msgstr "" +#: templates/registration/email_validation_complete.html:6 +msgid "Your email have successfully been validated." +msgstr "" + +#: templates/registration/email_validation_complete.html:8 +#, python-format +msgid "You can now log in." +msgstr "" + +#: templates/registration/email_validation_complete.html:10 +msgid "" +"You must pay now your membership in the Kfet to complete your registration." +msgstr "" + +#: templates/registration/email_validation_complete.html:13 +msgid "" +"The link was invalid. The token may have expired. Please send us an email to " +"activate your account." +msgstr "" + +#: templates/registration/future_profile_detail.html:56 +msgid "Delete registration" +msgstr "" + +#: templates/registration/future_profile_detail.html:64 +msgid "Validate account" +msgstr "" + +#: templates/registration/future_profile_detail.html:71 +msgid "Validate registration" +msgstr "" + +#: templates/registration/future_user_list.html:7 +msgid "New user" +msgstr "" + #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" @@ -934,6 +1383,36 @@ msgstr "" msgid "Forgotten your password or username?" msgstr "" +#: templates/registration/mails/email_validation_email.html:3 +msgid "Hi" +msgstr "" + +#: templates/registration/mails/email_validation_email.html:5 +msgid "" +"You recently registered on the Note Kfet. Please click on the link below to " +"confirm your registration." +msgstr "" + +#: templates/registration/mails/email_validation_email.html:9 +msgid "" +"This link is only valid for a couple of days, after that you will need to " +"contact us to validate your email." +msgstr "" + +#: templates/registration/mails/email_validation_email.html:11 +msgid "" +"After that, you'll have to wait that someone validates your account before " +"you can log in. You will need to pay your membership in the Kfet." +msgstr "" + +#: templates/registration/mails/email_validation_email.html:13 +msgid "Thanks" +msgstr "" + +#: templates/registration/mails/email_validation_email.html:15 +msgid "The Note Kfet team." +msgstr "" + #: templates/registration/password_change_done.html:8 msgid "Your password was changed." msgstr "" @@ -987,15 +1466,20 @@ msgstr "" msgid "Reset my password" msgstr "" +#: templates/registration/signup.html:5 templates/registration/signup.html:8 +#: templates/registration/signup.html:14 +msgid "Sign up" +msgstr "" + #: templates/treasury/invoice_form.html:6 msgid "Invoices list" msgstr "" -#: templates/treasury/invoice_form.html:42 +#: templates/treasury/invoice_form.html:41 msgid "Add product" msgstr "" -#: templates/treasury/invoice_form.html:43 +#: templates/treasury/invoice_form.html:42 msgid "Remove product" msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index ca43d5a4..27636a65 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-26 14:40+0100\n" +"POT-Creation-Date: 2020-04-09 21:59+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -13,83 +13,202 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: apps/activity/apps.py:10 apps/activity/models.py:76 +#: apps/activity/apps.py:10 apps/activity/models.py:102 +#: apps/activity/models.py:111 msgid "activity" msgstr "activité" -#: apps/activity/models.py:19 apps/activity/models.py:44 -#: apps/member/models.py:63 apps/member/models.py:114 -#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 -#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:232 -#: templates/member/profile_detail.html:15 +#: apps/activity/forms.py:45 apps/activity/models.py:208 +msgid "You can't invite someone once the activity is started." +msgstr "" +"Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré." + +#: apps/activity/forms.py:48 apps/activity/models.py:211 +msgid "This activity is not validated yet." +msgstr "Cette activité n'est pas encore validée." + +#: apps/activity/forms.py:58 apps/activity/models.py:219 +msgid "This person has been already invited 5 times this year." +msgstr "Cette personne a déjà été invitée 5 fois cette année." + +#: apps/activity/forms.py:62 apps/activity/models.py:223 +msgid "This person is already invited." +msgstr "Cette personne est déjà invitée." + +#: apps/activity/forms.py:66 apps/activity/models.py:227 +msgid "You can't invite more than 3 people to this activity." +msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité." + +#: apps/activity/models.py:23 apps/activity/models.py:48 +#: apps/member/models.py:99 apps/member/models.py:202 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 +#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:237 +#: templates/member/club_info.html:13 templates/member/profile_info.html:14 +#: templates/registration/future_profile_detail.html:16 msgid "name" msgstr "nom" -#: apps/activity/models.py:23 +#: apps/activity/models.py:27 templates/activity/activity_detail.html:39 msgid "can invite" msgstr "peut inviter" -#: apps/activity/models.py:26 +#: apps/activity/models.py:30 templates/activity/activity_detail.html:43 msgid "guest entry fee" msgstr "cotisation de l'entrée invité" -#: apps/activity/models.py:30 +#: apps/activity/models.py:34 msgid "activity type" msgstr "type d'activité" -#: apps/activity/models.py:31 +#: apps/activity/models.py:35 msgid "activity types" msgstr "types d'activité" -#: apps/activity/models.py:48 apps/note/models/transactions.py:70 -#: apps/permission/models.py:91 +#: apps/activity/models.py:53 apps/note/models/transactions.py:74 +#: apps/permission/models.py:103 templates/activity/activity_detail.html:16 msgid "description" msgstr "description" -#: apps/activity/models.py:54 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:63 +#: apps/activity/models.py:60 apps/note/models/notes.py:164 +#: apps/note/models/transactions.py:64 +#: templates/activity/activity_detail.html:19 msgid "type" msgstr "type" -#: apps/activity/models.py:60 +#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:223 +#: apps/note/models/notes.py:117 +msgid "user" +msgstr "utilisateur" + +#: apps/activity/models.py:73 templates/activity/activity_detail.html:33 msgid "organizer" msgstr "organisateur" -#: apps/activity/models.py:66 +#: apps/activity/models.py:80 templates/activity/activity_detail.html:36 msgid "attendees club" -msgstr "" +msgstr "club attendu" -#: apps/activity/models.py:69 +#: apps/activity/models.py:84 templates/activity/activity_detail.html:22 msgid "start date" msgstr "date de début" -#: apps/activity/models.py:72 +#: apps/activity/models.py:88 templates/activity/activity_detail.html:25 msgid "end date" msgstr "date de fin" -#: apps/activity/models.py:77 +#: apps/activity/models.py:93 apps/note/models/transactions.py:139 +#: templates/activity/activity_detail.html:47 +msgid "valid" +msgstr "valide" + +#: apps/activity/models.py:98 templates/activity/activity_detail.html:61 +msgid "open" +msgstr "ouvrir" + +#: apps/activity/models.py:103 msgid "activities" msgstr "activités" -#: apps/activity/models.py:108 +#: apps/activity/models.py:116 +msgid "entry time" +msgstr "heure d'entrée" + +#: apps/activity/models.py:122 apps/note/apps.py:14 +#: apps/note/models/notes.py:58 +msgid "note" +msgstr "note" + +#: apps/activity/models.py:133 templates/activity/activity_entry.html:38 +msgid "entry" +msgstr "entrée" + +#: apps/activity/models.py:134 templates/activity/activity_entry.html:38 +msgid "entries" +msgstr "entrées" + +#: apps/activity/models.py:141 +msgid "Already entered on " +msgstr "Déjà rentré le " + +#: apps/activity/models.py:141 apps/activity/tables.py:54 +msgid "{:%Y-%m-%d %H:%M:%S}" +msgstr "{:%d/%m/%Y %H:%M:%S}" + +#: apps/activity/models.py:149 +msgid "The balance is negative." +msgstr "La note est en négatif." + +#: apps/activity/models.py:179 +msgid "last name" +msgstr "nom de famille" + +#: apps/activity/models.py:184 templates/member/profile_info.html:14 +#: templates/registration/future_profile_detail.html:16 +msgid "first name" +msgstr "prénom" + +#: apps/activity/models.py:191 +msgid "inviter" +msgstr "hôte" + +#: apps/activity/models.py:232 msgid "guest" msgstr "invité" -#: apps/activity/models.py:109 +#: apps/activity/models.py:233 msgid "guests" msgstr "invités" +#: apps/activity/models.py:245 +msgid "Invitation" +msgstr "Invitation" + +#: apps/activity/tables.py:54 +msgid "Entered on " +msgstr "Entré le " + +#: apps/activity/tables.py:55 +msgid "remove" +msgstr "supprimer" + +#: apps/activity/tables.py:75 apps/treasury/models.py:138 +msgid "Type" +msgstr "Type" + +#: apps/activity/tables.py:77 apps/member/forms.py:75 +#: apps/registration/forms.py:55 apps/treasury/forms.py:121 +msgid "Last name" +msgstr "Nom de famille" + +#: apps/activity/tables.py:79 apps/member/forms.py:80 +#: apps/registration/forms.py:60 apps/treasury/forms.py:123 +#: templates/note/transaction_form.html:97 +msgid "First name" +msgstr "Prénom" + +#: apps/activity/tables.py:81 apps/note/models/notes.py:67 +msgid "Note" +msgstr "Note" + +#: apps/activity/tables.py:83 apps/member/tables.py:41 +msgid "Balance" +msgstr "Solde du compte" + +#: apps/activity/views.py:45 templates/base.html:106 +msgid "Activities" +msgstr "Activités" + +#: apps/activity/views.py:154 +msgid "Entry for activity \"{}\"" +msgstr "Entrées pour l'activité « {} »" + #: apps/api/apps.py:10 msgid "API" -msgstr "" +msgstr "API" #: apps/logs/apps.py:11 msgid "Logs" -msgstr "" - -#: apps/logs/models.py:21 apps/note/models/notes.py:117 -msgid "user" -msgstr "utilisateur" +msgstr "Logs" #: apps/logs/models.py:27 msgid "IP Address" @@ -115,11 +234,12 @@ msgstr "Nouvelles données" msgid "create" msgstr "Créer" -#: apps/logs/models.py:61 apps/note/tables.py:147 +#: apps/logs/models.py:61 apps/note/tables.py:144 +#: templates/activity/activity_detail.html:67 msgid "edit" msgstr "Modifier" -#: apps/logs/models.py:62 apps/note/tables.py:151 +#: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:149 msgid "delete" msgstr "Supprimer" @@ -135,65 +255,143 @@ msgstr "Date" msgid "Logs cannot be destroyed." msgstr "Les logs ne peuvent pas être détruits." +#: apps/logs/models.py:80 +msgid "changelog" +msgstr "" + +#: apps/logs/models.py:81 +msgid "changelogs" +msgstr "" + #: apps/member/apps.py:14 msgid "member" msgstr "adhérent" -#: apps/member/models.py:25 +#: apps/member/forms.py:54 apps/registration/forms.py:35 +msgid "Inscription paid by Société Générale" +msgstr "Inscription payée par la Société générale" + +#: apps/member/forms.py:56 apps/registration/forms.py:37 +msgid "Check this case is the Société Générale paid the inscription." +msgstr "Cochez cette case si la Société Générale a payé l'inscription." + +#: apps/member/forms.py:61 apps/registration/forms.py:42 +msgid "Credit type" +msgstr "Type de rechargement" + +#: apps/member/forms.py:62 apps/registration/forms.py:43 +msgid "No credit" +msgstr "Pas de rechargement" + +#: apps/member/forms.py:64 +msgid "You can credit the note of the user." +msgstr "Vous pouvez créditer la note de l'utisateur avant l'adhésion." + +#: apps/member/forms.py:68 apps/registration/forms.py:48 +msgid "Credit amount" +msgstr "Montant à créditer" + +#: apps/member/forms.py:85 apps/registration/forms.py:65 +#: apps/treasury/forms.py:125 templates/note/transaction_form.html:103 +msgid "Bank" +msgstr "Banque" + +#: apps/member/models.py:33 +#: templates/registration/future_profile_detail.html:47 msgid "phone number" msgstr "numéro de téléphone" -#: apps/member/models.py:31 templates/member/profile_detail.html:28 +#: apps/member/models.py:39 templates/member/profile_info.html:27 +#: templates/registration/future_profile_detail.html:41 msgid "section" msgstr "section" -#: apps/member/models.py:32 +#: apps/member/models.py:40 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -#: apps/member/models.py:38 templates/member/profile_detail.html:31 +#: apps/member/models.py:46 templates/member/profile_info.html:30 +#: templates/registration/future_profile_detail.html:44 msgid "address" msgstr "adresse" -#: apps/member/models.py:44 +#: apps/member/models.py:52 +#: templates/registration/future_profile_detail.html:50 msgid "paid" msgstr "payé" -#: apps/member/models.py:49 apps/member/models.py:50 +#: apps/member/models.py:53 +msgid "Tells if the user receive a salary." +msgstr "Indique si l'utilisateur perçoit un salaire." + +#: apps/member/models.py:58 +msgid "email confirmed" +msgstr "adresse email confirmée" + +#: apps/member/models.py:63 +msgid "registration valid" +msgstr "inscription valid" + +#: apps/member/models.py:68 +msgid "Société générale" +msgstr "Société générale" + +#: apps/member/models.py:69 +msgid "Has the user ever be paid by the Société générale?" +msgstr "Est-ce que l'utilisateur a déjà été payé par la Société Générale ?" + +#: apps/member/models.py:74 apps/member/models.py:75 msgid "user profile" msgstr "profil utilisateur" -#: apps/member/models.py:68 +#: apps/member/models.py:104 templates/member/club_info.html:46 +#: templates/registration/future_profile_detail.html:22 msgid "email" msgstr "courriel" -#: apps/member/models.py:73 -msgid "membership fee" -msgstr "cotisation pour adhérer" +#: apps/member/models.py:111 +msgid "parent club" +msgstr "club parent" -#: apps/member/models.py:77 +#: apps/member/models.py:120 +msgid "require memberships" +msgstr "nécessite des adhésions" + +#: apps/member/models.py:121 +msgid "Uncheck if this club don't require memberships." +msgstr "Décochez si ce club n'utilise pas d'adhésions." + +#: apps/member/models.py:126 templates/member/club_info.html:35 +msgid "membership fee (paid students)" +msgstr "cotisation pour adhérer (normalien élève)" + +#: apps/member/models.py:131 templates/member/club_info.html:38 +msgid "membership fee (unpaid students)" +msgstr "cotisation pour adhérer (normalien étudiant)" + +#: apps/member/models.py:137 templates/member/club_info.html:28 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:78 -msgid "The longest time a membership can last (NULL = infinite)." -msgstr "La durée maximale d'une adhésion (NULL = infinie)." +#: apps/member/models.py:138 +msgid "The longest time (in days) a membership can last (NULL = infinite)." +msgstr "La durée maximale (en jours) d'une adhésion (NULL = infinie)." -#: apps/member/models.py:83 +#: apps/member/models.py:145 templates/member/club_info.html:22 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:84 +#: apps/member/models.py:146 msgid "How long after January 1st the members can renew their membership." msgstr "" "Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur " "adhésion." -#: apps/member/models.py:89 +#: apps/member/models.py:153 templates/member/club_info.html:25 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:90 +#: apps/member/models.py:154 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." @@ -201,81 +399,113 @@ msgstr "" "Combien de temps l'adhésion peut durer après le 1er Janvier de l'année " "suivante avant que les adhérents peuvent renouveler leur adhésion." -#: apps/member/models.py:96 apps/note/models/notes.py:139 +#: apps/member/models.py:187 apps/member/models.py:229 +#: apps/note/models/notes.py:139 msgid "club" msgstr "club" -#: apps/member/models.py:97 +#: apps/member/models.py:188 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:120 apps/permission/models.py:276 +#: apps/member/models.py:208 apps/permission/models.py:294 msgid "role" msgstr "rôle" -#: apps/member/models.py:121 +#: apps/member/models.py:209 apps/member/models.py:234 msgid "roles" msgstr "rôles" -#: apps/member/models.py:145 +#: apps/member/models.py:239 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:148 +#: apps/member/models.py:243 msgid "membership ends on" -msgstr "l'adhésion finie le" +msgstr "l'adhésion finit le" -#: apps/member/models.py:152 +#: apps/member/models.py:248 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:162 +#: apps/member/models.py:266 apps/member/views.py:500 +msgid "User is not a member of the parent club" +msgstr "L'utilisateur n'est pas membre du club parent" + +#: apps/member/models.py:276 apps/member/views.py:509 +msgid "User is already a member of the club" +msgstr "L'utilisateur est déjà membre du club" + +#: apps/member/models.py:314 +#, python-brace-format +msgid "Membership of {user} for the club {club}" +msgstr "Adhésion de {user} pour le club {club}" + +#: apps/member/models.py:317 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:163 +#: apps/member/models.py:318 msgid "memberships" msgstr "adhésions" -#: apps/member/views.py:80 templates/member/profile_detail.html:46 +#: apps/member/tables.py:112 +msgid "Renew" +msgstr "Renouveler" + +#: apps/member/views.py:62 apps/registration/forms.py:23 +msgid "This address must be valid." +msgstr "Cette adresse doit être valide." + +#: apps/member/views.py:65 templates/member/profile_info.html:45 +#: templates/registration/future_profile_detail.html:55 msgid "Update Profile" msgstr "Modifier le profil" -#: apps/member/views.py:93 +#: apps/member/views.py:75 msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà." -#: apps/member/views.py:146 -#, python-format -msgid "Account #%(id)s: %(username)s" -msgstr "Compte n°%(id)s : %(username)s" +#: apps/member/views.py:180 +msgid "Search user" +msgstr "Chercher un utilisateur" -#: apps/member/views.py:216 -msgid "Alias successfully deleted" -msgstr "L'alias a bien été supprimé" +#: apps/member/views.py:495 +msgid "" +"This user don't have enough money to join this club, and can't have a " +"negative balance." +msgstr "" +"Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas " +"avoir un solde négatif." -#: apps/note/admin.py:120 apps/note/models/transactions.py:95 +#: apps/member/views.py:513 +msgid "The membership must start after {:%m-%d-%Y}." +msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}." + +#: apps/member/views.py:518 +msgid "The membership must begin before {:%m-%d-%Y}." +msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}." + +#: apps/member/views.py:528 apps/member/views.py:530 apps/member/views.py:532 +#: apps/registration/views.py:286 apps/registration/views.py:288 +#: apps/registration/views.py:290 +msgid "This field is required." +msgstr "Ce champ est requis." + +#: apps/note/admin.py:120 apps/note/models/transactions.py:99 msgid "source" msgstr "source" -#: apps/note/admin.py:128 apps/note/admin.py:156 -#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:108 +#: apps/note/admin.py:128 apps/note/admin.py:170 +#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:112 msgid "destination" msgstr "destination" -#: apps/note/apps.py:14 apps/note/models/notes.py:58 -msgid "note" -msgstr "note" - -#: apps/note/forms.py:20 -msgid "New Alias" -msgstr "Nouvel alias" - -#: apps/note/forms.py:25 +#: apps/note/forms.py:14 msgid "select an image" msgstr "Choisissez une image" -#: apps/note/forms.py:26 +#: apps/note/forms.py:15 msgid "Maximal size: 2MB" msgstr "Taille maximale : 2 Mo" @@ -310,7 +540,7 @@ msgstr "" msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:118 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:122 msgid "created at" msgstr "créée le" @@ -318,10 +548,6 @@ msgstr "créée le" msgid "notes" msgstr "notes" -#: apps/note/models/notes.py:67 -msgid "Note" -msgstr "Note" - #: apps/note/models/notes.py:77 apps/note/models/notes.py:101 msgid "This alias is already taken." msgstr "Cet alias est déjà pris." @@ -368,7 +594,8 @@ msgstr "Alias invalide" msgid "alias" msgstr "alias" -#: apps/note/models/notes.py:211 templates/member/profile_detail.html:37 +#: apps/note/models/notes.py:211 templates/member/club_info.html:43 +#: templates/member/profile_info.html:36 msgid "aliases" msgstr "alias" @@ -380,159 +607,241 @@ msgstr "L'alias est trop long." msgid "An alias with a similar name already exists: {} " msgstr "Un alias avec un nom similaire existe déjà : {}" -#: apps/note/models/notes.py:247 +#: apps/note/models/notes.py:251 msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:30 msgid "transaction category" msgstr "catégorie de transaction" -#: apps/note/models/transactions.py:32 +#: apps/note/models/transactions.py:31 msgid "transaction categories" msgstr "catégories de transaction" -#: apps/note/models/transactions.py:48 +#: apps/note/models/transactions.py:47 msgid "A template with this name already exist" msgstr "Un modèle de transaction avec un nom similaire existe déjà." -#: apps/note/models/transactions.py:57 apps/note/models/transactions.py:126 +#: apps/note/models/transactions.py:58 apps/note/models/transactions.py:130 msgid "amount" msgstr "montant" -#: apps/note/models/transactions.py:58 +#: apps/note/models/transactions.py:59 msgid "in centimes" msgstr "en centimes" -#: apps/note/models/transactions.py:76 +#: apps/note/models/transactions.py:70 +msgid "display" +msgstr "afficher" + +#: apps/note/models/transactions.py:80 msgid "transaction template" msgstr "modèle de transaction" -#: apps/note/models/transactions.py:77 +#: apps/note/models/transactions.py:81 msgid "transaction templates" msgstr "modèles de transaction" -#: apps/note/models/transactions.py:101 apps/note/models/transactions.py:114 +#: apps/note/models/transactions.py:105 apps/note/models/transactions.py:118 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "alias utilisé" -#: apps/note/models/transactions.py:122 +#: apps/note/models/transactions.py:126 msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:115 +#: apps/note/models/transactions.py:134 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:119 -msgid "valid" -msgstr "valide" +#: apps/note/models/transactions.py:144 apps/note/tables.py:95 +msgid "invalidity reason" +msgstr "Motif d'invalidité" -#: apps/note/models/transactions.py:124 +#: apps/note/models/transactions.py:152 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:125 +#: apps/note/models/transactions.py:153 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:168 templates/base.html:98 +#: apps/note/models/transactions.py:207 +#: templates/activity/activity_entry.html:13 templates/base.html:84 #: templates/note/transaction_form.html:19 #: templates/note/transaction_form.html:145 msgid "Transfer" msgstr "Virement" -#: apps/note/models/transactions.py:188 +#: apps/note/models/transactions.py:227 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:203 +#: apps/note/models/transactions.py:242 msgid "first_name" msgstr "prénom" -#: apps/note/models/transactions.py:208 +#: apps/note/models/transactions.py:247 msgid "bank" msgstr "banque" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:24 +#: apps/note/models/transactions.py:253 +#: templates/activity/activity_entry.html:17 +#: templates/note/transaction_form.html:24 msgid "Credit" msgstr "Crédit" -#: apps/note/models/transactions.py:214 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:253 templates/note/transaction_form.html:28 msgid "Debit" msgstr "Débit" -#: apps/note/models/transactions.py:230 apps/note/models/transactions.py:235 +#: apps/note/models/transactions.py:269 apps/note/models/transactions.py:274 msgid "membership transaction" -msgstr "transaction d'adhésion" +msgstr "Transaction d'adhésion" -#: apps/note/models/transactions.py:231 +#: apps/note/models/transactions.py:270 msgid "membership transactions" -msgstr "transactions d'adhésion" +msgstr "Transactions d'adhésion" -#: apps/note/views.py:39 +#: apps/note/tables.py:57 +msgid "Click to invalidate" +msgstr "Cliquez pour dévalider" + +#: apps/note/tables.py:57 +msgid "Click to validate" +msgstr "Cliquez pour valider" + +#: apps/note/tables.py:93 +msgid "No reason specified" +msgstr "Pas de motif spécifié" + +#: apps/note/tables.py:122 apps/note/tables.py:151 +msgid "Delete" +msgstr "Supprimer" + +#: apps/note/tables.py:146 templates/member/club_info.html:55 +#: templates/note/conso_form.html:121 +msgid "Edit" +msgstr "Éditer" + +#: apps/note/views.py:40 msgid "Transfer money" msgstr "Transférer de l'argent" -#: apps/note/views.py:145 templates/base.html:79 +#: apps/note/views.py:109 templates/base.html:79 msgid "Consumptions" msgstr "Consommations" -#: apps/permission/models.py:69 apps/permission/models.py:262 +#: apps/permission/models.py:82 apps/permission/models.py:281 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "" -#: apps/permission/models.py:71 apps/permission/models.py:264 +#: apps/permission/models.py:84 apps/permission/models.py:283 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" -#: apps/permission/models.py:84 +#: apps/permission/models.py:97 msgid "rank" msgstr "Rang" -#: apps/permission/models.py:147 +#: apps/permission/models.py:110 +msgid "permission mask" +msgstr "masque de permissions" + +#: apps/permission/models.py:111 +msgid "permission masks" +msgstr "masques de permissions" + +#: apps/permission/models.py:160 +msgid "permission" +msgstr "permission" + +#: apps/permission/models.py:161 +msgid "permissions" +msgstr "permissions" + +#: apps/permission/models.py:166 msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/treasury/apps.py:11 templates/base.html:102 +#: apps/permission/models.py:304 apps/permission/models.py:305 +msgid "role permissions" +msgstr "Permissions par rôles" + +#: apps/registration/apps.py:10 +msgid "registration" +msgstr "inscription" + +#: apps/registration/forms.py:70 +msgid "Join BDE Club" +msgstr "Adhérer au club BDE" + +#: apps/registration/forms.py:77 +msgid "Join Kfet Club" +msgstr "Adhérer au club Kfet" + +#: apps/registration/views.py:75 +msgid "Email validation" +msgstr "Validation de l'adresse mail" + +#: apps/registration/views.py:121 +msgid "Email validation unsuccessful" +msgstr " La validation de l'adresse mail a échoué" + +#: apps/registration/views.py:132 +msgid "Email validation email sent" +msgstr "L'email de vérification de l'adresse email a bien été envoyé." + +#: apps/registration/views.py:185 +msgid "Unregistered users" +msgstr "Utilisateurs en attente d'inscription" + +#: apps/registration/views.py:252 +msgid "You must join the BDE." +msgstr "Vous devez adhérer au BDE." + +#: apps/registration/views.py:274 +msgid "You must join BDE club before joining Kfet club." +msgstr "Vous devez adhérer au club BDE avant d'adhérer au club Kfet." + +#: apps/registration/views.py:279 +msgid "" +"The entered amount is not enough for the memberships, should be at least {}" +msgstr "" +"Le montant crédité est trop faible pour adhérer, il doit être au minimum de " +"{}" + +#: apps/treasury/apps.py:12 templates/base.html:111 msgid "Treasury" msgstr "Trésorerie" -#: apps/treasury/forms.py:56 apps/treasury/forms.py:95 +#: apps/treasury/forms.py:85 apps/treasury/forms.py:133 +#: templates/activity/activity_form.html:9 +#: templates/activity/activity_invite.html:8 #: templates/django_filters/rest_framework/form.html:5 -#: templates/member/club_form.html:10 templates/treasury/invoice_form.html:47 +#: templates/member/add_members.html:14 templates/member/club_form.html:9 +#: templates/treasury/invoice_form.html:46 msgid "Submit" msgstr "Envoyer" -#: apps/treasury/forms.py:58 +#: apps/treasury/forms.py:87 msgid "Close" msgstr "Fermer" -#: apps/treasury/forms.py:65 +#: apps/treasury/forms.py:96 msgid "Remittance is already closed." msgstr "La remise est déjà fermée." -#: apps/treasury/forms.py:70 +#: apps/treasury/forms.py:101 msgid "You can't change the type of the remittance." msgstr "Vous ne pouvez pas changer le type de la remise." -#: apps/treasury/forms.py:84 -msgid "Last name" -msgstr "Nom de famille" - -#: apps/treasury/forms.py:86 templates/note/transaction_form.html:92 -msgid "First name" -msgstr "Prénom" - -#: apps/treasury/forms.py:88 templates/note/transaction_form.html:98 -msgid "Bank" -msgstr "Banque" - -#: apps/treasury/forms.py:90 apps/treasury/tables.py:40 -#: templates/note/transaction_form.html:128 +#: apps/treasury/forms.py:127 apps/treasury/tables.py:47 +#: templates/note/transaction_form.html:133 #: templates/treasury/remittance_form.html:18 msgid "Amount" msgstr "Montant" @@ -553,7 +862,7 @@ msgstr "Objet" msgid "Description" msgstr "Description" -#: apps/treasury/models.py:46 templates/note/transaction_form.html:86 +#: apps/treasury/models.py:46 templates/note/transaction_form.html:91 msgid "Name" msgstr "Nom" @@ -569,66 +878,102 @@ msgstr "Lieu" msgid "Acquitted" msgstr "Acquittée" -#: apps/treasury/models.py:75 +#: apps/treasury/models.py:63 +msgid "invoice" +msgstr "facture" + +#: apps/treasury/models.py:64 +msgid "invoices" +msgstr "factures" + +#: apps/treasury/models.py:79 msgid "Designation" msgstr "Désignation" -#: apps/treasury/models.py:79 +#: apps/treasury/models.py:83 msgid "Quantity" msgstr "Quantité" -#: apps/treasury/models.py:83 +#: apps/treasury/models.py:87 msgid "Unit price" msgstr "Prix unitaire" -#: apps/treasury/models.py:120 +#: apps/treasury/models.py:103 +msgid "product" +msgstr "produit" + +#: apps/treasury/models.py:104 +msgid "products" +msgstr "produits" + +#: apps/treasury/models.py:121 +msgid "remittance type" +msgstr "type de remise" + +#: apps/treasury/models.py:122 +msgid "remittance types" +msgstr "types de remises" + +#: apps/treasury/models.py:132 msgid "Date" msgstr "Date" -#: apps/treasury/models.py:126 -msgid "Type" -msgstr "Type" - -#: apps/treasury/models.py:131 +#: apps/treasury/models.py:143 msgid "Comment" msgstr "Commentaire" -#: apps/treasury/models.py:136 +#: apps/treasury/models.py:148 msgid "Closed" msgstr "Fermée" -#: apps/treasury/models.py:159 +#: apps/treasury/models.py:152 +msgid "remittance" +msgstr "remise" + +#: apps/treasury/models.py:153 +msgid "remittances" +msgstr "remises" + +#: apps/treasury/models.py:185 msgid "Remittance #{:d}: {}" msgstr "Remise n°{:d} : {}" -#: apps/treasury/models.py:178 apps/treasury/tables.py:64 -#: apps/treasury/tables.py:72 templates/treasury/invoice_list.html:13 +#: apps/treasury/models.py:204 apps/treasury/tables.py:76 +#: apps/treasury/tables.py:84 templates/treasury/invoice_list.html:13 #: templates/treasury/remittance_list.html:13 msgid "Remittance" msgstr "Remise" -#: apps/treasury/tables.py:16 +#: apps/treasury/models.py:208 +msgid "special transaction proxy" +msgstr "Proxy de transaction spéciale" + +#: apps/treasury/models.py:209 +msgid "special transaction proxies" +msgstr "Proxys de transactions spéciales" + +#: apps/treasury/tables.py:19 msgid "Invoice #{:d}" msgstr "Facture n°{:d}" -#: apps/treasury/tables.py:19 templates/treasury/invoice_list.html:10 +#: apps/treasury/tables.py:22 templates/treasury/invoice_list.html:10 #: templates/treasury/remittance_list.html:10 msgid "Invoice" msgstr "Facture" -#: apps/treasury/tables.py:38 +#: apps/treasury/tables.py:45 msgid "Transaction count" msgstr "Nombre de transactions" -#: apps/treasury/tables.py:43 apps/treasury/tables.py:45 +#: apps/treasury/tables.py:50 apps/treasury/tables.py:52 msgid "View" msgstr "Voir" -#: apps/treasury/tables.py:66 +#: apps/treasury/tables.py:78 msgid "Add" msgstr "Ajouter" -#: apps/treasury/tables.py:74 +#: apps/treasury/tables.py:86 msgid "Remove" msgstr "supprimer" @@ -639,29 +984,96 @@ msgid "" "again unless your session expires or you logout." msgstr "" -#: note_kfet/settings/base.py:151 +#: note_kfet/settings/base.py:153 msgid "German" msgstr "" -#: note_kfet/settings/base.py:152 +#: note_kfet/settings/base.py:154 msgid "English" msgstr "" -#: note_kfet/settings/base.py:153 +#: note_kfet/settings/base.py:155 msgid "French" msgstr "" +#: templates/activity/activity_detail.html:29 +msgid "creater" +msgstr "Créateur" + +#: templates/activity/activity_detail.html:50 +msgid "opened" +msgstr "ouvert" + +#: templates/activity/activity_detail.html:57 +msgid "Entry page" +msgstr "Page des entrées" + +#: templates/activity/activity_detail.html:61 +msgid "close" +msgstr "fermer" + +#: templates/activity/activity_detail.html:64 +msgid "invalidate" +msgstr "dévalider" + +#: templates/activity/activity_detail.html:64 +msgid "validate" +msgstr "valider" + +#: templates/activity/activity_detail.html:70 +msgid "Invite" +msgstr "Inviter" + +#: templates/activity/activity_detail.html:77 +msgid "Guests list" +msgstr "Liste des invités" + +#: templates/activity/activity_entry.html:22 +#: templates/note/transaction_form.html:33 +msgid "Entries" +msgstr "Entrées" + +#: templates/activity/activity_entry.html:30 +msgid "Return to activity page" +msgstr "Retour à la page de l'activité" + +#: templates/activity/activity_list.html:5 +msgid "Upcoming activities" +msgstr "Activités à venir" + +#: templates/activity/activity_list.html:10 +msgid "There is no planned activity." +msgstr "Il n'y a pas d'activité prévue." + +#: templates/activity/activity_list.html:14 +msgid "New activity" +msgstr "Nouvelle activité" + +#: templates/activity/activity_list.html:18 +msgid "All activities" +msgstr "Toutes les activités" + #: templates/base.html:13 msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: templates/base.html:87 +#: templates/base.html:89 +msgid "Users" +msgstr "Utilisateurs" + +#: templates/base.html:94 msgid "Clubs" msgstr "Clubs" -#: templates/base.html:92 -msgid "Activities" -msgstr "Activités" +#: templates/base.html:100 +msgid "Registrations" +msgstr "Inscriptions" + +#: templates/base.html:150 +msgid "" +"Your e-mail address is not validated. Please check your mail inbox and click " +"on the validation link." +msgstr "" #: templates/cas_server/base.html:7 msgid "Central Authentication Service" @@ -720,33 +1132,49 @@ msgstr "" msgid "Field filters" msgstr "" -#: templates/member/club_detail.html:10 -msgid "Membership starts on" -msgstr "L'adhésion commence le" +#: templates/member/alias_update.html:5 +msgid "Add alias" +msgstr "Ajouter un alias" -#: templates/member/club_detail.html:12 -msgid "Membership ends on" -msgstr "L'adhésion finie le" +#: templates/member/club_info.html:17 +msgid "Club Parent" +msgstr "Club parent" -#: templates/member/club_detail.html:14 -msgid "Membership duration" -msgstr "Durée de l'adhésion" +#: templates/member/club_info.html:29 +msgid "days" +msgstr "jours" -#: templates/member/club_detail.html:18 templates/member/profile_detail.html:34 -msgid "balance" -msgstr "solde du compte" +#: templates/member/club_info.html:32 +msgid "membership fee" +msgstr "cotisation pour adhérer" -#: templates/member/club_detail.html:51 templates/member/profile_detail.html:75 -msgid "Transaction history" -msgstr "Historique des transactions" +#: templates/member/club_info.html:52 +msgid "Add member" +msgstr "Ajouter un membre" -#: templates/member/club_form.html:6 -msgid "Clubs list" -msgstr "Liste des clubs" +#: templates/member/club_info.html:59 templates/member/profile_info.html:48 +msgid "View Profile" +msgstr "Voir le profil" #: templates/member/club_list.html:8 -msgid "New club" -msgstr "Nouveau club" +msgid "search clubs" +msgstr "Chercher un club" + +#: templates/member/club_list.html:12 +msgid "Create club" +msgstr "Créer un club" + +#: templates/member/club_list.html:19 +msgid "club listing " +msgstr "Liste des clubs" + +#: templates/member/club_tables.html:6 +msgid "Member of the Club" +msgstr "Membre du club" + +#: templates/member/club_tables.html:17 templates/member/profile_tables.html:28 +msgid "Transaction history" +msgstr "Historique des transactions" #: templates/member/manage_auth_tokens.html:16 msgid "Token" @@ -760,35 +1188,45 @@ msgstr "Créé le" msgid "Regenerate token" msgstr "Regénérer le jeton" -#: templates/member/profile_alias.html:10 -msgid "Add alias" -msgstr "Ajouter un alias" +#: templates/member/profile_info.html:5 +#: templates/registration/future_profile_detail.html:12 +msgid "Account #" +msgstr "Compte n°" -#: templates/member/profile_detail.html:15 -msgid "first name" -msgstr "prénom" - -#: templates/member/profile_detail.html:18 +#: templates/member/profile_info.html:17 +#: templates/registration/future_profile_detail.html:19 msgid "username" msgstr "pseudo" -#: templates/member/profile_detail.html:21 +#: templates/member/profile_info.html:20 +#: templates/registration/future_profile_detail.html:34 msgid "password" msgstr "mot de passe" -#: templates/member/profile_detail.html:24 +#: templates/member/profile_info.html:23 +#: templates/registration/future_profile_detail.html:37 msgid "Change password" msgstr "Changer le mot de passe" -#: templates/member/profile_detail.html:42 +#: templates/member/profile_info.html:33 +msgid "balance" +msgstr "solde du compte" + +#: templates/member/profile_info.html:41 msgid "Manage auth token" msgstr "Gérer les jetons d'authentification" -#: templates/member/profile_detail.html:49 -msgid "View Profile" -msgstr "Voir le profil" +#: templates/member/profile_tables.html:7 +#: templates/registration/future_profile_detail.html:28 +msgid "This user doesn't have confirmed his/her e-mail address." +msgstr "Cet utilisateur n'a pas encore confirmé son adresse e-mail." -#: templates/member/profile_detail.html:62 +#: templates/member/profile_tables.html:8 +#: templates/registration/future_profile_detail.html:29 +msgid "Click here to resend a validation link." +msgstr "Cliquez ici pour renvoyer un lien de validation." + +#: templates/member/profile_tables.html:15 msgid "View my memberships" msgstr "Voir mes adhésions" @@ -796,12 +1234,12 @@ msgstr "Voir mes adhésions" msgid "Save Changes" msgstr "Sauvegarder les changements" -#: templates/member/signup.html:5 templates/member/signup.html:8 -#: templates/member/signup.html:14 -msgid "Sign up" -msgstr "Inscription" +#: templates/member/user_list.html:14 +#: templates/registration/future_user_list.html:17 +msgid "There is no pending user with this pattern." +msgstr "Il n'y a pas d'inscription en attente avec cette entrée." -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:50 +#: templates/note/conso_form.html:28 templates/note/transaction_form.html:55 msgid "Select emitters" msgstr "Sélection des émetteurs" @@ -817,10 +1255,6 @@ msgstr "Consommer !" msgid "Most used buttons" msgstr "Boutons les plus utilisés" -#: templates/note/conso_form.html:121 -msgid "Edit" -msgstr "Éditer" - #: templates/note/conso_form.html:126 msgid "Single consumptions" msgstr "Consommations simples" @@ -837,36 +1271,20 @@ msgstr "Historique des transactions récentes" msgid "Gift" msgstr "Don" -#: templates/note/transaction_form.html:68 +#: templates/note/transaction_form.html:73 msgid "External payment" msgstr "Paiement externe" -#: templates/note/transaction_form.html:76 +#: templates/note/transaction_form.html:81 msgid "Transfer type" msgstr "Type de transfert" -#: templates/note/transaction_form.html:86 -msgid "Name" -msgstr "Nom" - -#: templates/note/transaction_form.html:92 -msgid "First name" -msgstr "Prénom" - -#: templates/note/transaction_form.html:98 -msgid "Bank" -msgstr "Banque" - -#: templates/note/transaction_form.html:111 +#: templates/note/transaction_form.html:116 #: templates/note/transaction_form.html:169 #: templates/note/transaction_form.html:176 msgid "Select receivers" msgstr "Sélection des destinataires" -#: templates/note/transaction_form.html:128 -msgid "Amount" -msgstr "Montant" - #: templates/note/transaction_form.html:138 msgid "Reason" msgstr "Raison" @@ -887,18 +1305,61 @@ msgstr "Liste des boutons" msgid "search button" msgstr "Chercher un bouton" +#: templates/note/transactiontemplate_list.html:13 +msgid "New button" +msgstr "Nouveau bouton" + #: templates/note/transactiontemplate_list.html:20 msgid "buttons listing " msgstr "Liste des boutons" -#: templates/note/transactiontemplate_list.html:71 +#: templates/note/transactiontemplate_list.html:70 msgid "button successfully deleted " msgstr "Le bouton a bien été supprimé" -#: templates/note/transactiontemplate_list.html:75 +#: templates/note/transactiontemplate_list.html:74 msgid "Unable to delete button " msgstr "Impossible de supprimer le bouton " +#: templates/registration/email_validation_complete.html:6 +msgid "Your email have successfully been validated." +msgstr "Votre adresse e-mail a bien été validée." + +#: templates/registration/email_validation_complete.html:8 +#, python-format +msgid "You can now log in." +msgstr "Vous pouvez désormais vous connecter." + +#: templates/registration/email_validation_complete.html:10 +msgid "" +"You must pay now your membership in the Kfet to complete your registration." +msgstr "" +"Vous devez désormais payer votre adhésion à la Kfet pour compléter votre inscription." + +#: templates/registration/email_validation_complete.html:13 +msgid "" +"The link was invalid. The token may have expired. Please send us an email to " +"activate your account." +msgstr "" +"Le lien est invalide. Le jeton a sans doute expiré. Merci de nous contacter pour " +"activer votre compte." + +#: templates/registration/future_profile_detail.html:56 +msgid "Delete registration" +msgstr "Supprimer l'inscription" + +#: templates/registration/future_profile_detail.html:64 +msgid "Validate account" +msgstr "Valider le compte" + +#: templates/registration/future_profile_detail.html:71 +msgid "Validate registration" +msgstr "Valider l'inscription" + +#: templates/registration/future_user_list.html:7 +msgid "New user" +msgstr "Nouvel utilisateur" + #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" @@ -932,9 +1393,41 @@ msgstr "" msgid "Forgotten your password or username?" msgstr "" +#: templates/registration/mails/email_validation_email.html:3 +msgid "Hi" +msgstr "" + +#: templates/registration/mails/email_validation_email.html:5 +msgid "" +"You recently registered on the Note Kfet. Please click on the link below to " +"confirm your registration." +msgstr "" + +#: templates/registration/mails/email_validation_email.html:9 +msgid "" +"This link is only valid for a couple of days, after that you will need to " +"contact us to validate your email." +msgstr "" + +#: templates/registration/mails/email_validation_email.html:11 +msgid "" +"After that, you'll have to wait that someone validates your account before " +"you can log in. You will need to pay your membership in the Kfet." +msgstr "" +"Après cela, vous devrez attendre que quelqu'un valide votre compte avant " +"de pouvoir vous connecter. Vous devrez payer votre adhésion à la Kfet." + +#: templates/registration/mails/email_validation_email.html:13 +msgid "Thanks" +msgstr "Merci" + +#: templates/registration/mails/email_validation_email.html:15 +msgid "The Note Kfet team." +msgstr "L'équipe de la Note Kfet." + #: templates/registration/password_change_done.html:8 msgid "Your password was changed." -msgstr "" +msgstr "Votre mot de passe a bien été changé." #: templates/registration/password_change_form.html:9 msgid "" @@ -985,15 +1478,20 @@ msgstr "" msgid "Reset my password" msgstr "" +#: templates/registration/signup.html:5 templates/registration/signup.html:8 +#: templates/registration/signup.html:14 +msgid "Sign up" +msgstr "Inscription" + #: templates/treasury/invoice_form.html:6 msgid "Invoices list" msgstr "Liste des factures" -#: templates/treasury/invoice_form.html:42 +#: templates/treasury/invoice_form.html:41 msgid "Add product" msgstr "Ajouter produit" -#: templates/treasury/invoice_form.html:43 +#: templates/treasury/invoice_form.html:42 msgid "Remove product" msgstr "Retirer produit" @@ -1053,3 +1551,6 @@ msgstr "Il n'y a pas de transaction associée à une remise ouverte." #: templates/treasury/remittance_list.html:54 msgid "Closed remittances" msgstr "Remises fermées" + +#~ msgid "This membership is already renewed" +#~ msgstr "Cette adhésion est déjà renouvelée" diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py new file mode 100644 index 00000000..b3cccbce --- /dev/null +++ b/note_kfet/inputs.py @@ -0,0 +1,302 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from json import dumps as json_dumps + +from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput + + +class AmountInput(NumberInput): + """ + This input type lets the user type amounts in euros, but forms receive data in cents + """ + template_name = "note/amount_input.html" + + def format_value(self, value): + return None if value is None or value == "" else "{:.02f}".format(int(value) / 100, ) + + def value_from_datadict(self, data, files, name): + val = super().value_from_datadict(data, files, name) + return str(int(100 * float(val))) if val else val + + +class Autocomplete(TextInput): + template_name = "member/autocomplete_model.html" + + def __init__(self, model, attrs=None): + super().__init__(attrs) + + self.model = model + self.model_pk = None + + class Media: + """JS/CSS resources needed to render the date-picker calendar.""" + + js = ('js/autocomplete_model.js', ) + + def format_value(self, value): + if value: + self.attrs["model_pk"] = int(value) + return str(self.model.objects.get(pk=int(value))) + return "" + + +""" +The remaining of this file comes from the project `django-bootstrap-datepicker-plus` available on Github: +https://github.com/monim67/django-bootstrap-datepicker-plus +This is distributed under Apache License 2.0. + +This adds datetime pickers with bootstrap. +""" + +"""Contains Base Date-Picker input class for widgets of this package.""" + + +class DatePickerDictionary: + """Keeps track of all date-picker input classes.""" + + _i = 0 + items = dict() + + @classmethod + def generate_id(cls): + """Return a unique ID for each date-picker input class.""" + cls._i += 1 + return 'dp_%s' % cls._i + + +class BasePickerInput(DateTimeBaseInput): + """Base Date-Picker input class for widgets of this package.""" + + template_name = 'bootstrap_datepicker_plus/date_picker.html' + picker_type = 'DATE' + format = '%Y-%m-%d' + config = {} + _default_config = { + 'id': None, + 'picker_type': None, + 'linked_to': None, + 'options': {} # final merged options + } + options = {} # options extended by user + options_param = {} # options passed as parameter + _default_options = { + 'showClose': True, + 'showClear': True, + 'showTodayButton': True, + "locale": "fr", + } + + # source: https://github.com/tutorcruncher/django-bootstrap3-datetimepicker + # file: /blob/31fbb09/bootstrap3_datetime/widgets.py#L33 + format_map = ( + ('DDD', r'%j'), + ('DD', r'%d'), + ('MMMM', r'%B'), + ('MMM', r'%b'), + ('MM', r'%m'), + ('YYYY', r'%Y'), + ('YY', r'%y'), + ('HH', r'%H'), + ('hh', r'%I'), + ('mm', r'%M'), + ('ss', r'%S'), + ('a', r'%p'), + ('ZZ', r'%z'), + ) + + class Media: + """JS/CSS resources needed to render the date-picker calendar.""" + + js = ( + 'https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/' + 'moment-with-locales.min.js', + 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/' + '4.17.47/js/bootstrap-datetimepicker.min.js', + 'bootstrap_datepicker_plus/js/datepicker-widget.js' + ) + css = {'all': ( + 'https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/' + '4.17.47/css/bootstrap-datetimepicker.css', + 'bootstrap_datepicker_plus/css/datepicker-widget.css' + ), } + + @classmethod + def format_py2js(cls, datetime_format): + """Convert python datetime format to moment datetime format.""" + for js_format, py_format in cls.format_map: + datetime_format = datetime_format.replace(py_format, js_format) + return datetime_format + + @classmethod + def format_js2py(cls, datetime_format): + """Convert moment datetime format to python datetime format.""" + for js_format, py_format in cls.format_map: + datetime_format = datetime_format.replace(js_format, py_format) + return datetime_format + + def __init__(self, attrs=None, format=None, options=None): + """Initialize the Date-picker widget.""" + self.format_param = format + self.options_param = options if options else {} + self.config = self._default_config.copy() + self.config['id'] = DatePickerDictionary.generate_id() + self.config['picker_type'] = self.picker_type + self.config['options'] = self._calculate_options() + attrs = attrs if attrs else {} + if 'class' not in attrs: + attrs['class'] = 'form-control' + super().__init__(attrs, self._calculate_format()) + + def _calculate_options(self): + """Calculate and Return the options.""" + _options = self._default_options.copy() + _options.update(self.options) + if self.options_param: + _options.update(self.options_param) + return _options + + def _calculate_format(self): + """Calculate and Return the datetime format.""" + _format = self.format_param if self.format_param else self.format + if self.config['options'].get('format'): + _format = self.format_js2py(self.config['options'].get('format')) + else: + self.config['options']['format'] = self.format_py2js(_format) + return _format + + def get_context(self, name, value, attrs): + """Return widget context dictionary.""" + context = super().get_context( + name, value, attrs) + context['widget']['attrs']['dp_config'] = json_dumps(self.config) + return context + + def start_of(self, event_id): + """ + Set Date-Picker as the start-date of a date-range. + + Args: + - event_id (string): User-defined unique id for linking two fields + """ + DatePickerDictionary.items[str(event_id)] = self + return self + + def end_of(self, event_id, import_options=True): + """ + Set Date-Picker as the end-date of a date-range. + + Args: + - event_id (string): User-defined unique id for linking two fields + - import_options (bool): inherit options from start-date input, + default: TRUE + """ + event_id = str(event_id) + if event_id in DatePickerDictionary.items: + linked_picker = DatePickerDictionary.items[event_id] + self.config['linked_to'] = linked_picker.config['id'] + if import_options: + backup_moment_format = self.config['options']['format'] + self.config['options'].update(linked_picker.config['options']) + self.config['options'].update(self.options_param) + if self.format_param or 'format' in self.options_param: + self.config['options']['format'] = backup_moment_format + else: + self.format = linked_picker.format + # Setting useCurrent is necessary, see following issue + # https://github.com/Eonasdan/bootstrap-datetimepicker/issues/1075 + self.config['options']['useCurrent'] = False + self._link_to(linked_picker) + else: + raise KeyError( + 'start-date not specified for event_id "%s"' % event_id) + return self + + def _link_to(self, linked_picker): + """ + Executed when two date-inputs are linked together. + + This method for sub-classes to override to customize the linking. + """ + pass + + +class DatePickerInput(BasePickerInput): + """ + Widget to display a Date-Picker Calendar on a DateField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'DATE' + format = '%Y-%m-%d' + format_key = 'DATE_INPUT_FORMATS' + + +class TimePickerInput(BasePickerInput): + """ + Widget to display a Time-Picker Calendar on a TimeField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'TIME' + format = '%H:%M' + format_key = 'TIME_INPUT_FORMATS' + template_name = 'bootstrap_datepicker_plus/time_picker.html' + + +class DateTimePickerInput(BasePickerInput): + """ + Widget to display a DateTime-Picker Calendar on a DateTimeField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'DATETIME' + format = '%Y-%m-%d %H:%M' + format_key = 'DATETIME_INPUT_FORMATS' + + +class MonthPickerInput(BasePickerInput): + """ + Widget to display a Month-Picker Calendar on a DateField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'MONTH' + format = '01/%m/%Y' + format_key = 'DATE_INPUT_FORMATS' + + +class YearPickerInput(BasePickerInput): + """ + Widget to display a Year-Picker Calendar on a DateField property. + + Args: + - attrs (dict): HTML attributes of rendered HTML input + - format (string): Python DateTime format eg. "%Y-%m-%d" + - options (dict): Options to customize the widget, see README + """ + + picker_type = 'YEAR' + format = '01/01/%Y' + format_key = 'DATE_INPUT_FORMATS' + + def _link_to(self, linked_picker): + """Customize the options when linked with other date-time input""" + yformat = self.config['options']['format'].replace('-01-01', '-12-31') + self.config['options']['format'] = yformat diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index d49b2542..283f8e56 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -48,21 +48,20 @@ INSTALLED_APPS = [ 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django.forms', # API 'rest_framework', 'rest_framework.authtoken', - # Autocomplete - 'dal', - 'dal_select2', # Note apps + 'api', 'activity', + 'logs', 'member', 'note', - 'treasury', 'permission', - 'api', - 'logs', + 'registration', + 'treasury', ] LOGIN_REDIRECT_URL = '/note/transfer/' @@ -100,6 +99,8 @@ TEMPLATES = [ }, ] +FORM_RENDERER = 'django.forms.renderers.TemplatesSetting' + WSGI_APPLICATION = 'note_kfet.wsgi.application' # Password validation diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 40a9a614..90d44a07 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -15,13 +15,15 @@ urlpatterns = [ # Include project routers path('note/', include('note.urls')), + path('accounts/', include('member.urls')), + path('registration/', include('registration.urls')), + path('activity/', include('activity.urls')), path('treasury/', include('treasury.urls')), # Include Django Contrib and Core routers path('i18n/', include('django.conf.urls.i18n')), path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/', admin.site.urls), - path('accounts/', include('member.urls')), path('accounts/login/', CustomLoginView.as_view()), path('accounts/', include('django.contrib.auth.urls')), path('api/', include('api.urls')), @@ -36,14 +38,7 @@ if "cas_server" in settings.INSTALLED_APPS: # Include CAS Server routers path('cas/', include('cas_server.urls', namespace="cas_server")), ] -if "cas" in settings.INSTALLED_APPS: - from cas import views as cas_views - urlpatterns += [ - # Include CAS Client routers - path('accounts/login/cas/', cas_views.login, name='cas_login'), - path('accounts/logout/cas/', cas_views.logout, name='cas_logout'), - - ] + if "debug_toolbar" in settings.INSTALLED_APPS: import debug_toolbar urlpatterns = [ diff --git a/requirements/base.txt b/requirements/base.txt index 6c5fbc4c..9c978ed0 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -3,7 +3,6 @@ chardet==3.0.4 defusedxml==0.6.0 Django~=2.2 django-allauth==0.39.1 -django-autocomplete-light==3.5.1 django-crispy-forms==1.7.2 django-extensions==2.1.9 django-filter==2.2.0 diff --git a/static/bootstrap_datepicker_plus/css/datepicker-widget.css b/static/bootstrap_datepicker_plus/css/datepicker-widget.css new file mode 100644 index 00000000..baeec507 --- /dev/null +++ b/static/bootstrap_datepicker_plus/css/datepicker-widget.css @@ -0,0 +1,121 @@ +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot'); + src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'), + url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} + +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.glyphicon-time:before { + content: "\e023"; +} + +.glyphicon-chevron-left:before { + content: "\e079"; +} + +.glyphicon-chevron-right:before { + content: "\e080"; +} + +.glyphicon-chevron-up:before { + content: "\e113"; +} + +.glyphicon-chevron-down:before { + content: "\e114"; +} + +.glyphicon-calendar:before { + content: "\e109"; +} + +.glyphicon-screenshot:before { + content: "\e087"; +} + +.glyphicon-trash:before { + content: "\e020"; +} + +.glyphicon-remove:before { + content: "\e014"; +} + +.bootstrap-datetimepicker-widget .btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} + +.bootstrap-datetimepicker-widget.dropdown-menu { + position: absolute; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} + +.bootstrap-datetimepicker-widget .list-unstyled { + padding-left: 0; + list-style: none; +} + +.bootstrap-datetimepicker-widget .collapse { + display: none; +} + +.bootstrap-datetimepicker-widget .collapse.in { + display: block; +} + +/* fix for bootstrap4 */ +.bootstrap-datetimepicker-widget .table-condensed > thead > tr > th, +.bootstrap-datetimepicker-widget .table-condensed > tbody > tr > td, +.bootstrap-datetimepicker-widget .table-condensed > tfoot > tr > td { + padding: 5px; +} diff --git a/static/bootstrap_datepicker_plus/js/datepicker-widget.js b/static/bootstrap_datepicker_plus/js/datepicker-widget.js new file mode 100644 index 00000000..2288b46b --- /dev/null +++ b/static/bootstrap_datepicker_plus/js/datepicker-widget.js @@ -0,0 +1,55 @@ +jQuery(function ($) { + var datepickerDict = {}; + var isBootstrap4 = $.fn.collapse.Constructor.VERSION.split('.').shift() == "4"; + function fixMonthEndDate(e, picker) { + e.date && picker.val().length && picker.val(e.date.endOf('month').format('YYYY-MM-DD')); + } + $("[dp_config]:not([disabled])").each(function (i, element) { + var $element = $(element), data = {}; + try { + data = JSON.parse($element.attr('dp_config')); + } + catch (x) { } + if (data.id && data.options) { + data.$element = $element.datetimepicker(data.options); + data.datepickerdata = $element.data("DateTimePicker"); + datepickerDict[data.id] = data; + data.$element.next('.input-group-addon').on('click', function(){ + data.datepickerdata.show(); + }); + if(isBootstrap4){ + data.$element.on("dp.show", function (e) { + $('.collapse.in').addClass('show'); + }); + } + } + }); + $.each(datepickerDict, function (id, to_picker) { + if (to_picker.linked_to) { + var from_picker = datepickerDict[to_picker.linked_to]; + from_picker.datepickerdata.maxDate(to_picker.datepickerdata.date() || false); + to_picker.datepickerdata.minDate(from_picker.datepickerdata.date() || false); + from_picker.$element.on("dp.change", function (e) { + to_picker.datepickerdata.minDate(e.date || false); + }); + to_picker.$element.on("dp.change", function (e) { + if (to_picker.picker_type == 'MONTH') fixMonthEndDate(e, to_picker.$element); + from_picker.datepickerdata.maxDate(e.date || false); + }); + if (to_picker.picker_type == 'MONTH') { + to_picker.$element.on("dp.hide", function (e) { + fixMonthEndDate(e, to_picker.$element); + }); + fixMonthEndDate({ date: to_picker.datepickerdata.date() }, to_picker.$element); + } + } + }); + if(isBootstrap4) { + $('body').on('show.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){ + $(e.target).addClass('in'); + }); + $('body').on('hidden.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){ + $(e.target).removeClass('in'); + }); + } +}); diff --git a/static/js/autocomplete_model.js b/static/js/autocomplete_model.js new file mode 100644 index 00000000..8c3f6f09 --- /dev/null +++ b/static/js/autocomplete_model.js @@ -0,0 +1,37 @@ +$(document).ready(function () { + $(".autocomplete").keyup(function(e) { + let target = $("#" + e.target.id); + let prefix = target.attr("id"); + let api_url = target.attr("api_url"); + let api_url_suffix = target.attr("api_url_suffix"); + if (!api_url_suffix) + api_url_suffix = ""; + let name_field = target.attr("name_field"); + if (!name_field) + name_field = "name"; + let input = target.val(); + + $.getJSON(api_url + "?format=json&search=^" + input + api_url_suffix, function(objects) { + let html = ""; + + objects.results.forEach(function (obj) { + html += li(prefix + "_" + obj.id, obj[name_field]); + }); + + $("#" + prefix + "_list").html(html); + + objects.results.forEach(function (obj) { + $("#" + prefix + "_" + obj.id).click(function() { + target.val(obj[name_field]); + $("#" + prefix + "_pk").val(obj.id); + + if (typeof autocompleted != 'undefined') + autocompleted(obj, prefix) + }); + + if (input === obj[name_field]) + $("#" + prefix + "_pk").val(obj.id); + }); + }); + }); +}); \ No newline at end of file diff --git a/static/js/base.js b/static/js/base.js index 86e090f9..179bdd3b 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -19,16 +19,53 @@ function pretty_money(value) { * Add a message on the top of the page. * @param msg The message to display * @param alert_type The type of the alert. Choices: info, success, warning, danger + * @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored. */ -function addMsg(msg, alert_type) { +function addMsg(msg, alert_type, timeout=-1) { let msgDiv = $("#messages"); let html = msgDiv.html(); + let id = Math.floor(10000 * Math.random() + 1); html += "
" + - "" + "" + msg + "
\n"; msgDiv.html(html); + + if (timeout > 0) { + setTimeout(function () { + $("#close-message-" + id).click(); + }, timeout); + } } +/** + * add Muliple error message from err_obj + * @param errs_obj [{error_code:erro_message}] + * @param timeout The delay (in millis) after that the message is auto-closed. If negative, then it is ignored. + */ +function errMsg(errs_obj, timeout=-1) { + for (const err_msg of Object.values(errs_obj)) { + addMsg(err_msg,'danger', timeout); + } +} + +var reloadWithTurbolinks = (function () { + var scrollPosition; + + function reload () { + scrollPosition = [window.scrollX, window.scrollY]; + Turbolinks.visit(window.location.toString(), { action: 'replace' }) + } + + document.addEventListener('turbolinks:load', function () { + if (scrollPosition) { + window.scrollTo.apply(window, scrollPosition); + scrollPosition = null + } + }); + + return reload; +})(); + /** * Reload the balance of the user on the right top corner */ @@ -85,7 +122,7 @@ function displayNote(note, alias, user_note_field=null, profile_pic_field=null) if (alias !== note.name) alias += " (aka. " + note.name + ")"; if (user_note_field !== null) - + $("#" + user_note_field).addClass(displayStyle(note.balance)); $("#" + user_note_field).text(alias + (note.balance == null ? "" : (":\n" + pretty_money(note.balance)))); if (profile_pic_field != null){ @@ -202,7 +239,7 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes notes.length = 0; return; } - + $.getJSON("/api/note/consumer/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", @@ -277,9 +314,9 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes }); }) }); - + });// end getJSON alias - }); + }); }// end function autocomplete // When a validate button is clicked, we switch the validation status @@ -296,8 +333,9 @@ function de_validate(id, validated) { "X-CSRFTOKEN": CSRF_TOKEN }, data: { - "resourcetype": "RecurrentTransaction", - valid: !validated + resourcetype: "RecurrentTransaction", + valid: !validated, + invalidity_reason: invalidity_reason, }, success: function () { // Refresh jQuery objects diff --git a/static/js/transfer.js b/static/js/transfer.js index 5b673acb..293e5071 100644 --- a/static/js/transfer.js +++ b/static/js/transfer.js @@ -61,16 +61,24 @@ $(document).ready(function() { // Ensure we begin in gift mode. Removing these lines may cause problems when reloading. - $("#type_gift").prop('checked', 'true'); + let type_gift = $("#type_gift"); // Default mode + type_gift.removeAttr('checked'); $("#type_transfer").removeAttr('checked'); $("#type_credit").removeAttr('checked'); $("#type_debit").removeAttr('checked'); + $("label[for='type_gift']").attr('class', 'btn btn-sm btn-outline-primary'); $("label[for='type_transfer']").attr('class', 'btn btn-sm btn-outline-primary'); $("label[for='type_credit']").attr('class', 'btn btn-sm btn-outline-primary'); $("label[for='type_debit']").attr('class', 'btn btn-sm btn-outline-primary'); + + if (location.hash) + $("#type_" + location.hash.substr(1)).click(); + else + type_gift.click(); + location.hash = ""; }); -$("#transfer").click(function() { +$("#btn_transfer").click(function() { if ($("#type_gift").is(':checked')) { dests_notes_display.forEach(function (dest) { $.post("/api/note/transaction/transaction/", diff --git a/static/vendor/select2/Gruntfile.js b/static/vendor/select2/Gruntfile.js deleted file mode 100644 index b4c4508a..00000000 --- a/static/vendor/select2/Gruntfile.js +++ /dev/null @@ -1,262 +0,0 @@ -const sass = require('node-sass'); - -module.exports = function (grunt) { - // Full list of files that must be included by RequireJS - includes = [ - 'jquery.select2', - 'almond', - - 'jquery-mousewheel' // shimmed for non-full builds - ]; - - fullIncludes = [ - 'jquery', - - 'select2/compat/containerCss', - 'select2/compat/dropdownCss', - - 'select2/compat/initSelection', - 'select2/compat/inputData', - 'select2/compat/matcher', - 'select2/compat/query', - - 'select2/dropdown/attachContainer', - 'select2/dropdown/stopPropagation', - - 'select2/selection/stopPropagation' - ].concat(includes); - - var i18nModules = []; - var i18nPaths = {}; - - var i18nFiles = grunt.file.expand({ - cwd: 'src/js' - }, 'select2/i18n/*.js'); - - var testFiles = grunt.file.expand('tests/**/*.html'); - var testUrls = testFiles.map(function (filePath) { - return 'http://localhost:9999/' + filePath; - }); - - var testBuildNumber = "unknown"; - - if (process.env.TRAVIS_JOB_ID) { - testBuildNumber = "travis-" + process.env.TRAVIS_JOB_ID; - } else { - var currentTime = new Date(); - - testBuildNumber = "manual-" + currentTime.getTime(); - } - - for (var i = 0; i < i18nFiles.length; i++) { - var file = i18nFiles[i]; - var name = file.split('.')[0]; - - i18nModules.push({ - name: name - }); - - i18nPaths[name] = '../../' + name; - } - - var minifiedBanner = '/*! Select2 <%= package.version %> | https://github.com/select2/select2/blob/master/LICENSE.md */'; - - grunt.initConfig({ - package: grunt.file.readJSON('package.json'), - - concat: { - 'dist': { - options: { - banner: grunt.file.read('src/js/wrapper.start.js'), - }, - src: [ - 'dist/js/select2.js', - 'src/js/wrapper.end.js' - ], - dest: 'dist/js/select2.js' - }, - 'dist.full': { - options: { - banner: grunt.file.read('src/js/wrapper.start.js'), - }, - src: [ - 'dist/js/select2.full.js', - 'src/js/wrapper.end.js' - ], - dest: 'dist/js/select2.full.js' - } - }, - - connect: { - tests: { - options: { - base: '.', - hostname: '127.0.0.1', - port: 9999 - } - } - }, - - uglify: { - 'dist': { - src: 'dist/js/select2.js', - dest: 'dist/js/select2.min.js', - options: { - banner: minifiedBanner - } - }, - 'dist.full': { - src: 'dist/js/select2.full.js', - dest: 'dist/js/select2.full.min.js', - options: { - banner: minifiedBanner - } - } - }, - - qunit: { - all: { - options: { - urls: testUrls - } - } - }, - - jshint: { - options: { - jshintrc: true, - reporterOutput: '' - }, - code: { - src: ['src/js/**/*.js'] - }, - tests: { - src: ['tests/**/*.js'] - } - }, - - sass: { - dist: { - options: { - implementation: sass, - outputStyle: 'compressed' - }, - files: { - 'dist/css/select2.min.css': [ - 'src/scss/core.scss', - 'src/scss/theme/default/layout.css' - ] - } - }, - dev: { - options: { - implementation: sass, - outputStyle: 'nested' - }, - files: { - 'dist/css/select2.css': [ - 'src/scss/core.scss', - 'src/scss/theme/default/layout.css' - ] - } - } - }, - - requirejs: { - 'dist': { - options: { - baseUrl: 'src/js', - optimize: 'none', - name: 'select2/core', - out: 'dist/js/select2.js', - include: includes, - namespace: 'S2', - paths: { - 'almond': require.resolve('almond').slice(0, -3), - 'jquery': 'jquery.shim', - 'jquery-mousewheel': 'jquery.mousewheel.shim' - }, - wrap: { - startFile: 'src/js/banner.start.js', - endFile: 'src/js/banner.end.js' - } - } - }, - 'dist.full': { - options: { - baseUrl: 'src/js', - optimize: 'none', - name: 'select2/core', - out: 'dist/js/select2.full.js', - include: fullIncludes, - namespace: 'S2', - paths: { - 'almond': require.resolve('almond').slice(0, -3), - 'jquery': 'jquery.shim', - 'jquery-mousewheel': require.resolve('jquery-mousewheel').slice(0, -3) - }, - wrap: { - startFile: 'src/js/banner.start.js', - endFile: 'src/js/banner.end.js' - } - } - }, - 'i18n': { - options: { - baseUrl: 'src/js/select2/i18n', - dir: 'dist/js/i18n', - paths: i18nPaths, - modules: i18nModules, - namespace: 'S2', - wrap: { - start: minifiedBanner + grunt.file.read('src/js/banner.start.js'), - end: grunt.file.read('src/js/banner.end.js') - } - } - } - }, - - watch: { - js: { - files: [ - 'src/js/select2/**/*.js', - 'tests/**/*.js' - ], - tasks: [ - 'compile', - 'test', - 'minify' - ] - }, - css: { - files: [ - 'src/scss/**/*.scss' - ], - tasks: [ - 'compile', - 'minify' - ] - } - } - }); - - grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.loadNpmTasks('grunt-contrib-connect'); - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-contrib-qunit'); - grunt.loadNpmTasks('grunt-contrib-requirejs'); - grunt.loadNpmTasks('grunt-contrib-uglify'); - grunt.loadNpmTasks('grunt-contrib-watch'); - - grunt.loadNpmTasks('grunt-sass'); - - grunt.registerTask('default', ['compile', 'test', 'minify']); - - grunt.registerTask('compile', [ - 'requirejs:dist', 'requirejs:dist.full', 'requirejs:i18n', - 'concat:dist', 'concat:dist.full', - 'sass:dev' - ]); - grunt.registerTask('minify', ['uglify', 'sass:dist']); - grunt.registerTask('test', ['connect:tests', 'qunit', 'jshint']); -}; diff --git a/static/vendor/select2/bower.json b/static/vendor/select2/bower.json deleted file mode 100644 index 681600b5..00000000 --- a/static/vendor/select2/bower.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "select2", - "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.", - "main": [ - "dist/js/select2.js", - "src/scss/core.scss" - ], - "license": "MIT", - "repository": { - "type": "git", - "url": "git@github.com:select2/select2.git" - } -} diff --git a/static/vendor/select2/component.json b/static/vendor/select2/component.json deleted file mode 100644 index 75e19f1e..00000000 --- a/static/vendor/select2/component.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "select2", - "repo": "select/select2", - "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.", - "version": "4.0.7", - "demo": "https://select2.org/", - "keywords": [ - "jquery" - ], - "main": "dist/js/select2.js", - "styles": [ - "dist/css/select2.css" - ], - "scripts": [ - "dist/js/select2.js", - "dist/js/i18n/*.js" - ], - "license": "MIT" -} diff --git a/static/vendor/select2/composer.json b/static/vendor/select2/composer.json deleted file mode 100644 index 5ef2db2b..00000000 --- a/static/vendor/select2/composer.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "select2/select2", - "description": "Select2 is a jQuery based replacement for select boxes.", - "type": "component", - "homepage": "https://select2.org/", - "license": "MIT", - "extra": { - "component": { - "scripts": [ - "dist/js/select2.js" - ], - "styles": [ - "dist/css/select2.css" - ], - "files": [ - "dist/js/select2.js", - "dist/js/i18n/*.js", - "dist/css/select2.css" - ] - } - } -} diff --git a/static/vendor/select2/docs/announcements-4.0.html b/static/vendor/select2/docs/announcements-4.0.html deleted file mode 100644 index bc85b4c1..00000000 --- a/static/vendor/select2/docs/announcements-4.0.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - select2 - - - - - \ No newline at end of file diff --git a/static/vendor/select2/docs/community.html b/static/vendor/select2/docs/community.html deleted file mode 100644 index ffe8f83f..00000000 --- a/static/vendor/select2/docs/community.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - select2 - - - - - \ No newline at end of file diff --git a/static/vendor/select2/docs/examples.html b/static/vendor/select2/docs/examples.html deleted file mode 100644 index a463e84a..00000000 --- a/static/vendor/select2/docs/examples.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - select2 - - - - - \ No newline at end of file diff --git a/static/vendor/select2/docs/index.html b/static/vendor/select2/docs/index.html deleted file mode 100644 index ea8214d6..00000000 --- a/static/vendor/select2/docs/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - select2 - - - - - \ No newline at end of file diff --git a/static/vendor/select2/docs/options-old.html b/static/vendor/select2/docs/options-old.html deleted file mode 100644 index 4920b76b..00000000 --- a/static/vendor/select2/docs/options-old.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - select2 - - - - - \ No newline at end of file diff --git a/static/vendor/select2/docs/options.html b/static/vendor/select2/docs/options.html deleted file mode 100644 index 4920b76b..00000000 --- a/static/vendor/select2/docs/options.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - select2 - - - - - \ No newline at end of file diff --git a/static/vendor/select2/package.json b/static/vendor/select2/package.json deleted file mode 100644 index 231473b5..00000000 --- a/static/vendor/select2/package.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "select2", - "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.", - "homepage": "https://select2.org", - "author": { - "name": "Kevin Brown", - "url": "https://github.com/kevin-brown" - }, - "contributors": [ - { - "name": "Igor Vaynberg", - "url": "https://github.com/ivaynberg" - }, - { - "name": "Alex Weissman", - "url": "https://github.com/alexweissman" - } - ], - "repository": { - "type": "git", - "url": "git://github.com/select2/select2.git" - }, - "bugs": { - "url": "https://github.com/select2/select2/issues" - }, - "keywords": [ - "select", - "autocomplete", - "typeahead", - "dropdown", - "multiselect", - "tag", - "tagging" - ], - "license": "MIT", - "main": "dist/js/select2.js", - "style": "dist/css/select2.css", - "files": [ - "src", - "dist" - ], - "version": "4.0.7", - "jspm": { - "main": "js/select2", - "directories": { - "lib": "dist" - } - }, - "devDependencies": { - "almond": "~0.3.1", - "grunt": "^0.4.5", - "grunt-cli": "^1.3.2", - "grunt-contrib-concat": "^1.0.1", - "grunt-contrib-connect": "^2.0.0", - "grunt-contrib-jshint": "^1.1.0", - "grunt-contrib-qunit": "^1.3.0", - "grunt-contrib-requirejs": "^1.0.0", - "grunt-contrib-uglify": "~4.0.1", - "grunt-contrib-watch": "~1.1.0", - "grunt-sass": "^2.1.0", - "jquery-mousewheel": "~3.1.13", - "node-sass": "^4.12.0" - }, - "dependencies": {} -} diff --git a/static/vendor/select2/src/js/banner.end.js b/static/vendor/select2/src/js/banner.end.js deleted file mode 100644 index ea00a036..00000000 --- a/static/vendor/select2/src/js/banner.end.js +++ /dev/null @@ -1,6 +0,0 @@ - // Return the AMD loader configuration so it can be used outside of this file - return { - define: S2.define, - require: S2.require - }; -}()); diff --git a/static/vendor/select2/src/js/banner.start.js b/static/vendor/select2/src/js/banner.start.js deleted file mode 100644 index 78fe8b02..00000000 --- a/static/vendor/select2/src/js/banner.start.js +++ /dev/null @@ -1,6 +0,0 @@ -(function () { - // Restore the Select2 AMD loader so it can be used - // Needed mostly in the language files, where the loader is not inserted - if (jQuery && jQuery.fn && jQuery.fn.select2 && jQuery.fn.select2.amd) { - var S2 = jQuery.fn.select2.amd; - } diff --git a/static/vendor/select2/src/js/jquery.mousewheel.shim.js b/static/vendor/select2/src/js/jquery.mousewheel.shim.js deleted file mode 100644 index a27fda0e..00000000 --- a/static/vendor/select2/src/js/jquery.mousewheel.shim.js +++ /dev/null @@ -1,6 +0,0 @@ -define([ - 'jquery' -], function ($) { - // Used to shim jQuery.mousewheel for non-full builds. - return $; -}); diff --git a/static/vendor/select2/src/js/jquery.select2.js b/static/vendor/select2/src/js/jquery.select2.js deleted file mode 100644 index 9136ff78..00000000 --- a/static/vendor/select2/src/js/jquery.select2.js +++ /dev/null @@ -1,58 +0,0 @@ -define([ - 'jquery', - 'jquery-mousewheel', - - './select2/core', - './select2/defaults', - './select2/utils' -], function ($, _, Select2, Defaults, Utils) { - if ($.fn.select2 == null) { - // All methods that should return the element - var thisMethods = ['open', 'close', 'destroy']; - - $.fn.select2 = function (options) { - options = options || {}; - - if (typeof options === 'object') { - this.each(function () { - var instanceOptions = $.extend(true, {}, options); - - var instance = new Select2($(this), instanceOptions); - }); - - return this; - } else if (typeof options === 'string') { - var ret; - var args = Array.prototype.slice.call(arguments, 1); - - this.each(function () { - var instance = Utils.GetData(this, 'select2'); - - if (instance == null && window.console && console.error) { - console.error( - 'The select2(\'' + options + '\') method was called on an ' + - 'element that is not using Select2.' - ); - } - - ret = instance[options].apply(instance, args); - }); - - // Check if we should be returning `this` - if ($.inArray(options, thisMethods) > -1) { - return this; - } - - return ret; - } else { - throw new Error('Invalid arguments for Select2: ' + options); - } - }; - } - - if ($.fn.select2.defaults == null) { - $.fn.select2.defaults = Defaults; - } - - return Select2; -}); diff --git a/static/vendor/select2/src/js/jquery.shim.js b/static/vendor/select2/src/js/jquery.shim.js deleted file mode 100644 index 880993af..00000000 --- a/static/vendor/select2/src/js/jquery.shim.js +++ /dev/null @@ -1,14 +0,0 @@ -/* global jQuery:false, $:false */ -define(function () { - var _$ = jQuery || $; - - if (_$ == null && console && console.error) { - console.error( - 'Select2: An instance of jQuery or a jQuery-compatible library was not ' + - 'found. Make sure that you are including jQuery before Select2 on your ' + - 'web page.' - ); - } - - return _$; -}); diff --git a/static/vendor/select2/src/js/select2/compat/containerCss.js b/static/vendor/select2/src/js/select2/compat/containerCss.js deleted file mode 100644 index 45703ac8..00000000 --- a/static/vendor/select2/src/js/select2/compat/containerCss.js +++ /dev/null @@ -1,56 +0,0 @@ -define([ - 'jquery', - './utils' -], function ($, CompatUtils) { - // No-op CSS adapter that discards all classes by default - function _containerAdapter (clazz) { - return null; - } - - function ContainerCSS () { } - - ContainerCSS.prototype.render = function (decorated) { - var $container = decorated.call(this); - - var containerCssClass = this.options.get('containerCssClass') || ''; - - if ($.isFunction(containerCssClass)) { - containerCssClass = containerCssClass(this.$element); - } - - var containerCssAdapter = this.options.get('adaptContainerCssClass'); - containerCssAdapter = containerCssAdapter || _containerAdapter; - - if (containerCssClass.indexOf(':all:') !== -1) { - containerCssClass = containerCssClass.replace(':all:', ''); - - var _cssAdapter = containerCssAdapter; - - containerCssAdapter = function (clazz) { - var adapted = _cssAdapter(clazz); - - if (adapted != null) { - // Append the old one along with the adapted one - return adapted + ' ' + clazz; - } - - return clazz; - }; - } - - var containerCss = this.options.get('containerCss') || {}; - - if ($.isFunction(containerCss)) { - containerCss = containerCss(this.$element); - } - - CompatUtils.syncCssClasses($container, this.$element, containerCssAdapter); - - $container.css(containerCss); - $container.addClass(containerCssClass); - - return $container; - }; - - return ContainerCSS; -}); diff --git a/static/vendor/select2/src/js/select2/compat/dropdownCss.js b/static/vendor/select2/src/js/select2/compat/dropdownCss.js deleted file mode 100644 index 02f55820..00000000 --- a/static/vendor/select2/src/js/select2/compat/dropdownCss.js +++ /dev/null @@ -1,56 +0,0 @@ -define([ - 'jquery', - './utils' -], function ($, CompatUtils) { - // No-op CSS adapter that discards all classes by default - function _dropdownAdapter (clazz) { - return null; - } - - function DropdownCSS () { } - - DropdownCSS.prototype.render = function (decorated) { - var $dropdown = decorated.call(this); - - var dropdownCssClass = this.options.get('dropdownCssClass') || ''; - - if ($.isFunction(dropdownCssClass)) { - dropdownCssClass = dropdownCssClass(this.$element); - } - - var dropdownCssAdapter = this.options.get('adaptDropdownCssClass'); - dropdownCssAdapter = dropdownCssAdapter || _dropdownAdapter; - - if (dropdownCssClass.indexOf(':all:') !== -1) { - dropdownCssClass = dropdownCssClass.replace(':all:', ''); - - var _cssAdapter = dropdownCssAdapter; - - dropdownCssAdapter = function (clazz) { - var adapted = _cssAdapter(clazz); - - if (adapted != null) { - // Append the old one along with the adapted one - return adapted + ' ' + clazz; - } - - return clazz; - }; - } - - var dropdownCss = this.options.get('dropdownCss') || {}; - - if ($.isFunction(dropdownCss)) { - dropdownCss = dropdownCss(this.$element); - } - - CompatUtils.syncCssClasses($dropdown, this.$element, dropdownCssAdapter); - - $dropdown.css(dropdownCss); - $dropdown.addClass(dropdownCssClass); - - return $dropdown; - }; - - return DropdownCSS; -}); diff --git a/static/vendor/select2/src/js/select2/compat/initSelection.js b/static/vendor/select2/src/js/select2/compat/initSelection.js deleted file mode 100644 index 71ee29d1..00000000 --- a/static/vendor/select2/src/js/select2/compat/initSelection.js +++ /dev/null @@ -1,42 +0,0 @@ -define([ - 'jquery' -], function ($) { - function InitSelection (decorated, $element, options) { - if (options.get('debug') && window.console && console.warn) { - console.warn( - 'Select2: The `initSelection` option has been deprecated in favor' + - ' of a custom data adapter that overrides the `current` method. ' + - 'This method is now called multiple times instead of a single ' + - 'time when the instance is initialized. Support will be removed ' + - 'for the `initSelection` option in future versions of Select2' - ); - } - - this.initSelection = options.get('initSelection'); - this._isInitialized = false; - - decorated.call(this, $element, options); - } - - InitSelection.prototype.current = function (decorated, callback) { - var self = this; - - if (this._isInitialized) { - decorated.call(this, callback); - - return; - } - - this.initSelection.call(null, this.$element, function (data) { - self._isInitialized = true; - - if (!$.isArray(data)) { - data = [data]; - } - - callback(data); - }); - }; - - return InitSelection; -}); diff --git a/static/vendor/select2/src/js/select2/compat/inputData.js b/static/vendor/select2/src/js/select2/compat/inputData.js deleted file mode 100644 index 6e1dee26..00000000 --- a/static/vendor/select2/src/js/select2/compat/inputData.js +++ /dev/null @@ -1,128 +0,0 @@ -define([ - 'jquery', - '../utils' -], function ($, Utils) { - function InputData (decorated, $element, options) { - this._currentData = []; - this._valueSeparator = options.get('valueSeparator') || ','; - - if ($element.prop('type') === 'hidden') { - if (options.get('debug') && console && console.warn) { - console.warn( - 'Select2: Using a hidden input with Select2 is no longer ' + - 'supported and may stop working in the future. It is recommended ' + - 'to use a `' + - '' - ); - - this.$searchContainer = $search; - this.$search = $search.find('input'); - - $rendered.prepend($search); - - return $rendered; - }; - - Search.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - this.$search.on('keydown', function (evt) { - self.trigger('keypress', evt); - - self._keyUpPrevented = evt.isDefaultPrevented(); - }); - - // Workaround for browsers which do not support the `input` event - // This will prevent double-triggering of events for browsers which support - // both the `keyup` and `input` events. - this.$search.on('input', function (evt) { - // Unbind the duplicated `keyup` event - $(this).off('keyup'); - }); - - this.$search.on('keyup input', function (evt) { - self.handleSearch(evt); - }); - - container.on('open', function () { - self.$search.attr('tabindex', 0); - - self.$search.focus(); - - window.setTimeout(function () { - self.$search.focus(); - }, 0); - }); - - container.on('close', function () { - self.$search.attr('tabindex', -1); - - self.$search.val(''); - self.$search.blur(); - }); - - container.on('focus', function () { - if (!container.isOpen()) { - self.$search.focus(); - } - }); - - container.on('results:all', function (params) { - if (params.query.term == null || params.query.term === '') { - var showSearch = self.showSearch(params); - - if (showSearch) { - self.$searchContainer.removeClass('select2-search--hide'); - } else { - self.$searchContainer.addClass('select2-search--hide'); - } - } - }); - }; - - Search.prototype.handleSearch = function (evt) { - if (!this._keyUpPrevented) { - var input = this.$search.val(); - - this.trigger('query', { - term: input - }); - } - - this._keyUpPrevented = false; - }; - - Search.prototype.showSearch = function (_, params) { - return true; - }; - - return Search; -}); diff --git a/static/vendor/select2/src/js/select2/dropdown/selectOnClose.js b/static/vendor/select2/src/js/select2/dropdown/selectOnClose.js deleted file mode 100644 index 8c633356..00000000 --- a/static/vendor/select2/src/js/select2/dropdown/selectOnClose.js +++ /dev/null @@ -1,50 +0,0 @@ -define([ - '../utils' -], function (Utils) { - function SelectOnClose () { } - - SelectOnClose.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('close', function (params) { - self._handleSelectOnClose(params); - }); - }; - - SelectOnClose.prototype._handleSelectOnClose = function (_, params) { - if (params && params.originalSelect2Event != null) { - var event = params.originalSelect2Event; - - // Don't select an item if the close event was triggered from a select or - // unselect event - if (event._type === 'select' || event._type === 'unselect') { - return; - } - } - - var $highlightedResults = this.getHighlightedResults(); - - // Only select highlighted results - if ($highlightedResults.length < 1) { - return; - } - - var data = Utils.GetData($highlightedResults[0], 'data'); - - // Don't re-select already selected resulte - if ( - (data.element != null && data.element.selected) || - (data.element == null && data.selected) - ) { - return; - } - - this.trigger('select', { - data: data - }); - }; - - return SelectOnClose; -}); diff --git a/static/vendor/select2/src/js/select2/dropdown/stopPropagation.js b/static/vendor/select2/src/js/select2/dropdown/stopPropagation.js deleted file mode 100644 index 4f641827..00000000 --- a/static/vendor/select2/src/js/select2/dropdown/stopPropagation.js +++ /dev/null @@ -1,38 +0,0 @@ -define([ - -], function () { - function StopPropagation () { } - - StopPropagation.prototype.bind = function (decorated, container, $container) { - decorated.call(this, container, $container); - - var stoppedEvents = [ - 'blur', - 'change', - 'click', - 'dblclick', - 'focus', - 'focusin', - 'focusout', - 'input', - 'keydown', - 'keyup', - 'keypress', - 'mousedown', - 'mouseenter', - 'mouseleave', - 'mousemove', - 'mouseover', - 'mouseup', - 'search', - 'touchend', - 'touchstart' - ]; - - this.$dropdown.on(stoppedEvents.join(' '), function (evt) { - evt.stopPropagation(); - }); - }; - - return StopPropagation; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/af.js b/static/vendor/select2/src/js/select2/i18n/af.js deleted file mode 100644 index b2a70c97..00000000 --- a/static/vendor/select2/src/js/select2/i18n/af.js +++ /dev/null @@ -1,47 +0,0 @@ -define(function () { - // English - return { - errorLoading: function () { - return 'Die resultate kon nie gelaai word nie.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Verwyders asseblief ' + overChars + ' character'; - - if (overChars != 1) { - message += 's'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Voer asseblief ' + remainingChars + ' of meer karakters'; - - return message; - }, - loadingMore: function () { - return 'Meer resultate word gelaai…'; - }, - maximumSelected: function (args) { - var message = 'Kies asseblief net ' + args.maximum + ' item'; - - if (args.maximum != 1) { - message += 's'; - } - - return message; - }, - noResults: function () { - return 'Geen resultate gevind'; - }, - searching: function () { - return 'Besig…'; - }, - removeAllItems: function () { - return 'Verwyder alle items'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ar.js b/static/vendor/select2/src/js/select2/i18n/ar.js deleted file mode 100644 index 11f2c9e1..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ar.js +++ /dev/null @@ -1,33 +0,0 @@ -define(function () { - // Arabic - return { - errorLoading: function () { - return 'لا يمكن تحميل النتائج'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'الرجاء حذف ' + overChars + ' عناصر'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'الرجاء إضافة ' + remainingChars + ' عناصر'; - }, - loadingMore: function () { - return 'جاري تحميل نتائج إضافية...'; - }, - maximumSelected: function (args) { - return 'تستطيع إختيار ' + args.maximum + ' بنود فقط'; - }, - noResults: function () { - return 'لم يتم العثور على أي نتائج'; - }, - searching: function () { - return 'جاري البحث…'; - }, - removeAllItems: function () { - return 'قم بإزالة كل العناصر'; - } - }; -}); \ No newline at end of file diff --git a/static/vendor/select2/src/js/select2/i18n/az.js b/static/vendor/select2/src/js/select2/i18n/az.js deleted file mode 100644 index 8dae8749..00000000 --- a/static/vendor/select2/src/js/select2/i18n/az.js +++ /dev/null @@ -1,30 +0,0 @@ -define(function () { - // Azerbaijani - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return overChars + ' simvol silin'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return remainingChars + ' simvol daxil edin'; - }, - loadingMore: function () { - return 'Daha çox nəticə yüklənir…'; - }, - maximumSelected: function (args) { - return 'Sadəcə ' + args.maximum + ' element seçə bilərsiniz'; - }, - noResults: function () { - return 'Nəticə tapılmadı'; - }, - searching: function () { - return 'Axtarılır…'; - }, - removeAllItems: function () { - return 'Bütün elementləri sil'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/bg.js b/static/vendor/select2/src/js/select2/i18n/bg.js deleted file mode 100644 index e70af155..00000000 --- a/static/vendor/select2/src/js/select2/i18n/bg.js +++ /dev/null @@ -1,50 +0,0 @@ -define(function () { - // Bulgarian - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Моля въведете с ' + overChars + ' по-малко символ'; - - if (overChars > 1) { - message += 'a'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Моля въведете още ' + remainingChars + ' символ'; - - if (remainingChars > 1) { - message += 'a'; - } - - return message; - }, - loadingMore: function () { - return 'Зареждат се още…'; - }, - maximumSelected: function (args) { - var message = 'Можете да направите до ' + args.maximum + ' '; - - if (args.maximum > 1) { - message += 'избора'; - } else { - message += 'избор'; - } - - return message; - }, - noResults: function () { - return 'Няма намерени съвпадения'; - }, - searching: function () { - return 'Търсене…'; - }, - removeAllItems: function () { - return 'Премахнете всички елементи'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/bn.js b/static/vendor/select2/src/js/select2/i18n/bn.js deleted file mode 100644 index b28fdd79..00000000 --- a/static/vendor/select2/src/js/select2/i18n/bn.js +++ /dev/null @@ -1,45 +0,0 @@ -define(function () { - // Bangla - return { - errorLoading: function () { - return 'ফলাফলগুলি লোড করা যায়নি।'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'অনুগ্রহ করে ' + overChars + ' টি অক্ষর মুছে দিন।'; - - if (overChars != 1) { - message = 'অনুগ্রহ করে ' + overChars + ' টি অক্ষর মুছে দিন।'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = remainingChars + ' টি অক্ষর অথবা অধিক অক্ষর লিখুন।'; - - return message; - }, - loadingMore: function () { - return 'আরো ফলাফল লোড হচ্ছে ...'; - }, - maximumSelected: function (args) { - var message = args.maximum + ' টি আইটেম নির্বাচন করতে পারবেন।'; - - if (args.maximum != 1) { - message = args.maximum + ' টি আইটেম নির্বাচন করতে পারবেন।'; - } - - return message; - }, - noResults: function () { - return 'কোন ফলাফল পাওয়া যায়নি।'; - }, - searching: function () { - return 'অনুসন্ধান করা হচ্ছে ...'; - } - }; - }); - \ No newline at end of file diff --git a/static/vendor/select2/src/js/select2/i18n/bs.js b/static/vendor/select2/src/js/select2/i18n/bs.js deleted file mode 100644 index f94b12cd..00000000 --- a/static/vendor/select2/src/js/select2/i18n/bs.js +++ /dev/null @@ -1,58 +0,0 @@ -define(function () { - // Bosnian - function ending (count, one, some, many) { - if (count % 10 == 1 && count % 100 != 11) { - return one; - } - - if (count % 10 >= 2 && count % 10 <= 4 && - (count % 100 < 12 || count % 100 > 14)) { - return some; - } - - return many; - } - - return { - errorLoading: function () { - return 'Preuzimanje nije uspijelo.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Obrišite ' + overChars + ' simbol'; - - message += ending(overChars, '', 'a', 'a'); - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Ukucajte bar još ' + remainingChars + ' simbol'; - - message += ending(remainingChars, '', 'a', 'a'); - - return message; - }, - loadingMore: function () { - return 'Preuzimanje još rezultata…'; - }, - maximumSelected: function (args) { - var message = 'Možete izabrati samo ' + args.maximum + ' stavk'; - - message += ending(args.maximum, 'u', 'e', 'i'); - - return message; - }, - noResults: function () { - return 'Ništa nije pronađeno'; - }, - searching: function () { - return 'Pretraga…'; - }, - removeAllItems: function () { - return 'Uklonite sve stavke'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ca.js b/static/vendor/select2/src/js/select2/i18n/ca.js deleted file mode 100644 index d8b98557..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ca.js +++ /dev/null @@ -1,55 +0,0 @@ -define(function () { - // Catalan - return { - errorLoading: function () { - return 'La càrrega ha fallat'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Si us plau, elimina ' + overChars + ' car'; - - if (overChars == 1) { - message += 'àcter'; - } else { - message += 'àcters'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Si us plau, introdueix ' + remainingChars + ' car'; - - if (remainingChars == 1) { - message += 'àcter'; - } else { - message += 'àcters'; - } - - return message; - }, - loadingMore: function () { - return 'Carregant més resultats…'; - }, - maximumSelected: function (args) { - var message = 'Només es pot seleccionar ' + args.maximum + ' element'; - - if (args.maximum != 1) { - message += 's'; - } - - return message; - }, - noResults: function () { - return 'No s\'han trobat resultats'; - }, - searching: function () { - return 'Cercant…'; - }, - removeAllItems: function () { - return 'Treu tots els elements'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/cs.js b/static/vendor/select2/src/js/select2/i18n/cs.js deleted file mode 100644 index 9b3bce59..00000000 --- a/static/vendor/select2/src/js/select2/i18n/cs.js +++ /dev/null @@ -1,64 +0,0 @@ -define(function () { - // Czech - function small (count, masc) { - switch(count) { - case 2: - return masc ? 'dva' : 'dvě'; - case 3: - return 'tři'; - case 4: - return 'čtyři'; - } - return ''; - } - return { - errorLoading: function () { - return 'Výsledky nemohly být načteny.'; - }, - inputTooLong: function (args) { - var n = args.input.length - args.maximum; - - if (n == 1) { - return 'Prosím, zadejte o jeden znak méně.'; - } else if (n <= 4) { - return 'Prosím, zadejte o ' + small(n, true) + ' znaky méně.'; - } else { - return 'Prosím, zadejte o ' + n + ' znaků méně.'; - } - }, - inputTooShort: function (args) { - var n = args.minimum - args.input.length; - - if (n == 1) { - return 'Prosím, zadejte ještě jeden znak.'; - } else if (n <= 4) { - return 'Prosím, zadejte ještě další ' + small(n, true) + ' znaky.'; - } else { - return 'Prosím, zadejte ještě dalších ' + n + ' znaků.'; - } - }, - loadingMore: function () { - return 'Načítají se další výsledky…'; - }, - maximumSelected: function (args) { - var n = args.maximum; - - if (n == 1) { - return 'Můžete zvolit jen jednu položku.'; - } else if (n <= 4) { - return 'Můžete zvolit maximálně ' + small(n, false) + ' položky.'; - } else { - return 'Můžete zvolit maximálně ' + n + ' položek.'; - } - }, - noResults: function () { - return 'Nenalezeny žádné položky.'; - }, - searching: function () { - return 'Vyhledávání…'; - }, - removeAllItems: function () { - return 'Odstraňte všechny položky'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/da.js b/static/vendor/select2/src/js/select2/i18n/da.js deleted file mode 100644 index fdf5ebe8..00000000 --- a/static/vendor/select2/src/js/select2/i18n/da.js +++ /dev/null @@ -1,39 +0,0 @@ -define(function () { - // Danish - return { - errorLoading: function () { - return 'Resultaterne kunne ikke indlæses.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Angiv venligst ' + overChars + ' tegn mindre'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Angiv venligst ' + remainingChars + ' tegn mere'; - }, - loadingMore: function () { - return 'Indlæser flere resultater…'; - }, - maximumSelected: function (args) { - var message = 'Du kan kun vælge ' + args.maximum + ' emne'; - - if (args.maximum != 1) { - message += 'r'; - } - - return message; - }, - noResults: function () { - return 'Ingen resultater fundet'; - }, - searching: function () { - return 'Søger…'; - }, - removeAllItems: function () { - return 'Fjern alle elementer'; - } - }; -}); \ No newline at end of file diff --git a/static/vendor/select2/src/js/select2/i18n/de.js b/static/vendor/select2/src/js/select2/i18n/de.js deleted file mode 100644 index f8277c3b..00000000 --- a/static/vendor/select2/src/js/select2/i18n/de.js +++ /dev/null @@ -1,43 +0,0 @@ -define(function () { - // German - return { - errorLoading: function () { - return 'Die Ergebnisse konnten nicht geladen werden.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Bitte ' + overChars + ' Zeichen weniger eingeben'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Bitte ' + remainingChars + ' Zeichen mehr eingeben'; - }, - loadingMore: function () { - return 'Lade mehr Ergebnisse…'; - }, - maximumSelected: function (args) { - var message = 'Sie können nur ' + args.maximum + ' Eintr'; - - if (args.maximum === 1) { - message += 'ag'; - } else { - message += 'äge'; - } - - message += ' auswählen'; - - return message; - }, - noResults: function () { - return 'Keine Übereinstimmungen gefunden'; - }, - searching: function () { - return 'Suche…'; - }, - removeAllItems: function () { - return 'Entferne alle Gegenstände'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/dsb.js b/static/vendor/select2/src/js/select2/i18n/dsb.js deleted file mode 100644 index af3ec46f..00000000 --- a/static/vendor/select2/src/js/select2/i18n/dsb.js +++ /dev/null @@ -1,52 +0,0 @@ -define(function () { - // Lower Sorbian - var charsWords = ['znamuško', 'znamušce', 'znamuška','znamuškow']; - var itemsWords = ['zapisk', 'zapiska', 'zapiski','zapiskow']; - - var pluralWord = function pluralWord(numberOfChars, words) { - if (numberOfChars === 1) { - return words[0]; - } else if (numberOfChars === 2) { - return words[1]; - } else if (numberOfChars > 2 && numberOfChars <= 4) { - return words[2]; - } else if (numberOfChars >= 5) { - return words[3]; - } - }; - - return { - errorLoading: function () { - return 'Wuslědki njejsu se dali zacytaś.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Pšosym lašuj ' + overChars + ' ' + - pluralWord(overChars, charsWords); - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Pšosym zapódaj nanejmjenjej ' + remainingChars + ' ' + - pluralWord(remainingChars, charsWords); - }, - loadingMore: function () { - return 'Dalšne wuslědki se zacytaju…'; - }, - maximumSelected: function (args) { - return 'Móžoš jano ' + args.maximum + ' ' + - pluralWord(args.maximum, itemsWords) + 'wubraś.'; - }, - noResults: function () { - return 'Žedne wuslědki namakane'; - }, - searching: function () { - return 'Pyta se…'; - }, - removeAllItems: function () { - // To DO : in Lower Sorbian. - return 'Remove all items'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/el.js b/static/vendor/select2/src/js/select2/i18n/el.js deleted file mode 100644 index 49922292..00000000 --- a/static/vendor/select2/src/js/select2/i18n/el.js +++ /dev/null @@ -1,55 +0,0 @@ -define(function () { - // Greek (el) - return { - errorLoading: function () { - return 'Τα αποτελέσματα δεν μπόρεσαν να φορτώσουν.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Παρακαλώ διαγράψτε ' + overChars + ' χαρακτήρ'; - - if (overChars == 1) { - message += 'α'; - } - if (overChars != 1) { - message += 'ες'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Παρακαλώ συμπληρώστε ' + remainingChars + - ' ή περισσότερους χαρακτήρες'; - - return message; - }, - loadingMore: function () { - return 'Φόρτωση περισσότερων αποτελεσμάτων…'; - }, - maximumSelected: function (args) { - var message = 'Μπορείτε να επιλέξετε μόνο ' + args.maximum + ' επιλογ'; - - if (args.maximum == 1) { - message += 'ή'; - } - - if (args.maximum != 1) { - message += 'ές'; - } - - return message; - }, - noResults: function () { - return 'Δεν βρέθηκαν αποτελέσματα'; - }, - searching: function () { - return 'Αναζήτηση…'; - }, - removeAllItems: function () { - return 'Καταργήστε όλα τα στοιχεία'; - } - }; -}); \ No newline at end of file diff --git a/static/vendor/select2/src/js/select2/i18n/en.js b/static/vendor/select2/src/js/select2/i18n/en.js deleted file mode 100644 index 960a84e8..00000000 --- a/static/vendor/select2/src/js/select2/i18n/en.js +++ /dev/null @@ -1,47 +0,0 @@ -define(function () { - // English - return { - errorLoading: function () { - return 'The results could not be loaded.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Please delete ' + overChars + ' character'; - - if (overChars != 1) { - message += 's'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Please enter ' + remainingChars + ' or more characters'; - - return message; - }, - loadingMore: function () { - return 'Loading more results…'; - }, - maximumSelected: function (args) { - var message = 'You can only select ' + args.maximum + ' item'; - - if (args.maximum != 1) { - message += 's'; - } - - return message; - }, - noResults: function () { - return 'No results found'; - }, - searching: function () { - return 'Searching…'; - }, - removeAllItems: function () { - return 'Remove all items'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/es.js b/static/vendor/select2/src/js/select2/i18n/es.js deleted file mode 100644 index d95f303f..00000000 --- a/static/vendor/select2/src/js/select2/i18n/es.js +++ /dev/null @@ -1,55 +0,0 @@ -define(function () { - // Spanish - return { - errorLoading: function () { - return 'No se pudieron cargar los resultados'; - }, - inputTooLong: function (args) { - var remainingChars = args.input.length - args.maximum; - - var message = 'Por favor, elimine ' + remainingChars + ' car'; - - if (remainingChars == 1) { - message += 'ácter'; - } else { - message += 'acteres'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Por favor, introduzca ' + remainingChars + ' car'; - - if (remainingChars == 1) { - message += 'ácter'; - } else { - message += 'acteres'; - } - - return message; - }, - loadingMore: function () { - return 'Cargando más resultados…'; - }, - maximumSelected: function (args) { - var message = 'Sólo puede seleccionar ' + args.maximum + ' elemento'; - - if (args.maximum != 1) { - message += 's'; - } - - return message; - }, - noResults: function () { - return 'No se encontraron resultados'; - }, - searching: function () { - return 'Buscando…'; - }, - removeAllItems: function () { - return 'Eliminar todos los elementos'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/et.js b/static/vendor/select2/src/js/select2/i18n/et.js deleted file mode 100644 index 589e4e7b..00000000 --- a/static/vendor/select2/src/js/select2/i18n/et.js +++ /dev/null @@ -1,56 +0,0 @@ -define(function () { - // Estonian - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Sisesta ' + overChars + ' täht'; - - if (overChars != 1) { - message += 'e'; - } - - message += ' vähem'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Sisesta ' + remainingChars + ' täht'; - - if (remainingChars != 1) { - message += 'e'; - } - - message += ' rohkem'; - - return message; - }, - loadingMore: function () { - return 'Laen tulemusi…'; - }, - maximumSelected: function (args) { - var message = 'Saad vaid ' + args.maximum + ' tulemus'; - - if (args.maximum == 1) { - message += 'e'; - } else { - message += 't'; - } - - message += ' valida'; - - return message; - }, - noResults: function () { - return 'Tulemused puuduvad'; - }, - searching: function () { - return 'Otsin…'; - }, - removeAllItems: function () { - return 'Eemalda kõik esemed'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/eu.js b/static/vendor/select2/src/js/select2/i18n/eu.js deleted file mode 100644 index 362745a2..00000000 --- a/static/vendor/select2/src/js/select2/i18n/eu.js +++ /dev/null @@ -1,54 +0,0 @@ -define(function () { - // Basque - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Idatzi '; - - if (overChars == 1) { - message += 'karaktere bat'; - } else { - message += overChars + ' karaktere'; - } - - message += ' gutxiago'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Idatzi '; - - if (remainingChars == 1) { - message += 'karaktere bat'; - } else { - message += remainingChars + ' karaktere'; - } - - message += ' gehiago'; - - return message; - }, - loadingMore: function () { - return 'Emaitza gehiago kargatzen…'; - }, - maximumSelected: function (args) { - if (args.maximum === 1) { - return 'Elementu bakarra hauta dezakezu'; - } else { - return args.maximum + ' elementu hauta ditzakezu soilik'; - } - }, - noResults: function () { - return 'Ez da bat datorrenik aurkitu'; - }, - searching: function () { - return 'Bilatzen…'; - }, - removeAllItems: function () { - return 'Kendu elementu guztiak'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/fa.js b/static/vendor/select2/src/js/select2/i18n/fa.js deleted file mode 100644 index 4b92d0e4..00000000 --- a/static/vendor/select2/src/js/select2/i18n/fa.js +++ /dev/null @@ -1,41 +0,0 @@ -/* jshint -W100 */ -/* jslint maxlen: 86 */ -define(function () { - // Farsi (Persian) - return { - errorLoading: function () { - return 'امکان بارگذاری نتایج وجود ندارد.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'لطفاً ' + overChars + ' کاراکتر را حذف نمایید'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'لطفاً تعداد ' + remainingChars + ' کاراکتر یا بیشتر وارد نمایید'; - - return message; - }, - loadingMore: function () { - return 'در حال بارگذاری نتایج بیشتر...'; - }, - maximumSelected: function (args) { - var message = 'شما تنها می‌توانید ' + args.maximum + ' آیتم را انتخاب نمایید'; - - return message; - }, - noResults: function () { - return 'هیچ نتیجه‌ای یافت نشد'; - }, - searching: function () { - return 'در حال جستجو...'; - }, - removeAllItems: function () { - return 'همه موارد را حذف کنید'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/fi.js b/static/vendor/select2/src/js/select2/i18n/fi.js deleted file mode 100644 index cc062d61..00000000 --- a/static/vendor/select2/src/js/select2/i18n/fi.js +++ /dev/null @@ -1,33 +0,0 @@ -define(function () { - // Finnish - return { - errorLoading: function () { - return 'Tuloksia ei saatu ladattua.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Ole hyvä ja anna ' + overChars + ' merkkiä vähemmän'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Ole hyvä ja anna ' + remainingChars + ' merkkiä lisää'; - }, - loadingMore: function () { - return 'Ladataan lisää tuloksia…'; - }, - maximumSelected: function (args) { - return 'Voit valita ainoastaan ' + args.maximum + ' kpl'; - }, - noResults: function () { - return 'Ei tuloksia'; - }, - searching: function () { - return 'Haetaan…'; - }, - removeAllItems: function () { - return 'Poista kaikki kohteet'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/fr.js b/static/vendor/select2/src/js/select2/i18n/fr.js deleted file mode 100644 index 02c4d928..00000000 --- a/static/vendor/select2/src/js/select2/i18n/fr.js +++ /dev/null @@ -1,36 +0,0 @@ -define(function () { - // French - return { - errorLoading: function () { - return 'Les résultats ne peuvent pas être chargés.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Supprimez ' + overChars + ' caractère' + - ((overChars > 1) ? 's' : ''); - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Saisissez au moins ' + remainingChars + ' caractère' + - ((remainingChars > 1) ? 's' : ''); - }, - loadingMore: function () { - return 'Chargement de résultats supplémentaires…'; - }, - maximumSelected: function (args) { - return 'Vous pouvez seulement sélectionner ' + args.maximum + - ' élément' + ((args.maximum > 1) ? 's' : ''); - }, - noResults: function () { - return 'Aucun résultat trouvé'; - }, - searching: function () { - return 'Recherche en cours…'; - }, - removeAllItems: function () { - return 'Supprimer tous les éléments'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/gl.js b/static/vendor/select2/src/js/select2/i18n/gl.js deleted file mode 100644 index a92d6a5a..00000000 --- a/static/vendor/select2/src/js/select2/i18n/gl.js +++ /dev/null @@ -1,42 +0,0 @@ -define(function () { - // Galician - return { - errorLoading: function () { - return 'Non foi posíbel cargar os resultados.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - if (overChars === 1) { - return 'Elimine un carácter'; - } - return 'Elimine ' + overChars + ' caracteres'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - if (remainingChars === 1) { - return 'Engada un carácter'; - } - return 'Engada ' + remainingChars + ' caracteres'; - }, - loadingMore: function () { - return 'Cargando máis resultados…'; - }, - maximumSelected: function (args) { - if (args.maximum === 1) { - return 'Só pode seleccionar un elemento'; - } - return 'Só pode seleccionar ' + args.maximum + ' elementos'; - }, - noResults: function () { - return 'Non se atoparon resultados'; - }, - searching: function () { - return 'Buscando…'; - }, - removeAllItems: function () { - return 'Elimina todos os elementos'; - } - }; -}); \ No newline at end of file diff --git a/static/vendor/select2/src/js/select2/i18n/he.js b/static/vendor/select2/src/js/select2/i18n/he.js deleted file mode 100644 index 5192d866..00000000 --- a/static/vendor/select2/src/js/select2/i18n/he.js +++ /dev/null @@ -1,59 +0,0 @@ -define(function () { - // Hebrew - return { - errorLoading: function () { - return 'שגיאה בטעינת התוצאות'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'נא למחוק '; - - if (overChars === 1) { - message += 'תו אחד'; - } else { - message += overChars + ' תווים'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'נא להכניס '; - - if (remainingChars === 1) { - message += 'תו אחד'; - } else { - message += remainingChars + ' תווים'; - } - - message += ' או יותר'; - - return message; - }, - loadingMore: function () { - return 'טוען תוצאות נוספות…'; - }, - maximumSelected: function (args) { - var message = 'באפשרותך לבחור עד '; - - if (args.maximum === 1) { - message += 'פריט אחד'; - } else { - message += args.maximum + ' פריטים'; - } - - return message; - }, - noResults: function () { - return 'לא נמצאו תוצאות'; - }, - searching: function () { - return 'מחפש…'; - }, - removeAllItems: function () { - return 'הסר את כל הפריטים'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/hi.js b/static/vendor/select2/src/js/select2/i18n/hi.js deleted file mode 100644 index 234a5926..00000000 --- a/static/vendor/select2/src/js/select2/i18n/hi.js +++ /dev/null @@ -1,42 +0,0 @@ -define(function () { - // Hindi - return { - errorLoading: function () { - return 'परिणामों को लोड नहीं किया जा सका।'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = overChars + ' अक्षर को हटा दें'; - - if (overChars > 1) { - message = overChars + ' अक्षरों को हटा दें '; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'कृपया ' + remainingChars + ' या अधिक अक्षर दर्ज करें'; - - return message; - }, - loadingMore: function () { - return 'अधिक परिणाम लोड हो रहे है...'; - }, - maximumSelected: function (args) { - var message = 'आप केवल ' + args.maximum + ' आइटम का चयन कर सकते हैं'; - return message; - }, - noResults: function () { - return 'कोई परिणाम नहीं मिला'; - }, - searching: function () { - return 'खोज रहा है...'; - }, - removeAllItems: function () { - return 'सभी वस्तुओं को हटा दें'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/hr.js b/static/vendor/select2/src/js/select2/i18n/hr.js deleted file mode 100644 index 06223447..00000000 --- a/static/vendor/select2/src/js/select2/i18n/hr.js +++ /dev/null @@ -1,47 +0,0 @@ -define(function () { - // Croatian - function character (n) { - var message = ' ' + n + ' znak'; - - if (n % 10 < 5 && n % 10 > 0 && (n % 100 < 5 || n % 100 > 19)) { - if (n % 10 > 1) { - message += 'a'; - } - } else { - message += 'ova'; - } - - return message; - } - - return { - errorLoading: function () { - return 'Preuzimanje nije uspjelo.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Unesite ' + character(overChars); - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Unesite još ' + character(remainingChars); - }, - loadingMore: function () { - return 'Učitavanje rezultata…'; - }, - maximumSelected: function (args) { - return 'Maksimalan broj odabranih stavki je ' + args.maximum; - }, - noResults: function () { - return 'Nema rezultata'; - }, - searching: function () { - return 'Pretraga…'; - }, - removeAllItems: function () { - return 'Ukloni sve stavke'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/hsb.js b/static/vendor/select2/src/js/select2/i18n/hsb.js deleted file mode 100644 index a72417d4..00000000 --- a/static/vendor/select2/src/js/select2/i18n/hsb.js +++ /dev/null @@ -1,52 +0,0 @@ -define(function () { - // Upper Sorbian - var charsWords = ['znamješko', 'znamješce', 'znamješka','znamješkow']; - var itemsWords = ['zapisk', 'zapiskaj', 'zapiski','zapiskow']; - - var pluralWord = function pluralWord(numberOfChars, words) { - if (numberOfChars === 1) { - return words[0]; - } else if (numberOfChars === 2) { - return words[1]; - } else if (numberOfChars > 2 && numberOfChars <= 4) { - return words[2]; - } else if (numberOfChars >= 5) { - return words[3]; - } - }; - - return { - errorLoading: function () { - return 'Wuslědki njedachu so začitać.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Prošu zhašej ' + overChars + ' ' + - pluralWord(overChars, charsWords); - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Prošu zapodaj znajmjeńša ' + remainingChars + ' ' + - pluralWord(remainingChars, charsWords); - }, - loadingMore: function () { - return 'Dalše wuslědki so začitaja…'; - }, - maximumSelected: function (args) { - return 'Móžeš jenož ' + args.maximum + ' ' + - pluralWord(args.maximum, itemsWords) + 'wubrać'; - }, - noResults: function () { - return 'Žane wuslědki namakane'; - }, - searching: function () { - return 'Pyta so…'; - }, - removeAllItems: function () { - // To DO : in Upper Sorbian. - return 'Remove all items'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/hu.js b/static/vendor/select2/src/js/select2/i18n/hu.js deleted file mode 100644 index 9c62748c..00000000 --- a/static/vendor/select2/src/js/select2/i18n/hu.js +++ /dev/null @@ -1,33 +0,0 @@ -define(function () { - // Hungarian - return { - errorLoading: function () { - return 'Az eredmények betöltése nem sikerült.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Túl hosszú. ' + overChars + ' karakterrel több, mint kellene.'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Túl rövid. Még ' + remainingChars + ' karakter hiányzik.'; - }, - loadingMore: function () { - return 'Töltés…'; - }, - maximumSelected: function (args) { - return 'Csak ' + args.maximum + ' elemet lehet kiválasztani.'; - }, - noResults: function () { - return 'Nincs találat.'; - }, - searching: function () { - return 'Keresés…'; - }, - removeAllItems: function () { - return 'Távolítson el minden elemet'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/hy.js b/static/vendor/select2/src/js/select2/i18n/hy.js deleted file mode 100644 index 696e89dd..00000000 --- a/static/vendor/select2/src/js/select2/i18n/hy.js +++ /dev/null @@ -1,41 +0,0 @@ -define(function () { - // Armenian - return { - errorLoading: function () { - return 'Արդյունքները հնարավոր չէ բեռնել։'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Խնդրում ենք հեռացնել ' + overChars + ' նշան'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Խնդրում ենք մուտքագրել ' + remainingChars + - ' կամ ավել նշաններ'; - - return message; - }, - loadingMore: function () { - return 'Բեռնվում են նոր արդյունքներ․․․'; - }, - maximumSelected: function (args) { - var message = 'Դուք կարող եք ընտրել առավելագույնը ' + args.maximum + - ' կետ'; - - return message; - }, - noResults: function () { - return 'Արդյունքներ չեն գտնվել'; - }, - searching: function () { - return 'Որոնում․․․'; - }, - removeAllItems: function () { - return 'Հեռացնել բոլոր տարրերը'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/id.js b/static/vendor/select2/src/js/select2/i18n/id.js deleted file mode 100644 index ceb2c1bb..00000000 --- a/static/vendor/select2/src/js/select2/i18n/id.js +++ /dev/null @@ -1,33 +0,0 @@ -define(function () { - // Indonesian - return { - errorLoading: function () { - return 'Data tidak boleh diambil.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Hapuskan ' + overChars + ' huruf'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Masukkan ' + remainingChars + ' huruf lagi'; - }, - loadingMore: function () { - return 'Mengambil data…'; - }, - maximumSelected: function (args) { - return 'Anda hanya dapat memilih ' + args.maximum + ' pilihan'; - }, - noResults: function () { - return 'Tidak ada data yang sesuai'; - }, - searching: function () { - return 'Mencari…'; - }, - removeAllItems: function () { - return 'Hapus semua item'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/is.js b/static/vendor/select2/src/js/select2/i18n/is.js deleted file mode 100644 index f16283fa..00000000 --- a/static/vendor/select2/src/js/select2/i18n/is.js +++ /dev/null @@ -1,44 +0,0 @@ -define(function () { - // Icelandic - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Vinsamlegast styttið texta um ' + overChars + ' staf'; - - if (overChars <= 1) { - return message; - } - - return message + 'i'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Vinsamlegast skrifið ' + remainingChars + ' staf'; - - if (remainingChars > 1) { - message += 'i'; - } - - message += ' í viðbót'; - - return message; - }, - loadingMore: function () { - return 'Sæki fleiri niðurstöður…'; - }, - maximumSelected: function (args) { - return 'Þú getur aðeins valið ' + args.maximum + ' atriði'; - }, - noResults: function () { - return 'Ekkert fannst'; - }, - searching: function () { - return 'Leita…'; - }, - removeAllItems: function () { - return 'Fjarlægðu öll atriði'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/it.js b/static/vendor/select2/src/js/select2/i18n/it.js deleted file mode 100644 index 2bedeb9b..00000000 --- a/static/vendor/select2/src/js/select2/i18n/it.js +++ /dev/null @@ -1,51 +0,0 @@ -define(function () { - // Italian - return { - errorLoading: function () { - return 'I risultati non possono essere caricati.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Per favore cancella ' + overChars + ' caratter'; - - if (overChars !== 1) { - message += 'i'; - } else { - message += 'e'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Per favore inserisci ' +remainingChars+ ' o più caratteri'; - - return message; - }, - loadingMore: function () { - return 'Caricando più risultati…'; - }, - maximumSelected: function (args) { - var message = 'Puoi selezionare solo ' + args.maximum + ' element'; - - if (args.maximum !== 1) { - message += 'i'; - } else { - message += 'o'; - } - - return message; - }, - noResults: function () { - return 'Nessun risultato trovato'; - }, - searching: function () { - return 'Sto cercando…'; - }, - removeAllItems: function () { - return 'Rimuovi tutti gli oggetti'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ja.js b/static/vendor/select2/src/js/select2/i18n/ja.js deleted file mode 100644 index 68568f32..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ja.js +++ /dev/null @@ -1,39 +0,0 @@ -define(function () { - // Japanese - return { - errorLoading: function () { - return '結果が読み込まれませんでした'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = overChars + ' 文字を削除してください'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = '少なくとも ' + remainingChars + ' 文字を入力してください'; - - return message; - }, - loadingMore: function () { - return '読み込み中…'; - }, - maximumSelected: function (args) { - var message = args.maximum + ' 件しか選択できません'; - - return message; - }, - noResults: function () { - return '対象が見つかりません'; - }, - searching: function () { - return '検索しています…'; - }, - removeAllItems: function () { - return 'すべてのアイテムを削除'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ka.js b/static/vendor/select2/src/js/select2/i18n/ka.js deleted file mode 100644 index 306ea90c..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ka.js +++ /dev/null @@ -1,40 +0,0 @@ -define(function () { - // Georgian - return { - errorLoading: function () { - return 'მონაცემების ჩატვირთვა შეუძლებელია.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'გთხოვთ აკრიფეთ ' + overChars + ' სიმბოლოთი ნაკლები'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'გთხოვთ აკრიფეთ ' + remainingChars + ' სიმბოლო ან მეტი'; - - return message; - }, - loadingMore: function () { - return 'მონაცემების ჩატვირთვა…'; - }, - maximumSelected: function (args) { - var message = 'თქვენ შეგიძლიათ აირჩიოთ არაუმეტეს ' + args.maximum + - ' ელემენტი'; - - return message; - }, - noResults: function () { - return 'რეზულტატი არ მოიძებნა'; - }, - searching: function () { - return 'ძიება…'; - }, - removeAllItems: function () { - return 'ამოიღე ყველა ელემენტი'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/km.js b/static/vendor/select2/src/js/select2/i18n/km.js deleted file mode 100644 index 066d5230..00000000 --- a/static/vendor/select2/src/js/select2/i18n/km.js +++ /dev/null @@ -1,39 +0,0 @@ -define(function () { - // Khmer - return { - errorLoading: function () { - return 'មិនអាចទាញយកទិន្នន័យ'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'សូមលុបចេញ ' + overChars + ' អក្សរ'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'សូមបញ្ចូល' + remainingChars + ' អក្សរ រឺ ច្រើនជាងនេះ'; - - return message; - }, - loadingMore: function () { - return 'កំពុងទាញយកទិន្នន័យបន្ថែម...'; - }, - maximumSelected: function (args) { - var message = 'អ្នកអាចជ្រើសរើសបានតែ ' + args.maximum + ' ជម្រើសប៉ុណ្ណោះ'; - - return message; - }, - noResults: function () { - return 'មិនមានលទ្ធផល'; - }, - searching: function () { - return 'កំពុងស្វែងរក...'; - }, - removeAllItems: function () { - return 'លុបធាតុទាំងអស់'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ko.js b/static/vendor/select2/src/js/select2/i18n/ko.js deleted file mode 100644 index 03b5dc09..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ko.js +++ /dev/null @@ -1,39 +0,0 @@ -define(function () { - // Korean - return { - errorLoading: function () { - return '결과를 불러올 수 없습니다.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = '너무 깁니다. ' + overChars + ' 글자 지워주세요.'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = '너무 짧습니다. ' + remainingChars + ' 글자 더 입력해주세요.'; - - return message; - }, - loadingMore: function () { - return '불러오는 중…'; - }, - maximumSelected: function (args) { - var message = '최대 ' + args.maximum + '개까지만 선택 가능합니다.'; - - return message; - }, - noResults: function () { - return '결과가 없습니다.'; - }, - searching: function () { - return '검색 중…'; - }, - removeAllItems: function () { - return '모든 항목 삭제'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/lt.js b/static/vendor/select2/src/js/select2/i18n/lt.js deleted file mode 100644 index c6f8ad9e..00000000 --- a/static/vendor/select2/src/js/select2/i18n/lt.js +++ /dev/null @@ -1,55 +0,0 @@ -define(function () { - // rules from - // http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html#lt - function ending(count, one, few, other) { - if (count % 10 === 1 && (count % 100 < 11 || count % 100 > 19)) { - return one; - } else if ( - (count % 10 >= 2 && count % 10 <= 9) && - (count % 100 < 11 || count % 100 > 19)) { - return few; - } else { - return other; - } - } - - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Pašalinkite ' + overChars + ' simbol'; - - message += ending(overChars, 'į', 'ius', 'ių'); - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Įrašykite dar ' + remainingChars + ' simbol'; - - message += ending(remainingChars, 'į', 'ius', 'ių'); - - return message; - }, - loadingMore: function () { - return 'Kraunama daugiau rezultatų…'; - }, - maximumSelected: function (args) { - var message = 'Jūs galite pasirinkti tik ' + args.maximum + ' element'; - - message += ending(args.maximum, 'ą', 'us', 'ų'); - - return message; - }, - noResults: function () { - return 'Atitikmenų nerasta'; - }, - searching: function () { - return 'Ieškoma…'; - }, - removeAllItems: function () { - return 'Pašalinti visus elementus'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/lv.js b/static/vendor/select2/src/js/select2/i18n/lv.js deleted file mode 100644 index 53324fd6..00000000 --- a/static/vendor/select2/src/js/select2/i18n/lv.js +++ /dev/null @@ -1,54 +0,0 @@ -define(function () { - // Latvian - function ending (count, eleven, singular, other) { - if (count === 11) { - return eleven; - } - - if (count % 10 === 1) { - return singular; - } - - return other; - } - - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Lūdzu ievadiet par ' + overChars; - - message += ' simbol' + ending(overChars, 'iem', 'u', 'iem'); - - return message + ' mazāk'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Lūdzu ievadiet vēl ' + remainingChars; - - message += ' simbol' + ending(remainingChars, 'us', 'u', 'us'); - - return message; - }, - loadingMore: function () { - return 'Datu ielāde…'; - }, - maximumSelected: function (args) { - var message = 'Jūs varat izvēlēties ne vairāk kā ' + args.maximum; - - message += ' element' + ending(args.maximum, 'us', 'u', 'us'); - - return message; - }, - noResults: function () { - return 'Sakritību nav'; - }, - searching: function () { - return 'Meklēšana…'; - }, - removeAllItems: function () { - return 'Noņemt visus vienumus'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/mk.js b/static/vendor/select2/src/js/select2/i18n/mk.js deleted file mode 100644 index 60845112..00000000 --- a/static/vendor/select2/src/js/select2/i18n/mk.js +++ /dev/null @@ -1,50 +0,0 @@ -define(function () { - // Macedonian - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Ве молиме внесете ' + args.maximum + ' помалку карактер'; - - if (args.maximum !== 1) { - message += 'и'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Ве молиме внесете уште ' + args.maximum + ' карактер'; - - if (args.maximum !== 1) { - message += 'и'; - } - - return message; - }, - loadingMore: function () { - return 'Вчитување резултати…'; - }, - maximumSelected: function (args) { - var message = 'Можете да изберете само ' + args.maximum + ' ставк'; - - if (args.maximum === 1) { - message += 'а'; - } else { - message += 'и'; - } - - return message; - }, - noResults: function () { - return 'Нема пронајдено совпаѓања'; - }, - searching: function () { - return 'Пребарување…'; - }, - removeAllItems: function () { - return 'Отстрани ги сите предмети'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ms.js b/static/vendor/select2/src/js/select2/i18n/ms.js deleted file mode 100644 index 3338c87e..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ms.js +++ /dev/null @@ -1,33 +0,0 @@ -define(function () { - // Malay - return { - errorLoading: function () { - return 'Keputusan tidak berjaya dimuatkan.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Sila hapuskan ' + overChars + ' aksara'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Sila masukkan ' + remainingChars + ' atau lebih aksara'; - }, - loadingMore: function () { - return 'Sedang memuatkan keputusan…'; - }, - maximumSelected: function (args) { - return 'Anda hanya boleh memilih ' + args.maximum + ' pilihan'; - }, - noResults: function () { - return 'Tiada padanan yang ditemui'; - }, - searching: function () { - return 'Mencari…'; - }, - removeAllItems: function () { - return 'Keluarkan semua item'; - } - }; -}); \ No newline at end of file diff --git a/static/vendor/select2/src/js/select2/i18n/nb.js b/static/vendor/select2/src/js/select2/i18n/nb.js deleted file mode 100644 index aee869be..00000000 --- a/static/vendor/select2/src/js/select2/i18n/nb.js +++ /dev/null @@ -1,33 +0,0 @@ -define(function () { - // Norwegian (Bokmål) - return { - errorLoading: function () { - return 'Kunne ikke hente resultater.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Vennligst fjern ' + overChars + ' tegn'; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Vennligst skriv inn ' + remainingChars + ' tegn til'; - }, - loadingMore: function () { - return 'Laster flere resultater…'; - }, - maximumSelected: function (args) { - return 'Du kan velge maks ' + args.maximum + ' elementer'; - }, - noResults: function () { - return 'Ingen treff'; - }, - searching: function () { - return 'Søker…'; - }, - removeAllItems: function () { - return 'Fjern alle elementer'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ne.js b/static/vendor/select2/src/js/select2/i18n/ne.js deleted file mode 100644 index 133af69b..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ne.js +++ /dev/null @@ -1,47 +0,0 @@ -define(function () { - // Nepali - return { - errorLoading: function () { - return 'नतिजाहरु देखाउन सकिएन।'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'कृपया ' + overChars + ' अक्षर मेटाउनुहोस्।'; - - if (overChars != 1) { - message += 'कृपया ' + overChars + ' अक्षरहरु मेटाउनुहोस्।'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'कृपया बाँकी रहेका ' + remainingChars + - ' वा अरु धेरै अक्षरहरु भर्नुहोस्।'; - - return message; - }, - loadingMore: function () { - return 'अरु नतिजाहरु भरिँदैछन् …'; - }, - maximumSelected: function (args) { - var message = 'तँपाई ' + args.maximum + - ' वस्तु मात्र छान्न पाउँनुहुन्छ।'; - - if (args.maximum != 1) { - message = 'तँपाई ' + args.maximum + - ' वस्तुहरु मात्र छान्न पाउँनुहुन्छ।'; - } - - return message; - }, - noResults: function () { - return 'कुनै पनि नतिजा भेटिएन।'; - }, - searching: function () { - return 'खोजि हुँदैछ…'; - } - }; - }); diff --git a/static/vendor/select2/src/js/select2/i18n/nl.js b/static/vendor/select2/src/js/select2/i18n/nl.js deleted file mode 100644 index 7e4a5fea..00000000 --- a/static/vendor/select2/src/js/select2/i18n/nl.js +++ /dev/null @@ -1,46 +0,0 @@ -define(function () { - // Dutch - return { - errorLoading: function () { - return 'De resultaten konden niet worden geladen.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Gelieve ' + overChars + ' karakters te verwijderen'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Gelieve ' + remainingChars + - ' of meer karakters in te voeren'; - - return message; - }, - loadingMore: function () { - return 'Meer resultaten laden…'; - }, - maximumSelected: function (args) { - var verb = args.maximum == 1 ? 'kan' : 'kunnen'; - var message = 'Er ' + verb + ' maar ' + args.maximum + ' item'; - - if (args.maximum != 1) { - message += 's'; - } - message += ' worden geselecteerd'; - - return message; - }, - noResults: function () { - return 'Geen resultaten gevonden…'; - }, - searching: function () { - return 'Zoeken…'; - }, - removeAllItems: function () { - return 'Verwijder alle items'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/pl.js b/static/vendor/select2/src/js/select2/i18n/pl.js deleted file mode 100644 index 1a3d890a..00000000 --- a/static/vendor/select2/src/js/select2/i18n/pl.js +++ /dev/null @@ -1,48 +0,0 @@ -define(function () { - // Polish - var charsWords = ['znak', 'znaki', 'znaków']; - var itemsWords = ['element', 'elementy', 'elementów']; - - var pluralWord = function pluralWord(numberOfChars, words) { - if (numberOfChars === 1) { - return words[0]; - } else if (numberOfChars > 1 && numberOfChars <= 4) { - return words[1]; - } else if (numberOfChars >= 5) { - return words[2]; - } - }; - - return { - errorLoading: function () { - return 'Nie można załadować wyników.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - return 'Usuń ' + overChars + ' ' + pluralWord(overChars, charsWords); - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - return 'Podaj przynajmniej ' + remainingChars + ' ' + - pluralWord(remainingChars, charsWords); - }, - loadingMore: function () { - return 'Trwa ładowanie…'; - }, - maximumSelected: function (args) { - return 'Możesz zaznaczyć tylko ' + args.maximum + ' ' + - pluralWord(args.maximum, itemsWords); - }, - noResults: function () { - return 'Brak wyników'; - }, - searching: function () { - return 'Trwa wyszukiwanie…'; - }, - removeAllItems: function () { - return 'Usuń wszystkie przedmioty'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ps.js b/static/vendor/select2/src/js/select2/i18n/ps.js deleted file mode 100644 index cb5797a2..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ps.js +++ /dev/null @@ -1,48 +0,0 @@ -/* jslint maxlen: 87 */ -define(function () { - // Pashto (پښتو) - return { - errorLoading: function () { - return 'پايلي نه سي ترلاسه کېدای'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'د مهربانۍ لمخي ' + overChars + ' توری ړنګ کړئ'; - - if (overChars != 1) { - message = message.replace('توری', 'توري'); - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'لږ تر لږه ' + remainingChars + ' يا ډېر توري وليکئ'; - - return message; - }, - loadingMore: function () { - return 'نوري پايلي ترلاسه کيږي...'; - }, - maximumSelected: function (args) { - var message = 'تاسو يوازي ' + args.maximum + ' قلم په نښه کولای سی'; - - if (args.maximum != 1) { - message = message.replace('قلم', 'قلمونه'); - } - - return message; - }, - noResults: function () { - return 'پايلي و نه موندل سوې'; - }, - searching: function () { - return 'لټول کيږي...'; - }, - removeAllItems: function () { - return 'ټول توکي لرې کړئ'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/pt-BR.js b/static/vendor/select2/src/js/select2/i18n/pt-BR.js deleted file mode 100644 index 6e5beb0a..00000000 --- a/static/vendor/select2/src/js/select2/i18n/pt-BR.js +++ /dev/null @@ -1,49 +0,0 @@ -define(function () { - // Brazilian Portuguese - return { - errorLoading: function () { - return 'Os resultados não puderam ser carregados.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Apague ' + overChars + ' caracter'; - - if (overChars != 1) { - message += 'es'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Digite ' + remainingChars + ' ou mais caracteres'; - - return message; - }, - loadingMore: function () { - return 'Carregando mais resultados…'; - }, - maximumSelected: function (args) { - var message = 'Você só pode selecionar ' + args.maximum + ' ite'; - - if (args.maximum == 1) { - message += 'm'; - } else { - message += 'ns'; - } - - return message; - }, - noResults: function () { - return 'Nenhum resultado encontrado'; - }, - searching: function () { - return 'Buscando…'; - }, - removeAllItems: function () { - return 'Remover todos os itens'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/pt.js b/static/vendor/select2/src/js/select2/i18n/pt.js deleted file mode 100644 index 27bb0293..00000000 --- a/static/vendor/select2/src/js/select2/i18n/pt.js +++ /dev/null @@ -1,43 +0,0 @@ -define(function () { - // European Portuguese - return { - errorLoading: function () { - return 'Os resultados não puderam ser carregados.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Por favor apague ' + overChars + ' '; - - message += overChars != 1 ? 'caracteres' : 'caractere'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Introduza ' + remainingChars + ' ou mais caracteres'; - - return message; - }, - loadingMore: function () { - return 'A carregar mais resultados…'; - }, - maximumSelected: function (args) { - var message = 'Apenas pode seleccionar ' + args.maximum + ' '; - - message += args.maximum != 1 ? 'itens' : 'item'; - - return message; - }, - noResults: function () { - return 'Sem resultados'; - }, - searching: function () { - return 'A procurar…'; - }, - removeAllItems: function () { - return 'Remover todos os itens'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ro.js b/static/vendor/select2/src/js/select2/i18n/ro.js deleted file mode 100644 index 75d17815..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ro.js +++ /dev/null @@ -1,49 +0,0 @@ -define(function () { - // Romanian - return { - errorLoading: function () { - return 'Rezultatele nu au putut fi incărcate.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Vă rugăm să ștergeți' + overChars + ' caracter'; - - if (overChars !== 1) { - message += 'e'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Vă rugăm să introduceți ' + remainingChars + - ' sau mai multe caractere'; - - return message; - }, - loadingMore: function () { - return 'Se încarcă mai multe rezultate…'; - }, - maximumSelected: function (args) { - var message = 'Aveți voie să selectați cel mult ' + args.maximum; - message += ' element'; - - if (args.maximum !== 1) { - message += 'e'; - } - - return message; - }, - noResults: function () { - return 'Nu au fost găsite rezultate'; - }, - searching: function () { - return 'Căutare…'; - }, - removeAllItems: function () { - return 'Eliminați toate elementele'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/ru.js b/static/vendor/select2/src/js/select2/i18n/ru.js deleted file mode 100644 index 8bb24305..00000000 --- a/static/vendor/select2/src/js/select2/i18n/ru.js +++ /dev/null @@ -1,61 +0,0 @@ -define(function () { - // Russian - function ending (count, one, couple, more) { - if (count % 10 < 5 && count % 10 > 0 && - count % 100 < 5 || count % 100 > 20) { - if (count % 10 > 1) { - return couple; - } - } else { - return more; - } - - return one; - } - - return { - errorLoading: function () { - return 'Невозможно загрузить результаты'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Пожалуйста, введите на ' + overChars + ' символ'; - - message += ending(overChars, '', 'a', 'ов'); - - message += ' меньше'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Пожалуйста, введите ещё хотя бы ' + remainingChars + - ' символ'; - - message += ending(remainingChars, '', 'a', 'ов'); - - return message; - }, - loadingMore: function () { - return 'Загрузка данных…'; - }, - maximumSelected: function (args) { - var message = 'Вы можете выбрать не более ' + args.maximum + ' элемент'; - - message += ending(args.maximum, '', 'a', 'ов'); - - return message; - }, - noResults: function () { - return 'Совпадений не найдено'; - }, - searching: function () { - return 'Поиск…'; - }, - removeAllItems: function () { - return 'Удалить все элементы'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/sk.js b/static/vendor/select2/src/js/select2/i18n/sk.js deleted file mode 100644 index f3e821d5..00000000 --- a/static/vendor/select2/src/js/select2/i18n/sk.js +++ /dev/null @@ -1,62 +0,0 @@ -define(function () { - // Slovak - - // use text for the numbers 2 through 4 - var smallNumbers = { - 2: function (masc) { return (masc ? 'dva' : 'dve'); }, - 3: function () { return 'tri'; }, - 4: function () { return 'štyri'; } - }; - - return { - errorLoading: function () { - return 'Výsledky sa nepodarilo načítať.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - if (overChars == 1) { - return 'Prosím, zadajte o jeden znak menej'; - } else if (overChars >= 2 && overChars <= 4) { - return 'Prosím, zadajte o ' + smallNumbers[overChars](true) + - ' znaky menej'; - } else { - return 'Prosím, zadajte o ' + overChars + ' znakov menej'; - } - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - if (remainingChars == 1) { - return 'Prosím, zadajte ešte jeden znak'; - } else if (remainingChars <= 4) { - return 'Prosím, zadajte ešte ďalšie ' + - smallNumbers[remainingChars](true) + ' znaky'; - } else { - return 'Prosím, zadajte ešte ďalších ' + remainingChars + ' znakov'; - } - }, - loadingMore: function () { - return 'Načítanie ďalších výsledkov…'; - }, - maximumSelected: function (args) { - if (args.maximum == 1) { - return 'Môžete zvoliť len jednu položku'; - } else if (args.maximum >= 2 && args.maximum <= 4) { - return 'Môžete zvoliť najviac ' + smallNumbers[args.maximum](false) + - ' položky'; - } else { - return 'Môžete zvoliť najviac ' + args.maximum + ' položiek'; - } - }, - noResults: function () { - return 'Nenašli sa žiadne položky'; - }, - searching: function () { - return 'Vyhľadávanie…'; - }, - removeAllItems: function () { - return 'Odstráňte všetky položky'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/sl.js b/static/vendor/select2/src/js/select2/i18n/sl.js deleted file mode 100644 index df12e402..00000000 --- a/static/vendor/select2/src/js/select2/i18n/sl.js +++ /dev/null @@ -1,57 +0,0 @@ -define(function () { - // Slovene - return { - errorLoading: function () { - return 'Zadetkov iskanja ni bilo mogoče naložiti.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Prosim zbrišite ' + overChars + ' znak'; - - if (overChars == 2) { - message += 'a'; - } else if (overChars != 1) { - message += 'e'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Prosim vpišite še ' + remainingChars + ' znak'; - - if (remainingChars == 2) { - message += 'a'; - } else if (remainingChars != 1) { - message += 'e'; - } - - return message; - }, - loadingMore: function () { - return 'Nalagam več zadetkov…'; - }, - maximumSelected: function (args) { - var message = 'Označite lahko največ ' + args.maximum + ' predmet'; - - if (args.maximum == 2) { - message += 'a'; - } else if (args.maximum != 1) { - message += 'e'; - } - - return message; - }, - noResults: function () { - return 'Ni zadetkov.'; - }, - searching: function () { - return 'Iščem…'; - }, - removeAllItems: function () { - return 'Odstranite vse elemente'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/sq.js b/static/vendor/select2/src/js/select2/i18n/sq.js deleted file mode 100644 index d3e1b8a5..00000000 --- a/static/vendor/select2/src/js/select2/i18n/sq.js +++ /dev/null @@ -1,48 +0,0 @@ -define(function () { - // Albanian - return { - errorLoading: function () { - return 'Rezultatet nuk mund të ngarkoheshin.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Të lutem fshi ' + overChars + ' karakter'; - - if (overChars != 1) { - message += 'e'; - } - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Të lutem shkruaj ' + remainingChars + - ' ose më shumë karaktere'; - - return message; - }, - loadingMore: function () { - return 'Duke ngarkuar më shumë rezultate…'; - }, - maximumSelected: function (args) { - var message = 'Mund të zgjedhësh vetëm ' + args.maximum + ' element'; - - if (args.maximum != 1) { - message += 'e'; - } - - return message; - }, - noResults: function () { - return 'Nuk u gjet asnjë rezultat'; - }, - searching: function () { - return 'Duke kërkuar…'; - }, - removeAllItems: function () { - return 'Hiq të gjitha sendet'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/sr-Cyrl.js b/static/vendor/select2/src/js/select2/i18n/sr-Cyrl.js deleted file mode 100644 index b8f8d41e..00000000 --- a/static/vendor/select2/src/js/select2/i18n/sr-Cyrl.js +++ /dev/null @@ -1,58 +0,0 @@ -define(function () { - // Serbian Cyrilic - function ending (count, one, some, many) { - if (count % 10 == 1 && count % 100 != 11) { - return one; - } - - if (count % 10 >= 2 && count % 10 <= 4 && - (count % 100 < 12 || count % 100 > 14)) { - return some; - } - - return many; - } - - return { - errorLoading: function () { - return 'Преузимање није успело.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Обришите ' + overChars + ' симбол'; - - message += ending(overChars, '', 'а', 'а'); - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Укуцајте бар још ' + remainingChars + ' симбол'; - - message += ending(remainingChars, '', 'а', 'а'); - - return message; - }, - loadingMore: function () { - return 'Преузимање још резултата…'; - }, - maximumSelected: function (args) { - var message = 'Можете изабрати само ' + args.maximum + ' ставк'; - - message += ending(args.maximum, 'у', 'е', 'и'); - - return message; - }, - noResults: function () { - return 'Ништа није пронађено'; - }, - searching: function () { - return 'Претрага…'; - }, - removeAllItems: function () { - return 'Уклоните све ставке'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/sr.js b/static/vendor/select2/src/js/select2/i18n/sr.js deleted file mode 100644 index 96f1b723..00000000 --- a/static/vendor/select2/src/js/select2/i18n/sr.js +++ /dev/null @@ -1,58 +0,0 @@ -define(function () { - // Serbian - function ending (count, one, some, many) { - if (count % 10 == 1 && count % 100 != 11) { - return one; - } - - if (count % 10 >= 2 && count % 10 <= 4 && - (count % 100 < 12 || count % 100 > 14)) { - return some; - } - - return many; - } - - return { - errorLoading: function () { - return 'Preuzimanje nije uspelo.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Obrišite ' + overChars + ' simbol'; - - message += ending(overChars, '', 'a', 'a'); - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Ukucajte bar još ' + remainingChars + ' simbol'; - - message += ending(remainingChars, '', 'a', 'a'); - - return message; - }, - loadingMore: function () { - return 'Preuzimanje još rezultata…'; - }, - maximumSelected: function (args) { - var message = 'Možete izabrati samo ' + args.maximum + ' stavk'; - - message += ending(args.maximum, 'u', 'e', 'i'); - - return message; - }, - noResults: function () { - return 'Ništa nije pronađeno'; - }, - searching: function () { - return 'Pretraga…'; - }, - removeAllItems: function () { - return 'Уклоните све ставке'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/sv.js b/static/vendor/select2/src/js/select2/i18n/sv.js deleted file mode 100644 index e4e204f4..00000000 --- a/static/vendor/select2/src/js/select2/i18n/sv.js +++ /dev/null @@ -1,40 +0,0 @@ -define(function () { - // Swedish - return { - errorLoading: function () { - return 'Resultat kunde inte laddas.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Vänligen sudda ut ' + overChars + ' tecken'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Vänligen skriv in ' + remainingChars + - ' eller fler tecken'; - - return message; - }, - loadingMore: function () { - return 'Laddar fler resultat…'; - }, - maximumSelected: function (args) { - var message = 'Du kan max välja ' + args.maximum + ' element'; - - return message; - }, - noResults: function () { - return 'Inga träffar'; - }, - searching: function () { - return 'Söker…'; - }, - removeAllItems: function () { - return 'Ta bort alla objekt'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/th.js b/static/vendor/select2/src/js/select2/i18n/th.js deleted file mode 100644 index 01fc554f..00000000 --- a/static/vendor/select2/src/js/select2/i18n/th.js +++ /dev/null @@ -1,39 +0,0 @@ -define(function () { - // Thai - return { - errorLoading: function () { - return 'ไม่สามารถค้นข้อมูลได้'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'โปรดลบออก ' + overChars + ' ตัวอักษร'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'โปรดพิมพ์เพิ่มอีก ' + remainingChars + ' ตัวอักษร'; - - return message; - }, - loadingMore: function () { - return 'กำลังค้นข้อมูลเพิ่ม…'; - }, - maximumSelected: function (args) { - var message = 'คุณสามารถเลือกได้ไม่เกิน ' + args.maximum + ' รายการ'; - - return message; - }, - noResults: function () { - return 'ไม่พบข้อมูล'; - }, - searching: function () { - return 'กำลังค้นข้อมูล…'; - }, - removeAllItems: function () { - return 'ลบรายการทั้งหมด'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/tk.js b/static/vendor/select2/src/js/select2/i18n/tk.js deleted file mode 100644 index fb87b1d7..00000000 --- a/static/vendor/select2/src/js/select2/i18n/tk.js +++ /dev/null @@ -1,40 +0,0 @@ -define(function () { - // Turkmen - return { - errorLoading: function (){ - return 'Netije ýüklenmedi.'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = overChars + ' harp bozuň.'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Ýene-de iň az ' + remainingChars + ' harp ýazyň.'; - - return message; - }, - loadingMore: function () { - return 'Köpräk netije görkezilýär…'; - }, - maximumSelected: function (args) { - var message = 'Diňe ' + args.maximum + ' sanysyny saýlaň.'; - - return message; - }, - noResults: function () { - return 'Netije tapylmady.'; - }, - searching: function () { - return 'Gözlenýär…'; - }, - removeAllItems: function () { - // TO DO : add in turkmen, - return 'Remove all items'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/tr.js b/static/vendor/select2/src/js/select2/i18n/tr.js deleted file mode 100644 index 7fa1661d..00000000 --- a/static/vendor/select2/src/js/select2/i18n/tr.js +++ /dev/null @@ -1,39 +0,0 @@ -define(function () { - // Turkish - return { - errorLoading: function (){ - return 'Sonuç yüklenemedi'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = overChars + ' karakter daha girmelisiniz'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'En az ' + remainingChars + ' karakter daha girmelisiniz'; - - return message; - }, - loadingMore: function () { - return 'Daha fazla…'; - }, - maximumSelected: function (args) { - var message = 'Sadece ' + args.maximum + ' seçim yapabilirsiniz'; - - return message; - }, - noResults: function () { - return 'Sonuç bulunamadı'; - }, - searching: function () { - return 'Aranıyor…'; - }, - removeAllItems: function () { - return 'Tüm öğeleri kaldır'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/uk.js b/static/vendor/select2/src/js/select2/i18n/uk.js deleted file mode 100644 index 3471265e..00000000 --- a/static/vendor/select2/src/js/select2/i18n/uk.js +++ /dev/null @@ -1,46 +0,0 @@ -define(function () { - // Ukranian - function ending (count, one, couple, more) { - if (count % 100 > 10 && count % 100 < 15) { - return more; - } - if (count % 10 === 1) { - return one; - } - if (count % 10 > 1 && count % 10 < 5) { - return couple; - } - return more; - } - - return { - errorLoading: function () { - return 'Неможливо завантажити результати'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - return 'Будь ласка, видаліть ' + overChars + ' ' + - ending(args.maximum, 'літеру', 'літери', 'літер'); - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - return 'Будь ласка, введіть ' + remainingChars + ' або більше літер'; - }, - loadingMore: function () { - return 'Завантаження інших результатів…'; - }, - maximumSelected: function (args) { - return 'Ви можете вибрати лише ' + args.maximum + ' ' + - ending(args.maximum, 'пункт', 'пункти', 'пунктів'); - }, - noResults: function () { - return 'Нічого не знайдено'; - }, - searching: function () { - return 'Пошук…'; - }, - removeAllItems: function () { - return 'Видалити всі елементи'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/vi.js b/static/vendor/select2/src/js/select2/i18n/vi.js deleted file mode 100644 index 83312c77..00000000 --- a/static/vendor/select2/src/js/select2/i18n/vi.js +++ /dev/null @@ -1,37 +0,0 @@ -define(function () { - // Vietnamese - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = 'Vui lòng xóa bớt ' + overChars + ' ký tự'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = 'Vui lòng nhập thêm từ ' + remainingChars + - ' ký tự trở lên'; - - return message; - }, - loadingMore: function () { - return 'Đang lấy thêm kết quả…'; - }, - maximumSelected: function (args) { - var message = 'Chỉ có thể chọn được ' + args.maximum + ' lựa chọn'; - - return message; - }, - noResults: function () { - return 'Không tìm thấy kết quả'; - }, - searching: function () { - return 'Đang tìm…'; - }, - removeAllItems: function () { - return 'Xóa tất cả các mục'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/zh-CN.js b/static/vendor/select2/src/js/select2/i18n/zh-CN.js deleted file mode 100644 index 18ec5605..00000000 --- a/static/vendor/select2/src/js/select2/i18n/zh-CN.js +++ /dev/null @@ -1,39 +0,0 @@ -define(function () { - // Chinese (Simplified) - return { - errorLoading: function () { - return '无法载入结果。'; - }, - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = '请删除' + overChars + '个字符'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = '请再输入至少' + remainingChars + '个字符'; - - return message; - }, - loadingMore: function () { - return '载入更多结果…'; - }, - maximumSelected: function (args) { - var message = '最多只能选择' + args.maximum + '个项目'; - - return message; - }, - noResults: function () { - return '未找到结果'; - }, - searching: function () { - return '搜索中…'; - }, - removeAllItems: function () { - return '删除所有项目'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/i18n/zh-TW.js b/static/vendor/select2/src/js/select2/i18n/zh-TW.js deleted file mode 100644 index f2bf0c4e..00000000 --- a/static/vendor/select2/src/js/select2/i18n/zh-TW.js +++ /dev/null @@ -1,36 +0,0 @@ -define(function () { - // Chinese (Traditional) - return { - inputTooLong: function (args) { - var overChars = args.input.length - args.maximum; - - var message = '請刪掉' + overChars + '個字元'; - - return message; - }, - inputTooShort: function (args) { - var remainingChars = args.minimum - args.input.length; - - var message = '請再輸入' + remainingChars + '個字元'; - - return message; - }, - loadingMore: function () { - return '載入中…'; - }, - maximumSelected: function (args) { - var message = '你只能選擇最多' + args.maximum + '項'; - - return message; - }, - noResults: function () { - return '沒有找到相符的項目'; - }, - searching: function () { - return '搜尋中…'; - }, - removeAllItems: function () { - return '刪除所有項目'; - } - }; -}); diff --git a/static/vendor/select2/src/js/select2/keys.js b/static/vendor/select2/src/js/select2/keys.js deleted file mode 100644 index f449ba43..00000000 --- a/static/vendor/select2/src/js/select2/keys.js +++ /dev/null @@ -1,25 +0,0 @@ -define([ - -], function () { - var KEYS = { - BACKSPACE: 8, - TAB: 9, - ENTER: 13, - SHIFT: 16, - CTRL: 17, - ALT: 18, - ESC: 27, - SPACE: 32, - PAGE_UP: 33, - PAGE_DOWN: 34, - END: 35, - HOME: 36, - LEFT: 37, - UP: 38, - RIGHT: 39, - DOWN: 40, - DELETE: 46 - }; - - return KEYS; -}); diff --git a/static/vendor/select2/src/js/select2/options.js b/static/vendor/select2/src/js/select2/options.js deleted file mode 100644 index 8e0dc07e..00000000 --- a/static/vendor/select2/src/js/select2/options.js +++ /dev/null @@ -1,146 +0,0 @@ -define([ - 'require', - 'jquery', - './defaults', - './utils' -], function (require, $, Defaults, Utils) { - function Options (options, $element) { - this.options = options; - - if ($element != null) { - this.fromElement($element); - } - - this.options = Defaults.apply(this.options); - - if ($element && $element.is('input')) { - var InputCompat = require(this.get('amdBase') + 'compat/inputData'); - - this.options.dataAdapter = Utils.Decorate( - this.options.dataAdapter, - InputCompat - ); - } - } - - Options.prototype.fromElement = function ($e) { - var excludedData = ['select2']; - - if (this.options.multiple == null) { - this.options.multiple = $e.prop('multiple'); - } - - if (this.options.disabled == null) { - this.options.disabled = $e.prop('disabled'); - } - - if (this.options.language == null) { - if ($e.prop('lang')) { - this.options.language = $e.prop('lang').toLowerCase(); - } else if ($e.closest('[lang]').prop('lang')) { - this.options.language = $e.closest('[lang]').prop('lang'); - } - } - - if (this.options.dir == null) { - if ($e.prop('dir')) { - this.options.dir = $e.prop('dir'); - } else if ($e.closest('[dir]').prop('dir')) { - this.options.dir = $e.closest('[dir]').prop('dir'); - } else { - this.options.dir = 'ltr'; - } - } - - $e.prop('disabled', this.options.disabled); - $e.prop('multiple', this.options.multiple); - - if (Utils.GetData($e[0], 'select2Tags')) { - if (this.options.debug && window.console && console.warn) { - console.warn( - 'Select2: The `data-select2-tags` attribute has been changed to ' + - 'use the `data-data` and `data-tags="true"` attributes and will be ' + - 'removed in future versions of Select2.' - ); - } - - Utils.StoreData($e[0], 'data', Utils.GetData($e[0], 'select2Tags')); - Utils.StoreData($e[0], 'tags', true); - } - - if (Utils.GetData($e[0], 'ajaxUrl')) { - if (this.options.debug && window.console && console.warn) { - console.warn( - 'Select2: The `data-ajax-url` attribute has been changed to ' + - '`data-ajax--url` and support for the old attribute will be removed' + - ' in future versions of Select2.' - ); - } - - $e.attr('ajax--url', Utils.GetData($e[0], 'ajaxUrl')); - Utils.StoreData($e[0], 'ajax-Url', Utils.GetData($e[0], 'ajaxUrl')); - } - - var dataset = {}; - - function upperCaseLetter(_, letter) { - return letter.toUpperCase(); - } - - // Pre-load all of the attributes which are prefixed with `data-` - for (var attr = 0; attr < $e[0].attributes.length; attr++) { - var attributeName = $e[0].attributes[attr].name; - var prefix = 'data-'; - - if (attributeName.substr(0, prefix.length) == prefix) { - // Get the contents of the attribute after `data-` - var dataName = attributeName.substring(prefix.length); - - // Get the data contents from the consistent source - // This is more than likely the jQuery data helper - var dataValue = Utils.GetData($e[0], dataName); - - // camelCase the attribute name to match the spec - var camelDataName = dataName.replace(/-([a-z])/g, upperCaseLetter); - - // Store the data attribute contents into the dataset since - dataset[camelDataName] = dataValue; - } - } - - // Prefer the element's `dataset` attribute if it exists - // jQuery 1.x does not correctly handle data attributes with multiple dashes - if ($.fn.jquery && $.fn.jquery.substr(0, 2) == '1.' && $e[0].dataset) { - dataset = $.extend(true, {}, $e[0].dataset, dataset); - } - - // Prefer our internal data cache if it exists - var data = $.extend(true, {}, Utils.GetData($e[0]), dataset); - - data = Utils._convertData(data); - - for (var key in data) { - if ($.inArray(key, excludedData) > -1) { - continue; - } - - if ($.isPlainObject(this.options[key])) { - $.extend(this.options[key], data[key]); - } else { - this.options[key] = data[key]; - } - } - - return this; - }; - - Options.prototype.get = function (key) { - return this.options[key]; - }; - - Options.prototype.set = function (key, val) { - this.options[key] = val; - }; - - return Options; -}); diff --git a/static/vendor/select2/src/js/select2/results.js b/static/vendor/select2/src/js/select2/results.js deleted file mode 100644 index b56f8d3b..00000000 --- a/static/vendor/select2/src/js/select2/results.js +++ /dev/null @@ -1,530 +0,0 @@ -define([ - 'jquery', - './utils' -], function ($, Utils) { - function Results ($element, options, dataAdapter) { - this.$element = $element; - this.data = dataAdapter; - this.options = options; - - Results.__super__.constructor.call(this); - } - - Utils.Extend(Results, Utils.Observable); - - Results.prototype.render = function () { - var $results = $( - '
    ' - ); - - if (this.options.get('multiple')) { - $results.attr('aria-multiselectable', 'true'); - } - - this.$results = $results; - - return $results; - }; - - Results.prototype.clear = function () { - this.$results.empty(); - }; - - Results.prototype.displayMessage = function (params) { - var escapeMarkup = this.options.get('escapeMarkup'); - - this.clear(); - this.hideLoading(); - - var $message = $( - '
  • ' - ); - - var message = this.options.get('translations').get(params.message); - - $message.append( - escapeMarkup( - message(params.args) - ) - ); - - $message[0].className += ' select2-results__message'; - - this.$results.append($message); - }; - - Results.prototype.hideMessages = function () { - this.$results.find('.select2-results__message').remove(); - }; - - Results.prototype.append = function (data) { - this.hideLoading(); - - var $options = []; - - if (data.results == null || data.results.length === 0) { - if (this.$results.children().length === 0) { - this.trigger('results:message', { - message: 'noResults' - }); - } - - return; - } - - data.results = this.sort(data.results); - - for (var d = 0; d < data.results.length; d++) { - var item = data.results[d]; - - var $option = this.option(item); - - $options.push($option); - } - - this.$results.append($options); - }; - - Results.prototype.position = function ($results, $dropdown) { - var $resultsContainer = $dropdown.find('.select2-results'); - $resultsContainer.append($results); - }; - - Results.prototype.sort = function (data) { - var sorter = this.options.get('sorter'); - - return sorter(data); - }; - - Results.prototype.highlightFirstItem = function () { - var $options = this.$results - .find('.select2-results__option[aria-selected]'); - - var $selected = $options.filter('[aria-selected=true]'); - - // Check if there are any selected options - if ($selected.length > 0) { - // If there are selected options, highlight the first - $selected.first().trigger('mouseenter'); - } else { - // If there are no selected options, highlight the first option - // in the dropdown - $options.first().trigger('mouseenter'); - } - - this.ensureHighlightVisible(); - }; - - Results.prototype.setClasses = function () { - var self = this; - - this.data.current(function (selected) { - var selectedIds = $.map(selected, function (s) { - return s.id.toString(); - }); - - var $options = self.$results - .find('.select2-results__option[aria-selected]'); - - $options.each(function () { - var $option = $(this); - - var item = Utils.GetData(this, 'data'); - - // id needs to be converted to a string when comparing - var id = '' + item.id; - - if ((item.element != null && item.element.selected) || - (item.element == null && $.inArray(id, selectedIds) > -1)) { - $option.attr('aria-selected', 'true'); - } else { - $option.attr('aria-selected', 'false'); - } - }); - - }); - }; - - Results.prototype.showLoading = function (params) { - this.hideLoading(); - - var loadingMore = this.options.get('translations').get('searching'); - - var loading = { - disabled: true, - loading: true, - text: loadingMore(params) - }; - var $loading = this.option(loading); - $loading.className += ' loading-results'; - - this.$results.prepend($loading); - }; - - Results.prototype.hideLoading = function () { - this.$results.find('.loading-results').remove(); - }; - - Results.prototype.option = function (data) { - var option = document.createElement('li'); - option.className = 'select2-results__option'; - - var attrs = { - 'role': 'treeitem', - 'aria-selected': 'false' - }; - - if (data.disabled) { - delete attrs['aria-selected']; - attrs['aria-disabled'] = 'true'; - } - - if (data.id == null) { - delete attrs['aria-selected']; - } - - if (data._resultId != null) { - option.id = data._resultId; - } - - if (data.title) { - option.title = data.title; - } - - if (data.children) { - attrs.role = 'group'; - attrs['aria-label'] = data.text; - delete attrs['aria-selected']; - } - - for (var attr in attrs) { - var val = attrs[attr]; - - option.setAttribute(attr, val); - } - - if (data.children) { - var $option = $(option); - - var label = document.createElement('strong'); - label.className = 'select2-results__group'; - - var $label = $(label); - this.template(data, label); - - var $children = []; - - for (var c = 0; c < data.children.length; c++) { - var child = data.children[c]; - - var $child = this.option(child); - - $children.push($child); - } - - var $childrenContainer = $('
      ', { - 'class': 'select2-results__options select2-results__options--nested' - }); - - $childrenContainer.append($children); - - $option.append(label); - $option.append($childrenContainer); - } else { - this.template(data, option); - } - - Utils.StoreData(option, 'data', data); - - return option; - }; - - Results.prototype.bind = function (container, $container) { - var self = this; - - var id = container.id + '-results'; - - this.$results.attr('id', id); - - container.on('results:all', function (params) { - self.clear(); - self.append(params.data); - - if (container.isOpen()) { - self.setClasses(); - self.highlightFirstItem(); - } - }); - - container.on('results:append', function (params) { - self.append(params.data); - - if (container.isOpen()) { - self.setClasses(); - } - }); - - container.on('query', function (params) { - self.hideMessages(); - self.showLoading(params); - }); - - container.on('select', function () { - if (!container.isOpen()) { - return; - } - - self.setClasses(); - - if (self.options.get('scrollAfterSelect')) { - self.highlightFirstItem(); - } - }); - - container.on('unselect', function () { - if (!container.isOpen()) { - return; - } - - self.setClasses(); - - if (self.options.get('scrollAfterSelect')) { - self.highlightFirstItem(); - } - }); - - container.on('open', function () { - // When the dropdown is open, aria-expended="true" - self.$results.attr('aria-expanded', 'true'); - self.$results.attr('aria-hidden', 'false'); - - self.setClasses(); - self.ensureHighlightVisible(); - }); - - container.on('close', function () { - // When the dropdown is closed, aria-expended="false" - self.$results.attr('aria-expanded', 'false'); - self.$results.attr('aria-hidden', 'true'); - self.$results.removeAttr('aria-activedescendant'); - }); - - container.on('results:toggle', function () { - var $highlighted = self.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - $highlighted.trigger('mouseup'); - }); - - container.on('results:select', function () { - var $highlighted = self.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - var data = Utils.GetData($highlighted[0], 'data'); - - if ($highlighted.attr('aria-selected') == 'true') { - self.trigger('close', {}); - } else { - self.trigger('select', { - data: data - }); - } - }); - - container.on('results:previous', function () { - var $highlighted = self.getHighlightedResults(); - - var $options = self.$results.find('[aria-selected]'); - - var currentIndex = $options.index($highlighted); - - // If we are already at the top, don't move further - // If no options, currentIndex will be -1 - if (currentIndex <= 0) { - return; - } - - var nextIndex = currentIndex - 1; - - // If none are highlighted, highlight the first - if ($highlighted.length === 0) { - nextIndex = 0; - } - - var $next = $options.eq(nextIndex); - - $next.trigger('mouseenter'); - - var currentOffset = self.$results.offset().top; - var nextTop = $next.offset().top; - var nextOffset = self.$results.scrollTop() + (nextTop - currentOffset); - - if (nextIndex === 0) { - self.$results.scrollTop(0); - } else if (nextTop - currentOffset < 0) { - self.$results.scrollTop(nextOffset); - } - }); - - container.on('results:next', function () { - var $highlighted = self.getHighlightedResults(); - - var $options = self.$results.find('[aria-selected]'); - - var currentIndex = $options.index($highlighted); - - var nextIndex = currentIndex + 1; - - // If we are at the last option, stay there - if (nextIndex >= $options.length) { - return; - } - - var $next = $options.eq(nextIndex); - - $next.trigger('mouseenter'); - - var currentOffset = self.$results.offset().top + - self.$results.outerHeight(false); - var nextBottom = $next.offset().top + $next.outerHeight(false); - var nextOffset = self.$results.scrollTop() + nextBottom - currentOffset; - - if (nextIndex === 0) { - self.$results.scrollTop(0); - } else if (nextBottom > currentOffset) { - self.$results.scrollTop(nextOffset); - } - }); - - container.on('results:focus', function (params) { - params.element.addClass('select2-results__option--highlighted'); - }); - - container.on('results:message', function (params) { - self.displayMessage(params); - }); - - if ($.fn.mousewheel) { - this.$results.on('mousewheel', function (e) { - var top = self.$results.scrollTop(); - - var bottom = self.$results.get(0).scrollHeight - top + e.deltaY; - - var isAtTop = e.deltaY > 0 && top - e.deltaY <= 0; - var isAtBottom = e.deltaY < 0 && bottom <= self.$results.height(); - - if (isAtTop) { - self.$results.scrollTop(0); - - e.preventDefault(); - e.stopPropagation(); - } else if (isAtBottom) { - self.$results.scrollTop( - self.$results.get(0).scrollHeight - self.$results.height() - ); - - e.preventDefault(); - e.stopPropagation(); - } - }); - } - - this.$results.on('mouseup', '.select2-results__option[aria-selected]', - function (evt) { - var $this = $(this); - - var data = Utils.GetData(this, 'data'); - - if ($this.attr('aria-selected') === 'true') { - if (self.options.get('multiple')) { - self.trigger('unselect', { - originalEvent: evt, - data: data - }); - } else { - self.trigger('close', {}); - } - - return; - } - - self.trigger('select', { - originalEvent: evt, - data: data - }); - }); - - this.$results.on('mouseenter', '.select2-results__option[aria-selected]', - function (evt) { - var data = Utils.GetData(this, 'data'); - - self.getHighlightedResults() - .removeClass('select2-results__option--highlighted'); - - self.trigger('results:focus', { - data: data, - element: $(this) - }); - }); - }; - - Results.prototype.getHighlightedResults = function () { - var $highlighted = this.$results - .find('.select2-results__option--highlighted'); - - return $highlighted; - }; - - Results.prototype.destroy = function () { - this.$results.remove(); - }; - - Results.prototype.ensureHighlightVisible = function () { - var $highlighted = this.getHighlightedResults(); - - if ($highlighted.length === 0) { - return; - } - - var $options = this.$results.find('[aria-selected]'); - - var currentIndex = $options.index($highlighted); - - var currentOffset = this.$results.offset().top; - var nextTop = $highlighted.offset().top; - var nextOffset = this.$results.scrollTop() + (nextTop - currentOffset); - - var offsetDelta = nextTop - currentOffset; - nextOffset -= $highlighted.outerHeight(false) * 2; - - if (currentIndex <= 2) { - this.$results.scrollTop(0); - } else if (offsetDelta > this.$results.outerHeight() || offsetDelta < 0) { - this.$results.scrollTop(nextOffset); - } - }; - - Results.prototype.template = function (result, container) { - var template = this.options.get('templateResult'); - var escapeMarkup = this.options.get('escapeMarkup'); - - var content = template(result, container); - - if (content == null) { - container.style.display = 'none'; - } else if (typeof content === 'string') { - container.innerHTML = escapeMarkup(content); - } else { - $(container).append(content); - } - }; - - return Results; -}); diff --git a/static/vendor/select2/src/js/select2/selection/allowClear.js b/static/vendor/select2/src/js/select2/selection/allowClear.js deleted file mode 100644 index 0de5b9bb..00000000 --- a/static/vendor/select2/src/js/select2/selection/allowClear.js +++ /dev/null @@ -1,113 +0,0 @@ -define([ - 'jquery', - '../keys', - '../utils' -], function ($, KEYS, Utils) { - function AllowClear () { } - - AllowClear.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - if (this.placeholder == null) { - if (this.options.get('debug') && window.console && console.error) { - console.error( - 'Select2: The `allowClear` option should be used in combination ' + - 'with the `placeholder` option.' - ); - } - } - - this.$selection.on('mousedown', '.select2-selection__clear', - function (evt) { - self._handleClear(evt); - }); - - container.on('keypress', function (evt) { - self._handleKeyboardClear(evt, container); - }); - }; - - AllowClear.prototype._handleClear = function (_, evt) { - // Ignore the event if it is disabled - if (this.options.get('disabled')) { - return; - } - - var $clear = this.$selection.find('.select2-selection__clear'); - - // Ignore the event if nothing has been selected - if ($clear.length === 0) { - return; - } - - evt.stopPropagation(); - - var data = Utils.GetData($clear[0], 'data'); - - var previousVal = this.$element.val(); - this.$element.val(this.placeholder.id); - - var unselectData = { - data: data - }; - this.trigger('clear', unselectData); - if (unselectData.prevented) { - this.$element.val(previousVal); - return; - } - - for (var d = 0; d < data.length; d++) { - unselectData = { - data: data[d] - }; - - // Trigger the `unselect` event, so people can prevent it from being - // cleared. - this.trigger('unselect', unselectData); - - // If the event was prevented, don't clear it out. - if (unselectData.prevented) { - this.$element.val(previousVal); - return; - } - } - - this.$element.trigger('change'); - - this.trigger('toggle', {}); - }; - - AllowClear.prototype._handleKeyboardClear = function (_, evt, container) { - if (container.isOpen()) { - return; - } - - if (evt.which == KEYS.DELETE || evt.which == KEYS.BACKSPACE) { - this._handleClear(evt); - } - }; - - AllowClear.prototype.update = function (decorated, data) { - decorated.call(this, data); - - if (this.$selection.find('.select2-selection__placeholder').length > 0 || - data.length === 0) { - return; - } - - var removeAll = this.options.get('translations').get('removeAllItems'); - - var $remove = $( - '' + - '×' + - '' - ); - Utils.StoreData($remove[0], 'data', data); - - this.$selection.find('.select2-selection__rendered').prepend($remove); - }; - - return AllowClear; -}); diff --git a/static/vendor/select2/src/js/select2/selection/base.js b/static/vendor/select2/src/js/select2/selection/base.js deleted file mode 100644 index 6d373bd7..00000000 --- a/static/vendor/select2/src/js/select2/selection/base.js +++ /dev/null @@ -1,160 +0,0 @@ -define([ - 'jquery', - '../utils', - '../keys' -], function ($, Utils, KEYS) { - function BaseSelection ($element, options) { - this.$element = $element; - this.options = options; - - BaseSelection.__super__.constructor.call(this); - } - - Utils.Extend(BaseSelection, Utils.Observable); - - BaseSelection.prototype.render = function () { - var $selection = $( - '' - ); - - this._tabindex = 0; - - if (Utils.GetData(this.$element[0], 'old-tabindex') != null) { - this._tabindex = Utils.GetData(this.$element[0], 'old-tabindex'); - } else if (this.$element.attr('tabindex') != null) { - this._tabindex = this.$element.attr('tabindex'); - } - - $selection.attr('title', this.$element.attr('title')); - $selection.attr('tabindex', this._tabindex); - - this.$selection = $selection; - - return $selection; - }; - - BaseSelection.prototype.bind = function (container, $container) { - var self = this; - - var id = container.id + '-container'; - var resultsId = container.id + '-results'; - - this.container = container; - - this.$selection.on('focus', function (evt) { - self.trigger('focus', evt); - }); - - this.$selection.on('blur', function (evt) { - self._handleBlur(evt); - }); - - this.$selection.on('keydown', function (evt) { - self.trigger('keypress', evt); - - if (evt.which === KEYS.SPACE) { - evt.preventDefault(); - } - }); - - container.on('results:focus', function (params) { - self.$selection.attr('aria-activedescendant', params.data._resultId); - }); - - container.on('selection:update', function (params) { - self.update(params.data); - }); - - container.on('open', function () { - // When the dropdown is open, aria-expanded="true" - self.$selection.attr('aria-expanded', 'true'); - self.$selection.attr('aria-owns', resultsId); - - self._attachCloseHandler(container); - }); - - container.on('close', function () { - // When the dropdown is closed, aria-expanded="false" - self.$selection.attr('aria-expanded', 'false'); - self.$selection.removeAttr('aria-activedescendant'); - self.$selection.removeAttr('aria-owns'); - - window.setTimeout(function () { - self.$selection.focus(); - }, 0); - - self._detachCloseHandler(container); - }); - - container.on('enable', function () { - self.$selection.attr('tabindex', self._tabindex); - }); - - container.on('disable', function () { - self.$selection.attr('tabindex', '-1'); - }); - }; - - BaseSelection.prototype._handleBlur = function (evt) { - var self = this; - - // This needs to be delayed as the active element is the body when the tab - // key is pressed, possibly along with others. - window.setTimeout(function () { - // Don't trigger `blur` if the focus is still in the selection - if ( - (document.activeElement == self.$selection[0]) || - ($.contains(self.$selection[0], document.activeElement)) - ) { - return; - } - - self.trigger('blur', evt); - }, 1); - }; - - BaseSelection.prototype._attachCloseHandler = function (container) { - var self = this; - - $(document.body).on('mousedown.select2.' + container.id, function (e) { - var $target = $(e.target); - - var $select = $target.closest('.select2'); - - var $all = $('.select2.select2-container--open'); - - $all.each(function () { - var $this = $(this); - - if (this == $select[0]) { - return; - } - - var $element = Utils.GetData(this, 'element'); - - $element.select2('close'); - }); - }); - }; - - BaseSelection.prototype._detachCloseHandler = function (container) { - $(document.body).off('mousedown.select2.' + container.id); - }; - - BaseSelection.prototype.position = function ($selection, $container) { - var $selectionContainer = $container.find('.selection'); - $selectionContainer.append($selection); - }; - - BaseSelection.prototype.destroy = function () { - this._detachCloseHandler(this.container); - }; - - BaseSelection.prototype.update = function (data) { - throw new Error('The `update` method must be defined in child classes.'); - }; - - return BaseSelection; -}); diff --git a/static/vendor/select2/src/js/select2/selection/clickMask.js b/static/vendor/select2/src/js/select2/selection/clickMask.js deleted file mode 100644 index 2b4ac307..00000000 --- a/static/vendor/select2/src/js/select2/selection/clickMask.js +++ /dev/null @@ -1,29 +0,0 @@ -define([ - 'jquery' -], function ($) { - function ClickMask () { } - - ClickMask.prototype.bind = function (decorate, $container, container) { - var self = this; - - decorate.call(this, $container, container); - - this.$mask = $( - '
      ' - ); - - this.$mask.on('mousedown touchstart click', function () { - self.trigger('close', {}); - }); - }; - - ClickMask.prototype._attachCloseHandler = function (decorate, container) { - $(document.body).append(this.$mask); - }; - - ClickMask.prototype._detachCloseHandler = function (deocrate, container) { - this.$mask.detach(); - }; - - return ClickMask; -}); diff --git a/static/vendor/select2/src/js/select2/selection/eventRelay.js b/static/vendor/select2/src/js/select2/selection/eventRelay.js deleted file mode 100644 index 0f59822b..00000000 --- a/static/vendor/select2/src/js/select2/selection/eventRelay.js +++ /dev/null @@ -1,48 +0,0 @@ -define([ - 'jquery' -], function ($) { - function EventRelay () { } - - EventRelay.prototype.bind = function (decorated, container, $container) { - var self = this; - var relayEvents = [ - 'open', 'opening', - 'close', 'closing', - 'select', 'selecting', - 'unselect', 'unselecting', - 'clear', 'clearing' - ]; - - var preventableEvents = [ - 'opening', 'closing', 'selecting', 'unselecting', 'clearing' - ]; - - decorated.call(this, container, $container); - - container.on('*', function (name, params) { - // Ignore events that should not be relayed - if ($.inArray(name, relayEvents) === -1) { - return; - } - - // The parameters should always be an object - params = params || {}; - - // Generate the jQuery event for the Select2 event - var evt = $.Event('select2:' + name, { - params: params - }); - - self.$element.trigger(evt); - - // Only handle preventable events if it was one - if ($.inArray(name, preventableEvents) === -1) { - return; - } - - params.prevented = evt.isDefaultPrevented(); - }); - }; - - return EventRelay; -}); diff --git a/static/vendor/select2/src/js/select2/selection/multiple.js b/static/vendor/select2/src/js/select2/selection/multiple.js deleted file mode 100644 index 6f106788..00000000 --- a/static/vendor/select2/src/js/select2/selection/multiple.js +++ /dev/null @@ -1,111 +0,0 @@ -define([ - 'jquery', - './base', - '../utils' -], function ($, BaseSelection, Utils) { - function MultipleSelection ($element, options) { - MultipleSelection.__super__.constructor.apply(this, arguments); - } - - Utils.Extend(MultipleSelection, BaseSelection); - - MultipleSelection.prototype.render = function () { - var $selection = MultipleSelection.__super__.render.call(this); - - $selection.addClass('select2-selection--multiple'); - - $selection.html( - '
        ' - ); - - return $selection; - }; - - MultipleSelection.prototype.bind = function (container, $container) { - var self = this; - - MultipleSelection.__super__.bind.apply(this, arguments); - - this.$selection.on('click', function (evt) { - self.trigger('toggle', { - originalEvent: evt - }); - }); - - this.$selection.on( - 'click', - '.select2-selection__choice__remove', - function (evt) { - // Ignore the event if it is disabled - if (self.options.get('disabled')) { - return; - } - - var $remove = $(this); - var $selection = $remove.parent(); - - var data = Utils.GetData($selection[0], 'data'); - - self.trigger('unselect', { - originalEvent: evt, - data: data - }); - } - ); - }; - - MultipleSelection.prototype.clear = function () { - var $rendered = this.$selection.find('.select2-selection__rendered'); - $rendered.empty(); - $rendered.removeAttr('title'); - }; - - MultipleSelection.prototype.display = function (data, container) { - var template = this.options.get('templateSelection'); - var escapeMarkup = this.options.get('escapeMarkup'); - - return escapeMarkup(template(data, container)); - }; - - MultipleSelection.prototype.selectionContainer = function () { - var $container = $( - '
      • ' + - '' + - '×' + - '' + - '
      • ' - ); - - return $container; - }; - - MultipleSelection.prototype.update = function (data) { - this.clear(); - - if (data.length === 0) { - return; - } - - var $selections = []; - - for (var d = 0; d < data.length; d++) { - var selection = data[d]; - - var $selection = this.selectionContainer(); - var formatted = this.display(selection, $selection); - - $selection.append(formatted); - $selection.attr('title', selection.title || selection.text); - - Utils.StoreData($selection[0], 'data', selection); - - $selections.push($selection); - } - - var $rendered = this.$selection.find('.select2-selection__rendered'); - - Utils.appendMany($rendered, $selections); - }; - - return MultipleSelection; -}); diff --git a/static/vendor/select2/src/js/select2/selection/placeholder.js b/static/vendor/select2/src/js/select2/selection/placeholder.js deleted file mode 100644 index 87fbedac..00000000 --- a/static/vendor/select2/src/js/select2/selection/placeholder.js +++ /dev/null @@ -1,49 +0,0 @@ -define([ - '../utils' -], function (Utils) { - function Placeholder (decorated, $element, options) { - this.placeholder = this.normalizePlaceholder(options.get('placeholder')); - - decorated.call(this, $element, options); - } - - Placeholder.prototype.normalizePlaceholder = function (_, placeholder) { - if (typeof placeholder === 'string') { - placeholder = { - id: '', - text: placeholder - }; - } - - return placeholder; - }; - - Placeholder.prototype.createPlaceholder = function (decorated, placeholder) { - var $placeholder = this.selectionContainer(); - - $placeholder.html(this.display(placeholder)); - $placeholder.addClass('select2-selection__placeholder') - .removeClass('select2-selection__choice'); - - return $placeholder; - }; - - Placeholder.prototype.update = function (decorated, data) { - var singlePlaceholder = ( - data.length == 1 && data[0].id != this.placeholder.id - ); - var multipleSelections = data.length > 1; - - if (multipleSelections || singlePlaceholder) { - return decorated.call(this, data); - } - - this.clear(); - - var $placeholder = this.createPlaceholder(this.placeholder); - - this.$selection.find('.select2-selection__rendered').append($placeholder); - }; - - return Placeholder; -}); diff --git a/static/vendor/select2/src/js/select2/selection/search.js b/static/vendor/select2/src/js/select2/selection/search.js deleted file mode 100644 index 3ff96f94..00000000 --- a/static/vendor/select2/src/js/select2/selection/search.js +++ /dev/null @@ -1,228 +0,0 @@ -define([ - 'jquery', - '../utils', - '../keys' -], function ($, Utils, KEYS) { - function Search (decorated, $element, options) { - decorated.call(this, $element, options); - } - - Search.prototype.render = function (decorated) { - var $search = $( - '' - ); - - this.$searchContainer = $search; - this.$search = $search.find('input'); - - var $rendered = decorated.call(this); - - this._transferTabIndex(); - - return $rendered; - }; - - Search.prototype.bind = function (decorated, container, $container) { - var self = this; - - decorated.call(this, container, $container); - - container.on('open', function () { - self.$search.trigger('focus'); - }); - - container.on('close', function () { - self.$search.val(''); - self.$search.removeAttr('aria-activedescendant'); - self.$search.trigger('focus'); - }); - - container.on('enable', function () { - self.$search.prop('disabled', false); - - self._transferTabIndex(); - }); - - container.on('disable', function () { - self.$search.prop('disabled', true); - }); - - container.on('focus', function (evt) { - self.$search.trigger('focus'); - }); - - container.on('results:focus', function (params) { - self.$search.attr('aria-activedescendant', params.id); - }); - - this.$selection.on('focusin', '.select2-search--inline', function (evt) { - self.trigger('focus', evt); - }); - - this.$selection.on('focusout', '.select2-search--inline', function (evt) { - self._handleBlur(evt); - }); - - this.$selection.on('keydown', '.select2-search--inline', function (evt) { - evt.stopPropagation(); - - self.trigger('keypress', evt); - - self._keyUpPrevented = evt.isDefaultPrevented(); - - var key = evt.which; - - if (key === KEYS.BACKSPACE && self.$search.val() === '') { - var $previousChoice = self.$searchContainer - .prev('.select2-selection__choice'); - - if ($previousChoice.length > 0) { - var item = Utils.GetData($previousChoice[0], 'data'); - - self.searchRemoveChoice(item); - - evt.preventDefault(); - } - } - }); - - // Try to detect the IE version should the `documentMode` property that - // is stored on the document. This is only implemented in IE and is - // slightly cleaner than doing a user agent check. - // This property is not available in Edge, but Edge also doesn't have - // this bug. - var msie = document.documentMode; - var disableInputEvents = msie && msie <= 11; - - // Workaround for browsers which do not support the `input` event - // This will prevent double-triggering of events for browsers which support - // both the `keyup` and `input` events. - this.$selection.on( - 'input.searchcheck', - '.select2-search--inline', - function (evt) { - // IE will trigger the `input` event when a placeholder is used on a - // search box. To get around this issue, we are forced to ignore all - // `input` events in IE and keep using `keyup`. - if (disableInputEvents) { - self.$selection.off('input.search input.searchcheck'); - return; - } - - // Unbind the duplicated `keyup` event - self.$selection.off('keyup.search'); - } - ); - - this.$selection.on( - 'keyup.search input.search', - '.select2-search--inline', - function (evt) { - // IE will trigger the `input` event when a placeholder is used on a - // search box. To get around this issue, we are forced to ignore all - // `input` events in IE and keep using `keyup`. - if (disableInputEvents && evt.type === 'input') { - self.$selection.off('input.search input.searchcheck'); - return; - } - - var key = evt.which; - - // We can freely ignore events from modifier keys - if (key == KEYS.SHIFT || key == KEYS.CTRL || key == KEYS.ALT) { - return; - } - - // Tabbing will be handled during the `keydown` phase - if (key == KEYS.TAB) { - return; - } - - self.handleSearch(evt); - } - ); - }; - - /** - * This method will transfer the tabindex attribute from the rendered - * selection to the search box. This allows for the search box to be used as - * the primary focus instead of the selection container. - * - * @private - */ - Search.prototype._transferTabIndex = function (decorated) { - this.$search.attr('tabindex', this.$selection.attr('tabindex')); - this.$selection.attr('tabindex', '-1'); - }; - - Search.prototype.createPlaceholder = function (decorated, placeholder) { - this.$search.attr('placeholder', placeholder.text); - }; - - Search.prototype.update = function (decorated, data) { - var searchHadFocus = this.$search[0] == document.activeElement; - - this.$search.attr('placeholder', ''); - - decorated.call(this, data); - - this.$selection.find('.select2-selection__rendered') - .append(this.$searchContainer); - - this.resizeSearch(); - if (searchHadFocus) { - var isTagInput = this.$element.find('[data-select2-tag]').length; - if (isTagInput) { - // fix IE11 bug where tag input lost focus - this.$element.focus(); - } else { - this.$search.focus(); - } - } - }; - - Search.prototype.handleSearch = function () { - this.resizeSearch(); - - if (!this._keyUpPrevented) { - var input = this.$search.val(); - - this.trigger('query', { - term: input - }); - } - - this._keyUpPrevented = false; - }; - - Search.prototype.searchRemoveChoice = function (decorated, item) { - this.trigger('unselect', { - data: item - }); - - this.$search.val(item.text); - this.handleSearch(); - }; - - Search.prototype.resizeSearch = function () { - this.$search.css('width', '25px'); - - var width = ''; - - if (this.$search.attr('placeholder') !== '') { - width = this.$selection.find('.select2-selection__rendered').innerWidth(); - } else { - var minimumWidth = this.$search.val().length + 1; - - width = (minimumWidth * 0.75) + 'em'; - } - - this.$search.css('width', width); - }; - - return Search; -}); diff --git a/static/vendor/select2/src/js/select2/selection/single.js b/static/vendor/select2/src/js/select2/selection/single.js deleted file mode 100644 index e9e99aa4..00000000 --- a/static/vendor/select2/src/js/select2/selection/single.js +++ /dev/null @@ -1,100 +0,0 @@ -define([ - 'jquery', - './base', - '../utils', - '../keys' -], function ($, BaseSelection, Utils, KEYS) { - function SingleSelection () { - SingleSelection.__super__.constructor.apply(this, arguments); - } - - Utils.Extend(SingleSelection, BaseSelection); - - SingleSelection.prototype.render = function () { - var $selection = SingleSelection.__super__.render.call(this); - - $selection.addClass('select2-selection--single'); - - $selection.html( - '' + - '' + - '' + - '' - ); - - return $selection; - }; - - SingleSelection.prototype.bind = function (container, $container) { - var self = this; - - SingleSelection.__super__.bind.apply(this, arguments); - - var id = container.id + '-container'; - - this.$selection.find('.select2-selection__rendered') - .attr('id', id) - .attr('role', 'textbox') - .attr('aria-readonly', 'true'); - this.$selection.attr('aria-labelledby', id); - - this.$selection.on('mousedown', function (evt) { - // Only respond to left clicks - if (evt.which !== 1) { - return; - } - - self.trigger('toggle', { - originalEvent: evt - }); - }); - - this.$selection.on('focus', function (evt) { - // User focuses on the container - }); - - this.$selection.on('blur', function (evt) { - // User exits the container - }); - - container.on('focus', function (evt) { - if (!container.isOpen()) { - self.$selection.focus(); - } - }); - }; - - SingleSelection.prototype.clear = function () { - var $rendered = this.$selection.find('.select2-selection__rendered'); - $rendered.empty(); - $rendered.removeAttr('title'); // clear tooltip on empty - }; - - SingleSelection.prototype.display = function (data, container) { - var template = this.options.get('templateSelection'); - var escapeMarkup = this.options.get('escapeMarkup'); - - return escapeMarkup(template(data, container)); - }; - - SingleSelection.prototype.selectionContainer = function () { - return $(''); - }; - - SingleSelection.prototype.update = function (data) { - if (data.length === 0) { - this.clear(); - return; - } - - var selection = data[0]; - - var $rendered = this.$selection.find('.select2-selection__rendered'); - var formatted = this.display(selection, $rendered); - - $rendered.empty().append(formatted); - $rendered.attr('title', selection.title || selection.text); - }; - - return SingleSelection; -}); diff --git a/static/vendor/select2/src/js/select2/selection/stopPropagation.js b/static/vendor/select2/src/js/select2/selection/stopPropagation.js deleted file mode 100644 index 382f1e72..00000000 --- a/static/vendor/select2/src/js/select2/selection/stopPropagation.js +++ /dev/null @@ -1,38 +0,0 @@ -define([ - -], function () { - function StopPropagation () { } - - StopPropagation.prototype.bind = function (decorated, container, $container) { - decorated.call(this, container, $container); - - var stoppedEvents = [ - 'blur', - 'change', - 'click', - 'dblclick', - 'focus', - 'focusin', - 'focusout', - 'input', - 'keydown', - 'keyup', - 'keypress', - 'mousedown', - 'mouseenter', - 'mouseleave', - 'mousemove', - 'mouseover', - 'mouseup', - 'search', - 'touchend', - 'touchstart' - ]; - - this.$selection.on(stoppedEvents.join(' '), function (evt) { - evt.stopPropagation(); - }); - }; - - return StopPropagation; -}); diff --git a/static/vendor/select2/src/js/select2/translation.js b/static/vendor/select2/src/js/select2/translation.js deleted file mode 100644 index 5394c18b..00000000 --- a/static/vendor/select2/src/js/select2/translation.js +++ /dev/null @@ -1,36 +0,0 @@ -define([ - 'jquery', - 'require' -], function ($, require) { - function Translation (dict) { - this.dict = dict || {}; - } - - Translation.prototype.all = function () { - return this.dict; - }; - - Translation.prototype.get = function (key) { - return this.dict[key]; - }; - - Translation.prototype.extend = function (translation) { - this.dict = $.extend({}, translation.all(), this.dict); - }; - - // Static functions - - Translation._cache = {}; - - Translation.loadPath = function (path) { - if (!(path in Translation._cache)) { - var translations = require(path); - - Translation._cache[path] = translations; - } - - return new Translation(Translation._cache[path]); - }; - - return Translation; -}); diff --git a/static/vendor/select2/src/js/select2/utils.js b/static/vendor/select2/src/js/select2/utils.js deleted file mode 100644 index 53d82f3d..00000000 --- a/static/vendor/select2/src/js/select2/utils.js +++ /dev/null @@ -1,338 +0,0 @@ -define([ - 'jquery' -], function ($) { - var Utils = {}; - - Utils.Extend = function (ChildClass, SuperClass) { - var __hasProp = {}.hasOwnProperty; - - function BaseConstructor () { - this.constructor = ChildClass; - } - - for (var key in SuperClass) { - if (__hasProp.call(SuperClass, key)) { - ChildClass[key] = SuperClass[key]; - } - } - - BaseConstructor.prototype = SuperClass.prototype; - ChildClass.prototype = new BaseConstructor(); - ChildClass.__super__ = SuperClass.prototype; - - return ChildClass; - }; - - function getMethods (theClass) { - var proto = theClass.prototype; - - var methods = []; - - for (var methodName in proto) { - var m = proto[methodName]; - - if (typeof m !== 'function') { - continue; - } - - if (methodName === 'constructor') { - continue; - } - - methods.push(methodName); - } - - return methods; - } - - Utils.Decorate = function (SuperClass, DecoratorClass) { - var decoratedMethods = getMethods(DecoratorClass); - var superMethods = getMethods(SuperClass); - - function DecoratedClass () { - var unshift = Array.prototype.unshift; - - var argCount = DecoratorClass.prototype.constructor.length; - - var calledConstructor = SuperClass.prototype.constructor; - - if (argCount > 0) { - unshift.call(arguments, SuperClass.prototype.constructor); - - calledConstructor = DecoratorClass.prototype.constructor; - } - - calledConstructor.apply(this, arguments); - } - - DecoratorClass.displayName = SuperClass.displayName; - - function ctr () { - this.constructor = DecoratedClass; - } - - DecoratedClass.prototype = new ctr(); - - for (var m = 0; m < superMethods.length; m++) { - var superMethod = superMethods[m]; - - DecoratedClass.prototype[superMethod] = - SuperClass.prototype[superMethod]; - } - - var calledMethod = function (methodName) { - // Stub out the original method if it's not decorating an actual method - var originalMethod = function () {}; - - if (methodName in DecoratedClass.prototype) { - originalMethod = DecoratedClass.prototype[methodName]; - } - - var decoratedMethod = DecoratorClass.prototype[methodName]; - - return function () { - var unshift = Array.prototype.unshift; - - unshift.call(arguments, originalMethod); - - return decoratedMethod.apply(this, arguments); - }; - }; - - for (var d = 0; d < decoratedMethods.length; d++) { - var decoratedMethod = decoratedMethods[d]; - - DecoratedClass.prototype[decoratedMethod] = calledMethod(decoratedMethod); - } - - return DecoratedClass; - }; - - var Observable = function () { - this.listeners = {}; - }; - - Observable.prototype.on = function (event, callback) { - this.listeners = this.listeners || {}; - - if (event in this.listeners) { - this.listeners[event].push(callback); - } else { - this.listeners[event] = [callback]; - } - }; - - Observable.prototype.trigger = function (event) { - var slice = Array.prototype.slice; - var params = slice.call(arguments, 1); - - this.listeners = this.listeners || {}; - - // Params should always come in as an array - if (params == null) { - params = []; - } - - // If there are no arguments to the event, use a temporary object - if (params.length === 0) { - params.push({}); - } - - // Set the `_type` of the first object to the event - params[0]._type = event; - - if (event in this.listeners) { - this.invoke(this.listeners[event], slice.call(arguments, 1)); - } - - if ('*' in this.listeners) { - this.invoke(this.listeners['*'], arguments); - } - }; - - Observable.prototype.invoke = function (listeners, params) { - for (var i = 0, len = listeners.length; i < len; i++) { - listeners[i].apply(this, params); - } - }; - - Utils.Observable = Observable; - - Utils.generateChars = function (length) { - var chars = ''; - - for (var i = 0; i < length; i++) { - var randomChar = Math.floor(Math.random() * 36); - chars += randomChar.toString(36); - } - - return chars; - }; - - Utils.bind = function (func, context) { - return function () { - func.apply(context, arguments); - }; - }; - - Utils._convertData = function (data) { - for (var originalKey in data) { - var keys = originalKey.split('-'); - - var dataLevel = data; - - if (keys.length === 1) { - continue; - } - - for (var k = 0; k < keys.length; k++) { - var key = keys[k]; - - // Lowercase the first letter - // By default, dash-separated becomes camelCase - key = key.substring(0, 1).toLowerCase() + key.substring(1); - - if (!(key in dataLevel)) { - dataLevel[key] = {}; - } - - if (k == keys.length - 1) { - dataLevel[key] = data[originalKey]; - } - - dataLevel = dataLevel[key]; - } - - delete data[originalKey]; - } - - return data; - }; - - Utils.hasScroll = function (index, el) { - // Adapted from the function created by @ShadowScripter - // and adapted by @BillBarry on the Stack Exchange Code Review website. - // The original code can be found at - // http://codereview.stackexchange.com/q/13338 - // and was designed to be used with the Sizzle selector engine. - - var $el = $(el); - var overflowX = el.style.overflowX; - var overflowY = el.style.overflowY; - - //Check both x and y declarations - if (overflowX === overflowY && - (overflowY === 'hidden' || overflowY === 'visible')) { - return false; - } - - if (overflowX === 'scroll' || overflowY === 'scroll') { - return true; - } - - return ($el.innerHeight() < el.scrollHeight || - $el.innerWidth() < el.scrollWidth); - }; - - Utils.escapeMarkup = function (markup) { - var replaceMap = { - '\\': '\', - '&': '&', - '<': '<', - '>': '>', - '"': '"', - '\'': ''', - '/': '/' - }; - - // Do not try to escape the markup if it's not a string - if (typeof markup !== 'string') { - return markup; - } - - return String(markup).replace(/[&<>"'\/\\]/g, function (match) { - return replaceMap[match]; - }); - }; - - // Append an array of jQuery nodes to a given element. - Utils.appendMany = function ($element, $nodes) { - // jQuery 1.7.x does not support $.fn.append() with an array - // Fall back to a jQuery object collection using $.fn.add() - if ($.fn.jquery.substr(0, 3) === '1.7') { - var $jqNodes = $(); - - $.map($nodes, function (node) { - $jqNodes = $jqNodes.add(node); - }); - - $nodes = $jqNodes; - } - - $element.append($nodes); - }; - - // Cache objects in Utils.__cache instead of $.data (see #4346) - Utils.__cache = {}; - - var id = 0; - Utils.GetUniqueElementId = function (element) { - // Get a unique element Id. If element has no id, - // creates a new unique number, stores it in the id - // attribute and returns the new id. - // If an id already exists, it simply returns it. - - var select2Id = element.getAttribute('data-select2-id'); - if (select2Id == null) { - // If element has id, use it. - if (element.id) { - select2Id = element.id; - element.setAttribute('data-select2-id', select2Id); - } else { - element.setAttribute('data-select2-id', ++id); - select2Id = id.toString(); - } - } - return select2Id; - }; - - Utils.StoreData = function (element, name, value) { - // Stores an item in the cache for a specified element. - // name is the cache key. - var id = Utils.GetUniqueElementId(element); - if (!Utils.__cache[id]) { - Utils.__cache[id] = {}; - } - - Utils.__cache[id][name] = value; - }; - - Utils.GetData = function (element, name) { - // Retrieves a value from the cache by its key (name) - // name is optional. If no name specified, return - // all cache items for the specified element. - // and for a specified element. - var id = Utils.GetUniqueElementId(element); - if (name) { - if (Utils.__cache[id]) { - if (Utils.__cache[id][name] != null) { - return Utils.__cache[id][name]; - } - return $(element).data(name); // Fallback to HTML5 data attribs. - } - return $(element).data(name); // Fallback to HTML5 data attribs. - } else { - return Utils.__cache[id]; - } - }; - - Utils.RemoveData = function (element) { - // Removes all cached items for a specified element. - var id = Utils.GetUniqueElementId(element); - if (Utils.__cache[id] != null) { - delete Utils.__cache[id]; - } - }; - - return Utils; -}); diff --git a/static/vendor/select2/src/js/wrapper.end.js b/static/vendor/select2/src/js/wrapper.end.js deleted file mode 100644 index 43678296..00000000 --- a/static/vendor/select2/src/js/wrapper.end.js +++ /dev/null @@ -1,12 +0,0 @@ - // Autoload the jQuery bindings - // We know that all of the modules exist above this, so we're safe - var select2 = S2.require('jquery.select2'); - - // Hold the AMD module references on the jQuery function that was just loaded - // This allows Select2 to use the internal loader outside of this file, such - // as in the language files. - jQuery.fn.select2.amd = S2; - - // Return the Select2 instance for anyone who is importing it. - return select2; -})); diff --git a/static/vendor/select2/src/js/wrapper.start.js b/static/vendor/select2/src/js/wrapper.start.js deleted file mode 100644 index 1dfa51ea..00000000 --- a/static/vendor/select2/src/js/wrapper.start.js +++ /dev/null @@ -1,38 +0,0 @@ -/*! - * Select2 <%= package.version %> - * https://select2.github.io - * - * Released under the MIT license - * https://github.com/select2/select2/blob/master/LICENSE.md - */ -;(function (factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['jquery'], factory); - } else if (typeof module === 'object' && module.exports) { - // Node/CommonJS - module.exports = function (root, jQuery) { - if (jQuery === undefined) { - // require('jQuery') returns a factory that requires window to - // build a jQuery instance, we normalize how we use modules - // that require this pattern but the window provided is a noop - // if it's defined (how jquery works) - if (typeof window !== 'undefined') { - jQuery = require('jquery'); - } - else { - jQuery = require('jquery')(root); - } - } - factory(jQuery); - return jQuery; - }; - } else { - // Browser globals - factory(jQuery); - } -} (function (jQuery) { - // This is needed so we can catch the AMD loader configuration and use it - // The inner file should be wrapped (by `banner.start.js`) in a function that - // returns the AMD loader references. - var S2 = \ No newline at end of file diff --git a/static/vendor/select2/tests/a11y/search-tests.js b/static/vendor/select2/tests/a11y/search-tests.js deleted file mode 100644 index 58e56492..00000000 --- a/static/vendor/select2/tests/a11y/search-tests.js +++ /dev/null @@ -1,51 +0,0 @@ -module('Accessibility - Search'); - -var MultipleSelection = require('select2/selection/multiple'); -var InlineSearch = require('select2/selection/search'); - -var $ = require('jquery'); - -var Utils = require('select2/utils'); -var Options = require('select2/options'); -var options = new Options({}); - -test('aria-autocomplete attribute is present', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - var selection = new CustomSelection($select, options); - var $selection = selection.render(); - - // Update the selection so the search is rendered - selection.update([]); - - assert.equal( - $selection.find('input').attr('aria-autocomplete'), - 'list', - 'The search box is marked as autocomplete' - ); -}); - -test('aria-activedescendant should be removed when closed', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - var selection = new CustomSelection($select, options); - var $selection = selection.render(); - - var container = new MockContainer(); - selection.bind(container, $('')); - - // Update the selection so the search is rendered - selection.update([]); - - var $search = $selection.find('input'); - $search.attr('aria-activedescendant', 'something'); - - container.trigger('close'); - - assert.ok( - !$search.attr('aria-activedescendant'), - 'There is no active descendant when the dropdown is closed' - ); -}); diff --git a/static/vendor/select2/tests/a11y/selection-tests.js b/static/vendor/select2/tests/a11y/selection-tests.js deleted file mode 100644 index e2c14602..00000000 --- a/static/vendor/select2/tests/a11y/selection-tests.js +++ /dev/null @@ -1,154 +0,0 @@ -module('Accessibility - All'); - -var BaseSelection = require('select2/selection/base'); -var SingleSelection = require('select2/selection/single'); -var MultipleSelection = require('select2/selection/multiple'); - -var $ = require('jquery'); - -var Options = require('select2/options'); -var options = new Options({}); - -test('title is carried over from original element', function (assert) { - var $select = $('#qunit-fixture .single'); - - var selection = new BaseSelection($select, options); - var $selection = selection.render(); - - assert.equal( - $selection.attr('title'), - $select.attr('title'), - 'The title should have been copied over from the original element' - ); -}); - -test('aria-expanded reflects the state of the container', function (assert) { - var $select = $('#qunit-fixture .single'); - - var selection = new BaseSelection($select, options); - var $selection = selection.render(); - - var container = new MockContainer(); - - selection.bind(container, $('')); - - assert.equal( - $selection.attr('aria-expanded'), - 'false', - 'The container should not be expanded when it is closed' - ); - - container.trigger('open'); - - assert.equal( - $selection.attr('aria-expanded'), - 'true', - 'The container should be expanded when it is opened' - ); -}); - -test('static aria attributes are present', function (assert) { - var $select = $('#qunit-fixture .single'); - - var selection = new BaseSelection($select, options); - var $selection = selection.render(); - - assert.equal( - $selection.attr('role'), - 'combobox', - 'The container should identify as a combobox' - ); - - assert.equal( - $selection.attr('aria-haspopup'), - 'true', - 'The dropdown is considered a popup of the container' - ); -}); - -test('the container should be in the tab order', function (assert) { - var $select = $('#qunit-fixture .single'); - - var selection = new BaseSelection($select, options); - var $selection = selection.render(); - - var container = new MockContainer(); - selection.bind(container, $('')); - - assert.equal( - $selection.attr('tabindex'), - '0', - 'The tab index should allow it to fit in the natural tab order' - ); - - container.trigger('disable'); - - assert.equal( - $selection.attr('tabindex'), - '-1', - 'The selection should be dropped out of the tab order when disabled' - ); - - container.trigger('enable'); - - assert.equal( - $selection.attr('tabindex'), - '0', - 'The tab index should be restored when re-enabled' - ); -}); - -test('a custom tabindex is copied', function (assert) { - var $select = $('#qunit-fixture .single'); - $select.attr('tabindex', '999'); - - var selection = new BaseSelection($select, options); - var $selection = selection.render(); - - var container = new MockContainer(); - selection.bind(container, $('')); - - assert.equal( - $selection.attr('tabindex'), - '999', - 'The tab index should match the original tab index' - ); - - container.trigger('disable'); - - assert.equal( - $selection.attr('tabindex'), - '-1', - 'The selection should be dropped out of the tab order when disabled' - ); - - container.trigger('enable'); - - assert.equal( - $selection.attr('tabindex'), - '999', - 'The tab index should be restored when re-enabled' - ); -}); - -module('Accessibility - Single'); - -test('aria-labelledby should match the rendered container', function (assert) { - var $select = $('#qunit-fixture .single'); - - var selection = new SingleSelection($select, options); - var $selection = selection.render(); - - var container = new MockContainer(); - selection.bind(container, $('')); - - var $rendered = $selection.find('.select2-selection__rendered'); - - assert.equal( - $selection.attr('aria-labelledby'), - $rendered.attr('id'), - 'The rendered selection should label the container' - ); -}); - -module('Accessibility - Multiple'); diff --git a/static/vendor/select2/tests/data/array-tests.js b/static/vendor/select2/tests/data/array-tests.js deleted file mode 100644 index d0dead68..00000000 --- a/static/vendor/select2/tests/data/array-tests.js +++ /dev/null @@ -1,332 +0,0 @@ -module('Data adapters - Array'); - -var ArrayData = require('select2/data/array'); -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var UserDefinedType = function (id, text) { - var self = this; - - self.id = id; - self.text = text; - - return self; -}; - -var arrayOptions = new Options({ - data: [ - { - id: 'default', - text: 'Default' - }, - { - id: '1', - text: 'One' - }, - { - id: '2', - text: '2' - }, - new UserDefinedType(1, 'aaaaaa') - ] -}); - -var extraOptions = new Options ({ - data: [ - { - id: 'default', - text: 'Default', - extra: true - }, - { - id: 'One', - text: 'One', - extra: true - } - ] -}); - -var nestedOptions = new Options({ - data: [ - { - text: 'Default', - children: [ - { - text: 'Next', - children: [ - { - id: 'a', - text: 'Option' - } - ] - } - ] - } - ] -}); - -test('current gets default for single', function (assert) { - var $select = $('#qunit-fixture .single-empty'); - - var data = new ArrayData($select, arrayOptions); - - data.current(function (val) { - assert.equal( - val.length, - 1, - 'There should always be a selected item for array data.' - ); - - var item = val[0]; - - assert.equal( - item.id, - 'default', - 'The first item should be selected' - ); - }); -}); - -test('current gets default for multiple', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var data = new ArrayData($select, arrayOptions); - - data.current(function (val) { - assert.equal( - val.length, - 0, - 'There should be no default selection.' - ); - }); -}); - -test('current works with existing selections', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var data = new ArrayData($select, arrayOptions); - - $select.val(['One']); - - data.current(function (val) { - assert.equal( - val.length, - 1, - 'There should only be one existing selection.' - ); - - var option = val[0]; - - assert.equal( - option.id, - 'One', - 'The id should be equal to the value of the option tag.' - ); - - assert.equal( - option.text, - 'One', - 'The text should be equal to the text of the option tag.' - ); - }); -}); - -test('current works with selected data', function (assert) { - var $select = $('#qunit-fixture .single-empty'); - - var data = new ArrayData($select, arrayOptions); - - data.select({ - id: '2', - text: '2' - }); - - data.current(function (val) { - assert.equal( - val.length, - 1, - 'There should only be one option selected.' - ); - - var option = val[0]; - - assert.equal( - option.id, - '2', - 'The id should match the original id from the array.' - ); - - assert.equal( - option.text, - '2', - 'The text should match the original text from the array.' - ); - }); -}); - -test('select works for single', function (assert) { - var $select = $('#qunit-fixture .single-empty'); - - var data = new ArrayData($select, arrayOptions); - - assert.equal( - $select.val(), - 'default', - 'There should already be a selection' - ); - - data.select({ - id: '1', - text: 'One' - }); - - assert.equal( - $select.val(), - '1', - 'The selected value should be the same as the selected id' - ); -}); - -test('multiple sets the value', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var data = new ArrayData($select, arrayOptions); - - assert.ok( - $select.val() == null || $select.val().length == 0, - 'nothing should be selected' - ); - - data.select({ - id: 'default', - text: 'Default' - }); - - assert.deepEqual($select.val(), ['default']); -}); - -test('multiple adds to the old value', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var data = new ArrayData($select, arrayOptions); - - $select.val(['One']); - - assert.deepEqual($select.val(), ['One']); - - data.select({ - id: 'default', - text: 'Default' - }); - - assert.deepEqual($select.val(), ['One', 'default']); -}); - -test('option tags are automatically generated', function (assert) { - var $select = $('#qunit-fixture .single-empty'); - - var data = new ArrayData($select, arrayOptions); - - assert.equal( - $select.find('option').length, - 4, - 'An element should be created for the two with children' - ); -}); - -test('optgroup tags have the right properties', function (assert) { - var $select = $('#qunit-fixture .single-empty'); - - var data = new ArrayData($select, nestedOptions); - - var $group = $select.children('optgroup'); - - assert.equal( - $group.prop('label'), - 'Default', - 'An `` label should match the text property' - ); - - assert.equal( - $group.children().length, - 1, - 'The should have one child under it' - ); -}); - -test('existing selections are respected on initialization', function (assert) { - var $select = $( - '' - ); - - var options = new Options({ - data: [ - { - id: 'Second', - text: 'Second' - }, - { - id: 'Third', - text: 'Third' - } - ] - }); - - assert.equal($select.val(), 'Second'); - - var data = new ArrayData($select, options); - - assert.equal($select.val(), 'Second'); -}); \ No newline at end of file diff --git a/static/vendor/select2/tests/data/base-tests.js b/static/vendor/select2/tests/data/base-tests.js deleted file mode 100644 index b90158f3..00000000 --- a/static/vendor/select2/tests/data/base-tests.js +++ /dev/null @@ -1,29 +0,0 @@ -module('Data adapters - Base'); - -var BaseData = require('select2/data/base'); -var $ = require('jquery'); -var Options = require('select2/options'); - -var options = new Options({}); - -test('current is required', function (assert) { - var data = new BaseData($('#qunit-fixture select'), options); - - assert.throws( - function () { - data.current(function () {}); - }, - 'current has no default implementation' - ); -}); - -test('query is required', function (assert) { - var data = new BaseData($('#qunit-fixture select'), options); - - assert.throws( - function () { - data.query({}, function () {}); - }, - 'query has no default implementation' - ); -}); diff --git a/static/vendor/select2/tests/data/inputData-tests.js b/static/vendor/select2/tests/data/inputData-tests.js deleted file mode 100644 index f2124efb..00000000 --- a/static/vendor/select2/tests/data/inputData-tests.js +++ /dev/null @@ -1,158 +0,0 @@ -module('Data adapters - compatibility'); - -var $ = require('jquery'); - -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var ArrayData = require('select2/data/array'); -var InputData = require('select2/compat/inputData'); - -var InputAdapter = Utils.Decorate(ArrayData, InputData); - -test('test that options can be selected', function (assert) { - var options = new Options({ - data: [ - { - id: 'test', - text: 'Test' - } - ] - }); - var $element = $(''); - - var adapter = new InputAdapter($element, options); - - adapter.select({ - id: 'test' - }); - - assert.equal( - $element.val(), - 'test', - 'The id of the item should be the value' - ); -}); - -test('unselect the single selected option clears the value', function (assert) { - var options = new Options({ - data: [ - { - id: 'test', - text: 'Test', - selected: true - } - ] - }); - var $element = $(''); - - var adapter = new InputAdapter($element, options); - - adapter.unselect({ - id: 'test' - }); - - assert.equal( - $element.val(), - '', - 'The id should no longer be in the value' - ); -}); - -test('options can be unselected individually', function (assert) { - var options = new Options({ - data: [ - { - id: 'test', - text: 'Test' - }, - { - id: 'test2', - text: 'Test2' - }, - { - id: 'test3', - text: 'Test3' - } - ] - }); - var $element = $(''); - $element.val('test,test2,test3'); - - var adapter = new InputAdapter($element, options); - - adapter.unselect({ - id: 'test2' - }); - - assert.equal( - $element.val(), - 'test,test3', - 'The value should contain all the still selected options' - ); -}); - -test('default values can be set', function (assert) { - assert.expect(4); - - var options = new Options({ - data: [ - { - id: 'test', - text: 'Test' - } - ] - }); - var $element = $(''); - - var adapter = new InputAdapter($element, options); - - adapter.current(function (data) { - assert.equal( - data.length, - 1, - 'There should only be a single selected option' - ); - - var item = data[0]; - - assert.equal(item.id, 'test'); - assert.equal(item.text, 'Test'); - }); - - assert.equal( - $element.val(), - 'test', - 'The value should not have been altered' - ); -}); - -test('no default value', function (assert) { - assert.expect(2); - - var options = new Options({ - data: [ - { - id: 'test', - text: 'Test' - } - ] - }); - var $element = $(''); - - var adapter = new InputAdapter($element, options); - - adapter.current(function (data) { - assert.equal( - data.length, - 0, - 'There should be no selected options' - ); - }); - - assert.equal( - $element.val(), - '', - 'The value should not have been altered' - ); -}); diff --git a/static/vendor/select2/tests/data/maximumInputLength-tests.js b/static/vendor/select2/tests/data/maximumInputLength-tests.js deleted file mode 100644 index e8557139..00000000 --- a/static/vendor/select2/tests/data/maximumInputLength-tests.js +++ /dev/null @@ -1,138 +0,0 @@ -module('Data adapters - Maximum input length'); - -var MaximumInputLength = require('select2/data/maximumInputLength'); -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -function MaximumInputStub () { - this.called = false; -} - -MaximumInputStub.prototype.query = function (params, callback) { - this.called = true; -}; - -var MaximumInputData = Utils.Decorate(MaximumInputStub, MaximumInputLength); - -test('0 never displays the notice', function (assert) { - var zeroOptions = new Options({ - maximumInputLength: 0 - }); - - var data = new MaximumInputData(null, zeroOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumInputData(null, zeroOptions); - - data.query({ - term: 'test' - }); - - assert.ok(data.called); -}); - -test('< 0 never displays the notice', function (assert) { - var negativeOptions = new Options({ - maximumInputLength: -1 - }); - - var data = new MaximumInputData(null, negativeOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumInputData(null, negativeOptions); - - data.query({ - term: 'test' - }); - - assert.ok(data.called); -}); - -test('triggers when input is too long', function (assert) { - var options = new Options({ - maximumInputLength: 1 - }); - - var data = new MaximumInputData(null, options); - - data.trigger = function () { - assert.ok(true, 'The event should be triggered.'); - }; - - data.query({ - term: 'no' - }); - - assert.ok(!data.called, 'The adapter should not be called'); -}); - -test('does not trigger when equal', function (assert) { - var options = new Options({ - maximumInputLength: 10 - }); - - var data = new MaximumInputData(null, options); - - data.trigger = function () { - assert.ok(false, 'The event should not be triggered.'); - }; - - data.query({ - term: '1234567890' - }); - - assert.ok(data.called); -}); - -test('does not trigger when less', function (assert) { - var options = new Options({ - maximumInputLength: 10 - }); - - var data = new MaximumInputData(null, options); - - data.trigger = function () { - assert.ok(false, 'The event should not be triggered.'); - }; - - data.query({ - term: '123' - }); - - assert.ok(data.called); -}); - -test('works with null term', function (assert) { - var options = new Options({ - maximumInputLength: 1 - }); - - var data = new MaximumInputData(null, options); - - data.trigger = function () { - assert.ok(false, 'The event should not be triggered'); - }; - - data.query({}); - - assert.ok(data.called); -}); diff --git a/static/vendor/select2/tests/data/maximumSelectionLength-tests.js b/static/vendor/select2/tests/data/maximumSelectionLength-tests.js deleted file mode 100644 index 89943b38..00000000 --- a/static/vendor/select2/tests/data/maximumSelectionLength-tests.js +++ /dev/null @@ -1,202 +0,0 @@ -module('Data adapters - Maximum selection length'); - -var MaximumSelectionLength = require('select2/data/maximumSelectionLength'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -function MaximumSelectionStub () { - this.called = false; - this.currentData = []; -} - -MaximumSelectionStub.prototype.current = function (callback) { - callback(this.currentData); -}; - -MaximumSelectionStub.prototype.val = function (val) { - this.currentData.push(val); -}; - -MaximumSelectionStub.prototype.query = function (params, callback) { - this.called = true; -}; - -var MaximumSelectionData = Utils.Decorate( - MaximumSelectionStub, - MaximumSelectionLength -); - -test('0 never displays the notice', function (assert) { - var zeroOptions = new Options({ - maximumSelectionLength: 0 - }); - - var data = new MaximumSelectionData(null, zeroOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumSelectionData(null, zeroOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.val('1'); - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumSelectionData(null, zeroOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.val('1'); - data.val('2'); - - data.query({ - term: '' - }); - - assert.ok(data.called); -}); - -test('< 0 never displays the notice', function (assert) { - var negativeOptions = new Options({ - maximumSelectionLength: -1 - }); - - var data = new MaximumSelectionData(null, negativeOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumSelectionData(null, negativeOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.val('1'); - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumSelectionData(null, negativeOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.val('1'); - data.val('2'); - - data.query({ - term: '' - }); - - assert.ok(data.called); -}); - -test('triggers when >= 1 selection' , function (assert) { - var maxOfOneOptions = new Options({ - maximumSelectionLength: 1 - }); - var data = new MaximumSelectionData(null, maxOfOneOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumSelectionData(null, maxOfOneOptions); - - data.trigger = function () { - assert.ok(true, 'The event should be triggered.'); - }; - - data.val('1'); - - data.query({ - term: '' - }); - - assert.ok(!data.called); - -}); - -test('triggers when >= 2 selections' , function (assert) { - var maxOfTwoOptions = new Options({ - maximumSelectionLength: 2 - }); - var data = new MaximumSelectionData(null, maxOfTwoOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumSelectionData(null, maxOfTwoOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.val('1'); - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MaximumSelectionData(null, maxOfTwoOptions); - - data.trigger = function () { - assert.ok(true, 'The event should be triggered.'); - }; - - data.val('1'); - data.val('2'); - - data.query({ - term: '' - }); - - assert.ok(!data.called); - -}); diff --git a/static/vendor/select2/tests/data/minimumInputLength-tests.js b/static/vendor/select2/tests/data/minimumInputLength-tests.js deleted file mode 100644 index a67db08d..00000000 --- a/static/vendor/select2/tests/data/minimumInputLength-tests.js +++ /dev/null @@ -1,138 +0,0 @@ -module('Data adapters - Minimum input length'); - -var MinimumInputLength = require('select2/data/minimumInputLength'); -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -function StubData () { - this.called = false; -} - -StubData.prototype.query = function (params, callback) { - this.called = true; -}; - -var MinimumData = Utils.Decorate(StubData, MinimumInputLength); - -test('0 never displays the notice', function (assert) { - var zeroOptions = new Options({ - minimumInputLength: 0 - }); - - var data = new MinimumData(null, zeroOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MinimumData(null, zeroOptions); - - data.query({ - term: 'test' - }); - - assert.ok(data.called); -}); - -test('< 0 never displays the notice', function (assert) { - var negativeOptions = new Options({ - minimumInputLength: -1 - }); - - var data = new MinimumData(null, negativeOptions); - - data.trigger = function () { - assert.ok(false, 'No events should be triggered'); - }; - - data.query({ - term: '' - }); - - assert.ok(data.called); - - data = new MinimumData(null, negativeOptions); - - data.query({ - term: 'test' - }); - - assert.ok(data.called); -}); - -test('triggers when input is not long enough', function (assert) { - var options = new Options({ - minimumInputLength: 10 - }); - - var data = new MinimumData(null, options); - - data.trigger = function () { - assert.ok(true, 'The event should be triggered.'); - }; - - data.query({ - term: 'no' - }); - - assert.ok(!data.called); -}); - -test('does not trigger when equal', function (assert) { - var options = new Options({ - minimumInputLength: 10 - }); - - var data = new MinimumData(null, options); - - data.trigger = function () { - assert.ok(false, 'The event should not be triggered.'); - }; - - data.query({ - term: '1234567890' - }); - - assert.ok(data.called); -}); - -test('does not trigger when greater', function (assert) { - var options = new Options({ - minimumInputLength: 10 - }); - - var data = new MinimumData(null, options); - - data.trigger = function () { - assert.ok(false, 'The event should not be triggered.'); - }; - - data.query({ - term: '12345678901' - }); - - assert.ok(data.called); -}); - -test('works with null term', function (assert) { - var options = new Options({ - minimumInputLength: 1 - }); - - var data = new MinimumData(null, options); - - data.trigger = function () { - assert.ok(true, 'The event should be triggered'); - }; - - data.query({}); - - assert.ok(!data.called); -}); diff --git a/static/vendor/select2/tests/data/select-tests.js b/static/vendor/select2/tests/data/select-tests.js deleted file mode 100644 index b59c6d4b..00000000 --- a/static/vendor/select2/tests/data/select-tests.js +++ /dev/null @@ -1,557 +0,0 @@ -module('Data adapters - Select - current'); - -var SelectData = require('select2/data/select'); -var $ = require('jquery'); -var Options = require('select2/options'); -var selectOptions = new Options({}); - -test('current gets default for single', function (assert) { - var $select = $('#qunit-fixture .single'); - - var data = new SelectData($select, selectOptions); - - data.current(function (data) { - assert.equal( - data.length, - 1, - 'There should only be one selected option' - ); - - var option = data[0]; - - assert.equal( - option.id, - 'One', - 'The value of the option tag should be the id' - ); - - assert.equal( - option.text, - 'One', - 'The text within the option tag should be the text' - ); - }); -}); - -test('current gets default for multiple', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var data = new SelectData($select, selectOptions); - - data.current(function (data) { - assert.equal( - data.length, - 0, - 'Multiple selects have no default selection.' - ); - }); -}); - -test('current gets options with explicit value', function (assert) { - var $select = $('#qunit-fixture .single'); - - var $option = $(''); - $select.append($option); - - var data = new SelectData($select, selectOptions); - - $select.val('1'); - - data.current(function (data) { - assert.equal( - data.length, - 1, - 'There should be one selected option' - ); - - var option = data[0]; - - assert.equal( - option.id, - '1', - 'The option value should be the selected id' - ); - - assert.equal( - option.text, - 'One', - 'The text should match the text for the option tag' - ); - }); -}); - -test('current gets options with implicit value', function (assert) { - var $select = $('#qunit-fixture .single'); - - var data = new SelectData($select, selectOptions); - - $select.val('One'); - - data.current(function (val) { - assert.equal( - val.length, - 1, - 'There should only be one selected value' - ); - - var option = val[0]; - - assert.equal( - option.id, - 'One', - 'The id should be the same as the option text' - ); - - assert.equal( - option.text, - 'One', - 'The text should be the same as the option text' - ); - }); -}); - -test('select works for single', function (assert) { - var $select = $('#qunit-fixture .single-with-placeholder'); - - var data = new SelectData($select, selectOptions); - - assert.equal($select.val(), 'placeholder'); - - data.select({ - id: 'One', - text: 'One' - }); - - assert.equal($select.val(), 'One'); -}); - -test('multiple sets the value', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var data = new SelectData($select, selectOptions); - - assert.ok( - $select.val() == null || $select.val().length == 0, - 'nothing should be selected' - ); - - data.select({ - id: 'Two', - text: 'Two' - }); - - assert.deepEqual($select.val(), ['Two']); -}); - -test('multiple adds to the old value', function (assert) { - var $select = $('#qunit-fixture .multiple'); - - var data = new SelectData($select, selectOptions); - - $select.val(['Two']); - - assert.deepEqual($select.val(), ['Two']); - - data.select({ - id: 'One', - text: 'One' - }); - - assert.deepEqual($select.val(), ['One', 'Two']); -}); - -test('duplicates - single - same id on select triggers change', - function (assert) { - var $select = $('#qunit-fixture .duplicates'); - - var data = new SelectData($select, data); - var second = $('#qunit-fixture .duplicates option')[2]; - - var changeTriggered = false; - - assert.equal($select.val(), 'one'); - - $select.on('change', function () { - changeTriggered = true; - }); - - data.select({ - id: 'one', - text: 'Uno', - element: second - }); - - assert.equal( - $select.val(), - 'one', - 'The value never changed' - ); - - assert.ok( - changeTriggered, - 'The change event should be triggered' - ); - - assert.ok( - second.selected, - 'The second duplicate is selected, not the first' - ); -}); - -test('duplicates - single - different id on select triggers change', - function (assert) { - var $select = $('#qunit-fixture .duplicates'); - - var data = new SelectData($select, data); - var second = $('#qunit-fixture .duplicates option')[2]; - - var changeTriggered = false; - - $select.val('two'); - - $select.on('change', function () { - changeTriggered = true; - }); - - data.select({ - id: 'one', - text: 'Uno', - element: second - }); - - assert.equal( - $select.val(), - 'one', - 'The value changed to the duplicate id' - ); - - assert.ok( - changeTriggered, - 'The change event should be triggered' - ); - - assert.ok( - second.selected, - 'The second duplicate is selected, not the first' - ); -}); - -test('duplicates - multiple - same id on select triggers change', -function (assert) { - var $select = $('#qunit-fixture .duplicates-multi'); - - var data = new SelectData($select, data); - var second = $('#qunit-fixture .duplicates-multi option')[2]; - - var changeTriggered = false; - - $select.val(['one']); - - $select.on('change', function () { - changeTriggered = true; - }); - - data.select({ - id: 'one', - text: 'Uno', - element: second - }); - - assert.deepEqual( - $select.val(), - ['one', 'one'], - 'The value now has duplicates' - ); - - assert.ok( - changeTriggered, - 'The change event should be triggered' - ); - - assert.ok( - second.selected, - 'The second duplicate is selected, not the first' - ); -}); - -test('duplicates - multiple - different id on select triggers change', -function (assert) { - var $select = $('#qunit-fixture .duplicates-multi'); - - var data = new SelectData($select, data); - var second = $('#qunit-fixture .duplicates-multi option')[2]; - - var changeTriggered = false; - - $select.val(['two']); - - $select.on('change', function () { - changeTriggered = true; - }); - - data.select({ - id: 'one', - text: 'Uno', - element: second - }); - - assert.deepEqual( - $select.val(), - ['two', 'one'], - 'The value has the new id' - ); - - assert.ok( - changeTriggered, - 'The change event should be triggered' - ); - - assert.ok( - second.selected, - 'The second duplicate is selected, not the first' - ); -}); - -module('Data adapter - Select - query'); - -test('all options are returned with no term', function (assert) { - var $select = $('#qunit-fixture .single'); - - var data = new SelectData($select, selectOptions); - - data.query({}, function (data) { - assert.equal( - data.results.length, - 1, - 'The number of items returned should be equal to the number of options' - ); - }); -}); - -test('the matcher checks the text', function (assert) { - var $select = $('#qunit-fixture .single'); - - var data = new SelectData($select, selectOptions); - - data.query({ - term: 'One' - }, function (data) { - assert.equal( - data.results.length, - 1, - 'Only the "One" option should be found' - ); - }); -}); - -test('the matcher ignores case', function (assert) { - var $select = $('#qunit-fixture .single'); - - var data = new SelectData($select, selectOptions); - - data.query({ - term: 'one' - }, function (data) { - assert.equal( - data.results.length, - 1, - 'The "One" option should still be found' - ); - }); -}); - -test('no options may be returned with no matches', function (assert) { - var $select = $('#qunit-fixture .single'); - - var data = new SelectData($select, selectOptions); - - data.query({ - term: 'qwerty' - }, function (data) { - assert.equal( - data.results.length, - 0, - 'Only matching items should be returned' - ); - }); -}); - -test('optgroup tags are marked with children', function (assert) { - var $select = $('#qunit-fixture .groups'); - - var data = new SelectData($select, selectOptions); - - data.query({}, function (data) { - assert.ok( - 'children' in data.results[0], - 'The optgroup element should have children when queried' - ); - }); -}); - -test('empty optgroups are still shown when queried', function (assert) { - var $select = $('#qunit-fixture .groups'); - - var data = new SelectData($select, selectOptions); - - data.query({}, function (data) { - assert.equal( - data.results.length, - 2, - 'The empty optgroup element should still be returned when queried' - ); - - var item = data.results[1]; - - assert.equal( - item.text, - 'Empty', - 'The text of the empty optgroup should match the label' - ); - - assert.equal( - item.children.length, - 0, - 'There should be no children in the empty opgroup' - ); - }); -}); - -test('multiple options with the same value are returned', function (assert) { - var $select = $('#qunit-fixture .duplicates'); - - var data = new SelectData($select, selectOptions); - - data.query({}, function (data) { - assert.equal( - data.results.length, - 3, - 'The duplicate option should still be returned when queried' - ); - - var first = data.results[0]; - var duplicate = data.results[2]; - - assert.equal( - first.id, - duplicate.id, - 'The duplicates should have the same id' - ); - - assert.notEqual( - first.text, - duplicate.text, - 'The duplicates do not have the same text' - ); - }); -}); - -test('data objects use the text of the option', function (assert) { - var $select = $('#qunit-fixture .duplicates'); - - var data = new SelectData($select, selectOptions); - - var $option = $(''); - - var item = data.item($option); - - assert.equal(item.id, '&'); - assert.equal(item.text, '&'); -}); - -test('select option construction accepts id=0 (zero) value', function (assert) { - var $select = $('#qunit-fixture .single'); - - var selectOptions = [{ id: 0, text: 'Zero Value'}]; - var data = new SelectData($select, selectOptions); - - var optionElem = data.option(selectOptions[0]); - - // If was "Zero Value"", then it ignored id property - assert.equal( - optionElem[0].value, - '0', - 'Built option value should be "0" (zero as a string).' - ); -}); - -test('select option construction accepts id="" (empty string) value', - function (assert) { - var $select = $('#qunit-fixture .single'); - - var selectOptions = [{ id: '', text: 'Empty String'}]; - var data = new SelectData($select, selectOptions); - - var optionElem = data.option(selectOptions[0]); - - assert.equal( - optionElem[0].value, - '', - 'Built option value should be an empty string.' - ); -}); - -test('user-defined types are normalized properly', function (assert) { - var $select = $('#qunit-fixture .user-defined'), - - UserDefinedType = function (id, text) { - var self = this; - - self.id = id; - self.text = text; - - return self; - }; - - var testData = [ - 'Test', - { - id: 4, - text: 'item' - }, - new UserDefinedType(1, 'aaaaaa') - ]; - - var data = new SelectData($select, selectOptions); - - var normalizedItem = data._normalizeItem(testData[0]); - var normalizedItem2 = data._normalizeItem(testData[1]); - var normalizedItem3 = data._normalizeItem(testData[2]); - - assert.equal( - testData[0], - normalizedItem.id, - 'id property should be equal to text after normalize' - ); - - assert.equal( - testData[0], - normalizedItem.text, - 'text property should be equal after normalize' - ); - - assert.equal( - testData[1].id, - normalizedItem2.id, - 'id property should be equal after normalize' - ); - - assert.equal( - testData[1].text, - normalizedItem2.text, - 'text property should be equal after normalize' - ); - - assert.equal( - testData[2].id, - normalizedItem3.id, - 'id property should be equal after normalize' - ); - - assert.equal( - testData[2].text, - normalizedItem3.text, - 'text property should be equal after normalize' - ); - -}); diff --git a/static/vendor/select2/tests/data/tags-tests.js b/static/vendor/select2/tests/data/tags-tests.js deleted file mode 100644 index 6af8d8a7..00000000 --- a/static/vendor/select2/tests/data/tags-tests.js +++ /dev/null @@ -1,276 +0,0 @@ -module('Data adapters - Tags'); - -var SelectData = require('select2/data/select'); -var Tags = require('select2/data/tags'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var SelectTags = Utils.Decorate(SelectData, Tags); -var options = new Options({ - tags: true -}); - -test('does not trigger on blank or null terms', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: '' - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'One'); - assert.equal(item.text, 'One'); - }); - - data.query({ - term: null - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'One'); - assert.equal(item.text, 'One'); - }); -}); - -test('white space is trimmed by default', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: ' ' - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'One'); - assert.equal(item.text, 'One'); - }); - - data.query({ - term: ' One ' - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'One'); - assert.equal(item.text, 'One'); - }); -}); - -test('does not create option if text is same but lowercase', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: 'one' - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'One'); - assert.equal(item.text, 'One'); - }); -}); - -test('does not trigger for additional pages', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - page: 2 - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'One'); - assert.equal(item.text, 'One'); - }); -}); - -test('creates tag at beginning', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: 'o' - }, function (data) { - assert.equal(data.results.length, 2); - - var first = data.results[0]; - - assert.equal(first.id, 'o'); - assert.equal(first.text, 'o'); - }); -}); - -test('tags can be the only result', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: 'test' - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'test'); - assert.equal(item.text, 'test'); - }); -}); - -test('tags are injected as options', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: 'test' - }, function (data) { - assert.equal(data.results.length, 1); - - var $children = $('#qunit-fixture .single option'); - - assert.equal($children.length, 2); - - var $tag = $children.last(); - - assert.equal($tag.val(), 'test'); - assert.equal($tag.text(), 'test'); - }); -}); - -test('old tags are removed automatically', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: 'first' - }, function (data) { - assert.equal(data.results.length, 1); - - var $children = $('#qunit-fixture .single option'); - - assert.equal($children.length, 2); - }); - - data.query({ - term: 'second' - }, function (data) { - assert.equal(data.results.length, 1); - - var $children = $('#qunit-fixture .single option'); - - assert.equal($children.length, 2); - - var $tag = $children.last(); - - assert.equal($tag.val(), 'second'); - assert.equal($tag.text(), 'second'); - }); -}); - -test('insertTag controls the tag location', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.insertTag = function (data, tag) { - data.push(tag); - }; - - data.query({ - term: 'o' - }, function (data) { - assert.equal(data.results.length, 2); - - var item = data.results[1]; - - assert.equal(item.id, 'o'); - assert.equal(item.text, 'o'); - }); -}); - -test('insertTag can be controlled through the options', function (assert) { - var options = new Options({ - insertTag: function (data, tag) { - data.push(tag); - } - }); - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.query({ - term: 'o' - }, function (data) { - assert.equal(data.results.length, 2); - - var item = data.results[1]; - - assert.equal(item.id, 'o'); - assert.equal(item.text, 'o'); - }); -}); - -test('createTag controls the tag object', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.createTag = function (params) { - return { - id: 0, - text: params.term - }; - }; - - data.query({ - term: 'test' - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 0); - assert.equal(item.text, 'test'); - }); -}); - -test('createTag returns null for no tag', function (assert) { - var data = new SelectTags($('#qunit-fixture .single'), options); - - data.createTag = function (params) { - return null; - }; - - data.query({ - term: 'o' - }, function (data) { - assert.equal(data.results.length, 1); - }); -}); - -test('the createTag options customizes the function', function (assert) { - var data = new SelectTags( - $('#qunit-fixture .single'), - new Options({ - tags: true, - createTag: function (params) { - return { - id: params.term, - text: params.term, - tag: true - }; - } - }) - ); - - data.query({ - term: 'test' - }, function (data) { - assert.equal(data.results.length, 1); - - var item = data.results[0]; - - assert.equal(item.id, 'test'); - assert.equal(item.text, 'test'); - assert.equal(item.tag, true); - }); -}); \ No newline at end of file diff --git a/static/vendor/select2/tests/data/tokenizer-tests.js b/static/vendor/select2/tests/data/tokenizer-tests.js deleted file mode 100644 index 2fa01211..00000000 --- a/static/vendor/select2/tests/data/tokenizer-tests.js +++ /dev/null @@ -1,219 +0,0 @@ -module('Data adaptor - Tokenizer'); - -test('triggers the select event', function (assert) { - assert.expect(2); - - var SelectData = require('select2/data/select'); - var Tokenizer = require('select2/data/tokenizer'); - var Tags = require('select2/data/tags'); - - var Options = require('select2/options'); - var Utils = require('select2/utils'); - - var $ = require('jquery'); - - var TokenizedSelect = Utils.Decorate( - Utils.Decorate(SelectData, Tags), - Tokenizer - ); - var $select = $('#qunit-fixture .single'); - - var options = new Options({ - tags: true, - tokenSeparators: [','] - }); - - var container = new MockContainer(); - container.dropdown = container.selection = {}; - - var $container = $('
        '); - - var data = new TokenizedSelect($select, options); - data.bind(container, $container); - - data.on('select', function () { - assert.ok(true, 'The select event should be triggered'); - }); - - data.query({ - term: 'first,second' - }, function () { - assert.ok(true, 'The callback should have succeeded'); - }); -}); - -test('createTag can return null', function (assert) { - assert.expect(3); - - var SelectData = require('select2/data/select'); - var Tokenizer = require('select2/data/tokenizer'); - var Tags = require('select2/data/tags'); - - var Options = require('select2/options'); - var Utils = require('select2/utils'); - - var $ = require('jquery'); - - var TokenizedSelect = Utils.Decorate( - Utils.Decorate(SelectData, Tags), - Tokenizer - ); - var $select = $('#qunit-fixture .single'); - - var options = new Options({ - tags: true, - tokenSeparators: [','], - createTag: function () { - assert.ok(true, 'createTag should have been called'); - - return null; - } - }); - - var container = new MockContainer(); - container.dropdown = container.selection = {}; - - var $container = $('
        '); - - var data = new TokenizedSelect($select, options); - data.bind(container, $container); - - data.on('select', function (params) { - if (params.data == null) { - assert.ok(false, 'Null data should never be selected'); - } - }); - - data.query({ - term: 'first,second' - }, function () { - assert.ok(true, 'The callback should have succeeded'); - }); -}); - -test('createTag returning null does not cut the term', function (assert) { - assert.expect(4); - - var SelectData = require('select2/data/select'); - var Tokenizer = require('select2/data/tokenizer'); - var Tags = require('select2/data/tags'); - - var Options = require('select2/options'); - var Utils = require('select2/utils'); - - var $ = require('jquery'); - - var TokenizedSelect = Utils.Decorate( - Utils.Decorate(SelectData, Tags), - Tokenizer - ); - var $select = $('#qunit-fixture .single'); - - var options = new Options({ - tags: true, - tokenSeparators: [',', '"'], - createTag: function (params) { - var term = params.term; - - // Ignore blanks - if (term.length === 0) { - return null; - } - - // Ignore the leading quote - if (term === '"') { - return null; - } - - // If there is a leading quote, check for a second one - if (term[0] === '"' && term[term.length - 1] !== '"') { - return null; - } - - var text = term.substr(1, term.length - 2); - - return { - id: term, - text: text - }; - } - }); - - var container = new MockContainer(); - container.dropdown = container.selection = {}; - - var $container = $('
        '); - - var data = new TokenizedSelect($select, options); - data.bind(container, $container); - - data.on('select', function (params) { - assert.ok(params.data, 'Data should not be null'); - - assert.equal( - params.data.id, - '"first, second"', - 'The id should have the quotes' - ); - - assert.equal( - params.data.text, - 'first, second', - 'The text should not have the quotes' - ); - }); - - data.query({ - term: '"first, second",abc' - }, function () { - assert.ok(true, 'The callback should have succeeded'); - }); -}); - -test('works with multiple tokens given', function (assert) { - assert.expect(4); - - var SelectData = require('select2/data/select'); - var Tokenizer = require('select2/data/tokenizer'); - var Tags = require('select2/data/tags'); - - var Options = require('select2/options'); - var Utils = require('select2/utils'); - - var $ = require('jquery'); - - var TokenizedSelect = Utils.Decorate( - Utils.Decorate(SelectData, Tags), - Tokenizer - ); - var $select = $('#qunit-fixture .multiple'); - - var options = new Options({ - tags: true, - tokenSeparators: [','] - }); - - var container = new MockContainer(); - container.dropdown = container.selection = {}; - - var $container = $('
        '); - - var data = new TokenizedSelect($select, options); - data.bind(container, $container); - - data.on('select', function () { - assert.ok(true, 'The select event should be triggered'); - }); - - data.query({ - term: 'first,second,third' - }, function () { - assert.ok(true, 'The callback should have succeeded'); - }); - - assert.equal( - $select.children('option').length, - 3, - 'The two new tags should have been created' - ); -}); \ No newline at end of file diff --git a/static/vendor/select2/tests/dropdown/dropdownCss-tests.js b/static/vendor/select2/tests/dropdown/dropdownCss-tests.js deleted file mode 100644 index 9969335e..00000000 --- a/static/vendor/select2/tests/dropdown/dropdownCss-tests.js +++ /dev/null @@ -1,104 +0,0 @@ -module('Dropdown - dropdownCssClass compatibility'); - -var $ = require('jquery'); -var Utils = require('select2/utils'); -var Options = require('select2/options'); - -var Dropdown = require('select2/dropdown'); -var DropdownCSS = Utils.Decorate( - Dropdown, - require('select2/compat/dropdownCss') -); - -test('all classes will be copied if :all: is used', function (assert) { - var $element = $(''); - var options = new Options({ - dropdownCssClass: ':all:' - }); - - var select = new DropdownCSS($element, options); - var $dropdown = select.render(); - - assert.ok($dropdown.hasClass('test')); - assert.ok($dropdown.hasClass('copy')); - assert.ok($dropdown.hasClass('works')); - assert.ok(!$dropdown.hasClass(':all:')); -}); - -test(':all: can be used with other classes', function (assert) { - var $element = $(''); - var options = new Options({ - dropdownCssClass: ':all: other' - }); - - var select = new DropdownCSS($element, options); - var $dropdown = select.render(); - - assert.ok($dropdown.hasClass('test')); - assert.ok($dropdown.hasClass('copy')); - assert.ok($dropdown.hasClass('works')); - assert.ok($dropdown.hasClass('other')); - assert.ok(!$dropdown.hasClass(':all:')); -}); - -test('classes can be passed in as a string', function (assert) { - var $element = $(''); - var options = new Options({ - dropdownCssClass: 'other' - }); - - var select = new DropdownCSS($element, options); - var $dropdown = select.render(); - - assert.ok($dropdown.hasClass('other')); -}); - -test('a function can be used based on the element', function (assert){ - var $element = $(''); - var options = new Options({ - dropdownCssClass: function ($element) { - return 'function'; - } - }); - - var select = new DropdownCSS($element, options); - var $dropdown = select.render(); - - assert.ok($dropdown.hasClass('function')); - assert.ok(!$dropdown.hasClass('test')); -}); - -test(':all: works around custom adapters', function (assert) { - var $element = $(''); - var options = new Options({ - dropdownCssClass: ':all: something', - adaptDropdownCssClass: function (clazz) { - return clazz + '-modified'; - } - }); - - var select = new DropdownCSS($element, options); - var $dropdown = select.render(); - - assert.ok($dropdown.hasClass('something')); - - assert.ok($dropdown.hasClass('test')); - assert.ok($dropdown.hasClass('test-modified')); -}); - -module('Dropdown - adaptDropdownCss compatibility'); - -test('only return when adapted', function (assert) { - var $element = $(''); - var options = new Options({ - adaptDropdownCssClass: function (clazz) { - return 'modified'; - } - }); - - var select = new DropdownCSS($element, options); - var $dropdown = select.render(); - - assert.ok(!$dropdown.hasClass('original')); - assert.ok($dropdown.hasClass('modified')); -}); diff --git a/static/vendor/select2/tests/dropdown/positioning-tests.js b/static/vendor/select2/tests/dropdown/positioning-tests.js deleted file mode 100644 index 2da68982..00000000 --- a/static/vendor/select2/tests/dropdown/positioning-tests.js +++ /dev/null @@ -1,177 +0,0 @@ -module('Dropdown - attachBody - positioning'); - -test('appends to the dropdown parent', function (assert) { - assert.expect(4); - - var $ = require('jquery'); - - var $select = $(''); - var $parent = $('
        '); - - var $container = $(''); - var container = new MockContainer(); - - $parent.appendTo($('#qunit-fixture')); - $select.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Dropdown = require('select2/dropdown'); - var AttachBody = require('select2/dropdown/attachBody'); - - var DropdownAdapter = Utils.Decorate(Dropdown, AttachBody); - - var dropdown = new DropdownAdapter($select, new Options({ - dropdownParent: $parent - })); - - assert.equal( - $parent.children().length, - 1, - 'Only the select should be in the container' - ); - - var $dropdown = dropdown.render(); - - dropdown.bind(container, $container); - - dropdown.position($dropdown, $container); - - assert.equal( - $parent.children().length, - 1, - 'The dropdown should not be placed until after it is opened' - ); - - dropdown._showDropdown(); - - assert.equal( - $parent.children().length, - 2, - 'The dropdown should now be in the container as well' - ); - - assert.ok( - $.contains($parent[0], $dropdown[0]), - 'The dropdown should be contained within the parent container' - ); -}); - -test('dropdown is positioned down with static margins', function (assert) { - var $ = require('jquery'); - var $select = $(''); - var $parent = $('
        '); - $parent.css({ - position: 'static', - marginTop: '5px', - marginLeft: '10px' - }); - - var $container = $('test'); - var container = new MockContainer(); - - $('#qunit-fixture').empty(); - - $parent.appendTo($('#qunit-fixture')); - $container.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Dropdown = require('select2/dropdown'); - var AttachBody = require('select2/dropdown/attachBody'); - - var DropdownAdapter = Utils.Decorate(Dropdown, AttachBody); - - var dropdown = new DropdownAdapter($select, new Options({ - dropdownParent: $parent - })); - - var $dropdown = dropdown.render(); - - assert.equal( - $dropdown[0].style.top, - 0, - 'The drodpown should not have any offset before it is displayed' - ); - - dropdown.bind(container, $container); - dropdown.position($dropdown, $container); - dropdown._showDropdown(); - - assert.ok( - dropdown.$dropdown.hasClass('select2-dropdown--below'), - 'The dropdown should be forced down' - ); - - assert.equal( - $dropdown.css('top').replace(/\D+/, ''), - $container.outerHeight() + 5, - 'The offset should be 5px at the top' - ); - - assert.equal( - $dropdown.css('left'), - '10px', - 'The offset should be 10px on the left' - ); -}); - -test('dropdown is positioned down with absolute offsets', function (assert) { - var $ = require('jquery'); - var $select = $(''); - var $parent = $('
        '); - $parent.css({ - position: 'absolute', - top: '10px', - left: '5px' - }); - - var $container = $('test'); - var container = new MockContainer(); - - $parent.appendTo($('#qunit-fixture')); - $container.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Dropdown = require('select2/dropdown'); - var AttachBody = require('select2/dropdown/attachBody'); - - var DropdownAdapter = Utils.Decorate(Dropdown, AttachBody); - - var dropdown = new DropdownAdapter($select, new Options({ - dropdownParent: $parent - })); - - var $dropdown = dropdown.render(); - - assert.equal( - $dropdown[0].style.top, - 0, - 'The drodpown should not have any offset before it is displayed' - ); - - dropdown.bind(container, $container); - dropdown.position($dropdown, $container); - dropdown._showDropdown(); - - assert.ok( - dropdown.$dropdown.hasClass('select2-dropdown--below'), - 'The dropdown should be forced down' - ); - - assert.equal( - $dropdown.css('top').replace(/\D+/, ''), - $container.outerHeight(), - 'There should not be an extra top offset' - ); - - assert.equal( - $dropdown.css('left'), - '0px', - 'There should not be an extra left offset' - ); -}); \ No newline at end of file diff --git a/static/vendor/select2/tests/dropdown/selectOnClose-tests.js b/static/vendor/select2/tests/dropdown/selectOnClose-tests.js deleted file mode 100644 index 3f1d1d41..00000000 --- a/static/vendor/select2/tests/dropdown/selectOnClose-tests.js +++ /dev/null @@ -1,137 +0,0 @@ -module('Dropdown - selectOnClose'); - -var $ = require('jquery'); - -var Utils = require('select2/utils'); -var Options = require('select2/options'); - -var SelectData = require('select2/data/select'); - -var Results = require('select2/results'); -var SelectOnClose = require('select2/dropdown/selectOnClose'); - -var ModifiedResults = Utils.Decorate(Results, SelectOnClose); - -var options = new Options({ - selectOnClose: true -}); - -test('will not trigger if no results were given', function (assert) { - assert.expect(0); - - var $element = $(''); - var select = new ModifiedResults($element, options, new SelectData($element)); - - var $dropdown = select.render(); - - var container = new MockContainer(); - select.bind(container, $('
        ')); - - select.on('select', function () { - assert.ok(false, 'The select event should not have been triggered'); - }); - - container.trigger('close'); -}); - -test('will not trigger if the results list is empty', function (assert) { - assert.expect(1); - - var $element = $(''); - var select = new ModifiedResults($element, options, new SelectData($element)); - - var $dropdown = select.render(); - - var container = new MockContainer(); - select.bind(container, $('
        ')); - - select.on('select', function () { - assert.ok(false, 'The select event should not have been triggered'); - }); - - select.append({ - results: [] - }); - - assert.equal( - $dropdown.find('li').length, - 0, - 'There should not be any results in the dropdown' - ); - - container.trigger('close'); -}); - -test('will not trigger if no results here highlighted', function (assert) { - assert.expect(2); - - var $element = $(''); - var select = new ModifiedResults($element, options, new SelectData($element)); - - var $dropdown = select.render(); - - var container = new MockContainer(); - select.bind(container, $('
        ')); - - select.on('select', function () { - assert.ok(false, 'The select event should not have been triggered'); - }); - - select.append({ - results: [ - { - id: '1', - text: 'Test' - } - ] - }); - - assert.equal( - $dropdown.find('li').length, - 1, - 'There should be one result in the dropdown' - ); - - assert.equal( - $.trim($dropdown.find('li').text()), - 'Test', - 'The result should be the same as the one we appended' - ); - - container.trigger('close'); -}); - -test('will trigger if there is a highlighted result', function (assert) { - assert.expect(2); - - var $element = $(''); - var select = new ModifiedResults($element, options, new SelectData($element)); - - var $dropdown = select.render(); - - var container = new MockContainer(); - select.bind(container, $('
        ')); - - select.on('select', function () { - assert.ok(true, 'The select event should have been triggered'); - }); - - select.append({ - results: [ - { - id: '1', - text: 'Test' - } - ] - }); - - assert.equal( - $dropdown.find('li').length, - 1, - 'There should be one result in the dropdown' - ); - - $dropdown.find('li').addClass('select2-results__option--highlighted'); - - container.trigger('close'); -}); diff --git a/static/vendor/select2/tests/dropdown/stopPropagation-tests.js b/static/vendor/select2/tests/dropdown/stopPropagation-tests.js deleted file mode 100644 index 240a3e66..00000000 --- a/static/vendor/select2/tests/dropdown/stopPropagation-tests.js +++ /dev/null @@ -1,33 +0,0 @@ -module('Dropdown - Stoping event propagation'); - -var Dropdown = require('select2/dropdown'); -var StopPropagation = require('select2/dropdown/stopPropagation'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var CustomDropdown = Utils.Decorate(Dropdown, StopPropagation); - -var options = new Options(); - -test('click event does not propagate', function (assert) { - assert.expect(1); - - var $container = $('#qunit-fixture .event-container'); - var container = new MockContainer(); - - var dropdown = new CustomDropdown($('#qunit-fixture select'), options); - - var $dropdown = dropdown.render(); - dropdown.bind(container, $container); - - $container.append($dropdown); - $container.on('click', function () { - assert.ok(false, 'The click event should have been stopped'); - }); - - $dropdown.trigger('click'); - - assert.ok(true, 'Something went wrong if this failed'); -}); diff --git a/static/vendor/select2/tests/helpers.js b/static/vendor/select2/tests/helpers.js deleted file mode 100644 index 9409c8b6..00000000 --- a/static/vendor/select2/tests/helpers.js +++ /dev/null @@ -1,50 +0,0 @@ -// Restore the require/define -var require = $.fn.select2.amd.require; -var define = $.fn.select2.amd.define; - -// Disable jQuery's binding to $ -jQuery.noConflict(); - -var Utils = require('select2/utils'); - -function MockContainer () { - MockContainer.__super__.constructor.call(this); -} - -Utils.Extend(MockContainer, Utils.Observable); - -MockContainer.prototype.isOpen = function () { - return this.isOpen; -}; - -var log = []; -var testName; - -QUnit.done(function (test_results) { - var tests = []; - for(var i = 0, len = log.length; i < len; i++) { - var details = log[i]; - tests.push({ - name: details.name, - result: details.result, - expected: details.expected, - actual: details.actual, - source: details.source - }); - } - test_results.tests = tests; - - window.global_test_results = test_results; -}); -QUnit.testStart(function(testDetails){ - QUnit.log(function(details){ - if (!details.result) { - details.name = testDetails.name; - log.push(details); - } - }); -}); - -define('qunit', function () { - return QUnit; -}) \ No newline at end of file diff --git a/static/vendor/select2/tests/integration-jq1.html b/static/vendor/select2/tests/integration-jq1.html deleted file mode 100644 index d8d0ef55..00000000 --- a/static/vendor/select2/tests/integration-jq1.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - -
        -
        - - - - - - - - - - - - diff --git a/static/vendor/select2/tests/integration-jq2.html b/static/vendor/select2/tests/integration-jq2.html deleted file mode 100644 index 97f73828..00000000 --- a/static/vendor/select2/tests/integration-jq2.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - -
        -
        - - - - - - - - - - - - diff --git a/static/vendor/select2/tests/integration-jq3.html b/static/vendor/select2/tests/integration-jq3.html deleted file mode 100644 index 2f99fb5b..00000000 --- a/static/vendor/select2/tests/integration-jq3.html +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - -
        -
        - - - - - - - - - - - - diff --git a/static/vendor/select2/tests/integration/dom-changes.js b/static/vendor/select2/tests/integration/dom-changes.js deleted file mode 100644 index 2d89ae2a..00000000 --- a/static/vendor/select2/tests/integration/dom-changes.js +++ /dev/null @@ -1,257 +0,0 @@ -module('DOM integration'); - -test('adding a new unselected option changes nothing', function (assert) { - // Any browsers which support mutation observers will not trigger the event - var expected = 4; - if (window.MutationObserver) { - expected = 2; - } else if (!window.addEventListener) { - expected = 2; - } - - assert.expect(expected); - - var asyncDone = null; - var syncDone = assert.async(); - - if (expected != 2) { - asyncDone = assert.async(); - } - - var $ = require('jquery'); - var Options = require('select2/options'); - var Select2 = require('select2/core'); - - var $select = $( - '' - ); - - $('#qunit-fixture').append($select); - - var select = new Select2($select); - - select.on('selection:update', function (args) { - assert.equal( - args.data.length, - 1, - 'There was more than one selection' - ); - - assert.equal( - args.data[0].id, - 'One', - 'The selection changed to something other than One' - ); - - if (expected != 2) { - asyncDone(); - } - }); - - assert.equal( - $select.val(), - 'One' - ); - - var $option = $(''); - - $select.append($option); - - assert.equal( - $select.val(), - 'One' - ); - - syncDone(); -}); - -test('adding a new selected option changes the value', function (assert) { - // handle IE 8 not being supported - var expected = 4; - if (!window.MutationObserver && !window.addEventListener) { - expected = 2; - } - - assert.expect(expected); - - var asyncDone = null; - var syncDone = assert.async(); - - if (expected != 2) { - asyncDone = assert.async(); - } - - var $ = require('jquery'); - var Options = require('select2/options'); - var Select2 = require('select2/core'); - - var $select = $( - '' - ); - - $('#qunit-fixture').append($select); - - var select = new Select2($select); - - select.on('selection:update', function (args) { - assert.equal( - args.data.length, - 1, - 'There was more than one selection' - ); - - assert.equal( - args.data[0].id, - 'Three', - 'The selection did not change to Three' - ); - - if (expected != 2) { - asyncDone(); - } - }); - - assert.equal( - $select.val(), - 'One' - ); - - var $option = $(''); - - $select.append($option); - - assert.equal( - $select.val(), - 'Three' - ); - - syncDone(); -}); - -test('removing an unselected option changes nothing', function (assert) { - // Any browsers which support mutation observers will not trigger the event - var expected = 4; - if (!window.MutationObserver && !window.addEventListener) { - expected = 2; - } - - assert.expect(expected); - - var asyncDone = null; - var syncDone = assert.async(); - - if (expected != 2) { - asyncDone = assert.async(); - } - - var $ = require('jquery'); - var Options = require('select2/options'); - var Select2 = require('select2/core'); - - var $select = $( - '' - ); - - $('#qunit-fixture').append($select); - - var select = new Select2($select); - - select.on('selection:update', function (args) { - assert.equal( - args.data.length, - 1, - 'There was more than one selection' - ); - - assert.equal( - args.data[0].id, - 'One', - 'The selection changed to something other than One' - ); - - if (expected != 2) { - asyncDone(); - } - }); - - assert.equal( - $select.val(), - 'One' - ); - - $select.children().eq(1).remove(); - - assert.equal( - $select.val(), - 'One' - ); - - syncDone(); -}); - -test('removing a selected option changes the value', function (assert) { - // handle IE 8 not being supported - var expected = 3; - if (!window.MutationObserver && !window.addEventListener) { - expected = 2; - } - - assert.expect(expected); - - var asyncDone = null; - var syncDone = assert.async(); - - if (expected != 2) { - asyncDone = assert.async(); - } - - var $ = require('jquery'); - var Options = require('select2/options'); - var Select2 = require('select2/core'); - - var $select = $( - '' - ); - - $('#qunit-fixture').append($select); - - var select = new Select2($select); - - select.on('selection:update', function (args) { - assert.equal( - args.data.length, - 1, - 'There was more than one selection' - ); - - if (expected != 2) { - asyncDone(); - } - }); - - assert.equal( - $select.val(), - 'One' - ); - - $select.children().eq(0).remove(); - - assert.equal( - $select.val(), - 'Two' - ); - - syncDone(); -}); \ No newline at end of file diff --git a/static/vendor/select2/tests/integration/jquery-calls.js b/static/vendor/select2/tests/integration/jquery-calls.js deleted file mode 100644 index d7c3926f..00000000 --- a/static/vendor/select2/tests/integration/jquery-calls.js +++ /dev/null @@ -1,98 +0,0 @@ -module('select2(val)'); - -var Utils = require('select2/utils'); - -test('multiple elements with arguments works', function (assert) { - var $ = require('jquery'); - require('jquery.select2'); - - var $first = $( - '' - ); - var $second = $first.clone(); - - var $both = $first.add($second); - $both.select2(); - - $both.select2('val', '2'); - - assert.equal( - $first.val(), - '2', - 'The call should change the value on the first element' - ); - assert.equal( - $second.val(), - '2', - 'The call should also change the value on the second element' - ); -}); - -test('initializes when jQuery $.data contains' + - ' cyclic reference', function (assert) { - var $ = require('jquery'); - require('jquery.select2'); - - var $select = $( - '' - ); - - // Add a circular reference object using jQuery. - var recursiveObject = {}; - - recursiveObject.same = recursiveObject; - - $select.data('same', recursiveObject); - - $select.select2(); - - assert.equal( - $select.val(), - '3', - 'The option value should be pulled correctly' - ); -}); - -test('$element.data returns instance and options correctly', - function (assert) { - var $ = require('jquery'); - require('jquery.select2'); - - var $select = $( - '' - ); - - // Initialize. - $select.select2({maximumSelectionLength: 2, multiple: true}); - - assert.equal( - $select.val(), - '3', - 'Only 1 option should be pulled.' - ); - - // Try to resolve instance via .data('select2'). - var $instance = $select.data('select2'); - assert.ok($instance); - assert.ok($instance.options); - - // Ensure $select.data('select2') is the same instance - // created by .select2() - assert.equal($instance, Utils.GetData($instance.$element[0], - 'select2')); - - // Ensure initialized property matches. - assert.equal($instance.options.options.maximumSelectionLength, - 2); -}); \ No newline at end of file diff --git a/static/vendor/select2/tests/integration/select2-methods.js b/static/vendor/select2/tests/integration/select2-methods.js deleted file mode 100644 index b1e344a5..00000000 --- a/static/vendor/select2/tests/integration/select2-methods.js +++ /dev/null @@ -1,139 +0,0 @@ -module('select2(data)'); - -var $ = require('jquery'); -var Select2 = require('select2/core'); -var Options = require('select2/options'); - -test('single default selection returned', function (assert) { - var $select = $( - '' - ); - var options = new Options({}); - - var select = new Select2($select, options); - - var items = select.data(); - - assert.equal( - items.length, - 1, - 'The one selected item should be returned' - ); - - var first = items[0]; - - assert.equal( - first.id, - '3', - 'The first option was correct' - ); - - assert.equal( - first.text, - 'Three', - 'The first option was correct' - ); -}); - -test('multiple default selections returned', function (assert) { - var $select = $( - '' - ); - var options = new Options({}); - - var select = new Select2($select, options); - - var items = select.data(); - - assert.equal( - items.length, - 2, - 'The two selected items should be returned' - ); - - var first = items[0]; - - assert.equal( - first.id, - 'One', - 'The first option was correct' - ); - - var second = items[1]; - - assert.equal( - second.id, - '3', - 'The option value should be pulled correctly' - ); -}); - -module('select2(val)'); - -test('single value matches jquery value', function (assert) { - var $select = $( - '' - ); - var options = new Options({}); - - var select = new Select2($select, options); - - var value = select.val(); - - assert.equal( - value, - '3', - 'The value should match the option tag attribute' - ); - - assert.equal( - value, - $select.val(), - 'The value should match the jquery value' - ); -}); - -test('multiple value matches the jquery value', function (assert) { - var $select = $( - '' - ); - var options = new Options({}); - - var select = new Select2($select, options); - - var value = select.val(); - - assert.equal( - value.length, - 2, - 'Two options should be selected' - ); - - assert.deepEqual( - value, - ['One', '3'], - 'The values should match the option tag attribute' - ); - - assert.deepEqual( - value, - $select.val(), - 'The values should match the jquery values' - ); -}); diff --git a/static/vendor/select2/tests/options/ajax-tests.js b/static/vendor/select2/tests/options/ajax-tests.js deleted file mode 100644 index eb65e9ef..00000000 --- a/static/vendor/select2/tests/options/ajax-tests.js +++ /dev/null @@ -1,50 +0,0 @@ -module('Defaults - Ajax'); - -test('options are merged recursively with default options', function (assert) { - var defaults = require('select2/defaults'); - - var ajaxDelay = 250; - var ajaxUrl = 'http://www.test.com'; - - var mergedOptions; - - defaults.set('ajax--delay', ajaxDelay); - - mergedOptions = defaults.apply({ - ajax: { - url: ajaxUrl - } - }); - - assert.equal( - mergedOptions.ajax.delay, - ajaxDelay, - 'Ajax default options are present on the merged options' - ); - - assert.equal( - mergedOptions.ajax.url, - ajaxUrl, - 'Ajax provided options are present on the merged options' - ); - - defaults.reset(); -}); - -test('more than one default option can be changed via set()', function(assert) { - var defaults = require('select2/defaults'); - var ajaxDelay = 123; - var dataDataType = 'xml'; - defaults.set('ajax--delay', ajaxDelay); - defaults.set('ajax--data-type', dataDataType); - - assert.equal( - defaults.defaults.ajax.delay, - ajaxDelay, - 'Both ajax.delay and ajax.dataType present in defaults'); - assert.equal( - defaults.defaults.ajax.dataType, - dataDataType, - 'Both ajax.delay and ajax.dataType present in defaults'); - defaults.reset(); -}); diff --git a/static/vendor/select2/tests/options/data-tests.js b/static/vendor/select2/tests/options/data-tests.js deleted file mode 100644 index 107a2f0c..00000000 --- a/static/vendor/select2/tests/options/data-tests.js +++ /dev/null @@ -1,44 +0,0 @@ -module('Options - Attributes'); - -var $ = require('jquery'); - -var Options = require('select2/options'); - -test('no nesting', function (assert) { - var $test = $(''); - - var options = new Options({}, $test); - - assert.equal(options.get('test'), 'test'); -}); - -test('with nesting', function (assert) { - var $test = $(''); - - if ($test[0].dataset == null) { - assert.ok( - true, - 'We can not run this test with jQuery 1.x if dataset is not implemented' - ); - - return; - } - - var options = new Options({}, $test); - - assert.ok(!(options.get('first-Second'))); - assert.equal(options.get('first').second, 'test'); -}); - -test('overrides initialized data', function (assert) { - var $test = $(''); - - var options = new Options({ - options: 'yes', - override: 'no' - }, $test); - - assert.equal(options.get('options'), 'yes'); - assert.equal(options.get('override'), 'yes'); - assert.equal(options.get('data'), 'yes'); -}); diff --git a/static/vendor/select2/tests/options/deprecated-tests.js b/static/vendor/select2/tests/options/deprecated-tests.js deleted file mode 100644 index a51bba31..00000000 --- a/static/vendor/select2/tests/options/deprecated-tests.js +++ /dev/null @@ -1,250 +0,0 @@ -module('Options - Deprecated - initSelection'); - -var $ = require('jquery'); -var Options = require('select2/options'); - -test('converted into dataAdapter.current', function (assert) { - assert.expect(5); - - var $test = $(''); - var called = false; - - var options = new Options({ - initSelection: function ($element, callback) { - called = true; - - callback([{ - id: '1', - text: '2' - }]); - } - }, $test); - - assert.ok(!called, 'initSelection should not have been called'); - - var DataAdapter = options.get('dataAdapter'); - var data = new DataAdapter($test, options); - - data.current(function (data) { - assert.equal( - data.length, - 1, - 'There should have only been one object selected' - ); - - var item = data[0]; - - assert.equal( - item.id, - '1', - 'The id should have been set by initSelection' - ); - - assert.equal( - item.text, - '2', - 'The text should have been set by initSelection' - ); - }); - - assert.ok(called, 'initSelection should have been called'); -}); - -test('single option converted to array automatically', function (assert) { - assert.expect(2); - - var $test = $(''); - var called = false; - - var options = new Options({ - initSelection: function ($element, callback) { - called = true; - - callback({ - id: '1', - text: '2' - }); - } - }, $test); - - var DataAdapter = options.get('dataAdapter'); - var data = new DataAdapter($test, options); - - data.current(function (data) { - assert.ok( - $.isArray(data), - 'The data should have been converted to an array' - ); - }); - - assert.ok(called, 'initSelection should have been called'); -}); - -test('only called once', function (assert) { - assert.expect(8); - - var $test = $(''); - var called = 0; - - var options = new Options({ - initSelection: function ($element, callback) { - called++; - - callback([{ - id: '1', - text: '2' - }]); - } - }, $test); - - var DataAdapter = options.get('dataAdapter'); - var data = new DataAdapter($test, options); - - data.current(function (data) { - assert.equal( - data.length, - 1, - 'There should have only been a single option' - ); - - var item = data[0]; - - assert.equal( - item.id, - '1', - 'The id should match the one given by initSelection' - ); - - assert.equal( - item.text, - '2', - 'The text should match the one given by initSelection' - ); - }); - - assert.equal( - called, - 1, - 'initSelection should have been called' - ); - - data.current(function (data) { - assert.equal( - data.length, - 1, - 'There should have only been a single option' - ); - - var item = data[0]; - - assert.equal( - item.id, - '3', - 'The id should match the value given in the DOM' - ); - - assert.equal( - item.text, - '4', - 'The text should match the text given in the DOM' - ); - }); - - assert.equal( - called, - 1, - 'initSelection should have only been called once' - ); -}); - -module('Options - Deprecated - query'); - -test('converted into dataAdapter.query automatically', function (assert) { - assert.expect(6); - - var $test = $(''); - var called = false; - - var options = new Options({ - query: function (params) { - called = true; - - params.callback({ - results: [ - { - id: 'test', - text: params.term - } - ] - }); - } - }, $test); - - assert.ok(!called, 'The query option should not have been called'); - - var DataAdapter = options.get('dataAdapter'); - var data = new DataAdapter($test, options); - - data.query({ - term: 'term' - }, function (data) { - assert.ok( - 'results' in data, - 'It should have included the results key' - ); - - assert.equal( - data.results.length, - 1, - 'There should have only been a single result returned' - ); - - var item = data.results[0]; - - assert.equal( - item.id, - 'test', - 'The id should have been returned from the query function' - ); - - assert.equal( - item.text, - 'term', - 'The text should have matched the term that was passed in' - ); - }); - - assert.ok(called, 'The query function should have been called'); -}); - -module('Options - deprecated - data-ajax-url'); - -test('converted ajax-url to ajax--url automatically', function (assert) { - var $test = $(''); - var options = new Options({}, $test); - - assert.ok( - options.get('ajax'), - 'The `ajax` key was automatically created' - ); - assert.equal( - options.get('ajax').url, - 'test://url', - 'The `url` property for the `ajax` option was filled in correctly' - ); -}); - -test('converted select2-tags to data/tags automatically', function (assert) { - var $test = $(''); - var options = new Options({}, $test); - - assert.ok( - options.get('tags'), - 'The `tags` key is automatically set to true' - ); - assert.equal( - options.get('data'), - 'original data', - 'The `data` key is created with the original data' - ); -}); diff --git a/static/vendor/select2/tests/options/translation-tests.js b/static/vendor/select2/tests/options/translation-tests.js deleted file mode 100644 index ab433b61..00000000 --- a/static/vendor/select2/tests/options/translation-tests.js +++ /dev/null @@ -1,28 +0,0 @@ -module('Options - Translations'); - -var $ = require('jquery'); -var Options = require('select2/options'); - -test('partial dictionaries can be passed', function (assert) { - var options = new Options({ - language: { - searching: function () { - return 'Something'; - } - } - }); - - var translations = options.get('translations'); - - assert.equal( - translations.get('searching')(), - 'Something', - 'The partial dictionary still overrides translations' - ); - - assert.equal( - translations.get('noResults')(), - 'No results found', - 'You can still get English translations for keys not passed in' - ); -}); diff --git a/static/vendor/select2/tests/options/width-tests.js b/static/vendor/select2/tests/options/width-tests.js deleted file mode 100644 index e7240349..00000000 --- a/static/vendor/select2/tests/options/width-tests.js +++ /dev/null @@ -1,66 +0,0 @@ -module('Options - Width'); - -var $ = require('jquery'); - -var Select2 = require('select2/core'); -var select = new Select2($('')); - -test('string passed as width', function (assert) { - var $test = $(''); - - var width = select._resolveWidth($test, '80%'); - - assert.equal(width, '80%'); -}); - -test('width from style attribute', function (assert) { - var $test = $(''); - - var width = select._resolveWidth($test, 'style'); - - assert.equal(width, null); -}); - -test('width from computed element width', function (assert) { - var $style = $( - '' - ); - var $test = $(''); - - $('#qunit-fixture').append($style); - $('#qunit-fixture').append($test); - - var width = select._resolveWidth($test, 'element'); - - assert.equal(width, '500px'); -}); - -test('resolve gets the style if it is there', function (assert) { - var $test = $(''); - - $('#qunit-fixture').append($style); - $('#qunit-fixture').append($test); - - var width = select._resolveWidth($test, 'resolve'); - - assert.equal(width, '500px'); -}); diff --git a/static/vendor/select2/tests/results/focusing-tests.js b/static/vendor/select2/tests/results/focusing-tests.js deleted file mode 100644 index 08a2ef3c..00000000 --- a/static/vendor/select2/tests/results/focusing-tests.js +++ /dev/null @@ -1,241 +0,0 @@ -module('Results - highlighting results'); - -test('results:all with no data skips results:focus', function (assert) { - assert.expect(0); - - var $ = require('jquery'); - - var $select = $(''); - var $parent = $('
        '); - - var $container = $(''); - var container = new MockContainer(); - - $parent.appendTo($('#qunit-fixture')); - $select.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Results = require('select2/results'); - - var results = new Results($select, new Options({})); - - // Fake the data adapter for the `setClasses` method - results.data = {}; - results.data.current = function (callback) { - callback([{ id: 'test' }]); - }; - - results.render(); - - results.bind(container, $container); - - results.on('results:focus', function (params) { - assert.ok(false, 'The results:focus event was triggered'); - }); - - container.trigger('results:all', { - data: { - results: [] - } - }); -}); - -test('results:all triggers results:focus on the first item', function (assert) { - assert.expect(2); - - var $ = require('jquery'); - - var $select = $(''); - var $parent = $('
        '); - - var $container = $(''); - var container = new MockContainer(); - - $parent.appendTo($('#qunit-fixture')); - $select.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Results = require('select2/results'); - - var results = new Results($select, new Options({})); - - // Fake the data adapter for the `setClasses` method - results.data = {}; - results.data.current = function (callback) { - callback([{ id: 'test' }]); - }; - - results.render(); - - results.bind(container, $container); - - results.on('results:focus', function (params) { - assert.equal(params.data.id, 'test'); - assert.equal(params.data.text, 'Test'); - }); - - container.trigger('results:all', { - data: { - results: [ - { - id: 'test', - text: 'Test' - } - ] - } - }); -}); - -test('results:append does not trigger results:focus', function (assert) { - assert.expect(0); - - var $ = require('jquery'); - - var $select = $(''); - var $parent = $('
        '); - - var $container = $(''); - var container = new MockContainer(); - - $parent.appendTo($('#qunit-fixture')); - $select.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Results = require('select2/results'); - - var results = new Results($select, new Options({})); - - // Fake the data adapter for the `setClasses` method - results.data = {}; - results.data.current = function (callback) { - callback([{ id: 'test' }]); - }; - - results.render(); - - results.bind(container, $container); - - results.on('results:focus', function () { - assert.ok(false, 'The results:focus event was triggered'); - }); - - container.trigger('results:append', { - data: { - results: [ - { - id: 'test', - text: 'Test' - } - ] - } - }); -}); - -test('scrollAfterSelect triggers results:focus', function (assert) { - assert.expect(3); - - var $ = require('jquery'); - - var $select = $(''); - var $parent = $('
        '); - - var $container = $(''); - var container = new MockContainer(); - - $parent.appendTo($('#qunit-fixture')); - $select.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Results = require('select2/results'); - - var options = new Options({ scrollAfterSelect: true }); - var results = new Results($select, options); - - // Fake the data adapter for the `setClasses` method - results.data = {}; - results.data.current = function (callback) { - callback([{ id: 'test' }]); - }; - - results.render(); - - results.bind(container, $container); - - // check that default for scrollAfterSelect is true - assert.equal(options.get('scrollAfterSelect'), true); - - results.append({ - results: [ - { - id: 'test', - text: 'Test' - } - ] - }); - - results.on('results:focus', function (params) { - assert.equal(params.data.id, 'test'); - assert.equal(params.data.text, 'Test'); - }); - - container.trigger('select', {}); -}); - -test('!scrollAfterSelect does not trigger results:focus', function (assert) { - assert.expect(1); - - var $ = require('jquery'); - - var $select = $(''); - var $parent = $('
        '); - - var $container = $(''); - var container = new MockContainer(); - - $parent.appendTo($('#qunit-fixture')); - $select.appendTo($parent); - - var Utils = require('select2/utils'); - var Options = require('select2/options'); - - var Results = require('select2/results'); - - var options = new Options({ scrollAfterSelect: false }); - var results = new Results($select, options); - - // Fake the data adapter for the `setClasses` method - results.data = {}; - results.data.current = function (callback) { - callback([{ id: 'test' }]); - }; - - results.render(); - - results.bind(container, $container); - - // check that default for scrollAfterSelect is false - assert.equal(options.get('scrollAfterSelect'), false); - - results.append({ - results: [ - { - id: 'test', - text: 'Test' - } - ] - }); - - results.on('results:focus', function () { - assert.ok(false, 'The results:focus event was triggered'); - }); - - container.trigger('select', {}); -}); diff --git a/static/vendor/select2/tests/selection/allowClear-tests.js b/static/vendor/select2/tests/selection/allowClear-tests.js deleted file mode 100644 index a7ca726d..00000000 --- a/static/vendor/select2/tests/selection/allowClear-tests.js +++ /dev/null @@ -1,330 +0,0 @@ -module('Selection containers - Placeholders - Allow clear'); - -var Placeholder = require('select2/selection/placeholder'); -var AllowClear = require('select2/selection/allowClear'); - -var SingleSelection = require('select2/selection/single'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var AllowClearPlaceholder = Utils.Decorate( - Utils.Decorate(SingleSelection, Placeholder), - AllowClear -); - -var allowClearOptions = new Options({ - placeholder: { - id: 'placeholder', - text: 'This is the placeholder' - }, - allowClear: true -}); - -test('clear is not displayed for single placeholder', function (assert) { - var selection = new AllowClearPlaceholder( - $('#qunit-fixture .single-with-placeholder'), - allowClearOptions - ); - - var $selection = selection.render(); - - selection.update([{ - id: 'placeholder' - }]); - - assert.equal( - $selection.find('.select2-selection__clear').length, - 0, - 'The clear icon should not be displayed' - ); -}); - -test('clear is not displayed for multiple placeholder', function (assert) { - var selection = new AllowClearPlaceholder( - $('#qunit-fixture .multiple'), - allowClearOptions - ); - - var $selection = selection.render(); - - selection.update([]); - - assert.equal( - $selection.find('.select2-selection__clear').length, - 0, - 'The clear icon should not be displayed' - ); -}); - - -test('clear is displayed for placeholder', function (assert) { - var selection = new AllowClearPlaceholder( - $('#qunit-fixture .single-with-placeholder'), - allowClearOptions - ); - - var $selection = selection.render(); - - selection.update([{ - id: 'one', - test: 'one' - }]); - - assert.equal( - $selection.find('.select2-selection__clear').length, - 1, - 'The clear icon should be displayed' - ); -}); - -test('clear icon should have title displayed', function (assert) { - var selection = new AllowClearPlaceholder( - $('#qunit-fixture .single-with-placeholder'), - allowClearOptions - ); - - var $selection = selection.render(); - - selection.update([{ - id: 'one', - test: 'one' - }]); - - assert.equal( - $selection.find('.select2-selection__clear').attr('title'), - 'Remove all items', - 'The clear icon should have title displayed' - ); -}); - -test('clicking clear will set the placeholder value', function (assert) { - var $element = $('#qunit-fixture .single-with-placeholder'); - - var selection = new AllowClearPlaceholder( - $element, - allowClearOptions - ); - var container = new MockContainer(); - - var $selection = selection.render(); - - selection.bind(container, $('
        ')); - - $element.val('One'); - selection.update([{ - id: 'One', - text: 'One' - }]); - - var $remove = $selection.find('.select2-selection__clear'); - $remove.trigger('mousedown'); - - assert.equal( - $element.val(), - 'placeholder', - 'The value should have been reset to the placeholder' - ); -}); - -test('clicking clear will trigger the unselect event', function (assert) { - assert.expect(4); - - var $element = $('#qunit-fixture .single-with-placeholder'); - - var selection = new AllowClearPlaceholder( - $element, - allowClearOptions - ); - var container = new MockContainer(); - - var $selection = selection.render(); - - selection.bind(container, $('
        ')); - - $element.val('One'); - selection.update([{ - id: 'One', - text: 'One' - }]); - - selection.on('unselect', function (ev) { - assert.ok( - 'data' in ev && ev.data, - 'The event should have been triggered with the data property' - ); - - assert.ok( - $.isPlainObject(ev.data), - 'The data should be an object' - ); - - assert.equal( - ev.data.id, - 'One', - 'The data should be the unselected object' - ); - - assert.equal( - $element.val(), - 'placeholder', - 'The previous value should be unselected' - ); - }); - - var $remove = $selection.find('.select2-selection__clear'); - $remove.trigger('mousedown'); -}); - -test('preventing the unselect event cancels the clearing', function (assert) { - var $element = $('#qunit-fixture .single-with-placeholder'); - - var selection = new AllowClearPlaceholder( - $element, - allowClearOptions - ); - var container = new MockContainer(); - - var $selection = selection.render(); - - selection.bind(container, $('
        ')); - - $element.val('One'); - selection.update([{ - id: 'One', - text: 'One' - }]); - - selection.on('unselect', function (ev) { - ev.prevented = true; - }); - - var $remove = $selection.find('.select2-selection__clear'); - $remove.trigger('mousedown'); - - assert.equal( - $element.val(), - 'One', - 'The placeholder should not have been set' - ); -}); - -test('clicking clear will trigger the clear event', function (assert) { - assert.expect(5); - - var $element = $('#qunit-fixture .single-with-placeholder'); - - var selection = new AllowClearPlaceholder( - $element, - allowClearOptions - ); - var container = new MockContainer(); - - var $selection = selection.render(); - - selection.bind(container, $('
        ')); - - $element.val('One'); - selection.update([{ - id: 'One', - text: 'One' - }]); - - selection.on('clear', function (ev) { - assert.ok( - 'data' in ev && ev.data, - 'The event should have been triggered with the data property' - ); - - assert.ok( - $.isArray(ev.data), - 'The data should be an array' - ); - - assert.equal( - ev.data.length, - 1, - 'The data should contain one item for each value' - ); - - assert.equal( - ev.data[0].id, - 'One', - 'The data should contain unselected objects' - ); - - assert.equal( - $element.val(), - 'placeholder', - 'The previous value should be unselected' - ); - }); - - var $remove = $selection.find('.select2-selection__clear'); - $remove.trigger('mousedown'); -}); - -test('preventing the clear event cancels the clearing', function (assert) { - var $element = $('#qunit-fixture .single-with-placeholder'); - - var selection = new AllowClearPlaceholder( - $element, - allowClearOptions - ); - var container = new MockContainer(); - - var $selection = selection.render(); - - selection.bind(container, $('
        ')); - - $element.val('One'); - selection.update([{ - id: 'One', - text: 'One' - }]); - - selection.on('clear', function (ev) { - ev.prevented = true; - }); - - var $remove = $selection.find('.select2-selection__clear'); - $remove.trigger('mousedown'); - - assert.equal( - $element.val(), - 'One', - 'The placeholder should not have been set' - ); -}); - -test('clear does not work when disabled', function (assert) { - var $element = $('#qunit-fixture .single-with-placeholder'); - - var selection = new AllowClearPlaceholder( - $element, - allowClearOptions - ); - var container = new MockContainer(); - - var $selection = selection.render(); - - selection.bind(container, $('
        ')); - - selection.update([{ - id: 'One', - text: 'One' - }]); - - $element.val('One'); - selection.options.set('disabled', true); - - var $remove = $selection.find('.select2-selection__clear'); - $remove.trigger('mousedown'); - - assert.equal( - $element.val(), - 'One', - 'The placeholder should not have been set' - ); -}); diff --git a/static/vendor/select2/tests/selection/containerCss-tests.js b/static/vendor/select2/tests/selection/containerCss-tests.js deleted file mode 100644 index 522703a2..00000000 --- a/static/vendor/select2/tests/selection/containerCss-tests.js +++ /dev/null @@ -1,104 +0,0 @@ -module('Dropdown - containerCssClass compatibility'); - -var $ = require('jquery'); -var Utils = require('select2/utils'); -var Options = require('select2/options'); - -var SingleSelection = require('select2/selection/single'); -var ContainerCSS = Utils.Decorate( - SingleSelection, - require('select2/compat/containerCss') -); - -test('all classes will be copied if :all: is used', function (assert) { - var $element = $(''); - var options = new Options({ - containerCssClass: ':all:' - }); - - var select = new ContainerCSS($element, options); - var $container = select.render(); - - assert.ok($container.hasClass('test')); - assert.ok($container.hasClass('copy')); - assert.ok($container.hasClass('works')); - assert.ok(!$container.hasClass(':all:')); -}); - -test(':all: can be used with other classes', function (assert) { - var $element = $(''); - var options = new Options({ - containerCssClass: ':all: other' - }); - - var select = new ContainerCSS($element, options); - var $container = select.render(); - - assert.ok($container.hasClass('test')); - assert.ok($container.hasClass('copy')); - assert.ok($container.hasClass('works')); - assert.ok($container.hasClass('other')); - assert.ok(!$container.hasClass(':all:')); -}); - -test('classes can be passed in as a string', function (assert) { - var $element = $(''); - var options = new Options({ - containerCssClass: 'other' - }); - - var select = new ContainerCSS($element, options); - var $container = select.render(); - - assert.ok($container.hasClass('other')); -}); - -test('a function can be used based on the element', function (assert){ - var $element = $(''); - var options = new Options({ - containerCssClass: function ($element) { - return 'function'; - } - }); - - var select = new ContainerCSS($element, options); - var $container = select.render(); - - assert.ok($container.hasClass('function')); - assert.ok(!$container.hasClass('test')); -}); - -test(':all: works around custom adapters', function (assert) { - var $element = $(''); - var options = new Options({ - containerCssClass: ':all: something', - adaptContainerCssClass: function (clazz) { - return clazz + '-modified'; - } - }); - - var select = new ContainerCSS($element, options); - var $container = select.render(); - - assert.ok($container.hasClass('something')); - - assert.ok($container.hasClass('test')); - assert.ok($container.hasClass('test-modified')); -}); - -module('Selection - adaptContainerCss compatibility'); - -test('only return when adapted', function (assert) { - var $element = $(''); - var options = new Options({ - adaptContainerCssClass: function (clazz) { - return 'modified'; - } - }); - - var select = new ContainerCSS($element, options); - var $container = select.render(); - - assert.ok(!$container.hasClass('original')); - assert.ok($container.hasClass('modified')); -}); diff --git a/static/vendor/select2/tests/selection/multiple-tests.js b/static/vendor/select2/tests/selection/multiple-tests.js deleted file mode 100644 index 5e996ea6..00000000 --- a/static/vendor/select2/tests/selection/multiple-tests.js +++ /dev/null @@ -1,151 +0,0 @@ -module('Selection containers - Multiple'); - -var MultipleSelection = require('select2/selection/multiple'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var options = new Options({}); - -test('display uses templateSelection', function (assert) { - var called = false; - - var templateOptions = new Options({ - templateSelection: function (data) { - called = true; - - return data.text; - } - }); - - var selection = new MultipleSelection( - $('#qunit-fixture .multiple'), - templateOptions - ); - - var out = selection.display({ - text: 'test' - }); - - assert.ok(called); - - assert.equal(out, 'test'); -}); - -test('templateSelection can addClass', function (assert) { - var called = false; - - var templateOptions = new Options({ - templateSelection: function (data, container) { - called = true; - container.addClass('testclass'); - return data.text; - } - }); - - var selection = new MultipleSelection( - $('#qunit-fixture .multiple'), - templateOptions - ); - - var $container = selection.selectionContainer(); - - var out = selection.display({ - text: 'test' - }, $container); - - assert.ok(called); - - assert.equal(out, 'test'); - - assert.ok($container.hasClass('testclass')); -}); - -test('empty update clears the selection', function (assert) { - var selection = new MultipleSelection( - $('#qunit-fixture .multiple'), - options - ); - - var $selection = selection.render(); - var $rendered = $selection.find('.select2-selection__rendered'); - - $rendered.text('testing'); - $rendered.attr('title', 'testing'); - - selection.update([]); - - assert.equal($rendered.text(), ''); - assert.equal($rendered.attr('title'), undefined); -}); - -test('escapeMarkup is being used', function (assert) { - var selection = new MultipleSelection( - $('#qunit-fixture .multiple'), - options - ); - - var $selection = selection.render(); - var $rendered = $selection.find('.select2-selection__rendered'); - - var unescapedText = ''; - - selection.update([{ - text: unescapedText - }]); - - assert.equal( - $rendered.text().substr(1), - unescapedText, - 'The text should be escaped by default to prevent injection' - ); -}); - -test('clear button respects the disabled state', function (assert) { - var options = new Options({ - disabled: true - }); - - var $select = $('#qunit-fixture .multiple'); - - var container = new MockContainer(); - var $container = $('
        '); - - var selection = new MultipleSelection( - $select, - options - ); - - var $selection = selection.render(); - $container.append($selection); - - selection.bind(container, $container); - - // Select an option - selection.update([{ - text: 'Test' - }]); - - var $rendered = $selection.find('.select2-selection__rendered'); - - var $pill = $rendered.find('.select2-selection__choice'); - - assert.equal($pill.length, 1, 'There should only be one selection'); - - var $remove = $pill.find('.select2-selection__choice__remove'); - - assert.equal( - $remove.length, - 1, - 'The remove icon is displayed for the selection' - ); - - // Set up the unselect handler - selection.on('unselect', function (params) { - assert.ok(false, 'The unselect handler should not be triggered'); - }); - - // Trigger the handler for the remove icon - $remove.trigger('click'); -}); diff --git a/static/vendor/select2/tests/selection/placeholder-tests.js b/static/vendor/select2/tests/selection/placeholder-tests.js deleted file mode 100644 index 8a436ff3..00000000 --- a/static/vendor/select2/tests/selection/placeholder-tests.js +++ /dev/null @@ -1,74 +0,0 @@ -module('Selection containers - Placeholders'); - -var Placeholder = require('select2/selection/placeholder'); -var SingleSelection = require('select2/selection/single'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var SinglePlaceholder = Utils.Decorate(SingleSelection, Placeholder); - -var placeholderOptions = new Options({ - placeholder: { - id: 'placeholder', - text: 'This is the placeholder' - } -}); - -test('normalizing placeholder ignores objects', function (assert) { - var selection = new SinglePlaceholder( - $('#qunit-fixture .single'), - placeholderOptions - ); - - var original = { - id: 'test', - text: 'testing' - }; - - var normalized = selection.normalizePlaceholder(original); - - assert.equal(original, normalized); -}); - -test('normalizing placeholder gives object for string', function (assert) { - var selection = new SinglePlaceholder( - $('#qunit-fixture .single'), - placeholderOptions - ); - - var normalized = selection.normalizePlaceholder('placeholder'); - - assert.equal(normalized.id, ''); - assert.equal(normalized.text, 'placeholder'); -}); - - -test('text is shown for placeholder option on single', function (assert) { - var selection = new SinglePlaceholder( - $('#qunit-fixture .single'), - placeholderOptions - ); - - var $selection = selection.render(); - - selection.update([{ - id: 'placeholder' - }]); - - assert.equal($selection.text(), 'This is the placeholder'); -}); - -test('placeholder is shown when no options are selected', function (assert) { - var selection = new SinglePlaceholder( - $('#qunit-fixture .multiple'), - placeholderOptions - ); - - var $selection = selection.render(); - - selection.update([]); - - assert.equal($selection.text(), 'This is the placeholder'); -}); diff --git a/static/vendor/select2/tests/selection/search-tests.js b/static/vendor/select2/tests/selection/search-tests.js deleted file mode 100644 index 43345d72..00000000 --- a/static/vendor/select2/tests/selection/search-tests.js +++ /dev/null @@ -1,191 +0,0 @@ -module('Selection containers - Inline search'); - -var MultipleSelection = require('select2/selection/multiple'); -var InlineSearch = require('select2/selection/search'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var options = new Options({}); - -test('backspace will remove a choice', function (assert) { - assert.expect(3); - - var KEYS = require('select2/keys'); - - var $container = $('#qunit-fixture .event-container'); - var container = new MockContainer(); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - - var $element = $('#qunit-fixture .multiple'); - var selection = new CustomSelection($element, options); - - var $selection = selection.render(); - selection.bind(container, $container); - - // The unselect event should be triggered at some point - selection.on('unselect', function () { - assert.ok(true, 'A choice was unselected'); - }); - - // Add some selections and render the search - selection.update([ - { - id: '1', - text: 'One' - } - ]); - - var $search = $selection.find('input'); - var $choices = $selection.find('.select2-selection__choice'); - - assert.equal($search.length, 1, 'The search was visible'); - assert.equal($choices.length, 1, 'The choice was rendered'); - - // Trigger the backspace on the search - var backspace = $.Event('keydown', { - which: KEYS.BACKSPACE - }); - $search.trigger(backspace); -}); - -test('backspace will set the search text', function (assert) { - assert.expect(3); - - var KEYS = require('select2/keys'); - - var $container = $('#qunit-fixture .event-container'); - var container = new MockContainer(); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - - var $element = $('#qunit-fixture .multiple'); - var selection = new CustomSelection($element, options); - - var $selection = selection.render(); - selection.bind(container, $container); - - // Add some selections and render the search - selection.update([ - { - id: '1', - text: 'One' - } - ]); - - var $search = $selection.find('input'); - var $choices = $selection.find('.select2-selection__choice'); - - assert.equal($search.length, 1, 'The search was visible'); - assert.equal($choices.length, 1, 'The choice was rendered'); - - // Trigger the backspace on the search - var backspace = $.Event('keydown', { - which: KEYS.BACKSPACE - }); - $search.trigger(backspace); - - assert.equal($search.val(), 'One', 'The search text was set'); -}); - -test('updating selection does not shift the focus', function (assert) { - // Check for IE 8, which triggers a false negative during testing - if (window.attachEvent && !window.addEventListener) { - // We must expect 0 assertions or the test will fail - assert.expect(0); - return; - } - - var $container = $('#qunit-fixture .event-container'); - var container = new MockContainer(); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - - var $element = $('#qunit-fixture .multiple'); - var selection = new CustomSelection($element, options); - - var $selection = selection.render(); - selection.bind(container, $container); - - // Update the selection so the search is rendered - selection.update([]); - - // Make it visible so the browser can place focus on the search - $container.append($selection); - - var $search = $selection.find('input'); - $search.trigger('focus'); - - assert.equal($search.length, 1, 'The search was not visible'); - - assert.equal( - document.activeElement, - $search[0], - 'The search did not have focus originally' - ); - - // Trigger an update, this should redraw the search box - selection.update([]); - - assert.equal($search.length, 1, 'The search box disappeared'); - - assert.equal( - document.activeElement, - $search[0], - 'The search did not have focus after the selection was updated' - ); -}); - -test('the focus event shifts the focus', function (assert) { - // Check for IE 8, which triggers a false negative during testing - if (window.attachEvent && !window.addEventListener) { - // We must expect 0 assertions or the test will fail - assert.expect(0); - return; - } - - var $container = $('#qunit-fixture .event-container'); - var container = new MockContainer(); - - var CustomSelection = Utils.Decorate(MultipleSelection, InlineSearch); - - var $element = $('#qunit-fixture .multiple'); - var selection = new CustomSelection($element, options); - - var $selection = selection.render(); - selection.bind(container, $container); - - // Update the selection so the search is rendered - selection.update([]); - - // Make it visible so the browser can place focus on the search - $container.append($selection); - - // The search should not be automatically focused - - var $search = $selection.find('input'); - - assert.notEqual( - document.activeElement, - $search[0], - 'The search had focus originally' - ); - - assert.equal($search.length, 1, 'The search was not visible'); - - // Focus the container - - container.trigger('focus'); - - // Make sure it focuses the search - - assert.equal($search.length, 1, 'The search box disappeared'); - - assert.equal( - document.activeElement, - $search[0], - 'The search did not have focus originally' - ); -}); \ No newline at end of file diff --git a/static/vendor/select2/tests/selection/single-tests.js b/static/vendor/select2/tests/selection/single-tests.js deleted file mode 100644 index 9ab16367..00000000 --- a/static/vendor/select2/tests/selection/single-tests.js +++ /dev/null @@ -1,119 +0,0 @@ -module('Selection containers - Single'); - -var SingleSelection = require('select2/selection/single'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var options = new Options({}); - -test('display uses templateSelection', function (assert) { - var called = false; - - var templateOptions = new Options({ - templateSelection: function (data) { - called = true; - - return data.text; - } - }); - - var selection = new SingleSelection( - $('#qunit-fixture .single'), - templateOptions - ); - - var out = selection.display({ - text: 'test' - }); - - assert.ok(called); - - assert.equal(out, 'test'); -}); - -test('templateSelection can addClass', function (assert) { - var called = false; - - var templateOptions = new Options({ - templateSelection: function (data, container) { - called = true; - container.addClass('testclass'); - return data.text; - } - }); - - var selection = new SingleSelection( - $('#qunit-fixture .single'), - templateOptions - ); - - var $container = selection.selectionContainer(); - - var out = selection.display({ - text: 'test' - }, $container); - - assert.ok(called); - - assert.equal(out, 'test'); - - assert.ok($container.hasClass('testclass')); -}); - -test('empty update clears the selection', function (assert) { - var selection = new SingleSelection( - $('#qunit-fixture .single'), - options - ); - - var $selection = selection.render(); - var $rendered = $selection.find('.select2-selection__rendered'); - - $rendered.text('testing'); - $rendered.attr('title', 'testing'); - - selection.update([]); - - assert.equal($rendered.text(), ''); - assert.equal($rendered.attr('title'), undefined); -}); - -test('update renders the data text', function (assert) { - var selection = new SingleSelection( - $('#qunit-fixture .single'), - options - ); - - var $selection = selection.render(); - var $rendered = $selection.find('.select2-selection__rendered'); - - selection.update([{ - text: 'test' - }]); - - assert.equal($rendered.text(), 'test'); -}); - -test('escapeMarkup is being used', function (assert) { - var selection = new SingleSelection( - $('#qunit-fixture .single'), - options - ); - - var $selection = selection.render(); - var $rendered = $selection.find('.select2-selection__rendered'); - - var unescapedText = ''; - - selection.update([{ - text: unescapedText - }]); - - assert.equal( - $rendered.text(), - unescapedText, - 'The text should be escaped by default to prevent injection' - ); -}); diff --git a/static/vendor/select2/tests/selection/stopPropagation-tests.js b/static/vendor/select2/tests/selection/stopPropagation-tests.js deleted file mode 100644 index d8d8897b..00000000 --- a/static/vendor/select2/tests/selection/stopPropagation-tests.js +++ /dev/null @@ -1,33 +0,0 @@ -module('Selection containers - Stoping event propagation'); - -var SingleSelection = require('select2/selection/single'); -var StopPropagation = require('select2/selection/stopPropagation'); - -var $ = require('jquery'); -var Options = require('select2/options'); -var Utils = require('select2/utils'); - -var CutomSelection = Utils.Decorate(SingleSelection, StopPropagation); - -var options = new Options(); - -test('click event does not propagate', function (assert) { - assert.expect(1); - - var $container = $('#qunit-fixture .event-container'); - var container = new MockContainer(); - - var selection = new CutomSelection($('#qunit-fixture select'), options); - - var $selection = selection.render(); - selection.bind(container, $container); - - $container.append($selection); - $container.on('click', function () { - assert.ok(false, 'The click event should have been stopped'); - }); - - $selection.trigger('click'); - - assert.ok(true, 'Something went wrong if this failed'); -}); diff --git a/static/vendor/select2/tests/unit-jq1.html b/static/vendor/select2/tests/unit-jq1.html deleted file mode 100644 index 7de1bfa2..00000000 --- a/static/vendor/select2/tests/unit-jq1.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - -
        -
        -
        - -
        - - - - - - - - - - - - - - - - -
        - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/vendor/select2/tests/unit-jq2.html b/static/vendor/select2/tests/unit-jq2.html deleted file mode 100644 index eb7eb12d..00000000 --- a/static/vendor/select2/tests/unit-jq2.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - -
        -
        -
        - -
        - - - - - - - - - - - - - - - - -
        - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/vendor/select2/tests/unit-jq3.html b/static/vendor/select2/tests/unit-jq3.html deleted file mode 100644 index a14fa9e6..00000000 --- a/static/vendor/select2/tests/unit-jq3.html +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - -
        -
        -
        - -
        - - - - - - - - - - - - - - - - -
        - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/vendor/select2/tests/utils/decorator-tests.js b/static/vendor/select2/tests/utils/decorator-tests.js deleted file mode 100644 index 4888991f..00000000 --- a/static/vendor/select2/tests/utils/decorator-tests.js +++ /dev/null @@ -1,189 +0,0 @@ -module('Decorators'); - -var Utils = require('select2/utils'); - -test('overridden - method', function (assert) { - function BaseClass () {} - - BaseClass.prototype.hello = function () { - return 'A'; - }; - - function DecoratorClass () {} - - DecoratorClass.prototype.hello = function () { - return 'B'; - }; - - var DecoratedClass = Utils.Decorate(BaseClass, DecoratorClass); - - var inst = new DecoratedClass(); - - assert.strictEqual(inst.hello(), 'B'); -}); - -test('overridden - constructor', function (assert) { - function BaseClass () { - this.inherited = true; - } - - BaseClass.prototype.hello = function () { - return 'A'; - }; - - function DecoratorClass (decorated) { - this.called = true; - } - - DecoratorClass.prototype.other = function () { - return 'B'; - }; - - var DecoratedClass = Utils.Decorate(BaseClass, DecoratorClass); - - var inst = new DecoratedClass(); - - assert.ok(inst.called); - assert.ok(!inst.inherited); -}); - -test('not overridden - method', function (assert) { - function BaseClass () {} - - BaseClass.prototype.hello = function () { - return 'A'; - }; - - function DecoratorClass () {} - - DecoratorClass.prototype.other = function () { - return 'B'; - }; - - var DecoratedClass = Utils.Decorate(BaseClass, DecoratorClass); - - var inst = new DecoratedClass(); - - assert.strictEqual(inst.hello(), 'A'); -}); - -test('not overridden - constructor', function (assert) { - function BaseClass () { - this.called = true; - } - - BaseClass.prototype.hello = function () { - return 'A'; - }; - - function DecoratorClass () {} - - DecoratorClass.prototype.other = function () { - return 'B'; - }; - - var DecoratedClass = Utils.Decorate(BaseClass, DecoratorClass); - - var inst = new DecoratedClass(); - - assert.ok(inst.called); -}); - -test('inherited - method', function (assert) { - function BaseClass () {} - - BaseClass.prototype.hello = function () { - return 'A'; - }; - - function DecoratorClass (decorated) {} - - DecoratorClass.prototype.hello = function (decorated) { - return 'B' + decorated.call(this) + 'C'; - }; - - var DecoratedClass = Utils.Decorate(BaseClass, DecoratorClass); - - var inst = new DecoratedClass(); - - assert.strictEqual(inst.hello(), 'BAC'); -}); - -test('inherited - constructor', function (assert) { - function BaseClass () { - this.inherited = true; - } - - BaseClass.prototype.hello = function () { - return 'A'; - }; - - function DecoratorClass (decorated) { - this.called = true; - - decorated.call(this); - } - - DecoratorClass.prototype.other = function () { - return 'B'; - }; - - var DecoratedClass = Utils.Decorate(BaseClass, DecoratorClass); - - var inst = new DecoratedClass(); - - assert.ok(inst.called); - assert.ok(inst.inherited); -}); - -test('inherited - three levels', function (assert) { - function BaseClass (testArgument) { - this.baseCalled = true; - this.baseTestArgument = testArgument; - } - - BaseClass.prototype.test = function (a) { - return a + 'c'; - }; - - function MiddleClass (decorated, testArgument) { - this.middleCalled = true; - this.middleTestArgument = testArgument; - - decorated.call(this, testArgument); - } - - MiddleClass.prototype.test = function (decorated, a) { - return decorated.call(this, a + 'b'); - }; - - function DecoratorClass (decorated, testArgument) { - this.decoratorCalled = true; - this.decoratorTestArgument = testArgument; - - decorated.call(this, testArgument); - } - - DecoratorClass.prototype.test = function (decorated, a) { - return decorated.call(this, a + 'a'); - }; - - var DecoratedClass = Utils.Decorate( - Utils.Decorate(BaseClass, MiddleClass), - DecoratorClass - ); - - var inst = new DecoratedClass('test'); - - assert.ok(inst.baseCalled, 'The base class contructor was called'); - assert.ok(inst.middleCalled, 'The middle class constructor was called'); - assert.ok(inst.decoratorCalled, 'The decorator constructor was called'); - - assert.strictEqual(inst.baseTestArgument, 'test'); - assert.strictEqual(inst.middleTestArgument, 'test'); - assert.strictEqual(inst.decoratorTestArgument, 'test'); - - var out = inst.test('test'); - - assert.strictEqual(out, 'testabc'); -}); diff --git a/static/vendor/select2/tests/utils/escapeMarkup-tests.js b/static/vendor/select2/tests/utils/escapeMarkup-tests.js deleted file mode 100644 index 726f09de..00000000 --- a/static/vendor/select2/tests/utils/escapeMarkup-tests.js +++ /dev/null @@ -1,36 +0,0 @@ -module('Utils - escapeMarkup'); - -var Utils = require('select2/utils'); - -test('text passes through', function (assert) { - var text = 'testing this'; - var escaped = Utils.escapeMarkup(text); - - assert.equal(text, escaped); -}); - -test('html tags are escaped', function (assert) { - var text = ''; - var escaped = Utils.escapeMarkup(text); - - assert.notEqual(text, escaped); - assert.equal(escaped.indexOf(' +{% endblock %} diff --git a/templates/activity/activity_entry.html b/templates/activity/activity_entry.html new file mode 100644 index 00000000..a712228d --- /dev/null +++ b/templates/activity/activity_entry.html @@ -0,0 +1,146 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load render_table from django_tables2 %} +{% load pretty_money %} +{% load perms %} + +{% block content %} +
        +
        +
        + + {% trans "Transfer" %} + + {% if "note.notespecial"|not_empty_model_list %} + + {% trans "Credit" %} + + {% endif %} + {% for a in activities_open %} + + {% trans "Entries" %} {{ a.name }} + + {% endfor %} +
        +
        +
        + + + + + + + +
        + +
        +

        {{ entries.count }} {% if entries.count >= 2 %}{% trans "entries" %}{% else %}{% trans "entry" %}{% endif %}

        + {% render_table table %} +
        +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/activity/activity_form.html b/templates/activity/activity_form.html new file mode 100644 index 00000000..99c254e3 --- /dev/null +++ b/templates/activity/activity_form.html @@ -0,0 +1,11 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load crispy_forms_tags %} +{% block content %} +
        +{% csrf_token %} +{{form|crispy}} + +
        +{% endblock %} diff --git a/templates/activity/activity_invite.html b/templates/activity/activity_invite.html new file mode 100644 index 00000000..8bdb1965 --- /dev/null +++ b/templates/activity/activity_invite.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n crispy_forms_tags %} +{% block content %} +
        + {% csrf_token %} + {{ form|crispy }} + +
        +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/activity/activity_list.html b/templates/activity/activity_list.html new file mode 100644 index 00000000..f2871fd6 --- /dev/null +++ b/templates/activity/activity_list.html @@ -0,0 +1,33 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n crispy_forms_tags%} +{% block content %} +

        {% trans "Upcoming activities" %}

        + {% if upcoming.data %} + {% render_table upcoming %} + {% else %} +
        + {% trans "There is no planned activity." %} +
        + {% endif %} + + {% trans 'New activity' %} + +
        + +

        {% trans "All activities" %}

        + + {% render_table table %} +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 6a688fc9..3c2c637f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -84,14 +84,26 @@ SPDX-License-Identifier: GPL-3.0-or-later {% trans 'Transfer' %} {% endif %} + {% if "auth.user"|model_list|length >= 2 %} + + {% endif %} {% if "member.club"|not_empty_model_list %} {% endif %} + {% if "member.change_profile_registration_valid"|has_perm:user %} + + {% endif %} {% if "activity.activity"|not_empty_model_list %} {% endif %} {% if "treasury.invoice"|not_empty_model_change_list %} @@ -119,7 +131,7 @@ SPDX-License-Identifier: GPL-3.0-or-later {% else %} @@ -133,6 +145,11 @@ SPDX-License-Identifier: GPL-3.0-or-later
        + {% if user.is_authenticated and not user.profile.email_confirmed %} +
        + {% trans "Your e-mail address is not validated. Please check your mail inbox and click on the validation link." %} +
        + {% endif %} {% block contenttitle %}

        {{ title }}

        {% endblock %}
        {% block content %} diff --git a/templates/bootstrap_datepicker_plus/date_picker.html b/templates/bootstrap_datepicker_plus/date_picker.html new file mode 100644 index 00000000..67a11df1 --- /dev/null +++ b/templates/bootstrap_datepicker_plus/date_picker.html @@ -0,0 +1,6 @@ +
        + {% include "bootstrap_datepicker_plus/input.html" %} +
        +
        +
        +
        diff --git a/templates/bootstrap_datepicker_plus/input.html b/templates/bootstrap_datepicker_plus/input.html new file mode 100644 index 00000000..b2f8c403 --- /dev/null +++ b/templates/bootstrap_datepicker_plus/input.html @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/templates/bootstrap_datepicker_plus/time_picker.html b/templates/bootstrap_datepicker_plus/time_picker.html new file mode 100644 index 00000000..2bd509a3 --- /dev/null +++ b/templates/bootstrap_datepicker_plus/time_picker.html @@ -0,0 +1,6 @@ +
        + {% include "bootstrap_datepicker_plus/input.html" %} +
        +
        +
        +
        diff --git a/templates/member/add_members.html b/templates/member/add_members.html index 8b57e7d4..ad6f1f29 100644 --- a/templates/member/add_members.html +++ b/templates/member/add_members.html @@ -1,29 +1,55 @@ {% extends "member/noteowner_detail.html" %} {% load crispy_forms_tags %} {% load static %} +{% load i18n %} {% block profile_info %} {% include "member/club_info.html" %} {% endblock %} -{% block profile_content %} +{% block profile_content %}
        {% csrf_token %} - {% crispy formset helper %} -
        - -
        + {{ form|crispy }} +
        {% endblock %} {% block extrajavascript %} - - + {% endblock %} diff --git a/templates/member/autocomplete_model.html b/templates/member/autocomplete_model.html new file mode 100644 index 00000000..2236c6ef --- /dev/null +++ b/templates/member/autocomplete_model.html @@ -0,0 +1,9 @@ + + +
          +
        diff --git a/templates/member/club_detail.html b/templates/member/club_detail.html index 979c0897..fedd43fa 100644 --- a/templates/member/club_detail.html +++ b/templates/member/club_detail.html @@ -7,3 +7,14 @@ {% block profile_content %} {% include "member/club_tables.html" %} {% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/member/club_form.html b/templates/member/club_form.html index 99c254e3..9810ccab 100644 --- a/templates/member/club_form.html +++ b/templates/member/club_form.html @@ -9,3 +9,25 @@ {% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/member/club_info.html b/templates/member/club_info.html index 539d9867..93c76d59 100644 --- a/templates/member/club_info.html +++ b/templates/member/club_info.html @@ -1,4 +1,4 @@ -{% load i18n static pretty_money %} +{% load i18n static pretty_money perms %}

        Club {{ club.name }}

        @@ -13,34 +13,49 @@
        {% trans 'name'|capfirst %}
        {{ club.name}}
        -
        {% trans 'Club Parent'|capfirst %}
        -
        {{ club.parent_club.name}}
        + {% if club.parent_club %} +
        {% trans 'Club Parent'|capfirst %}
        +
        {{ club.parent_club.name}}
        + {% endif %} -
        {% trans 'membership start'|capfirst %}
        -
        {{ club.membership_start }}
        + {% if club.require_memberships %} +
        {% trans 'membership start'|capfirst %}
        +
        {{ club.membership_start }}
        -
        {% trans 'membership end'|capfirst %}
        -
        {{ club.membership_end }}
        +
        {% trans 'membership end'|capfirst %}
        +
        {{ club.membership_end }}
        -
        {% trans 'membership duration'|capfirst %}
        -
        {{ club.membership_duration }}
        +
        {% trans 'membership duration'|capfirst %}
        +
        {{ club.membership_duration }} {% trans "days" %}
        -
        {% trans 'membership fee'|capfirst %}
        -
        {{ club.membership_fee|pretty_money }}
        + {% if club.membership_fee_paid == club.membership_fee_unpaid %} +
        {% trans 'membership fee'|capfirst %}
        +
        {{ club.membership_fee_paid|pretty_money }}
        + {% else %} +
        {% trans 'membership fee (paid students)'|capfirst %}
        +
        {{ club.membership_fee_paid|pretty_money }}
        + +
        {% trans 'membership fee (unpaid students)'|capfirst %}
        +
        {{ club.membership_fee_unpaid|pretty_money }}
        + {% endif %} + {% endif %}
        {% trans 'aliases'|capfirst %}
        {{ object.note.alias_set.all|join:", " }}
        {% trans 'email'|capfirst %}
        -
        {{ club.email}}
        +
        {{ club.email }}
        diff --git a/templates/member/club_list.html b/templates/member/club_list.html index 7f0b02a1..4682164c 100644 --- a/templates/member/club_list.html +++ b/templates/member/club_list.html @@ -9,7 +9,7 @@
        - {% trans "Créer un club" %} + {% trans "Create club" %}
        @@ -36,7 +36,6 @@ function getInfo() { if (asked.length >= 1) { $.getJSON("/api/members/club/?format=json&search="+asked, function(buttons){ let selected_id = buttons.results.map((a => "#row-"+a.id)); - console.log(selected_id.join()); $(".table-row,"+selected_id.join()).show(); $(".table-row").not(selected_id.join()).hide(); diff --git a/templates/member/club_tables.html b/templates/member/club_tables.html index fbded9c3..32be9bd4 100644 --- a/templates/member/club_tables.html +++ b/templates/member/club_tables.html @@ -1,31 +1,23 @@ {% load render_table from django_tables2 %} {% load i18n %} -
        -
        - -
        - {% render_table member_list %} -
        -
        - -
        - -
        -
        - {% render_table history_list %} -
        -
        +
        + + {% render_table member_list %} +
        + +
        + +
        + +
        + {% render_table history_list %} +
        diff --git a/templates/member/noteowner_detail.html b/templates/member/noteowner_detail.html index ad329aee..fc781549 100644 --- a/templates/member/noteowner_detail.html +++ b/templates/member/noteowner_detail.html @@ -19,7 +19,7 @@ {% block extrajavascript %} +{% endblock %} diff --git a/templates/member/profile_info.html b/templates/member/profile_info.html index 9ff20385..74856355 100644 --- a/templates/member/profile_info.html +++ b/templates/member/profile_info.html @@ -44,7 +44,7 @@ diff --git a/templates/member/profile_tables.html b/templates/member/profile_tables.html index 9d2c687f..9629ff14 100644 --- a/templates/member/profile_tables.html +++ b/templates/member/profile_tables.html @@ -1,31 +1,34 @@ {% load render_table from django_tables2 %} {% load i18n %} -
        -
        - -
        - {% render_table club_list %} -
        -
        +{% load perms %} -
        - -
        -
        - {% render_table history_list %} -
        -
        +{% if not object.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:object.profile %} +
        + {% trans "This user doesn't have confirmed his/her e-mail address." %} + {% trans "Click here to resend a validation link." %} +
        +{% endif %} + +
        + + {% render_table club_list %} +
        + +
        + +
        + +
        + {% render_table history_list %}
        diff --git a/templates/member/user_list.html b/templates/member/user_list.html index 821ea619..0bcd7e89 100644 --- a/templates/member/user_list.html +++ b/templates/member/user_list.html @@ -2,28 +2,50 @@ {% load render_table from django_tables2 %} {% load crispy_forms_tags%} {% block content %} + -New User +
        -
        -{% crispy filter.form filter.form.helper %} -
        -
        -
        - {% render_table table %} +
        + {% if table.data %} + {% render_table table %} + {% else %} +
        + {% trans "There is no pending user with this pattern." %} +
        + {% endif %}
        -
        {% endblock %} {% block extrajavascript %} {% endblock %} diff --git a/templates/note/amount_input.html b/templates/note/amount_input.html new file mode 100644 index 00000000..6ef4a53a --- /dev/null +++ b/templates/note/amount_input.html @@ -0,0 +1,11 @@ +
        + +
        + +
        +
        \ No newline at end of file diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html index d2cd85e9..0b53df61 100644 --- a/templates/note/transaction_form.html +++ b/templates/note/transaction_form.html @@ -28,6 +28,11 @@ SPDX-License-Identifier: GPL-2.0-or-later {% trans "Debit" %} {% endif %} + {% for activity in activities_open %} + + {% trans "Entries" %} {{ activity.name }} + + {% endfor %}
        @@ -126,12 +131,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
        -
        - -
        - -
        -
        + {% include "note/amount_input.html" with widget=amount_widget %}
        @@ -142,7 +142,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
        - +
        diff --git a/templates/note/transactiontemplate_list.html b/templates/note/transactiontemplate_list.html index af0a02b4..cf9bc5ed 100644 --- a/templates/note/transactiontemplate_list.html +++ b/templates/note/transactiontemplate_list.html @@ -37,7 +37,6 @@ function getInfo() { if (asked.length >= 1) { $.getJSON("/api/note/transaction/template/?format=json&search="+asked, function(buttons){ let selected_id = buttons.results.map((a => "#row-"+a.id)); - console.log(selected_id.join()); $(".table-row,"+selected_id.join()).show(); $(".table-row").not(selected_id.join()).hide(); diff --git a/templates/registration/email_validation_complete.html b/templates/registration/email_validation_complete.html new file mode 100644 index 00000000..4835cfa1 --- /dev/null +++ b/templates/registration/email_validation_complete.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% load i18n %} + +{% block content %} + {% if validlink %} + {% trans "Your email have successfully been validated." %} + {% if user.profile.registration_valid %} + {% blocktrans %}You can now log in.{% endblocktrans %} + {% else %} + {% trans "You must pay now your membership in the Kfet to complete your registration." %} + {% endif %} + {% else %} + {% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %} + {% endif %} +{% endblock %} diff --git a/templates/registration/email_validation_email_sent.html b/templates/registration/email_validation_email_sent.html new file mode 100644 index 00000000..bd4cf8d8 --- /dev/null +++ b/templates/registration/email_validation_email_sent.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} + +{% block content %} +

        Account Activation

        + +An email has been sent. Please click on the link to activate your account. +{% endblock %} \ No newline at end of file diff --git a/templates/registration/future_profile_detail.html b/templates/registration/future_profile_detail.html new file mode 100644 index 00000000..8c78fb8d --- /dev/null +++ b/templates/registration/future_profile_detail.html @@ -0,0 +1,119 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load crispy_forms_tags %} +{% load perms %} + +{% block content %} +
        +
        +
        +
        +

        {% trans "Account #" %} {{ object.pk }}

        +
        +
        +
        +
        {% trans 'name'|capfirst %}, {% trans 'first name' %}
        +
        {{ object.last_name }} {{ object.first_name }}
        + +
        {% trans 'username'|capfirst %}
        +
        {{ object.username }}
        + +
        {% trans 'email'|capfirst %}
        +
        {{ object.email }}
        + + {% if not object.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:object.profile %} +
        +
        + {% trans "This user doesn't have confirmed his/her e-mail address." %} + {% trans "Click here to resend a validation link." %} +
        +
        + {% endif %} + +
        {% trans 'password'|capfirst %}
        +
        + + {% trans 'Change password' %} + +
        + +
        {% trans 'section'|capfirst %}
        +
        {{ object.profile.section }}
        + +
        {% trans 'address'|capfirst %}
        +
        {{ object.profile.address }}
        + +
        {% trans 'phone number'|capfirst %}
        +
        {{ object.profile.phone_number }}
        + +
        {% trans 'paid'|capfirst %}
        +
        {{ object.profile.paid|yesno }}
        +
        +
        + +
        +
        +
        +
        +
        +
        +

        {% trans "Validate account" %}

        +
        +
        + {% csrf_token %} + {{ form|crispy }} +
        + +
        +
        +
        +
        +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} \ No newline at end of file diff --git a/templates/registration/future_user_list.html b/templates/registration/future_user_list.html new file mode 100644 index 00000000..1e10dcbb --- /dev/null +++ b/templates/registration/future_user_list.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load crispy_forms_tags %} +{% load i18n %} + +{% block content %} + +
        + +
        + +
        + {% if table.data %} + {% render_table table %} + {% else %} +
        + {% trans "There is no pending user with this pattern." %} +
        + {% endif %} +
        +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/registration/mails/email_validation_email.html b/templates/registration/mails/email_validation_email.html new file mode 100644 index 00000000..577c1220 --- /dev/null +++ b/templates/registration/mails/email_validation_email.html @@ -0,0 +1,15 @@ +{% load i18n %} + +{% trans "Hi" %} {{ user.username }}, + +{% trans "You recently registered on the Note Kfet. Please click on the link below to confirm your registration." %} + +https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %} + +{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %} + +{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet." %} + +{% trans "Thanks" %}, + +{% trans "The Note Kfet team." %} diff --git a/templates/member/signup.html b/templates/registration/signup.html similarity index 100% rename from templates/member/signup.html rename to templates/registration/signup.html diff --git a/templates/treasury/invoice_form.html b/templates/treasury/invoice_form.html index 0edcbdcd..2875d410 100644 --- a/templates/treasury/invoice_form.html +++ b/templates/treasury/invoice_form.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load static %} {% load i18n %} -{% load crispy_forms_tags pretty_money %} +{% load crispy_forms_tags %} {% block content %}

        {% trans "Invoices list" %}

        @@ -26,18 +26,8 @@ {% endif %} {{ form.designation }} - {{ form.quantity }} - - {# Use custom input for amount, with the € symbol #} -
        - -
        - -
        -
        - + {{ form.quantity }} + {{ form.amount }} {# These fields are hidden but handled by the formset to link the id and the invoice id #} {{ form.invoice }} {{ form.id }} @@ -64,15 +54,7 @@ {{ formset.empty_form.designation }} {{ formset.empty_form.quantity }} - -
        - -
        - -
        -
        - + {{ formset.empty_form.amount }} {{ formset.empty_form.invoice }} {{ formset.empty_form.id }} diff --git a/tox.ini b/tox.ini index 01bf4edb..73cf0525 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ commands = [flake8] # Ignore too many errors, should be reduced in the future -ignore = D203, W503, E203, I100, I101 +ignore = D203, W503, E203, I100, I101, C901 exclude = .tox, .git,