diff --git a/.env_example b/.env_example index d903b724..9afdd138 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 0c0cfb2e..be27e63e 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 dcbd3c9d..dced014a 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 29f04b39..cab229c4 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 14746929..12386bd1 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -45,8 +45,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 @@ -153,9 +153,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 67fdba30..03d6bd68 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 6fe95f5a..e546d652 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 3a022434..17b8f044 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 381314b2..04742f32 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/models/transactions.py b/apps/note/models/transactions.py index 83f8f914..081f6022 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): """ diff --git a/apps/note/signals.py b/apps/note/signals.py index 37737a45..0baa39e6 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/views.py b/apps/note/views.py index 88d47847..b26e6cf1 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -30,7 +30,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): """ @@ -93,7 +93,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 04d93528..3dd47fa7 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -36,13 +36,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 +57,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 +67,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 +85,9 @@ class PermissionBackend(ModelBackend): NoteClub=NoteClub, NoteSpecial=NoteSpecial, F=F, - Q=Q + Q=Q, + now=datetime.datetime.now(), + today=datetime.date.today(), ) yield permission diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index d7eca508..bad0ec80 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 81174389..fe18c226 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 bf54b72f..cac0a8a0 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 00000000..c571c520 --- /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 bbd9872f..cbd26a19 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 cba5c2ae..46559487 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 35391b05..2c91a604 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 b9fdced3..f0aa4269 160000 --- a/apps/scripts +++ b/apps/scripts @@ -1 +1 @@ -Subproject commit b9fdced3c2ce34168b8f0d6004a20a69ca16e0de +Subproject commit f0aa426950b9b867bf99233795e260871be2cb99 diff --git a/apps/treasury/admin.py b/apps/treasury/admin.py index abeec3e3..9c8aaf2e 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 f1bbef75..0acb0aa1 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 30ac00e1..70d81f77 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 7a70fd24..ee97e6ac 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 ca1da3a4..5628504b 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 1ecc04db..9f4e43e6 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 d44cc414..8606fb5b 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//', LinkTransactionToRemittanceView.as_view(), name='link_transaction'), path('remittance/unlink_transaction//', UnlinkTransactionToRemittanceView.as_view(), name='unlink_transaction'), + + path('soge-credits/list/', SogeCreditListView.as_view(), name='soge_credits'), + path('soge-credits/manage//', SogeCreditManageView.as_view(), name='manage_soge_credit'), ] diff --git a/apps/treasury/views.py b/apps/treasury/views.py index 7361d1d2..f42e5e77 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 00000000..ad360dae --- /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 00000000..f93a44ed --- /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 00000000..e69de29b diff --git a/apps/wei/api/serializers.py b/apps/wei/api/serializers.py new file mode 100644 index 00000000..69254b75 --- /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 00000000..713f5c7f --- /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 00000000..aaa1f141 --- /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 00000000..23351283 --- /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 00000000..af948157 --- /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 00000000..96555372 --- /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 00000000..1e1dca56 --- /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 00000000..f43dafc2 --- /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 00000000..4f60f6d4 --- /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 00000000..e69de29b diff --git a/apps/wei/models.py b/apps/wei/models.py new file mode 100644 index 00000000..9cee0d61 --- /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 00000000..36d09342 --- /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 00000000..7cf91a60 --- /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//', WEIDetailView.as_view(), name="wei_detail"), + path('update//', WEIUpdateView.as_view(), name="wei_update"), + path('detail//registrations/', WEIRegistrationsView.as_view(), name="wei_registrations"), + path('detail//memberships/', WEIMembershipsView.as_view(), name="wei_memberships"), + path('detail//memberships/pdf/', MemberListRenderView.as_view(), name="wei_memberships_pdf"), + path('detail//memberships/pdf//', MemberListRenderView.as_view(), + name="wei_memberships_bus_pdf"), + path('detail//memberships/pdf///', MemberListRenderView.as_view(), + name="wei_memberships_team_pdf"), + path('add-bus//', BusCreateView.as_view(), name="add_bus"), + path('manage-bus//', BusManageView.as_view(), name="manage_bus"), + path('update-bus//', BusUpdateView.as_view(), name="update_bus"), + path('add-bus-team//', BusTeamCreateView.as_view(), name="add_team"), + path('manage-bus-team//', BusTeamManageView.as_view(), name="manage_bus_team"), + path('update-bus-team//', BusTeamUpdateView.as_view(), name="update_bus_team"), + path('register//1A/', WEIRegister1AView.as_view(), name="wei_register_1A"), + path('register//2A+/', WEIRegister2AView.as_view(), name="wei_register_2A"), + path('register//1A/myself/', WEIRegister1AView.as_view(), name="wei_register_1A_myself"), + path('register//2A+/myself/', WEIRegister2AView.as_view(), name="wei_register_2A_myself"), + path('edit-registration//', WEIUpdateRegistrationView.as_view(), name="wei_update_registration"), + path('delete-registration//', WEIDeleteRegistrationView.as_view(), name="wei_delete_registration"), + path('validate//', WEIValidateRegistrationView.as_view(), name="validate_registration"), + path('survey//', WEISurveyView.as_view(), name="wei_survey"), + path('survey//end/', WEISurveyEndView.as_view(), name="wei_survey_end"), + path('detail//closed/', WEIClosedView.as_view(), name="wei_closed"), +] diff --git a/apps/wei/views.py b/apps/wei/views.py new file mode 100644 index 00000000..597a44d4 --- /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 f616ffd6..408123ff 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-26 00:45+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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 "" @@ -202,7 +209,7 @@ msgstr "" 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 "" @@ -244,6 +251,7 @@ 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 "Design (C)" +msgstr "" + +#: apps/member/models.py:64 +msgid "Economy-management (D2)" +msgstr "" + +#: apps/member/models.py:65 +msgid "Social sciences (D3)" +msgstr "" + +#: apps/member/models.py:66 +msgid "English (E)" +msgstr "" + +#: 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:68 -msgid "Société générale" -msgstr "" - -#: apps/member/models.py:69 -msgid "Has the user ever be paid by the Société générale?" -msgstr "" - -#: apps/member/models.py:74 apps/member/models.py:75 +#: 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 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,12 +797,16 @@ 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 "" @@ -731,12 +818,12 @@ msgstr "" 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,59 +840,106 @@ 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 "" @@ -817,9 +951,10 @@ 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 -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." +#: apps/treasury/tables.py:117 +msgid "Valid" msgstr "" -#: note_kfet/settings/base.py:153 -msgid "German" +#: 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 "" +"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 "" + +#: 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 %(request_path)s 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 "" @@ -1064,7 +1521,11 @@ msgstr "" 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 "" -"

Log In Successful

You have successfully logged into the Central " -"Authentication Service.
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 this link to sign up." -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,9 +1651,8 @@ 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 @@ -1314,6 +1737,22 @@ msgstr "" 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 +1774,7 @@ msgid "" msgstr "" #: templates/registration/future_profile_detail.html:56 +#: templates/wei/weiregistration_confirm_delete.html:12 msgid "Delete registration" msgstr "" @@ -1343,6 +1783,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 +1792,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 +1805,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 +1818,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 +1905,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 +1929,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 +1960,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" +" here if he/" +"she is in her/his first year\n" +" or here if he/she " +"was an old member 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 27636a65..65c41e93 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 , 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-26 00:45+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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" @@ -198,7 +210,7 @@ msgstr "Solde du compte" 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" @@ -240,6 +252,7 @@ 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 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,12 +805,16 @@ 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" @@ -734,15 +826,15 @@ msgstr "Transférer de l'argent" 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,59 +848,117 @@ 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 "" @@ -822,9 +972,10 @@ 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 -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." -msgstr "" +#: apps/treasury/tables.py:117 +msgid "Valid" +msgstr "Valide" -#: note_kfet/settings/base.py:153 -msgid "German" +#: 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 "" +"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." + +#: 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)" + +#: 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 "English" -msgstr "" +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 %(request_path)s was not found on the server." +msgstr "" +"The chemin demandé %(request_path)s 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" @@ -1069,15 +1570,21 @@ msgstr "Clubs" 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 "" -"

Log In Successful

You have successfully logged into the Central " -"Authentication Service.
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 this link to sign up." -msgstr "" -"Si vous n'avez pas de compte Note Kfet, veuillez suivre ce lien pour vous inscrire." - -#: 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,10 +1705,9 @@ 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" @@ -1321,6 +1791,22 @@ msgstr "Le bouton a bien été supprimé" 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 +1820,19 @@ msgstr "Vous pouvez désormais vous connecter." 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 +1841,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 +1850,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 +1874,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 +1930,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 +2012,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 +2043,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" +" here if he/" +"she is in her/his first year\n" +" or here if he/she " +"was an old member 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" +"ici s'iel est en première année\n" +"ou ici s'iel est un ancien membre 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/note_kfet/inputs.py b/note_kfet/inputs.py index b3cccbce..1a17d5ac 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 1ab06b9c..73aae469 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 283f8e56..5ed8b1d8 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 66ad4fd4..0a0c21e1 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 5be8a3b8..7b8d37ef 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 90d44a07..9717087a 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 d468d2d5..c7c696fb 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 00000000..a3c2de62 --- /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 00000000..dbbd2342 --- /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( [, ]) + // + + 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 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 00000000..b80f38eb --- /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;ivs[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]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=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 8c3f6f09..aa1d220c 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 bb73b328..f9f040c1 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -312,7 +312,7 @@ function in_validate(id, validated) { else invalidity_reason = null; - $("#validate_" + id).html("⟳ ..."); + $("#validate_" + id).html(""); // Perform a PATCH request to the API in order to update the transaction // If the user has insuffisent rights, an error message will appear diff --git a/templates/400.html b/templates/400.html new file mode 100644 index 00000000..35606525 --- /dev/null +++ b/templates/400.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Bad request" %}

+ {% 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 00000000..317865f2 --- /dev/null +++ b/templates/403.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Permission denied" %}

+ {% blocktrans %}You don't have the right to perform this request.{% endblocktrans %} + {% if exception %} +
+ {% trans "Exception message:" %} {{ exception }} +
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/404.html b/templates/404.html new file mode 100644 index 00000000..8477f914 --- /dev/null +++ b/templates/404.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Page not found" %}

+ {% blocktrans %}The requested path {{ request_path }} was not found on the server.{% endblocktrans %} + {% if exception != "Resolver404" %} +
+ {% trans "Exception message:" %} {{ exception }} +
+ {% endif %} +{% endblock %} \ No newline at end of file diff --git a/templates/500.html b/templates/500.html new file mode 100644 index 00000000..50b62bc2 --- /dev/null +++ b/templates/500.html @@ -0,0 +1,8 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} +

{% trans "Server error" %}

+ {% 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 3c2c637f..646f27e9 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"> - + + {# JQuery, Bootstrap and Turbolinks JavaScript #} {% endblock %} diff --git a/templates/member/profile_info.html b/templates/member/profile_info.html index 74856355..7be10ba1 100644 --- a/templates/member/profile_info.html +++ b/templates/member/profile_info.html @@ -17,12 +17,14 @@
{% trans 'username'|capfirst %}
{{ object.username }}
-
{% trans 'password'|capfirst %}
-
- - {% trans 'Change password' %} - -
+ {% if object.pk == user.pk %} +
{% trans 'password'|capfirst %}
+
+ + {% trans 'Change password' %} + +
+ {% endif %}
{% trans 'section'|capfirst %}
{{ object.profile.section }}
diff --git a/templates/member/user_list.html b/templates/member/user_list.html index 0bcd7e89..018c479d 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 %} @@ -11,7 +13,7 @@ {% render_table table %} {% else %}
- {% trans "There is no pending user with this pattern." %} + {% trans "There is no user with this pattern." %}
{% endif %} diff --git a/templates/permission/all_rights.html b/templates/permission/all_rights.html new file mode 100644 index 00000000..85b47597 --- /dev/null +++ b/templates/permission/all_rights.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% load i18n %} + +{% block content %} + {% if user.is_authenticated %} +
+ +
+ {% endif %} +
    + {% regroup active_memberships by roles as memberships_per_role %} + {% for role in roles %} +
  • + {{ role }} {% if role.weirole %}(Pour le WEI){% endif %} + {% if role.clubs %} +
    + {% trans "Own this role in the clubs" %} {{ role.clubs|join:", " }} +
    + {% endif %} +
      + {% for permission in role.permissions.permissions.all %} +
    • {{ permission }} ({{ permission.type }} {{ permission.model }})
    • + {% empty %} + {% trans "No associated permission" %} + {% endfor %} +
    +
  • + {% endfor %} +
+{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/registration/email_validation_complete.html b/templates/registration/email_validation_complete.html index 4835cfa1..b54432f3 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 log in.{% 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 8c78fb8d..1d2d08c7 100644 --- a/templates/registration/future_profile_detail.html +++ b/templates/registration/future_profile_detail.html @@ -31,13 +31,6 @@ {% endif %} -
{% trans 'password'|capfirst %}
-
- - {% trans 'Change password' %} - -
-
{% trans 'section'|capfirst %}
{{ object.profile.section }}
diff --git a/templates/registration/login.html b/templates/registration/login.html index 175d37e0..32e54e00 100644 --- a/templates/registration/login.html +++ b/templates/registration/login.html @@ -16,13 +16,6 @@ SPDX-License-Identifier: GPL-2.0-or-later {% endblocktrans %}

{% endif %} - {%url 'cas_login' as cas_url %} - {% if cas_url %} -
- {% trans "You can also register via the central authentification server " %} - {% trans "using this link "%} -
- {%endif%}
{% csrf_token %} {{ form | crispy }} diff --git a/templates/registration/signup.html b/templates/registration/signup.html index d7b3c23e..76f88a49 100644 --- a/templates/registration/signup.html +++ b/templates/registration/signup.html @@ -5,13 +5,18 @@ {% block title %}{% trans "Sign up" %}{% endblock %} {% block content %} -

{% trans "Sign up" %}

- - {% csrf_token %} - {{ form|crispy }} - {{ profile_form|crispy }} - -
+

{% trans "Sign up" %}

+ +
+ {% 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 %} +
+ +
+ {% csrf_token %} + {{ form|crispy }} + {{ profile_form|crispy }} + +
{% endblock %} diff --git a/templates/scripts/mail-error500.html b/templates/scripts/mail-error500.html new file mode 100644 index 00000000..f4bb796f --- /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 00000000..b7aabd39 --- /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 f14d278d..4e95816e 100644 --- a/templates/treasury/invoice_list.html +++ b/templates/treasury/invoice_list.html @@ -12,6 +12,9 @@ {% trans "Remittance" %}s + + {% trans "Société générale credits" %} + diff --git a/templates/treasury/remittance_list.html b/templates/treasury/remittance_list.html index 8bc634e4..916c47d8 100644 --- a/templates/treasury/remittance_list.html +++ b/templates/treasury/remittance_list.html @@ -12,6 +12,9 @@ {% trans "Remittance" %}s + + {% trans "Société générale credits" %} + diff --git a/templates/treasury/sogecredit_detail.html b/templates/treasury/sogecredit_detail.html new file mode 100644 index 00000000..eafece53 --- /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 %} +
+
+

{% trans "Credit from the Société générale" %}

+
+
+
+
{% trans 'user'|capfirst %}
+
{{ object.user }}
+ + {% if "note.view_note_balance"|has_perm:object.user.note %} +
{% trans 'balance'|capfirst %}
+
{{ object.user.note.balance|pretty_money }}
+ {% endif %} + +
{% trans 'transactions'|capfirst %}
+
+ {% for transaction in object.transactions.all %} + {{ transaction.membership.club }} ({{ transaction.amount|pretty_money }})
+ {% endfor %} +
+ +
{% trans 'total amount'|capfirst %}
+
{{ object.amount|pretty_money }}
+
+
+ +
+ {% 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".' %} +
+ + +
+{% endblock %} diff --git a/templates/treasury/sogecredit_list.html b/templates/treasury/sogecredit_list.html new file mode 100644 index 00000000..c8a9207b --- /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 %} + + + + +
+ +
+
+ +
+ {% if table.data %} + {% render_table table %} + {% else %} +
+ {% trans "There is no matched user that have asked for a Société générale credit." %} +
+ {% endif %} +
+{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/wei/bus_detail.html b/templates/wei/bus_detail.html new file mode 100644 index 00000000..1b335be8 --- /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 00000000..4c7b22ce --- /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 %} +
+{% csrf_token %} +{{ form|crispy }} + +
+{% endblock %} diff --git a/templates/wei/bus_tables.html b/templates/wei/bus_tables.html new file mode 100644 index 00000000..50eccf6a --- /dev/null +++ b/templates/wei/bus_tables.html @@ -0,0 +1,50 @@ +{% load render_table from django_tables2 %} +{% load i18n %} + +
+
+

{{ object.name }}

+
+ + +
+ {{ object.description }} +
+ + +
+ +
+ +{% if teams.data %} +
+ + {% render_table teams %} +
+ +
+{% endif %} + +{% if memberships.data %} +
+ + {% render_table memberships %} +
+ +
+ + + + +{% endif %} diff --git a/templates/wei/busteam_detail.html b/templates/wei/busteam_detail.html new file mode 100644 index 00000000..481e24ea --- /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 00000000..4c7b22ce --- /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 %} +
+{% csrf_token %} +{{ form|crispy }} + +
+{% endblock %} diff --git a/templates/wei/busteam_tables.html b/templates/wei/busteam_tables.html new file mode 100644 index 00000000..3135158f --- /dev/null +++ b/templates/wei/busteam_tables.html @@ -0,0 +1,54 @@ +{% load render_table from django_tables2 %} +{% load i18n %} + +
+
+

{{ bus.name }}

+
+ + +
+ {{ bus.description }} +
+ + +
+ +
+ +
+
+

{{ object.name }}

+
+ + +
+ {{ object.description }} +
+ + +
+ +
+ +{% if memberships.data or True %} +
+ + {% render_table memberships %} +
+ +
+ + + + +{% endif %} diff --git a/templates/wei/survey.html b/templates/wei/survey.html new file mode 100644 index 00000000..36553849 --- /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 %} +
+
+

{% trans "Survey WEI" %}

+
+
+
+
{% trans 'user'|capfirst %}
+
{{ object.user }}
+
+ +
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+{% endblock %} diff --git a/templates/wei/survey_closed.html b/templates/wei/survey_closed.html new file mode 100644 index 00000000..28c182ef --- /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 %} +
+
+

{% trans "Survey WEI" %}

+
+
+

+ {% trans "The inscription for this WEI are now closed." %} +

+
+ +
+{% endblock %} diff --git a/templates/wei/survey_end.html b/templates/wei/survey_end.html new file mode 100644 index 00000000..888290f7 --- /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 %} +
+
+

{% trans "Survey WEI" %}

+
+
+

+ {% trans "The survey is now ended. Your answers have been saved." %} +

+
+
+{% endblock %} diff --git a/templates/wei/weiclub_detail.html b/templates/wei/weiclub_detail.html new file mode 100644 index 00000000..72c70aa5 --- /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 %} + +{% endblock %} diff --git a/templates/wei/weiclub_form.html b/templates/wei/weiclub_form.html new file mode 100644 index 00000000..64edf798 --- /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 %} +
+ {% csrf_token %} + {{ form|crispy }} + +
+{% endblock %} diff --git a/templates/wei/weiclub_info.html b/templates/wei/weiclub_info.html new file mode 100644 index 00000000..4af6afa0 --- /dev/null +++ b/templates/wei/weiclub_info.html @@ -0,0 +1,79 @@ +{% load i18n static pretty_money perms %} +
+
+

{{ club.name }}

+
+
+ + + +
+
+
+
{% trans 'name'|capfirst %}
+
{{ club.name }}
+ + {% if club.require_memberships %} +
{% trans 'date start'|capfirst %}
+
{{ club.date_start }}
+ +
{% trans 'date end'|capfirst %}
+
{{ club.date_end }}
+ +
{% trans 'year'|capfirst %}
+
{{ club.year }}
+ + {% if club.membership_fee_paid == club.membership_fee_unpaid %} +
{% trans 'membership fee'|capfirst %}
+
{{ club.membership_fee_paid|pretty_money }}
+ {% else %} + {% with bde_kfet_fee=club.parent_club.membership_fee_paid|add:club.parent_club.parent_club.membership_fee_paid %} +
{% trans 'WEI fee / including BDE and Kfet fee (paid students)'|capfirst %}
+
{{ club.membership_fee_paid|pretty_money }} / {{ club.membership_fee_paid|add:bde_kfet_fee|pretty_money }}
+ {% endwith %} + + {% with bde_kfet_fee=club.parent_club.membership_fee_unpaid|add:club.parent_club.parent_club.membership_fee_unpaid %} +
{% trans 'WEI fee / including BDE and Kfet fee (unpaid students)'|capfirst %}
+
{{ club.membership_fee_unpaid|pretty_money }} / {{ club.membership_fee_unpaid|add:bde_kfet_fee|pretty_money }}
+ {% endwith %} + {% endif %} + {% endif %} + + {% if "note.view_note"|has_perm:club.note %} +
{% trans 'balance'|capfirst %}
+
{{ club.note.balance | pretty_money }}
+ {% endif %} + + {% if "note.change_alias"|has_perm:club.note.alias_set.first %} +
{% trans 'aliases'|capfirst %}
+
{{ club.note.alias_set.all|join:", " }}
+ {% endif %} + +
{% trans 'email'|capfirst %}
+
{{ club.email }}
+
+
+ +
diff --git a/templates/wei/weiclub_list.html b/templates/wei/weiclub_list.html new file mode 100644 index 00000000..0ed8e0ac --- /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 %} +
+
+

+ {% trans "search WEI" %} +

+ +
+ {% trans "Create WEI" %} +
+
+
+
+
+
+
{% trans "WEI listing" %}
+
+
+ {% render_table table %} +
+
+
+
+ +{% endblock %} +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/wei/weiclub_tables.html b/templates/wei/weiclub_tables.html new file mode 100644 index 00000000..d5a8ff01 --- /dev/null +++ b/templates/wei/weiclub_tables.html @@ -0,0 +1,129 @@ +{% load render_table from django_tables2 %} +{% load i18n %} +
+
+

WEI

+
+
+

LE WEI, c'est cool !

+ +

+ 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. +

+ +

+ 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. +

+ +

+ 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. +

+ +

+ 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. +

+
+ + {% if club.is_current_wei %} + + {% endif %} +
+ +
+ +{% if buses.data %} +
+ + {% render_table buses %} +
+ +
+{% endif %} + +{% if member_list.data %} +
+ + {% render_table member_list %} +
+ +
+{% endif %} + +{% if history_list.data %} +
+ +
+ {% render_table history_list %} +
+
+ +
+{% endif %} + +{% if pre_registrations.data %} +
+ +
+ {% render_table pre_registrations %} +
+
+ +
+{% endif %} diff --git a/templates/wei/weilist_sample.tex b/templates/wei/weilist_sample.tex new file mode 100644 index 00000000..a2ff0755 --- /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 00000000..995b6c1a --- /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 %} +
+
+

{% trans "Review registration" %}

+
+
+
+
{% trans 'name'|capfirst %}, {% trans 'first name' %}
+
{{ registration.user.last_name }} {{ registration.user.first_name }}
+ +
{% trans 'username'|capfirst %}
+
{{ registration.user.username }}
+ +
{% trans 'email'|capfirst %}
+
{{ registration.user.email }}
+ + {% if not registration.user.profile.email_confirmed and "member.change_profile_email_confirmed"|has_perm:registration.user.profile %} +
+
+ {% trans "This user doesn't have confirmed his/her e-mail address." %} + {% trans "Click here to resend a validation link." %} +
+
+ {% endif %} + +
{% trans 'department'|capfirst %}
+
{{ registration.user.profile.department }}
+ +
{% trans 'ENS year'|capfirst %}
+
{{ registration.user.profile.ens_year }}
+ +
{% trans 'section'|capfirst %}
+
{{ registration.user.profile.section }}
+ +
{% trans 'address'|capfirst %}
+
{{ registration.user.profile.address }}
+ +
{% trans 'phone number'|capfirst %}
+
{{ registration.user.profile.phone_number }}
+ +
{% trans 'paid'|capfirst %}
+
{{ registration.user.profile.paid|yesno }}
+ +
+ +
{% trans 'first year'|capfirst %}
+
{{ registration.first_year|yesno }}
+ +
{% trans 'gender'|capfirst %}
+
{{ registration.gender }}
+ +
{% trans 'birth date'|capfirst %}
+
{{ registration.birth_date }}
+ +
{% trans 'health issues'|capfirst %}
+
{{ registration.health_issues }}
+ +
{% trans 'emergency contact name'|capfirst %}
+
{{ registration.emergency_contact_name }}
+ +
{% trans 'emergency contact phone'|capfirst %}
+
{{ registration.emergency_contact_phone }}
+ +
{% trans 'Register on the mailing list to stay informed of the events of the campus (1 mail/week)' %}
+
{{ registration.ml_events_registration|yesno }}
+ +
{% trans 'Register on the mailing list to stay informed of the sport events of the campus (1 mail/week)' %}
+
{{ registration.ml_sport_registration|yesno }}
+ +
{% trans 'Register on the mailing list to stay informed of the art events of the campus (1 mail/week)' %}
+
{{ registration.ml_art_registration|yesno }}
+ +
{% trans 'Payment from Société générale' %}
+
{{ registration.soge_credit|yesno }}
+ + {% if registration.first_year %} +
{% trans 'Suggested bus from the survey:' %}
+ {% if registration.information.valid or True %} +
{{ suggested_bus }}
+ +
+
{% trans 'Raw survey information' %}
+
+ + {% with information=registration.information %} + {% for key, value in information.items %} +
{{ key }}
+
{{ value }}
+ {% endfor %} + {% endwith %} + {% else %} +
{% trans "The algorithm didn't run." %}
+ {% endif %} + {% else %} +
{% trans 'caution check given'|capfirst %}
+
{{ registration.caution_check|yesno }}
+ + {% with information=registration.information %} +
{% trans 'preferred bus'|capfirst %}
+
{{ information.preferred_bus_name|join:', ' }}
+ +
{% trans 'preferred team'|capfirst %}
+
{{ information.preferred_team_name|join:', ' }}
+ +
{% trans 'preferred roles'|capfirst %}
+
{{ information.preferred_roles_name|join:', ' }}
+ {% endwith %} + {% endif %} +
+
+ +
+ +
+ +
+
+
+

{% trans "Validate registration" %}

+
+ {% if registration.is_validated %} +
+ {% 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:", " }} +
+ {% else %} + {% if registration.soge_credit %} +
+ {% 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 %} +
+ {% else %} + {% if registration.user.note.balance < fee %} +
+ {% 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 %} +
+ {% else %} +
+ {% trans "The note has enough money, the registration is possible." %} +
+ {% endif %} + {% endif %} + + {% if not registration.caution_check and not registration.first_year %} +
+ {% trans "The user didn't give her/his caution check." %} +
+ {% endif %} + + {% if not kfet_member %} +
+ {% 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 + here if he/she is in her/his first year + or here if he/she was an old member before you validate + the registration of the WEI. + {% endblocktrans %} +
+ {% endif %} + +
+ {% csrf_token %} + {{ form|crispy }} +
+ + {% endif %} +
+
+{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/wei/weimembership_list.html b/templates/wei/weimembership_list.html new file mode 100644 index 00000000..d058211f --- /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 %} + +
+ +
+ {% if table.data %} + {% render_table table %} + {% else %} +
+ {% trans "There is no membership found with this pattern." %} +
+ {% endif %} +
+ + + + + +
+ + + + +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/templates/wei/weiregistration_confirm_delete.html b/templates/wei/weiregistration_confirm_delete.html new file mode 100644 index 00000000..51bfc030 --- /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 %} +
+
+

{% trans "Delete registration" %}

+
+ {% if object.is_validated %} +
+
+ {% blocktrans %}This registration is already validated and can't be deleted.{% endblocktrans %} +
+
+ {% else %} +
+
+ {% 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 %} +
+
+ + {% endif %} +
+{% endblock %} diff --git a/templates/wei/weiregistration_form.html b/templates/wei/weiregistration_form.html new file mode 100644 index 00000000..86aea555 --- /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 %} +
+ {% csrf_token %} + {{ form|crispy }} + {{ membership_form|crispy }} + +
+{% endblock %} diff --git a/templates/wei/weiregistration_list.html b/templates/wei/weiregistration_list.html new file mode 100644 index 00000000..05626dd2 --- /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 %} + +
+ +
+ {% if table.data %} + {% render_table table %} + {% else %} +
+ {% trans "There is no pre-registration found with this pattern." %} +
+ {% endif %} +
+ + + + +{% endblock %} + +{% block extrajavascript %} + +{% endblock %} diff --git a/tox.ini b/tox.ini index 73cf0525..48bc3286 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