diff --git a/.env_example b/.env_example index d903b724c0d47ae052c44a9ad4973410096f7d9b..9afdd1380d5fb30479c299a9be7ff40bbf1d6a9b 100644 --- a/.env_example +++ b/.env_example @@ -11,3 +11,5 @@ DJANGO_SETTINGS_MODULE=note_kfet.settings DOMAIN=localhost CONTACT_EMAIL=tresorerie.bde@localhost NOTE_URL=localhost +NOTE_MAIL=notekfet@localhost +WEBMASTER_MAIL=notekfet@localhost diff --git a/README.md b/README.md index 0c0cfb2eb63b04b79950b11259609abeaf4576f1..be27e63ead715c3710eae2941f9b7bc970c7bd4b 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,8 @@ On supposera pour la suite que vous utilisez Debian/Ubuntu sur un serveur tout n DOMAIN=localhost # note.example.com CONTACT_EMAIL=tresorerie.bde@localhost NOTE_URL=localhost # serveur cas note.example.com si auto-hébergé. + NOTE_MAIL=notekfet@localhost # Adresse expéditrice des mails + WEBMASTER_MAIL=notekfet@localhost # Adresse sur laquelle contacter les webmasters de la note Ensuite on (re)bascule dans l'environement virtuel et on lance les migrations diff --git a/apps/activity/forms.py b/apps/activity/forms.py index dcbd3c9d467953c306d1f100feea666d5bff21e6..dced014a12f4bfbf5010449d30468ee70d2c6cc2 100644 --- a/apps/activity/forms.py +++ b/apps/activity/forms.py @@ -4,7 +4,7 @@ from datetime import timedelta, datetime from django import forms from django.contrib.contenttypes.models import ContentType -from django.utils.translation import gettext as _ +from django.utils.translation import gettext_lazy as _ from member.models import Club from note.models import NoteUser, Note from note_kfet.inputs import DateTimePickerInput, Autocomplete diff --git a/apps/activity/models.py b/apps/activity/models.py index 29f04b39f49689e995a533bca3214abd05d8f49f..cab229c40d45c98c896b3ed25bea357586438006 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -139,7 +139,7 @@ class Entry(models.Model): verbose_name = _("entry") verbose_name_plural = _("entries") - def save(self, *args,**kwargs): + 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(): @@ -153,7 +153,7 @@ class Entry(models.Model): if self.note.balance < 0: raise ValidationError(_("The balance is negative.")) - ret = super().save(*args,**kwargs) + ret = super().save(*args, **kwargs) if insert and self.guest: GuestTransaction.objects.create( diff --git a/apps/activity/views.py b/apps/activity/views.py index 14746929d00a2d42b55a9dddaf788f25eb06ca62..ef144460c0cf707d801d8db17ef03f6117945b88 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.contenttypes.models import ContentType from django.db.models import F, Q @@ -45,8 +46,8 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView 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"))) + context['upcoming'] = ActivityTable( + data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))) return context @@ -138,8 +139,14 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): | 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] + .filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")) + if settings.DATABASES[note_qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2': + note_qs = note_qs.distinct('note__pk')[:20] + else: + # SQLite doesn't support distinct fields. For compatibility reason (in dev mode), the note list will only + # have distinct aliases rather than distinct notes with a SQLite DB, but it can fill the result page. + # In production mode, please use PostgreSQL. + note_qs = note_qs.distinct()[:20] for note in note_qs: note.type = "Adhérent" note.activity = activity @@ -153,9 +160,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): 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 + return context diff --git a/apps/api/urls.py b/apps/api/urls.py index 67fdba309267bca584d7869b15d92341cf1ba2db..03d6bd680fbbf34f14e1d43d06557c82daa6c950 100644 --- a/apps/api/urls.py +++ b/apps/api/urls.py @@ -15,6 +15,7 @@ from note.api.urls import register_note_urls from treasury.api.urls import register_treasury_urls from logs.api.urls import register_logs_urls from permission.api.urls import register_permission_urls +from wei.api.urls import register_wei_urls class UserSerializer(serializers.ModelSerializer): @@ -78,6 +79,7 @@ register_note_urls(router, 'note') register_treasury_urls(router, 'treasury') register_permission_urls(router, 'permission') register_logs_urls(router, 'logs') +register_wei_urls(router, 'wei') app_name = 'api' diff --git a/apps/member/forms.py b/apps/member/forms.py index 6fe95f5a1bf37369dbd9ecdc41dc1d773bf62f89..e546d6524e1b82126f31aca923ffd478196baf3b 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -9,7 +9,7 @@ from note.models import NoteSpecial from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput from permission.models import PermissionMask -from .models import Profile, Club, Membership +from .models import Profile, Club, Membership, Role class CustomAuthenticationForm(AuthenticationForm): @@ -25,10 +25,16 @@ class ProfileForm(forms.ModelForm): A form for the extras field provided by the :model:`member.Profile` model. """ + def save(self, commit=True): + if not self.instance.section or (("department" in self.changed_data + or "promotion" in self.changed_data) and "section" not in self.changed_data): + self.instance.section = self.instance.section_generated + return super().save(commit) + class Meta: model = Profile fields = '__all__' - exclude = ('user', 'email_confirmed', 'registration_valid', 'soge', ) + exclude = ('user', 'email_confirmed', 'registration_valid', ) class ClubForm(forms.ModelForm): @@ -50,6 +56,8 @@ class ClubForm(forms.ModelForm): class MembershipForm(forms.ModelForm): + roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all()) + soge = forms.BooleanField( label=_("Inscription paid by Société Générale"), required=False, diff --git a/apps/member/models.py b/apps/member/models.py index 3a0224341ae62baeafe514593fde4a8b61111221..17b8f0442a3d6d01f30dbece8e5991b9b8805017 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -23,18 +23,20 @@ class Profile(models.Model): We do not want to patch the Django Contrib :model:`auth.User`model; so this model add an user profile with additional information. - """ + user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, ) + phone_number = models.CharField( verbose_name=_('phone number'), max_length=50, blank=True, null=True, ) + section = models.CharField( verbose_name=_('section'), help_text=_('e.g. "1A0", "9A♥", "SAPHIRE"'), @@ -42,12 +44,44 @@ class Profile(models.Model): blank=True, null=True, ) + + department = models.CharField( + max_length=8, + verbose_name=_("department"), + choices=[ + ('A0', _("Informatics (A0)")), + ('A1', _("Mathematics (A1)")), + ('A2', _("Physics (A2)")), + ("A'2", _("Applied physics (A'2)")), + ('A''2', _("Chemistry (A''2)")), + ('A3', _("Biology (A3)")), + ('B1234', _("SAPHIRE (B1234)")), + ('B1', _("Mechanics (B1)")), + ('B2', _("Civil engineering (B2)")), + ('B3', _("Mechanical engineering (B3)")), + ('B4', _("EEA (B4)")), + ('C', _("Design (C)")), + ('D2', _("Economy-management (D2)")), + ('D3', _("Social sciences (D3)")), + ('E', _("English (E)")), + ('EXT', _("External (EXT)")), + ] + ) + + promotion = models.PositiveIntegerField( + null=True, + default=datetime.date.today().year, + verbose_name=_("promotion"), + help_text=_("Year of entry to the school (None if not ENS student)"), + ) + address = models.CharField( verbose_name=_('address'), max_length=255, blank=True, null=True, ) + paid = models.BooleanField( verbose_name=_("paid"), help_text=_("Tells if the user receive a salary."), @@ -64,11 +98,29 @@ class Profile(models.Model): 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, - ) + @property + def ens_year(self): + """ + Number of years since the 1st august of the entry year, rounded up. + """ + if self.promotion is None: + return 0 + today = datetime.date.today() + years = today.year - self.promotion + if today.month >= 8: + years += 1 + return years + + @property + def section_generated(self): + return str(self.ens_year) + self.department + + @property + def soge(self): + if "treasury" in settings.INSTALLED_APPS: + from treasury.models import SogeCredit + return SogeCredit.objects.filter(user=self.user, credit_transaction__isnull=False).exists() + return False class Meta: verbose_name = _('user profile') @@ -85,7 +137,7 @@ class Profile(models.Model): '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'), + 'uid': urlsafe_base64_encode(force_bytes(self.user.pk)), }) self.user.email_user(subject, message) @@ -171,6 +223,7 @@ class Club(models.Model): 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._force_save = True self.save(force_update=True) def save(self, force_insert=False, force_update=False, using=None, @@ -220,6 +273,7 @@ class Membership(models.Model): user = models.ForeignKey( User, on_delete=models.PROTECT, + related_name="memberships", verbose_name=_("user"), ) @@ -308,7 +362,20 @@ class Membership(models.Model): reason="Adhésion " + self.club.name, ) transaction._force_save = True - transaction.save(force_insert=True) + print(hasattr(self, '_soge')) + if hasattr(self, '_soge') and "treasury" in settings.INSTALLED_APPS: + # If the soge pays, then the transaction is unvalidated in a first time, then submitted for control + # to treasurers. + transaction.valid = False + from treasury.models import SogeCredit + soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0] + soge_credit.refresh_from_db() + transaction.save(force_insert=True) + transaction.refresh_from_db() + soge_credit.transactions.add(transaction) + soge_credit.save() + else: + 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, ) diff --git a/apps/member/views.py b/apps/member/views.py index 381314b22e61cc2afe452b413981c4d8a6e639be..04742f327410f9b3e521ac7ff717118492a9c345 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -18,7 +18,7 @@ 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.models import Alias, NoteUser, NoteSpecial +from note.models import Alias, NoteUser from note.models.transactions import Transaction, SpecialTransaction from note.tables import HistoryTable, AliasTable from permission.backends import PermissionBackend @@ -128,7 +128,8 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): 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")\ + Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\ + .order_by("-created_at", "-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)) @@ -165,7 +166,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): Q(first_name__iregex=pattern) | Q(last_name__iregex=pattern) | Q(profile__section__iregex=pattern) - | Q(profile__username__iregex="^" + pattern) + | Q(username__iregex="^" + pattern) | Q(note__alias__name__iregex="^" + pattern) | Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern)) ) @@ -314,7 +315,8 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): 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') + .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\ + .order_by('-created_at', '-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 @@ -365,6 +367,15 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): form_class = ClubForm template_name = "member/club_form.html" + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + + # Don't update a WEI club through this view + if "wei" in settings.INSTALLED_APPS: + qs = qs.filter(weiclub=None) + + return qs + def get_success_url(self): return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk}) @@ -396,7 +407,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): 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"]) + .get(pk=self.kwargs["club_pk"], weiclub=None) form.fields['credit_amount'].initial = club.membership_fee_paid form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all() @@ -463,17 +474,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): 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 Société générale pays, then we store that information but the payment must be controlled by treasurers + # later. The membership transaction will be invalidated. 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" + credit_type = None + form.instance._soge = True if credit_type is None: credit_amount = 0 @@ -521,6 +526,13 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): # Now, all is fine, the membership can be created. + if club.name == "BDE": + # When we renew the BDE membership, we update the profile section. + # We could automate that and remove the section field from the Profile model, + # but with this way users can customize their section as they want. + user.profile.section = user.profile.section_generated + user.profile.save() + # 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"): @@ -544,11 +556,11 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): 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() + ret = super().form_valid(form) + # If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the + # Kfet membership. + if soge: kfet = Club.objects.get(name="Kfet") kfet_fee = kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid @@ -560,20 +572,23 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): date_end__gte=datetime.today(), ) - membership = Membership.objects.create( + membership = Membership( 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, ) + membership._soge = True + membership.save() + membership.refresh_from_db() 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) + return ret def get_success_url(self): return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index 5625e5b5ef8b87233bf55d08026f1e174fda1c65..c61ccd420d2f0cf4c2bbf25bcbb55e7e7d39e8ba 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -3,6 +3,8 @@ from rest_framework import serializers from rest_polymorphic.serializers import PolymorphicSerializer +from note_kfet.middlewares import get_current_authenticated_user +from permission.backends import PermissionBackend from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ @@ -97,6 +99,35 @@ class NotePolymorphicSerializer(PolymorphicSerializer): model = Note +class ConsumerSerializer(serializers.ModelSerializer): + """ + REST API Nested Serializer for Consumers. + return Alias, and the note Associated to it in + """ + note = serializers.SerializerMethodField() + + email_confirmed = serializers.SerializerMethodField() + + class Meta: + model = Alias + fields = '__all__' + + def get_note(self, obj): + """ + Display information about the associated note + """ + # If the user has no right to see the note, then we only display the note identifier + if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note): + print(obj.pk) + return NotePolymorphicSerializer().to_representation(obj.note) + return dict(id=obj.id) + + def get_email_confirmed(self, obj): + if isinstance(obj.note, NoteUser): + return obj.note.user.profile.email_confirmed + return True + + class TemplateCategorySerializer(serializers.ModelSerializer): """ REST API Serializer for Transaction templates. diff --git a/apps/note/api/urls.py b/apps/note/api/urls.py index 796a397f746aefd02c2e97b9aaf73bb25454f65a..2f3fd1a966d7418593688fd213b269cf1ca89474 100644 --- a/apps/note/api/urls.py +++ b/apps/note/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 NotePolymorphicViewSet, AliasViewSet, \ +from .views import NotePolymorphicViewSet, AliasViewSet, ConsumerViewSet, \ TemplateCategoryViewSet, TransactionViewSet, TransactionTemplateViewSet @@ -11,6 +11,7 @@ def register_note_urls(router, path): """ router.register(path + '/note', NotePolymorphicViewSet) router.register(path + '/alias', AliasViewSet) + router.register(path + '/consumer', ConsumerViewSet) router.register(path + '/transaction/category', TemplateCategoryViewSet) router.register(path + '/transaction/transaction', TransactionViewSet) diff --git a/apps/note/api/views.py b/apps/note/api/views.py index 23bed1c92114bc592bbe7d915f6e04ceb12c47f1..d5ad81293bd0e59b5fa31a590842a0ec8b5f9f59 100644 --- a/apps/note/api/views.py +++ b/apps/note/api/views.py @@ -10,8 +10,8 @@ from rest_framework.response import Response from rest_framework import status from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet -from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \ - TransactionTemplateSerializer, TransactionPolymorphicSerializer +from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\ + TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer from ..models.notes import Note, Alias from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory @@ -90,6 +90,30 @@ class AliasViewSet(ReadProtectedModelViewSet): return queryset +class ConsumerViewSet(ReadOnlyProtectedModelViewSet): + queryset = Alias.objects.all() + serializer_class = ConsumerSerializer + filter_backends = [SearchFilter, OrderingFilter] + search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ] + ordering_fields = ['name', 'normalized_name'] + + def get_queryset(self): + """ + Parse query and apply filters. + :return: The filtered set of requested aliases + """ + + queryset = super().get_queryset() + + alias = self.request.query_params.get("alias", ".*") + queryset = queryset.filter( + Q(name__regex="^" + alias) + | Q(normalized_name__regex="^" + Alias.normalize(alias)) + | Q(normalized_name__regex="^" + alias.lower())) + + return queryset + + class TemplateCategoryViewSet(ReadProtectedModelViewSet): """ REST API View set. diff --git a/apps/note/forms.py b/apps/note/forms.py index 50f226f2784892f7f5cbe6f26098442d2348db2b..bc479e2065726965a04b1d21bc9b89e0bf5671ae 100644 --- a/apps/note/forms.py +++ b/apps/note/forms.py @@ -4,7 +4,7 @@ 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 note_kfet.inputs import Autocomplete, AmountInput from .models import TransactionTemplate, NoteClub @@ -24,11 +24,6 @@ class TransactionTemplateForm(forms.ModelForm): model = TransactionTemplate fields = '__all__' - # Le champ de destination est remplacé par un champ d'auto-complétion. - # Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion - # et récupère les aliases valides - # Pour force le type d'une note, il faut rajouter le paramètre : - # forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special} widgets = { 'destination': Autocomplete( @@ -41,4 +36,5 @@ class TransactionTemplateForm(forms.ModelForm): 'placeholder': 'Note ...', }, ), + 'amount': AmountInput(), } diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index 80b22b593c884f2ef6fd6fd7f5e3d859d94967a1..c9c6bf699240597070db0b5f31df812eeb586bf2 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -8,6 +8,7 @@ from django.utils.translation import gettext_lazy as _ from polymorphic.models import PolymorphicModel from .notes import Note, NoteClub, NoteSpecial +from ..templatetags.pretty_money import pretty_money """ Defines transactions @@ -198,6 +199,14 @@ class Transaction(PolymorphicModel): self.source.save() self.destination.save() + def delete(self, **kwargs): + """ + Whenever we want to delete a transaction (caution with this), we ensure the transaction is invalid first. + """ + self.valid = False + self.save(**kwargs) + super().delete(**kwargs) + @property def total(self): return self.amount * self.quantity @@ -206,6 +215,10 @@ class Transaction(PolymorphicModel): def type(self): return _('Transfer') + def __str__(self): + return self.__class__.__name__ + " from " + str(self.source) + " to " + str(self.destination) + " of "\ + + pretty_money(self.quantity * self.amount) + ("" if self.valid else " invalid") + class RecurrentTransaction(Transaction): """ @@ -214,8 +227,7 @@ class RecurrentTransaction(Transaction): template = models.ForeignKey( TransactionTemplate, - null=True, - on_delete=models.SET_NULL, + on_delete=models.PROTECT, ) category = models.ForeignKey( TemplateCategory, diff --git a/apps/note/signals.py b/apps/note/signals.py index 37737a45d7fe671dda9ce214af24820b6292872e..0baa39e6243a583c06287fdb4515d94c23d2dd2f 100644 --- a/apps/note/signals.py +++ b/apps/note/signals.py @@ -10,14 +10,14 @@ def save_user_note(instance, raw, **_kwargs): # When provisionning data, do not try to autocreate return - if (instance.is_superuser or instance.profile.registration_valid) and instance.is_active: + if instance.is_superuser or instance.profile.registration_valid: # 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): +def save_club_note(instance, raw, **_kwargs): """ Hook to create and save a note when a club is updated """ @@ -25,7 +25,6 @@ def save_club_note(instance, created, raw, **_kwargs): # When provisionning data, do not try to autocreate return - if created: - from .models import NoteClub - NoteClub.objects.create(club=instance) + from .models import NoteClub + NoteClub.objects.get_or_create(club=instance) instance.note.save() diff --git a/apps/note/tables.py b/apps/note/tables.py index a38beb9a6c5934ab4670c5e85bb153803e9cd8a5..a1385abc4b0cd16efdc6b3c2066b763737fb792a 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -55,7 +55,7 @@ class HistoryTable(tables.Table): "class": lambda record: str(record.valid).lower() + ' validate', "data-toggle": "tooltip", "title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"), - "onclick": lambda record: 'in_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')', + "onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')', "onmouseover": lambda record: '$("#invalidity_reason_' + str(record.id) + '").show();$("#invalidity_reason_' + str(record.id) + '").focus();', @@ -129,13 +129,14 @@ class ButtonTable(tables.Table): 'table table-bordered condensed table-hover' } row_attrs = { - 'class': lambda record: 'table-row ' + 'table-success' if record.display else 'table-danger', + 'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'), 'id': lambda record: "row-" + str(record.pk), 'data-href': lambda record: record.pk } model = TransactionTemplate exclude = ('id',) + order_by = ('type', '-display', 'destination__name', 'name',) edit = tables.LinkColumn('note:template_update', args=[A('pk')], diff --git a/apps/note/views.py b/apps/note/views.py index 88d47847288fe6b79c07ea8c01ad90d106b45343..6c0ec1e2579b73f552c3de20730f72f89df04f20 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -1,5 +1,6 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import json from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin @@ -30,7 +31,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl table_class = HistoryTable def get_queryset(self, **kwargs): - return super().get_queryset(**kwargs).order_by("-id").all()[:20] + return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20] def get_context_data(self, **kwargs): """ @@ -80,6 +81,33 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up form_class = TransactionTemplateForm success_url = reverse_lazy('note:template_list') + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + if "logs" in settings.INSTALLED_APPS: + from logs.models import Changelog + update_logs = Changelog.objects.filter( + model=ContentType.objects.get_for_model(TransactionTemplate), + instance_pk=self.object.pk, + action="edit", + ) + price_history = [] + for log in update_logs.all(): + old_dict = json.loads(log.previous) + new_dict = json.loads(log.data) + old_price = old_dict["amount"] + new_price = new_dict["amount"] + if old_price != new_price: + price_history.append(dict(price=old_price, time=log.timestamp)) + + price_history.append(dict(price=self.object.amount, time=None)) + + price_history.reverse() + + context["price_history"] = price_history + + return context + class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): """ @@ -93,7 +121,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): table_class = HistoryTable def get_queryset(self, **kwargs): - return super().get_queryset(**kwargs).order_by("-id").all()[:20] + return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20] def get_context_data(self, **kwargs): """ diff --git a/apps/permission/backends.py b/apps/permission/backends.py index 04d93528ef5f9b6b2c5e5d660a0a4e98c597c099..97cded7047cb2324f20232f78ca2c3299e17801c 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import User, AnonymousUser from django.contrib.contenttypes.models import ContentType from django.db.models import Q, F from note.models import Note, NoteUser, NoteClub, NoteSpecial +from note_kfet import settings from note_kfet.middlewares import get_current_session from member.models import Membership, Club @@ -36,13 +37,15 @@ class PermissionBackend(ModelBackend): # 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), + return Permission.objects.annotate( + club=F("rolepermissions__role__membership__club"), + membership=F("rolepermissions__role__membership"), + ).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 @@ -55,6 +58,7 @@ class PermissionBackend(ModelBackend): :return: A generator of the requested permissions """ clubs = {} + memberships = {} for permission in PermissionBackend.get_raw_permissions(user, type): if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club: @@ -64,9 +68,16 @@ class PermissionBackend(ModelBackend): clubs[permission.club] = club = Club.objects.get(pk=permission.club) else: club = clubs[permission.club] + + if permission.membership not in memberships: + memberships[permission.membership] = membership = Membership.objects.get(pk=permission.membership) + else: + membership = memberships[permission.membership] + permission = permission.about( user=user, club=club, + membership=membership, User=User, Club=Club, Membership=Membership, @@ -75,7 +86,9 @@ class PermissionBackend(ModelBackend): NoteClub=NoteClub, NoteSpecial=NoteSpecial, F=F, - Q=Q + Q=Q, + now=datetime.datetime.now(), + today=datetime.date.today(), ) yield permission @@ -95,7 +108,7 @@ class PermissionBackend(ModelBackend): # Anonymous users can't do anything return Q(pk=-1) - if user.is_superuser and get_current_session().get("permission_mask", 0) >= 42: + if user.is_superuser and get_current_session().get("permission_mask", 42) >= 42: # Superusers have all rights return Q() @@ -129,9 +142,9 @@ class PermissionBackend(ModelBackend): sess = get_current_session() if sess is not None and sess.session_key is None: - return Permission.objects.none() + return False - if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42: + if user_obj.is_superuser and get_current_session().get("permission_mask", 42) >= 42: return True if obj is None: diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index d7eca508ba5647b51412e0d435b0589a52ec13fa..bad0ec8073c7de79a5efb10c814aa06dde69c009 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -1,1184 +1,2459 @@ [ - { - "model": "member.role", - "pk": 1, - "fields": { - "name": "Adh\u00e9rent BDE" - } - }, - { - "model": "member.role", - "pk": 2, - "fields": { - "name": "Adh\u00e9rent Kfet" - } - }, - { - "model": "member.role", - "pk": 3, - "fields": { - "name": "Membre de club" - } - }, - { - "model": "member.role", - "pk": 4, - "fields": { - "name": "Bureau de club" - } - }, - { - "model": "member.role", - "pk": 5, - "fields": { - "name": "Pr\u00e9sident\u00b7e de club" - } - }, - { - "model": "member.role", - "pk": 6, - "fields": { - "name": "Tr\u00e9sorier\u00b7\u00e8re de club" - } - }, - { - "model": "member.role", - "pk": 7, - "fields": { - "name": "Pr\u00e9sident\u00b7e BDE" - } - }, - { - "model": "member.role", - "pk": 8, - "fields": { - "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]" - } - }, - { - "model": "permission.permissionmask", - "pk": 1, - "fields": { - "rank": 0, - "description": "Droits basiques" - } - }, - { - "model": "permission.permissionmask", - "pk": 2, - "fields": { - "rank": 1, - "description": "Droits note seulement" - } - }, - { - "model": "permission.permissionmask", - "pk": 3, - "fields": { - "rank": 42, - "description": "Tous mes droits" - } - }, - { - "model": "permission.permission", - "pk": 1, - "fields": { - "model": 4, - "query": "{\"pk\": [\"user\", \"pk\"]}", - "type": "view", - "mask": 1, - "field": "", - "description": "View our User object" - } - }, - { - "model": "permission.permission", - "pk": 2, - "fields": { - "model": 17, - "query": "{\"user\": [\"user\"]}", - "type": "view", - "mask": 1, - "field": "", - "description": "View our profile" - } - }, - { - "model": "permission.permission", - "pk": 3, - "fields": { - "model": 27, - "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", - "type": "view", - "mask": 1, - "field": "", - "description": "View our own note" - } - }, - { - "model": "permission.permission", - "pk": 4, - "fields": { - "model": 8, - "query": "{\"user\": [\"user\"]}", - "type": "view", - "mask": 1, - "field": "", - "description": "View our API token" - } - }, - { - "model": "permission.permission", - "pk": 5, - "fields": { - "model": 22, - "query": "[\"OR\", {\"source\": [\"user\", \"note\"]}, {\"destination\": [\"user\", \"note\"]}]", - "type": "view", - "mask": 1, - "field": "", - "description": "View our own transactions" - } - }, - { - "model": "permission.permission", - "pk": 6, - "fields": { - "model": 19, - "query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]", - "type": "view", - "mask": 1, - "field": "", - "description": "View aliases of clubs and members of Kfet club" - } - }, - { - "model": "permission.permission", - "pk": 7, - "fields": { - "model": 4, - "query": "{\"pk\": [\"user\", \"pk\"]}", - "type": "change", - "mask": 1, - "field": "last_login", - "description": "Change myself's last login" - } - }, - { - "model": "permission.permission", - "pk": 8, - "fields": { - "model": 4, - "query": "{\"pk\": [\"user\", \"pk\"]}", - "type": "change", - "mask": 1, - "field": "username", - "description": "Change myself's username" - } - }, - { - "model": "permission.permission", - "pk": 9, - "fields": { - "model": 4, - "query": "{\"pk\": [\"user\", \"pk\"]}", - "type": "change", - "mask": 1, - "field": "first_name", - "description": "Change myself's first name" - } - }, - { - "model": "permission.permission", - "pk": 10, - "fields": { - "model": 4, - "query": "{\"pk\": [\"user\", \"pk\"]}", - "type": "change", - "mask": 1, - "field": "last_name", - "description": "Change myself's last name" - } - }, - { - "model": "permission.permission", - "pk": 11, - "fields": { - "model": 4, - "query": "{\"pk\": [\"user\", \"pk\"]}", - "type": "change", - "mask": 1, - "field": "email", - "description": "Change myself's email" - } - }, - { - "model": "permission.permission", - "pk": 12, - "fields": { - "model": 8, - "query": "{\"user\": [\"user\"]}", - "type": "delete", - "mask": 1, - "field": "", - "description": "Delete API Token" - } - }, - { - "model": "permission.permission", - "pk": 13, - "fields": { - "model": 8, - "query": "{\"user\": [\"user\"]}", - "type": "add", - "mask": 1, - "field": "", - "description": "Create API Token" - } - }, - { - "model": "permission.permission", - "pk": 14, - "fields": { - "model": 19, - "query": "{\"note\": [\"user\", \"note\"]}", - "type": "delete", - "mask": 1, - "field": "", - "description": "Remove alias" - } - }, - { - "model": "permission.permission", - "pk": 15, - "fields": { - "model": 19, - "query": "{\"note\": [\"user\", \"note\"]}", - "type": "add", - "mask": 1, - "field": "", - "description": "Add alias" - } - }, - { - "model": "permission.permission", - "pk": 16, - "fields": { - "model": 27, - "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", - "type": "change", - "mask": 1, - "field": "display_image", - "description": "Change myself's display image" - } - }, - { - "model": "permission.permission", - "pk": 17, - "fields": { - "model": 22, - "query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"amount__lte\": [\"user\", \"note\", \"balance\"]}, {\"valid\": false}]]", - "type": "add", - "mask": 1, - "field": "", - "description": "Transfer from myself's note" - } - }, - { - "model": "permission.permission", - "pk": 18, - "fields": { - "model": 20, - "query": "{}", - "type": "change", - "mask": 1, - "field": "balance", - "description": "Update a note balance with a transaction" - } - }, - { - "model": "permission.permission", - "pk": 19, - "fields": { - "model": 20, - "query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"pk__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club\": [\"club\"]}], [\"all\"]]}]", - "type": "view", - "mask": 2, - "field": "", - "description": "View notes of club members" - } - }, - { - "model": "permission.permission", - "pk": 20, - "fields": { - "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": "", - "description": "Create transactions with a club" - } - }, - { - "model": "permission.permission", - "pk": 21, - "fields": { - "model": 28, - "query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]", - "type": "add", - "mask": 2, - "field": "", - "description": "Create transactions from buttons with a club" - } - }, - { - "model": "permission.permission", - "pk": 22, - "fields": { - "model": 15, - "query": "{\"pk\": [\"club\", \"pk\"]}", - "type": "view", - "mask": 1, - "field": "", - "description": "View club infos" - } - }, - { - "model": "permission.permission", - "pk": 23, - "fields": { - "model": 22, - "query": "{}", - "type": "change", - "mask": 1, - "field": "valid", - "description": "Update validation status of a transaction" - } - }, - { - "model": "permission.permission", - "pk": 24, - "fields": { - "model": 22, - "query": "{}", - "type": "view", - "mask": 2, - "field": "", - "description": "View all transactions" - } - }, - { - "model": "permission.permission", - "pk": 25, - "fields": { - "model": 26, - "query": "{}", - "type": "view", - "mask": 2, - "field": "", - "description": "Display credit/debit interface" - } - }, - { - "model": "permission.permission", - "pk": 26, - "fields": { - "model": 29, - "query": "{}", - "type": "add", - "mask": 2, - "field": "", - "description": "Create credit/debit transaction" - } - }, - { - "model": "permission.permission", - "pk": 27, - "fields": { - "model": 21, - "query": "{}", - "type": "view", - "mask": 2, - "field": "", - "description": "View button categories" - } - }, - { - "model": "permission.permission", - "pk": 28, - "fields": { - "model": 21, - "query": "{}", - "type": "change", - "mask": 3, - "field": "", - "description": "Change button category" - } - }, - { - "model": "permission.permission", - "pk": 29, - "fields": { - "model": 21, - "query": "{}", - "type": "add", - "mask": 3, - "field": "", - "description": "Add button category" - } - }, - { - "model": "permission.permission", - "pk": 30, - "fields": { - "model": 23, - "query": "{}", - "type": "view", - "mask": 2, - "field": "", - "description": "View buttons" - } - }, - { - "model": "permission.permission", - "pk": 31, - "fields": { - "model": 23, - "query": "{}", - "type": "add", - "mask": 3, - "field": "", - "description": "Add buttons" - } - }, - { - "model": "permission.permission", - "pk": 32, - "fields": { - "model": 23, - "query": "{}", - "type": "change", - "mask": 3, - "field": "", - "description": "Update buttons" - } - }, - { - "model": "permission.permission", - "pk": 33, - "fields": { - "model": 22, - "query": "{}", - "type": "add", - "mask": 2, - "field": "", - "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, - "fields": { - "role": 1, - "permissions": [ - 1, - 2, - 3, - 4, - 5, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 48, - 52 - ] - } - }, - { - "model": "permission.rolepermissions", - "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, - 3, - 4, - 5, - 6, - 7, - 8, - 9, - 10, - 11, - 12, - 13, - 14, - 15, - 16, - 17, - 18, - 19, - 21, - 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": 10, - "fields": { - "role": 10, - "permissions": [ - 23, - 24, - 25, - 26, - 27, - 28, - 29, - 30, - 31, - 32, - 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 - ] - } - } -] + { + "model": "member.role", + "pk": 1, + "fields": { + "name": "Adh\u00e9rent BDE" + } + }, + { + "model": "member.role", + "pk": 2, + "fields": { + "name": "Adh\u00e9rent Kfet" + } + }, + { + "model": "member.role", + "pk": 3, + "fields": { + "name": "Membre de club" + } + }, + { + "model": "member.role", + "pk": 4, + "fields": { + "name": "Bureau de club" + } + }, + { + "model": "member.role", + "pk": 5, + "fields": { + "name": "Pr\u00e9sident\u00b7e de club" + } + }, + { + "model": "member.role", + "pk": 6, + "fields": { + "name": "Tr\u00e9sorier\u00b7\u00e8re de club" + } + }, + { + "model": "member.role", + "pk": 7, + "fields": { + "name": "Pr\u00e9sident\u00b7e BDE" + } + }, + { + "model": "member.role", + "pk": 8, + "fields": { + "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]" + } + }, + { + "model": "member.role", + "pk": 12, + "fields": { + "name": "GC WEI" + } + }, + { + "model": "member.role", + "pk": 13, + "fields": { + "name": "Chef de bus" + } + }, + { + "model": "member.role", + "pk": 14, + "fields": { + "name": "Chef d'\u00e9quipe" + } + }, + { + "model": "member.role", + "pk": 15, + "fields": { + "name": "\u00c9lectron libre" + } + }, + { + "model": "member.role", + "pk": 16, + "fields": { + "name": "\u00c9lectron libre (avec perm)" + } + }, + { + "model": "member.role", + "pk": 17, + "fields": { + "name": "1A" + } + }, + { + "model": "member.role", + "pk": 18, + "fields": { + "name": "Adhérent WEI" + } + }, + { + "model": "wei.weirole", + "pk": 12, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 13, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 14, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 15, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 16, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 17, + "fields": {} + }, + { + "model": "wei.weirole", + "pk": 18, + "fields": {} + }, + { + "model": "permission.permissionmask", + "pk": 1, + "fields": { + "rank": 0, + "description": "Droits basiques" + } + }, + { + "model": "permission.permissionmask", + "pk": 2, + "fields": { + "rank": 1, + "description": "Droits note seulement" + } + }, + { + "model": "permission.permissionmask", + "pk": 3, + "fields": { + "rank": 42, + "description": "Tous mes droits" + } + }, + { + "model": "permission.permission", + "pk": 1, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"pk\": [\"user\", \"pk\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View our User object" + } + }, + { + "model": "permission.permission", + "pk": 2, + "fields": { + "model": [ + "member", + "profile" + ], + "query": "{\"user\": [\"user\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View our profile" + } + }, + { + "model": "permission.permission", + "pk": 3, + "fields": { + "model": [ + "note", + "noteuser" + ], + "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View our own note" + } + }, + { + "model": "permission.permission", + "pk": 4, + "fields": { + "model": [ + "authtoken", + "token" + ], + "query": "{\"user\": [\"user\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View our API token" + } + }, + { + "model": "permission.permission", + "pk": 5, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"OR\", {\"source\": [\"user\", \"note\"]}, {\"destination\": [\"user\", \"note\"]}]", + "type": "view", + "mask": 1, + "field": "", + "description": "View our own transactions" + } + }, + { + "model": "permission.permission", + "pk": 6, + "fields": { + "model": [ + "note", + "alias" + ], + "query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]", + "type": "view", + "mask": 1, + "field": "", + "description": "View aliases of clubs and members of Kfet club" + } + }, + { + "model": "permission.permission", + "pk": 7, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"pk\": [\"user\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "last_login", + "description": "Change myself's last login" + } + }, + { + "model": "permission.permission", + "pk": 8, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"pk\": [\"user\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "username", + "description": "Change myself's username" + } + }, + { + "model": "permission.permission", + "pk": 9, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"pk\": [\"user\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "first_name", + "description": "Change myself's first name" + } + }, + { + "model": "permission.permission", + "pk": 10, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"pk\": [\"user\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "last_name", + "description": "Change myself's last name" + } + }, + { + "model": "permission.permission", + "pk": 11, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"pk\": [\"user\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "email", + "description": "Change myself's email" + } + }, + { + "model": "permission.permission", + "pk": 12, + "fields": { + "model": [ + "authtoken", + "token" + ], + "query": "{\"user\": [\"user\"]}", + "type": "delete", + "mask": 1, + "field": "", + "description": "Delete API Token" + } + }, + { + "model": "permission.permission", + "pk": 13, + "fields": { + "model": [ + "authtoken", + "token" + ], + "query": "{\"user\": [\"user\"]}", + "type": "add", + "mask": 1, + "field": "", + "description": "Create API Token" + } + }, + { + "model": "permission.permission", + "pk": 14, + "fields": { + "model": [ + "note", + "alias" + ], + "query": "{\"note\": [\"user\", \"note\"]}", + "type": "delete", + "mask": 1, + "field": "", + "description": "Remove alias" + } + }, + { + "model": "permission.permission", + "pk": 15, + "fields": { + "model": [ + "note", + "alias" + ], + "query": "{\"note\": [\"user\", \"note\"]}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add alias" + } + }, + { + "model": "permission.permission", + "pk": 16, + "fields": { + "model": [ + "note", + "noteuser" + ], + "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "display_image", + "description": "Change myself's display image" + } + }, + { + "model": "permission.permission", + "pk": 17, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, [\"OR\", {\"amount__lte\": [\"user\", \"note\", \"balance\"]}, {\"valid\": false}]]", + "type": "add", + "mask": 1, + "field": "", + "description": "Transfer from myself's note" + } + }, + { + "model": "permission.permission", + "pk": 18, + "fields": { + "model": [ + "note", + "note" + ], + "query": "{}", + "type": "change", + "mask": 1, + "field": "balance", + "description": "Update a note balance with a transaction" + } + }, + { + "model": "permission.permission", + "pk": 19, + "fields": { + "model": [ + "note", + "note" + ], + "query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"pk__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club\": [\"club\"]}], [\"all\"]]}]", + "type": "view", + "mask": 2, + "field": "", + "description": "View notes of club members" + } + }, + { + "model": "permission.permission", + "pk": 20, + "fields": { + "model": [ + "note", + "transaction" + ], + "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": "", + "description": "Create transactions with a club" + } + }, + { + "model": "permission.permission", + "pk": 21, + "fields": { + "model": [ + "note", + "recurrenttransaction" + ], + "query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]", + "type": "add", + "mask": 2, + "field": "", + "description": "Create transactions from buttons with a club" + } + }, + { + "model": "permission.permission", + "pk": 22, + "fields": { + "model": [ + "member", + "club" + ], + "query": "{\"pk\": [\"club\", \"pk\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View club infos" + } + }, + { + "model": "permission.permission", + "pk": 23, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "{}", + "type": "change", + "mask": 1, + "field": "valid", + "description": "Update validation status of a transaction" + } + }, + { + "model": "permission.permission", + "pk": 24, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View all transactions" + } + }, + { + "model": "permission.permission", + "pk": 25, + "fields": { + "model": [ + "note", + "notespecial" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "Display credit/debit interface" + } + }, + { + "model": "permission.permission", + "pk": 26, + "fields": { + "model": [ + "note", + "specialtransaction" + ], + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Create credit/debit transaction" + } + }, + { + "model": "permission.permission", + "pk": 27, + "fields": { + "model": [ + "note", + "templatecategory" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View button categories" + } + }, + { + "model": "permission.permission", + "pk": 28, + "fields": { + "model": [ + "note", + "templatecategory" + ], + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Change button category" + } + }, + { + "model": "permission.permission", + "pk": 29, + "fields": { + "model": [ + "note", + "templatecategory" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add button category" + } + }, + { + "model": "permission.permission", + "pk": 30, + "fields": { + "model": [ + "note", + "transactiontemplate" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View buttons" + } + }, + { + "model": "permission.permission", + "pk": 31, + "fields": { + "model": [ + "note", + "transactiontemplate" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add buttons" + } + }, + { + "model": "permission.permission", + "pk": 32, + "fields": { + "model": [ + "note", + "transactiontemplate" + ], + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Update buttons" + } + }, + { + "model": "permission.permission", + "pk": 33, + "fields": { + "model": [ + "note", + "transaction" + ], + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Create any transaction" + } + }, + { + "model": "permission.permission", + "pk": 34, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "[\"OR\", {\"valid\": true}, {\"creater\": [\"user\"]}]", + "type": "view", + "mask": 1, + "field": "", + "description": "View valid activites" + } + }, + { + "model": "permission.permission", + "pk": 35, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "[\"AND\", {\"valid\": false}, {\"creater\": [\"user\"]}]", + "type": "change", + "mask": 1, + "field": "", + "description": "Change our activities" + } + }, + { + "model": "permission.permission", + "pk": 36, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{\"creater\": [\"user\"], \"valid\": false}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add activities" + } + }, + { + "model": "permission.permission", + "pk": 37, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "valid", + "description": "Validate activities" + } + }, + { + "model": "permission.permission", + "pk": 38, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "open", + "description": "Open activities" + } + }, + { + "model": "permission.permission", + "pk": 39, + "fields": { + "model": [ + "activity", + "guest" + ], + "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": [ + "activity", + "guest" + ], + "query": "{\"inviter\": [\"user\", \"note\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View invited people" + } + }, + { + "model": "permission.permission", + "pk": 41, + "fields": { + "model": [ + "activity", + "activity" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View all activities" + } + }, + { + "model": "permission.permission", + "pk": 42, + "fields": { + "model": [ + "activity", + "guest" + ], + "query": "{}", + "type": "view", + "mask": 2, + "field": "", + "description": "View all invited people" + } + }, + { + "model": "permission.permission", + "pk": 43, + "fields": { + "model": [ + "activity", + "entry" + ], + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Manage entries" + } + }, + { + "model": "permission.permission", + "pk": 44, + "fields": { + "model": [ + "activity", + "guesttransaction" + ], + "query": "{}", + "type": "add", + "mask": 2, + "field": "", + "description": "Add invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 45, + "fields": { + "model": [ + "activity", + "guesttransaction" + ], + "query": "{}", + "type": "view", + "mask": 1, + "field": "", + "description": "View invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 46, + "fields": { + "model": [ + "activity", + "guesttransaction" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "valid", + "description": "Validate invitation transactions" + } + }, + { + "model": "permission.permission", + "pk": 47, + "fields": { + "model": [ + "member", + "club" + ], + "query": "{\"pk\": [\"club\", \"pk\"]}", + "type": "change", + "mask": 1, + "field": "", + "description": "Update club" + } + }, + { + "model": "permission.permission", + "pk": 48, + "fields": { + "model": [ + "member", + "membership" + ], + "query": "{\"user\": [\"user\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View our memberships" + } + }, + { + "model": "permission.permission", + "pk": 49, + "fields": { + "model": [ + "member", + "membership" + ], + "query": "{\"club\": [\"club\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View club's memberships" + } + }, + { + "model": "permission.permission", + "pk": 50, + "fields": { + "model": [ + "member", + "membership" + ], + "query": "{\"club\": [\"club\"]}", + "type": "add", + "mask": 2, + "field": "", + "description": "Add a membership to a club" + } + }, + { + "model": "permission.permission", + "pk": 51, + "fields": { + "model": [ + "member", + "membership" + ], + "query": "{\"club\": [\"club\"]}", + "type": "change", + "mask": 2, + "field": "roles", + "description": "Update user roles" + } + }, + { + "model": "permission.permission", + "pk": 52, + "fields": { + "model": [ + "member", + "profile" + ], + "query": "{\"user\": [\"user\"]}", + "type": "change", + "mask": 1, + "field": "", + "description": "Change own profile" + } + }, + { + "model": "permission.permission", + "pk": 53, + "fields": { + "model": [ + "member", + "profile" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "", + "description": "Change any profile" + } + }, + { + "model": "permission.permission", + "pk": 54, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{}", + "type": "change", + "mask": 2, + "field": "", + "description": "Change any user" + } + }, + { + "model": "permission.permission", + "pk": 55, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add user" + } + }, + { + "model": "permission.permission", + "pk": 56, + "fields": { + "model": [ + "member", + "profile" + ], + "query": "{\"email_confirmed\": false, \"registration_valid\": false}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add profile" + } + }, + { + "model": "permission.permission", + "pk": 57, + "fields": { + "model": [ + "auth", + "user" + ], + "query": "{\"profile__registration_valid\": false}", + "type": "delete", + "mask": 2, + "field": "", + "description": "Delete pre-registered user" + } + }, + { + "model": "permission.permission", + "pk": 58, + "fields": { + "model": [ + "member", + "profile" + ], + "query": "{\"registration_valid\": false}", + "type": "delete", + "mask": 2, + "field": "", + "description": "Delete pre-registered user profile" + } + }, + { + "model": "permission.permission", + "pk": 59, + "fields": { + "model": [ + "note", + "transactiontemplate" + ], + "query": "{\"destination\": [\"club\", \"note\"]}", + "type": "view", + "mask": 2, + "field": "", + "description": "New club button" + } + }, + { + "model": "permission.permission", + "pk": 60, + "fields": { + "model": [ + "note", + "transactiontemplate" + ], + "query": "{\"destination\": [\"club\", \"note\"]}", + "type": "add", + "mask": 2, + "field": "", + "description": "Create club button" + } + }, + { + "model": "permission.permission", + "pk": 61, + "fields": { + "model": [ + "note", + "transactiontemplate" + ], + "query": "{\"destination\": [\"club\", \"note\"]}", + "type": "change", + "mask": 2, + "field": "", + "description": "Update club button" + } + }, + { + "model": "permission.permission", + "pk": 62, + "fields": { + "model": [ + "note", + "transaction" + ], + "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": [ + "treasury", + "invoice" + ], + "query": "{}", + "type": "view", + "mask": 3, + "field": "", + "description": "View invoices" + } + }, + { + "model": "permission.permission", + "pk": 64, + "fields": { + "model": [ + "treasury", + "invoice" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add invoice" + } + }, + { + "model": "permission.permission", + "pk": 65, + "fields": { + "model": [ + "treasury", + "invoice" + ], + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Change invoice" + } + }, + { + "model": "permission.permission", + "pk": 66, + "fields": { + "model": [ + "treasury", + "product" + ], + "query": "{}", + "type": "view", + "mask": 3, + "field": "", + "description": "View products" + } + }, + { + "model": "permission.permission", + "pk": 67, + "fields": { + "model": [ + "treasury", + "product" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add products" + } + }, + { + "model": "permission.permission", + "pk": 68, + "fields": { + "model": [ + "treasury", + "product" + ], + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Change product" + } + }, + { + "model": "permission.permission", + "pk": 69, + "fields": { + "model": [ + "treasury", + "product" + ], + "query": "{}", + "type": "delete", + "mask": 3, + "field": "", + "description": "Delete product" + } + }, + { + "model": "permission.permission", + "pk": 70, + "fields": { + "model": [ + "treasury", + "sogecredit" + ], + "query": "{\"credit_transaction\": null}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credit" + } + }, + { + "model": "permission.permission", + "pk": 71, + "fields": { + "model": [ + "treasury", + "sogecredit" + ], + "query": "{}", + "type": "view", + "mask": 3, + "field": "", + "description": "View all Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credits" + } + }, + { + "model": "permission.permission", + "pk": 72, + "fields": { + "model": [ + "treasury", + "sogecredit" + ], + "query": "{}", + "type": "change", + "mask": 1, + "field": "", + "description": "Update Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credit" + } + }, + { + "model": "permission.permission", + "pk": 73, + "fields": { + "model": [ + "treasury", + "sogecredit" + ], + "query": "{}", + "type": "delete", + "mask": 3, + "field": "", + "description": "Delete Soci\u00e9t\u00e9 g\u00e9n\u00e9rale credit" + } + }, + { + "model": "permission.permission", + "pk": 74, + "fields": { + "model": [ + "wei", + "weiclub" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Create a WEI" + } + }, + { + "model": "permission.permission", + "pk": 75, + "fields": { + "model": [ + "wei", + "weiclub" + ], + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Update all WEI" + } + }, + { + "model": "permission.permission", + "pk": 76, + "fields": { + "model": [ + "wei", + "weiclub" + ], + "query": "{\"pk\": [\"club\", \"pk\"]}", + "type": "change", + "mask": 3, + "field": "", + "description": "Update this WEI" + } + }, + { + "model": "permission.permission", + "pk": 77, + "fields": { + "model": [ + "wei", + "weiclub" + ], + "query": "{\"pk\": [\"club\", \"pk\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View my WEI" + } + }, + { + "model": "permission.permission", + "pk": 78, + "fields": { + "model": [ + "wei", + "weiclub" + ], + "query": "{\"membership_start__lte\": [\"today\"], \"year\": [\"today\", \"year\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View last WEI" + } + }, + { + "model": "permission.permission", + "pk": 79, + "fields": { + "model": [ + "wei", + "weirole" + ], + "query": "{}", + "type": "view", + "mask": 1, + "field": "", + "description": "View WEI Roles" + } + }, + { + "model": "permission.permission", + "pk": 80, + "fields": { + "model": [ + "wei", + "weirole" + ], + "query": "{}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add WEI Role" + } + }, + { + "model": "permission.permission", + "pk": 81, + "fields": { + "model": [ + "wei", + "weirole" + ], + "query": "{}", + "type": "change", + "mask": 3, + "field": "", + "description": "Change WEI Role" + } + }, + { + "model": "permission.permission", + "pk": 82, + "fields": { + "model": [ + "wei", + "weirole" + ], + "query": "{}", + "type": "delete", + "mask": 3, + "field": "", + "description": "Delete WEI Role" + } + }, + { + "model": "permission.permission", + "pk": 83, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"user\": [\"user\"], \"wei__year\": [\"today\", \"year\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"], \"membership\": null}", + "type": "add", + "mask": 1, + "field": "", + "description": "Register myself to the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 84, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"first_year\": true, \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"], \"membership\": null}", + "type": "add", + "mask": 1, + "field": "", + "description": "Register first year members to the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 85, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"], \"membership\": null}", + "type": "add", + "mask": 1, + "field": "", + "description": "Register anyone to this WEI" + } + }, + { + "model": "permission.permission", + "pk": 86, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"]}", + "type": "delete", + "mask": 1, + "field": "", + "description": "Delete WEI registration" + } + }, + { + "model": "permission.permission", + "pk": 87, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "view", + "mask": 1, + "field": "", + "description": "View my own WEI registration" + } + }, + { + "model": "permission.permission", + "pk": 88, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View all WEI Registrations" + } + }, + { + "model": "permission.permission", + "pk": 89, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "soge_credit", + "description": "Update the soge credit field of any WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 90, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "change", + "mask": 1, + "field": "soge_credit", + "description": "Update the soge credit field of my own WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 91, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "caution_check", + "description": "Update the caution check field of any WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 92, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "birth_date", + "description": "Update the birth date of any WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 93, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "change", + "mask": 1, + "field": "birth_date", + "description": "Update the birth date of my own WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 94, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "gender", + "description": "Update the gender of any WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 95, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "change", + "mask": 1, + "field": "gender", + "description": "Update the gender of my own WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 96, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "health_issues", + "description": "Update the health issues of any WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 97, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "change", + "mask": 1, + "field": "health_issues", + "description": "Update the health issues of my own WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 98, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "emergency_contact_name", + "description": "Update the emergency contact name of any WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 99, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "change", + "mask": 1, + "field": "emergency_contact_name", + "description": "Update the emergency contact name of my own WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 100, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "emergency_contact_phone", + "description": "Update the emergency contact phone of any WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 101, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"wei__membership_start__lte\": [\"today\"], \"wei__membership_end__gte\": [\"today\"]}, [\"OR\", {\"wei\": [\"club\"]}, {\"wei__year\": [\"today\", \"year\"], \"membership\": null}]]", + "type": "change", + "mask": 1, + "field": "emergency_contact_phone", + "description": "Update the emergency contact phone of my own WEI Registration" + } + }, + { + "model": "permission.permission", + "pk": 102, + "fields": { + "model": [ + "wei", + "weiregistration" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "information_json", + "description": "Update information of any WEI registration" + } + }, + { + "model": "permission.permission", + "pk": 103, + "fields": { + "model": [ + "wei", + "bus" + ], + "query": "{\"wei\": [\"club\"]}", + "type": "add", + "mask": 3, + "field": "", + "description": "Add a bus for the current WEI" + } + }, + { + "model": "permission.permission", + "pk": 104, + "fields": { + "model": [ + "wei", + "bus" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 3, + "field": "name", + "description": "Update the name of a bus for the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 105, + "fields": { + "model": [ + "wei", + "bus" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 3, + "field": "description", + "description": "Update the description of a bus for the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 106, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "add", + "mask": 3, + "field": "", + "description": "Create a bus team for the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 107, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"wei\": [\"club\"], \"wei__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 3, + "field": "", + "description": "Update a bus team for the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 108, + "fields": { + "model": [ + "wei", + "bus" + ], + "query": "[\"AND\", {\"wei\": [\"club\"]}, [\"OR\", [\"NOT\", [\"membership\", \"registration\", \"first_year\"]], {\"wei__date_end__lte\": [\"today\"]}]]", + "type": "view", + "mask": 1, + "field": "", + "description": "View buses of the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 109, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "[\"AND\", {\"wei\": [\"club\"]}, [\"OR\", [\"NOT\", [\"membership\", \"registration\", \"first_year\"]], {\"wei__date_end__lte\": [\"today\"]}]]", + "type": "view", + "mask": 1, + "field": "", + "description": "View bus teams of the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 110, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "[\"AND\", {\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}, [\"OR\", {\"registration__soge_credit\": true}, {\"user__note__balance__gte\": [\"F\", \"fee\"]}]]", + "type": "add", + "mask": 3, + "field": "", + "description": "Create a WEI membership for the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 111, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "{\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "bus", + "description": "Update the bus of a WEI membership" + } + }, + { + "model": "permission.permission", + "pk": 112, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "{\"club\": [\"club\"], \"club__weiclub__membership_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "team", + "description": "Update the team of a WEI membership" + } + }, + { + "model": "permission.permission", + "pk": 113, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "{\"club\": [\"club\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View all WEI Memberships for the last WEI" + } + }, + { + "model": "permission.permission", + "pk": 114, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "[\"AND\", {\"user\": [\"user\"], \"club\": [\"club\"]}, [\"OR\", {\"registration__first_year\": false, \"club__weiclub__date_end__lte\": [\"today\"]}]]", + "type": "view", + "mask": 1, + "field": "", + "description": "View my own WEI membership if I am an old member or if the WEI is past" + } + }, + { + "model": "permission.permission", + "pk": 115, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "{\"club\": [\"club\"], \"bus\": [\"membership\", \"weimembership\", \"bus\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View the members of the bus" + } + }, + { + "model": "permission.permission", + "pk": 116, + "fields": { + "model": [ + "wei", + "weimembership" + ], + "query": "{\"club\": [\"club\"], \"team\": [\"membership\", \"weimembership\", \"team\"]}", + "type": "view", + "mask": 1, + "field": "", + "description": "View the members of the team" + } + }, + { + "model": "permission.permission", + "pk": 117, + "fields": { + "model": [ + "wei", + "bus" + ], + "query": "{\"pk\": [\"membership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "name", + "description": "Update the name of my bus" + } + }, + { + "model": "permission.permission", + "pk": 118, + "fields": { + "model": [ + "wei", + "bus" + ], + "query": "{\"pk\": [\"membership\", \"bus\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "description", + "description": "Update the description of my bus" + } + }, + { + "model": "permission.permission", + "pk": 119, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"bus\": [\"membership\", \"bus\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "add", + "mask": 1, + "field": "", + "description": "Add a team to my bus" + } + }, + { + "model": "permission.permission", + "pk": 120, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"bus\": [\"membership\", \"bus\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "name", + "description": "Update the name of a team of my bus" + } + }, + { + "model": "permission.permission", + "pk": 121, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"bus\": [\"membership\", \"bus\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "color", + "description": "Update the color of a team of my bus" + } + }, + { + "model": "permission.permission", + "pk": 122, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"bus\": [\"membership\", \"bus\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "description", + "description": "Update the description of a team of my bus" + } + }, + { + "model": "permission.permission", + "pk": 123, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"pk\": [\"membership\", \"team\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "name", + "description": "Update the name of my team" + } + }, + { + "model": "permission.permission", + "pk": 124, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"pk\": [\"membership\", \"team\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "color", + "description": "Update the color of my team" + } + }, + { + "model": "permission.permission", + "pk": 125, + "fields": { + "model": [ + "wei", + "busteam" + ], + "query": "{\"pk\": [\"membership\", \"team\", \"pk\"], \"wei__date_end__gte\": [\"today\"]}", + "type": "change", + "mask": 1, + "field": "description", + "description": "Update the description of my team" + } + }, + { + "model": "permission.rolepermissions", + "pk": 1, + "fields": { + "role": 1, + "permissions": [ + 1, + 2, + 3, + 4, + 5, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 48, + 52 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 2, + "fields": { + "role": 2, + "permissions": [ + 34, + 35, + 36, + 6, + 39, + 40, + 14, + 15, + 16, + 17, + 18, + 70, + 108, + 109, + 78, + 79, + 83 + ] + } + }, + { + "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, + 20, + 21, + 27, + 59, + 60, + 61, + 62 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 7, + "fields": { + "role": 7, + "permissions": [ + 24, + 25, + 26, + 27, + 33 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 8, + "fields": { + "role": 8, + "permissions": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 33, + 53, + 54, + 55, + 56, + 57, + 58, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 72, + 73, + 71 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 9, + "fields": { + "role": 9, + "permissions": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 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, + 62, + 63, + 64, + 65, + 66, + 67, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 78, + 79, + 80, + 81, + 82, + 83, + 84, + 85, + 86, + 87, + 88, + 89, + 90, + 91, + 92, + 93, + 94, + 95, + 96, + 97, + 98, + 99, + 100, + 101, + 102, + 103, + 104, + 105, + 106, + 107, + 108, + 109, + 110, + 111, + 112, + 113, + 114, + 115, + 116, + 117, + 118, + 119, + 120, + 121, + 122, + 123, + 124, + 125 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 10, + "fields": { + "role": 10, + "permissions": [ + 23, + 24, + 25, + 26, + 27, + 28, + 29, + 30, + 31, + 32, + 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 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 12, + "fields": { + "role": 12, + "permissions": [ + 76, + 80, + 81, + 82, + 85, + 86, + 88, + 89, + 91, + 92, + 94, + 96, + 98, + 100, + 102, + 103, + 104, + 105, + 106, + 107, + 110, + 111, + 112, + 113 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 13, + "fields": { + "role": 13, + "permissions": [ + 115, + 117, + 118, + 120, + 121, + 122 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 14, + "fields": { + "role": 14, + "permissions": [ + 116 + ] + } + }, + { + "model": "permission.rolepermissions", + "pk": 16, + "fields": { + "role": 18, + "permissions": [ + 77, + 84, + 87, + 90, + 93, + 95, + 97, + 99, + 101, + 108, + 109, + 114 + ] + } + } +] \ No newline at end of file diff --git a/apps/permission/models.py b/apps/permission/models.py index 8117438952e109186796732e9e3e2c22f92c0e90..fe18c226dee5a735e79427a59d257b7a1bcad665 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -120,7 +120,12 @@ class Permission(models.Model): ('delete', 'delete') ] - model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+') + model = models.ForeignKey( + ContentType, + on_delete=models.CASCADE, + related_name='+', + verbose_name=_("model"), + ) # A json encoded Q object with the following grammar # query -> [] | {} (the empty query representing all objects) @@ -142,18 +147,34 @@ class Permission(models.Model): # Examples: # Q(is_superuser=True) := {"is_superuser": true} # ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}] - query = models.TextField() + query = models.TextField( + verbose_name=_("query"), + ) - type = models.CharField(max_length=15, choices=PERMISSION_TYPES) + type = models.CharField( + max_length=15, + choices=PERMISSION_TYPES, + verbose_name=_("type"), + ) mask = models.ForeignKey( PermissionMask, on_delete=models.PROTECT, + related_name="permissions", + verbose_name=_("mask"), ) - field = models.CharField(max_length=255, blank=True) + field = models.CharField( + max_length=255, + blank=True, + verbose_name=_("field"), + ) - description = models.CharField(max_length=255, blank=True) + description = models.CharField( + max_length=255, + blank=True, + verbose_name=_("description"), + ) class Meta: unique_together = ('model', 'query', 'type', 'field') @@ -277,24 +298,22 @@ class Permission(models.Model): return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs) def __str__(self): - if self.field: - return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query) - else: - return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query) + return self.description class RolePermissions(models.Model): """ Permissions associated with a Role """ - role = models.ForeignKey( + role = models.OneToOneField( Role, on_delete=models.PROTECT, - related_name='+', + related_name='permissions', verbose_name=_('role'), ) permissions = models.ManyToManyField( Permission, + verbose_name=_("permissions"), ) def __str__(self): diff --git a/apps/permission/signals.py b/apps/permission/signals.py index bf54b72f27f3c6e89fb525babe743989ac2cf7b6..cac0a8a07015cf38e5ef2582f4d7dc4c1ab1e27f 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.core.exceptions import PermissionDenied +from django.utils.translation import gettext_lazy as _ from note_kfet.middlewares import get_current_authenticated_user from permission.backends import PermissionBackend @@ -57,13 +58,19 @@ def pre_save_object(sender, instance, **kwargs): if old_value == new_value: continue if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): - raise PermissionDenied + raise PermissionDenied( + _("You don't have the permission to change the field {field} on this instance of model" + " {app_label}.{model_name}.") + .format(field=field_name, app_label=app_label, model_name=model_name, ) + ) else: # We check if the user has right to add the object has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance) if not has_perm: - raise PermissionDenied + raise PermissionDenied( + _("You don't have the permission to add this instance of model {app_label}.{model_name}.") + .format(app_label=app_label, model_name=model_name, )) def pre_delete_object(instance, **kwargs): @@ -88,4 +95,6 @@ def pre_delete_object(instance, **kwargs): # We check if the user has rights to delete the object if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance): - raise PermissionDenied + raise PermissionDenied( + _("You don't have the permission to delete this instance of model {app_label}.{model_name}.") + .format(app_label=app_label, model_name=model_name)) diff --git a/apps/permission/urls.py b/apps/permission/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..c571c520e82fbbfd706f61f1cc7c3fead6c63596 --- /dev/null +++ b/apps/permission/urls.py @@ -0,0 +1,10 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path +from permission.views import RightsView + +app_name = 'permission' +urlpatterns = [ + path('rights', RightsView.as_view(), name="rights"), +] diff --git a/apps/permission/views.py b/apps/permission/views.py index bbd9872f92d6d356ae36b87147eccd3a1bb56ac4..cbd26a19f36781df5ca2e08cdf28ae75863d2128 100644 --- a/apps/permission/views.py +++ b/apps/permission/views.py @@ -1,11 +1,60 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import date -from permission.backends import PermissionBackend +from django.forms import HiddenInput +from django.utils.translation import gettext_lazy as _ +from django.views.generic import UpdateView, TemplateView +from member.models import Role, Membership + +from .backends import PermissionBackend class ProtectQuerysetMixin: + """ + This is a View class decorator and not a proper View class. + Ensure that the user has the right to see or update objects. + Display 404 error if the user can't see an object, remove the fields the user can't + update on an update form (useful if the user can't change only specified fields). + """ def get_queryset(self, **kwargs): qs = super().get_queryset(**kwargs) - return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")) + + def get_form(self, form_class=None): + form = super().get_form(form_class) + + if not isinstance(self, UpdateView): + return form + + # If we are in an UpdateView, we display only the fields the user has right to see. + # No worry if the user change the hidden fields: a 403 error will be performed if the user tries to make + # a custom request. + # We could also delete the field, but some views might be affected. + for key in form.base_fields: + if not PermissionBackend.check_perm(self.request.user, "wei.change_weiregistration_" + key, self.object): + form.fields[key].widget = HiddenInput() + + return form + + +class RightsView(TemplateView): + template_name = "permission/all_rights.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + context["title"] = _("All rights") + roles = Role.objects.all() + context["roles"] = roles + if self.request.user.is_authenticated: + active_memberships = Membership.objects.filter(user=self.request.user, + date_start__lte=date.today(), + date_end__gte=date.today()).all() + else: + active_memberships = Membership.objects.none() + + for role in roles: + role.clubs = [membership.club for membership in active_memberships if role in membership.roles.all()] + + return context diff --git a/apps/registration/forms.py b/apps/registration/forms.py index cba5c2ae118e8980ae6c8f2aedd17d8fb8e58661..46559487df84c20d51d89a25812ddd484356f3a7 100644 --- a/apps/registration/forms.py +++ b/apps/registration/forms.py @@ -27,6 +27,15 @@ class SignUpForm(UserCreationForm): fields = ('first_name', 'last_name', 'username', 'email', ) +class WEISignupForm(forms.Form): + wei_registration = forms.BooleanField( + label=_("Register to the WEI"), + required=False, + help_text=_("Check this case if you want to register to the WEI. If you hesitate, you will be able to register" + " later, after validating your account in the Kfet."), + ) + + class ValidationForm(forms.Form): """ Validate the inscription of the new users and pay memberships. diff --git a/apps/registration/views.py b/apps/registration/views.py index 35391b05364871fddafbe673922b3b0ca85b844d..2c91a604c4b56d0ed8506b2ea027cc03fc2591f9 100644 --- a/apps/registration/views.py +++ b/apps/registration/views.py @@ -11,12 +11,12 @@ 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 import CreateView, TemplateView, DetailView 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.models import SpecialTransaction from note.templatetags.pretty_money import pretty_money from permission.backends import PermissionBackend from permission.views import ProtectQuerysetMixin @@ -32,13 +32,13 @@ class UserCreateView(CreateView): """ 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() + del context["profile_form"].fields["section"] return context @@ -67,6 +67,9 @@ class UserCreateView(CreateView): return super().form_valid(form) + def get_success_url(self): + return reverse_lazy('registration:email_validation_sent') + class UserValidateView(TemplateView): """ @@ -112,7 +115,7 @@ class UserValidateView(TemplateView): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['user'] = self.get_user(self.kwargs["uidb64"]) + context['user_object'] = self.get_user(self.kwargs["uidb64"]) context['login_url'] = resolve_url(settings.LOGIN_URL) if self.validlink: context['validlink'] = True @@ -263,17 +266,17 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, 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" + # If the bank pays, then we don't credit now. Treasurers will validate the transaction + # and credit the note later. + credit_type = None - print("OK") + if credit_type is None: + credit_amount = 0 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: + if fee > credit_amount and not soge: # 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 {}") @@ -295,10 +298,9 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, 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() + user.refresh_from_db() if credit_type is not None and credit_amount > 0: # Credit the note @@ -316,21 +318,29 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, if join_BDE: # Create membership for the user to the BDE starting today - membership = Membership.objects.create( + membership = Membership( club=bde, user=user, fee=bde_fee, ) + if soge: + membership._soge = True + membership.save() + membership.refresh_from_db() 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( + membership = Membership( club=kfet, user=user, fee=kfet_fee, ) + if soge: + membership._soge = True + membership.save() + membership.refresh_from_db() membership.roles.add(Role.objects.get(name="Adhérent Kfet")) membership.save() diff --git a/apps/scripts b/apps/scripts index b9db26fa494870b02fc1b4b463a2322395a278a1..f0aa426950b9b867bf99233795e260871be2cb99 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit b9db26fa494870b02fc1b4b463a2322395a278a1 +Subproject commit f0aa426950b9b867bf99233795e260871be2cb99 diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py index abeec3e37681997023ffbfa2b7e7a857531d9420..9c8aaf2e1a608e5e6e895ef1ede8f08e4cb6c1a8 100644 --- a/apps/treasury/admin.py +++ b/apps/treasury/admin.py @@ -3,7 +3,7 @@ from django.contrib import admin -from .models import RemittanceType, Remittance +from .models import RemittanceType, Remittance, SogeCredit @admin.register(RemittanceType) @@ -25,3 +25,6 @@ class RemittanceAdmin(admin.ModelAdmin): if not obj: return True return not obj.closed and super().has_change_permission(request, obj) + + +admin.site.register(SogeCredit) diff --git a/apps/treasury/api/serializers.py b/apps/treasury/api/serializers.py index f1bbef75cfac7e2d714e7a3d83b073a5f39342a9..0acb0aa12ca6ec9fc0d9c9eb951637fc37a68159 100644 --- a/apps/treasury/api/serializers.py +++ b/apps/treasury/api/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers from note.api.serializers import SpecialTransactionSerializer -from ..models import Invoice, Product, RemittanceType, Remittance +from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit class ProductSerializer(serializers.ModelSerializer): @@ -60,3 +60,14 @@ class RemittanceSerializer(serializers.ModelSerializer): def get_transactions(self, obj): return serializers.ListSerializer(child=SpecialTransactionSerializer()).to_representation(obj.transactions) + + +class SogeCreditSerializer(serializers.ModelSerializer): + """ + REST API Serializer for SogeCredit types. + The djangorestframework plugin will analyse the model `SogeCredit` and parse all fields in the API. + """ + + class Meta: + model = SogeCredit + fields = '__all__' diff --git a/apps/treasury/api/urls.py b/apps/treasury/api/urls.py index 30ac00e1ae3a9f598634099c217d2f5b5bafee0a..70d81f77905063c39cbc2ef9b923a97feef1f965 100644 --- a/apps/treasury/api/urls.py +++ b/apps/treasury/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 InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet +from .views import InvoiceViewSet, ProductViewSet, RemittanceViewSet, RemittanceTypeViewSet, SogeCreditViewSet def register_treasury_urls(router, path): @@ -12,3 +12,4 @@ def register_treasury_urls(router, path): router.register(path + '/product', ProductViewSet) router.register(path + '/remittance_type', RemittanceTypeViewSet) router.register(path + '/remittance', RemittanceViewSet) + router.register(path + '/soge_credit', SogeCreditViewSet) diff --git a/apps/treasury/api/views.py b/apps/treasury/api/views.py index 7a70fd2466379461d2cff980a882b5107d39ae0e..ee97e6ac19658dd10cd879a40c93db0deedb885b 100644 --- a/apps/treasury/api/views.py +++ b/apps/treasury/api/views.py @@ -5,8 +5,9 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter from api.viewsets import ReadProtectedModelViewSet -from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer -from ..models import Invoice, Product, RemittanceType, Remittance +from .serializers import InvoiceSerializer, ProductSerializer, RemittanceTypeSerializer, RemittanceSerializer,\ + SogeCreditSerializer +from ..models import Invoice, Product, RemittanceType, Remittance, SogeCredit class InvoiceViewSet(ReadProtectedModelViewSet): @@ -39,7 +40,7 @@ class RemittanceTypeViewSet(ReadProtectedModelViewSet): The djangorestframework plugin will get all `RemittanceType` objects, serialize it to JSON with the given serializer then render it on /api/treasury/remittance_type/ """ - queryset = RemittanceType.objects.all() + queryset = RemittanceType.objects serializer_class = RemittanceTypeSerializer @@ -49,5 +50,15 @@ class RemittanceViewSet(ReadProtectedModelViewSet): The djangorestframework plugin will get all `Remittance` objects, serialize it to JSON with the given serializer, then render it on /api/treasury/remittance/ """ - queryset = Remittance.objects.all() + queryset = Remittance.objects serializer_class = RemittanceSerializer + + +class SogeCreditViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `SogeCredit` objects, serialize it to JSON with the given serializer, + then render it on /api/treasury/soge_credit/ + """ + queryset = SogeCredit.objects + serializer_class = SogeCreditSerializer diff --git a/apps/treasury/models.py b/apps/treasury/models.py index 9d7ee8487eb630fccd65dab732a004013d7f5443..1e7f2a95b90de5472e24b1afdd13ff896440e5bf 100644 --- a/apps/treasury/models.py +++ b/apps/treasury/models.py @@ -1,11 +1,13 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +from datetime import datetime +from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.db import models from django.db.models import Q from django.utils.translation import gettext_lazy as _ -from note.models import NoteSpecial, SpecialTransaction +from note.models import NoteSpecial, SpecialTransaction, MembershipTransaction class Invoice(models.Model): @@ -207,3 +209,101 @@ class SpecialTransactionProxy(models.Model): class Meta: verbose_name = _("special transaction proxy") verbose_name_plural = _("special transaction proxies") + + +class SogeCredit(models.Model): + """ + Manage the credits from the Société générale. + """ + user = models.OneToOneField( + User, + on_delete=models.PROTECT, + verbose_name=_("user"), + ) + + transactions = models.ManyToManyField( + MembershipTransaction, + related_name="+", + verbose_name=_("membership transactions"), + ) + + credit_transaction = models.OneToOneField( + SpecialTransaction, + on_delete=models.SET_NULL, + verbose_name=_("credit transaction"), + null=True, + ) + + @property + def valid(self): + return self.credit_transaction is not None + + @property + def amount(self): + return sum(transaction.total for transaction in self.transactions.all()) + + def invalidate(self): + """ + Invalidating a Société générale delete the transaction of the bank if it was already created. + Treasurers must know what they do, With Great Power Comes Great Responsibility... + """ + if self.valid: + self.credit_transaction.valid = False + self.credit_transaction._force_save = True + self.credit_transaction.save() + self.credit_transaction._force_delete = True + self.credit_transaction.delete() + self.credit_transaction = None + for transaction in self.transactions.all(): + transaction.valid = False + transaction._force_save = True + transaction.save() + + def validate(self, force=False): + if self.valid and not force: + # The credit is already done + return + + # First invalidate all transaction and delete the credit if already did (and force mode) + self.invalidate() + self.credit_transaction = SpecialTransaction.objects.create( + source=NoteSpecial.objects.get(special_type="Virement bancaire"), + destination=self.user.note, + quantity=1, + amount=self.amount, + reason="Crédit société générale", + last_name=self.user.last_name, + first_name=self.user.first_name, + bank="Société générale", + ) + self.save() + + for transaction in self.transactions.all(): + transaction.valid = True + transaction._force_save = True + transaction.created_at = datetime.now() + transaction.save() + + def delete(self, **kwargs): + """ + Deleting a SogeCredit is equivalent to say that the Société générale didn't pay. + Treasurers must know what they do, this is difficult to undo this operation. + With Great Power Comes Great Responsibility... + """ + + total_fee = sum(transaction.total for transaction in self.transactions.all() if not transaction.valid) + if self.user.note.balance < total_fee: + raise ValidationError(_("This user doesn't have enough money to pay the memberships with its note. " + "Please ask her/him to credit the note before invalidating this credit.")) + + self.invalidate() + for transaction in self.transactions.all(): + transaction._force_save = True + transaction.valid = True + transaction.created_at = datetime.now() + transaction.save() + super().delete(**kwargs) + + class Meta: + verbose_name = _("Credit from the Société générale") + verbose_name_plural = _("Credits from the Société générale") diff --git a/apps/treasury/tables.py b/apps/treasury/tables.py index 1ecc04db196ff8e94f45f60bbd5996a49c03e9e5..9f4e43e6e4fedb7ba7e43aec4bb0ace958c8c752 100644 --- a/apps/treasury/tables.py +++ b/apps/treasury/tables.py @@ -7,7 +7,7 @@ from django_tables2 import A from note.models import SpecialTransaction from note.templatetags.pretty_money import pretty_money -from .models import Invoice, Remittance +from .models import Invoice, Remittance, SogeCredit class InvoiceTable(tables.Table): @@ -101,3 +101,28 @@ class SpecialTransactionTable(tables.Table): model = SpecialTransaction template_name = 'django_tables2/bootstrap4.html' fields = ('id', 'source', 'destination', 'last_name', 'first_name', 'bank', 'amount', 'reason',) + + +class SogeCreditTable(tables.Table): + user = tables.LinkColumn( + 'treasury:manage_soge_credit', + args=[A('pk')], + ) + + amount = tables.Column( + verbose_name=_("Amount"), + ) + + valid = tables.Column( + verbose_name=_("Valid"), + ) + + def render_amount(self, value): + return pretty_money(value) + + def render_valid(self, value): + return _("Yes") if value else _("No") + + class Meta: + model = SogeCredit + fields = ('user', 'amount', 'valid', ) diff --git a/apps/treasury/urls.py b/apps/treasury/urls.py index d44cc4145921fd63d3c645d67c39612e2103a54c..8606fb5b6f1554390ce9778e8c37f025041d3683 100644 --- a/apps/treasury/urls.py +++ b/apps/treasury/urls.py @@ -4,7 +4,8 @@ from django.urls import path from .views import InvoiceCreateView, InvoiceListView, InvoiceUpdateView, InvoiceRenderView, RemittanceListView,\ - RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView + RemittanceCreateView, RemittanceUpdateView, LinkTransactionToRemittanceView, UnlinkTransactionToRemittanceView,\ + SogeCreditListView, SogeCreditManageView app_name = 'treasury' urlpatterns = [ @@ -21,4 +22,7 @@ urlpatterns = [ path('remittance/link_transaction/<int:pk>/', LinkTransactionToRemittanceView.as_view(), name='link_transaction'), path('remittance/unlink_transaction/<int:pk>/', UnlinkTransactionToRemittanceView.as_view(), name='unlink_transaction'), + + path('soge-credits/list/', SogeCreditListView.as_view(), name='soge_credits'), + path('soge-credits/manage/<int:pk>/', SogeCreditManageView.as_view(), name='manage_soge_credit'), ] diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 7361d1d2b6c1b5d02c58afb972c6593576852e27..f42e5e776a74ba509037811ba7a167b1158250bf 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -10,21 +10,23 @@ from crispy_forms.helper import FormHelper from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import ValidationError from django.db.models import Q +from django.forms import Form from django.http import HttpResponse from django.shortcuts import redirect from django.template.loader import render_to_string from django.urls import reverse_lazy -from django.views.generic import CreateView, UpdateView +from django.views.generic import CreateView, UpdateView, DetailView from django.views.generic.base import View, TemplateView +from django.views.generic.edit import BaseFormView from django_tables2 import SingleTableView -from note.models import SpecialTransaction, NoteSpecial +from note.models import SpecialTransaction, NoteSpecial, Alias 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 +from .models import Invoice, Product, Remittance, SpecialTransactionProxy, SogeCredit +from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable, SogeCreditTable class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): @@ -180,7 +182,7 @@ class InvoiceRenderView(LoginRequiredMixin, View): # Display the generated pdf as a HTTP Response pdf = open("{}/invoice-{}.pdf".format(tmp_dir, pk), 'rb').read() response = HttpResponse(pdf, content_type="application/pdf") - response['Content-Disposition'] = "inline;filename=invoice-{:d}.pdf".format(pk) + response['Content-Disposition'] = "inline;filename=Facture%20n°{:d}.pdf".format(pk) except IOError as e: raise e finally: @@ -203,9 +205,9 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["table"] = RemittanceTable(data=Remittance.objects - .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) - .all()) + 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 context @@ -307,3 +309,61 @@ class UnlinkTransactionToRemittanceView(LoginRequiredMixin, View): transaction.save() return redirect('treasury:remittance_list') + + +class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableView): + """ + List all Société Générale credits + """ + model = SogeCredit + table_class = SogeCreditTable + + def get_queryset(self, **kwargs): + """ + Filter the table with the given parameter. + :param kwargs: + :return: + """ + qs = super().get_queryset() + if "search" in self.request.GET: + pattern = self.request.GET["search"] + + if not pattern: + return qs.none() + + qs = qs.filter( + Q(user__first_name__iregex=pattern) + | Q(user__last_name__iregex=pattern) + | Q(user__note__alias__name__iregex="^" + pattern) + | Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + ) + else: + qs = qs.none() + + if "valid" in self.request.GET: + q = Q(credit_transaction=None) + if not self.request.GET["valid"]: + q = ~q + qs = qs.filter(q) + + return qs[:20] + + +class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormView, DetailView): + """ + Manage credits from the Société générale. + """ + model = SogeCredit + form_class = Form + + def form_valid(self, form): + if "validate" in form.data: + self.get_object().validate(True) + elif "delete" in form.data: + self.get_object().delete() + return super().form_valid(form) + + def get_success_url(self): + if "validate" in self.request.POST: + return reverse_lazy('treasury:manage_soge_credit', args=(self.get_object().pk,)) + return reverse_lazy('treasury:soge_credits') diff --git a/apps/wei/__init__.py b/apps/wei/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ad360dae2862a450ef1ddb799146f8bf9156c7c2 --- /dev/null +++ b/apps/wei/__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 = 'wei.apps.WeiConfig' diff --git a/apps/wei/admin.py b/apps/wei/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..f93a44edc7785fad3a356d6108d2cedd9c2beeea --- /dev/null +++ b/apps/wei/admin.py @@ -0,0 +1,13 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib import admin + +from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam + +admin.site.register(WEIClub) +admin.site.register(WEIRegistration) +admin.site.register(WEIMembership) +admin.site.register(WEIRole) +admin.site.register(Bus) +admin.site.register(BusTeam) diff --git a/apps/wei/api/__init__.py b/apps/wei/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/wei/api/serializers.py b/apps/wei/api/serializers.py new file mode 100644 index 0000000000000000000000000000000000000000..69254b752043c2b874c215351888fba013fe7349 --- /dev/null +++ b/apps/wei/api/serializers.py @@ -0,0 +1,72 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from rest_framework import serializers + +from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership + + +class WEIClubSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Clubs. + The djangorestframework plugin will analyse the model `WEIClub` and parse all fields in the API. + """ + + class Meta: + model = WEIClub + fields = '__all__' + + +class BusSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Bus. + The djangorestframework plugin will analyse the model `Bus` and parse all fields in the API. + """ + + class Meta: + model = Bus + fields = '__all__' + + +class BusTeamSerializer(serializers.ModelSerializer): + """ + REST API Serializer for Bus teams. + The djangorestframework plugin will analyse the model `BusTeam` and parse all fields in the API. + """ + + class Meta: + model = BusTeam + fields = '__all__' + + +class WEIRoleSerializer(serializers.ModelSerializer): + """ + REST API Serializer for WEI roles. + The djangorestframework plugin will analyse the model `WEIRole` and parse all fields in the API. + """ + + class Meta: + model = WEIRole + fields = '__all__' + + +class WEIRegistrationSerializer(serializers.ModelSerializer): + """ + REST API Serializer for WEI registrations. + The djangorestframework plugin will analyse the model `WEIRegistration` and parse all fields in the API. + """ + + class Meta: + model = WEIRegistration + fields = '__all__' + + +class WEIMembershipSerializer(serializers.ModelSerializer): + """ + REST API Serializer for WEI memberships. + The djangorestframework plugin will analyse the model `WEIMembership` and parse all fields in the API. + """ + + class Meta: + model = WEIMembership + fields = '__all__' diff --git a/apps/wei/api/urls.py b/apps/wei/api/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..713f5c7f5f6899317a1a3903dd46fa2f46464e01 --- /dev/null +++ b/apps/wei/api/urls.py @@ -0,0 +1,17 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .views import WEIClubViewSet, BusViewSet, BusTeamViewSet, WEIRoleViewSet, WEIRegistrationViewSet, \ + WEIMembershipViewSet + + +def register_wei_urls(router, path): + """ + Configure router for Member REST API. + """ + router.register(path + '/club', WEIClubViewSet) + router.register(path + '/bus', BusViewSet) + router.register(path + '/team', BusTeamViewSet) + router.register(path + '/role', WEIRoleViewSet) + router.register(path + '/registration', WEIRegistrationViewSet) + router.register(path + '/membership', WEIMembershipViewSet) diff --git a/apps/wei/api/views.py b/apps/wei/api/views.py new file mode 100644 index 0000000000000000000000000000000000000000..aaa1f141685538723d69a8050bfa01faefb43064 --- /dev/null +++ b/apps/wei/api/views.py @@ -0,0 +1,86 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later +from django_filters.rest_framework import DjangoFilterBackend +from rest_framework.filters import SearchFilter +from api.viewsets import ReadProtectedModelViewSet + +from .serializers import WEIClubSerializer, BusSerializer, BusTeamSerializer, WEIRoleSerializer, \ + WEIRegistrationSerializer, WEIMembershipSerializer +from ..models import WEIClub, Bus, BusTeam, WEIRole, WEIRegistration, WEIMembership + + +class WEIClubViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `WEIClub` objects, serialize it to JSON with the given serializer, + then render it on /api/wei/club/ + """ + queryset = WEIClub.objects.all() + serializer_class = WEIClubSerializer + filter_backends = [SearchFilter, DjangoFilterBackend] + search_fields = ['$name', ] + filterset_fields = ['name', 'year', ] + + +class BusViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `Bus` objects, serialize it to JSON with the given serializer, + then render it on /api/wei/bus/ + """ + queryset = Bus.objects + serializer_class = BusSerializer + filter_backends = [SearchFilter, DjangoFilterBackend] + search_fields = ['$name', ] + filterset_fields = ['name', 'wei', ] + + +class BusTeamViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer, + then render it on /api/wei/team/ + """ + queryset = BusTeam.objects + serializer_class = BusTeamSerializer + filter_backends = [SearchFilter, DjangoFilterBackend] + search_fields = ['$name', ] + filterset_fields = ['name', 'bus', 'bus__wei', ] + + +class WEIRoleViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `WEIRole` objects, serialize it to JSON with the given serializer, + then render it on /api/wei/role/ + """ + queryset = WEIRole.objects + serializer_class = WEIRoleSerializer + filter_backends = [SearchFilter] + search_fields = ['$name', ] + + +class WEIRegistrationViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all WEIRegistration objects, serialize it to JSON with the given serializer, + then render it on /api/wei/registration/ + """ + queryset = WEIRegistration.objects + serializer_class = WEIRegistrationSerializer + filter_backends = [SearchFilter, DjangoFilterBackend] + search_fields = ['$user__username', ] + filterset_fields = ['user', 'wei', ] + + +class WEIMembershipViewSet(ReadProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `BusTeam` objects, serialize it to JSON with the given serializer, + then render it on /api/wei/membership/ + """ + queryset = WEIMembership.objects + serializer_class = WEIMembershipSerializer + filter_backends = [SearchFilter, DjangoFilterBackend] + search_fields = ['$user__username', '$bus__name', '$team__name', ] + filterset_fields = ['user', 'club', 'bus', 'team', ] diff --git a/apps/wei/apps.py b/apps/wei/apps.py new file mode 100644 index 0000000000000000000000000000000000000000..233512830d39daf7d22605379c5f29b8c1e3519d --- /dev/null +++ b/apps/wei/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 WeiConfig(AppConfig): + name = 'wei' + verbose_name = _('WEI') diff --git a/apps/wei/forms/__init__.py b/apps/wei/forms/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..af948157e7299731585cf65beb269190325c18a2 --- /dev/null +++ b/apps/wei/forms/__init__.py @@ -0,0 +1,10 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .registration import WEIForm, WEIRegistrationForm, WEIMembershipForm, BusForm, BusTeamForm +from .surveys import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm, CurrentSurvey + +__all__ = [ + 'WEIForm', 'WEIRegistrationForm', 'WEIMembershipForm', 'BusForm', 'BusTeamForm', + 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', +] diff --git a/apps/wei/forms/registration.py b/apps/wei/forms/registration.py new file mode 100644 index 0000000000000000000000000000000000000000..96555372329de1061c5cf3867105ac155eecd4f9 --- /dev/null +++ b/apps/wei/forms/registration.py @@ -0,0 +1,124 @@ +# 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.models import User +from django.db.models import Q +from django.utils.translation import gettext_lazy as _ +from note_kfet.inputs import AmountInput, DatePickerInput, Autocomplete, ColorWidget + +from ..models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership, WEIRole + + +class WEIForm(forms.ModelForm): + class Meta: + model = WEIClub + exclude = ('parent_club', 'require_memberships', 'membership_duration', ) + widgets = { + "membership_fee_paid": AmountInput(), + "membership_fee_unpaid": AmountInput(), + "membership_start": DatePickerInput(), + "membership_end": DatePickerInput(), + "date_start": DatePickerInput(), + "date_end": DatePickerInput(), + } + + +class WEIRegistrationForm(forms.ModelForm): + class Meta: + model = WEIRegistration + exclude = ('wei', ) + widgets = { + "user": Autocomplete( + User, + attrs={ + 'api_url': '/api/user/', + 'name_field': 'username', + 'placeholder': 'Nom ...', + }, + ), + "birth_date": DatePickerInput(), + } + + +class WEIChooseBusForm(forms.Form): + bus = forms.ModelMultipleChoiceField( + queryset=Bus.objects, + label=_("bus"), + help_text=_("This choice is not definitive. The WEI organizers are free to attribute for you a bus and a team," + + " in particular if you are a free eletron."), + ) + + team = forms.ModelMultipleChoiceField( + queryset=BusTeam.objects, + label=_("Team"), + required=False, + help_text=_("Leave this field empty if you won't be in a team (staff, bus chief, free electron)"), + ) + + roles = forms.ModelMultipleChoiceField( + queryset=WEIRole.objects.filter(~Q(name="1A")), + label=_("WEI Roles"), + help_text=_("Select the roles that you are interested in."), + ) + + +class WEIMembershipForm(forms.ModelForm): + roles = forms.ModelMultipleChoiceField(queryset=WEIRole.objects, label=_("WEI Roles")) + + def clean(self): + cleaned_data = super().clean() + if cleaned_data["team"] is not None and cleaned_data["team"].bus != cleaned_data["bus"]: + self.add_error('bus', _("This team doesn't belong to the given bus.")) + return cleaned_data + + class Meta: + model = WEIMembership + fields = ('roles', 'bus', 'team',) + widgets = { + "bus": Autocomplete( + Bus, + attrs={ + 'api_url': '/api/wei/bus/', + 'placeholder': 'Bus ...', + } + ), + "team": Autocomplete( + BusTeam, + attrs={ + 'api_url': '/api/wei/team/', + 'placeholder': 'Équipe ...', + } + ), + } + + +class BusForm(forms.ModelForm): + class Meta: + model = Bus + fields = '__all__' + widgets = { + "wei": Autocomplete( + WEIClub, + attrs={ + 'api_url': '/api/wei/club/', + 'placeholder': 'WEI ...', + }, + ), + } + + +class BusTeamForm(forms.ModelForm): + class Meta: + model = BusTeam + fields = '__all__' + widgets = { + "bus": Autocomplete( + Bus, + attrs={ + 'api_url': '/api/wei/bus/', + 'placeholder': 'Bus ...', + }, + ), + "color": ColorWidget(), + } diff --git a/apps/wei/forms/surveys/__init__.py b/apps/wei/forms/surveys/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1e1dca56a7fc6502a220a69f79f82438e379c23c --- /dev/null +++ b/apps/wei/forms/surveys/__init__.py @@ -0,0 +1,12 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm +from .wei2020 import WEISurvey2020 + + +__all__ = [ + 'WEISurvey', 'WEISurveyInformation', 'WEISurveyAlgorithm', 'CurrentSurvey', +] + +CurrentSurvey = WEISurvey2020 diff --git a/apps/wei/forms/surveys/base.py b/apps/wei/forms/surveys/base.py new file mode 100644 index 0000000000000000000000000000000000000000..f43dafc2637017ec35ff6a5b8d86174f0b6abf96 --- /dev/null +++ b/apps/wei/forms/surveys/base.py @@ -0,0 +1,189 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Optional + +from django.db.models import QuerySet +from django.forms import Form + +from ...models import WEIClub, WEIRegistration, Bus + + +class WEISurveyInformation: + """ + Abstract data of the survey. + """ + valid = False + selected_bus_pk = None + selected_bus_name = None + + def __init__(self, registration): + self.__dict__.update(registration.information) + + def get_selected_bus(self) -> Optional[Bus]: + """ + If the algorithm ran, return the prefered bus according to the survey. + In the other case, return None. + """ + if not self.valid: + return None + return Bus.objects.get(pk=self.selected_bus_pk) + + def save(self, registration) -> None: + """ + Store the data of the survey into the database, with the information of the registration. + """ + registration.information = self.__dict__ + + +class WEIBusInformation: + """ + Abstract data of the bus. + """ + + def __init__(self, bus: Bus): + self.__dict__.update(bus.information) + self.bus = bus + + +class WEISurveyAlgorithm: + """ + Abstract algorithm that attributes a bus to each new member. + """ + + @classmethod + def get_survey_class(cls): + """ + The class of the survey associated with this algorithm. + """ + raise NotImplementedError + + @classmethod + def get_bus_information_class(cls): + """ + The class of the information associated to a bus extending WEIBusInformation. + Default: WEIBusInformation (contains nothing) + """ + return WEIBusInformation + + @classmethod + def get_registrations(cls) -> QuerySet: + """ + Queryset of all first year registrations + """ + return WEIRegistration.objects.filter(wei__year=cls.get_survey_class().get_year(), first_year=True) + + @classmethod + def get_buses(cls) -> QuerySet: + """ + Queryset of all buses of the associated wei. + """ + return Bus.objects.filter(wei__year=cls.get_survey_class().get_year()) + + @classmethod + def get_bus_information(cls, bus): + """ + Return the WEIBusInformation object containing the data stored in a given bus. + """ + return cls.get_bus_information_class()(bus) + + def run_algorithm(self) -> None: + """ + Once this method implemented, run the algorithm that attributes a bus to each first year member. + This method can be run in command line through ``python manage.py wei_algorithm`` + See ``wei.management.commmands.wei_algorithm`` + This method must call Survey.select_bus for each survey. + """ + raise NotImplementedError + + +class WEISurvey: + """ + Survey associated to a first year WEI registration. + The data is stored into WEISurveyInformation, this class acts as a manager. + This is an abstract class: this has to be extended each year to implement custom methods. + """ + + def __init__(self, registration: WEIRegistration): + self.registration = registration + self.information = self.get_survey_information_class()(registration) + + @classmethod + def get_year(cls) -> int: + """ + Get year of the wei concerned by the type of the survey. + """ + raise NotImplementedError + + @classmethod + def get_wei(cls) -> WEIClub: + """ + The WEI associated to this kind of survey. + """ + return WEIClub.objects.get(year=cls.get_year()) + + @classmethod + def get_survey_information_class(cls): + """ + The class of the data (extending WEISurveyInformation). + """ + raise NotImplementedError + + def get_form_class(self) -> Form: + """ + The form class of the survey. + This is proper to the status of the survey: the form class can evolve according to the progress of the survey. + """ + raise NotImplementedError + + def update_form(self, form) -> None: + """ + Once the form is instanciated, the information can be updated with the information of the registration + and the information of the survey. + This method is called once the form is created. + """ + pass + + def form_valid(self, form) -> None: + """ + Called when the information of the form are validated. + This method should update the information of the survey. + """ + raise NotImplementedError + + def is_complete(self) -> bool: + """ + Return True if the survey is complete. + If the survey is complete, then the button "Next" will display some text for the end of the survey. + If not, the survey is reloaded and continues. + """ + raise NotImplementedError + + def save(self) -> None: + """ + Store the information of the survey into the database. + """ + self.information.save(self.registration) + # The information is forced-saved. + # We don't want that anyone can update manually the information, so since most users don't have the + # right to save the information of a registration, we force save. + self.registration._force_save = True + self.registration.save() + + @classmethod + def get_algorithm_class(cls): + """ + Algorithm class associated to the survey. + The algorithm, extending WEISurveyAlgorithm, should associate a bus to each first year member. + The association is not permanent: that's only a suggestion. + """ + raise NotImplementedError + + def select_bus(self, bus) -> None: + """ + Set the suggestion into the data of the membership. + :param bus: The bus suggested. + """ + self.information.selected_bus_pk = bus.pk + self.information.selected_bus_name = bus.name + self.information.valid = True diff --git a/apps/wei/forms/surveys/wei2020.py b/apps/wei/forms/surveys/wei2020.py new file mode 100644 index 0000000000000000000000000000000000000000..4f60f6d4978f2a839ff8a3a5e871509d07a1cc67 --- /dev/null +++ b/apps/wei/forms/surveys/wei2020.py @@ -0,0 +1,89 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django import forms + +from .base import WEISurvey, WEISurveyInformation, WEISurveyAlgorithm +from ...models import Bus + + +class WEISurveyForm2020(forms.Form): + """ + Survey form for the year 2020. + For now, that's only a Bus selector. + TODO: Do a better survey (later) + """ + bus = forms.ModelChoiceField( + Bus.objects, + ) + + def set_registration(self, registration): + """ + Filter the bus selector with the buses of the current WEI. + """ + self.fields["bus"].queryset = Bus.objects.filter(wei=registration.wei) + + +class WEISurveyInformation2020(WEISurveyInformation): + """ + We store the id of the selected bus. We store only the name, but is not used in the selection: + that's only for humans that try to read data. + """ + chosen_bus_pk = None + chosen_bus_name = None + + +class WEISurvey2020(WEISurvey): + """ + Survey for the year 2020. + """ + @classmethod + def get_year(cls): + return 2020 + + @classmethod + def get_survey_information_class(cls): + return WEISurveyInformation2020 + + def get_form_class(self): + return WEISurveyForm2020 + + def update_form(self, form): + """ + Filter the bus selector with the buses of the WEI. + """ + form.set_registration(self.registration) + + def form_valid(self, form): + bus = form.cleaned_data["bus"] + self.information.chosen_bus_pk = bus.pk + self.information.chosen_bus_name = bus.name + self.save() + + @classmethod + def get_algorithm_class(cls): + return WEISurveyAlgorithm2020 + + def is_complete(self) -> bool: + """ + The survey is complete once the bus is chosen. + """ + return self.information.chosen_bus_pk is not None + + +class WEISurveyAlgorithm2020(WEISurveyAlgorithm): + """ + The algorithm class for the year 2020. + For now, the algorithm is quite simple: the selected bus is the chosen bus. + TODO: Improve this algorithm. + """ + + @classmethod + def get_survey_class(cls): + return WEISurvey2020 + + def run_algorithm(self): + for registration in self.get_registrations(): + survey = self.get_survey_class()(registration) + survey.select_bus(Bus.objects.get(pk=survey.information.chosen_bus_pk)) + survey.save() diff --git a/apps/wei/migrations/__init__.py b/apps/wei/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/apps/wei/models.py b/apps/wei/models.py new file mode 100644 index 0000000000000000000000000000000000000000..9cee0d610222c7e38da1c4c59ba22a59f01c5a0f --- /dev/null +++ b/apps/wei/models.py @@ -0,0 +1,329 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +from datetime import date + +from django.conf import settings +from django.contrib.auth.models import User +from django.db import models +from django.utils.translation import gettext_lazy as _ +from member.models import Role, Club, Membership +from note.models import MembershipTransaction + + +class WEIClub(Club): + """ + The WEI is a club. Register to the WEI is equivalent than be member of the club. + """ + year = models.PositiveIntegerField( + unique=True, + default=date.today().year, + verbose_name=_("year"), + ) + + date_start = models.DateField( + verbose_name=_("date start"), + ) + + date_end = models.DateField( + verbose_name=_("date end"), + ) + + @property + def is_current_wei(self): + """ + We consider that this is the current WEI iff there is no future WEI planned. + """ + return not WEIClub.objects.filter(date_start__gt=self.date_start).exists() + + def update_membership_dates(self): + """ + We can't join the WEI next years. + """ + return + + class Meta: + verbose_name = _("WEI") + verbose_name_plural = _("WEI") + + +class Bus(models.Model): + """ + The best bus for the best WEI + """ + wei = models.ForeignKey( + WEIClub, + on_delete=models.PROTECT, + related_name="buses", + verbose_name=_("WEI"), + ) + + name = models.CharField( + max_length=255, + verbose_name=_("name"), + ) + + description = models.TextField( + blank=True, + default="", + verbose_name=_("description"), + ) + + information_json = models.TextField( + default="{}", + verbose_name=_("survey information"), + help_text=_("Information about the survey for new members, encoded in JSON"), + ) + + @property + def information(self): + """ + The information about the survey for new members are stored in a dictionary that can evolve following the years. + The dictionary is stored as a JSON string. + """ + return json.loads(self.information_json) + + @information.setter + def information(self, information): + """ + Store information as a JSON string + """ + self.information_json = json.dumps(information) + + def __str__(self): + return self.name + + class Meta: + verbose_name = _("Bus") + verbose_name_plural = _("Buses") + unique_together = ('wei', 'name',) + + +class BusTeam(models.Model): + """ + A bus has multiple teams + """ + bus = models.ForeignKey( + Bus, + on_delete=models.CASCADE, + related_name="teams", + verbose_name=_("bus"), + ) + + name = models.CharField( + max_length=255, + ) + + color = models.PositiveIntegerField( # Use a color picker to get the hexa code + verbose_name=_("color"), + help_text=_("The color of the T-Shirt, stored with its number equivalent"), + ) + + description = models.TextField( + blank=True, + default="", + verbose_name=_("description"), + ) + + def __str__(self): + return self.name + " (" + str(self.bus) + ")" + + class Meta: + unique_together = ('bus', 'name',) + verbose_name = _("Bus team") + verbose_name_plural = _("Bus teams") + + +class WEIRole(Role): + """ + A Role for the WEI can be bus chief, team chief, free electron, ... + """ + + class Meta: + verbose_name = _("WEI Role") + verbose_name_plural = _("WEI Roles") + + +class WEIRegistration(models.Model): + """ + Store personal data that can be useful for the WEI. + """ + + user = models.ForeignKey( + User, + on_delete=models.PROTECT, + related_name="wei", + verbose_name=_("user"), + ) + + wei = models.ForeignKey( + WEIClub, + on_delete=models.PROTECT, + related_name="users", + verbose_name=_("WEI"), + ) + + soge_credit = models.BooleanField( + default=False, + verbose_name=_("Credit from Société générale"), + ) + + caution_check = models.BooleanField( + default=False, + verbose_name=_("Caution check given") + ) + + birth_date = models.DateField( + verbose_name=_("birth date"), + ) + + gender = models.CharField( + max_length=16, + choices=( + ('male', _("Male")), + ('female', _("Female")), + ('nonbinary', _("Non binary")), + ), + verbose_name=_("gender"), + ) + + health_issues = models.TextField( + blank=True, + default="", + verbose_name=_("health issues"), + ) + + emergency_contact_name = models.CharField( + max_length=255, + verbose_name=_("emergency contact name"), + ) + + emergency_contact_phone = models.CharField( + max_length=32, + verbose_name=_("emergency contact phone"), + ) + + ml_events_registration = models.BooleanField( + default=False, + verbose_name=_("Register on the mailing list to stay informed of the events of the campus (1 mail/week)"), + ) + + ml_sport_registration = models.BooleanField( + default=False, + verbose_name=_("Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)"), + ) + + ml_art_registration = models.BooleanField( + default=False, + verbose_name=_("Register on the mailing list to stay informed of the art events of the campus (1 mail/week)"), + ) + + first_year = models.BooleanField( + default=False, + verbose_name=_("first year"), + help_text=_("Tells if the user is new in the school.") + ) + + information_json = models.TextField( + default="{}", + verbose_name=_("registration information"), + help_text=_("Information about the registration (buses for old members, survey fot the new members), " + "encoded in JSON"), + ) + + @property + def information(self): + """ + The information about the registration (the survey for the new members, the bus for the older members, ...) + are stored in a dictionary that can evolve following the years. The dictionary is stored as a JSON string. + """ + return json.loads(self.information_json) + + @information.setter + def information(self, information): + """ + Store information as a JSON string + """ + self.information_json = json.dumps(information) + + @property + def is_validated(self): + try: + return self.membership is not None + except AttributeError: + return False + + def __str__(self): + return str(self.user) + + class Meta: + unique_together = ('user', 'wei',) + verbose_name = _("WEI User") + verbose_name_plural = _("WEI Users") + + +class WEIMembership(Membership): + bus = models.ForeignKey( + Bus, + on_delete=models.PROTECT, + related_name="memberships", + null=True, + default=None, + verbose_name=_("bus"), + ) + + team = models.ForeignKey( + BusTeam, + on_delete=models.PROTECT, + related_name="memberships", + null=True, + blank=True, + default=None, + verbose_name=_("team"), + ) + + registration = models.OneToOneField( + WEIRegistration, + on_delete=models.PROTECT, + null=True, + blank=True, + default=None, + related_name="membership", + verbose_name=_("WEI registration"), + ) + + class Meta: + verbose_name = _("WEI membership") + verbose_name_plural = _("WEI memberships") + + 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 WEI " + self.club.name, + valid=not self.registration.soge_credit # Soge transactions are by default invalidated + ) + transaction._force_save = True + transaction.save(force_insert=True) + + if self.registration.soge_credit and "treasury" in settings.INSTALLED_APPS: + # If the soge pays, then the transaction is unvalidated in a first time, then submitted for control + # to treasurers. + transaction.refresh_from_db() + from treasury.models import SogeCredit + soge_credit = SogeCredit.objects.get_or_create(user=self.user)[0] + soge_credit.refresh_from_db() + transaction.save() + soge_credit.transactions.add(transaction) + soge_credit.save() diff --git a/apps/wei/tables.py b/apps/wei/tables.py new file mode 100644 index 0000000000000000000000000000000000000000..36d0934215cd1b72ee3159b491f13edd1e05af45 --- /dev/null +++ b/apps/wei/tables.py @@ -0,0 +1,201 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import django_tables2 as tables +from django.urls import reverse_lazy +from django.utils.translation import gettext_lazy as _ +from django_tables2 import A + +from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership + + +class WEITable(tables.Table): + """ + List all WEI. + """ + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = WEIClub + template_name = 'django_tables2/bootstrap4.html' + fields = ('name', 'year', 'date_start', 'date_end',) + row_attrs = { + 'class': 'table-row', + 'id': lambda record: "row-" + str(record.pk), + 'data-href': lambda record: reverse_lazy('wei:wei_detail', args=(record.pk,)) + } + + +class WEIRegistrationTable(tables.Table): + """ + List all WEI registrations. + """ + user = tables.LinkColumn( + 'member:user_detail', + args=[A('user.pk')], + ) + + edit = tables.LinkColumn( + 'wei:wei_update_registration', + args=[A('pk')], + verbose_name=_("Edit"), + text=_("Edit"), + attrs={ + 'a': { + 'class': 'btn btn-warning' + } + } + ) + validate = tables.LinkColumn( + 'wei:validate_registration', + args=[A('pk')], + verbose_name=_("Validate"), + text=_("Validate"), + attrs={ + 'a': { + 'class': 'btn btn-success' + } + } + ) + + delete = tables.LinkColumn( + 'wei:wei_delete_registration', + args=[A('pk')], + verbose_name=_("delete"), + text=_("Delete"), + attrs={ + 'a': { + 'class': 'btn btn-danger' + } + }, + ) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = WEIRegistration + template_name = 'django_tables2/bootstrap4.html' + fields = ('user', 'user.first_name', 'user.last_name', 'first_year',) + row_attrs = { + 'class': 'table-row', + 'id': lambda record: "row-" + str(record.pk), + 'data-href': lambda record: record.pk + } + + +class WEIMembershipTable(tables.Table): + user = tables.LinkColumn( + 'wei:wei_update_registration', + args=[A('registration.pk')], + ) + + year = tables.Column( + accessor=A("pk"), + verbose_name=_("Year"), + ) + + bus = tables.LinkColumn( + 'wei:manage_bus', + args=[A('bus.pk')], + ) + + team = tables.LinkColumn( + 'wei:manage_bus_team', + args=[A('bus.pk')], + ) + + def render_year(self, record): + return str(record.user.profile.ens_year) + "A" + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = WEIMembership + template_name = 'django_tables2/bootstrap4.html' + fields = ('user', 'user.last_name', 'user.first_name', 'registration.gender', 'user.profile.department', + 'year', 'bus', 'team', ) + row_attrs = { + 'class': 'table-row', + 'id': lambda record: "row-" + str(record.pk), + } + + +class BusTable(tables.Table): + name = tables.LinkColumn( + 'wei:manage_bus', + args=[A('pk')], + ) + + teams = tables.Column( + accessor=A("teams"), + verbose_name=_("Teams"), + attrs={ + "td": { + "class": "text-truncate", + } + } + ) + + count = tables.Column( + verbose_name=_("Members count"), + ) + + def render_teams(self, value): + return ", ".join(team.name for team in value.all()) + + def render_count(self, value): + return str(value) + " " + (str(_("members")) if value > 0 else str(_("member"))) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = Bus + template_name = 'django_tables2/bootstrap4.html' + fields = ('name', 'teams', ) + row_attrs = { + 'class': 'table-row', + 'id': lambda record: "row-" + str(record.pk), + } + + +class BusTeamTable(tables.Table): + name = tables.LinkColumn( + 'wei:manage_bus_team', + args=[A('pk')], + ) + + color = tables.Column( + attrs={ + "td": { + "style": lambda record: "background-color: #{:06X}; color: #{:06X};" + .format(record.color, 0xFFFFFF - record.color, ) + } + } + ) + + def render_count(self, value): + return str(value) + " " + (str(_("members")) if value > 0 else str(_("member"))) + + count = tables.Column( + verbose_name=_("Members count"), + ) + + def render_color(self, value): + return "#{:06X}".format(value) + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover' + } + model = BusTeam + template_name = 'django_tables2/bootstrap4.html' + fields = ('name', 'color',) + row_attrs = { + 'class': 'table-row', + 'id': lambda record: "row-" + str(record.pk), + 'data-href': lambda record: reverse_lazy('wei:manage_bus_team', args=(record.pk, )) + } diff --git a/apps/wei/urls.py b/apps/wei/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..7cf91a60c6862c25f4dc524b937b14f5e38ac0da --- /dev/null +++ b/apps/wei/urls.py @@ -0,0 +1,43 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from .views import CurrentWEIDetailView, WEIListView, WEICreateView, WEIDetailView, WEIUpdateView,\ + WEIRegistrationsView, WEIMembershipsView, MemberListRenderView,\ + BusCreateView, BusManageView, BusUpdateView, BusTeamCreateView, BusTeamManageView, BusTeamUpdateView,\ + WEIRegister1AView, WEIRegister2AView, WEIUpdateRegistrationView, WEIDeleteRegistrationView,\ + WEIValidateRegistrationView, WEISurveyView, WEISurveyEndView, WEIClosedView + + +app_name = 'wei' +urlpatterns = [ + path('detail/', CurrentWEIDetailView.as_view(), name="current_wei_detail"), + path('list/', WEIListView.as_view(), name="wei_list"), + path('create/', WEICreateView.as_view(), name="wei_create"), + path('detail/<int:pk>/', WEIDetailView.as_view(), name="wei_detail"), + path('update/<int:pk>/', WEIUpdateView.as_view(), name="wei_update"), + path('detail/<int:pk>/registrations/', WEIRegistrationsView.as_view(), name="wei_registrations"), + path('detail/<int:pk>/memberships/', WEIMembershipsView.as_view(), name="wei_memberships"), + path('detail/<int:wei_pk>/memberships/pdf/', MemberListRenderView.as_view(), name="wei_memberships_pdf"), + path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/', MemberListRenderView.as_view(), + name="wei_memberships_bus_pdf"), + path('detail/<int:wei_pk>/memberships/pdf/<int:bus_pk>/<int:team_pk>/', MemberListRenderView.as_view(), + name="wei_memberships_team_pdf"), + path('add-bus/<int:pk>/', BusCreateView.as_view(), name="add_bus"), + path('manage-bus/<int:pk>/', BusManageView.as_view(), name="manage_bus"), + path('update-bus/<int:pk>/', BusUpdateView.as_view(), name="update_bus"), + path('add-bus-team/<int:pk>/', BusTeamCreateView.as_view(), name="add_team"), + path('manage-bus-team/<int:pk>/', BusTeamManageView.as_view(), name="manage_bus_team"), + path('update-bus-team/<int:pk>/', BusTeamUpdateView.as_view(), name="update_bus_team"), + path('register/<int:wei_pk>/1A/', WEIRegister1AView.as_view(), name="wei_register_1A"), + path('register/<int:wei_pk>/2A+/', WEIRegister2AView.as_view(), name="wei_register_2A"), + path('register/<int:wei_pk>/1A/myself/', WEIRegister1AView.as_view(), name="wei_register_1A_myself"), + path('register/<int:wei_pk>/2A+/myself/', WEIRegister2AView.as_view(), name="wei_register_2A_myself"), + path('edit-registration/<int:pk>/', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"), + path('delete-registration/<int:pk>/', WEIDeleteRegistrationView.as_view(), name="wei_delete_registration"), + path('validate/<int:pk>/', WEIValidateRegistrationView.as_view(), name="validate_registration"), + path('survey/<int:pk>/', WEISurveyView.as_view(), name="wei_survey"), + path('survey/<int:pk>/end/', WEISurveyEndView.as_view(), name="wei_survey_end"), + path('detail/<int:pk>/closed/', WEIClosedView.as_view(), name="wei_closed"), +] diff --git a/apps/wei/views.py b/apps/wei/views.py new file mode 100644 index 0000000000000000000000000000000000000000..597a44d4adba548945089b061d507c23c3e82cd7 --- /dev/null +++ b/apps/wei/views.py @@ -0,0 +1,946 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +import os +import shutil +import subprocess +from datetime import datetime, date +from tempfile import mkdtemp + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.contrib.auth.models import User +from django.core.exceptions import PermissionDenied +from django.db.models import Q, Count +from django.db.models.functions.text import Lower +from django.forms import HiddenInput +from django.http import HttpResponse +from django.shortcuts import redirect +from django.template.loader import render_to_string +from django.urls import reverse_lazy +from django.views import View +from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView +from django.utils.translation import gettext_lazy as _ +from django.views.generic.edit import BaseFormView, DeleteView +from django_tables2 import SingleTableView +from member.models import Membership, Club +from note.models import Transaction, NoteClub, Alias +from note.tables import HistoryTable +from note_kfet.settings import BASE_DIR +from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin + +from .forms.registration import WEIChooseBusForm +from .models import WEIClub, WEIRegistration, WEIMembership, Bus, BusTeam, WEIRole +from .forms import WEIForm, WEIRegistrationForm, BusForm, BusTeamForm, WEIMembershipForm, CurrentSurvey +from .tables import WEITable, WEIRegistrationTable, BusTable, BusTeamTable, WEIMembershipTable + + +class CurrentWEIDetailView(LoginRequiredMixin, RedirectView): + def get_redirect_url(self, *args, **kwargs): + wei = WEIClub.objects.filter(membership_start__lte=date.today()).order_by('date_start') + if wei.exists(): + wei = wei.last() + return reverse_lazy('wei:wei_detail', args=(wei.pk,)) + else: + return reverse_lazy('wei:wei_list') + + +class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + List existing WEI + """ + model = WEIClub + table_class = WEITable + ordering = '-year' + + +class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + """ + Create WEI + """ + model = WEIClub + form_class = WEIForm + + def form_valid(self, form): + form.instance.requires_membership = True + form.instance.parent_club = Club.objects.get(name="Kfet") + ret = super().form_valid(form) + NoteClub.objects.create(club=form.instance) + return ret + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk}) + + +class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + View WEI information + """ + model = WEIClub + context_object_name = "club" + + 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)) \ + .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) \ + .order_by('-created_at', '-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 = WEIMembership.objects.filter( + club=club, + date_end__gte=datetime.today(), + ).filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view")) + membership_table = WEIMembershipTable(data=club_member, prefix="membership-") + membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1)) + context['member_list'] = membership_table + + pre_registrations = WEIRegistration.objects.filter( + PermissionBackend.filter_queryset(self.request.user, WEIRegistration, "view")).filter( + membership=None, + wei=club + ) + pre_registrations_table = WEIRegistrationTable(data=pre_registrations, prefix="pre-registration-") + pre_registrations_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1)) + context['pre_registrations'] = pre_registrations_table + + my_registration = WEIRegistration.objects.filter(wei=club, user=self.request.user) + if my_registration.exists(): + my_registration = my_registration.get() + else: + my_registration = None + context["my_registration"] = my_registration + + buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \ + .filter(wei=self.object).annotate(count=Count("memberships")) + bus_table = BusTable(data=buses, prefix="bus-") + context['buses'] = bus_table + + random_user = User.objects.filter(~Q(wei__wei__in=[club])).first() + + if random_user is None: + # This case occurs when all users are registered to the WEI. + # Don't worry, Pikachu never went to the WEI. + # This bug can arrive only in dev mode. + context["can_add_first_year_member"] = True + context["can_add_any_member"] = True + else: + # Check if the user has the right to create a registration of a random first year member. + empty_fy_registration = WEIRegistration( + user=random_user, + first_year=True, + birth_date="1970-01-01", + gender="No", + emergency_contact_name="No", + emergency_contact_phone="No", + ) + context["can_add_first_year_member"] = PermissionBackend \ + .check_perm(self.request.user, "wei.add_weiregistration", empty_fy_registration) + + # Check if the user has the right to create a registration of a random old member. + empty_old_registration = WEIRegistration( + user=User.objects.filter(~Q(wei__wei__in=[club])).first(), + first_year=False, + birth_date="1970-01-01", + gender="No", + emergency_contact_name="No", + emergency_contact_phone="No", + ) + context["can_add_any_member"] = PermissionBackend \ + .check_perm(self.request.user, "wei.add_weiregistration", empty_old_registration) + + empty_bus = Bus( + wei=club, + name="", + ) + context["can_add_bus"] = PermissionBackend.check_perm(self.request.user, "wei.add_bus", empty_bus) + + context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists() + + return context + + +class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + List all WEI memberships + """ + model = WEIMembership + table_class = WEIMembershipTable + + def dispatch(self, request, *args, **kwargs): + self.club = WEIClub.objects.get(pk=self.kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs).filter(club=self.club) + + pattern = self.request.GET.get("search", "") + + if not pattern: + return qs.none() + + qs = qs.filter( + Q(user__first_name__iregex=pattern) + | Q(user__last_name__iregex=pattern) + | Q(user__note__alias__name__iregex="^" + pattern) + | Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + | Q(bus__name__iregex=pattern) + | Q(team__name__iregex=pattern) + ) + + return qs[:20] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.club + context["title"] = _("Find WEI Membership") + return context + + +class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): + """ + List all non-validated WEI registrations. + """ + model = WEIRegistration + table_class = WEIRegistrationTable + + def dispatch(self, request, *args, **kwargs): + self.club = WEIClub.objects.get(pk=self.kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) + + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None) + + pattern = self.request.GET.get("search", "") + + if not pattern: + return qs.none() + + qs = qs.filter( + Q(user__first_name__iregex=pattern) + | Q(user__last_name__iregex=pattern) + | Q(user__note__alias__name__iregex="^" + pattern) + | Q(user__note__alias__normalized_name__iregex="^" + Alias.normalize(pattern)) + ) + + return qs[:20] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.club + context["title"] = _("Find WEI Registration") + return context + + +class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Update the information of the WEI. + """ + model = WEIClub + context_object_name = "club" + form_class = WEIForm + + def dispatch(self, request, *args, **kwargs): + wei = self.get_object() + today = date.today() + # We can't update a past WEI + # But we can update it while it is not officially opened + if today > wei.membership_end: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_success_url(self): + return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.pk}) + + +class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + """ + Create Bus + """ + model = Bus + form_class = BusForm + + def dispatch(self, request, *args, **kwargs): + wei = WEIClub.objects.get(pk=self.kwargs["pk"]) + today = date.today() + # We can't add a bus once the WEI is started + if today >= wei.date_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"]) + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields["wei"].initial = WEIClub.objects.get(pk=self.kwargs["pk"]) + return form + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk}) + + +class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Update Bus + """ + model = Bus + form_class = BusForm + + def dispatch(self, request, *args, **kwargs): + wei = self.get_object().wei + today = date.today() + # We can't update a bus once the WEI is started + if today >= wei.date_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.object.wei + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields["wei"].disabled = True + return form + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.pk}) + + +class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + Manage Bus + """ + model = Bus + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.object.wei + + bus = self.object + teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \ + .filter(bus=bus).annotate(count=Count("memberships")) + teams_table = BusTeamTable(data=teams, prefix="team-") + context["teams"] = teams_table + + memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset( + self.request.user, WEIMembership, "view")).filter(bus=bus) + memberships_table = WEIMembershipTable(data=memberships, prefix="membership-") + memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1)) + context["memberships"] = memberships_table + + return context + + +class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + """ + Create BusTeam + """ + model = BusTeam + form_class = BusTeamForm + + def dispatch(self, request, *args, **kwargs): + wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"]) + today = date.today() + # We can't add a team once the WEI is started + if today >= wei.date_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + bus = Bus.objects.get(pk=self.kwargs["pk"]) + context["club"] = bus.wei + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields["bus"].initial = Bus.objects.get(pk=self.kwargs["pk"]) + return form + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:manage_bus", kwargs={"pk": self.object.bus.pk}) + + +class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Update Bus team + """ + model = BusTeam + form_class = BusTeamForm + + def dispatch(self, request, *args, **kwargs): + wei = self.get_object().bus.wei + today = date.today() + # We can't update a bus once the WEI is started + if today >= wei.date_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.object.bus.wei + context["bus"] = self.object.bus + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields["bus"].disabled = True + return form + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:manage_bus_team", kwargs={"pk": self.object.pk}) + + +class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): + """ + Manage Bus team + """ + model = BusTeam + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["bus"] = self.object.bus + context["club"] = self.object.bus.wei + + memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset( + self.request.user, WEIMembership, "view")).filter(team=self.object) + memberships_table = WEIMembershipTable(data=memberships, prefix="membership-") + memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1)) + context["memberships"] = memberships_table + + return context + + +class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + """ + Register a new user to the WEI + """ + model = WEIRegistration + form_class = WEIRegistrationForm + + def dispatch(self, request, *args, **kwargs): + wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) + today = date.today() + # We can't register someone once the WEI is started and before the membership start date + if today >= wei.date_start or today < wei.membership_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = _("Register 1A") + context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) + if "myself" in self.request.path: + context["form"].fields["user"].disabled = True + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields["user"].initial = self.request.user + del form.fields["first_year"] + del form.fields["caution_check"] + del form.fields["information_json"] + return form + + def form_valid(self, form): + form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) + form.instance.first_year = True + + if not form.instance.pk: + # Check if the user is not already registered to the WEI + if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists(): + form.add_error('user', _("This user is already registered to this WEI.")) + return self.form_invalid(form) + + # Check if the user can be in her/his first year (yeah, no cheat) + if WEIRegistration.objects.filter(user=form.instance.user).exists(): + form.add_error('user', _("This user can't be in her/his first year since he/she has already" + " participed to a WEI.")) + return self.form_invalid(form) + + return super().form_valid(form) + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) + + +class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + """ + Register an old user to the WEI + """ + model = WEIRegistration + form_class = WEIRegistrationForm + + def dispatch(self, request, *args, **kwargs): + wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) + today = date.today() + # We can't register someone once the WEI is started and before the membership start date + if today >= wei.date_start or today < wei.membership_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['title'] = _("Register 2A+") + context['club'] = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) + + if "myself" in self.request.path: + context["form"].fields["user"].disabled = True + + choose_bus_form = WEIChooseBusForm() + choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"]) + choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"]) + context['membership_form'] = choose_bus_form + + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields["user"].initial = self.request.user + if "myself" in self.request.path and self.request.user.profile.soge: + form.fields["soge_credit"].disabled = True + form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") + + del form.fields["caution_check"] + del form.fields["first_year"] + del form.fields["ml_events_registration"] + del form.fields["ml_art_registration"] + del form.fields["ml_sport_registration"] + del form.fields["information_json"] + + return form + + def form_valid(self, form): + form.instance.wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) + form.instance.first_year = False + + if not form.instance.pk: + # Check if the user is not already registered to the WEI + if WEIRegistration.objects.filter(wei=form.instance.wei, user=form.instance.user).exists(): + form.add_error('user', _("This user is already registered to this WEI.")) + return self.form_invalid(form) + + choose_bus_form = WEIChooseBusForm(self.request.POST) + if not choose_bus_form.is_valid(): + return self.form_invalid(form) + + information = form.instance.information + information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]] + information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]] + information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]] + information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]] + information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] + information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] + form.instance.information = information + form.instance.save() + + return super().form_valid(form) + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) + + +class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): + """ + Update a registration for the WEI + """ + model = WEIRegistration + form_class = WEIRegistrationForm + + def get_queryset(self, **kwargs): + return WEIRegistration.objects + + def dispatch(self, request, *args, **kwargs): + wei = self.get_object().wei + today = date.today() + # We can't update a registration once the WEI is started and before the membership start date + if today >= wei.date_start or today < wei.membership_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.object.wei + + if self.object.is_validated: + membership_form = WEIMembershipForm(instance=self.object.membership) + for field_name, field in membership_form.fields.items(): + if not PermissionBackend.check_perm( + self.request.user, "wei.change_membership_" + field_name, self.object.membership): + field.widget = HiddenInput() + context["membership_form"] = membership_form + elif not self.object.first_year and PermissionBackend.check_perm( + self.request.user, "wei.change_weiregistration_information_json", self.object): + choose_bus_form = WEIChooseBusForm( + dict( + bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(), + team=BusTeam.objects.filter(pk__in=self.object.information["preferred_team_pk"]).all(), + roles=WEIRole.objects.filter(pk__in=self.object.information["preferred_roles_pk"]).all(), + ) + ) + choose_bus_form.fields["bus"].queryset = Bus.objects.filter(wei=context["club"]) + choose_bus_form.fields["team"].queryset = BusTeam.objects.filter(bus__wei=context["club"]) + context["membership_form"] = choose_bus_form + + if not self.object.soge_credit and self.object.user.profile.soge: + form = context["form"] + form.fields["soge_credit"].disabled = True + form.fields["soge_credit"].help_text = _("You already opened an account in the Société générale.") + + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + form.fields["user"].disabled = True + if not self.object.first_year: + del form.fields["information_json"] + return form + + def form_valid(self, form): + # If the membership is already validated, then we update the bus and the team (and the roles) + if form.instance.is_validated: + membership_form = WEIMembershipForm(self.request.POST, instance=form.instance.membership) + if not membership_form.is_valid(): + return self.form_invalid(form) + membership_form.save() + # If it is not validated and if this is an old member, then we update the choices + elif not form.instance.first_year and PermissionBackend.check_perm( + self.request.user, "wei.change_weiregistration_information_json", self.object): + choose_bus_form = WEIChooseBusForm(self.request.POST) + if not choose_bus_form.is_valid(): + return self.form_invalid(form) + information = form.instance.information + information["preferred_bus_pk"] = [bus.pk for bus in choose_bus_form.cleaned_data["bus"]] + information["preferred_bus_name"] = [bus.name for bus in choose_bus_form.cleaned_data["bus"]] + information["preferred_team_pk"] = [team.pk for team in choose_bus_form.cleaned_data["team"]] + information["preferred_team_name"] = [team.name for team in choose_bus_form.cleaned_data["team"]] + information["preferred_roles_pk"] = [role.pk for role in choose_bus_form.cleaned_data["roles"]] + information["preferred_roles_name"] = [role.name for role in choose_bus_form.cleaned_data["roles"]] + form.instance.information = information + form.instance.save() + + return super().form_valid(form) + + def get_success_url(self): + self.object.refresh_from_db() + if self.object.first_year: + survey = CurrentSurvey(self.object) + if not survey.is_complete(): + return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk}) + return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.wei.pk}) + + +class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, DeleteView): + """ + Delete a non-validated WEI registration + """ + model = WEIRegistration + + def dispatch(self, request, *args, **kwargs): + object = self.get_object() + wei = object.wei + today = date.today() + # We can't delete a registration of a past WEI + if today > wei.membership_end: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + + if not PermissionBackend.check_perm(self.request.user, "wei.delete_weiregistration", object): + raise PermissionDenied(_("You don't have the right to delete this WEI registration.")) + + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.object.wei + return context + + def get_success_url(self): + return reverse_lazy('wei:wei_detail', args=(self.object.wei.pk,)) + + +class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): + """ + Validate WEI Registration + """ + model = WEIMembership + form_class = WEIMembershipForm + + def dispatch(self, request, *args, **kwargs): + wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei + today = date.today() + # We can't validate anyone once the WEI is started and before the membership start date + if today >= wei.date_start or today < wei.membership_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + + registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) + context["registration"] = registration + survey = CurrentSurvey(registration) + if survey.information.valid: + context["suggested_bus"] = survey.information.get_selected_bus() + context["club"] = registration.wei + context["fee"] = registration.wei.membership_fee_paid if registration.user.profile.paid \ + else registration.wei.membership_fee_unpaid + context["kfet_member"] = Membership.objects.filter( + club__name="Kfet", + user=registration.user, + date_start__lte=datetime.now().date(), + date_end__gte=datetime.now().date(), + ).exists() + + return context + + def get_form(self, form_class=None): + form = super().get_form(form_class) + registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) + form.fields["bus"].widget.attrs["api_url"] = "/api/wei/bus/?wei=" + str(registration.wei.pk) + if registration.first_year: + # Use the results of the survey to fill initial data + # A first year has no other role than "1A" + del form.fields["roles"] + survey = CurrentSurvey(registration) + if survey.information.valid: + form.fields["bus"].initial = survey.information.get_selected_bus() + else: + # Use the choice of the member to fill initial data + information = registration.information + if "preferred_bus_pk" in information and len(information["preferred_bus_pk"]) == 1: + form["bus"].initial = Bus.objects.get(pk=information["preferred_bus_pk"][0]) + if "preferred_team_pk" in information and len(information["preferred_team_pk"]) == 1: + form["team"].initial = Bus.objects.get(pk=information["preferred_team_pk"][0]) + if "preferred_roles_pk" in information: + form["roles"].initial = WEIRole.objects.filter( + Q(pk__in=information["preferred_roles_pk"]) | Q(name="Adhérent WEI") + ).all() + return form + + def form_valid(self, form): + """ + Create membership, check that all is good, make transactions + """ + registration = WEIRegistration.objects.get(pk=self.kwargs["pk"]) + club = registration.wei + user = registration.user + + membership = form.instance + membership.user = user + membership.club = club + membership.date_start = min(date.today(), club.date_start) + membership.registration = registration + + if user.profile.paid: + fee = club.membership_fee_paid + else: + fee = club.membership_fee_unpaid + + if not registration.soge_credit and user.note.balance < fee: + # Users must have money before registering to the WEI. + # TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note + form.add_error('bus', + _("This user don't have enough money to join this club, and can't have a negative balance.")) + return super().form_invalid(form) + + if not registration.caution_check and not registration.first_year: + form.add_error('bus', _("This user didn't give her/his caution check.")) + 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) + + # Now, all is fine, the membership can be created. + + if registration.first_year: + membership = form.instance + membership.save() + membership.refresh_from_db() + membership.roles.set(WEIRole.objects.filter(name="1A").all()) + membership.save() + + ret = super().form_valid(form) + + membership.refresh_from_db() + membership.roles.add(WEIRole.objects.get(name="Adhérent WEI")) + + return ret + + def get_success_url(self): + self.object.refresh_from_db() + return reverse_lazy("wei:wei_detail", kwargs={"pk": self.object.club.pk}) + + +class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView): + """ + Display the survey for the WEI for first year members. + Warning: this page is accessible for anyone that is connected, the view doesn't extend ProtectQuerySetMixin. + """ + model = WEIRegistration + template_name = "wei/survey.html" + survey = None + + def dispatch(self, request, *args, **kwargs): + obj = self.get_object() + + wei = obj.wei + today = date.today() + # We can't access to the WEI survey once the WEI is started and before the membership start date + if today >= wei.date_start or today < wei.membership_start: + return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,))) + + if not self.survey: + self.survey = CurrentSurvey(obj) + # If the survey is complete, then display the end page. + if self.survey.is_complete(): + return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) + # Non first year members don't have a survey + if not obj.first_year: + return redirect(reverse_lazy('wei:wei_survey_end', args=(self.survey.registration.pk,))) + return super().dispatch(request, *args, **kwargs) + + def get_form_class(self): + """ + Get the survey form. It may depend on the current state of the survey. + """ + return self.survey.get_form_class() + + def get_form(self, form_class=None): + """ + Update the form with the data of the survey. + """ + form = super().get_form(form_class) + self.survey.update_form(form) + return form + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = self.object.wei + context["title"] = _("Survey WEI") + return context + + def form_valid(self, form): + """ + Update the survey with the data of the form. + """ + self.survey.form_valid(form) + return super().form_valid(form) + + def get_success_url(self): + return reverse_lazy('wei:wei_survey', args=(self.get_object().pk,)) + + +class WEISurveyEndView(LoginRequiredMixin, TemplateView): + template_name = "wei/survey_end.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei + context["title"] = _("Survey WEI") + return context + + +class WEIClosedView(LoginRequiredMixin, TemplateView): + template_name = "wei/survey_closed.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"]) + context["title"] = _("Survey WEI") + return context + + +class MemberListRenderView(LoginRequiredMixin, View): + """ + Render Invoice as a generated PDF with the given information and a LaTeX template + """ + + def get_queryset(self, **kwargs): + qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view")) + qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by( + Lower('bus__name'), + Lower('team__name'), + 'user__profile__promotion', + Lower('user__last_name'), + Lower('user__first_name'), + 'id', + ) + + if "bus_pk" in self.kwargs: + qs = qs.filter(bus__pk=self.kwargs["bus_pk"]) + + if "team_pk" in self.kwargs: + qs = qs.filter(team__pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None) + + return qs.distinct() + + def get(self, request, **kwargs): + qs = self.get_queryset() + + wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"]) + bus = team = None + if "bus_pk" in self.kwargs: + bus = Bus.objects.get(pk=self.kwargs["bus_pk"]) + if "team_pk" in self.kwargs: + team = BusTeam.objects.filter(pk=self.kwargs["team_pk"] if self.kwargs["team_pk"] else None) + if team.exists(): + team = team.get() + bus = team.bus + else: + team = dict(name="Staff") + + # Fill the template with the information + tex = render_to_string("wei/weilist_sample.tex", dict(memberships=qs.all(), wei=wei, bus=bus, team=team)) + + try: + os.mkdir(BASE_DIR + "/tmp") + except FileExistsError: + pass + # We render the file in a temporary directory + tmp_dir = mkdtemp(prefix=BASE_DIR + "/tmp/") + + try: + with open("{}/wei-list.tex".format(tmp_dir), "wb") as f: + f.write(tex.encode("UTF-8")) + del tex + + error = subprocess.Popen( + ["pdflatex", "{}/wei-list.tex".format(tmp_dir)], + cwd=tmp_dir, + stdin=open(os.devnull, "r"), + stderr=open(os.devnull, "wb"), + stdout=open(os.devnull, "wb"), + ).wait() + + if error: + raise IOError("An error attempted while generating a WEI list (code=" + str(error) + ")") + + # Display the generated pdf as a HTTP Response + pdf = open("{}/wei-list.pdf".format(tmp_dir), 'rb').read() + response = HttpResponse(pdf, content_type="application/pdf") + response['Content-Disposition'] = "inline;filename=Liste%20des%20participants%20au%20WEI.pdf" + except IOError as e: + raise e + finally: + # Delete all temporary files + shutil.rmtree(tmp_dir) + + return response diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index f616ffd6d71f7c6e154c7c1dd43f356e1b7a5b55..ec0abeb75d74f3118477e36e8ade5052514a1122 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-04-09 21:59+0200\n" +"POT-Creation-Date: 2020-04-27 03:55+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -19,36 +19,38 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" #: apps/activity/apps.py:10 apps/activity/models.py:102 -#: apps/activity/models.py:111 +#: apps/activity/models.py:117 msgid "activity" msgstr "" -#: apps/activity/forms.py:45 apps/activity/models.py:208 +#: apps/activity/forms.py:45 apps/activity/models.py:213 msgid "You can't invite someone once the activity is started." msgstr "" -#: apps/activity/forms.py:48 apps/activity/models.py:211 +#: apps/activity/forms.py:48 apps/activity/models.py:216 msgid "This activity is not validated yet." msgstr "" -#: apps/activity/forms.py:58 apps/activity/models.py:219 +#: apps/activity/forms.py:58 apps/activity/models.py:224 msgid "This person has been already invited 5 times this year." msgstr "" -#: apps/activity/forms.py:62 apps/activity/models.py:223 +#: apps/activity/forms.py:62 apps/activity/models.py:228 msgid "This person is already invited." msgstr "" -#: apps/activity/forms.py:66 apps/activity/models.py:227 +#: apps/activity/forms.py:66 apps/activity/models.py:232 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 +#: apps/member/models.py:151 apps/member/models.py:255 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 +#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:250 +#: apps/wei/models.py:62 templates/member/club_info.html:13 +#: templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 +#: templates/wei/weiclub_info.html:13 templates/wei/weimembership_form.html:18 msgid "name" msgstr "" @@ -68,19 +70,23 @@ msgstr "" msgid "activity types" msgstr "" -#: apps/activity/models.py:53 apps/note/models/transactions.py:74 -#: apps/permission/models.py:103 templates/activity/activity_detail.html:16 +#: apps/activity/models.py:53 apps/note/models/transactions.py:75 +#: apps/permission/models.py:103 apps/permission/models.py:176 +#: apps/wei/models.py:68 apps/wei/models.py:124 +#: templates/activity/activity_detail.html:16 msgid "description" msgstr "" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:64 +#: apps/note/models/transactions.py:65 apps/permission/models.py:157 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "" -#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:223 -#: apps/note/models/notes.py:117 +#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:277 +#: apps/note/models/notes.py:117 apps/treasury/models.py:221 +#: apps/wei/models.py:155 templates/treasury/sogecredit_detail.html:14 +#: templates/wei/survey.html:16 msgid "user" msgstr "" @@ -100,7 +106,7 @@ msgstr "" msgid "end date" msgstr "" -#: apps/activity/models.py:93 apps/note/models/transactions.py:139 +#: apps/activity/models.py:93 apps/note/models/transactions.py:140 #: templates/activity/activity_detail.html:47 msgid "valid" msgstr "" @@ -113,57 +119,58 @@ msgstr "" msgid "activities" msgstr "" -#: apps/activity/models.py:116 +#: apps/activity/models.py:122 msgid "entry time" msgstr "" -#: apps/activity/models.py:122 apps/note/apps.py:14 +#: apps/activity/models.py:128 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 +#: apps/activity/models.py:139 templates/activity/activity_entry.html:38 msgid "entry" msgstr "" -#: apps/activity/models.py:134 templates/activity/activity_entry.html:38 +#: apps/activity/models.py:140 templates/activity/activity_entry.html:38 msgid "entries" msgstr "" -#: apps/activity/models.py:141 +#: apps/activity/models.py:146 msgid "Already entered on " msgstr "" -#: apps/activity/models.py:141 apps/activity/tables.py:54 +#: apps/activity/models.py:146 apps/activity/tables.py:54 msgid "{:%Y-%m-%d %H:%M:%S}" msgstr "" -#: apps/activity/models.py:149 +#: apps/activity/models.py:154 msgid "The balance is negative." msgstr "" -#: apps/activity/models.py:179 +#: apps/activity/models.py:184 msgid "last name" msgstr "" -#: apps/activity/models.py:184 templates/member/profile_info.html:14 +#: apps/activity/models.py:189 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 +#: templates/wei/weimembership_form.html:18 msgid "first name" msgstr "" -#: apps/activity/models.py:191 +#: apps/activity/models.py:196 msgid "inviter" msgstr "" -#: apps/activity/models.py:232 +#: apps/activity/models.py:237 msgid "guest" msgstr "" -#: apps/activity/models.py:233 +#: apps/activity/models.py:238 msgid "guests" msgstr "" -#: apps/activity/models.py:245 +#: apps/activity/models.py:250 msgid "Invitation" msgstr "" @@ -175,17 +182,17 @@ msgstr "" msgid "remove" msgstr "" -#: apps/activity/tables.py:75 apps/treasury/models.py:138 +#: apps/activity/tables.py:75 apps/treasury/models.py:140 msgid "Type" msgstr "" -#: apps/activity/tables.py:77 apps/member/forms.py:75 -#: apps/registration/forms.py:55 apps/treasury/forms.py:121 +#: apps/activity/tables.py:77 apps/member/forms.py:83 +#: apps/registration/forms.py:64 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 +#: apps/activity/tables.py:79 apps/member/forms.py:88 +#: apps/registration/forms.py:69 apps/treasury/forms.py:123 #: templates/note/transaction_form.html:97 msgid "First name" msgstr "" @@ -198,11 +205,11 @@ msgstr "" msgid "Balance" msgstr "" -#: apps/activity/views.py:45 templates/base.html:106 +#: apps/activity/views.py:46 templates/base.html:121 msgid "Activities" msgstr "" -#: apps/activity/views.py:154 +#: apps/activity/views.py:153 msgid "Entry for activity \"{}\"" msgstr "" @@ -218,7 +225,7 @@ msgstr "" msgid "IP Address" msgstr "" -#: apps/logs/models.py:35 +#: apps/logs/models.py:35 apps/permission/models.py:127 msgid "model" msgstr "" @@ -238,12 +245,13 @@ msgstr "" msgid "create" msgstr "" -#: apps/logs/models.py:61 apps/note/tables.py:144 +#: apps/logs/models.py:61 apps/note/tables.py:145 #: templates/activity/activity_detail.html:67 msgid "edit" msgstr "" #: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:149 +#: apps/wei/tables.py:65 msgid "delete" msgstr "" @@ -267,185 +275,258 @@ msgstr "" msgid "changelogs" msgstr "" -#: apps/member/apps.py:14 +#: apps/member/apps.py:14 apps/wei/tables.py:150 apps/wei/tables.py:181 msgid "member" msgstr "" -#: apps/member/forms.py:54 apps/registration/forms.py:35 +#: apps/member/forms.py:62 apps/registration/forms.py:44 msgid "Inscription paid by Société Générale" msgstr "" -#: apps/member/forms.py:56 apps/registration/forms.py:37 +#: apps/member/forms.py:64 apps/registration/forms.py:46 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 +#: apps/member/forms.py:69 apps/registration/forms.py:51 msgid "Credit type" msgstr "" -#: apps/member/forms.py:62 apps/registration/forms.py:43 +#: apps/member/forms.py:70 apps/registration/forms.py:52 msgid "No credit" msgstr "" -#: apps/member/forms.py:64 +#: apps/member/forms.py:72 msgid "You can credit the note of the user." msgstr "" -#: apps/member/forms.py:68 apps/registration/forms.py:48 +#: apps/member/forms.py:76 apps/registration/forms.py:57 msgid "Credit amount" msgstr "" -#: apps/member/forms.py:85 apps/registration/forms.py:65 +#: apps/member/forms.py:93 apps/registration/forms.py:74 #: apps/treasury/forms.py:125 templates/note/transaction_form.html:103 msgid "Bank" msgstr "" -#: apps/member/models.py:33 +#: apps/member/models.py:34 #: templates/registration/future_profile_detail.html:47 +#: templates/wei/weimembership_form.html:48 msgid "phone number" msgstr "" -#: apps/member/models.py:39 templates/member/profile_info.html:27 +#: apps/member/models.py:41 templates/member/profile_info.html:27 #: templates/registration/future_profile_detail.html:41 +#: templates/wei/weimembership_form.html:42 msgid "section" msgstr "" -#: apps/member/models.py:40 +#: apps/member/models.py:42 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "" -#: apps/member/models.py:46 templates/member/profile_info.html:30 -#: templates/registration/future_profile_detail.html:44 -msgid "address" +#: apps/member/models.py:50 templates/wei/weimembership_form.html:36 +msgid "department" msgstr "" #: apps/member/models.py:52 -#: templates/registration/future_profile_detail.html:50 -msgid "paid" +msgid "Informatics (A0)" msgstr "" #: apps/member/models.py:53 -msgid "Tells if the user receive a salary." +msgid "Mathematics (A1)" +msgstr "" + +#: apps/member/models.py:54 +msgid "Physics (A2)" +msgstr "" + +#: apps/member/models.py:55 +msgid "Applied physics (A'2)" +msgstr "" + +#: apps/member/models.py:56 +msgid "Chemistry (A''2)" +msgstr "" + +#: apps/member/models.py:57 +msgid "Biology (A3)" msgstr "" #: apps/member/models.py:58 -msgid "email confirmed" +msgid "SAPHIRE (B1234)" +msgstr "" + +#: apps/member/models.py:59 +msgid "Mechanics (B1)" +msgstr "" + +#: apps/member/models.py:60 +msgid "Civil engineering (B2)" +msgstr "" + +#: apps/member/models.py:61 +msgid "Mechanical engineering (B3)" +msgstr "" + +#: apps/member/models.py:62 +msgid "EEA (B4)" msgstr "" #: apps/member/models.py:63 -msgid "registration valid" +msgid "Design (C)" +msgstr "" + +#: apps/member/models.py:64 +msgid "Economy-management (D2)" msgstr "" -#: apps/member/models.py:68 -msgid "Société générale" +#: apps/member/models.py:65 +msgid "Social sciences (D3)" msgstr "" -#: apps/member/models.py:69 -msgid "Has the user ever be paid by the Société générale?" +#: apps/member/models.py:66 +msgid "English (E)" msgstr "" -#: apps/member/models.py:74 apps/member/models.py:75 +#: apps/member/models.py:67 +msgid "External (EXT)" +msgstr "" + +#: apps/member/models.py:74 +msgid "promotion" +msgstr "" + +#: apps/member/models.py:75 +msgid "Year of entry to the school (None if not ENS student)" +msgstr "" + +#: apps/member/models.py:79 templates/member/profile_info.html:30 +#: templates/registration/future_profile_detail.html:44 +#: templates/wei/weimembership_form.html:45 +msgid "address" +msgstr "" + +#: apps/member/models.py:86 +#: templates/registration/future_profile_detail.html:50 +#: templates/wei/weimembership_form.html:51 +msgid "paid" +msgstr "" + +#: apps/member/models.py:87 +msgid "Tells if the user receive a salary." +msgstr "" + +#: apps/member/models.py:92 +msgid "email confirmed" +msgstr "" + +#: apps/member/models.py:97 +msgid "registration valid" +msgstr "" + +#: apps/member/models.py:126 apps/member/models.py:127 msgid "user profile" msgstr "" -#: apps/member/models.py:104 templates/member/club_info.html:46 +#: apps/member/models.py:156 templates/member/club_info.html:57 #: templates/registration/future_profile_detail.html:22 +#: templates/wei/weiclub_info.html:52 templates/wei/weimembership_form.html:24 msgid "email" msgstr "" -#: apps/member/models.py:111 +#: apps/member/models.py:163 msgid "parent club" msgstr "" -#: apps/member/models.py:120 +#: apps/member/models.py:172 msgid "require memberships" msgstr "" -#: apps/member/models.py:121 +#: apps/member/models.py:173 msgid "Uncheck if this club don't require memberships." msgstr "" -#: apps/member/models.py:126 templates/member/club_info.html:35 +#: apps/member/models.py:178 templates/member/club_info.html:41 msgid "membership fee (paid students)" msgstr "" -#: apps/member/models.py:131 templates/member/club_info.html:38 +#: apps/member/models.py:183 templates/member/club_info.html:44 msgid "membership fee (unpaid students)" msgstr "" -#: apps/member/models.py:137 templates/member/club_info.html:28 +#: apps/member/models.py:189 templates/member/club_info.html:33 msgid "membership duration" msgstr "" -#: apps/member/models.py:138 +#: apps/member/models.py:190 msgid "The longest time (in days) a membership can last (NULL = infinite)." msgstr "" -#: apps/member/models.py:145 templates/member/club_info.html:22 +#: apps/member/models.py:197 templates/member/club_info.html:23 msgid "membership start" msgstr "" -#: apps/member/models.py:146 +#: apps/member/models.py:198 msgid "How long after January 1st the members can renew their membership." msgstr "" -#: apps/member/models.py:153 templates/member/club_info.html:25 +#: apps/member/models.py:205 templates/member/club_info.html:28 msgid "membership end" msgstr "" -#: apps/member/models.py:154 +#: apps/member/models.py:206 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:187 apps/member/models.py:229 +#: apps/member/models.py:240 apps/member/models.py:283 #: apps/note/models/notes.py:139 msgid "club" msgstr "" -#: apps/member/models.py:188 +#: apps/member/models.py:241 msgid "clubs" msgstr "" -#: apps/member/models.py:208 apps/permission/models.py:294 +#: apps/member/models.py:261 apps/permission/models.py:312 msgid "role" msgstr "" -#: apps/member/models.py:209 apps/member/models.py:234 +#: apps/member/models.py:262 apps/member/models.py:288 msgid "roles" msgstr "" -#: apps/member/models.py:239 +#: apps/member/models.py:293 msgid "membership starts on" msgstr "" -#: apps/member/models.py:243 +#: apps/member/models.py:297 msgid "membership ends on" msgstr "" -#: apps/member/models.py:248 +#: apps/member/models.py:302 msgid "fee" msgstr "" -#: apps/member/models.py:266 apps/member/views.py:500 +#: apps/member/models.py:320 apps/member/views.py:505 apps/wei/views.py:768 msgid "User is not a member of the parent club" msgstr "" -#: apps/member/models.py:276 apps/member/views.py:509 +#: apps/member/models.py:330 apps/member/views.py:514 msgid "User is already a member of the club" msgstr "" -#: apps/member/models.py:314 +#: apps/member/models.py:381 #, python-brace-format msgid "Membership of {user} for the club {club}" msgstr "" -#: apps/member/models.py:317 +#: apps/member/models.py:384 msgid "membership" msgstr "" -#: apps/member/models.py:318 +#: apps/member/models.py:385 msgid "memberships" msgstr "" @@ -459,6 +540,7 @@ msgstr "" #: apps/member/views.py:65 templates/member/profile_info.html:45 #: templates/registration/future_profile_detail.html:55 +#: templates/wei/weimembership_form.html:122 msgid "Update Profile" msgstr "" @@ -466,36 +548,36 @@ msgstr "" msgid "An alias with a similar name already exists." msgstr "" -#: apps/member/views.py:180 +#: apps/member/views.py:181 msgid "Search user" msgstr "" -#: apps/member/views.py:495 +#: apps/member/views.py:500 apps/wei/views.py:759 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." msgstr "" -#: apps/member/views.py:513 +#: apps/member/views.py:518 msgid "The membership must start after {:%m-%d-%Y}." msgstr "" -#: apps/member/views.py:518 +#: apps/member/views.py:523 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 +#: apps/member/views.py:540 apps/member/views.py:542 apps/member/views.py:544 +#: apps/registration/views.py:289 apps/registration/views.py:291 +#: apps/registration/views.py:293 msgid "This field is required." msgstr "" -#: apps/note/admin.py:120 apps/note/models/transactions.py:99 +#: apps/note/admin.py:120 apps/note/models/transactions.py:100 msgid "source" msgstr "" #: apps/note/admin.py:128 apps/note/admin.py:170 -#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:112 +#: apps/note/models/transactions.py:55 apps/note/models/transactions.py:113 msgid "destination" msgstr "" @@ -537,7 +619,7 @@ msgstr "" msgid "display image" msgstr "" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:122 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:123 msgid "created at" msgstr "" @@ -591,8 +673,8 @@ msgstr "" msgid "alias" msgstr "" -#: apps/note/models/notes.py:211 templates/member/club_info.html:43 -#: templates/member/profile_info.html:36 +#: apps/note/models/notes.py:211 templates/member/club_info.html:54 +#: templates/member/profile_info.html:36 templates/wei/weiclub_info.html:48 msgid "aliases" msgstr "" @@ -608,97 +690,98 @@ msgstr "" msgid "You can't delete your main alias." msgstr "" -#: apps/note/models/transactions.py:30 +#: apps/note/models/transactions.py:31 msgid "transaction category" msgstr "" -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:32 msgid "transaction categories" msgstr "" -#: apps/note/models/transactions.py:47 +#: apps/note/models/transactions.py:48 msgid "A template with this name already exist" msgstr "" -#: apps/note/models/transactions.py:58 apps/note/models/transactions.py:130 +#: apps/note/models/transactions.py:59 apps/note/models/transactions.py:131 msgid "amount" msgstr "" -#: apps/note/models/transactions.py:59 +#: apps/note/models/transactions.py:60 msgid "in centimes" msgstr "" -#: apps/note/models/transactions.py:70 +#: apps/note/models/transactions.py:71 msgid "display" msgstr "" -#: apps/note/models/transactions.py:80 +#: apps/note/models/transactions.py:81 msgid "transaction template" msgstr "" -#: apps/note/models/transactions.py:81 +#: apps/note/models/transactions.py:82 msgid "transaction templates" msgstr "" -#: apps/note/models/transactions.py:105 apps/note/models/transactions.py:118 +#: apps/note/models/transactions.py:106 apps/note/models/transactions.py:119 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "" -#: apps/note/models/transactions.py:126 +#: apps/note/models/transactions.py:127 msgid "quantity" msgstr "" -#: apps/note/models/transactions.py:134 +#: apps/note/models/transactions.py:135 msgid "reason" msgstr "" -#: apps/note/models/transactions.py:144 apps/note/tables.py:95 +#: apps/note/models/transactions.py:145 apps/note/tables.py:95 msgid "invalidity reason" msgstr "" -#: apps/note/models/transactions.py:152 +#: apps/note/models/transactions.py:153 msgid "transaction" msgstr "" -#: apps/note/models/transactions.py:153 +#: apps/note/models/transactions.py:154 +#: templates/treasury/sogecredit_detail.html:22 msgid "transactions" msgstr "" -#: apps/note/models/transactions.py:207 +#: apps/note/models/transactions.py:216 #: templates/activity/activity_entry.html:13 templates/base.html:84 #: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:145 +#: templates/note/transaction_form.html:140 msgid "Transfer" msgstr "" -#: apps/note/models/transactions.py:227 +#: apps/note/models/transactions.py:240 msgid "Template" msgstr "" -#: apps/note/models/transactions.py:242 +#: apps/note/models/transactions.py:255 msgid "first_name" msgstr "" -#: apps/note/models/transactions.py:247 +#: apps/note/models/transactions.py:260 msgid "bank" msgstr "" -#: apps/note/models/transactions.py:253 +#: apps/note/models/transactions.py:266 #: templates/activity/activity_entry.html:17 #: templates/note/transaction_form.html:24 msgid "Credit" msgstr "" -#: apps/note/models/transactions.py:253 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:266 templates/note/transaction_form.html:28 msgid "Debit" msgstr "" -#: apps/note/models/transactions.py:269 apps/note/models/transactions.py:274 +#: apps/note/models/transactions.py:282 apps/note/models/transactions.py:287 msgid "membership transaction" msgstr "" -#: apps/note/models/transactions.py:270 +#: apps/note/models/transactions.py:283 apps/treasury/models.py:227 msgid "membership transactions" msgstr "" @@ -714,29 +797,33 @@ msgstr "" msgid "No reason specified" msgstr "" -#: apps/note/tables.py:122 apps/note/tables.py:151 +#: apps/note/tables.py:122 apps/note/tables.py:151 apps/wei/tables.py:66 +#: templates/treasury/sogecredit_detail.html:59 +#: templates/wei/weiregistration_confirm_delete.html:32 msgid "Delete" msgstr "" -#: apps/note/tables.py:146 templates/member/club_info.html:55 -#: templates/note/conso_form.html:121 +#: apps/note/tables.py:146 apps/wei/tables.py:42 apps/wei/tables.py:43 +#: templates/member/club_info.html:67 templates/note/conso_form.html:121 +#: templates/wei/bus_tables.html:15 templates/wei/busteam_tables.html:15 +#: templates/wei/busteam_tables.html:33 templates/wei/weiclub_info.html:68 msgid "Edit" msgstr "" -#: apps/note/views.py:40 +#: apps/note/views.py:41 msgid "Transfer money" msgstr "" -#: apps/note/views.py:109 templates/base.html:79 +#: apps/note/views.py:137 templates/base.html:94 msgid "Consumptions" msgstr "" -#: apps/permission/models.py:82 apps/permission/models.py:281 +#: apps/permission/models.py:82 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" msgstr "" -#: apps/permission/models.py:84 apps/permission/models.py:283 +#: apps/permission/models.py:84 #, python-brace-format msgid "Can {type} {model} in {query}" msgstr "" @@ -753,73 +840,121 @@ msgstr "" msgid "permission masks" msgstr "" -#: apps/permission/models.py:160 +#: apps/permission/models.py:151 +msgid "query" +msgstr "" + +#: apps/permission/models.py:164 +msgid "mask" +msgstr "" + +#: apps/permission/models.py:170 +msgid "field" +msgstr "" + +#: apps/permission/models.py:181 msgid "permission" msgstr "" -#: apps/permission/models.py:161 +#: apps/permission/models.py:182 apps/permission/models.py:316 msgid "permissions" msgstr "" -#: apps/permission/models.py:166 +#: apps/permission/models.py:187 msgid "Specifying field applies only to view and change permission types." msgstr "" -#: apps/permission/models.py:304 apps/permission/models.py:305 +#: apps/permission/models.py:323 apps/permission/models.py:324 msgid "role permissions" msgstr "" +#: apps/permission/signals.py:62 +#, python-brace-format +msgid "" +"You don't have the permission to change the field {field} on this instance " +"of model {app_label}.{model_name}." +msgstr "" + +#: apps/permission/signals.py:72 +#, python-brace-format +msgid "" +"You don't have the permission to add this instance of model {app_label}." +"{model_name}." +msgstr "" + +#: apps/permission/signals.py:99 +#, python-brace-format +msgid "" +"You don't have the permission to delete this instance of model {app_label}." +"{model_name}." +msgstr "" + +#: apps/permission/views.py:47 +msgid "All rights" +msgstr "" + #: apps/registration/apps.py:10 msgid "registration" msgstr "" -#: apps/registration/forms.py:70 +#: apps/registration/forms.py:32 +msgid "Register to the WEI" +msgstr "" + +#: apps/registration/forms.py:34 +msgid "" +"Check this case if you want to register to the WEI. If you hesitate, you " +"will be able to register later, after validating your account in the Kfet." +msgstr "" + +#: apps/registration/forms.py:79 msgid "Join BDE Club" msgstr "" -#: apps/registration/forms.py:77 +#: apps/registration/forms.py:86 msgid "Join Kfet Club" msgstr "" -#: apps/registration/views.py:75 +#: apps/registration/views.py:78 msgid "Email validation" msgstr "" -#: apps/registration/views.py:121 +#: apps/registration/views.py:124 msgid "Email validation unsuccessful" msgstr "" -#: apps/registration/views.py:132 +#: apps/registration/views.py:135 msgid "Email validation email sent" msgstr "" -#: apps/registration/views.py:185 +#: apps/registration/views.py:188 msgid "Unregistered users" msgstr "" -#: apps/registration/views.py:252 +#: apps/registration/views.py:255 msgid "You must join the BDE." msgstr "" -#: apps/registration/views.py:274 +#: apps/registration/views.py:277 msgid "You must join BDE club before joining Kfet club." msgstr "" -#: apps/registration/views.py:279 +#: apps/registration/views.py:282 msgid "" "The entered amount is not enough for the memberships, should be at least {}" msgstr "" -#: apps/treasury/apps.py:12 templates/base.html:111 +#: apps/treasury/apps.py:12 templates/base.html:126 msgid "Treasury" msgstr "" #: 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/add_members.html:14 templates/member/club_form.html:9 -#: templates/treasury/invoice_form.html:46 +#: templates/treasury/invoice_form.html:46 templates/wei/bus_form.html:13 +#: templates/wei/busteam_form.html:13 templates/wei/weiclub_form.html:15 +#: templates/wei/weiregistration_form.html:14 msgid "Submit" msgstr "" @@ -836,123 +971,143 @@ msgid "You can't change the type of the remittance." msgstr "" #: apps/treasury/forms.py:127 apps/treasury/tables.py:47 -#: templates/note/transaction_form.html:133 +#: apps/treasury/tables.py:113 templates/note/transaction_form.html:133 #: templates/treasury/remittance_form.html:18 msgid "Amount" msgstr "" -#: apps/treasury/models.py:18 +#: apps/treasury/models.py:20 msgid "Invoice identifier" msgstr "" -#: apps/treasury/models.py:32 +#: apps/treasury/models.py:34 msgid "BDE" msgstr "" -#: apps/treasury/models.py:37 +#: apps/treasury/models.py:39 msgid "Object" msgstr "" -#: apps/treasury/models.py:41 +#: apps/treasury/models.py:43 msgid "Description" msgstr "" -#: apps/treasury/models.py:46 templates/note/transaction_form.html:91 +#: apps/treasury/models.py:48 templates/note/transaction_form.html:91 msgid "Name" msgstr "" -#: apps/treasury/models.py:50 +#: apps/treasury/models.py:52 msgid "Address" msgstr "" -#: apps/treasury/models.py:55 +#: apps/treasury/models.py:57 msgid "Place" msgstr "" -#: apps/treasury/models.py:59 +#: apps/treasury/models.py:61 msgid "Acquitted" msgstr "" -#: apps/treasury/models.py:63 +#: apps/treasury/models.py:65 msgid "invoice" msgstr "" -#: apps/treasury/models.py:64 +#: apps/treasury/models.py:66 msgid "invoices" msgstr "" -#: apps/treasury/models.py:79 +#: apps/treasury/models.py:81 msgid "Designation" msgstr "" -#: apps/treasury/models.py:83 +#: apps/treasury/models.py:85 msgid "Quantity" msgstr "" -#: apps/treasury/models.py:87 +#: apps/treasury/models.py:89 msgid "Unit price" msgstr "" -#: apps/treasury/models.py:103 +#: apps/treasury/models.py:105 msgid "product" msgstr "" -#: apps/treasury/models.py:104 +#: apps/treasury/models.py:106 msgid "products" msgstr "" -#: apps/treasury/models.py:121 +#: apps/treasury/models.py:123 msgid "remittance type" msgstr "" -#: apps/treasury/models.py:122 +#: apps/treasury/models.py:124 msgid "remittance types" msgstr "" -#: apps/treasury/models.py:132 +#: apps/treasury/models.py:134 msgid "Date" msgstr "" -#: apps/treasury/models.py:143 +#: apps/treasury/models.py:145 msgid "Comment" msgstr "" -#: apps/treasury/models.py:148 +#: apps/treasury/models.py:150 msgid "Closed" msgstr "" -#: apps/treasury/models.py:152 +#: apps/treasury/models.py:154 msgid "remittance" msgstr "" -#: apps/treasury/models.py:153 +#: apps/treasury/models.py:155 msgid "remittances" msgstr "" -#: apps/treasury/models.py:185 +#: apps/treasury/models.py:187 msgid "Remittance #{:d}: {}" msgstr "" -#: apps/treasury/models.py:204 apps/treasury/tables.py:76 +#: apps/treasury/models.py:206 apps/treasury/tables.py:76 #: apps/treasury/tables.py:84 templates/treasury/invoice_list.html:13 #: templates/treasury/remittance_list.html:13 +#: templates/treasury/sogecredit_list.html:13 msgid "Remittance" msgstr "" -#: apps/treasury/models.py:208 +#: apps/treasury/models.py:210 msgid "special transaction proxy" msgstr "" -#: apps/treasury/models.py:209 +#: apps/treasury/models.py:211 msgid "special transaction proxies" msgstr "" +#: apps/treasury/models.py:233 +msgid "credit transaction" +msgstr "" + +#: apps/treasury/models.py:296 +msgid "" +"This user doesn't have enough money to pay the memberships with its note. " +"Please ask her/him to credit the note before invalidating this credit." +msgstr "" + +#: apps/treasury/models.py:308 templates/treasury/sogecredit_detail.html:10 +msgid "Credit from the Société générale" +msgstr "" + +#: apps/treasury/models.py:309 +msgid "Credits from the Société générale" +msgstr "" + #: apps/treasury/tables.py:19 msgid "Invoice #{:d}" msgstr "" #: apps/treasury/tables.py:22 templates/treasury/invoice_list.html:10 #: templates/treasury/remittance_list.html:10 +#: templates/treasury/sogecredit_list.html:10 msgid "Invoice" msgstr "" @@ -972,25 +1127,327 @@ msgstr "" msgid "Remove" msgstr "" -#: note_kfet/settings/__init__.py:63 +#: apps/treasury/tables.py:117 +msgid "Valid" +msgstr "" + +#: apps/treasury/tables.py:124 +msgid "Yes" +msgstr "" + +#: apps/treasury/tables.py:124 +msgid "No" +msgstr "" + +#: apps/wei/apps.py:10 apps/wei/models.py:45 apps/wei/models.py:46 +#: apps/wei/models.py:57 apps/wei/models.py:162 templates/base.html:116 +msgid "WEI" +msgstr "" + +#: apps/wei/forms/registration.py:47 apps/wei/models.py:109 +#: apps/wei/models.py:271 +msgid "bus" +msgstr "" + +#: apps/wei/forms/registration.py:48 msgid "" -"The Central Authentication Service grants you access to most of our websites " -"by authenticating only once, so you don't need to type your credentials " -"again unless your session expires or you logout." +"This choice is not definitive. The WEI organizers are free to attribute for " +"you a bus and a team, in particular if you are a free eletron." msgstr "" -#: note_kfet/settings/base.py:153 -msgid "German" +#: apps/wei/forms/registration.py:54 +msgid "Team" +msgstr "" + +#: apps/wei/forms/registration.py:56 +msgid "" +"Leave this field empty if you won't be in a team (staff, bus chief, free " +"electron)" +msgstr "" + +#: apps/wei/forms/registration.py:61 apps/wei/forms/registration.py:67 +#: apps/wei/models.py:143 +msgid "WEI Roles" +msgstr "" + +#: apps/wei/forms/registration.py:62 +msgid "Select the roles that you are interested in." +msgstr "" + +#: apps/wei/forms/registration.py:72 +msgid "This team doesn't belong to the given bus." +msgstr "" + +#: apps/wei/models.py:20 templates/wei/weiclub_info.html:23 +msgid "year" +msgstr "" + +#: apps/wei/models.py:24 templates/wei/weiclub_info.html:17 +msgid "date start" +msgstr "" + +#: apps/wei/models.py:28 templates/wei/weiclub_info.html:20 +msgid "date end" +msgstr "" + +#: apps/wei/models.py:73 +msgid "survey information" +msgstr "" + +#: apps/wei/models.py:74 +msgid "Information about the survey for new members, encoded in JSON" +msgstr "" + +#: apps/wei/models.py:96 +msgid "Bus" +msgstr "" + +#: apps/wei/models.py:97 templates/wei/weiclub_tables.html:79 +msgid "Buses" +msgstr "" + +#: apps/wei/models.py:117 +msgid "color" +msgstr "" + +#: apps/wei/models.py:118 +msgid "The color of the T-Shirt, stored with its number equivalent" +msgstr "" + +#: apps/wei/models.py:132 +msgid "Bus team" +msgstr "" + +#: apps/wei/models.py:133 +msgid "Bus teams" +msgstr "" + +#: apps/wei/models.py:142 +msgid "WEI Role" +msgstr "" + +#: apps/wei/models.py:167 +msgid "Credit from Société générale" +msgstr "" + +#: apps/wei/models.py:172 +msgid "Caution check given" +msgstr "" + +#: apps/wei/models.py:176 templates/wei/weimembership_form.html:62 +msgid "birth date" +msgstr "" + +#: apps/wei/models.py:182 +msgid "Male" +msgstr "" + +#: apps/wei/models.py:183 +msgid "Female" +msgstr "" + +#: apps/wei/models.py:184 +msgid "Non binary" +msgstr "" + +#: apps/wei/models.py:186 templates/wei/weimembership_form.html:59 +msgid "gender" +msgstr "" + +#: apps/wei/models.py:192 templates/wei/weimembership_form.html:65 +msgid "health issues" +msgstr "" + +#: apps/wei/models.py:197 templates/wei/weimembership_form.html:68 +msgid "emergency contact name" +msgstr "" + +#: apps/wei/models.py:202 templates/wei/weimembership_form.html:71 +msgid "emergency contact phone" +msgstr "" + +#: apps/wei/models.py:207 templates/wei/weimembership_form.html:74 +msgid "" +"Register on the mailing list to stay informed of the events of the campus (1 " +"mail/week)" +msgstr "" + +#: apps/wei/models.py:212 templates/wei/weimembership_form.html:77 +msgid "" +"Register on the mailing list to stay informed of the sport events of the " +"campus (1 mail/week)" +msgstr "" + +#: apps/wei/models.py:217 templates/wei/weimembership_form.html:80 +msgid "" +"Register on the mailing list to stay informed of the art events of the " +"campus (1 mail/week)" +msgstr "" + +#: apps/wei/models.py:222 templates/wei/weimembership_form.html:56 +msgid "first year" +msgstr "" + +#: apps/wei/models.py:223 +msgid "Tells if the user is new in the school." +msgstr "" + +#: apps/wei/models.py:228 +msgid "registration information" +msgstr "" + +#: apps/wei/models.py:229 +msgid "" +"Information about the registration (buses for old members, survey fot the " +"new members), encoded in JSON" +msgstr "" + +#: apps/wei/models.py:260 +msgid "WEI User" +msgstr "" + +#: apps/wei/models.py:261 +msgid "WEI Users" +msgstr "" + +#: apps/wei/models.py:281 +msgid "team" +msgstr "" + +#: apps/wei/models.py:291 +msgid "WEI registration" +msgstr "" + +#: apps/wei/models.py:295 +msgid "WEI membership" +msgstr "" + +#: apps/wei/models.py:296 +msgid "WEI memberships" +msgstr "" + +#: apps/wei/tables.py:53 apps/wei/tables.py:54 +#: templates/treasury/sogecredit_detail.html:57 +msgid "Validate" +msgstr "" + +#: apps/wei/tables.py:96 +msgid "Year" +msgstr "" + +#: apps/wei/tables.py:134 templates/wei/bus_tables.html:26 +#: templates/wei/busteam_tables.html:43 +msgid "Teams" +msgstr "" + +#: apps/wei/tables.py:143 apps/wei/tables.py:184 +msgid "Members count" +msgstr "" + +#: apps/wei/tables.py:150 apps/wei/tables.py:181 +msgid "members" +msgstr "" + +#: apps/wei/views.py:201 +msgid "Find WEI Membership" +msgstr "" + +#: apps/wei/views.py:236 +msgid "Find WEI Registration" +msgstr "" + +#: apps/wei/views.py:445 templates/wei/weiclub_info.html:62 +msgid "Register 1A" +msgstr "" + +#: apps/wei/views.py:466 apps/wei/views.py:535 +msgid "This user is already registered to this WEI." +msgstr "" + +#: apps/wei/views.py:471 +msgid "" +"This user can't be in her/his first year since he/she has already participed " +"to a WEI." +msgstr "" + +#: apps/wei/views.py:499 templates/wei/weiclub_info.html:65 +msgid "Register 2A+" +msgstr "" + +#: apps/wei/views.py:517 apps/wei/views.py:604 +msgid "You already opened an account in the Société générale." +msgstr "" + +#: apps/wei/views.py:664 +msgid "You don't have the right to delete this WEI registration." +msgstr "" + +#: apps/wei/views.py:763 +msgid "This user didn't give her/his caution check." +msgstr "" + +#: apps/wei/views.py:837 apps/wei/views.py:857 apps/wei/views.py:867 +#: templates/wei/survey.html:12 templates/wei/survey_closed.html:12 +#: templates/wei/survey_end.html:12 +msgid "Survey WEI" msgstr "" #: note_kfet/settings/base.py:154 -msgid "English" +msgid "German" msgstr "" #: note_kfet/settings/base.py:155 +msgid "English" +msgstr "" + +#: note_kfet/settings/base.py:156 msgid "French" msgstr "" +#: templates/400.html:6 +msgid "Bad request" +msgstr "" + +#: templates/400.html:7 +msgid "" +"Sorry, your request was bad. Don't know what could be wrong. An email has " +"been sent to webmasters with the details of the error. You can now drink a " +"coke." +msgstr "" + +#: templates/403.html:6 +msgid "Permission denied" +msgstr "" + +#: templates/403.html:7 +msgid "You don't have the right to perform this request." +msgstr "" + +#: templates/403.html:10 templates/404.html:10 +msgid "Exception message:" +msgstr "" + +#: templates/404.html:6 +msgid "Page not found" +msgstr "" + +#: templates/404.html:7 +#, python-format +msgid "" +"The requested path <code>%(request_path)s</code> was not found on the server." +msgstr "" + +#: templates/500.html:6 +msgid "Server error" +msgstr "" + +#: templates/500.html:7 +msgid "" +"Sorry, an error occurred when processing your request. An email has been " +"sent to webmasters with the detail of the error, and this will be fixed " +"soon. You can now drink a beer." +msgstr "" + #: templates/activity/activity_detail.html:29 msgid "creater" msgstr "" @@ -1052,19 +1509,23 @@ msgstr "" msgid "The ENS Paris-Saclay BDE note." msgstr "" -#: templates/base.html:89 +#: templates/base.html:104 msgid "Users" msgstr "" -#: templates/base.html:94 +#: templates/base.html:109 msgid "Clubs" msgstr "" -#: templates/base.html:100 +#: templates/base.html:115 msgid "Registrations" msgstr "" -#: templates/base.html:150 +#: templates/base.html:120 +msgid "Rights" +msgstr "" + +#: templates/base.html:158 msgid "" "Your e-mail address is not validated. Please check your mail inbox and click " "on the validation link." @@ -1082,49 +1543,6 @@ msgid "" "upgrading." msgstr "" -#: templates/cas_server/logged.html:4 -msgid "" -"<h3>Log In Successful</h3>You have successfully logged into the Central " -"Authentication Service.<br/>For security reasons, please Log Out and Exit " -"your web browser when you are done accessing services that require " -"authentication!" -msgstr "" - -#: templates/cas_server/logged.html:8 -msgid "Log me out from all my sessions" -msgstr "" - -#: templates/cas_server/logged.html:14 -msgid "Forget the identity provider" -msgstr "" - -#: templates/cas_server/logged.html:18 -msgid "Logout" -msgstr "" - -#: templates/cas_server/login.html:6 -msgid "Please log in" -msgstr "" - -#: templates/cas_server/login.html:11 -msgid "" -"If you don't have any Note Kfet account, please follow <a href='/accounts/" -"signup'>this link to sign up</a>." -msgstr "" - -#: templates/cas_server/login.html:17 -msgid "Login" -msgstr "" - -#: templates/cas_server/warn.html:9 -msgid "Connect to the service" -msgstr "" - -#: templates/django_filters/rest_framework/crispy_form.html:4 -#: templates/django_filters/rest_framework/form.html:2 -msgid "Field filters" -msgstr "" - #: templates/member/alias_update.html:5 msgid "Add alias" msgstr "" @@ -1133,19 +1551,25 @@ msgstr "" msgid "Club Parent" msgstr "" -#: templates/member/club_info.html:29 +#: templates/member/club_info.html:34 msgid "days" msgstr "" -#: templates/member/club_info.html:32 +#: templates/member/club_info.html:38 templates/wei/weiclub_info.html:27 msgid "membership fee" msgstr "" -#: templates/member/club_info.html:52 +#: templates/member/club_info.html:50 templates/member/profile_info.html:33 +#: templates/treasury/sogecredit_detail.html:18 +#: templates/wei/weiclub_info.html:43 +msgid "balance" +msgstr "" + +#: templates/member/club_info.html:64 msgid "Add member" msgstr "" -#: templates/member/club_info.html:59 templates/member/profile_info.html:48 +#: templates/member/club_info.html:71 templates/member/profile_info.html:48 msgid "View Profile" msgstr "" @@ -1158,14 +1582,15 @@ msgid "Create club" msgstr "" #: templates/member/club_list.html:19 -msgid "club listing " +msgid "Club listing" msgstr "" -#: templates/member/club_tables.html:6 +#: templates/member/club_tables.html:7 msgid "Member of the Club" msgstr "" -#: templates/member/club_tables.html:17 templates/member/profile_tables.html:28 +#: templates/member/club_tables.html:20 templates/member/profile_tables.html:28 +#: templates/wei/weiclub_tables.html:105 msgid "Transaction history" msgstr "" @@ -1188,6 +1613,7 @@ msgstr "" #: templates/member/profile_info.html:17 #: templates/registration/future_profile_detail.html:19 +#: templates/wei/weimembership_form.html:21 msgid "username" msgstr "" @@ -1201,21 +1627,19 @@ msgstr "" msgid "Change password" msgstr "" -#: templates/member/profile_info.html:33 -msgid "balance" -msgstr "" - #: templates/member/profile_info.html:41 msgid "Manage auth token" msgstr "" #: templates/member/profile_tables.html:7 #: templates/registration/future_profile_detail.html:28 +#: templates/wei/weimembership_form.html:30 msgid "This user doesn't have confirmed his/her e-mail address." msgstr "" #: templates/member/profile_tables.html:8 #: templates/registration/future_profile_detail.html:29 +#: templates/wei/weimembership_form.html:31 msgid "Click here to resend a validation link." msgstr "" @@ -1227,36 +1651,40 @@ msgstr "" msgid "Save Changes" msgstr "" -#: templates/member/user_list.html:14 -#: templates/registration/future_user_list.html:17 -msgid "There is no pending user with this pattern." +#: templates/member/user_list.html:16 +msgid "There is no user with this pattern." msgstr "" -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:55 -msgid "Select emitters" +#: templates/note/conso_form.html:28 +msgid "Consum" +msgstr "" + +#: templates/note/conso_form.html:39 templates/note/transaction_form.html:61 +#: templates/note/transaction_form.html:76 +msgid "Name or alias..." msgstr "" -#: templates/note/conso_form.html:45 +#: templates/note/conso_form.html:48 msgid "Select consumptions" msgstr "" -#: templates/note/conso_form.html:51 +#: templates/note/conso_form.html:57 msgid "Consume!" msgstr "" -#: templates/note/conso_form.html:64 +#: templates/note/conso_form.html:71 msgid "Most used buttons" msgstr "" -#: templates/note/conso_form.html:126 +#: templates/note/conso_form.html:134 msgid "Single consumptions" msgstr "" -#: templates/note/conso_form.html:130 +#: templates/note/conso_form.html:139 msgid "Double consumptions" msgstr "" -#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 +#: templates/note/conso_form.html:150 templates/note/transaction_form.html:151 msgid "Recent transactions history" msgstr "" @@ -1264,56 +1692,86 @@ msgstr "" msgid "Gift" msgstr "" -#: templates/note/transaction_form.html:73 -msgid "External payment" +#: templates/note/transaction_form.html:55 +msgid "Select emitters" msgstr "" -#: templates/note/transaction_form.html:81 -msgid "Transfer type" +#: templates/note/transaction_form.html:70 +msgid "Select receivers" msgstr "" -#: templates/note/transaction_form.html:116 -#: templates/note/transaction_form.html:169 -#: templates/note/transaction_form.html:176 -msgid "Select receivers" +#: templates/note/transaction_form.html:87 +msgid "Action" msgstr "" -#: templates/note/transaction_form.html:138 +#: templates/note/transaction_form.html:102 msgid "Reason" msgstr "" -#: templates/note/transaction_form.html:183 -msgid "Credit note" +#: templates/note/transaction_form.html:110 +msgid "Transfer type" +msgstr "" + +#: templates/note/transactiontemplate_form.html:10 +msgid "Buttons list" msgstr "" -#: templates/note/transaction_form.html:190 -msgid "Debit note" +#: templates/note/transactiontemplate_form.html:21 +msgid "Price history" msgstr "" -#: templates/note/transactiontemplate_form.html:6 -msgid "Buttons list" +#: templates/note/transactiontemplate_form.html:24 +msgid "Obsolete since" +msgstr "" + +#: templates/note/transactiontemplate_form.html:24 +msgid "Current price" msgstr "" #: templates/note/transactiontemplate_list.html:9 -msgid "search button" +msgid "Search button" +msgstr "" + +#: templates/note/transactiontemplate_list.html:11 +msgid "Name of the button..." msgstr "" -#: templates/note/transactiontemplate_list.html:13 +#: templates/note/transactiontemplate_list.html:16 +msgid "Display visible buttons only" +msgstr "" + +#: templates/note/transactiontemplate_list.html:21 msgid "New button" msgstr "" -#: templates/note/transactiontemplate_list.html:20 +#: templates/note/transactiontemplate_list.html:28 msgid "buttons listing " msgstr "" -#: templates/note/transactiontemplate_list.html:70 +#: templates/note/transactiontemplate_list.html:86 msgid "button successfully deleted " msgstr "" -#: templates/note/transactiontemplate_list.html:74 +#: templates/note/transactiontemplate_list.html:90 msgid "Unable to delete button " msgstr "" +#: templates/permission/all_rights.html:10 +msgid "Filter with roles that I have in at least one club" +msgstr "" + +#: templates/permission/all_rights.html:21 +msgid "Own this role in the clubs" +msgstr "" + +#: templates/permission/all_rights.html:26 +msgid "Query:" +msgstr "" + +#: templates/permission/all_rights.html:28 +msgid "No associated permission" +msgstr "" + #: templates/registration/email_validation_complete.html:6 msgid "Your email have successfully been validated." msgstr "" @@ -1335,6 +1793,7 @@ msgid "" msgstr "" #: templates/registration/future_profile_detail.html:56 +#: templates/wei/weiregistration_confirm_delete.html:12 msgid "Delete registration" msgstr "" @@ -1343,6 +1802,8 @@ msgid "Validate account" msgstr "" #: templates/registration/future_profile_detail.html:71 +#: templates/wei/weimembership_form.html:132 +#: templates/wei/weimembership_form.html:190 msgid "Validate registration" msgstr "" @@ -1350,6 +1811,10 @@ msgstr "" msgid "New user" msgstr "" +#: templates/registration/future_user_list.html:17 +msgid "There is no pending user with this pattern." +msgstr "" + #: templates/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." msgstr "" @@ -1359,7 +1824,7 @@ msgid "Log in again" msgstr "" #: templates/registration/login.html:7 templates/registration/login.html:8 -#: templates/registration/login.html:28 +#: templates/registration/login.html:21 #: templates/registration/password_reset_complete.html:10 msgid "Log in" msgstr "" @@ -1372,14 +1837,6 @@ msgid "" msgstr "" #: templates/registration/login.html:22 -msgid "You can also register via the central authentification server " -msgstr "" - -#: templates/registration/login.html:23 -msgid "using this link " -msgstr "" - -#: templates/registration/login.html:29 msgid "Forgotten your password or username?" msgstr "" @@ -1467,10 +1924,18 @@ msgid "Reset my password" msgstr "" #: templates/registration/signup.html:5 templates/registration/signup.html:8 -#: templates/registration/signup.html:14 +#: templates/registration/signup.html:19 msgid "Sign up" msgstr "" +#: templates/registration/signup.html:11 +msgid "" +"If you already signed up, your registration is taken into account. The BDE " +"must validate your account before your can log in. You have to go to the " +"Kfet and pay the registration fee. You must also validate your email address " +"by following the link you received." +msgstr "" + #: templates/treasury/invoice_form.html:6 msgid "Invoices list" msgstr "" @@ -1483,7 +1948,13 @@ msgstr "" msgid "Remove product" msgstr "" -#: templates/treasury/invoice_list.html:21 +#: templates/treasury/invoice_list.html:16 +#: templates/treasury/remittance_list.html:16 +#: templates/treasury/sogecredit_list.html:16 +msgid "Société générale credits" +msgstr "" + +#: templates/treasury/invoice_list.html:24 msgid "New invoice" msgstr "" @@ -1508,34 +1979,302 @@ msgstr "" msgid "There is no transaction linked with this remittance." msgstr "" -#: templates/treasury/remittance_list.html:19 +#: templates/treasury/remittance_list.html:22 msgid "Opened remittances" msgstr "" -#: templates/treasury/remittance_list.html:24 +#: templates/treasury/remittance_list.html:27 msgid "There is no opened remittance." msgstr "" -#: templates/treasury/remittance_list.html:28 +#: templates/treasury/remittance_list.html:31 msgid "New remittance" msgstr "" -#: templates/treasury/remittance_list.html:32 +#: templates/treasury/remittance_list.html:35 msgid "Transfers without remittances" msgstr "" -#: templates/treasury/remittance_list.html:37 +#: templates/treasury/remittance_list.html:40 msgid "There is no transaction without any linked remittance." msgstr "" -#: templates/treasury/remittance_list.html:43 +#: templates/treasury/remittance_list.html:46 msgid "Transfers with opened remittances" msgstr "" -#: templates/treasury/remittance_list.html:48 +#: templates/treasury/remittance_list.html:51 msgid "There is no transaction with an opened linked remittance." msgstr "" -#: templates/treasury/remittance_list.html:54 +#: templates/treasury/remittance_list.html:57 msgid "Closed remittances" msgstr "" + +#: templates/treasury/sogecredit_detail.html:29 +msgid "total amount" +msgstr "" + +#: templates/treasury/sogecredit_detail.html:35 +msgid "" +"Warning: Validating this credit implies that all membership transactions " +"will be validated." +msgstr "" + +#: templates/treasury/sogecredit_detail.html:36 +msgid "" +"If you delete this credit, there all membership transactions will be also " +"validated, but no credit will be operated." +msgstr "" + +#: templates/treasury/sogecredit_detail.html:37 +msgid "" +"If this credit is validated, then the user won't be able to ask for a credit " +"from the Société générale." +msgstr "" + +#: templates/treasury/sogecredit_detail.html:38 +msgid "If you think there is an error, please contact the \"respos info\"." +msgstr "" + +#: templates/treasury/sogecredit_detail.html:44 +msgid "This credit is already validated." +msgstr "" + +#: templates/treasury/sogecredit_detail.html:49 +msgid "" +"Warning: if you don't validate this credit, the note of the user doesn't " +"have enough money to pay its memberships." +msgstr "" + +#: templates/treasury/sogecredit_detail.html:50 +msgid "Please ask the user to credit its note before deleting this credit." +msgstr "" + +#: templates/treasury/sogecredit_detail.html:64 +msgid "Return to credit list" +msgstr "" + +#: templates/treasury/sogecredit_list.html:26 +msgid "Filter with unvalidated credits only" +msgstr "" + +#: templates/treasury/sogecredit_list.html:36 +msgid "There is no matched user that have asked for a Société générale credit." +msgstr "" + +#: templates/wei/bus_tables.html:16 templates/wei/busteam_tables.html:16 +msgid "Add team" +msgstr "" + +#: templates/wei/bus_tables.html:39 +msgid "Members" +msgstr "" + +#: templates/wei/bus_tables.html:48 templates/wei/busteam_tables.html:52 +#: templates/wei/weimembership_list.html:30 +msgid "View as PDF" +msgstr "" + +#: templates/wei/survey.html:24 +msgid "Next" +msgstr "" + +#: templates/wei/survey_closed.html:16 +msgid "The inscription for this WEI are now closed." +msgstr "" + +#: templates/wei/survey_closed.html:20 +msgid "Return to WEI detail" +msgstr "" + +#: templates/wei/survey_end.html:16 +msgid "The survey is now ended. Your answers have been saved." +msgstr "" + +#: templates/wei/weiclub_info.html:31 +msgid "WEI fee / including BDE and Kfet fee (paid students)" +msgstr "" + +#: templates/wei/weiclub_info.html:36 +msgid "WEI fee / including BDE and Kfet fee (unpaid students)" +msgstr "" + +#: templates/wei/weiclub_info.html:58 +msgid "WEI list" +msgstr "" + +#: templates/wei/weiclub_info.html:71 +msgid "Add bus" +msgstr "" + +#: templates/wei/weiclub_info.html:75 +msgid "View WEI" +msgstr "" + +#: templates/wei/weiclub_list.html:8 +msgid "search WEI" +msgstr "" + +#: templates/wei/weiclub_list.html:12 +msgid "Create WEI" +msgstr "" + +#: templates/wei/weiclub_list.html:19 +msgid "WEI listing" +msgstr "" + +#: templates/wei/weiclub_tables.html:63 +msgid "Register to the WEI! – 1A" +msgstr "" + +#: templates/wei/weiclub_tables.html:65 +msgid "Register to the WEI! – 2A+" +msgstr "" + +#: templates/wei/weiclub_tables.html:67 +msgid "Update my registration" +msgstr "" + +#: templates/wei/weiclub_tables.html:92 +msgid "Members of the WEI" +msgstr "" + +#: templates/wei/weiclub_tables.html:120 +msgid "Unvalidated registrations" +msgstr "" + +#: templates/wei/weimembership_form.html:14 +msgid "Review registration" +msgstr "" + +#: templates/wei/weimembership_form.html:39 +msgid "ENS year" +msgstr "" + +#: templates/wei/weimembership_form.html:83 +msgid "Payment from Société générale" +msgstr "" + +#: templates/wei/weimembership_form.html:87 +msgid "Suggested bus from the survey:" +msgstr "" + +#: templates/wei/weimembership_form.html:92 +msgid "Raw survey information" +msgstr "" + +#: templates/wei/weimembership_form.html:102 +msgid "The algorithm didn't run." +msgstr "" + +#: templates/wei/weimembership_form.html:105 +msgid "caution check given" +msgstr "" + +#: templates/wei/weimembership_form.html:109 +msgid "preferred bus" +msgstr "" + +#: templates/wei/weimembership_form.html:112 +msgid "preferred team" +msgstr "" + +#: templates/wei/weimembership_form.html:115 +msgid "preferred roles" +msgstr "" + +#: templates/wei/weimembership_form.html:123 +#: templates/wei/weiregistration_confirm_delete.html:31 +msgid "Update registration" +msgstr "" + +#: templates/wei/weimembership_form.html:136 +msgid "The registration is already validated and can't be unvalidated." +msgstr "" + +#: templates/wei/weimembership_form.html:137 +msgid "The user joined the bus" +msgstr "" + +#: templates/wei/weimembership_form.html:138 +msgid "in the team" +msgstr "" + +#: templates/wei/weimembership_form.html:139 +msgid "in no team (staff)" +msgstr "" + +#: templates/wei/weimembership_form.html:139 +msgid "with the following roles:" +msgstr "" + +#: templates/wei/weimembership_form.html:144 +msgid "" +"\n" +" The WEI will be paid by Société générale. The " +"membership will be created even if the bank didn't pay the BDE yet.\n" +" The membership transaction will be created but " +"will be invalid. You will have to validate it once the bank\n" +" validated the creation of the account, or to " +"change the payment method.\n" +" " +msgstr "" + +#: templates/wei/weimembership_form.html:154 +#, python-format +msgid "" +"\n" +" The note don't have enough money " +"(%(balance)s, %(pretty_fee)s required). The registration may fail.\n" +" " +msgstr "" + +#: templates/wei/weimembership_form.html:161 +msgid "The note has enough money, the registration is possible." +msgstr "" + +#: templates/wei/weimembership_form.html:168 +msgid "The user didn't give her/his caution check." +msgstr "" + +#: templates/wei/weimembership_form.html:176 +#, python-format +msgid "" +"\n" +" This user is not a member of the Kfet club. " +"Please adhere\n" +" <a href=\"%(future_user_detail)s\">here if he/" +"she is in her/his first year</a>\n" +" or <a href=\"%(club_detail)s\">here if he/she " +"was an old member</a> before you validate\n" +" the registration of the WEI.\n" +" " +msgstr "" + +#: templates/wei/weimembership_list.html:18 +msgid "There is no membership found with this pattern." +msgstr "" + +#: templates/wei/weimembership_list.html:24 +msgid "View unvalidated registrations..." +msgstr "" + +#: templates/wei/weiregistration_confirm_delete.html:17 +msgid "This registration is already validated and can't be deleted." +msgstr "" + +#: templates/wei/weiregistration_confirm_delete.html:24 +#, python-format +msgid "" +"Are you sure you want to delete the registration of %(user)s for the WEI " +"%(wei_name)s? This action can't be undone." +msgstr "" + +#: templates/wei/weiregistration_list.html:18 +msgid "There is no pre-registration found with this pattern." +msgstr "" + +#: templates/wei/weiregistration_list.html:24 +msgid "View validated memberships..." +msgstr "" diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 27636a65c5aee6787b817e86e8bbfbfc9af1a1dd..f23c19135a9ffde6460516176e1ef25b472eaafb 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -1,9 +1,14 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. +# #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-04-09 21:59+0200\n" +"POT-Creation-Date: 2020-04-27 03:55+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <LL@li.org>\n" @@ -14,37 +19,39 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n > 1);\n" #: apps/activity/apps.py:10 apps/activity/models.py:102 -#: apps/activity/models.py:111 +#: apps/activity/models.py:117 msgid "activity" msgstr "activité" -#: apps/activity/forms.py:45 apps/activity/models.py:208 +#: apps/activity/forms.py:45 apps/activity/models.py:213 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 +#: apps/activity/forms.py:48 apps/activity/models.py:216 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 +#: apps/activity/forms.py:58 apps/activity/models.py:224 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 +#: apps/activity/forms.py:62 apps/activity/models.py:228 msgid "This person is already invited." msgstr "Cette personne est déjà invitée." -#: apps/activity/forms.py:66 apps/activity/models.py:227 +#: apps/activity/forms.py:66 apps/activity/models.py:232 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 +#: apps/member/models.py:151 apps/member/models.py:255 +#: apps/note/models/notes.py:188 apps/note/models/transactions.py:25 +#: apps/note/models/transactions.py:45 apps/note/models/transactions.py:250 +#: apps/wei/models.py:62 templates/member/club_info.html:13 +#: templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 +#: templates/wei/weiclub_info.html:13 templates/wei/weimembership_form.html:18 msgid "name" msgstr "nom" @@ -64,19 +71,23 @@ msgstr "type d'activité" msgid "activity types" msgstr "types d'activité" -#: apps/activity/models.py:53 apps/note/models/transactions.py:74 -#: apps/permission/models.py:103 templates/activity/activity_detail.html:16 +#: apps/activity/models.py:53 apps/note/models/transactions.py:75 +#: apps/permission/models.py:103 apps/permission/models.py:176 +#: apps/wei/models.py:68 apps/wei/models.py:124 +#: templates/activity/activity_detail.html:16 msgid "description" msgstr "description" #: apps/activity/models.py:60 apps/note/models/notes.py:164 -#: apps/note/models/transactions.py:64 +#: apps/note/models/transactions.py:65 apps/permission/models.py:157 #: templates/activity/activity_detail.html:19 msgid "type" msgstr "type" -#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:223 -#: apps/note/models/notes.py:117 +#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:277 +#: apps/note/models/notes.py:117 apps/treasury/models.py:221 +#: apps/wei/models.py:155 templates/treasury/sogecredit_detail.html:14 +#: templates/wei/survey.html:16 msgid "user" msgstr "utilisateur" @@ -96,7 +107,7 @@ msgstr "date de début" msgid "end date" msgstr "date de fin" -#: apps/activity/models.py:93 apps/note/models/transactions.py:139 +#: apps/activity/models.py:93 apps/note/models/transactions.py:140 #: templates/activity/activity_detail.html:47 msgid "valid" msgstr "valide" @@ -109,57 +120,58 @@ msgstr "ouvrir" msgid "activities" msgstr "activités" -#: apps/activity/models.py:116 +#: apps/activity/models.py:122 msgid "entry time" msgstr "heure d'entrée" -#: apps/activity/models.py:122 apps/note/apps.py:14 +#: apps/activity/models.py:128 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 +#: apps/activity/models.py:139 templates/activity/activity_entry.html:38 msgid "entry" msgstr "entrée" -#: apps/activity/models.py:134 templates/activity/activity_entry.html:38 +#: apps/activity/models.py:140 templates/activity/activity_entry.html:38 msgid "entries" msgstr "entrées" -#: apps/activity/models.py:141 +#: apps/activity/models.py:146 msgid "Already entered on " msgstr "Déjà rentré le " -#: apps/activity/models.py:141 apps/activity/tables.py:54 +#: apps/activity/models.py:146 apps/activity/tables.py:54 msgid "{:%Y-%m-%d %H:%M:%S}" msgstr "{:%d/%m/%Y %H:%M:%S}" -#: apps/activity/models.py:149 +#: apps/activity/models.py:154 msgid "The balance is negative." msgstr "La note est en négatif." -#: apps/activity/models.py:179 +#: apps/activity/models.py:184 msgid "last name" msgstr "nom de famille" -#: apps/activity/models.py:184 templates/member/profile_info.html:14 +#: apps/activity/models.py:189 templates/member/profile_info.html:14 #: templates/registration/future_profile_detail.html:16 +#: templates/wei/weimembership_form.html:18 msgid "first name" msgstr "prénom" -#: apps/activity/models.py:191 +#: apps/activity/models.py:196 msgid "inviter" msgstr "hôte" -#: apps/activity/models.py:232 +#: apps/activity/models.py:237 msgid "guest" msgstr "invité" -#: apps/activity/models.py:233 +#: apps/activity/models.py:238 msgid "guests" msgstr "invités" -#: apps/activity/models.py:245 +#: apps/activity/models.py:250 msgid "Invitation" msgstr "Invitation" @@ -171,17 +183,17 @@ msgstr "Entré le " msgid "remove" msgstr "supprimer" -#: apps/activity/tables.py:75 apps/treasury/models.py:138 +#: apps/activity/tables.py:75 apps/treasury/models.py:140 msgid "Type" msgstr "Type" -#: apps/activity/tables.py:77 apps/member/forms.py:75 -#: apps/registration/forms.py:55 apps/treasury/forms.py:121 +#: apps/activity/tables.py:77 apps/member/forms.py:83 +#: apps/registration/forms.py:64 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 +#: apps/activity/tables.py:79 apps/member/forms.py:88 +#: apps/registration/forms.py:69 apps/treasury/forms.py:123 #: templates/note/transaction_form.html:97 msgid "First name" msgstr "Prénom" @@ -194,11 +206,11 @@ msgstr "Note" msgid "Balance" msgstr "Solde du compte" -#: apps/activity/views.py:45 templates/base.html:106 +#: apps/activity/views.py:46 templates/base.html:121 msgid "Activities" msgstr "Activités" -#: apps/activity/views.py:154 +#: apps/activity/views.py:153 msgid "Entry for activity \"{}\"" msgstr "Entrées pour l'activité « {} »" @@ -214,7 +226,7 @@ msgstr "Logs" msgid "IP Address" msgstr "Adresse IP" -#: apps/logs/models.py:35 +#: apps/logs/models.py:35 apps/permission/models.py:127 msgid "model" msgstr "Modèle" @@ -234,12 +246,13 @@ msgstr "Nouvelles données" msgid "create" msgstr "Créer" -#: apps/logs/models.py:61 apps/note/tables.py:144 +#: apps/logs/models.py:61 apps/note/tables.py:145 #: templates/activity/activity_detail.html:67 msgid "edit" msgstr "Modifier" #: apps/logs/models.py:62 apps/note/tables.py:120 apps/note/tables.py:149 +#: apps/wei/tables.py:65 msgid "delete" msgstr "Supprimer" @@ -257,141 +270,214 @@ msgstr "Les logs ne peuvent pas être détruits." #: apps/logs/models.py:80 msgid "changelog" -msgstr "" +msgstr "journal de modification" #: apps/logs/models.py:81 msgid "changelogs" -msgstr "" +msgstr "journaux de modifications" -#: apps/member/apps.py:14 +#: apps/member/apps.py:14 apps/wei/tables.py:150 apps/wei/tables.py:181 msgid "member" msgstr "adhérent" -#: apps/member/forms.py:54 apps/registration/forms.py:35 +#: apps/member/forms.py:62 apps/registration/forms.py:44 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 +#: apps/member/forms.py:64 apps/registration/forms.py:46 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 +#: apps/member/forms.py:69 apps/registration/forms.py:51 msgid "Credit type" msgstr "Type de rechargement" -#: apps/member/forms.py:62 apps/registration/forms.py:43 +#: apps/member/forms.py:70 apps/registration/forms.py:52 msgid "No credit" msgstr "Pas de rechargement" -#: apps/member/forms.py:64 +#: apps/member/forms.py:72 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 +#: apps/member/forms.py:76 apps/registration/forms.py:57 msgid "Credit amount" msgstr "Montant à créditer" -#: apps/member/forms.py:85 apps/registration/forms.py:65 +#: apps/member/forms.py:93 apps/registration/forms.py:74 #: apps/treasury/forms.py:125 templates/note/transaction_form.html:103 msgid "Bank" msgstr "Banque" -#: apps/member/models.py:33 +#: apps/member/models.py:34 #: templates/registration/future_profile_detail.html:47 +#: templates/wei/weimembership_form.html:48 msgid "phone number" msgstr "numéro de téléphone" -#: apps/member/models.py:39 templates/member/profile_info.html:27 +#: apps/member/models.py:41 templates/member/profile_info.html:27 #: templates/registration/future_profile_detail.html:41 +#: templates/wei/weimembership_form.html:42 msgid "section" msgstr "section" -#: apps/member/models.py:40 +#: apps/member/models.py:42 msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" -#: apps/member/models.py:46 templates/member/profile_info.html:30 +#: apps/member/models.py:50 templates/wei/weimembership_form.html:36 +msgid "department" +msgstr "département" + +#: apps/member/models.py:52 +msgid "Informatics (A0)" +msgstr "Informatique (A0)" + +#: apps/member/models.py:53 +msgid "Mathematics (A1)" +msgstr "Mathématiques (A1)" + +#: apps/member/models.py:54 +msgid "Physics (A2)" +msgstr "Physique (A2)" + +#: apps/member/models.py:55 +msgid "Applied physics (A'2)" +msgstr "Physique appliquée (A'2)" + +#: apps/member/models.py:56 +msgid "Chemistry (A''2)" +msgstr "Chimie (A''2)" + +#: apps/member/models.py:57 +msgid "Biology (A3)" +msgstr "Biologie (A3)" + +#: apps/member/models.py:58 +msgid "SAPHIRE (B1234)" +msgstr "SAPHIRE (B1234)" + +#: apps/member/models.py:59 +msgid "Mechanics (B1)" +msgstr "Mécanique (B1)" + +#: apps/member/models.py:60 +msgid "Civil engineering (B2)" +msgstr "Génie civil (B2)" + +#: apps/member/models.py:61 +msgid "Mechanical engineering (B3)" +msgstr "Génie mécanique (B3)" + +#: apps/member/models.py:62 +msgid "EEA (B4)" +msgstr "EEA (B4)" + +#: apps/member/models.py:63 +msgid "Design (C)" +msgstr "Design (C)" + +#: apps/member/models.py:64 +msgid "Economy-management (D2)" +msgstr "Économie-gestion (D2)" + +#: apps/member/models.py:65 +msgid "Social sciences (D3)" +msgstr "Sciences sociales (D3)" + +#: apps/member/models.py:66 +msgid "English (E)" +msgstr "Anglais (E)" + +#: apps/member/models.py:67 +msgid "External (EXT)" +msgstr "Externe (EXT)" + +#: apps/member/models.py:74 +msgid "promotion" +msgstr "promotion" + +#: apps/member/models.py:75 +msgid "Year of entry to the school (None if not ENS student)" +msgstr "Année d'entrée dans l'école (None si non-étudiant·e de l'ENS)" + +#: apps/member/models.py:79 templates/member/profile_info.html:30 #: templates/registration/future_profile_detail.html:44 +#: templates/wei/weimembership_form.html:45 msgid "address" msgstr "adresse" -#: apps/member/models.py:52 +#: apps/member/models.py:86 #: templates/registration/future_profile_detail.html:50 +#: templates/wei/weimembership_form.html:51 msgid "paid" msgstr "payé" -#: apps/member/models.py:53 +#: apps/member/models.py:87 msgid "Tells if the user receive a salary." msgstr "Indique si l'utilisateur perçoit un salaire." -#: apps/member/models.py:58 +#: apps/member/models.py:92 msgid "email confirmed" msgstr "adresse email confirmée" -#: apps/member/models.py:63 +#: apps/member/models.py:97 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 +#: apps/member/models.py:126 apps/member/models.py:127 msgid "user profile" msgstr "profil utilisateur" -#: apps/member/models.py:104 templates/member/club_info.html:46 +#: apps/member/models.py:156 templates/member/club_info.html:57 #: templates/registration/future_profile_detail.html:22 +#: templates/wei/weiclub_info.html:52 templates/wei/weimembership_form.html:24 msgid "email" msgstr "courriel" -#: apps/member/models.py:111 +#: apps/member/models.py:163 msgid "parent club" msgstr "club parent" -#: apps/member/models.py:120 +#: apps/member/models.py:172 msgid "require memberships" msgstr "nécessite des adhésions" -#: apps/member/models.py:121 +#: apps/member/models.py:173 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 +#: apps/member/models.py:178 templates/member/club_info.html:41 msgid "membership fee (paid students)" msgstr "cotisation pour adhérer (normalien élève)" -#: apps/member/models.py:131 templates/member/club_info.html:38 +#: apps/member/models.py:183 templates/member/club_info.html:44 msgid "membership fee (unpaid students)" msgstr "cotisation pour adhérer (normalien étudiant)" -#: apps/member/models.py:137 templates/member/club_info.html:28 +#: apps/member/models.py:189 templates/member/club_info.html:33 msgid "membership duration" msgstr "durée de l'adhésion" -#: apps/member/models.py:138 +#: apps/member/models.py:190 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:145 templates/member/club_info.html:22 +#: apps/member/models.py:197 templates/member/club_info.html:23 msgid "membership start" msgstr "début de l'adhésion" -#: apps/member/models.py:146 +#: apps/member/models.py:198 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:153 templates/member/club_info.html:25 +#: apps/member/models.py:205 templates/member/club_info.html:28 msgid "membership end" msgstr "fin de l'adhésion" -#: apps/member/models.py:154 +#: apps/member/models.py:206 msgid "" "How long the membership can last after January 1st of the next year after " "members can renew their membership." @@ -399,53 +485,53 @@ 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:187 apps/member/models.py:229 +#: apps/member/models.py:240 apps/member/models.py:283 #: apps/note/models/notes.py:139 msgid "club" msgstr "club" -#: apps/member/models.py:188 +#: apps/member/models.py:241 msgid "clubs" msgstr "clubs" -#: apps/member/models.py:208 apps/permission/models.py:294 +#: apps/member/models.py:261 apps/permission/models.py:312 msgid "role" msgstr "rôle" -#: apps/member/models.py:209 apps/member/models.py:234 +#: apps/member/models.py:262 apps/member/models.py:288 msgid "roles" msgstr "rôles" -#: apps/member/models.py:239 +#: apps/member/models.py:293 msgid "membership starts on" msgstr "l'adhésion commence le" -#: apps/member/models.py:243 +#: apps/member/models.py:297 msgid "membership ends on" msgstr "l'adhésion finit le" -#: apps/member/models.py:248 +#: apps/member/models.py:302 msgid "fee" msgstr "cotisation" -#: apps/member/models.py:266 apps/member/views.py:500 +#: apps/member/models.py:320 apps/member/views.py:505 apps/wei/views.py:768 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 +#: apps/member/models.py:330 apps/member/views.py:514 msgid "User is already a member of the club" msgstr "L'utilisateur est déjà membre du club" -#: apps/member/models.py:314 +#: apps/member/models.py:381 #, 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 +#: apps/member/models.py:384 msgid "membership" msgstr "adhésion" -#: apps/member/models.py:318 +#: apps/member/models.py:385 msgid "memberships" msgstr "adhésions" @@ -459,6 +545,7 @@ msgstr "Cette adresse doit être valide." #: apps/member/views.py:65 templates/member/profile_info.html:45 #: templates/registration/future_profile_detail.html:55 +#: templates/wei/weimembership_form.html:122 msgid "Update Profile" msgstr "Modifier le profil" @@ -466,11 +553,11 @@ msgstr "Modifier le profil" msgid "An alias with a similar name already exists." msgstr "Un alias avec un nom similaire existe déjà ." -#: apps/member/views.py:180 +#: apps/member/views.py:181 msgid "Search user" msgstr "Chercher un utilisateur" -#: apps/member/views.py:495 +#: apps/member/views.py:500 apps/wei/views.py:759 msgid "" "This user don't have enough money to join this club, and can't have a " "negative balance." @@ -478,26 +565,26 @@ msgstr "" "Cet utilisateur n'a pas assez d'argent pour rejoindre ce club et ne peut pas " "avoir un solde négatif." -#: apps/member/views.py:513 +#: apps/member/views.py:518 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 +#: apps/member/views.py:523 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 +#: apps/member/views.py:540 apps/member/views.py:542 apps/member/views.py:544 +#: apps/registration/views.py:289 apps/registration/views.py:291 +#: apps/registration/views.py:293 msgid "This field is required." msgstr "Ce champ est requis." -#: apps/note/admin.py:120 apps/note/models/transactions.py:99 +#: apps/note/admin.py:120 apps/note/models/transactions.py:100 msgid "source" msgstr "source" #: apps/note/admin.py:128 apps/note/admin.py:170 -#: apps/note/models/transactions.py:54 apps/note/models/transactions.py:112 +#: apps/note/models/transactions.py:55 apps/note/models/transactions.py:113 msgid "destination" msgstr "destination" @@ -540,7 +627,7 @@ msgstr "" msgid "display image" msgstr "image affichée" -#: apps/note/models/notes.py:53 apps/note/models/transactions.py:122 +#: apps/note/models/notes.py:53 apps/note/models/transactions.py:123 msgid "created at" msgstr "créée le" @@ -594,8 +681,8 @@ msgstr "Alias invalide" msgid "alias" msgstr "alias" -#: apps/note/models/notes.py:211 templates/member/club_info.html:43 -#: templates/member/profile_info.html:36 +#: apps/note/models/notes.py:211 templates/member/club_info.html:54 +#: templates/member/profile_info.html:36 templates/wei/weiclub_info.html:48 msgid "aliases" msgstr "alias" @@ -611,97 +698,98 @@ msgstr "Un alias avec un nom similaire existe déjà : {}" msgid "You can't delete your main alias." msgstr "Vous ne pouvez pas supprimer votre alias principal." -#: apps/note/models/transactions.py:30 +#: apps/note/models/transactions.py:31 msgid "transaction category" msgstr "catégorie de transaction" -#: apps/note/models/transactions.py:31 +#: apps/note/models/transactions.py:32 msgid "transaction categories" msgstr "catégories de transaction" -#: apps/note/models/transactions.py:47 +#: apps/note/models/transactions.py:48 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:58 apps/note/models/transactions.py:130 +#: apps/note/models/transactions.py:59 apps/note/models/transactions.py:131 msgid "amount" msgstr "montant" -#: apps/note/models/transactions.py:59 +#: apps/note/models/transactions.py:60 msgid "in centimes" msgstr "en centimes" -#: apps/note/models/transactions.py:70 +#: apps/note/models/transactions.py:71 msgid "display" msgstr "afficher" -#: apps/note/models/transactions.py:80 +#: apps/note/models/transactions.py:81 msgid "transaction template" msgstr "modèle de transaction" -#: apps/note/models/transactions.py:81 +#: apps/note/models/transactions.py:82 msgid "transaction templates" msgstr "modèles de transaction" -#: apps/note/models/transactions.py:105 apps/note/models/transactions.py:118 +#: apps/note/models/transactions.py:106 apps/note/models/transactions.py:119 #: apps/note/tables.py:33 apps/note/tables.py:42 msgid "used alias" msgstr "alias utilisé" -#: apps/note/models/transactions.py:126 +#: apps/note/models/transactions.py:127 msgid "quantity" msgstr "quantité" -#: apps/note/models/transactions.py:134 +#: apps/note/models/transactions.py:135 msgid "reason" msgstr "raison" -#: apps/note/models/transactions.py:144 apps/note/tables.py:95 +#: apps/note/models/transactions.py:145 apps/note/tables.py:95 msgid "invalidity reason" msgstr "Motif d'invalidité" -#: apps/note/models/transactions.py:152 +#: apps/note/models/transactions.py:153 msgid "transaction" msgstr "transaction" -#: apps/note/models/transactions.py:153 +#: apps/note/models/transactions.py:154 +#: templates/treasury/sogecredit_detail.html:22 msgid "transactions" msgstr "transactions" -#: apps/note/models/transactions.py:207 +#: apps/note/models/transactions.py:216 #: templates/activity/activity_entry.html:13 templates/base.html:84 #: templates/note/transaction_form.html:19 -#: templates/note/transaction_form.html:145 +#: templates/note/transaction_form.html:140 msgid "Transfer" msgstr "Virement" -#: apps/note/models/transactions.py:227 +#: apps/note/models/transactions.py:240 msgid "Template" msgstr "Bouton" -#: apps/note/models/transactions.py:242 +#: apps/note/models/transactions.py:255 msgid "first_name" msgstr "prénom" -#: apps/note/models/transactions.py:247 +#: apps/note/models/transactions.py:260 msgid "bank" msgstr "banque" -#: apps/note/models/transactions.py:253 +#: apps/note/models/transactions.py:266 #: templates/activity/activity_entry.html:17 #: templates/note/transaction_form.html:24 msgid "Credit" msgstr "Crédit" -#: apps/note/models/transactions.py:253 templates/note/transaction_form.html:28 +#: apps/note/models/transactions.py:266 templates/note/transaction_form.html:28 msgid "Debit" msgstr "Débit" -#: apps/note/models/transactions.py:269 apps/note/models/transactions.py:274 +#: apps/note/models/transactions.py:282 apps/note/models/transactions.py:287 msgid "membership transaction" msgstr "Transaction d'adhésion" -#: apps/note/models/transactions.py:270 +#: apps/note/models/transactions.py:283 apps/treasury/models.py:227 msgid "membership transactions" msgstr "Transactions d'adhésion" @@ -717,32 +805,36 @@ msgstr "Cliquez pour valider" msgid "No reason specified" msgstr "Pas de motif spécifié" -#: apps/note/tables.py:122 apps/note/tables.py:151 +#: apps/note/tables.py:122 apps/note/tables.py:151 apps/wei/tables.py:66 +#: templates/treasury/sogecredit_detail.html:59 +#: templates/wei/weiregistration_confirm_delete.html:32 msgid "Delete" msgstr "Supprimer" -#: apps/note/tables.py:146 templates/member/club_info.html:55 -#: templates/note/conso_form.html:121 +#: apps/note/tables.py:146 apps/wei/tables.py:42 apps/wei/tables.py:43 +#: templates/member/club_info.html:67 templates/note/conso_form.html:121 +#: templates/wei/bus_tables.html:15 templates/wei/busteam_tables.html:15 +#: templates/wei/busteam_tables.html:33 templates/wei/weiclub_info.html:68 msgid "Edit" msgstr "Éditer" -#: apps/note/views.py:40 +#: apps/note/views.py:41 msgid "Transfer money" msgstr "Transférer de l'argent" -#: apps/note/views.py:109 templates/base.html:79 +#: apps/note/views.py:137 templates/base.html:94 msgid "Consumptions" msgstr "Consommations" -#: apps/permission/models.py:82 apps/permission/models.py:281 +#: apps/permission/models.py:82 #, python-brace-format msgid "Can {type} {model}.{field} in {query}" -msgstr "" +msgstr "Can {type} {model}.{field} in {query}" -#: apps/permission/models.py:84 apps/permission/models.py:283 +#: apps/permission/models.py:84 #, python-brace-format msgid "Can {type} {model} in {query}" -msgstr "" +msgstr "Can {type} {model} in {query}" #: apps/permission/models.py:97 msgid "rank" @@ -756,75 +848,134 @@ msgstr "masque de permissions" msgid "permission masks" msgstr "masques de permissions" -#: apps/permission/models.py:160 +#: apps/permission/models.py:151 +msgid "query" +msgstr "requête" + +#: apps/permission/models.py:164 +msgid "mask" +msgstr "masque" + +#: apps/permission/models.py:170 +msgid "field" +msgstr "champ" + +#: apps/permission/models.py:181 msgid "permission" msgstr "permission" -#: apps/permission/models.py:161 +#: apps/permission/models.py:182 apps/permission/models.py:316 msgid "permissions" msgstr "permissions" -#: apps/permission/models.py:166 +#: apps/permission/models.py:187 msgid "Specifying field applies only to view and change permission types." msgstr "" +"Spécifie le champ concerné, ne fonctionne que pour les permissions view et " +"change." -#: apps/permission/models.py:304 apps/permission/models.py:305 +#: apps/permission/models.py:323 apps/permission/models.py:324 msgid "role permissions" msgstr "Permissions par rôles" +#: apps/permission/signals.py:62 +#, python-brace-format +msgid "" +"You don't have the permission to change the field {field} on this instance " +"of model {app_label}.{model_name}." +msgstr "" +"Vous n'avez pas la permission de modifier le champ {field} sur l'instance du " +"modèle {app_label}.{model_name}." + +#: apps/permission/signals.py:72 +#, python-brace-format +msgid "" +"You don't have the permission to add this instance of model {app_label}." +"{model_name}." +msgstr "" +"Vous n'avez pas la permission d'ajouter cette instance du modèle {app_label}." +"{model_name}." + +#: apps/permission/signals.py:99 +#, python-brace-format +msgid "" +"You don't have the permission to delete this instance of model {app_label}." +"{model_name}." +msgstr "" +"Vous n'avez pas la permission de supprimer cette instance du modèle " +"{app_label}.{model_name}." + +#: apps/permission/views.py:47 +msgid "All rights" +msgstr "Tous les droits" + #: apps/registration/apps.py:10 msgid "registration" msgstr "inscription" -#: apps/registration/forms.py:70 +#: apps/registration/forms.py:32 +msgid "Register to the WEI" +msgstr "S'inscrire au WEI" + +#: apps/registration/forms.py:34 +msgid "" +"Check this case if you want to register to the WEI. If you hesitate, you " +"will be able to register later, after validating your account in the Kfet." +msgstr "" +"Cochez cette case si vous voulez vous inscrire au WEI. Si vous hésitez, vous " +"pourrez toujours vous inscrire plus tard, après avoir validé votre compte à " +"la Kfet." + +#: apps/registration/forms.py:79 msgid "Join BDE Club" msgstr "Adhérer au club BDE" -#: apps/registration/forms.py:77 +#: apps/registration/forms.py:86 msgid "Join Kfet Club" msgstr "Adhérer au club Kfet" -#: apps/registration/views.py:75 +#: apps/registration/views.py:78 msgid "Email validation" msgstr "Validation de l'adresse mail" -#: apps/registration/views.py:121 +#: apps/registration/views.py:124 msgid "Email validation unsuccessful" msgstr " La validation de l'adresse mail a échoué" -#: apps/registration/views.py:132 +#: apps/registration/views.py:135 msgid "Email validation email sent" msgstr "L'email de vérification de l'adresse email a bien été envoyé." -#: apps/registration/views.py:185 +#: apps/registration/views.py:188 msgid "Unregistered users" msgstr "Utilisateurs en attente d'inscription" -#: apps/registration/views.py:252 +#: apps/registration/views.py:255 msgid "You must join the BDE." msgstr "Vous devez adhérer au BDE." -#: apps/registration/views.py:274 +#: apps/registration/views.py:277 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 +#: apps/registration/views.py:282 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 +#: apps/treasury/apps.py:12 templates/base.html:126 msgid "Treasury" msgstr "Trésorerie" #: 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/add_members.html:14 templates/member/club_form.html:9 -#: templates/treasury/invoice_form.html:46 +#: templates/treasury/invoice_form.html:46 templates/wei/bus_form.html:13 +#: templates/wei/busteam_form.html:13 templates/wei/weiclub_form.html:15 +#: templates/wei/weiregistration_form.html:14 msgid "Submit" msgstr "Envoyer" @@ -841,123 +992,145 @@ 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:127 apps/treasury/tables.py:47 -#: templates/note/transaction_form.html:133 +#: apps/treasury/tables.py:113 templates/note/transaction_form.html:133 #: templates/treasury/remittance_form.html:18 msgid "Amount" msgstr "Montant" -#: apps/treasury/models.py:18 +#: apps/treasury/models.py:20 msgid "Invoice identifier" msgstr "Numéro de facture" -#: apps/treasury/models.py:32 +#: apps/treasury/models.py:34 msgid "BDE" msgstr "BDE" -#: apps/treasury/models.py:37 +#: apps/treasury/models.py:39 msgid "Object" msgstr "Objet" -#: apps/treasury/models.py:41 +#: apps/treasury/models.py:43 msgid "Description" msgstr "Description" -#: apps/treasury/models.py:46 templates/note/transaction_form.html:91 +#: apps/treasury/models.py:48 templates/note/transaction_form.html:91 msgid "Name" msgstr "Nom" -#: apps/treasury/models.py:50 +#: apps/treasury/models.py:52 msgid "Address" msgstr "Adresse" -#: apps/treasury/models.py:55 +#: apps/treasury/models.py:57 msgid "Place" msgstr "Lieu" -#: apps/treasury/models.py:59 +#: apps/treasury/models.py:61 msgid "Acquitted" msgstr "Acquittée" -#: apps/treasury/models.py:63 +#: apps/treasury/models.py:65 msgid "invoice" msgstr "facture" -#: apps/treasury/models.py:64 +#: apps/treasury/models.py:66 msgid "invoices" msgstr "factures" -#: apps/treasury/models.py:79 +#: apps/treasury/models.py:81 msgid "Designation" msgstr "Désignation" -#: apps/treasury/models.py:83 +#: apps/treasury/models.py:85 msgid "Quantity" msgstr "Quantité" -#: apps/treasury/models.py:87 +#: apps/treasury/models.py:89 msgid "Unit price" msgstr "Prix unitaire" -#: apps/treasury/models.py:103 +#: apps/treasury/models.py:105 msgid "product" msgstr "produit" -#: apps/treasury/models.py:104 +#: apps/treasury/models.py:106 msgid "products" msgstr "produits" -#: apps/treasury/models.py:121 +#: apps/treasury/models.py:123 msgid "remittance type" msgstr "type de remise" -#: apps/treasury/models.py:122 +#: apps/treasury/models.py:124 msgid "remittance types" msgstr "types de remises" -#: apps/treasury/models.py:132 +#: apps/treasury/models.py:134 msgid "Date" msgstr "Date" -#: apps/treasury/models.py:143 +#: apps/treasury/models.py:145 msgid "Comment" msgstr "Commentaire" -#: apps/treasury/models.py:148 +#: apps/treasury/models.py:150 msgid "Closed" msgstr "Fermée" -#: apps/treasury/models.py:152 +#: apps/treasury/models.py:154 msgid "remittance" msgstr "remise" -#: apps/treasury/models.py:153 +#: apps/treasury/models.py:155 msgid "remittances" msgstr "remises" -#: apps/treasury/models.py:185 +#: apps/treasury/models.py:187 msgid "Remittance #{:d}: {}" msgstr "Remise n°{:d} : {}" -#: apps/treasury/models.py:204 apps/treasury/tables.py:76 +#: apps/treasury/models.py:206 apps/treasury/tables.py:76 #: apps/treasury/tables.py:84 templates/treasury/invoice_list.html:13 #: templates/treasury/remittance_list.html:13 +#: templates/treasury/sogecredit_list.html:13 msgid "Remittance" msgstr "Remise" -#: apps/treasury/models.py:208 +#: apps/treasury/models.py:210 msgid "special transaction proxy" msgstr "Proxy de transaction spéciale" -#: apps/treasury/models.py:209 +#: apps/treasury/models.py:211 msgid "special transaction proxies" msgstr "Proxys de transactions spéciales" +#: apps/treasury/models.py:233 +msgid "credit transaction" +msgstr "transaction de crédit" + +#: apps/treasury/models.py:296 +msgid "" +"This user doesn't have enough money to pay the memberships with its note. " +"Please ask her/him to credit the note before invalidating this credit." +msgstr "" +"Cet utilisateur n'a pas assez d'argent pour payer les adhésions avec sa " +"note. Merci de lui demander de recharger sa note avant d'invalider ce crédit." + +#: apps/treasury/models.py:308 templates/treasury/sogecredit_detail.html:10 +msgid "Credit from the Société générale" +msgstr "Crédit de la Société générale" + +#: apps/treasury/models.py:309 +msgid "Credits from the Société générale" +msgstr "Crédits de la Société générale" + #: apps/treasury/tables.py:19 msgid "Invoice #{:d}" msgstr "Facture n°{:d}" #: apps/treasury/tables.py:22 templates/treasury/invoice_list.html:10 #: templates/treasury/remittance_list.html:10 +#: templates/treasury/sogecredit_list.html:10 msgid "Invoice" msgstr "Facture" @@ -977,24 +1150,352 @@ msgstr "Ajouter" msgid "Remove" msgstr "supprimer" -#: note_kfet/settings/__init__.py:63 +#: apps/treasury/tables.py:117 +msgid "Valid" +msgstr "Valide" + +#: apps/treasury/tables.py:124 +msgid "Yes" +msgstr "Oui" + +#: apps/treasury/tables.py:124 +msgid "No" +msgstr "Non" + +#: apps/wei/apps.py:10 apps/wei/models.py:45 apps/wei/models.py:46 +#: apps/wei/models.py:57 apps/wei/models.py:162 templates/base.html:116 +msgid "WEI" +msgstr "WEI" + +#: apps/wei/forms/registration.py:47 apps/wei/models.py:109 +#: apps/wei/models.py:271 +msgid "bus" +msgstr "Bus" + +#: apps/wei/forms/registration.py:48 msgid "" -"The Central Authentication Service grants you access to most of our websites " -"by authenticating only once, so you don't need to type your credentials " -"again unless your session expires or you logout." +"This choice is not definitive. The WEI organizers are free to attribute for " +"you a bus and a team, in particular if you are a free eletron." msgstr "" +"Ce choix n'est pas définitif. Les organisateurs du WEI sont libres de vous " +"attribuer un bus et une équipe, en particulier si vous êtes un électron " +"libre." -#: note_kfet/settings/base.py:153 -msgid "German" +#: apps/wei/forms/registration.py:54 +msgid "Team" +msgstr "Équipe" + +#: apps/wei/forms/registration.py:56 +msgid "" +"Leave this field empty if you won't be in a team (staff, bus chief, free " +"electron)" msgstr "" +"Laissez ce champ vide si vous ne serez pas dans une équipe (staff, chef de " +"bus ou électron libre)" -#: note_kfet/settings/base.py:154 -msgid "English" +#: apps/wei/forms/registration.py:61 apps/wei/forms/registration.py:67 +#: apps/wei/models.py:143 +msgid "WEI Roles" +msgstr "Rôles au WEI" + +#: apps/wei/forms/registration.py:62 +msgid "Select the roles that you are interested in." +msgstr "Sélectionnez les rôles qui vous intéressent." + +#: apps/wei/forms/registration.py:72 +msgid "This team doesn't belong to the given bus." +msgstr "Cette équipe n'appartient pas à ce bus." + +#: apps/wei/models.py:20 templates/wei/weiclub_info.html:23 +msgid "year" +msgstr "année" + +#: apps/wei/models.py:24 templates/wei/weiclub_info.html:17 +msgid "date start" +msgstr "début" + +#: apps/wei/models.py:28 templates/wei/weiclub_info.html:20 +msgid "date end" +msgstr "fin" + +#: apps/wei/models.py:73 +msgid "survey information" +msgstr "informations sur le questionnaire" + +#: apps/wei/models.py:74 +msgid "Information about the survey for new members, encoded in JSON" +msgstr "" +"Informations sur le sondage pour les nouveaux membres, encodées en JSON" + +#: apps/wei/models.py:96 +msgid "Bus" +msgstr "Bus" + +#: apps/wei/models.py:97 templates/wei/weiclub_tables.html:79 +msgid "Buses" +msgstr "Bus" + +#: apps/wei/models.py:117 +msgid "color" +msgstr "couleur" + +#: apps/wei/models.py:118 +msgid "The color of the T-Shirt, stored with its number equivalent" msgstr "" +"La couleur du T-Shirt, stocké sous la forme de son équivalent numérique" + +#: apps/wei/models.py:132 +msgid "Bus team" +msgstr "Équipe de bus" + +#: apps/wei/models.py:133 +msgid "Bus teams" +msgstr "Équipes de bus" + +#: apps/wei/models.py:142 +msgid "WEI Role" +msgstr "Rôle au WEI" + +#: apps/wei/models.py:167 +msgid "Credit from Société générale" +msgstr "Crédit de la Société générale" + +#: apps/wei/models.py:172 +msgid "Caution check given" +msgstr "Chèque de caution donné" + +#: apps/wei/models.py:176 templates/wei/weimembership_form.html:62 +msgid "birth date" +msgstr "date de naissance" + +#: apps/wei/models.py:182 +msgid "Male" +msgstr "Homme" + +#: apps/wei/models.py:183 +msgid "Female" +msgstr "Femme" + +#: apps/wei/models.py:184 +msgid "Non binary" +msgstr "Non-binaire" + +#: apps/wei/models.py:186 templates/wei/weimembership_form.html:59 +msgid "gender" +msgstr "genre" + +#: apps/wei/models.py:192 templates/wei/weimembership_form.html:65 +msgid "health issues" +msgstr "problèmes de santé" + +#: apps/wei/models.py:197 templates/wei/weimembership_form.html:68 +msgid "emergency contact name" +msgstr "Nom du contact en cas d'urgence" + +#: apps/wei/models.py:202 templates/wei/weimembership_form.html:71 +msgid "emergency contact phone" +msgstr "Téléphone du contact en cas d'urgence" + +#: apps/wei/models.py:207 templates/wei/weimembership_form.html:74 +msgid "" +"Register on the mailing list to stay informed of the events of the campus (1 " +"mail/week)" +msgstr "" +"S'inscrire sur la liste de diffusion pour rester informé des événements sur " +"le campus (1 mail par semaine)" + +#: apps/wei/models.py:212 templates/wei/weimembership_form.html:77 +msgid "" +"Register on the mailing list to stay informed of the sport events of the " +"campus (1 mail/week)" +msgstr "" +"S'inscrire sur la liste de diffusion pour rester informé des actualités " +"sportives sur le campus (1 mail par semaine)" + +#: apps/wei/models.py:217 templates/wei/weimembership_form.html:80 +msgid "" +"Register on the mailing list to stay informed of the art events of the " +"campus (1 mail/week)" +msgstr "" +"S'inscrire sur la liste de diffusion pour rester informé des actualités " +"artistiques sur le campus (1 mail par semaine)" + +#: apps/wei/models.py:222 templates/wei/weimembership_form.html:56 +msgid "first year" +msgstr "première année" + +#: apps/wei/models.py:223 +msgid "Tells if the user is new in the school." +msgstr "Indique si l'utilisateur est nouveau dans l'école." + +#: apps/wei/models.py:228 +msgid "registration information" +msgstr "informations sur l'inscription" + +#: apps/wei/models.py:229 +msgid "" +"Information about the registration (buses for old members, survey fot the " +"new members), encoded in JSON" +msgstr "" +"Informations sur l'inscription (bus pour les 2A+, questionnaire pour les " +"1A), encodées en JSON" + +#: apps/wei/models.py:260 +msgid "WEI User" +msgstr "Participant au WEI" + +#: apps/wei/models.py:261 +msgid "WEI Users" +msgstr "Participants au WEI" + +#: apps/wei/models.py:281 +msgid "team" +msgstr "équipe" + +#: apps/wei/models.py:291 +msgid "WEI registration" +msgstr "inscription au WEI" + +#: apps/wei/models.py:295 +msgid "WEI membership" +msgstr "adhésion au WEI" + +#: apps/wei/models.py:296 +msgid "WEI memberships" +msgstr "adhésions au WEI" + +#: apps/wei/tables.py:53 apps/wei/tables.py:54 +#: templates/treasury/sogecredit_detail.html:57 +msgid "Validate" +msgstr "Valider" + +#: apps/wei/tables.py:96 +msgid "Year" +msgstr "Année" + +#: apps/wei/tables.py:134 templates/wei/bus_tables.html:26 +#: templates/wei/busteam_tables.html:43 +msgid "Teams" +msgstr "Équipes" + +#: apps/wei/tables.py:143 apps/wei/tables.py:184 +msgid "Members count" +msgstr "Nombre de membres" + +#: apps/wei/tables.py:150 apps/wei/tables.py:181 +msgid "members" +msgstr "adhérents" + +#: apps/wei/views.py:201 +msgid "Find WEI Membership" +msgstr "Trouver une adhésion au WEI" + +#: apps/wei/views.py:236 +msgid "Find WEI Registration" +msgstr "Trouver une inscription au WEI" + +#: apps/wei/views.py:445 templates/wei/weiclub_info.html:62 +msgid "Register 1A" +msgstr "Inscrire un 1A" + +#: apps/wei/views.py:466 apps/wei/views.py:535 +msgid "This user is already registered to this WEI." +msgstr "Cette personne est déjà inscrite au WEI." + +#: apps/wei/views.py:471 +msgid "" +"This user can't be in her/his first year since he/she has already participed " +"to a WEI." +msgstr "" +"Cet utilisateur ne peut pas être en première année puisqu'iel a déjà " +"participé à un WEI." + +#: apps/wei/views.py:499 templates/wei/weiclub_info.html:65 +msgid "Register 2A+" +msgstr "Inscrire un 2A+" + +#: apps/wei/views.py:517 apps/wei/views.py:604 +msgid "You already opened an account in the Société générale." +msgstr "Vous avez déjà ouvert un compte auprès de la société générale." + +#: apps/wei/views.py:664 +msgid "You don't have the right to delete this WEI registration." +msgstr "Vous n'avez pas la permission de supprimer cette inscription au WEI." + +#: apps/wei/views.py:763 +msgid "This user didn't give her/his caution check." +msgstr "Cet utilisateur n'a pas donné son chèque de caution." + +#: apps/wei/views.py:837 apps/wei/views.py:857 apps/wei/views.py:867 +#: templates/wei/survey.html:12 templates/wei/survey_closed.html:12 +#: templates/wei/survey_end.html:12 +msgid "Survey WEI" +msgstr "Questionnaire WEI" + +#: note_kfet/settings/base.py:154 +msgid "German" +msgstr "Allemand" #: note_kfet/settings/base.py:155 +msgid "English" +msgstr "Anglais" + +#: note_kfet/settings/base.py:156 msgid "French" +msgstr "Français" + +#: templates/400.html:6 +msgid "Bad request" +msgstr "Requête invalide" + +#: templates/400.html:7 +msgid "" +"Sorry, your request was bad. Don't know what could be wrong. An email has " +"been sent to webmasters with the details of the error. You can now drink a " +"coke." msgstr "" +"Désolé, votre requête est invalide. Aucune idée de ce qui a pu se produire. " +"Un e-mail a été envoyé aux responsables de la plateforme avec les détails de " +"cette erreur. Vous pouvez désormais allez boire un coca." + +#: templates/403.html:6 +msgid "Permission denied" +msgstr "Accès refusé" + +#: templates/403.html:7 +msgid "You don't have the right to perform this request." +msgstr "Vous n'avez pas la permission d'exécuter cette requête." + +#: templates/403.html:10 templates/404.html:10 +msgid "Exception message:" +msgstr "Message d'erreur :" + +#: templates/404.html:6 +msgid "Page not found" +msgstr "Page inexistante" + +#: templates/404.html:7 +#, python-format +msgid "" +"The requested path <code>%(request_path)s</code> was not found on the server." +msgstr "" +"The chemin demandé <code>%(request_path)s</code> n'a pas été trouvé sur le " +"serveur." + +#: templates/500.html:6 +msgid "Server error" +msgstr "Erreur du serveur" + +#: templates/500.html:7 +msgid "" +"Sorry, an error occurred when processing your request. An email has been " +"sent to webmasters with the detail of the error, and this will be fixed " +"soon. You can now drink a beer." +msgstr "" +"Désolé, une erreur est survenue lors de l'analyse de votre requête. Un email " +"a été envoyé aux responsables de la plateforme avec les détails de cette " +"erreur, qui sera corrigée rapidement. Vous pouvez désormais aller boire une " +"bière." #: templates/activity/activity_detail.html:29 msgid "creater" @@ -1057,27 +1558,33 @@ msgstr "Toutes les activités" msgid "The ENS Paris-Saclay BDE note." msgstr "La note du BDE de l'ENS Paris-Saclay." -#: templates/base.html:89 +#: templates/base.html:104 msgid "Users" msgstr "Utilisateurs" -#: templates/base.html:94 +#: templates/base.html:109 msgid "Clubs" msgstr "Clubs" -#: templates/base.html:100 +#: templates/base.html:115 msgid "Registrations" msgstr "Inscriptions" -#: templates/base.html:150 +#: templates/base.html:120 +msgid "Rights" +msgstr "Droits" + +#: templates/base.html:158 msgid "" "Your e-mail address is not validated. Please check your mail inbox and click " "on the validation link." msgstr "" +"Votre adresse e-mail n'est pas validée. Merci de vérifier votre boîte mail " +"et de cliquer sur le lien de validation." #: templates/cas_server/base.html:7 msgid "Central Authentication Service" -msgstr "" +msgstr "Service Central d'Authentification" #: templates/cas_server/base.html:43 #, python-format @@ -1086,51 +1593,9 @@ msgid "" "%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider " "upgrading." msgstr "" - -#: templates/cas_server/logged.html:4 -msgid "" -"<h3>Log In Successful</h3>You have successfully logged into the Central " -"Authentication Service.<br/>For security reasons, please Log Out and Exit " -"your web browser when you are done accessing services that require " -"authentication!" -msgstr "" - -#: templates/cas_server/logged.html:8 -msgid "Log me out from all my sessions" -msgstr "" - -#: templates/cas_server/logged.html:14 -msgid "Forget the identity provider" -msgstr "" - -#: templates/cas_server/logged.html:18 -msgid "Logout" -msgstr "" - -#: templates/cas_server/login.html:6 -msgid "Please log in" -msgstr "" - -#: templates/cas_server/login.html:11 -msgid "" -"If you don't have any Note Kfet account, please follow <a href='/accounts/" -"signup'>this link to sign up</a>." -msgstr "" -"Si vous n'avez pas de compte Note Kfet, veuillez suivre <a href='/accounts/" -"signup'>ce lien pour vous inscrire</a>." - -#: templates/cas_server/login.html:17 -msgid "Login" -msgstr "" - -#: templates/cas_server/warn.html:9 -msgid "Connect to the service" -msgstr "" - -#: templates/django_filters/rest_framework/crispy_form.html:4 -#: templates/django_filters/rest_framework/form.html:2 -msgid "Field filters" -msgstr "" +"Une nouvelle version de l'application est disponible. Cette instance utilise " +"la version %(VERSION)s et la dernière version est %(LAST_VERSION)s. Merci de " +"vous mettre à jour." #: templates/member/alias_update.html:5 msgid "Add alias" @@ -1140,19 +1605,25 @@ msgstr "Ajouter un alias" msgid "Club Parent" msgstr "Club parent" -#: templates/member/club_info.html:29 +#: templates/member/club_info.html:34 msgid "days" msgstr "jours" -#: templates/member/club_info.html:32 +#: templates/member/club_info.html:38 templates/wei/weiclub_info.html:27 msgid "membership fee" msgstr "cotisation pour adhérer" -#: templates/member/club_info.html:52 +#: templates/member/club_info.html:50 templates/member/profile_info.html:33 +#: templates/treasury/sogecredit_detail.html:18 +#: templates/wei/weiclub_info.html:43 +msgid "balance" +msgstr "solde du compte" + +#: templates/member/club_info.html:64 msgid "Add member" msgstr "Ajouter un membre" -#: templates/member/club_info.html:59 templates/member/profile_info.html:48 +#: templates/member/club_info.html:71 templates/member/profile_info.html:48 msgid "View Profile" msgstr "Voir le profil" @@ -1165,14 +1636,15 @@ msgid "Create club" msgstr "Créer un club" #: templates/member/club_list.html:19 -msgid "club listing " +msgid "Club listing" msgstr "Liste des clubs" -#: templates/member/club_tables.html:6 +#: templates/member/club_tables.html:7 msgid "Member of the Club" msgstr "Membre du club" -#: templates/member/club_tables.html:17 templates/member/profile_tables.html:28 +#: templates/member/club_tables.html:20 templates/member/profile_tables.html:28 +#: templates/wei/weiclub_tables.html:105 msgid "Transaction history" msgstr "Historique des transactions" @@ -1195,6 +1667,7 @@ msgstr "Compte n°" #: templates/member/profile_info.html:17 #: templates/registration/future_profile_detail.html:19 +#: templates/wei/weimembership_form.html:21 msgid "username" msgstr "pseudo" @@ -1208,21 +1681,19 @@ msgstr "mot de passe" msgid "Change password" msgstr "Changer le mot de passe" -#: 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_tables.html:7 #: templates/registration/future_profile_detail.html:28 +#: templates/wei/weimembership_form.html:30 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_tables.html:8 #: templates/registration/future_profile_detail.html:29 +#: templates/wei/weimembership_form.html:31 msgid "Click here to resend a validation link." msgstr "Cliquez ici pour renvoyer un lien de validation." @@ -1234,36 +1705,40 @@ msgstr "Voir mes adhésions" msgid "Save Changes" msgstr "Sauvegarder les changements" -#: 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/member/user_list.html:16 +msgid "There is no user with this pattern." +msgstr "Il n'y a pas d'utilisateur trouvé avec cette entrée." -#: templates/note/conso_form.html:28 templates/note/transaction_form.html:55 -msgid "Select emitters" -msgstr "Sélection des émetteurs" +#: templates/note/conso_form.html:28 +msgid "Consum" +msgstr "Consommer" -#: templates/note/conso_form.html:45 +#: templates/note/conso_form.html:39 templates/note/transaction_form.html:61 +#: templates/note/transaction_form.html:76 +msgid "Name or alias..." +msgstr "" + +#: templates/note/conso_form.html:48 msgid "Select consumptions" msgstr "Sélection des consommations" -#: templates/note/conso_form.html:51 +#: templates/note/conso_form.html:57 msgid "Consume!" msgstr "Consommer !" -#: templates/note/conso_form.html:64 +#: templates/note/conso_form.html:71 msgid "Most used buttons" msgstr "Boutons les plus utilisés" -#: templates/note/conso_form.html:126 +#: templates/note/conso_form.html:134 msgid "Single consumptions" msgstr "Consommations simples" -#: templates/note/conso_form.html:130 +#: templates/note/conso_form.html:139 msgid "Double consumptions" msgstr "Consommations doubles" -#: templates/note/conso_form.html:141 templates/note/transaction_form.html:152 +#: templates/note/conso_form.html:150 templates/note/transaction_form.html:151 msgid "Recent transactions history" msgstr "Historique des transactions récentes" @@ -1271,56 +1746,86 @@ msgstr "Historique des transactions récentes" msgid "Gift" msgstr "Don" -#: templates/note/transaction_form.html:73 -msgid "External payment" -msgstr "Paiement externe" - -#: templates/note/transaction_form.html:81 -msgid "Transfer type" -msgstr "Type de transfert" +#: templates/note/transaction_form.html:55 +msgid "Select emitters" +msgstr "Sélection des émetteurs" -#: templates/note/transaction_form.html:116 -#: templates/note/transaction_form.html:169 -#: templates/note/transaction_form.html:176 +#: templates/note/transaction_form.html:70 msgid "Select receivers" msgstr "Sélection des destinataires" -#: templates/note/transaction_form.html:138 +#: templates/note/transaction_form.html:87 +msgid "Action" +msgstr "Action" + +#: templates/note/transaction_form.html:102 msgid "Reason" msgstr "Raison" -#: templates/note/transaction_form.html:183 -msgid "Credit note" -msgstr "Note à recharger" - -#: templates/note/transaction_form.html:190 -msgid "Debit note" -msgstr "Note à débiter" +#: templates/note/transaction_form.html:110 +msgid "Transfer type" +msgstr "Type de transfert" -#: templates/note/transactiontemplate_form.html:6 +#: templates/note/transactiontemplate_form.html:10 msgid "Buttons list" msgstr "Liste des boutons" +#: templates/note/transactiontemplate_form.html:21 +msgid "Price history" +msgstr "Historique des prix" + +#: templates/note/transactiontemplate_form.html:24 +msgid "Obsolete since" +msgstr "Obsolète depuis" + +#: templates/note/transactiontemplate_form.html:24 +msgid "Current price" +msgstr "Prix actuel" + #: templates/note/transactiontemplate_list.html:9 -msgid "search button" +msgid "Search button" msgstr "Chercher un bouton" -#: templates/note/transactiontemplate_list.html:13 +#: templates/note/transactiontemplate_list.html:11 +msgid "Name of the button..." +msgstr "Nom du bouton ..." + +#: templates/note/transactiontemplate_list.html:16 +msgid "Display visible buttons only" +msgstr "N'afficher que les boutons visibles uniquement" + +#: templates/note/transactiontemplate_list.html:21 msgid "New button" msgstr "Nouveau bouton" -#: templates/note/transactiontemplate_list.html:20 +#: templates/note/transactiontemplate_list.html:28 msgid "buttons listing " msgstr "Liste des boutons" -#: templates/note/transactiontemplate_list.html:70 +#: templates/note/transactiontemplate_list.html:86 msgid "button successfully deleted " msgstr "Le bouton a bien été supprimé" -#: templates/note/transactiontemplate_list.html:74 +#: templates/note/transactiontemplate_list.html:90 msgid "Unable to delete button " msgstr "Impossible de supprimer le bouton " +#: templates/permission/all_rights.html:10 +msgid "Filter with roles that I have in at least one club" +msgstr "Filtrer les rôles que je possède dans au moins un club" + +#: templates/permission/all_rights.html:21 +msgid "Own this role in the clubs" +msgstr "Possède ce rôle dans les clubs" + +#: templates/permission/all_rights.html:26 +msgid "Query:" +msgstr "Requête :" + +#: templates/permission/all_rights.html:28 +msgid "No associated permission" +msgstr "Pas de permission associée" + #: templates/registration/email_validation_complete.html:6 msgid "Your email have successfully been validated." msgstr "Votre adresse e-mail a bien été validée." @@ -1334,17 +1839,19 @@ msgstr "Vous pouvez désormais <a href=\"%(login_url)s\">vous connecter</a>." 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." +"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." +"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 +#: templates/wei/weiregistration_confirm_delete.html:12 msgid "Delete registration" msgstr "Supprimer l'inscription" @@ -1353,6 +1860,8 @@ msgid "Validate account" msgstr "Valider le compte" #: templates/registration/future_profile_detail.html:71 +#: templates/wei/weimembership_form.html:132 +#: templates/wei/weimembership_form.html:190 msgid "Validate registration" msgstr "Valider l'inscription" @@ -1360,19 +1869,23 @@ msgstr "Valider l'inscription" msgid "New user" msgstr "Nouvel utilisateur" +#: 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/registration/logged_out.html:8 msgid "Thanks for spending some quality time with the Web site today." -msgstr "" +msgstr "Merci d'avoir utilisé la Note Kfet." #: templates/registration/logged_out.html:9 msgid "Log in again" -msgstr "" +msgstr "Se connecter à nouveau" #: templates/registration/login.html:7 templates/registration/login.html:8 -#: templates/registration/login.html:28 +#: templates/registration/login.html:21 #: templates/registration/password_reset_complete.html:10 msgid "Log in" -msgstr "" +msgstr "Se connecter" #: templates/registration/login.html:13 #, python-format @@ -1380,42 +1893,40 @@ msgid "" "You are authenticated as %(username)s, but are not authorized to access this " "page. Would you like to login to a different account?" msgstr "" +"Vous êtes connecté en tant que %(username)s, mais vous n'avez le droit " +"d'accéder à cette page. Voulez vous essayer avec un autre compte ?" #: templates/registration/login.html:22 -msgid "You can also register via the central authentification server " -msgstr "" - -#: templates/registration/login.html:23 -msgid "using this link " -msgstr "" - -#: templates/registration/login.html:29 msgid "Forgotten your password or username?" -msgstr "" +msgstr "Mot de passe ou pseudo oublié ?" #: templates/registration/mails/email_validation_email.html:3 msgid "Hi" -msgstr "" +msgstr "Bonjour" #: 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 "" +"Vous vous êtes inscrits récemment sur la Note Kfet. Merci de cliquer sur le " +"lien ci-dessous pour confirmer votre adresse email." #: 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 "" +"Ce lien n'est valide que pendant quelques jours. Après cela, vous devrez " +"nous contacter pour valider votre email." #: 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." +"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" @@ -1438,51 +1949,76 @@ msgstr "" #: templates/registration/password_change_form.html:11 #: templates/registration/password_reset_confirm.html:12 msgid "Change my password" -msgstr "" +msgstr "Changer mon mot de passe" #: templates/registration/password_reset_complete.html:8 msgid "Your password has been set. You may go ahead and log in now." msgstr "" +"Votre mot de passe a été enregistré. Vous pouvez vous connecter dès à " +"présent." #: templates/registration/password_reset_confirm.html:9 msgid "" "Please enter your new password twice so we can verify you typed it in " "correctly." msgstr "" +"Entrer votre nouveau mot de passe, et le confirmer en le renseignant une " +"seconde fois." #: templates/registration/password_reset_confirm.html:15 msgid "" "The password reset link was invalid, possibly because it has already been " "used. Please request a new password reset." msgstr "" +"Le lien de reinitialisation du mot de passe est invalide, il a peut-être été " +"déjà utilisé. Faites une nouvelle demande." #: templates/registration/password_reset_done.html:8 msgid "" "We've emailed you instructions for setting your password, if an account " "exists with the email you entered. You should receive them shortly." msgstr "" +"Nous vous avons envoyé par mail les instructions pour changer votre mot de " +"passe." #: templates/registration/password_reset_done.html:9 msgid "" "If you don't receive an email, please make sure you've entered the address " "you registered with, and check your spam folder." msgstr "" +"Si vous ne recevez pas d'email, vérifiez que vous avez bien utilisé " +"l'adresse associé à votre compte, et regarder également le dossier spam." #: templates/registration/password_reset_form.html:8 msgid "" "Forgotten your password? Enter your email address below, and we'll email " "instructions for setting a new one." msgstr "" +"Mot de passe oublié ? Entrez votre adresse mail ci-dessous, et vous recevrez " +"les instructions pour choisir un nouveau mot de passe." #: templates/registration/password_reset_form.html:11 msgid "Reset my password" -msgstr "" +msgstr "Réinitialiser mon mot de passe" #: templates/registration/signup.html:5 templates/registration/signup.html:8 -#: templates/registration/signup.html:14 +#: templates/registration/signup.html:19 msgid "Sign up" msgstr "Inscription" +#: templates/registration/signup.html:11 +msgid "" +"If you already signed up, your registration is taken into account. The BDE " +"must validate your account before your can log in. You have to go to the " +"Kfet and pay the registration fee. You must also validate your email address " +"by following the link you received." +msgstr "" +"Si vous vous êtes déjà inscrits, votre inscription a bien été prise en " +"compte. Le BDE doit d'abord valider votre compte avantque vous puissiez vous " +"connecter. Vous devez vous rendre à la Kfet et payer les frais d'adhésion. " +"Vous devez également valider votre adresse email en suivant le lien que vous " +"avez reçu." + #: templates/treasury/invoice_form.html:6 msgid "Invoices list" msgstr "Liste des factures" @@ -1495,7 +2031,13 @@ msgstr "Ajouter produit" msgid "Remove product" msgstr "Retirer produit" -#: templates/treasury/invoice_list.html:21 +#: templates/treasury/invoice_list.html:16 +#: templates/treasury/remittance_list.html:16 +#: templates/treasury/sogecredit_list.html:16 +msgid "Société générale credits" +msgstr "Crédits de la Société générale" + +#: templates/treasury/invoice_list.html:24 msgid "New invoice" msgstr "Nouvelle facture" @@ -1520,37 +2062,336 @@ msgstr "Transactions liées" msgid "There is no transaction linked with this remittance." msgstr "Il n'y a pas de transaction liée à cette remise." -#: templates/treasury/remittance_list.html:19 +#: templates/treasury/remittance_list.html:22 msgid "Opened remittances" msgstr "Remises ouvertes" -#: templates/treasury/remittance_list.html:24 +#: templates/treasury/remittance_list.html:27 msgid "There is no opened remittance." msgstr "Il n'y a pas de remise ouverte." -#: templates/treasury/remittance_list.html:28 +#: templates/treasury/remittance_list.html:31 msgid "New remittance" msgstr "Nouvelle remise" -#: templates/treasury/remittance_list.html:32 +#: templates/treasury/remittance_list.html:35 msgid "Transfers without remittances" msgstr "Transactions sans remise associée" -#: templates/treasury/remittance_list.html:37 +#: templates/treasury/remittance_list.html:40 msgid "There is no transaction without any linked remittance." msgstr "Il n'y a pas de transactions sans remise associée." -#: templates/treasury/remittance_list.html:43 +#: templates/treasury/remittance_list.html:46 msgid "Transfers with opened remittances" msgstr "Transactions associées à une remise ouverte" -#: templates/treasury/remittance_list.html:48 +#: templates/treasury/remittance_list.html:51 msgid "There is no transaction with an opened linked remittance." msgstr "Il n'y a pas de transaction associée à une remise ouverte." -#: templates/treasury/remittance_list.html:54 +#: templates/treasury/remittance_list.html:57 msgid "Closed remittances" msgstr "Remises fermées" -#~ msgid "This membership is already renewed" -#~ msgstr "Cette adhésion est déjà renouvelée" +#: templates/treasury/sogecredit_detail.html:29 +msgid "total amount" +msgstr "montant total" + +#: templates/treasury/sogecredit_detail.html:35 +msgid "" +"Warning: Validating this credit implies that all membership transactions " +"will be validated." +msgstr "" +"Attention : Valider ce crédit implique que les transactions d'adhésion " +"seront validées." + +#: templates/treasury/sogecredit_detail.html:36 +msgid "" +"If you delete this credit, there all membership transactions will be also " +"validated, but no credit will be operated." +msgstr "" +"Si vous supprimez cette demande de crédit, alors toutes les transactions " +"d'adhésion seront aussi validées, but il n'y aura pas de transaction de " +"crédit créée." + +#: templates/treasury/sogecredit_detail.html:37 +msgid "" +"If this credit is validated, then the user won't be able to ask for a credit " +"from the Société générale." +msgstr "" +"Si ce crédit est validé, alors l'utilisateur ne pourra plus demander d'être " +"crédité par la Société générale à l'avenir." + +#: templates/treasury/sogecredit_detail.html:38 +msgid "If you think there is an error, please contact the \"respos info\"." +msgstr "Si vous pensez qu'il y a une erreur, merci de contacter un respo info." + +#: templates/treasury/sogecredit_detail.html:44 +msgid "This credit is already validated." +msgstr "Ce crédit a déjà été validé." + +#: templates/treasury/sogecredit_detail.html:49 +msgid "" +"Warning: if you don't validate this credit, the note of the user doesn't " +"have enough money to pay its memberships." +msgstr "" +"Attention : si vous ne validez pas ce crédit, la note de l'utilisateur n'a " +"pas assez d'argent pour payer les adhésions." + +#: templates/treasury/sogecredit_detail.html:50 +msgid "Please ask the user to credit its note before deleting this credit." +msgstr "" +"Merci de demander à l'utilisateur de recharger sa note avant de supprimer la " +"demande de crédit." + +#: templates/treasury/sogecredit_detail.html:64 +msgid "Return to credit list" +msgstr "Retour à la liste des crédits" + +#: templates/treasury/sogecredit_list.html:26 +msgid "Filter with unvalidated credits only" +msgstr "Filtrer avec uniquement les crédits non valides" + +#: templates/treasury/sogecredit_list.html:36 +msgid "There is no matched user that have asked for a Société générale credit." +msgstr "" +"Il n'y a pas d'utilisateur trouvé ayant demandé un crédit de la Société " +"générale." + +#: templates/wei/bus_tables.html:16 templates/wei/busteam_tables.html:16 +msgid "Add team" +msgstr "Ajouter une équipe" + +#: templates/wei/bus_tables.html:39 +msgid "Members" +msgstr "Membres" + +#: templates/wei/bus_tables.html:48 templates/wei/busteam_tables.html:52 +#: templates/wei/weimembership_list.html:30 +msgid "View as PDF" +msgstr "Télécharger au format PDF" + +#: templates/wei/survey.html:24 +msgid "Next" +msgstr "Suivant" + +#: templates/wei/survey_closed.html:16 +msgid "The inscription for this WEI are now closed." +msgstr "Les inscriptions pour le WEI sont fermées." + +#: templates/wei/survey_closed.html:20 +msgid "Return to WEI detail" +msgstr "Retour aux détails du WEI" + +#: templates/wei/survey_end.html:16 +msgid "The survey is now ended. Your answers have been saved." +msgstr "" +"Le sondage est désormais terminé, vos réponses ont bien été enregistrées." + +#: templates/wei/weiclub_info.html:31 +msgid "WEI fee / including BDE and Kfet fee (paid students)" +msgstr "Prix du WEI / incluant l'adhésion BDE/Kfet (élèves)" + +#: templates/wei/weiclub_info.html:36 +msgid "WEI fee / including BDE and Kfet fee (unpaid students)" +msgstr "Prix du WEI / incluant l'adhésion BDE/Kfet (étudiants)" + +#: templates/wei/weiclub_info.html:58 +msgid "WEI list" +msgstr "Liste des WEI" + +#: templates/wei/weiclub_info.html:71 +msgid "Add bus" +msgstr "Ajouter un bus" + +#: templates/wei/weiclub_info.html:75 +msgid "View WEI" +msgstr "Voir le WEI" + +#: templates/wei/weiclub_list.html:8 +msgid "search WEI" +msgstr "Chercher un WEI" + +#: templates/wei/weiclub_list.html:12 +msgid "Create WEI" +msgstr "Créer un WEI" + +#: templates/wei/weiclub_list.html:19 +msgid "WEI listing" +msgstr "Liste des WEI" + +#: templates/wei/weiclub_tables.html:63 +msgid "Register to the WEI! – 1A" +msgstr "M'inscrire au WEI ! – 1A" + +#: templates/wei/weiclub_tables.html:65 +msgid "Register to the WEI! – 2A+" +msgstr "M'inscrire au WEI ! – 2A+" + +#: templates/wei/weiclub_tables.html:67 +msgid "Update my registration" +msgstr "Mettre à jour mon inscription" + +#: templates/wei/weiclub_tables.html:92 +msgid "Members of the WEI" +msgstr "Membres du WEI" + +#: templates/wei/weiclub_tables.html:120 +msgid "Unvalidated registrations" +msgstr "Inscriptions non validées" + +#: templates/wei/weimembership_form.html:14 +msgid "Review registration" +msgstr "Vérifier l'inscription" + +#: templates/wei/weimembership_form.html:39 +msgid "ENS year" +msgstr "Année à l'ENS" + +#: templates/wei/weimembership_form.html:83 +msgid "Payment from Société générale" +msgstr "Paiement de la Société générale" + +#: templates/wei/weimembership_form.html:87 +msgid "Suggested bus from the survey:" +msgstr "Bus suggéré par le sondage :" + +#: templates/wei/weimembership_form.html:92 +msgid "Raw survey information" +msgstr "Informations brutes du sondage" + +#: templates/wei/weimembership_form.html:102 +msgid "The algorithm didn't run." +msgstr "L'algorithme n'a pas été exécuté." + +#: templates/wei/weimembership_form.html:105 +msgid "caution check given" +msgstr "chèque de caution donné" + +#: templates/wei/weimembership_form.html:109 +msgid "preferred bus" +msgstr "bus préféré" + +#: templates/wei/weimembership_form.html:112 +msgid "preferred team" +msgstr "équipe préférée" + +#: templates/wei/weimembership_form.html:115 +msgid "preferred roles" +msgstr "rôles préférés" + +#: templates/wei/weimembership_form.html:123 +#: templates/wei/weiregistration_confirm_delete.html:31 +msgid "Update registration" +msgstr "Mettre à jour l'inscription" + +#: templates/wei/weimembership_form.html:136 +msgid "The registration is already validated and can't be unvalidated." +msgstr "L'inscription a déjà été validée et ne peut pas être dévalidée." + +#: templates/wei/weimembership_form.html:137 +msgid "The user joined the bus" +msgstr "L'utilisateur a rejoint le bus" + +#: templates/wei/weimembership_form.html:138 +msgid "in the team" +msgstr "dans l'équipe" + +#: templates/wei/weimembership_form.html:139 +msgid "in no team (staff)" +msgstr "dans aucune équipe (staff)" + +#: templates/wei/weimembership_form.html:139 +msgid "with the following roles:" +msgstr "avec les rôles suivants :" + +#: templates/wei/weimembership_form.html:144 +msgid "" +"\n" +" The WEI will be paid by Société générale. The " +"membership will be created even if the bank didn't pay the BDE yet.\n" +" The membership transaction will be created but " +"will be invalid. You will have to validate it once the bank\n" +" validated the creation of the account, or to " +"change the payment method.\n" +" " +msgstr "" +"\n" +"Le WEI va être payé par la Société générale. L'adhésion sera créée même si " +"la banque n'a pas encore payé le BDE.\n" +"La transaction d'adhésion sera créée mais invalide. Vous devrez la valider " +"une fois que la banque\n" +"aura validé la création du compte, ou bien changer de moyen de paiement.\n" +" " + +#: templates/wei/weimembership_form.html:154 +#, python-format +msgid "" +"\n" +" The note don't have enough money " +"(%(balance)s, %(pretty_fee)s required). The registration may fail.\n" +" " +msgstr "" +"\n" +"La note n'a pas assez d'argent (%(balance)s, %(pretty_fee)s requis). " +"L'inscription va échouer.\n" +" " + +#: templates/wei/weimembership_form.html:161 +msgid "The note has enough money, the registration is possible." +msgstr "La note a assez d'argent, l'inscription est possible." + +#: templates/wei/weimembership_form.html:168 +msgid "The user didn't give her/his caution check." +msgstr "L'utilisateur n'a pas donné son chèque de caution." + +#: templates/wei/weimembership_form.html:176 +#, python-format +msgid "" +"\n" +" This user is not a member of the Kfet club. " +"Please adhere\n" +" <a href=\"%(future_user_detail)s\">here if he/" +"she is in her/his first year</a>\n" +" or <a href=\"%(club_detail)s\">here if he/she " +"was an old member</a> before you validate\n" +" the registration of the WEI.\n" +" " +msgstr "" +"\n" +"Cet utilisateur n'est pas membre du club Kfet. Merci de le faire adhérer\n" +"<a href=\"%(future_user_detail)s\">ici s'iel est en première année</a>\n" +"ou <a href=\"%(club_detail)s\">ici s'iel est un ancien membre</a> avant de " +"valider\n" +"l'inscription au WEI.\n" +" " + +#: templates/wei/weimembership_list.html:18 +msgid "There is no membership found with this pattern." +msgstr "Il n'y a pas d'adhésion trouvée avec cette entrée." + +#: templates/wei/weimembership_list.html:24 +msgid "View unvalidated registrations..." +msgstr "Voir les inscriptions non validées ..." + +#: templates/wei/weiregistration_confirm_delete.html:17 +msgid "This registration is already validated and can't be deleted." +msgstr "L'inscription a déjà été validée et ne peut pas être supprimée." + +#: templates/wei/weiregistration_confirm_delete.html:24 +#, python-format +msgid "" +"Are you sure you want to delete the registration of %(user)s for the WEI " +"%(wei_name)s? This action can't be undone." +msgstr "" +"Êtes-vous sûr de vouloir supprimer l'inscription de %(user)s pour le WEI " +"%(wei_name)s ? Cette action ne pourra pas être annulée." + +#: templates/wei/weiregistration_list.html:18 +msgid "There is no pre-registration found with this pattern." +msgstr "Il n'y a pas de pré-inscription en attente avec cette entrée." + +#: templates/wei/weiregistration_list.html:24 +msgid "View validated memberships..." +msgstr "Voir les adhésions validées ..." diff --git a/media/pic/default.png b/media/pic/default.png index f933bc341619178775520150d514effb2339b10a..41a31a1cf5eddf31e98a7e6cd06b4c45f2faee92 100644 Binary files a/media/pic/default.png and b/media/pic/default.png differ diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py index b3cccbcec9705294907034064aa17d5a047d5955..1a17d5ac702ec381e9ed2c01e05bbcf4c71a5fdc 100644 --- a/note_kfet/inputs.py +++ b/note_kfet/inputs.py @@ -3,7 +3,7 @@ from json import dumps as json_dumps -from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput +from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput, Widget class AmountInput(NumberInput): @@ -41,6 +41,29 @@ class Autocomplete(TextInput): return "" +class ColorWidget(Widget): + """ + Pulled from django-colorfield. + Select a color. + """ + template_name = 'colorfield/color.html' + + class Media: + js = [ + 'colorfield/jscolor/jscolor.min.js', + 'colorfield/colorfield.js', + ] + + def format_value(self, value): + if value is None: + value = 0xFFFFFF + return "#{:06X}".format(value) + + def value_from_datadict(self, data, files, name): + val = super().value_from_datadict(data, files, name) + return int(val[1:], 16) + + """ The remaining of this file comes from the project `django-bootstrap-datepicker-plus` available on Github: https://github.com/monim67/django-bootstrap-datepicker-plus diff --git a/note_kfet/settings/__init__.py b/note_kfet/settings/__init__.py index 1ab06b9c108f4194cc7e8f59e5bbe5ccf4ed93d7..73aae469e32078e942e49d4c01f4f21be3913698 100644 --- a/note_kfet/settings/__init__.py +++ b/note_kfet/settings/__init__.py @@ -39,41 +39,20 @@ else: from .development import * try: - #in secrets.py defines everything you want + # in secrets.py defines everything you want from .secrets import * + INSTALLED_APPS += OPTIONAL_APPS except ImportError: pass -if "cas" in INSTALLED_APPS: - MIDDLEWARE += ['cas.middleware.CASMiddleware'] +if "cas_server" in INSTALLED_APPS: # CAS Settings - CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/" CAS_AUTO_CREATE_USER = False CAS_LOGO_URL = "/static/img/Saperlistpopette.png" CAS_FAVICON_URL = "/static/favicon/favicon-32x32.png" - CAS_SHOW_SERVICE_MESSAGES = True CAS_SHOW_POWERED = False - CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False - CAS_PROVIDE_URL_TO_LOGOUT = True - CAS_INFO_MESSAGES = { - "cas_explained": { - "message": _( - u"The Central Authentication Service grants you access to most of our websites by " - u"authenticating only once, so you don't need to type your credentials again unless " - u"your session expires or you logout." - ), - "discardable": True, - "type": "info", # one of info, success, info, warning, danger - }, - } - - CAS_INFO_MESSAGES_ORDER = [ - 'cas_explained', - ] - AUTHENTICATION_BACKENDS += ('cas.backends.CASBackend',) - if "logs" in INSTALLED_APPS: MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',) diff --git a/note_kfet/settings/base.py b/note_kfet/settings/base.py index 283f8e5606dec26e89b738314b4c4d0f881005b8..5ed8b1d8a9357c73c784a03ac4c672af73152e43 100644 --- a/note_kfet/settings/base.py +++ b/note_kfet/settings/base.py @@ -62,6 +62,7 @@ INSTALLED_APPS = [ 'permission', 'registration', 'treasury', + 'wei', ] LOGIN_REDIRECT_URL = '/note/transfer/' diff --git a/note_kfet/settings/development.py b/note_kfet/settings/development.py index 66ad4fd44ed80b718cb1b430d9ad97b6692b6ba5..0a0c21e1b4c4b12008593aa1ac75616c3517c940 100644 --- a/note_kfet/settings/development.py +++ b/note_kfet/settings/development.py @@ -62,10 +62,6 @@ CSRF_COOKIE_HTTPONLY = False X_FRAME_OPTIONS = 'DENY' SESSION_COOKIE_AGE = 60 * 60 * 3 -# CAS Client settings -# Can be modified in secrets.py -CAS_SERVER_URL = "http://localhost:8000/cas/" - STATIC_ROOT = '' # not needed in development settings STATICFILES_DIRS = [ os.path.join(BASE_DIR, 'static')] diff --git a/note_kfet/settings/production.py b/note_kfet/settings/production.py index 5be8a3b899c5093272b2db12c4aaab44060c233c..7b8d37ef5d6b4f6fdebf5d768fea49964549a494 100644 --- a/note_kfet/settings/production.py +++ b/note_kfet/settings/production.py @@ -51,6 +51,3 @@ CSRF_COOKIE_SECURE = False CSRF_COOKIE_HTTPONLY = False X_FRAME_OPTIONS = 'DENY' SESSION_COOKIE_AGE = 60 * 60 * 3 - -# CAS Client settings -CAS_SERVER_URL = "https://" + os.getenv("NOTE_URL", "note.example.com") + "/cas/" diff --git a/note_kfet/urls.py b/note_kfet/urls.py index 90d44a07e5dbae32e70629291a43fedc1519ba4f..9717087abbbf4c8e8951c469fa935d9589b68084 100644 --- a/note_kfet/urls.py +++ b/note_kfet/urls.py @@ -5,6 +5,7 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include +from django.views.defaults import bad_request, permission_denied, page_not_found, server_error from django.views.generic import RedirectView from member.views import CustomLoginView @@ -19,14 +20,16 @@ urlpatterns = [ path('registration/', include('registration.urls')), path('activity/', include('activity.urls')), path('treasury/', include('treasury.urls')), + path('wei/', include('wei.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('admin/', admin.site.urls, name="admin"), path('accounts/login/', CustomLoginView.as_view()), path('accounts/', include('django.contrib.auth.urls')), path('api/', include('api.urls')), + path('permission/', include('permission.urls')), ] urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) @@ -44,3 +47,11 @@ if "debug_toolbar" in settings.INSTALLED_APPS: urlpatterns = [ path('__debug__/', include(debug_toolbar.urls)), ] + urlpatterns + + +handler400 = bad_request +handler403 = permission_denied + +# Only displayed in production, when debug mode is set to False +handler404 = page_not_found +handler500 = server_error diff --git a/requirements/cas.txt b/requirements/cas.txt index d468d2d5077580bf004e625a136f31a1734dcef0..c7c696fbffa45cfeaf0c4728b9df240cb4d673ed 100644 --- a/requirements/cas.txt +++ b/requirements/cas.txt @@ -1,2 +1 @@ -django-cas-client==1.5.3 django-cas-server==1.1.0 diff --git a/static/colorfield/colorfield.js b/static/colorfield/colorfield.js new file mode 100644 index 0000000000000000000000000000000000000000..a3c2de625b1003d717ca511840f13c929f90d198 --- /dev/null +++ b/static/colorfield/colorfield.js @@ -0,0 +1,12 @@ +/** global: django */ + +window.onload = function() { + if (typeof(django) !== 'undefined' && typeof(django.jQuery) !== 'undefined') { + (function($) { + // add colopicker to inlines added dynamically + $(document).on('formset:added', function onFormsetAdded(event, row) { + jscolor.installByClassName('jscolor'); + }); + }(django.jQuery)); + } +}; \ No newline at end of file diff --git a/static/colorfield/jscolor/jscolor.js b/static/colorfield/jscolor/jscolor.js new file mode 100755 index 0000000000000000000000000000000000000000..dbbd2342bf8e40134338940e70e087849ea06ca9 --- /dev/null +++ b/static/colorfield/jscolor/jscolor.js @@ -0,0 +1,1855 @@ +/** + * jscolor - JavaScript Color Picker + * + * @link http://jscolor.com + * @license For open source use: GPLv3 + * For commercial use: JSColor Commercial License + * @author Jan Odvarko + * @version 2.0.5 + * + * See usage examples at http://jscolor.com/examples/ + */ + + +"use strict"; + + +if (!window.jscolor) { window.jscolor = (function () { + + +var jsc = { + + + register : function () { + jsc.attachDOMReadyEvent(jsc.init); + jsc.attachEvent(document, 'mousedown', jsc.onDocumentMouseDown); + jsc.attachEvent(document, 'touchstart', jsc.onDocumentTouchStart); + jsc.attachEvent(window, 'resize', jsc.onWindowResize); + }, + + + init : function () { + if (jsc.jscolor.lookupClass) { + jsc.jscolor.installByClassName(jsc.jscolor.lookupClass); + } + }, + + + tryInstallOnElements : function (elms, className) { + var matchClass = new RegExp('(^|\\s)(' + className + ')(\\s*(\\{[^}]*\\})|\\s|$)', 'i'); + + for (var i = 0; i < elms.length; i += 1) { + if (elms[i].type !== undefined && elms[i].type.toLowerCase() == 'color') { + if (jsc.isColorAttrSupported) { + // skip inputs of type 'color' if supported by the browser + continue; + } + } + var m; + if (!elms[i].jscolor && elms[i].className && (m = elms[i].className.match(matchClass))) { + var targetElm = elms[i]; + var optsStr = null; + + var dataOptions = jsc.getDataAttr(targetElm, 'jscolor'); + if (dataOptions !== null) { + optsStr = dataOptions; + } else if (m[4]) { + optsStr = m[4]; + } + + var opts = {}; + if (optsStr) { + try { + opts = (new Function ('return (' + optsStr + ')'))(); + } catch(eParseError) { + jsc.warn('Error parsing jscolor options: ' + eParseError + ':\n' + optsStr); + } + } + targetElm.jscolor = new jsc.jscolor(targetElm, opts); + } + } + }, + + + isColorAttrSupported : (function () { + var elm = document.createElement('input'); + if (elm.setAttribute) { + elm.setAttribute('type', 'color'); + if (elm.type.toLowerCase() == 'color') { + return true; + } + } + return false; + })(), + + + isCanvasSupported : (function () { + var elm = document.createElement('canvas'); + return !!(elm.getContext && elm.getContext('2d')); + })(), + + + fetchElement : function (mixed) { + return typeof mixed === 'string' ? document.getElementById(mixed) : mixed; + }, + + + isElementType : function (elm, type) { + return elm.nodeName.toLowerCase() === type.toLowerCase(); + }, + + + getDataAttr : function (el, name) { + var attrName = 'data-' + name; + var attrValue = el.getAttribute(attrName); + if (attrValue !== null) { + return attrValue; + } + return null; + }, + + + attachEvent : function (el, evnt, func) { + if (el.addEventListener) { + el.addEventListener(evnt, func, false); + } else if (el.attachEvent) { + el.attachEvent('on' + evnt, func); + } + }, + + + detachEvent : function (el, evnt, func) { + if (el.removeEventListener) { + el.removeEventListener(evnt, func, false); + } else if (el.detachEvent) { + el.detachEvent('on' + evnt, func); + } + }, + + + _attachedGroupEvents : {}, + + + attachGroupEvent : function (groupName, el, evnt, func) { + if (!jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + jsc._attachedGroupEvents[groupName] = []; + } + jsc._attachedGroupEvents[groupName].push([el, evnt, func]); + jsc.attachEvent(el, evnt, func); + }, + + + detachGroupEvents : function (groupName) { + if (jsc._attachedGroupEvents.hasOwnProperty(groupName)) { + for (var i = 0; i < jsc._attachedGroupEvents[groupName].length; i += 1) { + var evt = jsc._attachedGroupEvents[groupName][i]; + jsc.detachEvent(evt[0], evt[1], evt[2]); + } + delete jsc._attachedGroupEvents[groupName]; + } + }, + + + attachDOMReadyEvent : function (func) { + var fired = false; + var fireOnce = function () { + if (!fired) { + fired = true; + func(); + } + }; + + if (document.readyState === 'complete') { + setTimeout(fireOnce, 1); // async + return; + } + + if (document.addEventListener) { + document.addEventListener('DOMContentLoaded', fireOnce, false); + + // Fallback + window.addEventListener('load', fireOnce, false); + + } else if (document.attachEvent) { + // IE + document.attachEvent('onreadystatechange', function () { + if (document.readyState === 'complete') { + document.detachEvent('onreadystatechange', arguments.callee); + fireOnce(); + } + }) + + // Fallback + window.attachEvent('onload', fireOnce); + + // IE7/8 + if (document.documentElement.doScroll && window == window.top) { + var tryScroll = function () { + if (!document.body) { return; } + try { + document.documentElement.doScroll('left'); + fireOnce(); + } catch (e) { + setTimeout(tryScroll, 1); + } + }; + tryScroll(); + } + } + }, + + + warn : function (msg) { + if (window.console && window.console.warn) { + window.console.warn(msg); + } + }, + + + preventDefault : function (e) { + if (e.preventDefault) { e.preventDefault(); } + e.returnValue = false; + }, + + + captureTarget : function (target) { + // IE + if (target.setCapture) { + jsc._capturedTarget = target; + jsc._capturedTarget.setCapture(); + } + }, + + + releaseTarget : function () { + // IE + if (jsc._capturedTarget) { + jsc._capturedTarget.releaseCapture(); + jsc._capturedTarget = null; + } + }, + + + fireEvent : function (el, evnt) { + if (!el) { + return; + } + if (document.createEvent) { + var ev = document.createEvent('HTMLEvents'); + ev.initEvent(evnt, true, true); + el.dispatchEvent(ev); + } else if (document.createEventObject) { + var ev = document.createEventObject(); + el.fireEvent('on' + evnt, ev); + } else if (el['on' + evnt]) { // alternatively use the traditional event model + el['on' + evnt](); + } + }, + + + classNameToList : function (className) { + return className.replace(/^\s+|\s+$/g, '').split(/\s+/); + }, + + + // The className parameter (str) can only contain a single class name + hasClass : function (elm, className) { + if (!className) { + return false; + } + return -1 != (' ' + elm.className.replace(/\s+/g, ' ') + ' ').indexOf(' ' + className + ' '); + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + setClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + if (!jsc.hasClass(elm, classList[i])) { + elm.className += (elm.className ? ' ' : '') + classList[i]; + } + } + }, + + + // The className parameter (str) can contain multiple class names separated by whitespace + unsetClass : function (elm, className) { + var classList = jsc.classNameToList(className); + for (var i = 0; i < classList.length; i += 1) { + var repl = new RegExp( + '^\\s*' + classList[i] + '\\s*|' + + '\\s*' + classList[i] + '\\s*$|' + + '\\s+' + classList[i] + '(\\s+)', + 'g' + ); + elm.className = elm.className.replace(repl, '$1'); + } + }, + + + getStyle : function (elm) { + return window.getComputedStyle ? window.getComputedStyle(elm) : elm.currentStyle; + }, + + + setStyle : (function () { + var helper = document.createElement('div'); + var getSupportedProp = function (names) { + for (var i = 0; i < names.length; i += 1) { + if (names[i] in helper.style) { + return names[i]; + } + } + }; + var props = { + borderRadius: getSupportedProp(['borderRadius', 'MozBorderRadius', 'webkitBorderRadius']), + boxShadow: getSupportedProp(['boxShadow', 'MozBoxShadow', 'webkitBoxShadow']) + }; + return function (elm, prop, value) { + switch (prop.toLowerCase()) { + case 'opacity': + var alphaOpacity = Math.round(parseFloat(value) * 100); + elm.style.opacity = value; + elm.style.filter = 'alpha(opacity=' + alphaOpacity + ')'; + break; + default: + elm.style[props[prop]] = value; + break; + } + }; + })(), + + + setBorderRadius : function (elm, value) { + jsc.setStyle(elm, 'borderRadius', value || '0'); + }, + + + setBoxShadow : function (elm, value) { + jsc.setStyle(elm, 'boxShadow', value || 'none'); + }, + + + getElementPos : function (e, relativeToViewport) { + var x=0, y=0; + var rect = e.getBoundingClientRect(); + x = rect.left; + y = rect.top; + if (!relativeToViewport) { + var viewPos = jsc.getViewPos(); + x += viewPos[0]; + y += viewPos[1]; + } + return [x, y]; + }, + + + getElementSize : function (e) { + return [e.offsetWidth, e.offsetHeight]; + }, + + + // get pointer's X/Y coordinates relative to viewport + getAbsPointerPos : function (e) { + if (!e) { e = window.event; } + var x = 0, y = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + x = e.changedTouches[0].clientX; + y = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + x = e.clientX; + y = e.clientY; + } + return { x: x, y: y }; + }, + + + // get pointer's X/Y coordinates relative to target element + getRelPointerPos : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + var targetRect = target.getBoundingClientRect(); + + var x = 0, y = 0; + + var clientX = 0, clientY = 0; + if (typeof e.changedTouches !== 'undefined' && e.changedTouches.length) { + // touch devices + clientX = e.changedTouches[0].clientX; + clientY = e.changedTouches[0].clientY; + } else if (typeof e.clientX === 'number') { + clientX = e.clientX; + clientY = e.clientY; + } + + x = clientX - targetRect.left; + y = clientY - targetRect.top; + return { x: x, y: y }; + }, + + + getViewPos : function () { + var doc = document.documentElement; + return [ + (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0), + (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0) + ]; + }, + + + getViewSize : function () { + var doc = document.documentElement; + return [ + (window.innerWidth || doc.clientWidth), + (window.innerHeight || doc.clientHeight), + ]; + }, + + + redrawPosition : function () { + + if (jsc.picker && jsc.picker.owner) { + var thisObj = jsc.picker.owner; + + var tp, vp; + + if (thisObj.fixed) { + // Fixed elements are positioned relative to viewport, + // therefore we can ignore the scroll offset + tp = jsc.getElementPos(thisObj.targetElement, true); // target pos + vp = [0, 0]; // view pos + } else { + tp = jsc.getElementPos(thisObj.targetElement); // target pos + vp = jsc.getViewPos(); // view pos + } + + var ts = jsc.getElementSize(thisObj.targetElement); // target size + var vs = jsc.getViewSize(); // view size + var ps = jsc.getPickerOuterDims(thisObj); // picker size + var a, b, c; + switch (thisObj.position.toLowerCase()) { + case 'left': a=1; b=0; c=-1; break; + case 'right':a=1; b=0; c=1; break; + case 'top': a=0; b=1; c=-1; break; + default: a=0; b=1; c=1; break; + } + var l = (ts[b]+ps[b])/2; + + // compute picker position + if (!thisObj.smartPosition) { + var pp = [ + tp[a], + tp[b]+ts[b]-l+l*c + ]; + } else { + var pp = [ + -vp[a]+tp[a]+ps[a] > vs[a] ? + (-vp[a]+tp[a]+ts[a]/2 > vs[a]/2 && tp[a]+ts[a]-ps[a] >= 0 ? tp[a]+ts[a]-ps[a] : tp[a]) : + tp[a], + -vp[b]+tp[b]+ts[b]+ps[b]-l+l*c > vs[b] ? + (-vp[b]+tp[b]+ts[b]/2 > vs[b]/2 && tp[b]+ts[b]-l-l*c >= 0 ? tp[b]+ts[b]-l-l*c : tp[b]+ts[b]-l+l*c) : + (tp[b]+ts[b]-l+l*c >= 0 ? tp[b]+ts[b]-l+l*c : tp[b]+ts[b]-l-l*c) + ]; + } + + var x = pp[a]; + var y = pp[b]; + var positionValue = thisObj.fixed ? 'fixed' : 'absolute'; + var contractShadow = + (pp[0] + ps[0] > tp[0] || pp[0] < tp[0] + ts[0]) && + (pp[1] + ps[1] < tp[1] + ts[1]); + + jsc._drawPosition(thisObj, x, y, positionValue, contractShadow); + } + }, + + + _drawPosition : function (thisObj, x, y, positionValue, contractShadow) { + var vShadow = contractShadow ? 0 : thisObj.shadowBlur; // px + + jsc.picker.wrap.style.position = positionValue; + jsc.picker.wrap.style.left = x + 'px'; + jsc.picker.wrap.style.top = y + 'px'; + + jsc.setBoxShadow( + jsc.picker.boxS, + thisObj.shadow ? + new jsc.BoxShadow(0, vShadow, thisObj.shadowBlur, 0, thisObj.shadowColor) : + null); + }, + + + getPickerDims : function (thisObj) { + var displaySlider = !!jsc.getSliderComponent(thisObj); + var dims = [ + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.width + + (displaySlider ? 2 * thisObj.insetWidth + jsc.getPadToSliderPadding(thisObj) + thisObj.sliderSize : 0), + 2 * thisObj.insetWidth + 2 * thisObj.padding + thisObj.height + + (thisObj.closable ? 2 * thisObj.insetWidth + thisObj.padding + thisObj.buttonHeight : 0) + ]; + return dims; + }, + + + getPickerOuterDims : function (thisObj) { + var dims = jsc.getPickerDims(thisObj); + return [ + dims[0] + 2 * thisObj.borderWidth, + dims[1] + 2 * thisObj.borderWidth + ]; + }, + + + getPadToSliderPadding : function (thisObj) { + return Math.max(thisObj.padding, 1.5 * (2 * thisObj.pointerBorderWidth + thisObj.pointerThickness)); + }, + + + getPadYComponent : function (thisObj) { + switch (thisObj.mode.charAt(1).toLowerCase()) { + case 'v': return 'v'; break; + } + return 's'; + }, + + + getSliderComponent : function (thisObj) { + if (thisObj.mode.length > 2) { + switch (thisObj.mode.charAt(2).toLowerCase()) { + case 's': return 's'; break; + case 'v': return 'v'; break; + } + } + return null; + }, + + + onDocumentMouseDown : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'mouse'); + } else { + // Mouse is outside the picker controls -> hide the color picker! + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onDocumentTouchStart : function (e) { + if (!e) { e = window.event; } + var target = e.target || e.srcElement; + + if (target._jscLinkedInstance) { + if (target._jscLinkedInstance.showOnClick) { + target._jscLinkedInstance.show(); + } + } else if (target._jscControlName) { + jsc.onControlPointerStart(e, target, target._jscControlName, 'touch'); + } else { + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + } + }, + + + onWindowResize : function (e) { + jsc.redrawPosition(); + }, + + + onParentScroll : function (e) { + // hide the picker when one of the parent elements is scrolled + if (jsc.picker && jsc.picker.owner) { + jsc.picker.owner.hide(); + } + }, + + + _pointerMoveEvent : { + mouse: 'mousemove', + touch: 'touchmove' + }, + _pointerEndEvent : { + mouse: 'mouseup', + touch: 'touchend' + }, + + + _pointerOrigin : null, + _capturedTarget : null, + + + onControlPointerStart : function (e, target, controlName, pointerType) { + var thisObj = target._jscInstance; + + jsc.preventDefault(e); + jsc.captureTarget(target); + + var registerDragEvents = function (doc, offset) { + jsc.attachGroupEvent('drag', doc, jsc._pointerMoveEvent[pointerType], + jsc.onDocumentPointerMove(e, target, controlName, pointerType, offset)); + jsc.attachGroupEvent('drag', doc, jsc._pointerEndEvent[pointerType], + jsc.onDocumentPointerEnd(e, target, controlName, pointerType)); + }; + + registerDragEvents(document, [0, 0]); + + if (window.parent && window.frameElement) { + var rect = window.frameElement.getBoundingClientRect(); + var ofs = [-rect.left, -rect.top]; + registerDragEvents(window.parent.window.document, ofs); + } + + var abs = jsc.getAbsPointerPos(e); + var rel = jsc.getRelPointerPos(e); + jsc._pointerOrigin = { + x: abs.x - rel.x, + y: abs.y - rel.y + }; + + switch (controlName) { + case 'pad': + // if the slider is at the bottom, move it up + switch (jsc.getSliderComponent(thisObj)) { + case 's': if (thisObj.hsv[1] === 0) { thisObj.fromHSV(null, 100, null); }; break; + case 'v': if (thisObj.hsv[2] === 0) { thisObj.fromHSV(null, null, 100); }; break; + } + jsc.setPad(thisObj, e, 0, 0); + break; + + case 'sld': + jsc.setSld(thisObj, e, 0); + break; + } + + jsc.dispatchFineChange(thisObj); + }, + + + onDocumentPointerMove : function (e, target, controlName, pointerType, offset) { + return function (e) { + var thisObj = target._jscInstance; + switch (controlName) { + case 'pad': + if (!e) { e = window.event; } + jsc.setPad(thisObj, e, offset[0], offset[1]); + jsc.dispatchFineChange(thisObj); + break; + + case 'sld': + if (!e) { e = window.event; } + jsc.setSld(thisObj, e, offset[1]); + jsc.dispatchFineChange(thisObj); + break; + } + } + }, + + + onDocumentPointerEnd : function (e, target, controlName, pointerType) { + return function (e) { + var thisObj = target._jscInstance; + jsc.detachGroupEvents('drag'); + jsc.releaseTarget(); + // Always dispatch changes after detaching outstanding mouse handlers, + // in case some user interaction will occur in user's onchange callback + // that would intrude with current mouse events + jsc.dispatchChange(thisObj); + }; + }, + + + dispatchChange : function (thisObj) { + if (thisObj.valueElement) { + if (jsc.isElementType(thisObj.valueElement, 'input')) { + jsc.fireEvent(thisObj.valueElement, 'change'); + } + } + }, + + + dispatchFineChange : function (thisObj) { + if (thisObj.onFineChange) { + var callback; + if (typeof thisObj.onFineChange === 'string') { + callback = new Function (thisObj.onFineChange); + } else { + callback = thisObj.onFineChange; + } + callback.call(thisObj); + } + }, + + + setPad : function (thisObj, e, ofsX, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var x = ofsX + pointerAbs.x - jsc._pointerOrigin.x - thisObj.padding - thisObj.insetWidth; + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var xVal = x * (360 / (thisObj.width - 1)); + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getPadYComponent(thisObj)) { + case 's': thisObj.fromHSV(xVal, yVal, null, jsc.leaveSld); break; + case 'v': thisObj.fromHSV(xVal, null, yVal, jsc.leaveSld); break; + } + }, + + + setSld : function (thisObj, e, ofsY) { + var pointerAbs = jsc.getAbsPointerPos(e); + var y = ofsY + pointerAbs.y - jsc._pointerOrigin.y - thisObj.padding - thisObj.insetWidth; + + var yVal = 100 - (y * (100 / (thisObj.height - 1))); + + switch (jsc.getSliderComponent(thisObj)) { + case 's': thisObj.fromHSV(null, yVal, null, jsc.leavePad); break; + case 'v': thisObj.fromHSV(null, null, yVal, jsc.leavePad); break; + } + }, + + + _vmlNS : 'jsc_vml_', + _vmlCSS : 'jsc_vml_css_', + _vmlReady : false, + + + initVML : function () { + if (!jsc._vmlReady) { + // init VML namespace + var doc = document; + if (!doc.namespaces[jsc._vmlNS]) { + doc.namespaces.add(jsc._vmlNS, 'urn:schemas-microsoft-com:vml'); + } + if (!doc.styleSheets[jsc._vmlCSS]) { + var tags = ['shape', 'shapetype', 'group', 'background', 'path', 'formulas', 'handles', 'fill', 'stroke', 'shadow', 'textbox', 'textpath', 'imagedata', 'line', 'polyline', 'curve', 'rect', 'roundrect', 'oval', 'arc', 'image']; + var ss = doc.createStyleSheet(); + ss.owningElement.id = jsc._vmlCSS; + for (var i = 0; i < tags.length; i += 1) { + ss.addRule(jsc._vmlNS + '\\:' + tags[i], 'behavior:url(#default#VML);'); + } + } + jsc._vmlReady = true; + } + }, + + + createPalette : function () { + + var paletteObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, type) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var hGrad = ctx.createLinearGradient(0, 0, canvas.width, 0); + hGrad.addColorStop(0 / 6, '#F00'); + hGrad.addColorStop(1 / 6, '#FF0'); + hGrad.addColorStop(2 / 6, '#0F0'); + hGrad.addColorStop(3 / 6, '#0FF'); + hGrad.addColorStop(4 / 6, '#00F'); + hGrad.addColorStop(5 / 6, '#F0F'); + hGrad.addColorStop(6 / 6, '#F00'); + + ctx.fillStyle = hGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + var vGrad = ctx.createLinearGradient(0, 0, 0, canvas.height); + switch (type.toLowerCase()) { + case 's': + vGrad.addColorStop(0, 'rgba(255,255,255,0)'); + vGrad.addColorStop(1, 'rgba(255,255,255,1)'); + break; + case 'v': + vGrad.addColorStop(0, 'rgba(0,0,0,0)'); + vGrad.addColorStop(1, 'rgba(0,0,0,1)'); + break; + } + ctx.fillStyle = vGrad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + paletteObj.elm = canvas; + paletteObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var hGrad = document.createElement(jsc._vmlNS + ':fill'); + hGrad.type = 'gradient'; + hGrad.method = 'linear'; + hGrad.angle = '90'; + hGrad.colors = '16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0' + + var hRect = document.createElement(jsc._vmlNS + ':rect'); + hRect.style.position = 'absolute'; + hRect.style.left = -1 + 'px'; + hRect.style.top = -1 + 'px'; + hRect.stroked = false; + hRect.appendChild(hGrad); + vmlContainer.appendChild(hRect); + + var vGrad = document.createElement(jsc._vmlNS + ':fill'); + vGrad.type = 'gradient'; + vGrad.method = 'linear'; + vGrad.angle = '180'; + vGrad.opacity = '0'; + + var vRect = document.createElement(jsc._vmlNS + ':rect'); + vRect.style.position = 'absolute'; + vRect.style.left = -1 + 'px'; + vRect.style.top = -1 + 'px'; + vRect.stroked = false; + vRect.appendChild(vGrad); + vmlContainer.appendChild(vRect); + + var drawFunc = function (width, height, type) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + hRect.style.width = + vRect.style.width = + (width + 1) + 'px'; + hRect.style.height = + vRect.style.height = + (height + 1) + 'px'; + + // Colors must be specified during every redraw, otherwise IE won't display + // a full gradient during a subsequential redraw + hGrad.color = '#F00'; + hGrad.color2 = '#F00'; + + switch (type.toLowerCase()) { + case 's': + vGrad.color = vGrad.color2 = '#FFF'; + break; + case 'v': + vGrad.color = vGrad.color2 = '#000'; + break; + } + }; + + paletteObj.elm = vmlContainer; + paletteObj.draw = drawFunc; + } + + return paletteObj; + }, + + + createSliderGradient : function () { + + var sliderObj = { + elm: null, + draw: null + }; + + if (jsc.isCanvasSupported) { + // Canvas implementation for modern browsers + + var canvas = document.createElement('canvas'); + var ctx = canvas.getContext('2d'); + + var drawFunc = function (width, height, color1, color2) { + canvas.width = width; + canvas.height = height; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var grad = ctx.createLinearGradient(0, 0, 0, canvas.height); + grad.addColorStop(0, color1); + grad.addColorStop(1, color2); + + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + }; + + sliderObj.elm = canvas; + sliderObj.draw = drawFunc; + + } else { + // VML fallback for IE 7 and 8 + + jsc.initVML(); + + var vmlContainer = document.createElement('div'); + vmlContainer.style.position = 'relative'; + vmlContainer.style.overflow = 'hidden'; + + var grad = document.createElement(jsc._vmlNS + ':fill'); + grad.type = 'gradient'; + grad.method = 'linear'; + grad.angle = '180'; + + var rect = document.createElement(jsc._vmlNS + ':rect'); + rect.style.position = 'absolute'; + rect.style.left = -1 + 'px'; + rect.style.top = -1 + 'px'; + rect.stroked = false; + rect.appendChild(grad); + vmlContainer.appendChild(rect); + + var drawFunc = function (width, height, color1, color2) { + vmlContainer.style.width = width + 'px'; + vmlContainer.style.height = height + 'px'; + + rect.style.width = (width + 1) + 'px'; + rect.style.height = (height + 1) + 'px'; + + grad.color = color1; + grad.color2 = color2; + }; + + sliderObj.elm = vmlContainer; + sliderObj.draw = drawFunc; + } + + return sliderObj; + }, + + + leaveValue : 1<<0, + leaveStyle : 1<<1, + leavePad : 1<<2, + leaveSld : 1<<3, + + + BoxShadow : (function () { + var BoxShadow = function (hShadow, vShadow, blur, spread, color, inset) { + this.hShadow = hShadow; + this.vShadow = vShadow; + this.blur = blur; + this.spread = spread; + this.color = color; + this.inset = !!inset; + }; + + BoxShadow.prototype.toString = function () { + var vals = [ + Math.round(this.hShadow) + 'px', + Math.round(this.vShadow) + 'px', + Math.round(this.blur) + 'px', + Math.round(this.spread) + 'px', + this.color + ]; + if (this.inset) { + vals.push('inset'); + } + return vals.join(' '); + }; + + return BoxShadow; + })(), + + + // + // Usage: + // var myColor = new jscolor(<targetElement> [, <options>]) + // + + jscolor : function (targetElement, options) { + + // General options + // + this.value = null; // initial HEX color. To change it later, use methods fromString(), fromHSV() and fromRGB() + this.valueElement = targetElement; // element that will be used to display and input the color code + this.styleElement = targetElement; // element that will preview the picked color using CSS backgroundColor + this.required = true; // whether the associated text <input> can be left empty + this.refine = true; // whether to refine the entered color code (e.g. uppercase it and remove whitespace) + this.hash = false; // whether to prefix the HEX color code with # symbol + this.uppercase = true; // whether to show the color code in upper case + this.onFineChange = null; // called instantly every time the color changes (value can be either a function or a string with javascript code) + this.activeClass = 'jscolor-active'; // class to be set to the target element when a picker window is open on it + this.overwriteImportant = false; // whether to overwrite colors of styleElement using !important + this.minS = 0; // min allowed saturation (0 - 100) + this.maxS = 100; // max allowed saturation (0 - 100) + this.minV = 0; // min allowed value (brightness) (0 - 100) + this.maxV = 100; // max allowed value (brightness) (0 - 100) + + // Accessing the picked color + // + this.hsv = [0, 0, 100]; // read-only [0-360, 0-100, 0-100] + this.rgb = [255, 255, 255]; // read-only [0-255, 0-255, 0-255] + + // Color Picker options + // + this.width = 181; // width of color palette (in px) + this.height = 101; // height of color palette (in px) + this.showOnClick = true; // whether to display the color picker when user clicks on its target element + this.mode = 'HSV'; // HSV | HVS | HS | HV - layout of the color picker controls + this.position = 'bottom'; // left | right | top | bottom - position relative to the target element + this.smartPosition = true; // automatically change picker position when there is not enough space for it + this.sliderSize = 16; // px + this.crossSize = 8; // px + this.closable = false; // whether to display the Close button + this.closeText = 'Close'; + this.buttonColor = '#000000'; // CSS color + this.buttonHeight = 18; // px + this.padding = 12; // px + this.backgroundColor = '#FFFFFF'; // CSS color + this.borderWidth = 1; // px + this.borderColor = '#BBBBBB'; // CSS color + this.borderRadius = 8; // px + this.insetWidth = 1; // px + this.insetColor = '#BBBBBB'; // CSS color + this.shadow = true; // whether to display shadow + this.shadowBlur = 15; // px + this.shadowColor = 'rgba(0,0,0,0.2)'; // CSS color + this.pointerColor = '#4C4C4C'; // px + this.pointerBorderColor = '#FFFFFF'; // px + this.pointerBorderWidth = 1; // px + this.pointerThickness = 2; // px + this.zIndex = 1000; + this.container = null; // where to append the color picker (BODY element by default) + + + for (var opt in options) { + if (options.hasOwnProperty(opt)) { + this[opt] = options[opt]; + } + } + + + this.hide = function () { + if (isPickerOwner()) { + detachPicker(); + } + }; + + + this.show = function () { + drawPicker(); + }; + + + this.redraw = function () { + if (isPickerOwner()) { + drawPicker(); + } + }; + + + this.importColor = function () { + if (!this.valueElement) { + this.exportColor(); + } else { + if (jsc.isElementType(this.valueElement, 'input')) { + if (!this.refine) { + if (!this.fromString(this.valueElement.value, jsc.leaveValue)) { + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + } + } else if (!this.required && /^\s*$/.test(this.valueElement.value)) { + this.valueElement.value = ''; + if (this.styleElement) { + this.styleElement.style.backgroundImage = this.styleElement._jscOrigStyle.backgroundImage; + this.styleElement.style.backgroundColor = this.styleElement._jscOrigStyle.backgroundColor; + this.styleElement.style.color = this.styleElement._jscOrigStyle.color; + } + this.exportColor(jsc.leaveValue | jsc.leaveStyle); + + } else if (this.fromString(this.valueElement.value)) { + // managed to import color successfully from the value -> OK, don't do anything + } else { + this.exportColor(); + } + } else { + // not an input element -> doesn't have any value + this.exportColor(); + } + } + }; + + + this.exportColor = function (flags) { + if (!(flags & jsc.leaveValue) && this.valueElement) { + var value = this.toString(); + if (this.uppercase) { value = value.toUpperCase(); } + if (this.hash) { value = '#' + value; } + + if (jsc.isElementType(this.valueElement, 'input')) { + this.valueElement.value = value; + } else { + this.valueElement.innerHTML = value; + } + } + if (!(flags & jsc.leaveStyle)) { + if (this.styleElement) { + var bgColor = '#' + this.toString(); + var fgColor = this.isLight() ? '#000' : '#FFF'; + + this.styleElement.style.backgroundImage = 'none'; + this.styleElement.style.backgroundColor = bgColor; + this.styleElement.style.color = fgColor; + + if (this.overwriteImportant) { + this.styleElement.setAttribute('style', + 'background: ' + bgColor + ' !important; ' + + 'color: ' + fgColor + ' !important;' + ); + } + } + } + if (!(flags & jsc.leavePad) && isPickerOwner()) { + redrawPad(); + } + if (!(flags & jsc.leaveSld) && isPickerOwner()) { + redrawSld(); + } + }; + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + this.fromHSV = function (h, s, v, flags) { // null = don't change + if (h !== null) { + if (isNaN(h)) { return false; } + h = Math.max(0, Math.min(360, h)); + } + if (s !== null) { + if (isNaN(s)) { return false; } + s = Math.max(0, Math.min(100, this.maxS, s), this.minS); + } + if (v !== null) { + if (isNaN(v)) { return false; } + v = Math.max(0, Math.min(100, this.maxV, v), this.minV); + } + + this.rgb = HSV_RGB( + h===null ? this.hsv[0] : (this.hsv[0]=h), + s===null ? this.hsv[1] : (this.hsv[1]=s), + v===null ? this.hsv[2] : (this.hsv[2]=v) + ); + + this.exportColor(flags); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + this.fromRGB = function (r, g, b, flags) { // null = don't change + if (r !== null) { + if (isNaN(r)) { return false; } + r = Math.max(0, Math.min(255, r)); + } + if (g !== null) { + if (isNaN(g)) { return false; } + g = Math.max(0, Math.min(255, g)); + } + if (b !== null) { + if (isNaN(b)) { return false; } + b = Math.max(0, Math.min(255, b)); + } + + var hsv = RGB_HSV( + r===null ? this.rgb[0] : r, + g===null ? this.rgb[1] : g, + b===null ? this.rgb[2] : b + ); + if (hsv[0] !== null) { + this.hsv[0] = Math.max(0, Math.min(360, hsv[0])); + } + if (hsv[2] !== 0) { + this.hsv[1] = hsv[1]===null ? null : Math.max(0, this.minS, Math.min(100, this.maxS, hsv[1])); + } + this.hsv[2] = hsv[2]===null ? null : Math.max(0, this.minV, Math.min(100, this.maxV, hsv[2])); + + // update RGB according to final HSV, as some values might be trimmed + var rgb = HSV_RGB(this.hsv[0], this.hsv[1], this.hsv[2]); + this.rgb[0] = rgb[0]; + this.rgb[1] = rgb[1]; + this.rgb[2] = rgb[2]; + + this.exportColor(flags); + }; + + + this.fromString = function (str, flags) { + var m; + if (m = str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)) { + // HEX notation + // + + if (m[1].length === 6) { + // 6-char notation + this.fromRGB( + parseInt(m[1].substr(0,2),16), + parseInt(m[1].substr(2,2),16), + parseInt(m[1].substr(4,2),16), + flags + ); + } else { + // 3-char notation + this.fromRGB( + parseInt(m[1].charAt(0) + m[1].charAt(0),16), + parseInt(m[1].charAt(1) + m[1].charAt(1),16), + parseInt(m[1].charAt(2) + m[1].charAt(2),16), + flags + ); + } + return true; + + } else if (m = str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)) { + var params = m[1].split(','); + var re = /^\s*(\d*)(\.\d+)?\s*$/; + var mR, mG, mB; + if ( + params.length >= 3 && + (mR = params[0].match(re)) && + (mG = params[1].match(re)) && + (mB = params[2].match(re)) + ) { + var r = parseFloat((mR[1] || '0') + (mR[2] || '')); + var g = parseFloat((mG[1] || '0') + (mG[2] || '')); + var b = parseFloat((mB[1] || '0') + (mB[2] || '')); + this.fromRGB(r, g, b, flags); + return true; + } + } + return false; + }; + + + this.toString = function () { + return ( + (0x100 | Math.round(this.rgb[0])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[1])).toString(16).substr(1) + + (0x100 | Math.round(this.rgb[2])).toString(16).substr(1) + ); + }; + + + this.toHEXString = function () { + return '#' + this.toString().toUpperCase(); + }; + + + this.toRGBString = function () { + return ('rgb(' + + Math.round(this.rgb[0]) + ',' + + Math.round(this.rgb[1]) + ',' + + Math.round(this.rgb[2]) + ')' + ); + }; + + + this.isLight = function () { + return ( + 0.213 * this.rgb[0] + + 0.715 * this.rgb[1] + + 0.072 * this.rgb[2] > + 255 / 2 + ); + }; + + + this._processParentElementsInDOM = function () { + if (this._linkedElementsProcessed) { return; } + this._linkedElementsProcessed = true; + + var elm = this.targetElement; + do { + // If the target element or one of its parent nodes has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // Ensure to attach onParentScroll only once to each parent element + // (multiple targetElements can share the same parent nodes) + // + // Note: It's not just offsetParents that can be scrollable, + // that's why we loop through all parent nodes + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.parentNode) && !jsc.isElementType(elm, 'body')); + }; + + + // r: 0-255 + // g: 0-255 + // b: 0-255 + // + // returns: [ 0-360, 0-100, 0-100 ] + // + function RGB_HSV (r, g, b) { + r /= 255; + g /= 255; + b /= 255; + var n = Math.min(Math.min(r,g),b); + var v = Math.max(Math.max(r,g),b); + var m = v - n; + if (m === 0) { return [ null, 0, 100 * v ]; } + var h = r===n ? 3+(b-g)/m : (g===n ? 5+(r-b)/m : 1+(g-r)/m); + return [ + 60 * (h===6?0:h), + 100 * (m/v), + 100 * v + ]; + } + + + // h: 0-360 + // s: 0-100 + // v: 0-100 + // + // returns: [ 0-255, 0-255, 0-255 ] + // + function HSV_RGB (h, s, v) { + var u = 255 * (v / 100); + + if (h === null) { + return [ u, u, u ]; + } + + h /= 60; + s /= 100; + + var i = Math.floor(h); + var f = i%2 ? h-i : 1-(h-i); + var m = u * (1 - s); + var n = u * (1 - s * f); + switch (i) { + case 6: + case 0: return [u,n,m]; + case 1: return [n,u,m]; + case 2: return [m,u,n]; + case 3: return [m,n,u]; + case 4: return [n,m,u]; + case 5: return [u,m,n]; + } + } + + + function detachPicker () { + jsc.unsetClass(THIS.targetElement, THIS.activeClass); + jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap); + delete jsc.picker.owner; + } + + + function drawPicker () { + + // At this point, when drawing the picker, we know what the parent elements are + // and we can do all related DOM operations, such as registering events on them + // or checking their positioning + THIS._processParentElementsInDOM(); + + if (!jsc.picker) { + jsc.picker = { + owner: null, + wrap : document.createElement('div'), + box : document.createElement('div'), + boxS : document.createElement('div'), // shadow area + boxB : document.createElement('div'), // border + pad : document.createElement('div'), + padB : document.createElement('div'), // border + padM : document.createElement('div'), // mouse/touch area + padPal : jsc.createPalette(), + cross : document.createElement('div'), + crossBY : document.createElement('div'), // border Y + crossBX : document.createElement('div'), // border X + crossLY : document.createElement('div'), // line Y + crossLX : document.createElement('div'), // line X + sld : document.createElement('div'), + sldB : document.createElement('div'), // border + sldM : document.createElement('div'), // mouse/touch area + sldGrad : jsc.createSliderGradient(), + sldPtrS : document.createElement('div'), // slider pointer spacer + sldPtrIB : document.createElement('div'), // slider pointer inner border + sldPtrMB : document.createElement('div'), // slider pointer middle border + sldPtrOB : document.createElement('div'), // slider pointer outer border + btn : document.createElement('div'), + btnT : document.createElement('span') // text + }; + + jsc.picker.pad.appendChild(jsc.picker.padPal.elm); + jsc.picker.padB.appendChild(jsc.picker.pad); + jsc.picker.cross.appendChild(jsc.picker.crossBY); + jsc.picker.cross.appendChild(jsc.picker.crossBX); + jsc.picker.cross.appendChild(jsc.picker.crossLY); + jsc.picker.cross.appendChild(jsc.picker.crossLX); + jsc.picker.padB.appendChild(jsc.picker.cross); + jsc.picker.box.appendChild(jsc.picker.padB); + jsc.picker.box.appendChild(jsc.picker.padM); + + jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm); + jsc.picker.sldB.appendChild(jsc.picker.sld); + jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB); + jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB); + jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB); + jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS); + jsc.picker.box.appendChild(jsc.picker.sldB); + jsc.picker.box.appendChild(jsc.picker.sldM); + + jsc.picker.btn.appendChild(jsc.picker.btnT); + jsc.picker.box.appendChild(jsc.picker.btn); + + jsc.picker.boxB.appendChild(jsc.picker.box); + jsc.picker.wrap.appendChild(jsc.picker.boxS); + jsc.picker.wrap.appendChild(jsc.picker.boxB); + } + + var p = jsc.picker; + + var displaySlider = !!jsc.getSliderComponent(THIS); + var dims = jsc.getPickerDims(THIS); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var padToSliderPadding = jsc.getPadToSliderPadding(THIS); + var borderRadius = Math.min( + THIS.borderRadius, + Math.round(THIS.padding * Math.PI)); // px + var padCursor = 'crosshair'; + + // wrap + p.wrap.style.clear = 'both'; + p.wrap.style.width = (dims[0] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.height = (dims[1] + 2 * THIS.borderWidth) + 'px'; + p.wrap.style.zIndex = THIS.zIndex; + + // picker + p.box.style.width = dims[0] + 'px'; + p.box.style.height = dims[1] + 'px'; + + p.boxS.style.position = 'absolute'; + p.boxS.style.left = '0'; + p.boxS.style.top = '0'; + p.boxS.style.width = '100%'; + p.boxS.style.height = '100%'; + jsc.setBorderRadius(p.boxS, borderRadius + 'px'); + + // picker border + p.boxB.style.position = 'relative'; + p.boxB.style.border = THIS.borderWidth + 'px solid'; + p.boxB.style.borderColor = THIS.borderColor; + p.boxB.style.background = THIS.backgroundColor; + jsc.setBorderRadius(p.boxB, borderRadius + 'px'); + + // IE hack: + // If the element is transparent, IE will trigger the event on the elements under it, + // e.g. on Canvas or on elements with border + p.padM.style.background = + p.sldM.style.background = + '#FFF'; + jsc.setStyle(p.padM, 'opacity', '0'); + jsc.setStyle(p.sldM, 'opacity', '0'); + + // pad + p.pad.style.position = 'relative'; + p.pad.style.width = THIS.width + 'px'; + p.pad.style.height = THIS.height + 'px'; + + // pad palettes (HSV and HVS) + p.padPal.draw(THIS.width, THIS.height, jsc.getPadYComponent(THIS)); + + // pad border + p.padB.style.position = 'absolute'; + p.padB.style.left = THIS.padding + 'px'; + p.padB.style.top = THIS.padding + 'px'; + p.padB.style.border = THIS.insetWidth + 'px solid'; + p.padB.style.borderColor = THIS.insetColor; + + // pad mouse area + p.padM._jscInstance = THIS; + p.padM._jscControlName = 'pad'; + p.padM.style.position = 'absolute'; + p.padM.style.left = '0'; + p.padM.style.top = '0'; + p.padM.style.width = (THIS.padding + 2 * THIS.insetWidth + THIS.width + padToSliderPadding / 2) + 'px'; + p.padM.style.height = dims[1] + 'px'; + p.padM.style.cursor = padCursor; + + // pad cross + p.cross.style.position = 'absolute'; + p.cross.style.left = + p.cross.style.top = + '0'; + p.cross.style.width = + p.cross.style.height = + crossOuterSize + 'px'; + + // pad cross border Y and X + p.crossBY.style.position = + p.crossBX.style.position = + 'absolute'; + p.crossBY.style.background = + p.crossBX.style.background = + THIS.pointerBorderColor; + p.crossBY.style.width = + p.crossBX.style.height = + (2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.crossBY.style.height = + p.crossBX.style.width = + crossOuterSize + 'px'; + p.crossBY.style.left = + p.crossBX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2) - THIS.pointerBorderWidth) + 'px'; + p.crossBY.style.top = + p.crossBX.style.left = + '0'; + + // pad cross line Y and X + p.crossLY.style.position = + p.crossLX.style.position = + 'absolute'; + p.crossLY.style.background = + p.crossLX.style.background = + THIS.pointerColor; + p.crossLY.style.height = + p.crossLX.style.width = + (crossOuterSize - 2 * THIS.pointerBorderWidth) + 'px'; + p.crossLY.style.width = + p.crossLX.style.height = + THIS.pointerThickness + 'px'; + p.crossLY.style.left = + p.crossLX.style.top = + (Math.floor(crossOuterSize / 2) - Math.floor(THIS.pointerThickness / 2)) + 'px'; + p.crossLY.style.top = + p.crossLX.style.left = + THIS.pointerBorderWidth + 'px'; + + // slider + p.sld.style.overflow = 'hidden'; + p.sld.style.width = THIS.sliderSize + 'px'; + p.sld.style.height = THIS.height + 'px'; + + // slider gradient + p.sldGrad.draw(THIS.sliderSize, THIS.height, '#000', '#000'); + + // slider border + p.sldB.style.display = displaySlider ? 'block' : 'none'; + p.sldB.style.position = 'absolute'; + p.sldB.style.right = THIS.padding + 'px'; + p.sldB.style.top = THIS.padding + 'px'; + p.sldB.style.border = THIS.insetWidth + 'px solid'; + p.sldB.style.borderColor = THIS.insetColor; + + // slider mouse area + p.sldM._jscInstance = THIS; + p.sldM._jscControlName = 'sld'; + p.sldM.style.display = displaySlider ? 'block' : 'none'; + p.sldM.style.position = 'absolute'; + p.sldM.style.right = '0'; + p.sldM.style.top = '0'; + p.sldM.style.width = (THIS.sliderSize + padToSliderPadding / 2 + THIS.padding + 2 * THIS.insetWidth) + 'px'; + p.sldM.style.height = dims[1] + 'px'; + p.sldM.style.cursor = 'default'; + + // slider pointer inner and outer border + p.sldPtrIB.style.border = + p.sldPtrOB.style.border = + THIS.pointerBorderWidth + 'px solid ' + THIS.pointerBorderColor; + + // slider pointer outer border + p.sldPtrOB.style.position = 'absolute'; + p.sldPtrOB.style.left = -(2 * THIS.pointerBorderWidth + THIS.pointerThickness) + 'px'; + p.sldPtrOB.style.top = '0'; + + // slider pointer middle border + p.sldPtrMB.style.border = THIS.pointerThickness + 'px solid ' + THIS.pointerColor; + + // slider pointer spacer + p.sldPtrS.style.width = THIS.sliderSize + 'px'; + p.sldPtrS.style.height = sliderPtrSpace + 'px'; + + // the Close button + function setBtnBorder () { + var insetColors = THIS.insetColor.split(/\s+/); + var outsetColor = insetColors.length < 2 ? insetColors[0] : insetColors[1] + ' ' + insetColors[0] + ' ' + insetColors[0] + ' ' + insetColors[1]; + p.btn.style.borderColor = outsetColor; + } + p.btn.style.display = THIS.closable ? 'block' : 'none'; + p.btn.style.position = 'absolute'; + p.btn.style.left = THIS.padding + 'px'; + p.btn.style.bottom = THIS.padding + 'px'; + p.btn.style.padding = '0 15px'; + p.btn.style.height = THIS.buttonHeight + 'px'; + p.btn.style.border = THIS.insetWidth + 'px solid'; + setBtnBorder(); + p.btn.style.color = THIS.buttonColor; + p.btn.style.font = '12px sans-serif'; + p.btn.style.textAlign = 'center'; + try { + p.btn.style.cursor = 'pointer'; + } catch(eOldIE) { + p.btn.style.cursor = 'hand'; + } + p.btn.onmousedown = function () { + THIS.hide(); + }; + p.btnT.style.lineHeight = THIS.buttonHeight + 'px'; + p.btnT.innerHTML = ''; + p.btnT.appendChild(document.createTextNode(THIS.closeText)); + + // place pointers + redrawPad(); + redrawSld(); + + // If we are changing the owner without first closing the picker, + // make sure to first deal with the old owner + if (jsc.picker.owner && jsc.picker.owner !== THIS) { + jsc.unsetClass(jsc.picker.owner.targetElement, THIS.activeClass); + } + + // Set the new picker owner + jsc.picker.owner = THIS; + + // The redrawPosition() method needs picker.owner to be set, that's why we call it here, + // after setting the owner + if (jsc.isElementType(container, 'body')) { + jsc.redrawPosition(); + } else { + jsc._drawPosition(THIS, 0, 0, 'relative', false); + } + + if (p.wrap.parentNode != container) { + container.appendChild(p.wrap); + } + + jsc.setClass(THIS.targetElement, THIS.activeClass); + } + + + function redrawPad () { + // redraw the pad pointer + switch (jsc.getPadYComponent(THIS)) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var x = Math.round((THIS.hsv[0] / 360) * (THIS.width - 1)); + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + var crossOuterSize = (2 * THIS.pointerBorderWidth + THIS.pointerThickness + 2 * THIS.crossSize); + var ofs = -Math.floor(crossOuterSize / 2); + jsc.picker.cross.style.left = (x + ofs) + 'px'; + jsc.picker.cross.style.top = (y + ofs) + 'px'; + + // redraw the slider + switch (jsc.getSliderComponent(THIS)) { + case 's': + var rgb1 = HSV_RGB(THIS.hsv[0], 100, THIS.hsv[2]); + var rgb2 = HSV_RGB(THIS.hsv[0], 0, THIS.hsv[2]); + var color1 = 'rgb(' + + Math.round(rgb1[0]) + ',' + + Math.round(rgb1[1]) + ',' + + Math.round(rgb1[2]) + ')'; + var color2 = 'rgb(' + + Math.round(rgb2[0]) + ',' + + Math.round(rgb2[1]) + ',' + + Math.round(rgb2[2]) + ')'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + case 'v': + var rgb = HSV_RGB(THIS.hsv[0], THIS.hsv[1], 100); + var color1 = 'rgb(' + + Math.round(rgb[0]) + ',' + + Math.round(rgb[1]) + ',' + + Math.round(rgb[2]) + ')'; + var color2 = '#000'; + jsc.picker.sldGrad.draw(THIS.sliderSize, THIS.height, color1, color2); + break; + } + } + + + function redrawSld () { + var sldComponent = jsc.getSliderComponent(THIS); + if (sldComponent) { + // redraw the slider pointer + switch (sldComponent) { + case 's': var yComponent = 1; break; + case 'v': var yComponent = 2; break; + } + var y = Math.round((1 - THIS.hsv[yComponent] / 100) * (THIS.height - 1)); + jsc.picker.sldPtrOB.style.top = (y - (2 * THIS.pointerBorderWidth + THIS.pointerThickness) - Math.floor(sliderPtrSpace / 2)) + 'px'; + } + } + + + function isPickerOwner () { + return jsc.picker && jsc.picker.owner === THIS; + } + + + function blurValue () { + THIS.importColor(); + } + + + // Find the target element + if (typeof targetElement === 'string') { + var id = targetElement; + var elm = document.getElementById(id); + if (elm) { + this.targetElement = elm; + } else { + jsc.warn('Could not find target element with ID \'' + id + '\''); + } + } else if (targetElement) { + this.targetElement = targetElement; + } else { + jsc.warn('Invalid target element: \'' + targetElement + '\''); + } + + if (this.targetElement._jscLinkedInstance) { + jsc.warn('Cannot link jscolor twice to the same element. Skipping.'); + return; + } + this.targetElement._jscLinkedInstance = this; + + // Find the value element + this.valueElement = jsc.fetchElement(this.valueElement); + // Find the style element + this.styleElement = jsc.fetchElement(this.styleElement); + + var THIS = this; + var container = + this.container ? + jsc.fetchElement(this.container) : + document.getElementsByTagName('body')[0]; + var sliderPtrSpace = 3; // px + + // For BUTTON elements it's important to stop them from sending the form when clicked + // (e.g. in Safari) + if (jsc.isElementType(this.targetElement, 'button')) { + if (this.targetElement.onclick) { + var origCallback = this.targetElement.onclick; + this.targetElement.onclick = function (evt) { + origCallback.call(this, evt); + return false; + }; + } else { + this.targetElement.onclick = function () { return false; }; + } + } + + /* + var elm = this.targetElement; + do { + // If the target element or one of its offsetParents has fixed position, + // then use fixed positioning instead + // + // Note: In Firefox, getComputedStyle returns null in a hidden iframe, + // that's why we need to check if the returned style object is non-empty + var currStyle = jsc.getStyle(elm); + if (currStyle && currStyle.position.toLowerCase() === 'fixed') { + this.fixed = true; + } + + if (elm !== this.targetElement) { + // attach onParentScroll so that we can recompute the picker position + // when one of the offsetParents is scrolled + if (!elm._jscEventsAttached) { + jsc.attachEvent(elm, 'scroll', jsc.onParentScroll); + elm._jscEventsAttached = true; + } + } + } while ((elm = elm.offsetParent) && !jsc.isElementType(elm, 'body')); + */ + + // valueElement + if (this.valueElement) { + if (jsc.isElementType(this.valueElement, 'input')) { + var updateField = function () { + THIS.fromString(THIS.valueElement.value, jsc.leaveValue); + jsc.dispatchFineChange(THIS); + }; + jsc.attachEvent(this.valueElement, 'keyup', updateField); + jsc.attachEvent(this.valueElement, 'input', updateField); + jsc.attachEvent(this.valueElement, 'blur', blurValue); + this.valueElement.setAttribute('autocomplete', 'off'); + } + } + + // styleElement + if (this.styleElement) { + this.styleElement._jscOrigStyle = { + backgroundImage : this.styleElement.style.backgroundImage, + backgroundColor : this.styleElement.style.backgroundColor, + color : this.styleElement.style.color + }; + } + + if (this.value) { + // Try to set the color from the .value option and if unsuccessful, + // export the current color + this.fromString(this.value) || this.exportColor(); + } else { + this.importColor(); + } + } + +}; + + +//================================ +// Public properties and methods +//================================ + + +// By default, search for all elements with class="jscolor" and install a color picker on them. +// +// You can change what class name will be looked for by setting the property jscolor.lookupClass +// anywhere in your HTML document. To completely disable the automatic lookup, set it to null. +// +jsc.jscolor.lookupClass = 'jscolor'; + + +jsc.jscolor.installByClassName = function (className) { + var inputElms = document.getElementsByTagName('input'); + var buttonElms = document.getElementsByTagName('button'); + + jsc.tryInstallOnElements(inputElms, className); + jsc.tryInstallOnElements(buttonElms, className); +}; + + +jsc.register(); + + +return jsc.jscolor; + + +})(); } diff --git a/static/colorfield/jscolor/jscolor.min.js b/static/colorfield/jscolor/jscolor.min.js new file mode 100755 index 0000000000000000000000000000000000000000..b80f38eb1e741def1cb6210e7c079335867d56b5 --- /dev/null +++ b/static/colorfield/jscolor/jscolor.min.js @@ -0,0 +1 @@ +"use strict";if(!window.jscolor){window.jscolor=(function(){var jsc={register:function(){jsc.attachDOMReadyEvent(jsc.init);jsc.attachEvent(document,'mousedown',jsc.onDocumentMouseDown);jsc.attachEvent(document,'touchstart',jsc.onDocumentTouchStart);jsc.attachEvent(window,'resize',jsc.onWindowResize)},init:function(){if(jsc.jscolor.lookupClass){jsc.jscolor.installByClassName(jsc.jscolor.lookupClass)}},tryInstallOnElements:function(elms,className){var matchClass=new RegExp('(^|\\s)('+className+')(\\s*(\\{[^}]*\\})|\\s|$)','i');for(var i=0;i<elms.length;i+=1){if(elms[i].type!==undefined&&elms[i].type.toLowerCase()=='color'){if(jsc.isColorAttrSupported){continue}}var m;if(!elms[i].jscolor&&elms[i].className&&(m=elms[i].className.match(matchClass))){var targetElm=elms[i];var optsStr=null;var dataOptions=jsc.getDataAttr(targetElm,'jscolor');if(dataOptions!==null){optsStr=dataOptions}else if(m[4]){optsStr=m[4]}var opts={};if(optsStr){try{opts=(new Function('return ('+optsStr+')'))()}catch(eParseError){jsc.warn('Error parsing jscolor options: '+eParseError+':\n'+optsStr)}}targetElm.jscolor=new jsc.jscolor(targetElm,opts)}}},isColorAttrSupported:(function(){var elm=document.createElement('input');if(elm.setAttribute){elm.setAttribute('type','color');if(elm.type.toLowerCase()=='color'){return true}}return false})(),isCanvasSupported:(function(){var elm=document.createElement('canvas');return!!(elm.getContext&&elm.getContext('2d'))})(),fetchElement:function(mixed){return typeof mixed==='string'?document.getElementById(mixed):mixed},isElementType:function(elm,type){return elm.nodeName.toLowerCase()===type.toLowerCase()},getDataAttr:function(el,name){var attrName='data-'+name;var attrValue=el.getAttribute(attrName);if(attrValue!==null){return attrValue}return null},attachEvent:function(el,evnt,func){if(el.addEventListener){el.addEventListener(evnt,func,false)}else if(el.attachEvent){el.attachEvent('on'+evnt,func)}},detachEvent:function(el,evnt,func){if(el.removeEventListener){el.removeEventListener(evnt,func,false)}else if(el.detachEvent){el.detachEvent('on'+evnt,func)}},_attachedGroupEvents:{},attachGroupEvent:function(groupName,el,evnt,func){if(!jsc._attachedGroupEvents.hasOwnProperty(groupName)){jsc._attachedGroupEvents[groupName]=[]}jsc._attachedGroupEvents[groupName].push([el,evnt,func]);jsc.attachEvent(el,evnt,func)},detachGroupEvents:function(groupName){if(jsc._attachedGroupEvents.hasOwnProperty(groupName)){for(var i=0;i<jsc._attachedGroupEvents[groupName].length;i+=1){var evt=jsc._attachedGroupEvents[groupName][i];jsc.detachEvent(evt[0],evt[1],evt[2])}delete jsc._attachedGroupEvents[groupName]}},attachDOMReadyEvent:function(func){var fired=false;var fireOnce=function(){if(!fired){fired=true;func()}};if(document.readyState==='complete'){setTimeout(fireOnce,1);return}if(document.addEventListener){document.addEventListener('DOMContentLoaded',fireOnce,false);window.addEventListener('load',fireOnce,false)}else if(document.attachEvent){document.attachEvent('onreadystatechange',function(){if(document.readyState==='complete'){document.detachEvent('onreadystatechange',arguments.callee);fireOnce()}});window.attachEvent('onload',fireOnce);if(document.documentElement.doScroll&&window==window.top){var tryScroll=function(){if(!document.body){return}try{document.documentElement.doScroll('left');fireOnce()}catch(e){setTimeout(tryScroll,1)}};tryScroll()}}},warn:function(msg){if(window.console&&window.console.warn){window.console.warn(msg)}},preventDefault:function(e){if(e.preventDefault){e.preventDefault()}e.returnValue=false},captureTarget:function(target){if(target.setCapture){jsc._capturedTarget=target;jsc._capturedTarget.setCapture()}},releaseTarget:function(){if(jsc._capturedTarget){jsc._capturedTarget.releaseCapture();jsc._capturedTarget=null}},fireEvent:function(el,evnt){if(!el){return}if(document.createEvent){var ev=document.createEvent('HTMLEvents');ev.initEvent(evnt,true,true);el.dispatchEvent(ev)}else if(document.createEventObject){var ev=document.createEventObject();el.fireEvent('on'+evnt,ev)}else if(el['on'+evnt]){el['on'+evnt]()}},classNameToList:function(className){return className.replace(/^\s+|\s+$/g,'').split(/\s+/)},hasClass:function(elm,className){if(!className){return false}return -1!=(' '+elm.className.replace(/\s+/g,' ')+' ').indexOf(' '+className+' ')},setClass:function(elm,className){var classList=jsc.classNameToList(className);for(var i=0;i<classList.length;i+=1){if(!jsc.hasClass(elm,classList[i])){elm.className+=(elm.className?' ':'')+classList[i]}}},unsetClass:function(elm,className){var classList=jsc.classNameToList(className);for(var i=0;i<classList.length;i+=1){var repl=new RegExp('^\\s*'+classList[i]+'\\s*|\\s*'+classList[i]+'\\s*$|\\s+'+classList[i]+'(\\s+)','g');elm.className=elm.className.replace(repl,'$1')}},getStyle:function(elm){return window.getComputedStyle?window.getComputedStyle(elm):elm.currentStyle},setStyle:(function(){var helper=document.createElement('div');var getSupportedProp=function(names){for(var i=0;i<names.length;i+=1){if(names[i]in helper.style){return names[i]}}};var props={borderRadius:getSupportedProp(['borderRadius','MozBorderRadius','webkitBorderRadius']),boxShadow:getSupportedProp(['boxShadow','MozBoxShadow','webkitBoxShadow'])};return function(elm,prop,value){switch(prop.toLowerCase()){case 'opacity':var alphaOpacity=Math.round(parseFloat(value)*100);elm.style.opacity=value;elm.style.filter='alpha(opacity='+alphaOpacity+')';break;default:elm.style[props[prop]]=value;break}}})(),setBorderRadius:function(elm,value){jsc.setStyle(elm,'borderRadius',value||'0')},setBoxShadow:function(elm,value){jsc.setStyle(elm,'boxShadow',value||'none')},getElementPos:function(e,relativeToViewport){var x=0,y=0;var rect=e.getBoundingClientRect();x=rect.left;y=rect.top;if(!relativeToViewport){var viewPos=jsc.getViewPos();x+=viewPos[0];y+=viewPos[1]}return[x,y]},getElementSize:function(e){return[e.offsetWidth,e.offsetHeight]},getAbsPointerPos:function(e){if(!e){e=window.event}var x=0,y=0;if(typeof e.changedTouches!=='undefined'&&e.changedTouches.length){x=e.changedTouches[0].clientX;y=e.changedTouches[0].clientY}else if(typeof e.clientX==='number'){x=e.clientX;y=e.clientY}return{x:x,y:y}},getRelPointerPos:function(e){if(!e){e=window.event}var target=e.target||e.srcElement;var targetRect=target.getBoundingClientRect();var x=0,y=0;var clientX=0,clientY=0;if(typeof e.changedTouches!=='undefined'&&e.changedTouches.length){clientX=e.changedTouches[0].clientX;clientY=e.changedTouches[0].clientY}else if(typeof e.clientX==='number'){clientX=e.clientX;clientY=e.clientY}x=clientX-targetRect.left;y=clientY-targetRect.top;return{x:x,y:y}},getViewPos:function(){var doc=document.documentElement;return[(window.pageXOffset||doc.scrollLeft)-(doc.clientLeft||0),(window.pageYOffset||doc.scrollTop)-(doc.clientTop||0)]},getViewSize:function(){var doc=document.documentElement;return[(window.innerWidth||doc.clientWidth),(window.innerHeight||doc.clientHeight)]},redrawPosition:function(){if(jsc.picker&&jsc.picker.owner){var thisObj=jsc.picker.owner;var tp,vp;if(thisObj.fixed){tp=jsc.getElementPos(thisObj.targetElement,true);vp=[0,0];}else{tp=jsc.getElementPos(thisObj.targetElement);vp=jsc.getViewPos();}var ts=jsc.getElementSize(thisObj.targetElement);var vs=jsc.getViewSize();var ps=jsc.getPickerOuterDims(thisObj);var a,b,c;switch(thisObj.position.toLowerCase()){case 'left':a=1;b=0;c=-1;break;case 'right':a=1;b=0;c=1;break;case 'top':a=0;b=1;c=-1;break;default:a=0;b=1;c=1;break}var l=(ts[b]+ps[b])/2;if(!thisObj.smartPosition){var pp=[tp[a],tp[b]+ts[b]-l+l*c]}else{var pp=[-vp[a]+tp[a]+ps[a]>vs[a]?(-vp[a]+tp[a]+ts[a]/2>vs[a]/2&&tp[a]+ts[a]-ps[a]>=0?tp[a]+ts[a]-ps[a]:tp[a]):tp[a],-vp[b]+tp[b]+ts[b]+ps[b]-l+l*c>vs[b]?(-vp[b]+tp[b]+ts[b]/2>vs[b]/2&&tp[b]+ts[b]-l-l*c>=0?tp[b]+ts[b]-l-l*c:tp[b]+ts[b]-l+l*c):(tp[b]+ts[b]-l+l*c>=0?tp[b]+ts[b]-l+l*c:tp[b]+ts[b]-l-l*c)]}var x=pp[a];var y=pp[b];var positionValue=thisObj.fixed?'fixed':'absolute';var contractShadow=(pp[0]+ps[0]>tp[0]||pp[0]<tp[0]+ts[0])&&(pp[1]+ps[1]<tp[1]+ts[1]);jsc._drawPosition(thisObj,x,y,positionValue,contractShadow)}},_drawPosition:function(thisObj,x,y,positionValue,contractShadow){var vShadow=contractShadow?0:thisObj.shadowBlur;jsc.picker.wrap.style.position=positionValue;jsc.picker.wrap.style.left=x+'px';jsc.picker.wrap.style.top=y+'px';jsc.setBoxShadow(jsc.picker.boxS,thisObj.shadow?new jsc.BoxShadow(0,vShadow,thisObj.shadowBlur,0,thisObj.shadowColor):null)},getPickerDims:function(thisObj){var displaySlider=!!jsc.getSliderComponent(thisObj);var dims=[2*thisObj.insetWidth+2*thisObj.padding+thisObj.width+(displaySlider?2*thisObj.insetWidth+jsc.getPadToSliderPadding(thisObj)+thisObj.sliderSize:0),2*thisObj.insetWidth+2*thisObj.padding+thisObj.height+(thisObj.closable?2*thisObj.insetWidth+thisObj.padding+thisObj.buttonHeight:0)];return dims},getPickerOuterDims:function(thisObj){var dims=jsc.getPickerDims(thisObj);return[dims[0]+2*thisObj.borderWidth,dims[1]+2*thisObj.borderWidth]},getPadToSliderPadding:function(thisObj){return Math.max(thisObj.padding,1.5*(2*thisObj.pointerBorderWidth+thisObj.pointerThickness))},getPadYComponent:function(thisObj){switch(thisObj.mode.charAt(1).toLowerCase()){case 'v':return 'v';break}return 's'},getSliderComponent:function(thisObj){if(thisObj.mode.length>2){switch(thisObj.mode.charAt(2).toLowerCase()){case 's':return 's';break;case 'v':return 'v';break}}return null},onDocumentMouseDown:function(e){if(!e){e=window.event}var target=e.target||e.srcElement;if(target._jscLinkedInstance){if(target._jscLinkedInstance.showOnClick){target._jscLinkedInstance.show()}}else if(target._jscControlName){jsc.onControlPointerStart(e,target,target._jscControlName,'mouse')}else{if(jsc.picker&&jsc.picker.owner){jsc.picker.owner.hide()}}},onDocumentTouchStart:function(e){if(!e){e=window.event}var target=e.target||e.srcElement;if(target._jscLinkedInstance){if(target._jscLinkedInstance.showOnClick){target._jscLinkedInstance.show()}}else if(target._jscControlName){jsc.onControlPointerStart(e,target,target._jscControlName,'touch')}else{if(jsc.picker&&jsc.picker.owner){jsc.picker.owner.hide()}}},onWindowResize:function(e){jsc.redrawPosition()},onParentScroll:function(e){if(jsc.picker&&jsc.picker.owner){jsc.picker.owner.hide()}},_pointerMoveEvent:{mouse:'mousemove',touch:'touchmove'},_pointerEndEvent:{mouse:'mouseup',touch:'touchend'},_pointerOrigin:null,_capturedTarget:null,onControlPointerStart:function(e,target,controlName,pointerType){var thisObj=target._jscInstance;jsc.preventDefault(e);jsc.captureTarget(target);var registerDragEvents=function(doc,offset){jsc.attachGroupEvent('drag',doc,jsc._pointerMoveEvent[pointerType],jsc.onDocumentPointerMove(e,target,controlName,pointerType,offset));jsc.attachGroupEvent('drag',doc,jsc._pointerEndEvent[pointerType],jsc.onDocumentPointerEnd(e,target,controlName,pointerType))};registerDragEvents(document,[0,0]);if(window.parent&&window.frameElement){var rect=window.frameElement.getBoundingClientRect();var ofs=[-rect.left,-rect.top];registerDragEvents(window.parent.window.document,ofs)}var abs=jsc.getAbsPointerPos(e);var rel=jsc.getRelPointerPos(e);jsc._pointerOrigin={x:abs.x-rel.x,y:abs.y-rel.y};switch(controlName){case 'pad':switch(jsc.getSliderComponent(thisObj)){case 's':if(thisObj.hsv[1]===0){thisObj.fromHSV(null,100,null)};break;case 'v':if(thisObj.hsv[2]===0){thisObj.fromHSV(null,null,100)};break}jsc.setPad(thisObj,e,0,0);break;case 'sld':jsc.setSld(thisObj,e,0);break}jsc.dispatchFineChange(thisObj)},onDocumentPointerMove:function(e,target,controlName,pointerType,offset){return function(e){var thisObj=target._jscInstance;switch(controlName){case 'pad':if(!e){e=window.event}jsc.setPad(thisObj,e,offset[0],offset[1]);jsc.dispatchFineChange(thisObj);break;case 'sld':if(!e){e=window.event}jsc.setSld(thisObj,e,offset[1]);jsc.dispatchFineChange(thisObj);break}}},onDocumentPointerEnd:function(e,target,controlName,pointerType){return function(e){var thisObj=target._jscInstance;jsc.detachGroupEvents('drag');jsc.releaseTarget();jsc.dispatchChange(thisObj)}},dispatchChange:function(thisObj){if(thisObj.valueElement){if(jsc.isElementType(thisObj.valueElement,'input')){jsc.fireEvent(thisObj.valueElement,'change')}}},dispatchFineChange:function(thisObj){if(thisObj.onFineChange){var callback;if(typeof thisObj.onFineChange==='string'){callback=new Function(thisObj.onFineChange)}else{callback=thisObj.onFineChange}callback.call(thisObj)}},setPad:function(thisObj,e,ofsX,ofsY){var pointerAbs=jsc.getAbsPointerPos(e);var x=ofsX+pointerAbs.x-jsc._pointerOrigin.x-thisObj.padding-thisObj.insetWidth;var y=ofsY+pointerAbs.y-jsc._pointerOrigin.y-thisObj.padding-thisObj.insetWidth;var xVal=x*(360/(thisObj.width-1));var yVal=100-(y*(100/(thisObj.height-1)));switch(jsc.getPadYComponent(thisObj)){case 's':thisObj.fromHSV(xVal,yVal,null,jsc.leaveSld);break;case 'v':thisObj.fromHSV(xVal,null,yVal,jsc.leaveSld);break}},setSld:function(thisObj,e,ofsY){var pointerAbs=jsc.getAbsPointerPos(e);var y=ofsY+pointerAbs.y-jsc._pointerOrigin.y-thisObj.padding-thisObj.insetWidth;var yVal=100-(y*(100/(thisObj.height-1)));switch(jsc.getSliderComponent(thisObj)){case 's':thisObj.fromHSV(null,yVal,null,jsc.leavePad);break;case 'v':thisObj.fromHSV(null,null,yVal,jsc.leavePad);break}},_vmlNS:'jsc_vml_',_vmlCSS:'jsc_vml_css_',_vmlReady:false,initVML:function(){if(!jsc._vmlReady){var doc=document;if(!doc.namespaces[jsc._vmlNS]){doc.namespaces.add(jsc._vmlNS,'urn:schemas-microsoft-com:vml')}if(!doc.styleSheets[jsc._vmlCSS]){var tags=['shape','shapetype','group','background','path','formulas','handles','fill','stroke','shadow','textbox','textpath','imagedata','line','polyline','curve','rect','roundrect','oval','arc','image'];var ss=doc.createStyleSheet();ss.owningElement.id=jsc._vmlCSS;for(var i=0;i<tags.length;i+=1){ss.addRule(jsc._vmlNS+'\\:'+tags[i],'behavior:url(#default#VML);')}}jsc._vmlReady=true}},createPalette:function(){var paletteObj={elm:null,draw:null};if(jsc.isCanvasSupported){var canvas=document.createElement('canvas');var ctx=canvas.getContext('2d');var drawFunc=function(width,height,type){canvas.width=width;canvas.height=height;ctx.clearRect(0,0,canvas.width,canvas.height);var hGrad=ctx.createLinearGradient(0,0,canvas.width,0);hGrad.addColorStop(0/6,'#F00');hGrad.addColorStop(1/6,'#FF0');hGrad.addColorStop(2/6,'#0F0');hGrad.addColorStop(3/6,'#0FF');hGrad.addColorStop(4/6,'#00F');hGrad.addColorStop(5/6,'#F0F');hGrad.addColorStop(6/6,'#F00');ctx.fillStyle=hGrad;ctx.fillRect(0,0,canvas.width,canvas.height);var vGrad=ctx.createLinearGradient(0,0,0,canvas.height);switch(type.toLowerCase()){case 's':vGrad.addColorStop(0,'rgba(255,255,255,0)');vGrad.addColorStop(1,'rgba(255,255,255,1)');break;case 'v':vGrad.addColorStop(0,'rgba(0,0,0,0)');vGrad.addColorStop(1,'rgba(0,0,0,1)');break}ctx.fillStyle=vGrad;ctx.fillRect(0,0,canvas.width,canvas.height)};paletteObj.elm=canvas;paletteObj.draw=drawFunc}else{jsc.initVML();var vmlContainer=document.createElement('div');vmlContainer.style.position='relative';vmlContainer.style.overflow='hidden';var hGrad=document.createElement(jsc._vmlNS+':fill');hGrad.type='gradient';hGrad.method='linear';hGrad.angle='90';hGrad.colors='16.67% #F0F, 33.33% #00F, 50% #0FF, 66.67% #0F0, 83.33% #FF0';var hRect=document.createElement(jsc._vmlNS+':rect');hRect.style.position='absolute';hRect.style.left=-1+'px';hRect.style.top=-1+'px';hRect.stroked=false;hRect.appendChild(hGrad);vmlContainer.appendChild(hRect);var vGrad=document.createElement(jsc._vmlNS+':fill');vGrad.type='gradient';vGrad.method='linear';vGrad.angle='180';vGrad.opacity='0';var vRect=document.createElement(jsc._vmlNS+':rect');vRect.style.position='absolute';vRect.style.left=-1+'px';vRect.style.top=-1+'px';vRect.stroked=false;vRect.appendChild(vGrad);vmlContainer.appendChild(vRect);var drawFunc=function(width,height,type){vmlContainer.style.width=width+'px';vmlContainer.style.height=height+'px';hRect.style.width=vRect.style.width=(width+1)+'px';hRect.style.height=vRect.style.height=(height+1)+'px';hGrad.color='#F00';hGrad.color2='#F00';switch(type.toLowerCase()){case 's':vGrad.color=vGrad.color2='#FFF';break;case 'v':vGrad.color=vGrad.color2='#000';break}};paletteObj.elm=vmlContainer;paletteObj.draw=drawFunc}return paletteObj},createSliderGradient:function(){var sliderObj={elm:null,draw:null};if(jsc.isCanvasSupported){var canvas=document.createElement('canvas');var ctx=canvas.getContext('2d');var drawFunc=function(width,height,color1,color2){canvas.width=width;canvas.height=height;ctx.clearRect(0,0,canvas.width,canvas.height);var grad=ctx.createLinearGradient(0,0,0,canvas.height);grad.addColorStop(0,color1);grad.addColorStop(1,color2);ctx.fillStyle=grad;ctx.fillRect(0,0,canvas.width,canvas.height)};sliderObj.elm=canvas;sliderObj.draw=drawFunc}else{jsc.initVML();var vmlContainer=document.createElement('div');vmlContainer.style.position='relative';vmlContainer.style.overflow='hidden';var grad=document.createElement(jsc._vmlNS+':fill');grad.type='gradient';grad.method='linear';grad.angle='180';var rect=document.createElement(jsc._vmlNS+':rect');rect.style.position='absolute';rect.style.left=-1+'px';rect.style.top=-1+'px';rect.stroked=false;rect.appendChild(grad);vmlContainer.appendChild(rect);var drawFunc=function(width,height,color1,color2){vmlContainer.style.width=width+'px';vmlContainer.style.height=height+'px';rect.style.width=(width+1)+'px';rect.style.height=(height+1)+'px';grad.color=color1;grad.color2=color2};sliderObj.elm=vmlContainer;sliderObj.draw=drawFunc}return sliderObj},leaveValue:1<<0,leaveStyle:1<<1,leavePad:1<<2,leaveSld:1<<3,BoxShadow:(function(){var BoxShadow=function(hShadow,vShadow,blur,spread,color,inset){this.hShadow=hShadow;this.vShadow=vShadow;this.blur=blur;this.spread=spread;this.color=color;this.inset=!!inset};BoxShadow.prototype.toString=function(){var vals=[Math.round(this.hShadow)+'px',Math.round(this.vShadow)+'px',Math.round(this.blur)+'px',Math.round(this.spread)+'px',this.color];if(this.inset){vals.push('inset')}return vals.join(' ')};return BoxShadow})(),jscolor:function(targetElement,options){this.value=null;this.valueElement=targetElement;this.styleElement=targetElement;this.required=true;this.refine=true;this.hash=false;this.uppercase=true;this.onFineChange=null;this.activeClass='jscolor-active';this.overwriteImportant=false;this.minS=0;this.maxS=100;this.minV=0;this.maxV=100;this.hsv=[0,0,100];this.rgb=[255,255,255];this.width=181;this.height=101;this.showOnClick=true;this.mode='HSV';this.position='bottom';this.smartPosition=true;this.sliderSize=16;this.crossSize=8;this.closable=false;this.closeText='Close';this.buttonColor='#000000';this.buttonHeight=18;this.padding=12;this.backgroundColor='#FFFFFF';this.borderWidth=1;this.borderColor='#BBBBBB';this.borderRadius=8;this.insetWidth=1;this.insetColor='#BBBBBB';this.shadow=true;this.shadowBlur=15;this.shadowColor='rgba(0,0,0,0.2)';this.pointerColor='#4C4C4C';this.pointerBorderColor='#FFFFFF';this.pointerBorderWidth=1;this.pointerThickness=2;this.zIndex=1000;this.container=null;for(var opt in options){if(options.hasOwnProperty(opt)){this[opt]=options[opt]}}this.hide=function(){if(isPickerOwner()){detachPicker()}};this.show=function(){drawPicker()};this.redraw=function(){if(isPickerOwner()){drawPicker()}};this.importColor=function(){if(!this.valueElement){this.exportColor()}else{if(jsc.isElementType(this.valueElement,'input')){if(!this.refine){if(!this.fromString(this.valueElement.value,jsc.leaveValue)){if(this.styleElement){this.styleElement.style.backgroundImage=this.styleElement._jscOrigStyle.backgroundImage;this.styleElement.style.backgroundColor=this.styleElement._jscOrigStyle.backgroundColor;this.styleElement.style.color=this.styleElement._jscOrigStyle.color}this.exportColor(jsc.leaveValue|jsc.leaveStyle)}}else if(!this.required&&/^\s*$/.test(this.valueElement.value)){this.valueElement.value='';if(this.styleElement){this.styleElement.style.backgroundImage=this.styleElement._jscOrigStyle.backgroundImage;this.styleElement.style.backgroundColor=this.styleElement._jscOrigStyle.backgroundColor;this.styleElement.style.color=this.styleElement._jscOrigStyle.color}this.exportColor(jsc.leaveValue|jsc.leaveStyle)}else if(this.fromString(this.valueElement.value)){}else{this.exportColor()}}else{this.exportColor()}}};this.exportColor=function(flags){if(!(flags&jsc.leaveValue)&&this.valueElement){var value=this.toString();if(this.uppercase){value=value.toUpperCase()}if(this.hash){value='#'+value}if(jsc.isElementType(this.valueElement,'input')){this.valueElement.value=value}else{this.valueElement.innerHTML=value}}if(!(flags&jsc.leaveStyle)){if(this.styleElement){var bgColor='#'+this.toString();var fgColor=this.isLight()?'#000':'#FFF';this.styleElement.style.backgroundImage='none';this.styleElement.style.backgroundColor=bgColor;this.styleElement.style.color=fgColor;if(this.overwriteImportant){this.styleElement.setAttribute('style','background: '+bgColor+' !important; color: '+fgColor+' !important;')}}}if(!(flags&jsc.leavePad)&&isPickerOwner()){redrawPad()}if(!(flags&jsc.leaveSld)&&isPickerOwner()){redrawSld()}};this.fromHSV=function(h,s,v,flags){if(h!==null){if(isNaN(h)){return false}h=Math.max(0,Math.min(360,h))}if(s!==null){if(isNaN(s)){return false}s=Math.max(0,Math.min(100,this.maxS,s),this.minS)}if(v!==null){if(isNaN(v)){return false}v=Math.max(0,Math.min(100,this.maxV,v),this.minV)}this.rgb=HSV_RGB(h===null?this.hsv[0]:(this.hsv[0]=h),s===null?this.hsv[1]:(this.hsv[1]=s),v===null?this.hsv[2]:(this.hsv[2]=v));this.exportColor(flags)};this.fromRGB=function(r,g,b,flags){if(r!==null){if(isNaN(r)){return false}r=Math.max(0,Math.min(255,r))}if(g!==null){if(isNaN(g)){return false}g=Math.max(0,Math.min(255,g))}if(b!==null){if(isNaN(b)){return false}b=Math.max(0,Math.min(255,b))}var hsv=RGB_HSV(r===null?this.rgb[0]:r,g===null?this.rgb[1]:g,b===null?this.rgb[2]:b);if(hsv[0]!==null){this.hsv[0]=Math.max(0,Math.min(360,hsv[0]))}if(hsv[2]!==0){this.hsv[1]=hsv[1]===null?null:Math.max(0,this.minS,Math.min(100,this.maxS,hsv[1]))}this.hsv[2]=hsv[2]===null?null:Math.max(0,this.minV,Math.min(100,this.maxV,hsv[2]));var rgb=HSV_RGB(this.hsv[0],this.hsv[1],this.hsv[2]);this.rgb[0]=rgb[0];this.rgb[1]=rgb[1];this.rgb[2]=rgb[2];this.exportColor(flags)};this.fromString=function(str,flags){var m;if(m=str.match(/^\W*([0-9A-F]{3}([0-9A-F]{3})?)\W*$/i)){if(m[1].length===6){this.fromRGB(parseInt(m[1].substr(0,2),16),parseInt(m[1].substr(2,2),16),parseInt(m[1].substr(4,2),16),flags)}else{this.fromRGB(parseInt(m[1].charAt(0)+m[1].charAt(0),16),parseInt(m[1].charAt(1)+m[1].charAt(1),16),parseInt(m[1].charAt(2)+m[1].charAt(2),16),flags)}return true}else if(m=str.match(/^\W*rgba?\(([^)]*)\)\W*$/i)){var params=m[1].split(',');var re=/^\s*(\d*)(\.\d+)?\s*$/;var mR,mG,mB;if(params.length>=3&&(mR=params[0].match(re))&&(mG=params[1].match(re))&&(mB=params[2].match(re))){var r=parseFloat((mR[1]||'0')+(mR[2]||''));var g=parseFloat((mG[1]||'0')+(mG[2]||''));var b=parseFloat((mB[1]||'0')+(mB[2]||''));this.fromRGB(r,g,b,flags);return true}}return false};this.toString=function(){return((0x100|Math.round(this.rgb[0])).toString(16).substr(1)+(0x100|Math.round(this.rgb[1])).toString(16).substr(1)+(0x100|Math.round(this.rgb[2])).toString(16).substr(1))};this.toHEXString=function(){return '#'+this.toString().toUpperCase()};this.toRGBString=function(){return('rgb('+Math.round(this.rgb[0])+','+Math.round(this.rgb[1])+','+Math.round(this.rgb[2])+')')};this.isLight=function(){return(0.213*this.rgb[0]+0.715*this.rgb[1]+0.072*this.rgb[2]>255/2)};this._processParentElementsInDOM=function(){if(this._linkedElementsProcessed){return}this._linkedElementsProcessed=true;var elm=this.targetElement;do{var currStyle=jsc.getStyle(elm);if(currStyle&&currStyle.position.toLowerCase()==='fixed'){this.fixed=true}if(elm!==this.targetElement){if(!elm._jscEventsAttached){jsc.attachEvent(elm,'scroll',jsc.onParentScroll);elm._jscEventsAttached=true}}}while((elm=elm.parentNode)&&!jsc.isElementType(elm,'body'))};function RGB_HSV(r,g,b){r/=255;g/=255;b/=255;var n=Math.min(Math.min(r,g),b);var v=Math.max(Math.max(r,g),b);var m=v-n;if(m===0){return[null,0,100*v]}var h=r===n?3+(b-g)/m:(g===n?5+(r-b)/m:1+(g-r)/m);return[60*(h===6?0:h),100*(m/v),100*v]}function HSV_RGB(h,s,v){var u=255*(v/100);if(h===null){return[u,u,u]}h/=60;s/=100;var i=Math.floor(h);var f=i%2?h-i:1-(h-i);var m=u*(1-s);var n=u*(1-s*f);switch(i){case 6:case 0:return[u,n,m];case 1:return[n,u,m];case 2:return[m,u,n];case 3:return[m,n,u];case 4:return[n,m,u];case 5:return[u,m,n]}}function detachPicker(){jsc.unsetClass(THIS.targetElement,THIS.activeClass);jsc.picker.wrap.parentNode.removeChild(jsc.picker.wrap);delete jsc.picker.owner}function drawPicker(){THIS._processParentElementsInDOM();if(!jsc.picker){jsc.picker={owner:null,wrap:document.createElement('div'),box:document.createElement('div'),boxS:document.createElement('div'),boxB:document.createElement('div'),pad:document.createElement('div'),padB:document.createElement('div'),padM:document.createElement('div'),padPal:jsc.createPalette(),cross:document.createElement('div'),crossBY:document.createElement('div'),crossBX:document.createElement('div'),crossLY:document.createElement('div'),crossLX:document.createElement('div'),sld:document.createElement('div'),sldB:document.createElement('div'),sldM:document.createElement('div'),sldGrad:jsc.createSliderGradient(),sldPtrS:document.createElement('div'),sldPtrIB:document.createElement('div'),sldPtrMB:document.createElement('div'),sldPtrOB:document.createElement('div'),btn:document.createElement('div'),btnT:document.createElement('span')};jsc.picker.pad.appendChild(jsc.picker.padPal.elm);jsc.picker.padB.appendChild(jsc.picker.pad);jsc.picker.cross.appendChild(jsc.picker.crossBY);jsc.picker.cross.appendChild(jsc.picker.crossBX);jsc.picker.cross.appendChild(jsc.picker.crossLY);jsc.picker.cross.appendChild(jsc.picker.crossLX);jsc.picker.padB.appendChild(jsc.picker.cross);jsc.picker.box.appendChild(jsc.picker.padB);jsc.picker.box.appendChild(jsc.picker.padM);jsc.picker.sld.appendChild(jsc.picker.sldGrad.elm);jsc.picker.sldB.appendChild(jsc.picker.sld);jsc.picker.sldB.appendChild(jsc.picker.sldPtrOB);jsc.picker.sldPtrOB.appendChild(jsc.picker.sldPtrMB);jsc.picker.sldPtrMB.appendChild(jsc.picker.sldPtrIB);jsc.picker.sldPtrIB.appendChild(jsc.picker.sldPtrS);jsc.picker.box.appendChild(jsc.picker.sldB);jsc.picker.box.appendChild(jsc.picker.sldM);jsc.picker.btn.appendChild(jsc.picker.btnT);jsc.picker.box.appendChild(jsc.picker.btn);jsc.picker.boxB.appendChild(jsc.picker.box);jsc.picker.wrap.appendChild(jsc.picker.boxS);jsc.picker.wrap.appendChild(jsc.picker.boxB)}var p=jsc.picker;var displaySlider=!!jsc.getSliderComponent(THIS);var dims=jsc.getPickerDims(THIS);var crossOuterSize=(2*THIS.pointerBorderWidth+THIS.pointerThickness+2*THIS.crossSize);var padToSliderPadding=jsc.getPadToSliderPadding(THIS);var borderRadius=Math.min(THIS.borderRadius,Math.round(THIS.padding*Math.PI));var padCursor='crosshair';p.wrap.style.clear='both';p.wrap.style.width=(dims[0]+2*THIS.borderWidth)+'px';p.wrap.style.height=(dims[1]+2*THIS.borderWidth)+'px';p.wrap.style.zIndex=THIS.zIndex;p.box.style.width=dims[0]+'px';p.box.style.height=dims[1]+'px';p.boxS.style.position='absolute';p.boxS.style.left='0';p.boxS.style.top='0';p.boxS.style.width='100%';p.boxS.style.height='100%';jsc.setBorderRadius(p.boxS,borderRadius+'px');p.boxB.style.position='relative';p.boxB.style.border=THIS.borderWidth+'px solid';p.boxB.style.borderColor=THIS.borderColor;p.boxB.style.background=THIS.backgroundColor;jsc.setBorderRadius(p.boxB,borderRadius+'px');p.padM.style.background=p.sldM.style.background='#FFF';jsc.setStyle(p.padM,'opacity','0');jsc.setStyle(p.sldM,'opacity','0');p.pad.style.position='relative';p.pad.style.width=THIS.width+'px';p.pad.style.height=THIS.height+'px';p.padPal.draw(THIS.width,THIS.height,jsc.getPadYComponent(THIS));p.padB.style.position='absolute';p.padB.style.left=THIS.padding+'px';p.padB.style.top=THIS.padding+'px';p.padB.style.border=THIS.insetWidth+'px solid';p.padB.style.borderColor=THIS.insetColor;p.padM._jscInstance=THIS;p.padM._jscControlName='pad';p.padM.style.position='absolute';p.padM.style.left='0';p.padM.style.top='0';p.padM.style.width=(THIS.padding+2*THIS.insetWidth+THIS.width+padToSliderPadding/2)+'px';p.padM.style.height=dims[1]+'px';p.padM.style.cursor=padCursor;p.cross.style.position='absolute';p.cross.style.left=p.cross.style.top='0';p.cross.style.width=p.cross.style.height=crossOuterSize+'px';p.crossBY.style.position=p.crossBX.style.position='absolute';p.crossBY.style.background=p.crossBX.style.background=THIS.pointerBorderColor;p.crossBY.style.width=p.crossBX.style.height=(2*THIS.pointerBorderWidth+THIS.pointerThickness)+'px';p.crossBY.style.height=p.crossBX.style.width=crossOuterSize+'px';p.crossBY.style.left=p.crossBX.style.top=(Math.floor(crossOuterSize/2)-Math.floor(THIS.pointerThickness/2)-THIS.pointerBorderWidth)+'px';p.crossBY.style.top=p.crossBX.style.left='0';p.crossLY.style.position=p.crossLX.style.position='absolute';p.crossLY.style.background=p.crossLX.style.background=THIS.pointerColor;p.crossLY.style.height=p.crossLX.style.width=(crossOuterSize-2*THIS.pointerBorderWidth)+'px';p.crossLY.style.width=p.crossLX.style.height=THIS.pointerThickness+'px';p.crossLY.style.left=p.crossLX.style.top=(Math.floor(crossOuterSize/2)-Math.floor(THIS.pointerThickness/2))+'px';p.crossLY.style.top=p.crossLX.style.left=THIS.pointerBorderWidth+'px';p.sld.style.overflow='hidden';p.sld.style.width=THIS.sliderSize+'px';p.sld.style.height=THIS.height+'px';p.sldGrad.draw(THIS.sliderSize,THIS.height,'#000','#000');p.sldB.style.display=displaySlider?'block':'none';p.sldB.style.position='absolute';p.sldB.style.right=THIS.padding+'px';p.sldB.style.top=THIS.padding+'px';p.sldB.style.border=THIS.insetWidth+'px solid';p.sldB.style.borderColor=THIS.insetColor;p.sldM._jscInstance=THIS;p.sldM._jscControlName='sld';p.sldM.style.display=displaySlider?'block':'none';p.sldM.style.position='absolute';p.sldM.style.right='0';p.sldM.style.top='0';p.sldM.style.width=(THIS.sliderSize+padToSliderPadding/2+THIS.padding+2*THIS.insetWidth)+'px';p.sldM.style.height=dims[1]+'px';p.sldM.style.cursor='default';p.sldPtrIB.style.border=p.sldPtrOB.style.border=THIS.pointerBorderWidth+'px solid '+THIS.pointerBorderColor;p.sldPtrOB.style.position='absolute';p.sldPtrOB.style.left= -(2*THIS.pointerBorderWidth+THIS.pointerThickness)+'px';p.sldPtrOB.style.top='0';p.sldPtrMB.style.border=THIS.pointerThickness+'px solid '+THIS.pointerColor;p.sldPtrS.style.width=THIS.sliderSize+'px';p.sldPtrS.style.height=sliderPtrSpace+'px';function setBtnBorder(){var insetColors=THIS.insetColor.split(/\s+/);var outsetColor=insetColors.length<2?insetColors[0]:insetColors[1]+' '+insetColors[0]+' '+insetColors[0]+' '+insetColors[1];p.btn.style.borderColor=outsetColor}p.btn.style.display=THIS.closable?'block':'none';p.btn.style.position='absolute';p.btn.style.left=THIS.padding+'px';p.btn.style.bottom=THIS.padding+'px';p.btn.style.padding='0 15px';p.btn.style.height=THIS.buttonHeight+'px';p.btn.style.border=THIS.insetWidth+'px solid';setBtnBorder();p.btn.style.color=THIS.buttonColor;p.btn.style.font='12px sans-serif';p.btn.style.textAlign='center';try{p.btn.style.cursor='pointer'}catch(eOldIE){p.btn.style.cursor='hand'}p.btn.onmousedown=function(){THIS.hide()};p.btnT.style.lineHeight=THIS.buttonHeight+'px';p.btnT.innerHTML='';p.btnT.appendChild(document.createTextNode(THIS.closeText));redrawPad();redrawSld();if(jsc.picker.owner&&jsc.picker.owner!==THIS){jsc.unsetClass(jsc.picker.owner.targetElement,THIS.activeClass)}jsc.picker.owner=THIS;if(jsc.isElementType(container,'body')){jsc.redrawPosition()}else{jsc._drawPosition(THIS,0,0,'relative',false)}if(p.wrap.parentNode!=container){container.appendChild(p.wrap)}jsc.setClass(THIS.targetElement,THIS.activeClass)}function redrawPad(){switch(jsc.getPadYComponent(THIS)){case 's':var yComponent=1;break;case 'v':var yComponent=2;break}var x=Math.round((THIS.hsv[0]/360)*(THIS.width-1));var y=Math.round((1-THIS.hsv[yComponent]/100)*(THIS.height-1));var crossOuterSize=(2*THIS.pointerBorderWidth+THIS.pointerThickness+2*THIS.crossSize);var ofs= -Math.floor(crossOuterSize/2);jsc.picker.cross.style.left=(x+ofs)+'px';jsc.picker.cross.style.top=(y+ofs)+'px';switch(jsc.getSliderComponent(THIS)){case 's':var rgb1=HSV_RGB(THIS.hsv[0],100,THIS.hsv[2]);var rgb2=HSV_RGB(THIS.hsv[0],0,THIS.hsv[2]);var color1='rgb('+Math.round(rgb1[0])+','+Math.round(rgb1[1])+','+Math.round(rgb1[2])+')';var color2='rgb('+Math.round(rgb2[0])+','+Math.round(rgb2[1])+','+Math.round(rgb2[2])+')';jsc.picker.sldGrad.draw(THIS.sliderSize,THIS.height,color1,color2);break;case 'v':var rgb=HSV_RGB(THIS.hsv[0],THIS.hsv[1],100);var color1='rgb('+Math.round(rgb[0])+','+Math.round(rgb[1])+','+Math.round(rgb[2])+')';var color2='#000';jsc.picker.sldGrad.draw(THIS.sliderSize,THIS.height,color1,color2);break}}function redrawSld(){var sldComponent=jsc.getSliderComponent(THIS);if(sldComponent){switch(sldComponent){case 's':var yComponent=1;break;case 'v':var yComponent=2;break}var y=Math.round((1-THIS.hsv[yComponent]/100)*(THIS.height-1));jsc.picker.sldPtrOB.style.top=(y-(2*THIS.pointerBorderWidth+THIS.pointerThickness)-Math.floor(sliderPtrSpace/2))+'px'}}function isPickerOwner(){return jsc.picker&&jsc.picker.owner===THIS}function blurValue(){THIS.importColor()}if(typeof targetElement==='string'){var id=targetElement;var elm=document.getElementById(id);if(elm){this.targetElement=elm}else{jsc.warn('Could not find target element with ID \''+id+'\'')}}else if(targetElement){this.targetElement=targetElement}else{jsc.warn('Invalid target element: \''+targetElement+'\'')}if(this.targetElement._jscLinkedInstance){jsc.warn('Cannot link jscolor twice to the same element. Skipping.');return}this.targetElement._jscLinkedInstance=this;this.valueElement=jsc.fetchElement(this.valueElement);this.styleElement=jsc.fetchElement(this.styleElement);var THIS=this;var container=this.container?jsc.fetchElement(this.container):document.getElementsByTagName('body')[0];var sliderPtrSpace=3;if(jsc.isElementType(this.targetElement,'button')){if(this.targetElement.onclick){var origCallback=this.targetElement.onclick;this.targetElement.onclick=function(evt){origCallback.call(this,evt);return false}}else{this.targetElement.onclick=function(){return false}}}if(this.valueElement){if(jsc.isElementType(this.valueElement,'input')){var updateField=function(){THIS.fromString(THIS.valueElement.value,jsc.leaveValue);jsc.dispatchFineChange(THIS)};jsc.attachEvent(this.valueElement,'keyup',updateField);jsc.attachEvent(this.valueElement,'input',updateField);jsc.attachEvent(this.valueElement,'blur',blurValue);this.valueElement.setAttribute('autocomplete','off')}}if(this.styleElement){this.styleElement._jscOrigStyle={backgroundImage:this.styleElement.style.backgroundImage,backgroundColor:this.styleElement.style.backgroundColor,color:this.styleElement.style.color}}if(this.value){this.fromString(this.value)||this.exportColor()}else{this.importColor()}}};jsc.jscolor.lookupClass='jscolor';jsc.jscolor.installByClassName=function(className){var inputElms=document.getElementsByTagName('input');var buttonElms=document.getElementsByTagName('button');jsc.tryInstallOnElements(inputElms,className);jsc.tryInstallOnElements(buttonElms,className)};jsc.register();return jsc.jscolor})()} \ No newline at end of file diff --git a/static/js/autocomplete_model.js b/static/js/autocomplete_model.js index 8c3f6f0915b963853219b9a21e8ca81a87cb08d7..aa1d220cceb72b4c12e5e3a6042212194c1a89c6 100644 --- a/static/js/autocomplete_model.js +++ b/static/js/autocomplete_model.js @@ -11,7 +11,7 @@ $(document).ready(function () { name_field = "name"; let input = target.val(); - $.getJSON(api_url + "?format=json&search=^" + input + api_url_suffix, function(objects) { + $.getJSON(api_url + (api_url.includes("?") ? "&" : "?") + "format=json&search=^" + input + api_url_suffix, function(objects) { let html = ""; objects.results.forEach(function (obj) { diff --git a/static/js/base.js b/static/js/base.js index bb73b328ca7503048d188e52064195d0fecf07f4..b04f71cb0d47151ecc466559baf111675f89ad8a 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -21,7 +21,7 @@ function pretty_money(value) { * @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, timeout=-1) { +function addMsg(msg, alert_type, timeout = -1) { let msgDiv = $("#messages"); let html = msgDiv.html(); let id = Math.floor(10000 * Math.random() + 1); @@ -42,28 +42,28 @@ function addMsg(msg, alert_type, timeout=-1) { * @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) { +function errMsg(errs_obj, timeout = -1) { for (const err_msg of Object.values(errs_obj)) { - addMsg(err_msg,'danger', timeout); - } + addMsg(err_msg, 'danger', timeout); + } } var reloadWithTurbolinks = (function () { - var scrollPosition; - - function reload () { - scrollPosition = [window.scrollX, window.scrollY]; - Turbolinks.visit(window.location.toString(), { action: 'replace' }) - } + var scrollPosition; - document.addEventListener('turbolinks:load', function () { - if (scrollPosition) { - window.scrollTo.apply(window, scrollPosition); - scrollPosition = null + function reload() { + scrollPosition = [window.scrollX, window.scrollY]; + Turbolinks.visit(window.location.toString(), {action: 'replace'}) } - }); - return reload; + document.addEventListener('turbolinks:load', function () { + if (scrollPosition) { + window.scrollTo.apply(window, scrollPosition); + scrollPosition = null + } + }); + + return reload; })(); /** @@ -79,17 +79,36 @@ function refreshBalance() { * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. */ function getMatchedNotes(pattern, fun) { - $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club|activity&ordering=normalized_name", fun); + $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun); } /** * Generate a <li> entry with a given id and text */ -function li(id, text) { - return "<li class=\"list-group-item py-1 d-flex justify-content-between align-items-center\"" + - " id=\"" + id + "\">" + text + "</li>\n"; +function li(id, text, extra_css) { + return "<li class=\"list-group-item py-1 px-2 d-flex justify-content-between align-items-center text-truncate " + extra_css + "\"" + + " id=\"" + id + "\">" + text + "</li>\n"; } +/** + * Return style to apply according to the balance of the note and the validation status of the email address + * @param note The concerned note. + */ +function displayStyle(note) { + let balance = note.balance; + var css = ""; + if (balance < -5000) + css += " text-danger bg-dark"; + else if (balance < -1000) + css += " text-danger"; + else if (balance < 0) + css += " text-warning"; + if (!note.email_confirmed) + css += " text-white bg-primary"; + return css; +} + + /** * Render note name and picture * @param note The note to render @@ -97,27 +116,29 @@ function li(id, text) { * @param user_note_field * @param profile_pic_field */ -function displayNote(note, alias, user_note_field=null, profile_pic_field=null) { +function displayNote(note, alias, user_note_field = null, profile_pic_field = null) { if (!note.display_image) { note.display_image = '/media/pic/default.png'; - $.getJSON("/api/note/note/" + note.id + "/?format=json", function(new_note) { - note.display_image = new_note.display_image.replace("http:", "https:"); - note.name = new_note.name; - note.balance = new_note.balance; - note.user = new_note.user; - - displayNote(note, alias, user_note_field, profile_pic_field); - }); - return; } - let img = note.display_image; - if (alias !== note.name) + if (alias !== note.name && note.name) alias += " (aka. " + note.name + ")"; - if (user_note_field !== null) - $("#" + user_note_field).text(alias + (note.balance == null ? "" : (" : " + pretty_money(note.balance)))); - if (profile_pic_field != null) - $("#" + profile_pic_field).attr('src', img); + if (user_note_field !== null) { + $("#" + user_note_field).removeAttr('class'); + $("#" + user_note_field).addClass(displayStyle(note)); + $("#" + user_note_field).text(alias + (note.balance == null ? "" : (" :\n" + pretty_money(note.balance)))); + if (profile_pic_field != null) { + $("#" + profile_pic_field).attr('src', img); + $("#" + profile_pic_field).click(function () { + console.log(note); + if (note.resourcetype === "NoteUser") { + document.location.href = "/accounts/user/" + note.user; + } else if (note.resourcetype === "NoteClub") { + document.location.href = "/accounts/club/" + note.club; + } + }); + } + } } /** @@ -132,8 +153,8 @@ function displayNote(note, alias, user_note_field=null, profile_pic_field=null) * (useful in consumptions, put null if not used) * @returns an anonymous function to be compatible with jQuery events */ -function removeNote(d, note_prefix="note", notes_display, note_list_id, user_note_field=null, profile_pic_field=null) { - return (function() { +function removeNote(d, note_prefix = "note", notes_display, note_list_id, user_note_field = null, profile_pic_field = null) { + return (function () { let new_notes_display = []; let html = ""; notes_display.forEach(function (disp) { @@ -141,12 +162,13 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not disp.quantity -= disp.id === d.id ? 1 : 0; new_notes_display.push(disp); html += li(note_prefix + "_" + disp.id, disp.name - + "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>"); + + "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>", + displayStyle(disp.note)); } }); notes_display.length = 0; - new_notes_display.forEach(function(disp) { + new_notes_display.forEach(function (disp) { notes_display.push(disp); }); @@ -154,7 +176,7 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not notes_display.forEach(function (disp) { let obj = $("#" + note_prefix + "_" + disp.id); obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, profile_pic_field)); - obj.hover(function() { + obj.hover(function () { if (disp.note) displayNote(disp.note, disp.name, user_note_field, profile_pic_field); }); @@ -165,7 +187,6 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not /** * Generate an auto-complete field to query a note with its alias * @param field_id The identifier of the text field where the alias is typed - * @param alias_matched_id The div block identifier where the matched aliases are displayed * @param note_list_id The div block identifier where the notes of the buyers are displayed * @param notes An array containing the note objects of the buyers * @param notes_display An array containing the infos of the buyers: [alias, note id, note object, quantity] @@ -179,143 +200,145 @@ function removeNote(d, note_prefix="note", notes_display, note_list_id, user_not * the associated note is not displayed. * Useful for a consumption if the item is selected before. */ -function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes_display, alias_prefix="alias", - note_prefix="note", user_note_field=null, profile_pic_field=null, alias_click=null) { +function autoCompleteNote(field_id, note_list_id, notes, notes_display, alias_prefix = "alias", + note_prefix = "note", user_note_field = null, profile_pic_field = null, alias_click = null) { let field = $("#" + field_id); - // When the user clicks on the search field, it is immediately cleared - field.click(function() { + + // Configure tooltip + field.tooltip({ + html: true, + placement: 'bottom', + title: 'Loading...', + trigger: 'manual', + container: field.parent() + }); + + // Clear search on click + field.click(function () { + field.tooltip('hide'); field.val(""); }); let old_pattern = null; - // When the user type "Enter", the first alias is clicked, and the informations are displayed - field.keypress(function(event) { - if (event.originalEvent.charCode === 13) { - let li_obj = $("#" + alias_matched_id + " li").first(); + // When the user type "Enter", the first alias is clicked + field.keypress(function (event) { + if (event.originalEvent.charCode === 13 && notes.length > 0) { + let li_obj = field.parent().find("ul li").first(); displayNote(notes[0], li_obj.text(), user_note_field, profile_pic_field); li_obj.trigger("click"); } }); // When the user type something, the matched aliases are refreshed - field.keyup(function(e) { + field.keyup(function (e) { if (e.originalEvent.charCode === 13) return; let pattern = field.val(); + // If the pattern is not modified, we don't query the API - if (pattern === old_pattern || pattern === "") + if (pattern === old_pattern) return; - old_pattern = pattern; - - // Clear old matched notes notes.length = 0; - let aliases_matched_obj = $("#" + alias_matched_id); - let aliases_matched_html = ""; - - // Get matched notes with the given pattern - getMatchedNotes(pattern, function(aliases) { - // The response arrived too late, we stop the request - if (pattern !== $("#" + field_id).val()) - return; - - aliases.results.forEach(function (alias) { - let note = alias.note; - note = { - id: note, - name: alias.name, - alias: alias, - balance: null - }; - aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name); - notes.push(note); - }); - - // Display the list of matched aliases - aliases_matched_obj.html(aliases_matched_html); + // get matched Alias with note associated + if (pattern === "") { + field.tooltip('hide'); + notes.length = 0; + return; + } - notes.forEach(function (note) { - let alias = note.alias; - let alias_obj = $("#" + alias_prefix + "_" + alias.id); - // When an alias is hovered, the profile picture and the balance are displayed at the right place - alias_obj.hover(function () { - displayNote(note, alias.name, user_note_field, profile_pic_field); + $.getJSON("/api/note/consumer/?format=json&alias=" + + pattern + + "&search=user|club&ordering=normalized_name", + function (consumers) { + // The response arrived too late, we stop the request + if (pattern !== $("#" + field_id).val()) + return; + + // Build tooltip content + let aliases_matched_html = '<ul class="list-group list-group-flush">'; + consumers.results.forEach(function (consumer) { + let note = consumer.note; + note.email_confirmed = consumer.email_confirmed; + let extra_css = displayStyle(note); + aliases_matched_html += li(alias_prefix + '_' + consumer.id, + consumer.name, + extra_css); + notes.push(note); }); + aliases_matched_html += '</ul>'; - // When the user click on an alias, the associated note is added to the emitters - alias_obj.click(function () { - field.val(""); - old_pattern = ""; - // If the note is already an emitter, we increase the quantity - var disp = null; - notes_display.forEach(function (d) { - // We compare the note ids - if (d.id === note.id) { - d.quantity += 1; - disp = d; - } - }); - // In the other case, we add a new emitter - if (disp == null) { - disp = { - name: alias.name, - id: note.id, - note: note, - quantity: 1 - }; - notes_display.push(disp); - } - - // If the function alias_click exists, it is called. If it doesn't return true, then the notes are - // note displayed. Useful for a consumption when a button is already clicked - if (alias_click && !alias_click()) - return; - - let note_list = $("#" + note_list_id); - let html = ""; - notes_display.forEach(function (disp) { - html += li(note_prefix + "_" + disp.id, disp.name - + "<span class=\"badge badge-dark badge-pill\">" + disp.quantity + "</span>"); - }); + // Show tooltip + field.attr('data-original-title', aliases_matched_html).tooltip('show'); - // Emitters are displayed - note_list.html(html); + consumers.results.forEach(function (consumer) { + let note = consumer.note; + let consumer_obj = $("#" + alias_prefix + "_" + consumer.id); + consumer_obj.hover(function () { + displayNote(consumer.note, consumer.name, user_note_field, profile_pic_field) + }); + consumer_obj.click(function () { + var disp = null; + notes_display.forEach(function (d) { + // We compare the note ids + if (d.id === note.id) { + d.quantity += 1; + disp = d; + } + }); + // In the other case, we add a new emitter + if (disp == null) { + disp = { + name: consumer.name, + id: consumer.id, + note: note, + quantity: 1 + }; + notes_display.push(disp); + } - notes_display.forEach(function (disp) { - let line_obj = $("#" + note_prefix + "_" + disp.id); - // Hover an emitter display also the profile picture - line_obj.hover(function () { - displayNote(disp.note, disp.name, user_note_field, profile_pic_field); + // If the function alias_click exists, it is called. If it doesn't return true, then the notes are + // note displayed. Useful for a consumption when a button is already clicked + if (alias_click && !alias_click()) + return; + + let note_list = $("#" + note_list_id); + let html = ""; + notes_display.forEach(function (disp) { + html += li(note_prefix + "_" + disp.id, + disp.name + + "<span class=\"badge badge-dark badge-pill\">" + + disp.quantity + "</span>", + displayStyle(disp.note)); }); - // When an emitter is clicked, it is removed - line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, - profile_pic_field)); - }); - }); - }); - }); - }); -} + // Emitters are displayed + note_list.html(html); -// When a validate button is clicked, we switch the validation status -function in_validate(id, validated) { + // Update tooltip position + field.tooltip('update'); - let invalidity_reason; - let reason_obj = $("#invalidity_reason_" + id); + notes_display.forEach(function (disp) { + let line_obj = $("#" + note_prefix + "_" + disp.id); + // Hover an emitter display also the profile picture + line_obj.hover(function () { + displayNote(disp.note, disp.name, user_note_field, profile_pic_field); + }); - if (validated) - invalidity_reason = reason_obj.val(); - else - invalidity_reason = null; + // When an emitter is clicked, it is removed + line_obj.click(removeNote(disp, note_prefix, notes_display, note_list_id, user_note_field, + profile_pic_field)); + }); + }) + }); - $("#validate_" + id).html("<strong style=\"font-size: 16pt;\">⟳ ...</strong>"); + $("#validate_" + id).html("<i class='fa fa-spinner'></i>"); // Perform a PATCH request to the API in order to update the transaction - // If the user has insuffisent rights, an error message will appear + // If the user has insufficient rights, an error message will appear $.ajax({ "url": "/api/note/transaction/transaction/" + id + "/", type: "PATCH", @@ -324,19 +347,19 @@ function in_validate(id, validated) { "X-CSRFTOKEN": CSRF_TOKEN }, data: { - resourcetype: "RecurrentTransaction", - valid: !validated, - invalidity_reason: invalidity_reason, + "resourcetype": "RecurrentTransaction", + "valid": !validated, + "invalidity_reason": invalidity_reason, }, success: function () { // Refresh jQuery objects - $(".validate").click(in_validate); + $(".validate").click(de_validate); refreshBalance(); // error if this method doesn't exist. Please define it. refreshHistory(); }, - error: function(err) { + error: function (err) { addMsg("Une erreur est survenue lors de la validation/dévalidation " + "de cette transaction : " + err.responseText, "danger"); diff --git a/static/js/consos.js b/static/js/consos.js index 20859933431612d6db881f24fb1d6ce42bf61a72..2fa772495a9c6c40895bab16bad27e34896a76b7 100644 --- a/static/js/consos.js +++ b/static/js/consos.js @@ -24,13 +24,10 @@ $(document).ready(function() { }); // Switching in double consumptions mode should update the layout - let double_conso_obj = $("#double_conso"); - double_conso_obj.click(function() { - $("#consos_list_div").show(); - $("#infos_div").attr('class', 'col-sm-5 col-xl-6'); - $("#note_infos_div").attr('class', 'col-xl-3'); + $("#double_conso").click(function() { + $("#consos_list_div").removeClass('d-none'); $("#user_select_div").attr('class', 'col-xl-4'); - $("#buttons_div").attr('class', 'col-sm-7 col-xl-6'); + $("#infos_div").attr('class', 'col-sm-5 col-xl-6'); let note_list_obj = $("#note_list"); if (buttons.length > 0 && note_list_obj.text().length > 0) { @@ -44,13 +41,10 @@ $(document).ready(function() { } }); - let single_conso_obj = $("#single_conso"); - single_conso_obj.click(function() { - $("#consos_list_div").hide(); - $("#infos_div").attr('class', 'col-sm-5 col-md-4'); - $("#note_infos_div").attr('class', 'col-xl-5'); + $("#single_conso").click(function() { + $("#consos_list_div").addClass('d-none'); $("#user_select_div").attr('class', 'col-xl-7'); - $("#buttons_div").attr('class', 'col-sm-7 col-md-8'); + $("#infos_div").attr('class', 'col-sm-5 col-md-4'); let consos_list_obj = $("#consos_list"); if (buttons.length > 0) { @@ -69,12 +63,8 @@ $(document).ready(function() { } }); - // Ensure we begin in single consumption. Removing these lines may cause problems when reloading. - single_conso_obj.prop('checked', 'true'); - double_conso_obj.removeAttr('checked'); - $("label[for='double_conso']").attr('class', 'btn btn-sm btn-outline-primary'); - - $("#consos_list_div").hide(); + // Ensure we begin in single consumption. Fix issue with TurboLinks and BootstrapJS + $("label[for='double_conso']").removeClass('active'); $("#consume_all").click(consumeAll); }); @@ -84,7 +74,7 @@ notes_display = []; buttons = []; // When the user searches an alias, we update the auto-completion -autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display, +autoCompleteNote("note", "note_list", notes, notes_display, "alias", "note", "user_note", "profile_pic", function() { if (buttons.length > 0 && $("#single_conso").is(":checked")) { consumeAll(); @@ -152,7 +142,6 @@ function reset() { notes.length = 0; buttons.length = 0; $("#note_list").html(""); - $("#alias_matched").html(""); $("#consos_list").html(""); $("#user_note").text(""); $("#profile_pic").attr("src", "/media/pic/default.png"); @@ -167,7 +156,7 @@ function reset() { function consumeAll() { notes_display.forEach(function(note_display) { buttons.forEach(function(button) { - consume(note_display.id, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, + consume(note_display.note.id, note_display.name, button.dest, button.quantity * note_display.quantity, button.amount, button.name + " (" + button.category_name + ")", button.type, button.category_id, button.id); }); }); diff --git a/static/js/transfer.js b/static/js/transfer.js index e5aafc3903864e3ecf256cb6ed4c0f601c2ad581..78a8ca088f42a5d4d4c44da99c769174f46a0b39 100644 --- a/static/js/transfer.js +++ b/static/js/transfer.js @@ -14,8 +14,6 @@ function reset() { dests.length = 0; $("#source_note_list").html(""); $("#dest_note_list").html(""); - $("#source_alias_matched").html(""); - $("#dest_alias_matched").html(""); $("#amount").val(""); $("#reason").val(""); $("#last_name").val(""); @@ -28,37 +26,110 @@ function reset() { } $(document).ready(function() { - autoCompleteNote("source_note", "source_alias_matched", "source_note_list", sources, sources_notes_display, - "source_alias", "source_note", "user_note", "profile_pic"); - autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display, - "dest_alias", "dest_note", "user_note", "profile_pic", function() { - if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) { - let last = dests_notes_display[dests_notes_display.length - 1]; - dests_notes_display.length = 0; - dests_notes_display.push(last); - - last.quantity = 1; - - if (!last.note.user) { - $.getJSON("/api/note/note/" + last.note.id + "/?format=json", function(note) { - last.note.user = note.user; - $.getJSON("/api/user/" + last.note.user + "/", function(user) { - $("#last_name").val(user.last_name); - $("#first_name").val(user.first_name); - }); - }); - } - else { + /** + * If we are in credit/debit mode, check that only one note is entered. + * More over, get first name and last name to autocomplete fields. + */ + function checkUniqueNote() { + if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) { + let arr = $("#type_credit").is(":checked") ? dests_notes_display : sources_notes_display; + + if (arr.length === 0) + return; + + let last = arr[arr.length - 1]; + arr.length = 0; + arr.push(last); + + last.quantity = 1; + + if (!last.note.user) { + $.getJSON("/api/note/note/" + last.note.id + "/?format=json", function(note) { + last.note.user = note.user; $.getJSON("/api/user/" + last.note.user + "/", function(user) { $("#last_name").val(user.last_name); $("#first_name").val(user.first_name); }); - } + }); } + else { + $.getJSON("/api/user/" + last.note.user + "/", function(user) { + $("#last_name").val(user.last_name); + $("#first_name").val(user.first_name); + }); + } + } + + return true; + } + + autoCompleteNote("source_note", "source_note_list", sources, sources_notes_display, + "source_alias", "source_note", "user_note", "profile_pic", checkUniqueNote); + autoCompleteNote("dest_note", "dest_note_list", dests, dests_notes_display, + "dest_alias", "dest_note", "user_note", "profile_pic", checkUniqueNote); - return true; - }); + let source = $("#source_note"); + let dest = $("#dest_note"); + $("#type_gift").click(function() { + $("#special_transaction_div").addClass('d-none'); + source.attr('disabled', true); + source.val(username); + source.tooltip('hide'); + $("#source_note_list").addClass('d-none'); + dest.attr('disabled', false); + $("#dest_note_list").removeClass('d-none'); + }); + + $("#type_transfer").click(function() { + $("#special_transaction_div").addClass('d-none'); + source.attr('disabled', false); + $("#source_note_list").removeClass('d-none'); + dest.attr('disabled', false); + $("#dest_note_list").removeClass('d-none'); + }); + + $("#type_credit").click(function() { + $("#special_transaction_div").removeClass('d-none'); + $("#source_note_list").addClass('d-none'); + $("#dest_note_list").removeClass('d-none'); + source.attr('disabled', true); + source.val($("#credit_type option:selected").text()); + source.tooltip('hide'); + dest.attr('disabled', false); + dest.val(''); + dest.tooltip('hide'); + + if (dests_notes_display.length > 1) { + $("#dest_note_list").html(''); + dests_notes_display.length = 0; + } + }); + + $("#type_debit").click(function() { + $("#special_transaction_div").removeClass('d-none'); + $("#source_note_list").removeClass('d-none'); + $("#dest_note_list").addClass('d-none'); + source.attr('disabled', false); + source.val(''); + source.tooltip('hide'); + dest.attr('disabled', true); + dest.val($("#credit_type option:selected").text()); + dest.tooltip('hide'); + + if (sources_notes_display.length > 1) { + $("#source_note_list").html(''); + sources_notes_display.length = 0; + } + }); + + $("#credit_type").change(function() { + let type = $("#credit_type option:selected").text(); + if ($("#type_credit").is(":checked")) + source.val(type); + else + dest.val(type); + }); // Ensure we begin in gift mode. Removing these lines may cause problems when reloading. let type_gift = $("#type_gift"); // Default mode @@ -91,7 +162,7 @@ $("#btn_transfer").click(function() { "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "Transaction", "source": user_id, - "destination": dest.id, + "destination": dest.note.id, "destination_alias": dest.name }).done(function () { addMsg("Le transfert de " @@ -99,7 +170,7 @@ $("#btn_transfer").click(function() { + " vers la note " + dest.name + " a été fait avec succès !", "success"); reset(); - }).fail(function () { + }).fail(function () { // do it again but valid = false $.post("/api/note/transaction/transaction/", { "csrfmiddlewaretoken": CSRF_TOKEN, @@ -111,7 +182,7 @@ $("#btn_transfer").click(function() { "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "Transaction", "source": user_id, - "destination": dest.id, + "destination": dest.note.id, "destination_alias": dest.name }).done(function () { addMsg("Le transfert de " @@ -141,9 +212,9 @@ $("#btn_transfer").click(function() { "valid": true, "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "Transaction", - "source": source.id, + "source": source.note.id, "source_alias": source.name, - "destination": dest.id, + "destination": dest.note.id, "destination_alias": dest.name }).done(function () { addMsg("Le transfert de " @@ -151,7 +222,7 @@ $("#btn_transfer").click(function() { + " vers la note " + dest.name + " a été fait avec succès !", "success"); reset(); - }).fail(function (err) { + }).fail(function (err) { // do it again but valid = false $.post("/api/note/transaction/transaction/", { "csrfmiddlewaretoken": CSRF_TOKEN, @@ -162,9 +233,9 @@ $("#btn_transfer").click(function() { "invalidity_reason": "Solde insuffisant", "polymorphic_ctype": TRANSFER_POLYMORPHIC_CTYPE, "resourcetype": "Transaction", - "source": source.id, + "source": source.note.id, "source_alias": source.name, - "destination": dest.id, + "destination": dest.note.id, "destination_alias": dest.name }).done(function () { addMsg("Le transfert de " @@ -184,10 +255,11 @@ $("#btn_transfer").click(function() { }); } else if ($("#type_credit").is(':checked') || $("#type_debit").is(':checked')) { let special_note = $("#credit_type").val(); - let user_note = dests_notes_display[0].id; + let user_note; let given_reason = $("#reason").val(); let source, dest, reason; if ($("#type_credit").is(':checked')) { + user_note = dests_notes_display[0].note.id; source = special_note; dest = user_note; reason = "Crédit " + $("#credit_type option:selected").text().toLowerCase(); @@ -195,6 +267,7 @@ $("#btn_transfer").click(function() { reason += " (" + given_reason + ")"; } else { + user_note = sources_notes_display[0].note.id; source = user_note; dest = special_note; reason = "Retrait " + $("#credit_type option:selected").text().toLowerCase(); @@ -225,4 +298,4 @@ $("#btn_transfer").click(function() { reset(); }); } -}); \ No newline at end of file +}); diff --git a/templates/400.html b/templates/400.html new file mode 100644 index 0000000000000000000000000000000000000000..35606525f45e6862b9c3a41a244c89adc825466d --- /dev/null +++ b/templates/400.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} + <h1>{% trans "Bad request" %}</h1> + {% blocktrans %}Sorry, your request was bad. Don't know what could be wrong. An email has been sent to webmasters with the details of the error. You can now drink a coke.{% endblocktrans %} +{% endblock %} \ No newline at end of file diff --git a/templates/403.html b/templates/403.html new file mode 100644 index 0000000000000000000000000000000000000000..317865f241bb7ce5a1bf97eb3434f2a7b8abc4c7 --- /dev/null +++ b/templates/403.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} + <h1>{% trans "Permission denied" %}</h1> + {% blocktrans %}You don't have the right to perform this request.{% endblocktrans %} + {% if exception %} + <div> + {% trans "Exception message:" %} {{ exception }} + </div> + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 0000000000000000000000000000000000000000..8477f914d72460f4faae2d8176feac17362d163b --- /dev/null +++ b/templates/404.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} + <h1>{% trans "Page not found" %}</h1> + {% blocktrans %}The requested path <code>{{ request_path }}</code> was not found on the server.{% endblocktrans %} + {% if exception != "Resolver404" %} + <div> + {% trans "Exception message:" %} {{ exception }} + </div> + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 0000000000000000000000000000000000000000..50b62bc2a080aea5111a69d96045b35bb45abff5 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} + <h1>{% trans "Server error" %}</h1> + {% blocktrans %}Sorry, an error occurred when processing your request. An email has been sent to webmasters with the detail of the error, and this will be fixed soon. You can now drink a beer.{% endblocktrans %} +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index 3c2c637f332621e6626a3008323bf24f0d62d30a..ddc2398f25b6695de30feda1a8d6d9f3a4e76a48 100644 --- a/templates/base.html +++ b/templates/base.html @@ -31,8 +31,8 @@ SPDX-License-Identifier: GPL-3.0-or-later href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous"> - <link rel="stylesheet" - href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css"> + <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/all.css"> + <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/v4-shims.css"> {# JQuery, Bootstrap and Turbolinks JavaScript #} <script src="https://code.jquery.com/jquery-3.4.1.min.js" @@ -58,6 +58,20 @@ SPDX-License-Identifier: GPL-3.0-or-later cursor: pointer; text-decoration: underline; } + .tooltip.show { + opacity: 1; /* opaque tooltip */ + } + .tooltip-inner { + background-color: #fff; + box-shadow: 0 .5rem 1rem rgba(0,0,0,.15); + border: 1px solid rgba(0,0,0,.250); + color: #000; + margin: 0 .5rem .25rem .5rem; + padding: 0; + } + .bs-tooltip-bottom .arrow::before { + border-bottom-color: rgba(0,0,0,.250); + } </style> {% block extracss %}{% endblock %} @@ -76,39 +90,52 @@ SPDX-License-Identifier: GPL-3.0-or-later <ul class="navbar-nav"> {% if "note.transactiontemplate"|not_empty_model_list %} <li class="nav-item active"> - <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> + <a class="nav-link" href="{% url 'note:consos' %}"><i class="fas fa-coffee"></i> {% trans 'Consumptions' %}</a> </li> {% endif %} {% if "note.transaction"|not_empty_model_list %} <li class="nav-item active"> - <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> + <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fas fa-exchange-alt"></i>{% trans 'Transfer' %} </a> </li> {% endif %} {% if "auth.user"|model_list|length >= 2 %} <li class="nav-item active"> - <a class="nav-link" href="{% url 'member:user_list' %}"><i class="fa fa-user"></i> {% trans 'Users' %}</a> + <a class="nav-link" href="{% url 'member:user_list' %}"><i class="fas fa-user"></i> {% trans 'Users' %}</a> </li> {% endif %} {% if "member.club"|not_empty_model_list %} <li class="nav-item active"> - <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> + <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fas fa-users"></i> {% trans 'Clubs' %}</a> </li> {% endif %} {% if "member.change_profile_registration_valid"|has_perm:user %} <li class="nav-item active"> <a class="nav-link" href="{% url 'registration:future_user_list' %}"> - <i class="fa fa-user-plus"></i> {% trans "Registrations" %} + <i class="fas fa-user-plus"></i> {% trans "Registrations" %} </a> </li> {% endif %} {% if "activity.activity"|not_empty_model_list %} <li class="nav-item active"> - <a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> + <a class="nav-link" href="{% url 'activity:activity_list' %}"><i class="fas fa-calendar"></i> {% trans 'Activities' %}</a> + </li> + {% endif %} + {% if "treasury.invoice"|not_empty_model_list %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fas fa-credit-card"></i> {% trans 'Treasury' %}</a> + </li> + {% endif %} + {% if "wei.weiclub"|not_empty_model_list %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'wei:current_wei_detail' %}"><i class="fas fa-bus"></i> {% trans 'WEI' %}</a> </li> {% endif %} - {% if "treasury.invoice"|not_empty_model_change_list %} + <li class="nav-item active"> + <a class="nav-link" href="{% url 'permission:rights' %}"><i class="fas fa-balance-scale"></i> {% trans 'Rights' %}</a> + </li> + {% if user.is_staff %} <li class="nav-item active"> - <a class="nav-link" href="{% url 'treasury:invoice_list' %}"><i class="fa fa-money"></i>{% trans 'Treasury' %} </a> + <a data-turbolinks="false" class="nav-link" href="{% url 'admin:index' %}"><i class="fas fa-user-cog"></i> {% trans 'Administration' %}</a> </li> {% endif %} </ul> @@ -116,28 +143,28 @@ SPDX-License-Identifier: GPL-3.0-or-later {% if user.is_authenticated %} <li class="dropdown"> <a class="nav-link dropdown-toggle" href="#" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> - <i class="fa fa-user"></i> + <i class="fas fa-user"></i> <span id="user_balance">{{ user.username }} ({{ user.note.balance | pretty_money }})</span> </a> <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdownMenuLink"> <a class="dropdown-item" href="{% url 'member:user_detail' pk=user.pk %}"> - <i class="fa fa-user"></i> Mon compte + <i class="fas fa-user"></i> Mon compte </a> <a class="dropdown-item" href="{% url 'logout' %}"> - <i class="fa fa-sign-out"></i> Se déconnecter + <i class="fas fa-sign-out-alt"></i> Se déconnecter </a> </div> </li> {% else %} <li class="nav-item active"> <a class="nav-link" href="{% url 'registration:signup' %}"> - <i class="fa fa-user-plus"></i> S'inscrire + <i class="fas fa-user-plus"></i> S'inscrire </a> </li> <li class="nav-item active"> <a class="nav-link" href="{% url 'login' %}"> - <i class="fa fa-sign-in"></i> Se connecter + <i class="fas fa-sign-in-alt"></i> Se connecter </a> </li> {% endif %} diff --git a/templates/cas_server/form.html b/templates/cas_server/form.html deleted file mode 100644 index 405dedd12fc1c35ca35827cc456aea95b0d23f17..0000000000000000000000000000000000000000 --- a/templates/cas_server/form.html +++ /dev/null @@ -1,26 +0,0 @@ -{% load cas_server %} -{% for error in form.non_field_errors %} -<div class="alert alert-danger alert-dismissable"> - <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> - {{error}} -</div> -{% endfor %} -{% for field in form %}{% if not field|is_hidden %} -<div class="form-group - {% if not form.non_field_errors %} - {% if field.errors %} has-error - {% elif form.cleaned_data %} has-success - {% endif %} - {% endif %}" ->{% spaceless %} - {% if field|is_checkbox %} - <div class="checkbox"><label for="{{field.auto_id}}">{{field}}{{field.label}}</label></div> - {% else %} - <label class="control-label" for="{{field.auto_id}}">{{field.label}}</label> - {{field}} - {% endif %} - {% for error in field.errors %} - <span class="help-block">{{error}}</span> - {% endfor %} -{% endspaceless %}</div> -{% else %}{{field}}{% endif %}{% endfor %} diff --git a/templates/cas_server/logged.html b/templates/cas_server/logged.html deleted file mode 100644 index 46e1c9a8824ba03c0796cb1f263a58b6a8f99e38..0000000000000000000000000000000000000000 --- a/templates/cas_server/logged.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "cas_server/base.html" %} -{% load i18n %} -{% block content %} -<div class="alert alert-success" role="alert">{% blocktrans %}<h3>Log In Successful</h3>You have successfully logged into the Central Authentication Service.<br/>For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!{% endblocktrans %}</div> -<form class="form-signin" method="get" action="logout"> - <div class="checkbox"> - <label> - <input type="checkbox" name="all" value="1">{% trans "Log me out from all my sessions" %} - </label> - </div> - {% if settings.CAS_FEDERATE and request.COOKIES.remember_provider %} - <div class="checkbox"> - <label> - <input type="checkbox" name="forget_provider" value="1">{% trans "Forget the identity provider" %} - </label> - </div> - {% endif %} - <button class="btn btn-danger btn-block btn-lg" type="submit">{% trans "Logout" %}</button> -</form> -{% endblock %} - diff --git a/templates/cas_server/login.html b/templates/cas_server/login.html deleted file mode 100644 index ddc2eb32aa80727c714e0f7b87c8fd912de0bc39..0000000000000000000000000000000000000000 --- a/templates/cas_server/login.html +++ /dev/null @@ -1,33 +0,0 @@ -{% extends "cas_server/base.html" %} -{% load i18n %} - -{% block ante_messages %} -{% if auto_submit %}<noscript>{% endif %} -<h2 class="form-signin-heading">{% trans "Please log in" %}</h2> -{% if auto_submit %}</noscript>{% endif %} -{% endblock %} -{% block content %} - <div class="alert alert-warning"> - {% trans "If you don't have any Note Kfet account, please follow <a href='/accounts/signup'>this link to sign up</a>." %} - </div> -<form class="form-signin" method="post" id="login_form"{% if post_url %} action="{{post_url}}"{% endif %}> - {% csrf_token %} - {% include "cas_server/form.html" %} - {% if auto_submit %}<noscript>{% endif %} - <button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Login" %}</button> - {% if auto_submit %}</noscript>{% endif %} -</form> -{% endblock %} -{% block javascript_inline %} -jQuery(function( $ ){ - $("#id_warn").click(function(e){ - if($("#id_warn").is(':checked')){ - createCookie("warn", "on", 10 * 365); - } else { - eraseCookie("warn"); - } - }); -});{% if auto_submit %} -document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %} -{% endblock %} - diff --git a/templates/cas_server/logout.html b/templates/cas_server/logout.html deleted file mode 100644 index 8069337678aa7272d2d10ab066647774f52a9720..0000000000000000000000000000000000000000 --- a/templates/cas_server/logout.html +++ /dev/null @@ -1,7 +0,0 @@ -{% extends "cas_server/base.html" %} -{% load static %} -{% load i18n %} -{% block content %} -<div class="alert alert-success" role="alert">{{logout_msg}}</div> -{% endblock %} - diff --git a/templates/cas_server/proxy.xml b/templates/cas_server/proxy.xml deleted file mode 100644 index ab51d89a89c9f59f9a57d7ecf421db644d5dc0b3..0000000000000000000000000000000000000000 --- a/templates/cas_server/proxy.xml +++ /dev/null @@ -1,5 +0,0 @@ -<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> - <cas:proxySuccess> - <cas:proxyTicket>{{ticket}}</cas:proxyTicket> - </cas:proxySuccess> - </cas:serviceResponse> diff --git a/templates/cas_server/samlValidate.xml b/templates/cas_server/samlValidate.xml deleted file mode 100644 index 3b130fd2c7cf2d47c62e1793dcc55d8292d6f040..0000000000000000000000000000000000000000 --- a/templates/cas_server/samlValidate.xml +++ /dev/null @@ -1,59 +0,0 @@ -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> - <SOAP-ENV:Header /> - <SOAP-ENV:Body> - <Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol" - xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" - IssueInstant="{{ IssueInstant }}" - MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}" - ResponseID="{{ ResponseID }}"> - <Status> - <StatusCode Value="samlp:Success"> - </StatusCode> - </Status> - <Assertion xmlns="urn:oasis:names:tc:SAML:1.0:assertion" AssertionID="{{ResponseID}}" - IssueInstant="{{IssueInstant}}" Issuer="localhost" MajorVersion="1" - MinorVersion="1"> - <Conditions NotBefore="{{IssueInstant}}" NotOnOrAfter="{{expireInstant}}"> - <AudienceRestrictionCondition> - <Audience> - {{Recipient}} - </Audience> - </AudienceRestrictionCondition> - </Conditions> - <AttributeStatement> - <Subject> - <NameIdentifier>{{username}}</NameIdentifier> - <SubjectConfirmation> - <ConfirmationMethod> - urn:oasis:names:tc:SAML:1.0:cm:artifact - </ConfirmationMethod> - </SubjectConfirmation> - </Subject> - <Attribute AttributeName="authenticationDate" AttributeNamespace="http://www.ja-sig.org/products/cas/"> - <AttributeValue>{{auth_date}}</AttributeValue> - </Attribute> - <Attribute AttributeName="longTermAuthenticationRequestTokenUsed" AttributeNamespace="http://www.ja-sig.org/products/cas/"> - <AttributeValue>false</AttributeValue>{# we do not support long-term (Remember-Me) auth #} - </Attribute> - <Attribute AttributeName="isFromNewLogin" AttributeNamespace="http://www.ja-sig.org/products/cas/"> - <AttributeValue>{{is_new_login}}</AttributeValue> - </Attribute> -{% for name, value in attributes %} <Attribute AttributeName="{{name}}" AttributeNamespace="http://www.ja-sig.org/products/cas/"> - <AttributeValue>{{value}}</AttributeValue> - </Attribute> -{% endfor %} </AttributeStatement> - <AuthenticationStatement AuthenticationInstant="{{IssueInstant}}" - AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password"> - <Subject> - <NameIdentifier>{{username}}</NameIdentifier> - <SubjectConfirmation> - <ConfirmationMethod> - urn:oasis:names:tc:SAML:1.0:cm:artifact - </ConfirmationMethod> - </SubjectConfirmation> - </Subject> - </AuthenticationStatement> - </Assertion> - </Response> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> diff --git a/templates/cas_server/samlValidateError.xml b/templates/cas_server/samlValidateError.xml deleted file mode 100644 index c72daba1d01329b2f72d7f4a39c2ca1c70d88bda..0000000000000000000000000000000000000000 --- a/templates/cas_server/samlValidateError.xml +++ /dev/null @@ -1,14 +0,0 @@ -<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> - <SOAP-ENV:Header /> - <SOAP-ENV:Body> - <Response xmlns="urn:oasis:names:tc:SAML:1.0:protocol" - xmlns:samlp="urn:oasis:names:tc:SAML:1.0:protocol" - IssueInstant="{{ IssueInstant }}" - MajorVersion="1" MinorVersion="1" Recipient="{{ Recipient }}" - ResponseID="{{ ResponseID }}"> - <Status> - <StatusCode Value="samlp:{{code}}">{{msg}}</StatusCode> - </Status> - </Response> - </SOAP-ENV:Body> -</SOAP-ENV:Envelope> diff --git a/templates/cas_server/serviceValidate.xml b/templates/cas_server/serviceValidate.xml deleted file mode 100644 index f583dbeace3f43da2a04a89b40e0cadaf5868d32..0000000000000000000000000000000000000000 --- a/templates/cas_server/serviceValidate.xml +++ /dev/null @@ -1,19 +0,0 @@ -<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> - <cas:authenticationSuccess> - <cas:user>{{username}}</cas:user> - <cas:attributes> - <cas:authenticationDate>{{auth_date}}</cas:authenticationDate> - <cas:longTermAuthenticationRequestTokenUsed>false</cas:longTermAuthenticationRequestTokenUsed>{# we do not support long-term (Remember-Me) auth #} - <cas:isFromNewLogin>{{is_new_login}}</cas:isFromNewLogin> -{% for key, value in attributes %} <cas:{{key}}>{{value}}</cas:{{key}}> -{% endfor %} </cas:attributes> - <cas:attribute name="authenticationDate" value="{{auth_date}}"/> - <cas:attribute name="longTermAuthenticationRequestTokenUsed" value="false"/> - <cas:attribute name="isFromNewLogin" value="{{is_new_login}}"/> -{% for key, value in attributes %} <cas:attribute name="{{key}}" value="{{value}}"/> -{% endfor %}{% if proxyGrantingTicket %} <cas:proxyGrantingTicket>{{proxyGrantingTicket}}</cas:proxyGrantingTicket> -{% endif %}{% if proxies %} <cas:proxies> -{% for proxy in proxies %} <cas:proxy>{{proxy}}</cas:proxy> -{% endfor %} </cas:proxies> -{% endif %} </cas:authenticationSuccess> -</cas:serviceResponse> diff --git a/templates/cas_server/serviceValidateError.xml b/templates/cas_server/serviceValidateError.xml deleted file mode 100644 index cab8d9bdd82b288b6a79f44acafff24c6c4f0d1c..0000000000000000000000000000000000000000 --- a/templates/cas_server/serviceValidateError.xml +++ /dev/null @@ -1,3 +0,0 @@ -<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas"> - <cas:authenticationFailure code="{{code}}">{{msg}}</cas:authenticationFailure> -</cas:serviceResponse> diff --git a/templates/cas_server/warn.html b/templates/cas_server/warn.html deleted file mode 100644 index 4f80b15a2c460304034f877b565986681eb530a1..0000000000000000000000000000000000000000 --- a/templates/cas_server/warn.html +++ /dev/null @@ -1,11 +0,0 @@ -{% extends "cas_server/base.html" %} -{% load static %} -{% load i18n %} - -{% block content %} - <form class="form-signin" method="post"> -{% csrf_token %} -{% include "cas_server/form.html" %} -<button class="btn btn-primary btn-block btn-lg" type="submit">{% trans "Connect to the service" %}</button> - </form> -{% endblock %} diff --git a/templates/colorfield/color.html b/templates/colorfield/color.html new file mode 100755 index 0000000000000000000000000000000000000000..274335986587ee4d789c0f2ff15582e06e79aa6c --- /dev/null +++ b/templates/colorfield/color.html @@ -0,0 +1,8 @@ +<input type="text" + id="{{ widget.attrs.id }}" + class="form-control colorfield_field jscolor" + name="{{ widget.name }}" + value="{% firstof widget.value widget.attrs.default '' %}" + placeholder="{% firstof widget.value widget.attrs.default '' %}" + data-jscolor="{hash:true,width:225,height:150,required:{% if widget.attrs.required %}true{% else %}false{% endif %}}" + {% if widget.attrs.required %}required{% endif %} /> diff --git a/templates/member/club_info.html b/templates/member/club_info.html index 93c76d59e9f608ec8218b7cee102c9b49d59e074..18cfe631fa130074a7c4b489a0065682f26032f5 100644 --- a/templates/member/club_info.html +++ b/templates/member/club_info.html @@ -15,18 +15,24 @@ {% if club.parent_club %} <dt class="col-xl-6"><a href="{% url 'member:club_detail' club.parent_club.pk %}">{% trans 'Club Parent'|capfirst %}</a></dt> - <dd class="col-xl-6"> {{ club.parent_club.name}}</dd> + <dd class="col-xl-6"> {{ club.parent_club.name }}</dd> {% endif %} {% if club.require_memberships %} - <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> - <dd class="col-xl-6">{{ club.membership_start }}</dd> + {% if club.membership_start %} + <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_start }}</dd> + {% endif %} - <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> - <dd class="col-xl-6">{{ club.membership_end }}</dd> + {% if club.membership_end %} + <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_end }}</dd> + {% endif %} - <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt> - <dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd> + {% if club.membership_duration %} + <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd> + {% endif %} {% if club.membership_fee_paid == club.membership_fee_unpaid %} <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> @@ -39,23 +45,31 @@ <dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}</dd> {% endif %} {% endif %} + + {% if "note.view_note"|has_perm:club.note %} + <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd> + {% endif %} <dt class="col-xl-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> - <dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd> + <dd class="col-xl-6 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd> - <dt class="col-xl-3">{% trans 'email'|capfirst %}</dt> - <dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd> + <dt class="col-xl-4">{% trans 'email'|capfirst %}</dt> + <dd class="col-xl-8"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd> </dl> </div> - <div class="card-footer text-center"> - {% if can_add_members %} - <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a> - {% endif %} - {% if ".change_"|has_perm:club %} - <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a> - {% endif %} - {% url 'member:club_detail' club.pk as club_detail_url %} - {%if request.path_info != club_detail_url %} - <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> - {% endif %} </div> + {% if not club.weiclub %} + <div class="card-footer text-center"> + {% if can_add_members %} + <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' club_pk=club.pk %}"> {% trans "Add member" %}</a> + {% endif %} + {% if ".change_"|has_perm:club %} + <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a> + {% endif %} + {% url 'member:club_detail' club.pk as club_detail_url %} + {%if request.path_info != club_detail_url %} + <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> + {% endif %} + </div> + {% endif %} </div> diff --git a/templates/member/club_list.html b/templates/member/club_list.html index 4682164cfb44384bf192bb05df60a2dff6bf7e0c..8ba0ef3bac3ef9728a7fddf3c3e97c8d6ab91161 100644 --- a/templates/member/club_list.html +++ b/templates/member/club_list.html @@ -16,7 +16,7 @@ <div class="col-md-10"> <div class="card card-border shadow"> <div class="card-header text-center"> - <h5> {% trans "club listing "%}</h5> + <h5> {% trans "Club listing" %}</h5> </div> <div class="card-body px-0 py-0" id="club_table"> {% render_table table %} diff --git a/templates/member/club_tables.html b/templates/member/club_tables.html index 32be9bd43b7652618eeaa8a55be50f672e2a1786..e937fbe940db65742e8219ea9f07314a47bfe9ed 100644 --- a/templates/member/club_tables.html +++ b/templates/member/club_tables.html @@ -1,23 +1,27 @@ {% load render_table from django_tables2 %} {% load i18n %} -<div class="card"> - <div class="card-header position-relative" id="clubListHeading"> - <a class="btn btn-link stretched-link font-weight-bold"> - <i class="fa fa-users"></i> {% trans "Member of the Club" %} - </a> - </div> +{% if member_list.data %} + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold"> + <i class="fa fa-users"></i> {% trans "Member of the Club" %} + </a> + </div> {% render_table member_list %} -</div> + </div> -<hr> + <hr> +{% endif %} -<div class="card"> - <div class="card-header position-relative" id="historyListHeading"> - <a class="btn btn-link stretched-link font-weight-bold"> - <i class="fa fa-euro"></i> {% trans "Transaction history" %} - </a> - </div> +{% if history_list.data %} + <div class="card"> + <div class="card-header position-relative" id="historyListHeading"> + <a class="btn btn-link stretched-link font-weight-bold"> + <i class="fa fa-euro"></i> {% trans "Transaction history" %} + </a> + </div> <div id="history_list"> {% render_table history_list %} </div> -</div> + </div> +{% endif %} diff --git a/templates/member/noteowner_detail.html b/templates/member/noteowner_detail.html index fc781549eb6bb6abedf9c5221a3227c165d0aec3..93ba19ef1118d138224a08e32f67d536481062a5 100644 --- a/templates/member/noteowner_detail.html +++ b/templates/member/noteowner_detail.html @@ -6,11 +6,11 @@ {% block content %} <div class="row mt-4"> - <div class="col-md-3 mb-4"> + <div class="col-xl-4"> {% block profile_info %} {% endblock %} </div> - <div class="col-md-9"> + <div class="col-xl-8"> {% block profile_content %} {% endblock %} </div> @@ -19,9 +19,11 @@ {% block extrajavascript %} <script> - function refreshHistory() { - $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); - $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); - } + {% if object %} + function refreshHistory() { + $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); + $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); + } + {% endif %} </script> {% endblock %} diff --git a/templates/member/profile_info.html b/templates/member/profile_info.html index 748563559ff82a2b52fdbb4b931551795132cd6a..7be10ba18a3398b21a6415b47857e17fcada1f96 100644 --- a/templates/member/profile_info.html +++ b/templates/member/profile_info.html @@ -17,12 +17,14 @@ <dt class="col-xl-6">{% trans 'username'|capfirst %}</dt> <dd class="col-xl-6">{{ object.username }}</dd> - <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> - <dd class="col-xl-6"> - <a class="small" href="{% url 'password_change' %}"> - {% trans 'Change password' %} - </a> - </dd> + {% if object.pk == user.pk %} + <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> + <dd class="col-xl-6"> + <a class="small" href="{% url 'password_change' %}"> + {% trans 'Change password' %} + </a> + </dd> + {% endif %} <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> <dd class="col-xl-6">{{ object.profile.section }}</dd> diff --git a/templates/member/user_list.html b/templates/member/user_list.html index 0bcd7e89c2dd9e2db54b72e2fd69ccdb70a0d6a4..018c479d32b5bb7ba6b243d4a7c9637c56938f41 100644 --- a/templates/member/user_list.html +++ b/templates/member/user_list.html @@ -1,6 +1,8 @@ {% extends "base.html" %} {% load render_table from django_tables2 %} -{% load crispy_forms_tags%} +{% load crispy_forms_tags %} +{% load i18n %} + {% block content %} <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ..."> @@ -11,7 +13,7 @@ {% render_table table %} {% else %} <div class="alert alert-warning"> - {% trans "There is no pending user with this pattern." %} + {% trans "There is no user with this pattern." %} </div> {% endif %} </div> diff --git a/templates/note/conso_form.html b/templates/note/conso_form.html index b108a96f83f81df9588c1c992c719b152bc7a0d8..451d8a2716e6430e7b69eb9c34734ddd03697b4f 100644 --- a/templates/note/conso_form.html +++ b/templates/note/conso_form.html @@ -10,10 +10,10 @@ <div class="col-sm-5 col-md-4" id="infos_div"> <div class="row"> {# User details column #} - <div class="col-xl-5" id="note_infos_div"> - <div class="card border-success shadow mb-4"> + <div class="col"> + <div class="card border-success shadow mb-4 text-center"> <img src="/media/pic/default.png" - id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block"> + id="profile_pic" alt="" class="card-img-top"> <div class="card-body text-center"> <span id="user_note"></span> </div> @@ -25,38 +25,45 @@ <div class="card border-success shadow mb-4"> <div class="card-header"> <p class="card-text font-weight-bold"> - {% trans "Select emitters" %} + {% trans "Consum" %} </p> </div> - <ul class="list-group list-group-flush" id="note_list"> - </ul> - <div class="card-body"> - <input class="form-control mx-auto d-block" type="text" id="note" /> - <ul class="list-group list-group-flush" id="alias_matched"> + <div class="card-body p-0" style="min-height:125px;"> + <ul class="list-group list-group-flush" id="note_list"> </ul> </div> + + {# User search with autocompletion #} + <div class="card-footer"> + <input class="form-control mx-auto d-block" + placeholder="{% trans "Name or alias..." %}" type="text" id="note" autofocus /> + </div> </div> </div> - <div class="col-xl-5" id="consos_list_div"> + <div class="col-xl-5 d-none" id="consos_list_div"> <div class="card border-info shadow mb-4"> <div class="card-header"> <p class="card-text font-weight-bold"> {% trans "Select consumptions" %} </p> </div> - <ul class="list-group list-group-flush" id="consos_list"> - </ul> - <button id="consume_all" class="form-control btn btn-primary"> - {% trans "Consume!" %} - </button> + <div class="card-body p-0" style="min-height:125px;"> + <ul class="list-group list-group-flush" id="consos_list"> + </ul> + </div> + <div class="card-footer text-center"> + <a id="consume_all" href="#" class="btn btn-primary"> + {% trans "Consume!" %} + </a> + </div> </div> </div> </div> </div> {# Buttons column #} - <div class="col-sm-7 col-md-8" id="buttons_div"> + <div class="col"> {# Show last used buttons #} <div class="card shadow mb-4"> <div class="card-header"> @@ -123,10 +130,12 @@ <div class="btn-group btn-group-toggle float-right" data-toggle="buttons"> <label for="single_conso" class="btn btn-sm btn-outline-primary active"> <input type="radio" name="conso_type" id="single_conso" checked> + <i class="fa fa-long-arrow-left" aria-hidden="true"></i> {% trans "Single consumptions" %} </label> <label for="double_conso" class="btn btn-sm btn-outline-primary"> <input type="radio" name="conso_type" id="double_conso"> + <i class="fa fa-arrows-h" aria-hidden="true"></i> {% trans "Double consumptions" %} </label> </div> @@ -146,7 +155,7 @@ {% endblock %} {% block extrajavascript %} - <script type="text/javascript" src="/static/js/consos.js"></script> + <script type="text/javascript" src="{% static "js/consos.js" %}"></script> <script type="text/javascript"> {% for button in most_used %} {% if button.display %} diff --git a/templates/note/transaction_form.html b/templates/note/transaction_form.html index 0b53df61f2586fd0bfc7e4e386230bf38e45e793..c252f636cd7ff0ed60eb5f1c3b00f5665710dd60 100644 --- a/templates/note/transaction_form.html +++ b/templates/note/transaction_form.html @@ -38,8 +38,7 @@ SPDX-License-Identifier: GPL-2.0-or-later </div> <div class="row"> - - <div class="col-xl-4" id="note_infos_div"> + <div class="col-md-3" id="note_infos_div"> <div class="card border-success shadow mb-4"> <img src="/media/pic/default.png" id="profile_pic" alt="" class="img-fluid rounded mx-auto d-block"> @@ -48,7 +47,8 @@ SPDX-License-Identifier: GPL-2.0-or-later </div> </div> </div> - <div class="col-md-4" id="emitters_div" style="display: none;"> + + <div class="col-md-3" id="emitters_div"> <div class="card border-success shadow mb-4"> <div class="card-header"> <p class="card-text font-weight-bold"> @@ -58,24 +58,53 @@ SPDX-License-Identifier: GPL-2.0-or-later <ul class="list-group list-group-flush" id="source_note_list"> </ul> <div class="card-body"> - <input class="form-control mx-auto d-block" type="text" id="source_note" /> - <ul class="list-group list-group-flush" id="source_alias_matched"> + <input class="form-control mx-auto d-block" type="text" id="source_note" placeholder="{% trans "Name or alias..." %}" /> + </div> + </div> + </div> + + <div class="col-md-3" id="dests_div"> + <div class="card border-info shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold" id="dest_title"> + {% trans "Select receivers" %} + </p> + </div> + <ul class="list-group list-group-flush" id="dest_note_list"> + </ul> + <div class="card-body"> + <input class="form-control mx-auto d-block" type="text" id="dest_note" placeholder="{% trans "Name or alias..." %}" /> + <ul class="list-group list-group-flush" id="dest_alias_matched"> </ul> </div> </div> </div> - {% if "note.notespecial"|not_empty_model_list %} - <div class="col-md-4" id="external_div" style="display: none;"> - <div class="card border-success shadow mb-4"> - <div class="card-header"> - <p class="card-text font-weight-bold"> - {% trans "External payment" %} - </p> + <div class="col-md-3" id="external_div"> + <div class="card border-warning shadow mb-4"> + <div class="card-header"> + <p class="card-text font-weight-bold"> + {% trans "Action" %} + </p> + </div> + <ul class="list-group list-group-flush" id="source_note_list"> + </ul> + <div class="card-body"> + <div class="form-row"> + <div class="col-md-12"> + <label for="amount">{% trans "Amount" %} :</label> + {% include "note/amount_input.html" with widget=amount_widget %} + </div> </div> - <ul class="list-group list-group-flush" id="source_note_list"> - </ul> - <div class="card-body"> + + <div class="form-row"> + <div class="col-md-12"> + <label for="reason">{% trans "Reason" %} :</label> + <input class="form-control mx-auto d-block" type="text" id="reason" required /> + </div> + </div> + + <div class="d-none" id="special_transaction_div"> <div class="form-row"> <div class="col-md-12"> <label for="credit_type">{% trans "Transfer type" %} :</label> @@ -105,44 +134,14 @@ SPDX-License-Identifier: GPL-2.0-or-later </div> </div> </div> + <hr> + <div class="form-row"> + <div class="col-md-12"> + <button id="btn_transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button> + </div> + </div> </div> </div> - {% endif %} - - <div class="col-md-8" id="dests_div"> - <div class="card border-info shadow mb-4"> - <div class="card-header"> - <p class="card-text font-weight-bold" id="dest_title"> - {% trans "Select receivers" %} - </p> - </div> - <ul class="list-group list-group-flush" id="dest_note_list"> - </ul> - <div class="card-body"> - <input class="form-control mx-auto d-block" type="text" id="dest_note" /> - <ul class="list-group list-group-flush" id="dest_alias_matched"> - </ul> - </div> - </div> - </div> - </div> - - - <div class="form-row"> - <div class="form-group col-md-6"> - <label for="amount">{% trans "Amount" %} :</label> - {% include "note/amount_input.html" with widget=amount_widget %} - </div> - - <div class="form-group col-md-6"> - <label for="reason">{% trans "Reason" %} :</label> - <input class="form-control mx-auto d-block" type="text" id="reason" required /> - </div> - </div> - - <div class="form-row"> - <div class="col-md-12"> - <button id="btn_transfer" class="form-control btn btn-primary">{% trans 'Transfer' %}</button> </div> </div> @@ -161,34 +160,7 @@ SPDX-License-Identifier: GPL-2.0-or-later TRANSFER_POLYMORPHIC_CTYPE = {{ polymorphic_ctype }}; SPECIAL_TRANSFER_POLYMORPHIC_CTYPE = {{ special_polymorphic_ctype }}; user_id = {{ user.note.pk }}; - - $("#type_gift").click(function() { - $("#emitters_div").hide(); - $("#external_div").hide(); - $("#dests_div").attr('class', 'col-md-8'); - $("#dest_title").text("{% trans "Select receivers" %}"); - }); - - $("#type_transfer").click(function() { - $("#external_div").hide(); - $("#emitters_div").show(); - $("#dests_div").attr('class', 'col-md-4'); - $("#dest_title").text("{% trans "Select receivers" %}"); - }); - - $("#type_credit").click(function() { - $("#emitters_div").hide(); - $("#external_div").show(); - $("#dests_div").attr('class', 'col-md-4'); - $("#dest_title").text("{% trans "Credit note" %}"); - }); - - $("#type_debit").click(function() { - $("#emitters_div").hide(); - $("#external_div").show(); - $("#dests_div").attr('class', 'col-md-4'); - $("#dest_title").text("{% trans "Debit note" %}"); - }); + username = "{{ user.username }}"; </script> <script src="/static/js/transfer.js"></script> {% endblock %} diff --git a/templates/note/transactiontemplate_form.html b/templates/note/transactiontemplate_form.html index 1f9a574a050ff49056f3a6931a00636cb951b747..e4bc42a717aa6d9fa1ad79c4361030a65eafd440 100644 --- a/templates/note/transactiontemplate_form.html +++ b/templates/note/transactiontemplate_form.html @@ -1,12 +1,28 @@ {% extends "base.html" %} + {% load static %} {% load i18n %} {% load crispy_forms_tags %} +{% load pretty_money %} + {% block content %} -<p><a class="btn btn-default" href="{% url 'note:template_list' %}">{% trans "Buttons list" %}</a></p> -<form method="post"> -{% csrf_token %} -{{form|crispy}} -<button class="btn btn-primary" type="submit">Submit</button> -</form> + <p> + <a class="btn btn-default" href="{% url 'note:template_list' %}">{% trans "Buttons list" %}</a> + </p> + <form method="post"> + {% csrf_token %} + {{form|crispy}} + <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> + </form> + + {% if price_history and price_history.1 %} + <hr> + + <h4>{% trans "Price history" %}</h4> + <ul> + {% for price in price_history %} + <li>{{ price.price|pretty_money }} {% if price.time %}({% trans "Obsolete since" %} {{ price.time }}){% else %}({% trans "Current price" %}){% endif %}</li> + {% endfor %} + </ul> + {% endif %} {% endblock %} diff --git a/templates/note/transactiontemplate_list.html b/templates/note/transactiontemplate_list.html index cf9bc5edc91b2fb9ce28dacba346b19b24cbf6ec..280f9faf3ca0853a3263f1c99f9d701e2510f4d1 100644 --- a/templates/note/transactiontemplate_list.html +++ b/templates/note/transactiontemplate_list.html @@ -6,9 +6,17 @@ <div class="row justify-content-center mb-4"> <div class="col-md-10 text-center"> <h4> - {% trans "search button" %} + {% trans "Search button" %} </h4> - <input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/> + <input class="form-control mx-auto w-25" type="text" id="search_field" placeholder="{% trans "Name of the button..." %}"> + <div class="form-group"> + <div id="div_active_only" class="form-check"> + <label for="active_only" class="form-check-label"> + <input type="checkbox" name="active_only" class="checkboxinput form-check-input" checked="" id="active_only"> + {% trans "Display visible buttons only" %} + </label> + </div> + </div> <hr> <a class="btn btn-primary text-center my-1" href="{% url 'note:template_create' %}">{% trans "New button" %}</a> </div> @@ -29,50 +37,65 @@ {% block extrajavascript %} <script> -/* fonction appelée à la fin du timer */ -function getInfo() { - var asked = $("#search_field").val(); - /* on ne fait la requête que si on a au moins un caractère pour chercher */ - var sel = $(".table-row"); - 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)); - $(".table-row,"+selected_id.join()).show(); - $(".table-row").not(selected_id.join()).hide(); - - }); - }else{ - // show everything - $('table tr').show(); - } -} -var timer; -var timer_on; -/* Fontion appelée quand le texte change (délenche le timer) */ -function search_field_moved(secondfield) { - if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur. - clearTimeout(timer); - timer = setTimeout("getInfo(" + secondfield + ")", 300); + /* fonction appelée à la fin du timer */ + function getInfo() { + var asked = $("#search_field").val(); + /* on ne fait la requête que si on a au moins un caractère pour chercher */ + if (asked.length >= 1) { + $.getJSON("/api/note/transaction/template/?format=json&search=" + asked + ($("#active_only").is(":checked") ? "&display=true" : ""), function(buttons) { + console.log(buttons); + let selected_id = buttons.results.map((a => "#row-" + a.id)); + console.log(".table-row " + selected_id.join()); + $(".table-row " + selected_id.join()).removeClass('d-none'); + $(".table-row").not(selected_id.join()).addClass('d-none'); + }); + } + else { + if ($("#active_only").is(":checked")) { + $('.table-success').removeClass('d-none'); + $('.table-danger').addClass('d-none'); + } + else { + // show everything + $('table tr').removeClass('d-none'); + } + } } - else { // Sinon, on le lance et on enregistre le fait qu'il tourne. - timer = setTimeout("getInfo(" + secondfield + ")", 300); - timer_on = true; + + var timer; + var timer_on; + /* Fontion appelée quand le texte change (délenche le timer) */ + function search_field_moved() { + if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur. + clearTimeout(timer); + timer = setTimeout(getInfo, 300); + } + else { // Sinon, on le lance et on enregistre le fait qu'il tourne. + timer = setTimeout(getInfo, 300); + timer_on = true; + } } -} -// on click of button "delete" , call the API - function delete_button(button_id){ - $.ajax({ - url:"/api/note/transaction/template/"+button_id+"/", - method:"DELETE", - headers: {"X-CSRFTOKEN": CSRF_TOKEN} - }) - .done(function(){ - addMsg('{% trans "button successfully deleted "%}','success'); - $("#buttons_table").load("{% url 'note:template_list' %} #buttons_table"); - }) - .fail(function(){ - addMsg(' {% trans "Unable to delete button "%} #' + button_id,'danger' ) - }); - } + // on click of button "delete" , call the API + function delete_button(button_id) { + $.ajax({ + url:"/api/note/transaction/template/"+button_id+"/", + method:"DELETE", + headers: {"X-CSRFTOKEN": CSRF_TOKEN} + }) + .done(function(){ + addMsg('{% trans "button successfully deleted "%}','success'); + $("#buttons_table").load("{% url 'note:template_list' %} #buttons_table"); + }) + .fail(function(){ + addMsg(' {% trans "Unable to delete button "%} #' + button_id,'danger' ) + }); + } + + $(document).ready(function() { + $("#search_field").keyup(search_field_moved); + $("#active_only").change(search_field_moved); + + search_field_moved(); + }); </script> {% endblock %} diff --git a/templates/permission/all_rights.html b/templates/permission/all_rights.html new file mode 100644 index 0000000000000000000000000000000000000000..85b47597164ef43275d3caa9175948bd9cb0c93c --- /dev/null +++ b/templates/permission/all_rights.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} + {% if user.is_authenticated %} + <div class="form-check"> + <label for="owned_only" class="form-check-label"> + <input id="owned_only" name="owned_only" type="checkbox" class="checkboxinput form-check-input"> + {% trans "Filter with roles that I have in at least one club" %} + </label> + </div> + {% endif %} + <ul> + {% regroup active_memberships by roles as memberships_per_role %} + {% for role in roles %} + <li class="{% if not role.clubs %}no-club{% endif %}"> + {{ role }} {% if role.weirole %}(<em>Pour le WEI</em>){% endif %} + {% if role.clubs %} + <div class="alert alert-success"> + {% trans "Own this role in the clubs" %} {{ role.clubs|join:", " }} + </div> + {% endif %} + <ul> + {% for permission in role.permissions.permissions.all %} + <li data-toggle="tooltip" title="{% trans "Query:" %} {{ permission.query }}">{{ permission }} ({{ permission.type }} {{ permission.model }})</li> + {% empty %} + <em>{% trans "No associated permission" %}</em> + {% endfor %} + </ul> + </li> + {% endfor %} + </ul> +{% endblock %} + +{% block extrajavascript %} + <script> + $(document).ready(function() { + let checkbox = $("#owned_only"); + + function update() { + if (checkbox.is(":checked")) + $(".no-club").addClass('d-none'); + else + $(".no-club").removeClass('d-none'); + } + + checkbox.change(update); + update(); + }); + </script> +{% endblock %} diff --git a/templates/registration/email_validation_complete.html b/templates/registration/email_validation_complete.html index 4835cfa16f2ee9c0fd5bcda1d121487171339400..b54432f36220215ae04a059d4ddfddc898090dec 100644 --- a/templates/registration/email_validation_complete.html +++ b/templates/registration/email_validation_complete.html @@ -4,7 +4,7 @@ {% block content %} {% if validlink %} {% trans "Your email have successfully been validated." %} - {% if user.profile.registration_valid %} + {% if user_object.profile.registration_valid %} {% blocktrans %}You can now <a href="{{ login_url }}">log in</a>.{% endblocktrans %} {% else %} {% trans "You must pay now your membership in the Kfet to complete your registration." %} diff --git a/templates/registration/future_profile_detail.html b/templates/registration/future_profile_detail.html index 8c78fb8dd480da490099e0d7b0ac4c7a66c53f6c..1d2d08c791be34eb9874fd53b61062050b9f7ec1 100644 --- a/templates/registration/future_profile_detail.html +++ b/templates/registration/future_profile_detail.html @@ -31,13 +31,6 @@ </dd> {% endif %} - <dt class="col-xl-6">{% trans 'password'|capfirst %}</dt> - <dd class="col-xl-6"> - <a class="small" href="{% url 'password_change' %}"> - {% trans 'Change password' %} - </a> - </dd> - <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> <dd class="col-xl-6">{{ object.profile.section }}</dd> diff --git a/templates/registration/login.html b/templates/registration/login.html index 175d37e0d1a4260510c858b6693b4059bace23ea..32e54e00e3c85e2c10cccc0ac9cfa5ffa4a56c0d 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -16,13 +16,6 @@ SPDX-License-Identifier: GPL-2.0-or-later {% endblocktrans %} </p> {% endif %} - {%url 'cas_login' as cas_url %} - {% if cas_url %} - <div class="alert alert-info"> - {% trans "You can also register via the central authentification server " %} - <a href="{{ cas_url }}"> {% trans "using this link "%}</a> - </div> - {%endif%} <form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %} {{ form | crispy }} <input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary"> diff --git a/templates/registration/signup.html b/templates/registration/signup.html index d7b3c23ef8bb0fba2baebc04e178ecf1bc59ab78..76f88a49f838d66a1d24748a61f0ed6e76319aef 100644 --- a/templates/registration/signup.html +++ b/templates/registration/signup.html @@ -5,13 +5,18 @@ {% block title %}{% trans "Sign up" %}{% endblock %} {% block content %} - <h2>{% trans "Sign up" %}</h2> - <form method="post"> - {% csrf_token %} - {{ form|crispy }} - {{ profile_form|crispy }} - <button class="btn btn-success" type="submit"> - {% trans "Sign up" %} - </button> - </form> + <h2>{% trans "Sign up" %}</h2> + + <div class="alert alert-warning"> + {% blocktrans %}If you already signed up, your registration is taken into account. The BDE must validate your account before your can log in. You have to go to the Kfet and pay the registration fee. You must also validate your email address by following the link you received.{% endblocktrans %} + </div> + + <form method="post"> + {% csrf_token %} + {{ form|crispy }} + {{ profile_form|crispy }} + <button class="btn btn-success" type="submit"> + {% trans "Sign up" %} + </button> + </form> {% endblock %} diff --git a/templates/scripts/mail-error500.html b/templates/scripts/mail-error500.html new file mode 100644 index 0000000000000000000000000000000000000000..f4bb796f6b720dbc4702404c0457d4d0d1ef4562 --- /dev/null +++ b/templates/scripts/mail-error500.html @@ -0,0 +1,2 @@ +{# The data is already sent as HTML, so we return only the HTML data. Devs don't need a pretty mail... #} +{{ error }} \ No newline at end of file diff --git a/templates/scripts/mail-error500.txt b/templates/scripts/mail-error500.txt new file mode 100644 index 0000000000000000000000000000000000000000..b7aabd396261c77a9841cf729397b9f3d302cfe4 --- /dev/null +++ b/templates/scripts/mail-error500.txt @@ -0,0 +1,7 @@ +Une erreur est survenue dans la Note Kfet. Les détails sont ci-dessous. + +Cordialement, + +L'équipe de la Note Kfet. + +{{ error }} \ No newline at end of file diff --git a/templates/treasury/invoice_list.html b/templates/treasury/invoice_list.html index f14d278dd0375528512d84fb5d0e00c8dfbff214..4e95816e7f113ac39c478fd4533d4b07015adbab 100644 --- a/templates/treasury/invoice_list.html +++ b/templates/treasury/invoice_list.html @@ -12,6 +12,9 @@ <a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary"> {% trans "Remittance" %}s </a> + <a href="{% url "treasury:soge_credits" %}" class="btn btn-sm btn-outline-primary"> + {% trans "Société générale credits" %} + </a> </div> </div> </div> diff --git a/templates/treasury/remittance_list.html b/templates/treasury/remittance_list.html index 8bc634e4b35e029a95d57369dd24d0c50c0aeb5e..916c47d8cc427454f4e1c812fe5d88d4d9251336 100644 --- a/templates/treasury/remittance_list.html +++ b/templates/treasury/remittance_list.html @@ -12,6 +12,9 @@ <a href="#" class="btn btn-sm btn-outline-primary active"> {% trans "Remittance" %}s </a> + <a href="{% url "treasury:soge_credits" %}" class="btn btn-sm btn-outline-primary"> + {% trans "Société générale credits" %} + </a> </div> </div> </div> diff --git a/templates/treasury/sogecredit_detail.html b/templates/treasury/sogecredit_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..eafece533eb23004c0cfc326b09522c3e77eade1 --- /dev/null +++ b/templates/treasury/sogecredit_detail.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} +{% load static %} +{% load i18n %} +{% load pretty_money %} +{% load perms %} + +{% block content %} + <div class="card bg-light shadow"> + <div class="card-header text-center"> + <h4>{% trans "Credit from the Société générale" %}</h4> + </div> + <div class="card-body"> + <dl class="row"> + <dt class="col-xl-6 text-right">{% trans 'user'|capfirst %}</dt> + <dd class="col-xl-6"><a href="{% url 'member:user_detail' pk=object.user.pk %}">{{ object.user }}</a></dd> + + {% if "note.view_note_balance"|has_perm:object.user.note %} + <dt class="col-xl-6 text-right">{% trans 'balance'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.user.note.balance|pretty_money }}</dd> + {% endif %} + + <dt class="col-xl-6 text-right">{% trans 'transactions'|capfirst %}</dt> + <dd class="col-xl-6"> + {% for transaction in object.transactions.all %} + {{ transaction.membership.club }} ({{ transaction.amount|pretty_money }})<br> + {% endfor %} + </dd> + + <dt class="col-xl-6 text-right">{% trans 'total amount'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.amount|pretty_money }}</dd> + </dl> + </div> + + <div class="alert alert-warning"> + {% trans 'Warning: Validating this credit implies that all membership transactions will be validated.' %} + {% trans 'If you delete this credit, there all membership transactions will be also validated, but no credit will be operated.' %} + {% trans "If this credit is validated, then the user won't be able to ask for a credit from the Société générale." %} + {% trans 'If you think there is an error, please contact the "respos info".' %} + </div> + + <div class="card-footer text-center" id="buttons_footer"> + {% if object.valid %} + <div class="alert alert-danger"> + {% trans "This credit is already validated." %} + </div> + {% else %} + {% if object.user.note.balance < object.amount %} + <div class="alert alert-warning"> + {% trans "Warning: if you don't validate this credit, the note of the user doesn't have enough money to pay its memberships." %} + {% trans "Please ask the user to credit its note before deleting this credit." %} + </div> + {% endif %} + + <form method="post"> + {% csrf_token %} + <div class="btn-group btn-block"> + <button name="validate" class="btn btn-success">{% trans "Validate" %}</button> + {% if object.user.note.balance >= object.amount %} + <button name="delete" class="btn btn-danger">{% trans "Delete" %}</button> + {% endif %} + </div> + </form> + {% endif %} + <a href="{% url 'treasury:soge_credits' %}"><button class="btn btn-primary btn-block">{% trans "Return to credit list" %}</button></a> + </div> + </div> +{% endblock %} diff --git a/templates/treasury/sogecredit_list.html b/templates/treasury/sogecredit_list.html new file mode 100644 index 0000000000000000000000000000000000000000..c8a9207b6fd768bd913303ed040ce01b51bdc873 --- /dev/null +++ b/templates/treasury/sogecredit_list.html @@ -0,0 +1,66 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n %} +{% block content %} + + <div class="row"> + <div class="col-xl-12"> + <div class="btn-group btn-group-toggle" style="width: 100%; padding: 0 0 2em 0" data-toggle="buttons"> + <a href="{% url "treasury:invoice_list" %}" class="btn btn-sm btn-outline-primary"> + {% trans "Invoice" %}s + </a> + <a href="{% url "treasury:remittance_list" %}" class="btn btn-sm btn-outline-primary"> + {% trans "Remittance" %}s + </a> + <a href="#" class="btn btn-sm btn-outline-primary active"> + {% trans "Société générale credits" %} + </a> + </div> + </div> + </div> + + <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ..."> + <div class="form-check"> + <label for="invalid_only" class="form-check-label"> + <input id="invalid_only" name="invalid_only" type="checkbox" class="checkboxinput form-check-input"> + {% trans "Filter with unvalidated credits only" %} + </label> + </div> + <hr> + + <div id="credits_table"> + {% if table.data %} + {% render_table table %} + {% else %} + <div class="alert alert-warning"> + {% trans "There is no matched user that have asked for a Société générale credit." %} + </div> + {% endif %} + </div> +{% endblock %} + +{% block extrajavascript %} +<script type="text/javascript"> + $(document).ready(function() { + let old_pattern = null; + let searchbar_obj = $("#searchbar"); + let invalid_only_obj = $("#invalid_only"); + + function reloadTable() { + let pattern = searchbar_obj.val(); + + if (pattern === old_pattern || pattern === "") + return; + + $("#credits_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + (invalid_only_obj.is(':checked') ? "&valid=false" : "") + " #credits_table"); + + $(".table-row").click(function() { + window.document.location = $(this).data("href"); + }); + } + + searchbar_obj.keyup(reloadTable); + invalid_only_obj.change(reloadTable); + }); +</script> +{% endblock %} diff --git a/templates/wei/bus_detail.html b/templates/wei/bus_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..1b335be8763e0e9d37a70a37230f68cc97822d80 --- /dev/null +++ b/templates/wei/bus_detail.html @@ -0,0 +1,9 @@ +{% extends "member/noteowner_detail.html" %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} +{% include "wei/bus_tables.html" %} +{% endblock %} diff --git a/templates/wei/bus_form.html b/templates/wei/bus_form.html new file mode 100644 index 0000000000000000000000000000000000000000..4c7b22ce4a2ba0db9c912b3613043cb292108f72 --- /dev/null +++ b/templates/wei/bus_form.html @@ -0,0 +1,15 @@ +{% extends "member/noteowner_detail.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + +{% block profile_info %} + {% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} +<form method="post"> +{% csrf_token %} +{{ form|crispy }} +<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> +</form> +{% endblock %} diff --git a/templates/wei/bus_tables.html b/templates/wei/bus_tables.html new file mode 100644 index 0000000000000000000000000000000000000000..50eccf6a24cfdfe2ac3585e4e657d25f3ddc8d8d --- /dev/null +++ b/templates/wei/bus_tables.html @@ -0,0 +1,50 @@ +{% load render_table from django_tables2 %} +{% load i18n %} + +<div class="card"> + <div class="card-header text-center"> + <h4>{{ object.name }}</h4> + </div> + + + <div class="card-body"> + {{ object.description }} + </div> + + <div class="card-footer text-center"> + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=object.pk %}">{% trans "Edit" %}</a> + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=object.pk %}">{% trans "Add team" %}</a> + </div> +</div> + +<hr> + +{% if teams.data %} + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold"> + <i class="fa fa-bus"></i> {% trans "Teams" %} + </a> + </div> + {% render_table teams %} + </div> + + <hr> +{% endif %} + +{% if memberships.data %} + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold"> + <i class="fa fa-bus"></i> {% trans "Members" %} + </a> + </div> + {% render_table memberships %} + </div> + + <hr> + + <a href="{% url 'wei:wei_memberships_bus_pdf' wei_pk=club.pk bus_pk=object.pk %}" data-turbolinks="false"> + <button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button> + </a> +{% endif %} diff --git a/templates/wei/busteam_detail.html b/templates/wei/busteam_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..481e24ea6de01740a233d1ce668d0e7e58ca5a2f --- /dev/null +++ b/templates/wei/busteam_detail.html @@ -0,0 +1,9 @@ +{% extends "member/noteowner_detail.html" %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} +{% include "wei/busteam_tables.html" %} +{% endblock %} diff --git a/templates/wei/busteam_form.html b/templates/wei/busteam_form.html new file mode 100644 index 0000000000000000000000000000000000000000..4c7b22ce4a2ba0db9c912b3613043cb292108f72 --- /dev/null +++ b/templates/wei/busteam_form.html @@ -0,0 +1,15 @@ +{% extends "member/noteowner_detail.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + +{% block profile_info %} + {% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} +<form method="post"> +{% csrf_token %} +{{ form|crispy }} +<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> +</form> +{% endblock %} diff --git a/templates/wei/busteam_tables.html b/templates/wei/busteam_tables.html new file mode 100644 index 0000000000000000000000000000000000000000..3135158f9741a0929785ffd81cbaf6dc0a993c74 --- /dev/null +++ b/templates/wei/busteam_tables.html @@ -0,0 +1,54 @@ +{% load render_table from django_tables2 %} +{% load i18n %} + +<div class="card"> + <div class="card-header text-center"> + <h4>{{ bus.name }}</h4> + </div> + + + <div class="card-body"> + {{ bus.description }} + </div> + + <div class="card-footer text-center"> + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus' pk=bus.pk %}">{% trans "Edit" %}</a> + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_team' pk=bus.pk %}">{% trans "Add team" %}</a> + </div> +</div> + +<hr> + +<div class="card"> + <div class="card-header text-center" style="background-color: #{{ object.color|stringformat:"06X" }}; color: #{{ -16777215|add:object.color|stringformat:"06X"|slice:"1:" }};"> + <h4>{{ object.name }}</h4> + </div> + + + <div class="card-body"> + {{ object.description }} + </div> + + <div class="card-footer text-center"> + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:update_bus_team' pk=object.pk %}">{% trans "Edit" %}</a> + </div> +</div> + +<hr> + +{% if memberships.data or True %} + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold"> + <i class="fa fa-bus"></i> {% trans "Teams" %} + </a> + </div> + {% render_table memberships %} + </div> + + <hr> + + <a href="{% url 'wei:wei_memberships_team_pdf' wei_pk=club.pk bus_pk=object.bus.pk team_pk=object.pk %}" data-turbolinks="false"> + <button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button> + </a> +{% endif %} diff --git a/templates/wei/survey.html b/templates/wei/survey.html new file mode 100644 index 0000000000000000000000000000000000000000..36553849eaf0d41a03c313d97db58b0c38578c43 --- /dev/null +++ b/templates/wei/survey.html @@ -0,0 +1,29 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <div class="card"> + <div class="card-header text-center"> + <h4>{% trans "Survey WEI" %}</h4> + </div> + <div class="card-body"> + <dl class="row"> + <dt class="col-xl-6">{% trans 'user'|capfirst %}</dt> + <dd class="col-xl-6">{{ object.user }}</dd> + </dl> + + <form method="post"> + {% csrf_token %} + {{ form|crispy }} + <div class="card-footer text-center"> + <input class="btn btn-success" type="submit" value="{% trans "Next" %}"/> + </div> + </form> + </div> + </div> +{% endblock %} diff --git a/templates/wei/survey_closed.html b/templates/wei/survey_closed.html new file mode 100644 index 0000000000000000000000000000000000000000..28c182ef73c093ff223936c6f4f0b0b58d94fd22 --- /dev/null +++ b/templates/wei/survey_closed.html @@ -0,0 +1,23 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <div class="card"> + <div class="card-header text-center"> + <h4>{% trans "Survey WEI" %}</h4> + </div> + <div class="card-body"> + <p> + {% trans "The inscription for this WEI are now closed." %} + </p> + </div> + <div class="card-footer text-center"> + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_detail' pk=club.pk %}">{% trans "Return to WEI detail" %}</a> + </div> + </div> +{% endblock %} diff --git a/templates/wei/survey_end.html b/templates/wei/survey_end.html new file mode 100644 index 0000000000000000000000000000000000000000..888290f7893b226322d0bc2356508f82deb2a449 --- /dev/null +++ b/templates/wei/survey_end.html @@ -0,0 +1,20 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <div class="card"> + <div class="card-header text-center"> + <h4>{% trans "Survey WEI" %}</h4> + </div> + <div class="card-body"> + <p> + {% trans "The survey is now ended. Your answers have been saved." %} + </p> + </div> + </div> +{% endblock %} diff --git a/templates/wei/weiclub_detail.html b/templates/wei/weiclub_detail.html new file mode 100644 index 0000000000000000000000000000000000000000..72c70aa5ee9344aeb401e72f0e033b1b53772131 --- /dev/null +++ b/templates/wei/weiclub_detail.html @@ -0,0 +1,20 @@ +{% extends "member/noteowner_detail.html" %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} +{% include "wei/weiclub_tables.html" %} +{% endblock %} + +{% block extrajavascript %} + <script> + function refreshHistory() { + $("#history_list").load("{% url 'wei:wei_detail' pk=object.pk %} #history_list"); + $("#profile_infos").load("{% url 'wei:wei_detail' pk=object.pk %} #profile_infos"); + } + + window.history.replaceState({}, document.title, location.pathname); + </script> +{% endblock %} diff --git a/templates/wei/weiclub_form.html b/templates/wei/weiclub_form.html new file mode 100644 index 0000000000000000000000000000000000000000..64edf7986b2b8776eacd11ee7c6827195fe43c7a --- /dev/null +++ b/templates/wei/weiclub_form.html @@ -0,0 +1,17 @@ +{% extends "member/noteowner_detail.html" %} +{% load crispy_forms_tags %} +{% load i18n %} + +{% block profile_info %} + {% if club %} + {% include "wei/weiclub_info.html" %} + {% endif %} +{% endblock %} + +{% block profile_content %} + <form method="post"> + {% csrf_token %} + {{ form|crispy }} + <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> + </form> +{% endblock %} diff --git a/templates/wei/weiclub_info.html b/templates/wei/weiclub_info.html new file mode 100644 index 0000000000000000000000000000000000000000..4af6afa04d62adc8e406ed7fb4b59ed8f65dde08 --- /dev/null +++ b/templates/wei/weiclub_info.html @@ -0,0 +1,79 @@ +{% load i18n static pretty_money perms %} +<div class="card bg-light shadow"> + <div class="card-header text-center"> + <h4>{{ club.name }} </h4> + </div> + <div class="card-top text-center"> + <a href="{% url 'member:club_update_pic' club.pk %}"> + <img src="{{ club.note.display_image.url }}" class="img-thumbnail mt-2" > + </a> + </div> + <div class="card-body" id="profile_infos"> + <dl class="row"> + <dt class="col-xl-6">{% trans 'name'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.name }}</dd> + + {% if club.require_memberships %} + <dt class="col-xl-6">{% trans 'date start'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.date_start }}</dd> + + <dt class="col-xl-6">{% trans 'date end'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.date_end }}</dd> + + <dt class="col-xl-6">{% trans 'year'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.year }}</dd> + + {% if club.membership_fee_paid == club.membership_fee_unpaid %} + <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd> + {% else %} + {% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %} + <dt class="col-xl-6">{% trans 'WEI fee / including BDE and Kfet fee (paid students)'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }} / {{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }}</dd> + {% endwith %} + + {% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %} + <dt class="col-xl-6">{% trans 'WEI fee / including BDE and Kfet fee (unpaid students)'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }} / {{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }}</dd> + {% endwith %} + {% endif %} + {% endif %} + + {% if "note.view_note"|has_perm:club.note %} + <dt class="col-xl-6">{% trans 'balance'|capfirst %}</dt> + <dd class="col-xl-6">{{ club.note.balance | pretty_money }}</dd> + {% endif %} + + {% if "note.change_alias"|has_perm:club.note.alias_set.first %} + <dt class="col-xl-4"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> + <dd class="col-xl-8 text-truncate">{{ club.note.alias_set.all|join:", " }}</dd> + {% endif %} + + <dt class="col-xl-4">{% trans 'email'|capfirst %}</dt> + <dd class="col-xl-8"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd> + </dl> + </div> + <div class="card-footer text-center"> + {% if True %} + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_list' %}"> {% trans "WEI list" %}</a> + {% endif %} + {% if club.is_current_wei %} + {% if can_add_first_year_member %} + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_register_1A' wei_pk=club.pk %}"> {% trans "Register 1A" %}</a> + {% endif %} + {% if can_add_any_member %} + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_register_2A' wei_pk=club.pk %}"> {% trans "Register 2A+" %}</a> + {% endif %} + {% if "wei.change_"|has_perm:club %} + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:wei_update' pk=club.pk %}"> {% trans "Edit" %}</a> + {% endif %} + {% if can_add_bus %} + <a class="btn btn-primary btn-sm my-1" href="{% url 'wei:add_bus' pk=club.pk %}"> {% trans "Add bus" %}</a> + {% endif %} + {% url 'wei:wei_detail' club.pk as club_detail_url %} + {%if request.path_info != club_detail_url %} + <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View WEI' %}</a> + {% endif %} + {% endif %} + </div> +</div> diff --git a/templates/wei/weiclub_list.html b/templates/wei/weiclub_list.html new file mode 100644 index 0000000000000000000000000000000000000000..0ed8e0aca84cc7622623e7e50bc691af0ed3a384 --- /dev/null +++ b/templates/wei/weiclub_list.html @@ -0,0 +1,70 @@ +{% extends "base.html" %} +{% load render_table from django_tables2 %} +{% load i18n %} +{% block content %} +<div class="row justify-content-center mb-4"> + <div class="col-md-10 text-center"> + <h4> + {% trans "search WEI" %} + </h4> + <input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved()" id="search_field"/> + <hr> + <a class="btn btn-primary text-center my-4" href="{% url 'wei:wei_create' %}">{% trans "Create WEI" %}</a> + </div> +</div> +<div class="row justify-content-center"> + <div class="col-md-10"> + <div class="card card-border shadow"> + <div class="card-header text-center"> + <h5> {% trans "WEI listing" %}</h5> + </div> + <div class="card-body px-0 py-0" id="club_table"> + {% render_table table %} + </div> + </div> + </div> +</div> + +{% endblock %} +{% block extrajavascript %} +<script type="text/javascript"> + +function getInfo() { + var asked = $("#search_field").val(); + /* on ne fait la requête que si on a au moins un caractère pour chercher */ + var sel = $(".table-row"); + if (asked.length >= 1) { + $.getJSON("/api/wei/club/?format=json&search="+asked, function(buttons){ + let selected_id = buttons.results.map((a => "#row-"+a.id)); + $(".table-row,"+selected_id.join()).show(); + $(".table-row").not(selected_id.join()).hide(); + + }); + }else{ + // show everything + $('table tr').show(); + } +} +var timer; +var timer_on; +/* Fontion appelée quand le texte change (délenche le timer) */ +function search_field_moved(secondfield) { + if (timer_on) { // Si le timer a déjà été lancé, on réinitialise le compteur. + clearTimeout(timer); + timer = setTimeout("getInfo(" + secondfield + ")", 300); + } + else { // Sinon, on le lance et on enregistre le fait qu'il tourne. + timer = setTimeout("getInfo(" + secondfield + ")", 300); + timer_on = true; + } +} + +// clickable row +$(document).ready(function($) { + $(".table-row").click(function() { + window.document.location = $(this).data("href"); + }); +}); + +</script> +{% endblock %} diff --git a/templates/wei/weiclub_tables.html b/templates/wei/weiclub_tables.html new file mode 100644 index 0000000000000000000000000000000000000000..d5a8ff0175c540193e6eca90f0ea48fb32d0c58d --- /dev/null +++ b/templates/wei/weiclub_tables.html @@ -0,0 +1,129 @@ +{% load render_table from django_tables2 %} +{% load i18n %} +<div class="card"> + <div class="card-header text-center"> + <h4>WEI</h4> + </div> + <div class="card-body"> + <p>LE WEI, c'est cool !</p> + + <p> + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore + magna aliqua. Dapibus ultrices in iaculis nunc sed augue. In hendrerit gravida rutrum quisque non tellus orci + ac. Massa vitae tortor condimentum lacinia quis vel eros. Elit ut aliquam purus sit amet. Aliquam faucibus + purus in massa tempor. Quisque id diam vel quam elementum pulvinar etiam non. Condimentum id venenatis a + condimentum vitae sapien pellentesque habitant. Egestas congue quisque egestas diam in. Vestibulum rhoncus + est pellentesque elit ullamcorper. Massa sed elementum tempus egestas sed sed. Sapien pellentesque habitant + morbi tristique. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi tincidunt ornare. Sed + adipiscing diam donec adipiscing. Leo integer malesuada nunc vel risus commodo viverra maecenas. + </p> + + <p> + Fusce id velit ut tortor pretium viverra suspendisse. Urna condimentum mattis pellentesque id nibh tortor id + aliquet. Vel facilisis volutpat est velit egestas dui. Turpis egestas sed tempus urna et pharetra pharetra + massa massa. Eget nunc scelerisque viverra mauris in. Etiam dignissim diam quis enim. Urna cursus eget nunc + scelerisque viverra mauris in aliquam sem. Amet porttitor eget dolor morbi non arcu risus quis. Ullamcorper + sit amet risus nullam eget felis. Ullamcorper eget nulla facilisi etiam dignissim diam quis. Enim nulla + aliquet porttitor lacus luctus accumsan tortor. Urna condimentum mattis pellentesque id nibh tortor id. + Feugiat in fermentum posuere urna nec. Risus nec feugiat in fermentum posuere urna nec tincidunt. Porttitor + massa id neque aliquam vestibulum morbi. Diam quis enim lobortis scelerisque. Ornare massa eget egestas + purus. Ut tortor pretium viverra suspendisse. Purus in mollis nunc sed. Tristique magna sit amet purus + gravida. + </p> + + <p> + Ut porttitor leo a diam sollicitudin tempor. Viverra nam libero justo laoreet sit amet cursus sit amet. + Lectus arcu bibendum at varius vel pharetra vel turpis nunc. Vivamus arcu felis bibendum ut tristique et + egestas quis ipsum. Parturient montes nascetur ridiculus mus mauris. A cras semper auctor neque vitae + tempus quam pellentesque. Netus et malesuada fames ac. Mauris in aliquam sem fringilla ut. Sapien + pellentesque habitant morbi tristique. Mauris sit amet massa vitae tortor condimentum. Sagittis + aliquam malesuada bibendum arcu vitae elementum curabitur vitae nunc. Amet consectetur adipiscing elit + duis tristique sollicitudin nibh sit. Nunc mattis enim ut tellus elementum. Sapien eget mi proin sed libero + enim. Pulvinar sapien et ligula ullamcorper. Nibh mauris cursus mattis molestie a iaculis at erat + pellentesque. Molestie at elementum eu facilisis. Velit sed ullamcorper morbi tincidunt. Quam vulputate + dignissim suspendisse in est ante. + </p> + + <p> + Id cursus metus aliquam eleifend mi. Eu turpis egestas pretium aenean pharetra magna ac. Faucibus ornare + suspendisse sed nisi lacus sed viverra tellus. Sed vulputate mi sit amet mauris commodo. Lacus laoreet non + curabitur gravida arcu ac. At ultrices mi tempus imperdiet nulla malesuada pellentesque elit eget. Fusce ut + placerat orci nulla pellentesque dignissim. Quis blandit turpis cursus in hac habitasse platea dictumst + quisque. Tellus id interdum velit laoreet id donec ultrices. Risus feugiat in ante metus dictum. Velit ut + tortor pretium viverra suspendisse. Lacus vel facilisis volutpat est velit egestas dui id. Nunc eget lorem + dolor sed viverra ipsum nunc aliquet bibendum. Varius quam quisque id diam vel quam. Orci dapibus ultrices + in iaculis. Neque gravida in fermentum et sollicitudin ac orci. + </p> + </div> + + {% if club.is_current_wei %} + <div class="card-footer text-center"> + {% if not my_registration %} + {% if not not_first_year %} + <a href="{% url "wei:wei_register_1A_myself" wei_pk=club.pk %}"><button class="btn btn-success">{% trans "Register to the WEI! – 1A" %}</button></a> + {% endif %} + <a href="{% url "wei:wei_register_2A_myself" wei_pk=club.pk %}"><button class="btn btn-success">{% trans "Register to the WEI! – 2A+" %}</button></a> + {% else %} + <a href="{% url "wei:wei_update_registration" pk=my_registration.pk %}"><button class="btn btn-warning">{% trans "Update my registration" %}</button></a> + {% endif %} + </div> + {% endif %} +</div> + +<hr> + +{% if buses.data %} + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold"> + <i class="fa fa-bus"></i> {% trans "Buses" %} + </a> + </div> + {% render_table buses %} + </div> + + <hr> +{% endif %} + +{% if member_list.data %} + <div class="card"> + <div class="card-header position-relative" id="clubListHeading"> + <a class="btn btn-link stretched-link font-weight-bold" href="{% url "wei:wei_memberships" pk=club.pk %}"> + <i class="fa fa-users"></i> {% trans "Members of the WEI" %} + </a> + </div> + {% render_table member_list %} + </div> + + <hr> +{% endif %} + +{% if history_list.data %} + <div class="card"> + <div class="card-header position-relative" id="historyListHeading"> + <a class="btn btn-link stretched-link font-weight-bold"> + <i class="fa fa-euro"></i> {% trans "Transaction history" %} + </a> + </div> + <div id="history_list"> + {% render_table history_list %} + </div> + </div> + + <hr> +{% endif %} + +{% if pre_registrations.data %} + <div class="card"> + <div class="card-header position-relative" id="historyListHeading"> + <a class="btn btn-link stretched-link font-weight-bold" href="{% url 'wei:wei_registrations' pk=club.pk %}"> + <i class="fa fa-user-plus"></i> {% trans "Unvalidated registrations" %} + </a> + </div> + <div id="history_list"> + {% render_table pre_registrations %} + </div> + </div> + + <hr> +{% endif %} diff --git a/templates/wei/weilist_sample.tex b/templates/wei/weilist_sample.tex new file mode 100644 index 0000000000000000000000000000000000000000..a2ff075535764c20f0ffa136294286ef3bf3fa40 --- /dev/null +++ b/templates/wei/weilist_sample.tex @@ -0,0 +1,51 @@ +\documentclass[landscape,10pt]{article} + +\usepackage[utf8]{inputenc} +\usepackage[T1]{fontenc} +\usepackage[french]{babel} + +\usepackage[margin=1.5cm]{geometry} +\usepackage{ltablex} +\usepackage{tabularx} + +\begin{document} +\begin{center} +\huge{Liste des inscrits \og {{ wei.name }} \fg{}} + +{% if bus %} +\LARGE{Bus {{ bus.name }}} + + +{% if team %} +\Large{Équipe {{ team.name }}} +{% endif %} +{% endif %} +\end{center} + +\begin{center} +\footnotesize +\begin{tabularx}{\textwidth}{ccccccccc} +\textbf{Nom} & \textbf{Prénom} & \textbf{Date de naissance} & \textbf{Genre} & \textbf{Section} + & \textbf{Bus} & \textbf{Équipe} & \textbf{Rôles} \\ +{% for membership in memberships %} +{{ membership.user.last_name|safe }} & {{ membership.user.first_name|safe }} & {{ membership.registration.birth_date|safe }} +& {{ membership.registration.get_gender_display|safe }} & {{ membership.user.profile.section_generated|safe }} & {{ membership.bus.name|safe }} +& {% if membership.team %}{{ membership.team.name|safe }}{% else %}--{% endif %} & {{ membership.roles.first|safe }} \\ +{% endfor %} +\end{tabularx} +\end{center} + +\footnotesize +Section = Année à l'ENS + code du département + +\begin{center} +\begin{tabular}{ccccccccc} +\textbf{Code} & A0 & A1 & A2 & A'2 & A''2 & A3 & B1234 & B1 \\ +\textbf{Département} & Informatique & Maths & Physique & Physique appliquée & Chimie & Biologie & SAPHIRE & Mécanique \\ +\hline +\textbf{Code} & B2 & B3 & B4 & C & D2 & D3 & E & EXT \\ +\textbf{Département} & Génie civil & Génie mécanique & EEA & Design & Éco-gestion & Sciences sociales & Anglais & Extérieur +\end{tabular} +\end{center} + +\end{document} diff --git a/templates/wei/weimembership_form.html b/templates/wei/weimembership_form.html new file mode 100644 index 0000000000000000000000000000000000000000..995b6c1a2ad2bc227bbaea07101835148898587c --- /dev/null +++ b/templates/wei/weimembership_form.html @@ -0,0 +1,209 @@ +{% extends "member/noteowner_detail.html" %} +{% load crispy_forms_tags %} +{% load i18n %} +{% load pretty_money %} +{% load perms %} + +{% block profile_info %} + {% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <div class="card bg-light shadow"> + <div class="card-header text-center"> + <h4>{% trans "Review registration" %}</h4> + </div> + <div class="card-body"> + <dl class="row"> + <dt class="col-xl-6">{% trans 'name'|capfirst %}, {% trans 'first name' %}</dt> + <dd class="col-xl-6">{{ registration.user.last_name }} {{ registration.user.first_name }}</dd> + + <dt class="col-xl-6">{% trans 'username'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.user.username }}</dd> + + <dt class="col-xl-6">{% trans 'email'|capfirst %}</dt> + <dd class="col-xl-6"><a href="mailto:{{ registration.user.email }}">{{ registration.user.email }}</a></dd> + + {% if not registration.user.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:registration.user.profile %} + <dd class="col-xl-12"> + <div class="alert alert-warning"> + {% trans "This user doesn't have confirmed his/her e-mail address." %} + <a href="{% url "registration:email_validation_resend" pk=registration.user.pk %}">{% trans "Click here to resend a validation link." %}</a> + </div> + </dd> + {% endif %} + + <dt class="col-xl-6">{% trans 'department'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.user.profile.department }}</dd> + + <dt class="col-xl-6">{% trans 'ENS year'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.user.profile.ens_year }}</dd> + + <dt class="col-xl-6">{% trans 'section'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.user.profile.section }}</dd> + + <dt class="col-xl-6">{% trans 'address'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.user.profile.address }}</dd> + + <dt class="col-xl-6">{% trans 'phone number'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.user.profile.phone_number }}</dd> + + <dt class="col-xl-6">{% trans 'paid'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.user.profile.paid|yesno }}</dd> + + <hr> + + <dt class="col-xl-6">{% trans 'first year'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.first_year|yesno }}</dd> + + <dt class="col-xl-6">{% trans 'gender'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.gender }}</dd> + + <dt class="col-xl-6">{% trans 'birth date'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.birth_date }}</dd> + + <dt class="col-xl-6">{% trans 'health issues'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.health_issues }}</dd> + + <dt class="col-xl-6">{% trans 'emergency contact name'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.emergency_contact_name }}</dd> + + <dt class="col-xl-6">{% trans 'emergency contact phone'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.emergency_contact_phone }}</dd> + + <dt class="col-xl-6">{% trans 'Register on the mailing list to stay informed of the events of the campus (1 mail/week)' %}</dt> + <dd class="col-xl-6">{{ registration.ml_events_registration|yesno }}</dd> + + <dt class="col-xl-6">{% trans 'Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)' %}</dt> + <dd class="col-xl-6">{{ registration.ml_sport_registration|yesno }}</dd> + + <dt class="col-xl-6">{% trans 'Register on the mailing list to stay informed of the art events of the campus (1 mail/week)' %}</dt> + <dd class="col-xl-6">{{ registration.ml_art_registration|yesno }}</dd> + + <dt class="col-xl-6">{% trans 'Payment from Société générale' %}</dt> + <dd class="col-xl-6">{{ registration.soge_credit|yesno }}</dd> + + {% if registration.first_year %} + <dt class="col-xl-6">{% trans 'Suggested bus from the survey:' %}</dt> + {% if registration.information.valid or True %} + <dd class="col-xl-6">{{ suggested_bus }}</dd> + + <div class="card-header text-center col-xl-12"> + <h5>{% trans 'Raw survey information' %}</h5> + </div> + + {% with information=registration.information %} + {% for key, value in information.items %} + <dt class="col-xl-6">{{ key }}</dt> + <dd class="col-xl-6">{{ value }}</dd> + {% endfor %} + {% endwith %} + {% else %} + <dd class="col-xl-6"><em>{% trans "The algorithm didn't run." %}</em></dd> + {% endif %} + {% else %} + <dt class="col-xl-6">{% trans 'caution check given'|capfirst %}</dt> + <dd class="col-xl-6">{{ registration.caution_check|yesno }}</dd> + + {% with information=registration.information %} + <dt class="col-xl-6">{% trans 'preferred bus'|capfirst %}</dt> + <dd class="col-xl-6">{{ information.preferred_bus_name|join:', ' }}</dd> + + <dt class="col-xl-6">{% trans 'preferred team'|capfirst %}</dt> + <dd class="col-xl-6">{{ information.preferred_team_name|join:', ' }}</dd> + + <dt class="col-xl-6">{% trans 'preferred roles'|capfirst %}</dt> + <dd class="col-xl-6">{{ information.preferred_roles_name|join:', ' }}</dd> + {% endwith %} + {% endif %} + </dl> + </div> + <div class="card-footer text-center"> + <a class="btn btn-primary btn-sm" href="{% url 'wei:wei_update_registration' registration.pk %}">{% trans 'Update registration' %}</a> + {% if "auth.change_user"|has_perm:registration.user %} + <a class="btn btn-primary btn-sm" href="{% url 'member:user_update_profile' registration.user.pk %}">{% trans 'Update Profile' %}</a> + {% endif %} + </div> + </div> + + <hr> + + <div class="card bg-light shadow"> + <form method="post"> + <div class="card-header text-center" > + <h4> {% trans "Validate registration" %}</h4> + </div> + {% if registration.is_validated %} + <div class="alert alert-warning"> + {% trans "The registration is already validated and can't be unvalidated." %} + {% trans "The user joined the bus" %} {{ registration.membership.bus }} + {% if registration.membership.team %}{% trans "in the team" %} {{ registration.membership.team }}, + {% else %}{% trans "in no team (staff)" %},{% endif %} {% trans "with the following roles:" %} {{ registration.membership.roles.all|join:", " }} + </div> + {% else %} + {% if registration.soge_credit %} + <div class="alert alert-warning"> + {% blocktrans %} + The WEI will be paid by Société générale. The membership will be created even if the bank didn't pay the BDE yet. + The membership transaction will be created but will be invalid. You will have to validate it once the bank + validated the creation of the account, or to change the payment method. + {% endblocktrans %} + </div> + {% else %} + {% if registration.user.note.balance < fee %} + <div class="alert alert-danger"> + {% with pretty_fee=fee|pretty_money %} + {% blocktrans with balance=registration.user.note.balance|pretty_money %} + The note don't have enough money ({{ balance }}, {{ pretty_fee }} required). The registration may fail. + {% endblocktrans %} + {% endwith %} + </div> + {% else %} + <div class="alert alert-success"> + {% trans "The note has enough money, the registration is possible." %} + </div> + {% endif %} + {% endif %} + + {% if not registration.caution_check and not registration.first_year %} + <div class="alert alert-danger"> + {% trans "The user didn't give her/his caution check." %} + </div> + {% endif %} + + {% if not kfet_member %} + <div class="alert alert-danger"> + {% url 'registration:future_user_detail' pk=registration.user.pk as future_user_detail %} + {% url 'member:club_detail' pk=club.parent_club.parent_club.pk as club_detail %} + {% blocktrans %} + This user is not a member of the Kfet club. Please adhere + <a href="{{ future_user_detail }}">here if he/she is in her/his first year</a> + or <a href="{{ club_detail }}">here if he/she was an old member</a> before you validate + the registration of the WEI. + {% endblocktrans %} + </div> + {% endif %} + + <div class="card-body" id="profile_infos"> + {% csrf_token %} + {{ form|crispy }} + </div> + <div class="card-footer text-center"> + <button class="btn btn-success btn-sm">{% trans 'Validate registration' %}</button> + </div> + {% endif %} + </form> + </div> +{% endblock %} + +{% block extrajavascript %} + <script> + function autocompleted(obj, prefix) { + console.log(prefix); + if (prefix === "id_bus") { + console.log(obj); + $("#id_team").attr('api_url', '/api/wei/team/?bus=' + obj.id); + } + } + </script> +{% endblock %} diff --git a/templates/wei/weimembership_list.html b/templates/wei/weimembership_list.html new file mode 100644 index 0000000000000000000000000000000000000000..d058211f32b861e9191815b559ae4911cbc75f59 --- /dev/null +++ b/templates/wei/weimembership_list.html @@ -0,0 +1,52 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/bus/équipe ..."> + <hr> + + <div id="memberships_table"> + {% if table.data %} + {% render_table table %} + {% else %} + <div class="alert alert-warning"> + {% trans "There is no membership found with this pattern." %} + </div> + {% endif %} + </div> + + <a href="{% url 'wei:wei_registrations' pk=club.pk %}"> + <button class="btn btn-block btn-info">{% trans "View unvalidated registrations..." %}</button> + </a> + + <hr> + + <a href="{% url 'wei:wei_memberships_pdf' wei_pk=club.pk %}" data-turbolinks="false"> + <button class="btn btn-block btn-danger"><i class="fa fa-file-pdf-o"></i> {% trans "View as PDF" %}</button> + </a> +{% endblock %} + +{% block extrajavascript %} +<script type="text/javascript"> + $(document).ready(function() { + let old_pattern = null; + let searchbar_obj = $("#searchbar"); + + function reloadTable() { + let pattern = searchbar_obj.val(); + + if (pattern === old_pattern) + return; + + $("#memberships_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #memberships_table"); + } + + searchbar_obj.keyup(reloadTable); + }); +</script> +{% endblock %} diff --git a/templates/wei/weiregistration_confirm_delete.html b/templates/wei/weiregistration_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..51bfc030d3c8633cdc7bd9483cd63cc6e7970c89 --- /dev/null +++ b/templates/wei/weiregistration_confirm_delete.html @@ -0,0 +1,37 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <div class="card bg-light shadow"> + <div class="card-header text-center"> + <h4>{% trans "Delete registration" %}</h4> + </div> + {% if object.is_validated %} + <div class="card-body"> + <div class="alert alert-danger"> + {% blocktrans %}This registration is already validated and can't be deleted.{% endblocktrans %} + </div> + </div> + {% else %} + <div class="card-body"> + <div class="alert alert-warning"> + {% with user=object.user wei_name=object.wei.name %} + {% blocktrans %}Are you sure you want to delete the registration of {{ user }} for the WEI {{ wei_name }}? This action can't be undone.{% endblocktrans %} + {% endwith %} + </div> + </div> + <div class="card-footer text-center"> + <form method="post"> + {% csrf_token %} + <a class="btn btn-warning" href="{% url 'wei:wei_update_registration' object.pk %}">{% trans "Update registration" %}</a> + <button class="btn btn-danger" type="submit">{% trans "Delete" %}</button> + </form> + </div> + {% endif %} + </div> +{% endblock %} diff --git a/templates/wei/weiregistration_form.html b/templates/wei/weiregistration_form.html new file mode 100644 index 0000000000000000000000000000000000000000..86aea555d094af848868acc5e7cb40412808d5dc --- /dev/null +++ b/templates/wei/weiregistration_form.html @@ -0,0 +1,16 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load crispy_forms_tags %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <form method="post"> + {% csrf_token %} + {{ form|crispy }} + {{ membership_form|crispy }} + <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> + </form> +{% endblock %} diff --git a/templates/wei/weiregistration_list.html b/templates/wei/weiregistration_list.html new file mode 100644 index 0000000000000000000000000000000000000000..05626dd20061b78034218146dfd3f54b1279f972 --- /dev/null +++ b/templates/wei/weiregistration_list.html @@ -0,0 +1,46 @@ +{% extends "member/noteowner_detail.html" %} +{% load i18n %} +{% load render_table from django_tables2 %} + +{% block profile_info %} +{% include "wei/weiclub_info.html" %} +{% endblock %} + +{% block profile_content %} + <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note ..."> + <hr> + + <div id="registrations_table"> + {% if table.data %} + {% render_table table %} + {% else %} + <div class="alert alert-warning"> + {% trans "There is no pre-registration found with this pattern." %} + </div> + {% endif %} + </div> + + <a href="{% url 'wei:wei_memberships' pk=club.pk %}"> + <button class="btn btn-block btn-info">{% trans "View validated memberships..." %}</button> + </a> +{% endblock %} + +{% block extrajavascript %} +<script type="text/javascript"> + $(document).ready(function() { + let old_pattern = null; + let searchbar_obj = $("#searchbar"); + + function reloadTable() { + let pattern = searchbar_obj.val(); + + if (pattern === old_pattern) + return; + + $("#registrations_table").load(location.pathname + "?search=" + pattern.replace(" ", "%20") + " #registrations_table"); + } + + searchbar_obj.keyup(reloadTable); + }); +</script> +{% endblock %} diff --git a/tox.ini b/tox.ini index 73cf05257e26a19198df57a0bd0c911df250e1bc..48bc32868994ebb2b5b9f61f7e421389be54a0cd 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ deps = pep8-naming pyflakes commands = - flake8 apps/activity apps/api apps/logs apps/member apps/note apps/permission apps/treasury + flake8 apps/activity apps/api apps/logs apps/member apps/note apps/permission apps/treasury apps/wei [flake8] # Ignore too many errors, should be reduced in the future