mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Merge branch 'new-note-type' into 'master'
Memberships Closes #43 and #16 See merge request bde/nk20!71
This commit is contained in:
		| @@ -73,15 +73,6 @@ class Activity(models.Model): | ||||
|         verbose_name=_('organizer'), | ||||
|     ) | ||||
|  | ||||
|     note = models.ForeignKey( | ||||
|         'note.Note', | ||||
|         on_delete=models.PROTECT, | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         related_name='+', | ||||
|         verbose_name=_('note'), | ||||
|     ) | ||||
|  | ||||
|     attendees_club = models.ForeignKey( | ||||
|         'member.Club', | ||||
|         on_delete=models.PROTECT, | ||||
| @@ -160,9 +151,7 @@ class Entry(models.Model): | ||||
|         if insert and self.guest: | ||||
|             GuestTransaction.objects.create( | ||||
|                 source=self.note, | ||||
|                 source_alias=self.note.user.username, | ||||
|                 destination=self.note, | ||||
|                 destination_alias=self.activity.organizer.name, | ||||
|                 destination=self.activity.organizer.note, | ||||
|                 quantity=1, | ||||
|                 amount=self.activity.activity_type.guest_entry_fee, | ||||
|                 reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from datetime import datetime, timezone | ||||
|  | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| @@ -11,13 +12,14 @@ from django.utils.translation import gettext_lazy as _ | ||||
| from django_tables2.views import SingleTableView | ||||
| from note.models import NoteUser, Alias, NoteSpecial | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.views import ProtectQuerysetMixin | ||||
|  | ||||
| from .forms import ActivityForm, GuestForm | ||||
| from .models import Activity, Guest, Entry | ||||
| from .tables import ActivityTable, GuestTable, EntryTable | ||||
|  | ||||
|  | ||||
| class ActivityCreateView(LoginRequiredMixin, CreateView): | ||||
| class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     model = Activity | ||||
|     form_class = ActivityForm | ||||
|  | ||||
| @@ -30,13 +32,12 @@ class ActivityCreateView(LoginRequiredMixin, CreateView): | ||||
|         return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk}) | ||||
|  | ||||
|  | ||||
| class ActivityListView(LoginRequiredMixin, SingleTableView): | ||||
| class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     model = Activity | ||||
|     table_class = ActivityTable | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         return super().get_queryset()\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).reverse() | ||||
|         return super().get_queryset().reverse() | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         ctx = super().get_context_data(**kwargs) | ||||
| @@ -50,7 +51,7 @@ class ActivityListView(LoginRequiredMixin, SingleTableView): | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class ActivityDetailView(LoginRequiredMixin, DetailView): | ||||
| class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     model = Activity | ||||
|     context_object_name = "activity" | ||||
|  | ||||
| @@ -66,7 +67,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView): | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class ActivityUpdateView(LoginRequiredMixin, UpdateView): | ||||
| class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     model = Activity | ||||
|     form_class = ActivityForm | ||||
|  | ||||
| @@ -74,18 +75,20 @@ class ActivityUpdateView(LoginRequiredMixin, UpdateView): | ||||
|         return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]}) | ||||
|  | ||||
|  | ||||
| class ActivityInviteView(LoginRequiredMixin, CreateView): | ||||
| class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     model = Guest | ||||
|     form_class = GuestForm | ||||
|     template_name = "activity/activity_invite.html" | ||||
|  | ||||
|     def get_form(self, form_class=None): | ||||
|         form = super().get_form(form_class) | ||||
|         form.activity = Activity.objects.get(pk=self.kwargs["pk"]) | ||||
|         form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ | ||||
|             .get(pk=self.kwargs["pk"]) | ||||
|         return form | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         form.instance.activity = Activity.objects.get(pk=self.kwargs["pk"]) | ||||
|         form.instance.activity = Activity.objects\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"]) | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|     def get_success_url(self, **kwargs): | ||||
| @@ -98,7 +101,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         ctx = super().get_context_data(**kwargs) | ||||
|  | ||||
|         activity = Activity.objects.get(pk=self.kwargs["pk"]) | ||||
|         activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\ | ||||
|             .get(pk=self.kwargs["pk"]) | ||||
|         ctx["activity"] = activity | ||||
|  | ||||
|         matched = [] | ||||
|   | ||||
| @@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs): | ||||
|     if instance._meta.label_lower in EXCLUDED: | ||||
|         return | ||||
|  | ||||
|     if hasattr(instance, "_no_log"): | ||||
|         return | ||||
|  | ||||
|     # noinspection PyProtectedMember | ||||
|     previous = instance._previous | ||||
|  | ||||
| @@ -106,6 +109,9 @@ def delete_object(sender, instance, **kwargs): | ||||
|     if instance._meta.label_lower in EXCLUDED: | ||||
|         return | ||||
|  | ||||
|     if hasattr(instance, "_no_log"): | ||||
|         return | ||||
|  | ||||
|     # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP | ||||
|     user, ip = get_current_authenticated_user(), get_current_ip() | ||||
|  | ||||
|   | ||||
| @@ -1,33 +0,0 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from crispy_forms.helper import FormHelper | ||||
| from crispy_forms.layout import Layout, Submit | ||||
| from django.contrib.auth.models import User | ||||
| from django.db.models import CharField | ||||
| from django_filters import FilterSet, CharFilter | ||||
|  | ||||
|  | ||||
| class UserFilter(FilterSet): | ||||
|     class Meta: | ||||
|         model = User | ||||
|         fields = ['last_name', 'first_name', 'username', 'profile__section'] | ||||
|         filter_overrides = { | ||||
|             CharField: { | ||||
|                 'filter_class': CharFilter, | ||||
|                 'extra': lambda f: { | ||||
|                     'lookup_expr': 'icontains' | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|  | ||||
| class UserFilterFormHelper(FormHelper): | ||||
|     form_method = 'GET' | ||||
|     layout = Layout( | ||||
|         'last_name', | ||||
|         'first_name', | ||||
|         'username', | ||||
|         'profile__section', | ||||
|         Submit('Submit', 'Apply Filter'), | ||||
|     ) | ||||
| @@ -5,10 +5,12 @@ | ||||
|         "fields": { | ||||
|             "name": "BDE", | ||||
|             "email": "tresorerie.bde@example.com", | ||||
|             "membership_fee": 500, | ||||
|             "membership_duration": "396 00:00:00", | ||||
|             "membership_start": "213 00:00:00", | ||||
|             "membership_end": "273 00:00:00" | ||||
|             "require_memberships":  true, | ||||
|             "membership_fee_paid": 500, | ||||
|             "membership_fee_unpaid": 500, | ||||
|             "membership_duration": 396, | ||||
|             "membership_start": "2019-08-31", | ||||
|             "membership_end": "2020-09-30" | ||||
|         } | ||||
|     }, | ||||
|     { | ||||
| @@ -17,10 +19,13 @@ | ||||
|         "fields": { | ||||
|             "name": "Kfet", | ||||
|             "email": "tresorerie.bde@example.com", | ||||
|             "membership_fee": 3500, | ||||
|             "membership_duration": "396 00:00:00", | ||||
|             "membership_start": "213 00:00:00", | ||||
|             "membership_end": "273 00:00:00" | ||||
|             "parent_club": 1, | ||||
|             "require_memberships":  true, | ||||
|             "membership_fee_paid": 3500, | ||||
|             "membership_fee_unpaid": 3500, | ||||
|             "membership_duration": 396, | ||||
|             "membership_start": "2019-08-31", | ||||
|             "membership_end": "2020-09-30" | ||||
|         } | ||||
|     } | ||||
| ] | ||||
|   | ||||
| @@ -1,13 +1,10 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from crispy_forms.bootstrap import Div | ||||
| from crispy_forms.helper import FormHelper | ||||
| from crispy_forms.layout import Layout | ||||
| from django import forms | ||||
| from django.contrib.auth.forms import UserCreationForm, AuthenticationForm | ||||
| from django.contrib.auth.models import User | ||||
| from note_kfet.inputs import Autocomplete | ||||
| from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput | ||||
| from permission.models import PermissionMask | ||||
|  | ||||
| from .models import Profile, Club, Membership | ||||
| @@ -47,11 +44,18 @@ class ClubForm(forms.ModelForm): | ||||
|     class Meta: | ||||
|         model = Club | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class AddMembersForm(forms.Form): | ||||
|     class Meta: | ||||
|         fields = ('',) | ||||
|         widgets = { | ||||
|             "membership_fee_paid": AmountInput(), | ||||
|             "membership_fee_unpaid": AmountInput(), | ||||
|             "parent_club": Autocomplete( | ||||
|                 Club, | ||||
|                 attrs={ | ||||
|                     'api_url': '/api/members/club/', | ||||
|                 } | ||||
|             ), | ||||
|             "membership_start": DatePickerInput(), | ||||
|             "membership_end": DatePickerInput(), | ||||
|         } | ||||
|  | ||||
|  | ||||
| class MembershipForm(forms.ModelForm): | ||||
| @@ -71,28 +75,5 @@ class MembershipForm(forms.ModelForm): | ||||
|                         'placeholder': 'Nom ...', | ||||
|                     }, | ||||
|                 ), | ||||
|             'date_start': DatePickerInput(), | ||||
|         } | ||||
|  | ||||
|  | ||||
| MemberFormSet = forms.modelformset_factory( | ||||
|     Membership, | ||||
|     form=MembershipForm, | ||||
|     extra=2, | ||||
|     can_delete=True, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class FormSetHelper(FormHelper): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         self.form_tag = False | ||||
|         self.form_method = 'POST' | ||||
|         self.form_class = 'form-inline' | ||||
|         # self.template = 'bootstrap/table_inline_formset.html' | ||||
|         self.layout = Layout( | ||||
|             Div( | ||||
|                 Div('user', css_class='col-sm-2'), | ||||
|                 Div('roles', css_class='col-sm-2'), | ||||
|                 Div('date_start', css_class='col-sm-2'), | ||||
|                 css_class="row formset-row", | ||||
|             )) | ||||
|   | ||||
| @@ -4,10 +4,12 @@ | ||||
| import datetime | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import User | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from django.urls import reverse, reverse_lazy | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from note.models import MembershipTransaction | ||||
|  | ||||
|  | ||||
| class Profile(models.Model): | ||||
| @@ -77,22 +79,43 @@ class Club(models.Model): | ||||
|     ) | ||||
|  | ||||
|     # Memberships | ||||
|     membership_fee = models.PositiveIntegerField( | ||||
|         verbose_name=_('membership fee'), | ||||
|  | ||||
|     # When set to False, the membership system won't be used. | ||||
|     # Useful to create notes for activities or departments. | ||||
|     require_memberships = models.BooleanField( | ||||
|         default=True, | ||||
|         verbose_name=_("require memberships"), | ||||
|         help_text=_("Uncheck if this club don't require memberships."), | ||||
|     ) | ||||
|     membership_duration = models.DurationField( | ||||
|  | ||||
|     membership_fee_paid = models.PositiveIntegerField( | ||||
|         default=0, | ||||
|         verbose_name=_('membership fee (paid students)'), | ||||
|     ) | ||||
|  | ||||
|     membership_fee_unpaid = models.PositiveIntegerField( | ||||
|         default=0, | ||||
|         verbose_name=_('membership fee (unpaid students)'), | ||||
|     ) | ||||
|  | ||||
|     membership_duration = models.PositiveIntegerField( | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         verbose_name=_('membership duration'), | ||||
|         help_text=_('The longest time a membership can last ' | ||||
|         help_text=_('The longest time (in days) a membership can last ' | ||||
|                     '(NULL = infinite).'), | ||||
|     ) | ||||
|     membership_start = models.DurationField( | ||||
|  | ||||
|     membership_start = models.DateField( | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         verbose_name=_('membership start'), | ||||
|         help_text=_('How long after January 1st the members can renew ' | ||||
|                     'their membership.'), | ||||
|     ) | ||||
|     membership_end = models.DurationField( | ||||
|  | ||||
|     membership_end = models.DateField( | ||||
|         blank=True, | ||||
|         null=True, | ||||
|         verbose_name=_('membership end'), | ||||
|         help_text=_('How long the membership can last after January 1st ' | ||||
| @@ -100,6 +123,33 @@ class Club(models.Model): | ||||
|                     'membership.'), | ||||
|     ) | ||||
|  | ||||
|     def update_membership_dates(self): | ||||
|         """ | ||||
|         This function is called each time the club detail view is displayed. | ||||
|         Update the year of the membership dates. | ||||
|         """ | ||||
|         if not self.membership_start: | ||||
|             return | ||||
|  | ||||
|         today = datetime.date.today() | ||||
|  | ||||
|         if (today - self.membership_start).days >= 365: | ||||
|             self.membership_start = datetime.date(self.membership_start.year + 1, | ||||
|                                                   self.membership_start.month, self.membership_start.day) | ||||
|             self.membership_end = datetime.date(self.membership_end.year + 1, | ||||
|                                                 self.membership_end.month, self.membership_end.day) | ||||
|             self.save(force_update=True) | ||||
|  | ||||
|     def save(self, force_insert=False, force_update=False, using=None, | ||||
|              update_fields=None): | ||||
|         if not self.require_memberships: | ||||
|             self.membership_fee_paid = 0 | ||||
|             self.membership_fee_unpaid = 0 | ||||
|             self.membership_duration = None | ||||
|             self.membership_start = None | ||||
|             self.membership_end = None | ||||
|         super().save(force_insert, force_update, update_fields) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _("club") | ||||
|         verbose_name_plural = _("clubs") | ||||
| @@ -114,9 +164,6 @@ class Club(models.Model): | ||||
| class Role(models.Model): | ||||
|     """ | ||||
|     Role that an :model:`auth.User` can have in a :model:`member.Club` | ||||
|  | ||||
|     TODO: Integrate the right management, and create some standard Roles at the | ||||
|     creation of the club. | ||||
|     """ | ||||
|     name = models.CharField( | ||||
|         verbose_name=_('name'), | ||||
| @@ -138,24 +185,31 @@ class Membership(models.Model): | ||||
|  | ||||
|     """ | ||||
|     user = models.ForeignKey( | ||||
|         settings.AUTH_USER_MODEL, | ||||
|         User, | ||||
|         on_delete=models.PROTECT, | ||||
|         verbose_name=_("user"), | ||||
|     ) | ||||
|  | ||||
|     club = models.ForeignKey( | ||||
|         Club, | ||||
|         on_delete=models.PROTECT, | ||||
|         verbose_name=_("club"), | ||||
|     ) | ||||
|     roles = models.ForeignKey( | ||||
|  | ||||
|     roles = models.ManyToManyField( | ||||
|         Role, | ||||
|         on_delete=models.PROTECT, | ||||
|         verbose_name=_("roles"), | ||||
|     ) | ||||
|  | ||||
|     date_start = models.DateField( | ||||
|         verbose_name=_('membership starts on'), | ||||
|     ) | ||||
|  | ||||
|     date_end = models.DateField( | ||||
|         verbose_name=_('membership ends on'), | ||||
|         null=True, | ||||
|     ) | ||||
|  | ||||
|     fee = models.PositiveIntegerField( | ||||
|         verbose_name=_('fee'), | ||||
|     ) | ||||
| @@ -168,10 +222,54 @@ class Membership(models.Model): | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if self.club.parent_club is not None: | ||||
|             if not Membership.objects.filter(user=self.user, club=self.club.parent_club): | ||||
|                 raise ValidationError(_('User is not a member of the parent club')) | ||||
|             if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists(): | ||||
|                 raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name) | ||||
|  | ||||
|         created = not self.pk | ||||
|         if created: | ||||
|             if Membership.objects.filter( | ||||
|                     user=self.user, | ||||
|                     club=self.club, | ||||
|                     date_start__lte=self.date_start, | ||||
|                     date_end__gte=self.date_start, | ||||
|             ).exists(): | ||||
|                 raise ValidationError(_('User is already a member of the club')) | ||||
|  | ||||
|             if self.user.profile.paid: | ||||
|                 self.fee = self.club.membership_fee_paid | ||||
|             else: | ||||
|                 self.fee = self.club.membership_fee_unpaid | ||||
|  | ||||
|             if self.club.membership_duration is not None: | ||||
|                 self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) | ||||
|             else: | ||||
|                 self.date_end = self.date_start + datetime.timedelta(days=424242) | ||||
|             if self.club.membership_end is not None and self.date_end > self.club.membership_end: | ||||
|                 self.date_end = self.club.membership_end | ||||
|  | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|         self.make_transaction() | ||||
|  | ||||
|     def make_transaction(self): | ||||
|         if not self.fee or MembershipTransaction.objects.filter(membership=self).exists(): | ||||
|             return | ||||
|  | ||||
|         if self.fee: | ||||
|             transaction = MembershipTransaction( | ||||
|                 membership=self, | ||||
|                 source=self.user.note, | ||||
|                 destination=self.club.note, | ||||
|                 quantity=1, | ||||
|                 amount=self.fee, | ||||
|                 reason="Adhésion " + self.club.name, | ||||
|             ) | ||||
|             transaction._force_save = True | ||||
|             transaction.save(force_insert=True) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, ) | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('membership') | ||||
|         verbose_name_plural = _('memberships') | ||||
|   | ||||
| @@ -1,10 +1,17 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
| from datetime import datetime | ||||
|  | ||||
| import django_tables2 as tables | ||||
| from django.contrib.auth.models import User | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.html import format_html | ||||
| from note.templatetags.pretty_money import pretty_money | ||||
| from note_kfet.middlewares import get_current_authenticated_user | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
| from .models import Club | ||||
| from .models import Club, Membership | ||||
|  | ||||
|  | ||||
| class ClubTable(tables.Table): | ||||
| @@ -24,7 +31,11 @@ class ClubTable(tables.Table): | ||||
|  | ||||
| class UserTable(tables.Table): | ||||
|     section = tables.Column(accessor='profile.section') | ||||
|     solde = tables.Column(accessor='note.balance') | ||||
|  | ||||
|     balance = tables.Column(accessor='note.balance', verbose_name=_("Balance")) | ||||
|  | ||||
|     def render_balance(self, value): | ||||
|         return pretty_money(value) | ||||
|  | ||||
|     class Meta: | ||||
|         attrs = { | ||||
| @@ -33,3 +44,68 @@ class UserTable(tables.Table): | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|         fields = ('last_name', 'first_name', 'username', 'email') | ||||
|         model = User | ||||
|         row_attrs = { | ||||
|             'class': 'table-row', | ||||
|             'data-href': lambda record: record.pk | ||||
|         } | ||||
|  | ||||
|  | ||||
| class MembershipTable(tables.Table): | ||||
|     roles = tables.Column( | ||||
|         attrs={ | ||||
|             "td": { | ||||
|                 "class": "text-truncate", | ||||
|             } | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|     def render_club(self, value): | ||||
|         s = value.name | ||||
|         if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value): | ||||
|             s = format_html("<a href={url}>{name}</a>", | ||||
|                             url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s) | ||||
|  | ||||
|         return s | ||||
|  | ||||
|     def render_fee(self, value, record): | ||||
|         t = pretty_money(value) | ||||
|  | ||||
|         # If it is required and if the user has the right, the renew button is displayed. | ||||
|         if record.club.membership_start is not None: | ||||
|             if record.date_start < record.club.membership_start:  # If the renew is available | ||||
|                 if not Membership.objects.filter( | ||||
|                         club=record.club, | ||||
|                         user=record.user, | ||||
|                         date_start__gte=record.club.membership_start, | ||||
|                         date_end__lte=record.club.membership_end, | ||||
|                 ).exists():  # If the renew is not yet performed | ||||
|                     empty_membership = Membership( | ||||
|                         club=record.club, | ||||
|                         user=record.user, | ||||
|                         date_start=datetime.now().date(), | ||||
|                         date_end=datetime.now().date(), | ||||
|                         fee=0, | ||||
|                     ) | ||||
|                     if PermissionBackend.check_perm(get_current_authenticated_user(), | ||||
|                                                     "member:add_membership", empty_membership):  # If the user has right | ||||
|                         t = format_html(t + ' <a class="btn btn-warning" href="{url}">{text}</a>', | ||||
|                                         url=reverse_lazy('member:club_renew_membership', | ||||
|                                                          kwargs={"pk": record.pk}), text=_("Renew")) | ||||
|         return t | ||||
|  | ||||
|     def render_roles(self, record): | ||||
|         roles = record.roles.all() | ||||
|         s = ", ".join(str(role) for role in roles) | ||||
|         if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record): | ||||
|             s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk})) | ||||
|                             + "'>" + s + "</a>") | ||||
|         return s | ||||
|  | ||||
|     class Meta: | ||||
|         attrs = { | ||||
|             'class': 'table table-condensed table-striped table-hover', | ||||
|             'style': 'table-layout: fixed;' | ||||
|         } | ||||
|         template_name = 'django_tables2/bootstrap4.html' | ||||
|         fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', ) | ||||
|         model = Membership | ||||
|   | ||||
| @@ -8,17 +8,21 @@ from . import views | ||||
| app_name = 'member' | ||||
| urlpatterns = [ | ||||
|     path('signup/', views.UserCreateView.as_view(), name="signup"), | ||||
|  | ||||
|     path('club/', views.ClubListView.as_view(), name="club_list"), | ||||
|     path('club/create/', views.ClubCreateView.as_view(), name="club_create"), | ||||
|     path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"), | ||||
|     path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), | ||||
|     path('club/create/', views.ClubCreateView.as_view(), name="club_create"), | ||||
|     path('club/<int:pk>/update', views.ClubUpdateView.as_view(), name="club_update"), | ||||
|     path('club/<int:pk>/update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), | ||||
|     path('club/<int:pk>/aliases', views.ClubAliasView.as_view(), name="club_alias"), | ||||
|     path('club/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"), | ||||
|     path('club/renew_membership/<int:pk>/', views.ClubRenewMembershipView.as_view(), name="club_renew_membership"), | ||||
|     path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"), | ||||
|     path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), | ||||
|     path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"), | ||||
|  | ||||
|     path('user/', views.UserListView.as_view(), name="user_list"), | ||||
|     path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"), | ||||
|     path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"), | ||||
|     path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), | ||||
|     path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"), | ||||
|     path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"), | ||||
|     path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"), | ||||
|     path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), | ||||
|     path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"), | ||||
|     path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), | ||||
| ] | ||||
|   | ||||
| @@ -2,17 +2,21 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import io | ||||
| from datetime import datetime, timedelta | ||||
|  | ||||
| from PIL import Image | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.models import User | ||||
| from django.contrib.auth.views import LoginView | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db.models import Q | ||||
| from django.forms import HiddenInput | ||||
| from django.shortcuts import redirect | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.views.generic import CreateView, DetailView, UpdateView, TemplateView | ||||
| from django.views.generic.base import View | ||||
| from django.views.generic.edit import FormMixin | ||||
| from django_tables2.views import SingleTableView | ||||
| from rest_framework.authtoken.models import Token | ||||
| @@ -21,12 +25,11 @@ from note.models import Alias, NoteUser | ||||
| from note.models.transactions import Transaction | ||||
| from note.tables import HistoryTable, AliasTable | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.views import ProtectQuerysetMixin | ||||
|  | ||||
| from .filters import UserFilter, UserFilterFormHelper | ||||
| from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \ | ||||
|     CustomAuthenticationForm | ||||
| from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm | ||||
| from .models import Club, Membership | ||||
| from .tables import ClubTable, UserTable | ||||
| from .tables import ClubTable, UserTable, MembershipTable | ||||
|  | ||||
|  | ||||
| class CustomLoginView(LoginView): | ||||
| @@ -63,7 +66,7 @@ class UserCreateView(CreateView): | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|  | ||||
| class UserUpdateView(LoginRequiredMixin, UpdateView): | ||||
| class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     model = User | ||||
|     fields = ['first_name', 'last_name', 'username', 'email'] | ||||
|     template_name = 'member/profile_update.html' | ||||
| @@ -97,7 +100,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): | ||||
|         if form.is_valid() and profile_form.is_valid(): | ||||
|             new_username = form.data['username'] | ||||
|             alias = Alias.objects.filter(name=new_username) | ||||
|             # Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer | ||||
|             # Si le nouveau pseudo n'est pas un de nos alias, | ||||
|             # on supprime éventuellement un alias similaire pour le remplacer | ||||
|             if not alias.exists(): | ||||
|                 similar = Alias.objects.filter( | ||||
|                     normalized_name=Alias.normalize(new_username)) | ||||
| @@ -119,7 +123,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView): | ||||
|             return reverse_lazy('member:user_detail', args=(self.object.id,)) | ||||
|  | ||||
|  | ||||
| class UserDetailView(LoginRequiredMixin, DetailView): | ||||
| class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     """ | ||||
|     Affiche les informations sur un utilisateur, sa note, ses clubs... | ||||
|     """ | ||||
| @@ -127,44 +131,56 @@ class UserDetailView(LoginRequiredMixin, DetailView): | ||||
|     context_object_name = "user_object" | ||||
|     template_name = "member/profile_detail.html" | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         user = context['user_object'] | ||||
|         history_list = \ | ||||
|             Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id") | ||||
|             Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) | ||||
|         context['history_list'] = HistoryTable(history_list) | ||||
|         club_list = \ | ||||
|             Membership.objects.all().filter(user=user).only("club") | ||||
|         context['club_list'] = ClubTable(club_list) | ||||
|         club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) | ||||
|         context['club_list'] = MembershipTable(data=club_list) | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class UserListView(LoginRequiredMixin, SingleTableView): | ||||
| class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     Affiche la liste des utilisateurs, avec une fonction de recherche statique | ||||
|     """ | ||||
|     model = User | ||||
|     table_class = UserTable | ||||
|     template_name = 'member/user_list.html' | ||||
|     filter_class = UserFilter | ||||
|     formhelper_class = UserFilterFormHelper | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) | ||||
|         self.filter = self.filter_class(self.request.GET, queryset=qs) | ||||
|         self.filter.form.helper = self.formhelper_class() | ||||
|         return self.filter.qs | ||||
|         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(first_name__iregex=pattern) | ||||
|                 | Q(last_name__iregex=pattern) | ||||
|                 | Q(profile__section__iregex=pattern) | ||||
|                 | Q(note__alias__name__iregex="^" + pattern) | ||||
|                 | Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern)) | ||||
|             ) | ||||
|         else: | ||||
|             qs = qs.none() | ||||
|  | ||||
|         return qs[:20] | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         context["filter"] = self.filter | ||||
|  | ||||
|         context["title"] = _("Search user") | ||||
|  | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class ProfileAliasView(LoginRequiredMixin, DetailView): | ||||
| class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     model = User | ||||
|     template_name = 'member/profile_alias.html' | ||||
|     context_object_name = 'user_object' | ||||
| @@ -176,11 +192,11 @@ class ProfileAliasView(LoginRequiredMixin, DetailView): | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView): | ||||
| class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView): | ||||
|     form_class = ImageForm | ||||
|  | ||||
|     def get_context_data(self, *args, **kwargs): | ||||
|         context = super().get_context_data(*args, **kwargs) | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         context['form'] = self.form_class(self.request.POST, self.request.FILES) | ||||
|         return context | ||||
|  | ||||
| @@ -237,8 +253,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): | ||||
|     template_name = "member/manage_auth_tokens.html" | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
|         if 'regenerate' in request.GET and Token.objects.filter( | ||||
|                 user=request.user).exists(): | ||||
|         if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists(): | ||||
|             Token.objects.get(user=self.request.user).delete() | ||||
|             return redirect(reverse_lazy('member:auth_token') + "?show", | ||||
|                             permanent=True) | ||||
| @@ -247,8 +262,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         context['token'] = Token.objects.get_or_create( | ||||
|             user=self.request.user)[0] | ||||
|         context['token'] = Token.objects.get_or_create(user=self.request.user)[0] | ||||
|         return context | ||||
|  | ||||
|  | ||||
| @@ -257,7 +271,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView): | ||||
| # ******************************* # | ||||
|  | ||||
|  | ||||
| class ClubCreateView(LoginRequiredMixin, CreateView): | ||||
| class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     """ | ||||
|     Create Club | ||||
|     """ | ||||
| @@ -269,38 +283,49 @@ class ClubCreateView(LoginRequiredMixin, CreateView): | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|  | ||||
| class ClubListView(LoginRequiredMixin, SingleTableView): | ||||
| class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     List existing Clubs | ||||
|     """ | ||||
|     model = Club | ||||
|     table_class = ClubTable | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) | ||||
|  | ||||
|  | ||||
| class ClubDetailView(LoginRequiredMixin, DetailView): | ||||
| class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     model = Club | ||||
|     context_object_name = "club" | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|  | ||||
|         club = context["club"] | ||||
|         club_transactions = \ | ||||
|             Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) | ||||
|         if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club): | ||||
|             club.update_membership_dates() | ||||
|  | ||||
|         club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id') | ||||
|         context['history_list'] = HistoryTable(club_transactions) | ||||
|         club_member = \ | ||||
|             Membership.objects.all().filter(club=club) | ||||
|         # TODO: consider only valid Membership | ||||
|         context['member_list'] = club_member | ||||
|         club_member = Membership.objects.filter( | ||||
|             club=club, | ||||
|             date_end__gte=datetime.today(), | ||||
|         ).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")) | ||||
|  | ||||
|         context['member_list'] = MembershipTable(data=club_member) | ||||
|  | ||||
|         empty_membership = Membership( | ||||
|             club=club, | ||||
|             user=User.objects.first(), | ||||
|             date_start=datetime.now().date(), | ||||
|             date_end=datetime.now().date(), | ||||
|             fee=0, | ||||
|         ) | ||||
|         context["can_add_members"] = PermissionBackend()\ | ||||
|             .has_perm(self.request.user, "member.add_membership", empty_membership) | ||||
|  | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class ClubAliasView(LoginRequiredMixin, DetailView): | ||||
| class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView): | ||||
|     model = Club | ||||
|     template_name = 'member/club_alias.html' | ||||
|     context_object_name = 'club' | ||||
| @@ -312,12 +337,14 @@ class ClubAliasView(LoginRequiredMixin, DetailView): | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class ClubUpdateView(LoginRequiredMixin, UpdateView): | ||||
| class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     model = Club | ||||
|     context_object_name = "club" | ||||
|     form_class = ClubForm | ||||
|     template_name = "member/club_form.html" | ||||
|     success_url = reverse_lazy("member:club_detail") | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk}) | ||||
|  | ||||
|  | ||||
| class ClubPictureUpdateView(PictureUpdateView): | ||||
| @@ -329,35 +356,123 @@ class ClubPictureUpdateView(PictureUpdateView): | ||||
|         return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id}) | ||||
|  | ||||
|  | ||||
| class ClubAddMemberView(LoginRequiredMixin, CreateView): | ||||
| class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     model = Membership | ||||
|     form_class = MembershipForm | ||||
|     template_name = 'member/add_members.html' | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view") | ||||
|                                              | PermissionBackend.filter_queryset(self.request.user, Membership, | ||||
|                                                                                  "change")) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         club = Club.objects.get(pk=self.kwargs["pk"]) | ||||
|         club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ | ||||
|             .get(pk=self.kwargs["pk"]) | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         context['formset'] = MemberFormSet() | ||||
|         context['helper'] = FormSetHelper() | ||||
|         context['club'] = club | ||||
|         context['no_cache'] = True | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     def post(self, request, *args, **kwargs): | ||||
|         return | ||||
|         # TODO: Implement POST | ||||
|         # formset = MembershipFormset(request.POST) | ||||
|         # if formset.is_valid(): | ||||
|         #     return self.form_valid(formset) | ||||
|         # else: | ||||
|         #     return self.form_invalid(formset) | ||||
|     def form_valid(self, form): | ||||
|         club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ | ||||
|             .get(pk=self.kwargs["pk"]) | ||||
|         user = self.request.user | ||||
|         form.instance.club = club | ||||
|  | ||||
|     def form_valid(self, formset): | ||||
|         formset.save() | ||||
|         return super().form_valid(formset) | ||||
|         if user.profile.paid: | ||||
|             fee = club.membership_fee_paid | ||||
|         else: | ||||
|             fee = club.membership_fee_unpaid | ||||
|         if user.note.balance < fee and not Membership.objects.filter( | ||||
|                 club__name="Kfet", | ||||
|                 user=user, | ||||
|                 date_start__lte=datetime.now().date(), | ||||
|                 date_end__gte=datetime.now().date(), | ||||
|         ).exists(): | ||||
|             # Users without a valid Kfet membership can't have a negative balance. | ||||
|             # Club 2 = Kfet (hard-code :'( ) | ||||
|             # TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note | ||||
|             form.add_error('user', | ||||
|                            _("This user don't have enough money to join this club, and can't have a negative balance.")) | ||||
|  | ||||
|         if club.parent_club is not None: | ||||
|             if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists(): | ||||
|                 form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name) | ||||
|                 return super().form_invalid(form) | ||||
|  | ||||
|         if Membership.objects.filter( | ||||
|                 user=form.instance.user, | ||||
|                 club=club, | ||||
|                 date_start__lte=form.instance.date_start, | ||||
|                 date_end__gte=form.instance.date_start, | ||||
|         ).exists(): | ||||
|             form.add_error('user', _('User is already a member of the club')) | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|         if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start: | ||||
|             form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|         if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end: | ||||
|             form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) | ||||
|  | ||||
|  | ||||
| class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     model = Membership | ||||
|     form_class = MembershipForm | ||||
|     template_name = 'member/add_members.html' | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         club = self.object.club | ||||
|         context['club'] = club | ||||
|         form = context['form'] | ||||
|         form.fields['user'].disabled = True | ||||
|         form.fields['date_start'].widget = HiddenInput() | ||||
|  | ||||
|         return context | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start: | ||||
|             form.add_error('user', _("The membership must start after {:%m-%d-%Y}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|         if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end: | ||||
|             form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.") | ||||
|                            .format(form.instance.club.membership_start)) | ||||
|             return super().form_invalid(form) | ||||
|  | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id}) | ||||
|  | ||||
|  | ||||
| class ClubRenewMembershipView(ProtectQuerysetMixin, LoginRequiredMixin, View): | ||||
|     def get(self, *args, **kwargs): | ||||
|         user = self.request.user | ||||
|         membership = Membership.objects.filter(PermissionBackend.filter_queryset(user, Membership, "change"))\ | ||||
|             .filter(pk=self.kwargs["pk"]).get() | ||||
|  | ||||
|         if Membership.objects.filter( | ||||
|             club=membership.club, | ||||
|             user=membership.user, | ||||
|             date_start__gte=membership.club.membership_start, | ||||
|             date_end__lte=membership.club.membership_end, | ||||
|         ).exists(): | ||||
|             raise ValidationError(_("This membership is already renewed")) | ||||
|  | ||||
|         new_membership = Membership.objects.create( | ||||
|             user=user, | ||||
|             club=membership.club, | ||||
|             date_start=membership.date_end + timedelta(days=1), | ||||
|         ) | ||||
|         new_membership.roles.set(membership.roles.all()) | ||||
|         new_membership.save() | ||||
|  | ||||
|         return redirect(reverse_lazy('member:club_detail', kwargs={'pk': membership.club.pk})) | ||||
|   | ||||
| @@ -138,6 +138,13 @@ class TransactionAdmin(PolymorphicParentModelAdmin): | ||||
|         return [] | ||||
|  | ||||
|  | ||||
| @admin.register(MembershipTransaction) | ||||
| class MembershipTransactionAdmin(PolymorphicChildModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for Transaction | ||||
|     """ | ||||
|  | ||||
|  | ||||
| @admin.register(TransactionTemplate) | ||||
| class TransactionTemplateAdmin(admin.ModelAdmin): | ||||
|     """ | ||||
|   | ||||
| @@ -90,7 +90,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer): | ||||
|         Note: NoteSerializer, | ||||
|         NoteUser: NoteUserSerializer, | ||||
|         NoteClub: NoteClubSerializer, | ||||
|         NoteSpecial: NoteSpecialSerializer | ||||
|         NoteSpecial: NoteSpecialSerializer, | ||||
|     } | ||||
|  | ||||
|     class Meta: | ||||
|   | ||||
| @@ -46,12 +46,14 @@ class TransactionTemplate(models.Model): | ||||
|         unique=True, | ||||
|         error_messages={'unique': _("A template with this name already exist")}, | ||||
|     ) | ||||
|  | ||||
|     destination = models.ForeignKey( | ||||
|         NoteClub, | ||||
|         on_delete=models.PROTECT, | ||||
|         related_name='+',  # no reverse | ||||
|         verbose_name=_('destination'), | ||||
|     ) | ||||
|  | ||||
|     amount = models.PositiveIntegerField( | ||||
|         verbose_name=_('amount'), | ||||
|         help_text=_('in centimes'), | ||||
| @@ -62,9 +64,12 @@ class TransactionTemplate(models.Model): | ||||
|         verbose_name=_('type'), | ||||
|         max_length=31, | ||||
|     ) | ||||
|  | ||||
|     display = models.BooleanField( | ||||
|         default=True, | ||||
|         verbose_name=_("display"), | ||||
|     ) | ||||
|  | ||||
|     description = models.CharField( | ||||
|         verbose_name=_('description'), | ||||
|         max_length=255, | ||||
| @@ -140,6 +145,7 @@ class Transaction(PolymorphicModel): | ||||
|         max_length=255, | ||||
|         default=None, | ||||
|         null=True, | ||||
|         blank=True, | ||||
|     ) | ||||
|  | ||||
|     class Meta: | ||||
|   | ||||
| @@ -118,7 +118,8 @@ class AliasTable(tables.Table): | ||||
|  | ||||
|     delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, | ||||
|                                        extra_context={"delete_trans": _('delete')}, | ||||
|                                        attrs={'td': {'class': 'col-sm-1'}}) | ||||
|                                        attrs={'td': {'class': 'col-sm-1'}}, | ||||
|                                        verbose_name=_("Delete"),) | ||||
|  | ||||
|  | ||||
| class ButtonTable(tables.Table): | ||||
| @@ -134,17 +135,20 @@ class ButtonTable(tables.Table): | ||||
|         } | ||||
|  | ||||
|         model = TransactionTemplate | ||||
|         exclude = ('id',) | ||||
|  | ||||
|     edit = tables.LinkColumn('note:template_update', | ||||
|                              args=[A('pk')], | ||||
|                              attrs={'td': {'class': 'col-sm-1'}, | ||||
|                                     'a': {'class': 'btn btn-sm btn-primary'}}, | ||||
|                              text=_('edit'), | ||||
|                              accessor='pk') | ||||
|                              accessor='pk', | ||||
|                              verbose_name=_("Edit"),) | ||||
|  | ||||
|     delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE, | ||||
|                                        extra_context={"delete_trans": _('delete')}, | ||||
|                                        attrs={'td': {'class': 'col-sm-1'}}) | ||||
|                                        attrs={'td': {'class': 'col-sm-1'}}, | ||||
|                                        verbose_name=_("Delete"),) | ||||
|  | ||||
|     def render_amount(self, value): | ||||
|         return pretty_money(value) | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from django_tables2 import SingleTableView | ||||
| from django.urls import reverse_lazy | ||||
| from note_kfet.inputs import AmountInput | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.views import ProtectQuerysetMixin | ||||
|  | ||||
| from .forms import TransactionTemplateForm | ||||
| from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial | ||||
| @@ -16,7 +17,7 @@ from .models.transactions import SpecialTransaction | ||||
| from .tables import HistoryTable, ButtonTable | ||||
|  | ||||
|  | ||||
| class TransactionCreateView(LoginRequiredMixin, SingleTableView): | ||||
| class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`. | ||||
|     e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial` | ||||
| @@ -26,12 +27,9 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView): | ||||
|     model = Transaction | ||||
|     # Transaction history table | ||||
|     table_class = HistoryTable | ||||
|     table_pagination = {"per_page": 50} | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         return Transaction.objects.filter(PermissionBackend.filter_queryset( | ||||
|             self.request.user, Transaction, "view") | ||||
|         ).order_by("-id").all()[:50] | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset(**kwargs).order_by("-id").all()[:50] | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """ | ||||
| @@ -42,12 +40,14 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView): | ||||
|         context['amount_widget'] = AmountInput(attrs={"id": "amount"}) | ||||
|         context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk | ||||
|         context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk | ||||
|         context['special_types'] = NoteSpecial.objects.order_by("special_type").all() | ||||
|         context['special_types'] = NoteSpecial.objects\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\ | ||||
|             .order_by("special_type").all() | ||||
|  | ||||
|         return context | ||||
|  | ||||
|  | ||||
| class TransactionTemplateCreateView(LoginRequiredMixin, CreateView): | ||||
| class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     """ | ||||
|     Create TransactionTemplate | ||||
|     """ | ||||
| @@ -56,7 +56,7 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView): | ||||
|     success_url = reverse_lazy('note:template_list') | ||||
|  | ||||
|  | ||||
| class TransactionTemplateListView(LoginRequiredMixin, SingleTableView): | ||||
| class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     List TransactionsTemplates | ||||
|     """ | ||||
| @@ -64,7 +64,7 @@ class TransactionTemplateListView(LoginRequiredMixin, SingleTableView): | ||||
|     table_class = ButtonTable | ||||
|  | ||||
|  | ||||
| class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): | ||||
| class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     """ | ||||
|     """ | ||||
|     model = TransactionTemplate | ||||
| @@ -72,21 +72,19 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView): | ||||
|     success_url = reverse_lazy('note:template_list') | ||||
|  | ||||
|  | ||||
| class ConsoView(LoginRequiredMixin, SingleTableView): | ||||
| class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     The Magic View that make people pay their beer and burgers. | ||||
|     (Most of the magic happens in the dark world of Javascript see consos.js) | ||||
|     """ | ||||
|     model = Transaction | ||||
|     template_name = "note/conso_form.html" | ||||
|  | ||||
|     # Transaction history table | ||||
|     table_class = HistoryTable | ||||
|     table_pagination = {"per_page": 50} | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         return Transaction.objects.filter( | ||||
|             PermissionBackend.filter_queryset(self.request.user, Transaction, "view") | ||||
|         ).order_by("-id").all()[:50] | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset(**kwargs).order_by("-id").all()[:50] | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """ | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|  | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from ..models import Permission | ||||
| from ..models import Permission, RolePermissions | ||||
|  | ||||
|  | ||||
| class PermissionSerializer(serializers.ModelSerializer): | ||||
| @@ -15,3 +15,14 @@ class PermissionSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Permission | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class RolePermissionsSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for RolePermissions types. | ||||
|     The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API. | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = RolePermissions | ||||
|         fields = '__all__' | ||||
|   | ||||
| @@ -1,11 +1,12 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import PermissionViewSet | ||||
| from .views import PermissionViewSet, RolePermissionsViewSet | ||||
|  | ||||
|  | ||||
| def register_permission_urls(router, path): | ||||
|     """ | ||||
|     Configure router for permission REST API. | ||||
|     """ | ||||
|     router.register(path, PermissionViewSet) | ||||
|     router.register(path + "/permission", PermissionViewSet) | ||||
|     router.register(path + "/roles", RolePermissionsViewSet) | ||||
|   | ||||
| @@ -4,17 +4,29 @@ | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from api.viewsets import ReadOnlyProtectedModelViewSet | ||||
|  | ||||
| from .serializers import PermissionSerializer | ||||
| from ..models import Permission | ||||
| from .serializers import PermissionSerializer, RolePermissionsSerializer | ||||
| from ..models import Permission, RolePermissions | ||||
|  | ||||
|  | ||||
| class PermissionViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/logs/ | ||||
|     The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/permission/permission/ | ||||
|     """ | ||||
|     queryset = Permission.objects.all() | ||||
|     serializer_class = PermissionSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['model', 'type', ] | ||||
|  | ||||
|  | ||||
| class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer | ||||
|     then render it on /api/permission/roles/ | ||||
|     """ | ||||
|     queryset = RolePermissions.objects.all() | ||||
|     serializer_class = RolePermissionsSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['role', ] | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import datetime | ||||
|  | ||||
| from django.contrib.auth.backends import ModelBackend | ||||
| from django.contrib.auth.models import User, AnonymousUser | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| @@ -9,6 +11,7 @@ from note.models import Note, NoteUser, NoteClub, NoteSpecial | ||||
| from note_kfet.middlewares import get_current_session | ||||
| from member.models import Membership, Club | ||||
|  | ||||
| from .decorators import memoize | ||||
| from .models import Permission | ||||
|  | ||||
|  | ||||
| @@ -20,6 +23,28 @@ class PermissionBackend(ModelBackend): | ||||
|     supports_anonymous_user = False | ||||
|     supports_inactive_user = False | ||||
|  | ||||
|     @staticmethod | ||||
|     @memoize | ||||
|     def get_raw_permissions(user, t): | ||||
|         """ | ||||
|         Query permissions of a certain type for a user, then memoize it. | ||||
|         :param user: The owner of the permissions | ||||
|         :param t: The type of the permissions: view, change, add or delete | ||||
|         :return: The queryset of the permissions of the user (memoized) grouped by clubs | ||||
|         """ | ||||
|         if isinstance(user, AnonymousUser): | ||||
|             # Unauthenticated users have no permissions | ||||
|             return Permission.objects.none() | ||||
|  | ||||
|         return Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ | ||||
|             .filter( | ||||
|                 rolepermissions__role__membership__user=user, | ||||
|                 rolepermissions__role__membership__date_start__lte=datetime.date.today(), | ||||
|                 rolepermissions__role__membership__date_end__gte=datetime.date.today(), | ||||
|                 type=t, | ||||
|                 mask__rank__lte=get_current_session().get("permission_mask", 0), | ||||
|         ).distinct('club', 'pk',) | ||||
|  | ||||
|     @staticmethod | ||||
|     def permissions(user, model, type): | ||||
|         """ | ||||
| @@ -29,16 +54,16 @@ class PermissionBackend(ModelBackend): | ||||
|         :param type: The type of the permissions: view, change, add or delete | ||||
|         :return: A generator of the requested permissions | ||||
|         """ | ||||
|         for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ | ||||
|                 .filter( | ||||
|             rolepermissions__role__membership__user=user, | ||||
|             model__app_label=model.app_label,  # For polymorphic models, we don't filter on model type | ||||
|             type=type, | ||||
|         ).all(): | ||||
|             if not isinstance(model, permission.model.__class__): | ||||
|         clubs = {} | ||||
|  | ||||
|         for permission in PermissionBackend.get_raw_permissions(user, type): | ||||
|             if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club: | ||||
|                 continue | ||||
|  | ||||
|             club = Club.objects.get(pk=permission.club) | ||||
|             if permission.club not in clubs: | ||||
|                 clubs[permission.club] = club = Club.objects.get(pk=permission.club) | ||||
|             else: | ||||
|                 club = clubs[permission.club] | ||||
|             permission = permission.about( | ||||
|                 user=user, | ||||
|                 club=club, | ||||
| @@ -52,10 +77,10 @@ class PermissionBackend(ModelBackend): | ||||
|                 F=F, | ||||
|                 Q=Q | ||||
|             ) | ||||
|             if permission.mask.rank <= get_current_session().get("permission_mask", 0): | ||||
|                 yield permission | ||||
|             yield permission | ||||
|  | ||||
|     @staticmethod | ||||
|     @memoize | ||||
|     def filter_queryset(user, model, t, field=None): | ||||
|         """ | ||||
|         Filter a queryset by considering the permissions of a given user. | ||||
| @@ -89,10 +114,23 @@ class PermissionBackend(ModelBackend): | ||||
|             query = query | perm.query | ||||
|         return query | ||||
|  | ||||
|     def has_perm(self, user_obj, perm, obj=None): | ||||
|     @staticmethod | ||||
|     @memoize | ||||
|     def check_perm(user_obj, perm, obj=None): | ||||
|         """ | ||||
|         Check is the given user has the permission over a given object. | ||||
|         The result is then memoized. | ||||
|         Exception: for add permissions, since the object is not hashable since it doesn't have any | ||||
|         primary key, the result is not memoized. Moreover, the right could change | ||||
|         (e.g. for a transaction, the balance of the user could change) | ||||
|         """ | ||||
|         if user_obj is None or isinstance(user_obj, AnonymousUser): | ||||
|             return False | ||||
|  | ||||
|         sess = get_current_session() | ||||
|         if sess is not None and sess.session_key is None: | ||||
|             return Permission.objects.none() | ||||
|  | ||||
|         if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42: | ||||
|             return True | ||||
|  | ||||
| @@ -104,10 +142,13 @@ class PermissionBackend(ModelBackend): | ||||
|         perm_field = perm[2] if len(perm) == 3 else None | ||||
|         ct = ContentType.objects.get_for_model(obj) | ||||
|         if any(permission.applies(obj, perm_type, perm_field) | ||||
|                for permission in self.permissions(user_obj, ct, perm_type)): | ||||
|                for permission in PermissionBackend.permissions(user_obj, ct, perm_type)): | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     def has_perm(self, user_obj, perm, obj=None): | ||||
|         return PermissionBackend.check_perm(user_obj, perm, obj) | ||||
|  | ||||
|     def has_module_perms(self, user_obj, app_label): | ||||
|         return False | ||||
|  | ||||
|   | ||||
							
								
								
									
										59
									
								
								apps/permission/decorators.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								apps/permission/decorators.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,59 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from functools import lru_cache | ||||
| from time import time | ||||
|  | ||||
| from django.contrib.sessions.models import Session | ||||
| from note_kfet.middlewares import get_current_session | ||||
|  | ||||
|  | ||||
| def memoize(f): | ||||
|     """ | ||||
|     Memoize results and store in sessions | ||||
|  | ||||
|     This decorator is useful for permissions: they are loaded once needed, then stored for next calls. | ||||
|     The storage is contained with sessions since it depends on the selected mask. | ||||
|     """ | ||||
|     sess_funs = {} | ||||
|     last_collect = time() | ||||
|  | ||||
|     def collect(): | ||||
|         """ | ||||
|         Clear cache of results when sessions are invalid, to flush useless data. | ||||
|         This function is called every minute. | ||||
|         """ | ||||
|         nonlocal sess_funs | ||||
|  | ||||
|         new_sess_funs = {} | ||||
|         for sess_key in sess_funs: | ||||
|             if Session.objects.filter(session_key=sess_key).exists(): | ||||
|                 new_sess_funs[sess_key] = sess_funs[sess_key] | ||||
|         sess_funs = new_sess_funs | ||||
|  | ||||
|     def func(*args, **kwargs): | ||||
|         nonlocal last_collect | ||||
|  | ||||
|         if time() - last_collect > 60: | ||||
|             # Clear cache | ||||
|             collect() | ||||
|             last_collect = time() | ||||
|  | ||||
|         # If there is no session, then we don't memoize anything. | ||||
|         sess = get_current_session() | ||||
|         if sess is None or sess.session_key is None: | ||||
|             return f(*args, **kwargs) | ||||
|  | ||||
|         sess_key = sess.session_key | ||||
|         if sess_key not in sess_funs: | ||||
|             # lru_cache makes the job of memoization | ||||
|             # We store only the 512 latest data per session. It has to be enough. | ||||
|             sess_funs[sess_key] = lru_cache(512)(f) | ||||
|         try: | ||||
|             return sess_funs[sess_key](*args, **kwargs) | ||||
|         except TypeError:  # For add permissions, objects are not hashable (not yet created). Don't memoize this case. | ||||
|             return f(*args, **kwargs) | ||||
|  | ||||
|     func.func_name = f.__name__ | ||||
|  | ||||
|     return func | ||||
| @@ -386,7 +386,7 @@ | ||||
|         "note", | ||||
|         "transaction" | ||||
|       ], | ||||
|       "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]", | ||||
|       "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": "", | ||||
| @@ -783,6 +783,66 @@ | ||||
|       "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.rolepermissions", | ||||
|     "pk": 1, | ||||
| @@ -795,7 +855,8 @@ | ||||
|         8, | ||||
|         9, | ||||
|         10, | ||||
|         11 | ||||
|         11, | ||||
|         48 | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
| @@ -880,5 +941,75 @@ | ||||
|         46 | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.rolepermissions", | ||||
|     "pk": 6, | ||||
|     "fields": { | ||||
|       "role": 7, | ||||
|       "permissions": [ | ||||
|         22, | ||||
|         47 | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.rolepermissions", | ||||
|     "pk": 7, | ||||
|     "fields": { | ||||
|       "role": 5, | ||||
|       "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 | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
| ] | ||||
|   | ||||
| @@ -38,20 +38,33 @@ class InstancedPermission: | ||||
|             if permission_type == self.type: | ||||
|                 self.update_query() | ||||
|  | ||||
|                 # Don't increase indexes | ||||
|                 obj.pk = 0 | ||||
|                 # Don't increase indexes, if the primary key is an AutoField | ||||
|                 if not hasattr(obj, "pk") or not obj.pk: | ||||
|                     obj.pk = 0 | ||||
|                     oldpk = None | ||||
|                 else: | ||||
|                     oldpk = obj.pk | ||||
|                 # Ensure previous models are deleted | ||||
|                 self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete() | ||||
|                 # Force insertion, no data verification, no trigger | ||||
|                 obj._force_save = True | ||||
|                 Model.save(obj, force_insert=True) | ||||
|                 ret = obj in self.model.model_class().objects.filter(self.query).all() | ||||
|                 # We don't want log anything | ||||
|                 obj._no_log = True | ||||
|                 ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() | ||||
|                 # Delete testing object | ||||
|                 obj._force_delete = True | ||||
|                 Model.delete(obj) | ||||
|  | ||||
|                 # If the primary key was specified, we restore it | ||||
|                 obj.pk = oldpk | ||||
|                 return ret | ||||
|  | ||||
|         if permission_type == self.type: | ||||
|             if self.field and field_name != self.field: | ||||
|                 return False | ||||
|             self.update_query() | ||||
|             return obj in self.model.model_class().objects.filter(self.query).all() | ||||
|             return self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() | ||||
|         else: | ||||
|             return False | ||||
|  | ||||
|   | ||||
| @@ -44,7 +44,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions): | ||||
|  | ||||
|         perms = self.get_required_object_permissions(request.method, model_cls) | ||||
|         # if not user.has_perms(perms, obj): | ||||
|         if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms): | ||||
|         if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms): | ||||
|             # If the user does not have permissions we need to determine if | ||||
|             # they have read permissions to see 403, or not, and simply see | ||||
|             # a 404 response. | ||||
|   | ||||
| @@ -2,8 +2,6 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.db.models.signals import pre_save, pre_delete, post_save, post_delete | ||||
| from logs import signals as logs_signals | ||||
| from note_kfet.middlewares import get_current_authenticated_user | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
| @@ -29,6 +27,9 @@ def pre_save_object(sender, instance, **kwargs): | ||||
|     if instance._meta.label_lower in EXCLUDED: | ||||
|         return | ||||
|  | ||||
|     if hasattr(instance, "_force_save"): | ||||
|         return | ||||
|  | ||||
|     user = get_current_authenticated_user() | ||||
|     if user is None: | ||||
|         # Action performed on shell is always granted | ||||
| @@ -43,7 +44,7 @@ def pre_save_object(sender, instance, **kwargs): | ||||
|         # We check if the user can change the model | ||||
|  | ||||
|         # If the user has all right on a model, then OK | ||||
|         if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance): | ||||
|         if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance): | ||||
|             return | ||||
|  | ||||
|         # In the other case, we check if he/she has the right to change one field | ||||
| @@ -55,35 +56,17 @@ def pre_save_object(sender, instance, **kwargs): | ||||
|             # If the field wasn't modified, no need to check the permissions | ||||
|             if old_value == new_value: | ||||
|                 continue | ||||
|             if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): | ||||
|             if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): | ||||
|                 raise PermissionDenied | ||||
|     else: | ||||
|         # We check if the user can add the model | ||||
|  | ||||
|         # While checking permissions, the object will be inserted in the DB, then removed. | ||||
|         # We disable temporary the connectors | ||||
|         pre_save.disconnect(pre_save_object) | ||||
|         pre_delete.disconnect(pre_delete_object) | ||||
|         # We disable also logs connectors | ||||
|         pre_save.disconnect(logs_signals.pre_save_object) | ||||
|         post_save.disconnect(logs_signals.save_object) | ||||
|         post_delete.disconnect(logs_signals.delete_object) | ||||
|  | ||||
|         # We check if the user has right to add the object | ||||
|         has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance) | ||||
|  | ||||
|         # Then we reconnect all | ||||
|         pre_save.connect(pre_save_object) | ||||
|         pre_delete.connect(pre_delete_object) | ||||
|         pre_save.connect(logs_signals.pre_save_object) | ||||
|         post_save.connect(logs_signals.save_object) | ||||
|         post_delete.connect(logs_signals.delete_object) | ||||
|         has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance) | ||||
|  | ||||
|         if not has_perm: | ||||
|             raise PermissionDenied | ||||
|  | ||||
|  | ||||
| def pre_delete_object(sender, instance, **kwargs): | ||||
| def pre_delete_object(instance, **kwargs): | ||||
|     """ | ||||
|     Before a model get deleted, we check the permissions | ||||
|     """ | ||||
| @@ -91,6 +74,9 @@ def pre_delete_object(sender, instance, **kwargs): | ||||
|     if instance._meta.label_lower in EXCLUDED: | ||||
|         return | ||||
|  | ||||
|     if hasattr(instance, "_force_delete"): | ||||
|         return | ||||
|  | ||||
|     user = get_current_authenticated_user() | ||||
|     if user is None: | ||||
|         # Action performed on shell is always granted | ||||
| @@ -101,5 +87,5 @@ def pre_delete_object(sender, instance, **kwargs): | ||||
|     model_name = model_name_full[1] | ||||
|  | ||||
|     # We check if the user has rights to delete the object | ||||
|     if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance): | ||||
|     if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance): | ||||
|         raise PermissionDenied | ||||
|   | ||||
| @@ -4,6 +4,7 @@ | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.template.defaultfilters import stringfilter | ||||
| from django import template | ||||
| from note.models import Transaction | ||||
| from note_kfet.middlewares import get_current_authenticated_user, get_current_session | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
| @@ -19,13 +20,8 @@ def not_empty_model_list(model_name): | ||||
|         return False | ||||
|     elif user.is_superuser and session.get("permission_mask", 0) >= 42: | ||||
|         return True | ||||
|     if session.get("not_empty_model_list_" + model_name, None): | ||||
|         return session.get("not_empty_model_list_" + model_name, None) == 1 | ||||
|     spl = model_name.split(".") | ||||
|     ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) | ||||
|     qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")).all() | ||||
|     session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2 | ||||
|     return session.get("not_empty_model_list_" + model_name) == 1 | ||||
|     qs = model_list(model_name) | ||||
|     return qs.exists() | ||||
|  | ||||
|  | ||||
| @stringfilter | ||||
| @@ -39,20 +35,54 @@ def not_empty_model_change_list(model_name): | ||||
|         return False | ||||
|     elif user.is_superuser and session.get("permission_mask", 0) >= 42: | ||||
|         return True | ||||
|     if session.get("not_empty_model_change_list_" + model_name, None): | ||||
|         return session.get("not_empty_model_change_list_" + model_name, None) == 1 | ||||
|     qs = model_list(model_name, "change") | ||||
|     return qs.exists() | ||||
|  | ||||
|  | ||||
| @stringfilter | ||||
| def model_list(model_name, t="view"): | ||||
|     """ | ||||
|     Return the queryset of all visible instances of the given model. | ||||
|     """ | ||||
|     user = get_current_authenticated_user() | ||||
|     if user is None: | ||||
|         return False | ||||
|     spl = model_name.split(".") | ||||
|     ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) | ||||
|     qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change")) | ||||
|     session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2 | ||||
|     return session.get("not_empty_model_change_list_" + model_name) == 1 | ||||
|     qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all() | ||||
|     return qs | ||||
|  | ||||
|  | ||||
| def has_perm(perm, obj): | ||||
|     return PermissionBackend().has_perm(get_current_authenticated_user(), perm, obj) | ||||
|     return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj) | ||||
|  | ||||
|  | ||||
| def can_create_transaction(): | ||||
|     """ | ||||
|     :return: True iff the authenticated user can create a transaction. | ||||
|     """ | ||||
|     user = get_current_authenticated_user() | ||||
|     session = get_current_session() | ||||
|     if user is None: | ||||
|         return False | ||||
|     elif user.is_superuser and session.get("permission_mask", 0) >= 42: | ||||
|         return True | ||||
|     if session.get("can_create_transaction", None): | ||||
|         return session.get("can_create_transaction", None) == 1 | ||||
|  | ||||
|     empty_transaction = Transaction( | ||||
|         source=user.note, | ||||
|         destination=user.note, | ||||
|         quantity=1, | ||||
|         amount=0, | ||||
|         reason="Check permissions", | ||||
|     ) | ||||
|     session["can_create_transaction"] = PermissionBackend.check_perm(user, "note.add_transaction", empty_transaction) | ||||
|     return session.get("can_create_transaction") == 1 | ||||
|  | ||||
|  | ||||
| register = template.Library() | ||||
| register.filter('not_empty_model_list', not_empty_model_list) | ||||
| register.filter('not_empty_model_change_list', not_empty_model_change_list) | ||||
| register.filter('model_list', model_list) | ||||
| register.filter('has_perm', has_perm) | ||||
|   | ||||
							
								
								
									
										11
									
								
								apps/permission/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/permission/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
|  | ||||
| class ProtectQuerysetMixin: | ||||
|     def get_queryset(self, **kwargs): | ||||
|         qs = super().get_queryset(**kwargs) | ||||
|  | ||||
|         return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")) | ||||
| @@ -8,6 +8,7 @@ from crispy_forms.layout import Submit | ||||
| from django import forms | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from note_kfet.inputs import DatePickerInput, AmountInput | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
| from .models import Invoice, Product, Remittance, SpecialTransactionProxy | ||||
|  | ||||
| @@ -131,7 +132,8 @@ class LinkTransactionToRemittanceForm(forms.ModelForm): | ||||
|         # Add submit button | ||||
|         self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'})) | ||||
|  | ||||
|         self.fields["remittance"].queryset = Remittance.objects.filter(closed=False) | ||||
|         self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\ | ||||
|             .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) | ||||
|  | ||||
|     def clean_last_name(self): | ||||
|         """ | ||||
|   | ||||
| @@ -19,13 +19,15 @@ from django.views.generic.base import View, TemplateView | ||||
| from django_tables2 import SingleTableView | ||||
| from note.models import SpecialTransaction, NoteSpecial | ||||
| from note_kfet.settings.base import BASE_DIR | ||||
| from permission.backends import PermissionBackend | ||||
| from permission.views import ProtectQuerysetMixin | ||||
|  | ||||
| from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm | ||||
| from .models import Invoice, Product, Remittance, SpecialTransactionProxy | ||||
| from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable | ||||
|  | ||||
|  | ||||
| class InvoiceCreateView(LoginRequiredMixin, CreateView): | ||||
| class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     """ | ||||
|     Create Invoice | ||||
|     """ | ||||
| @@ -67,7 +69,7 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView): | ||||
|         return reverse_lazy('treasury:invoice_list') | ||||
|  | ||||
|  | ||||
| class InvoiceListView(LoginRequiredMixin, SingleTableView): | ||||
| class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     List existing Invoices | ||||
|     """ | ||||
| @@ -75,7 +77,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView): | ||||
|     table_class = InvoiceTable | ||||
|  | ||||
|  | ||||
| class InvoiceUpdateView(LoginRequiredMixin, UpdateView): | ||||
| class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     """ | ||||
|     Create Invoice | ||||
|     """ | ||||
| @@ -130,7 +132,7 @@ class InvoiceRenderView(LoginRequiredMixin, View): | ||||
|  | ||||
|     def get(self, request, **kwargs): | ||||
|         pk = kwargs["pk"] | ||||
|         invoice = Invoice.objects.get(pk=pk) | ||||
|         invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk) | ||||
|         products = Product.objects.filter(invoice=invoice).all() | ||||
|  | ||||
|         # Informations of the BDE. Should be updated when the school will move. | ||||
| @@ -188,7 +190,7 @@ class InvoiceRenderView(LoginRequiredMixin, View): | ||||
|         return response | ||||
|  | ||||
|  | ||||
| class RemittanceCreateView(LoginRequiredMixin, CreateView): | ||||
| class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): | ||||
|     """ | ||||
|     Create Remittance | ||||
|     """ | ||||
| @@ -201,7 +203,9 @@ class RemittanceCreateView(LoginRequiredMixin, CreateView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         ctx = super().get_context_data(**kwargs) | ||||
|  | ||||
|         ctx["table"] = RemittanceTable(data=Remittance.objects.all()) | ||||
|         ctx["table"] = RemittanceTable(data=Remittance.objects | ||||
|                                        .filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view")) | ||||
|                                        .all()) | ||||
|         ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none()) | ||||
|  | ||||
|         return ctx | ||||
| @@ -216,22 +220,28 @@ class RemittanceListView(LoginRequiredMixin, TemplateView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         ctx = super().get_context_data(**kwargs) | ||||
|  | ||||
|         ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all()) | ||||
|         ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all()) | ||||
|         ctx["opened_remittances"] = RemittanceTable( | ||||
|             data=Remittance.objects.filter(closed=False).filter( | ||||
|                 PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) | ||||
|         ctx["closed_remittances"] = RemittanceTable( | ||||
|             data=Remittance.objects.filter(closed=True).filter( | ||||
|                 PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all()) | ||||
|  | ||||
|         ctx["special_transactions_no_remittance"] = SpecialTransactionTable( | ||||
|             data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), | ||||
|                                                    specialtransactionproxy__remittance=None).all(), | ||||
|                                                    specialtransactionproxy__remittance=None).filter( | ||||
|                 PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), | ||||
|             exclude=('remittance_remove', )) | ||||
|         ctx["special_transactions_with_remittance"] = SpecialTransactionTable( | ||||
|             data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)), | ||||
|                                                    specialtransactionproxy__remittance__closed=False).all(), | ||||
|                                                    specialtransactionproxy__remittance__closed=False).filter( | ||||
|                 PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(), | ||||
|             exclude=('remittance_add', )) | ||||
|  | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class RemittanceUpdateView(LoginRequiredMixin, UpdateView): | ||||
| class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     """ | ||||
|     Update Remittance | ||||
|     """ | ||||
| @@ -244,8 +254,10 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView): | ||||
|     def get_context_data(self, **kwargs): | ||||
|         ctx = super().get_context_data(**kwargs) | ||||
|  | ||||
|         ctx["table"] = RemittanceTable(data=Remittance.objects.all()) | ||||
|         data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all() | ||||
|         ctx["table"] = RemittanceTable(data=Remittance.objects.filter( | ||||
|             PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()) | ||||
|         data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter( | ||||
|             PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all() | ||||
|         ctx["special_transactions"] = SpecialTransactionTable( | ||||
|             data=data, | ||||
|             exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', )) | ||||
| @@ -253,7 +265,7 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView): | ||||
|         return ctx | ||||
|  | ||||
|  | ||||
| class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView): | ||||
| class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView): | ||||
|     """ | ||||
|     Attach a special transaction to a remittance | ||||
|     """ | ||||
|   | ||||
| @@ -8,7 +8,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2020-03-30 17:31+0200\n" | ||||
| "POT-Creation-Date: 2020-04-01 18:39+0200\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @@ -44,9 +44,9 @@ 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:64 apps/member/models.py:122 | ||||
| #: apps/member/models.py:66 apps/member/models.py:169 | ||||
| #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 | ||||
| #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231 | ||||
| #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:232 | ||||
| #: templates/member/club_info.html:13 templates/member/profile_info.html:14 | ||||
| msgid "name" | ||||
| msgstr "" | ||||
| @@ -68,7 +68,7 @@ msgid "activity types" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:53 apps/note/models/transactions.py:69 | ||||
| #: apps/permission/models.py:90 templates/activity/activity_detail.html:16 | ||||
| #: apps/permission/models.py:103 templates/activity/activity_detail.html:16 | ||||
| msgid "description" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -78,7 +78,7 @@ msgstr "" | ||||
| msgid "type" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/models.py:66 apps/logs/models.py:21 | ||||
| #: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:190 | ||||
| #: apps/note/models/notes.py:117 | ||||
| msgid "user" | ||||
| msgstr "" | ||||
| @@ -169,11 +169,11 @@ msgstr "" | ||||
| msgid "Type" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/tables.py:77 apps/treasury/forms.py:120 | ||||
| #: apps/activity/tables.py:77 apps/treasury/forms.py:121 | ||||
| msgid "Last name" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/tables.py:79 apps/treasury/forms.py:122 | ||||
| #: apps/activity/tables.py:79 apps/treasury/forms.py:123 | ||||
| #: templates/note/transaction_form.html:92 | ||||
| msgid "First name" | ||||
| msgstr "" | ||||
| @@ -186,11 +186,11 @@ msgstr "" | ||||
| msgid "Balance" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/views.py:44 templates/base.html:94 | ||||
| #: apps/activity/views.py:45 templates/base.html:94 | ||||
| msgid "Activities" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/activity/views.py:149 | ||||
| #: apps/activity/views.py:153 | ||||
| msgid "Entry for activity \"{}\"" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -251,121 +251,165 @@ msgstr "" | ||||
| msgid "member" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:26 | ||||
| #: apps/member/models.py:28 | ||||
| msgid "phone number" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:32 templates/member/profile_info.html:27 | ||||
| #: apps/member/models.py:34 templates/member/profile_info.html:27 | ||||
| msgid "section" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:33 | ||||
| #: apps/member/models.py:35 | ||||
| msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:39 templates/member/profile_info.html:30 | ||||
| #: apps/member/models.py:41 templates/member/profile_info.html:30 | ||||
| msgid "address" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:45 | ||||
| #: apps/member/models.py:47 | ||||
| msgid "paid" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:50 apps/member/models.py:51 | ||||
| #: apps/member/models.py:52 apps/member/models.py:53 | ||||
| msgid "user profile" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:69 templates/member/club_info.html:36 | ||||
| #: apps/member/models.py:71 templates/member/club_info.html:46 | ||||
| msgid "email" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:76 | ||||
| #: apps/member/models.py:78 | ||||
| msgid "parent club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:81 templates/member/club_info.html:30 | ||||
| msgid "membership fee" | ||||
| #: apps/member/models.py:87 | ||||
| msgid "require memberships" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:85 templates/member/club_info.html:27 | ||||
| #: apps/member/models.py:88 | ||||
| msgid "Uncheck if this club don't require memberships." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:93 templates/member/club_info.html:35 | ||||
| msgid "membership fee (paid students)" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:98 templates/member/club_info.html:38 | ||||
| msgid "membership fee (unpaid students)" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:104 templates/member/club_info.html:28 | ||||
| msgid "membership duration" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:86 | ||||
| msgid "The longest time a membership can last (NULL = infinite)." | ||||
| #: apps/member/models.py:105 | ||||
| msgid "The longest time (in days) a membership can last (NULL = infinite)." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:91 templates/member/club_info.html:21 | ||||
| #: apps/member/models.py:112 templates/member/club_info.html:22 | ||||
| msgid "membership start" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:92 | ||||
| #: apps/member/models.py:113 | ||||
| msgid "How long after January 1st the members can renew their membership." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:97 templates/member/club_info.html:24 | ||||
| #: apps/member/models.py:120 templates/member/club_info.html:25 | ||||
| msgid "membership end" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:98 | ||||
| #: apps/member/models.py:121 | ||||
| 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:104 apps/note/models/notes.py:139 | ||||
| #: apps/member/models.py:154 apps/member/models.py:196 | ||||
| #: apps/note/models/notes.py:139 | ||||
| msgid "club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:105 | ||||
| #: apps/member/models.py:155 | ||||
| msgid "clubs" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:128 apps/permission/models.py:275 | ||||
| #: apps/member/models.py:175 apps/permission/models.py:288 | ||||
| msgid "role" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:129 | ||||
| #: apps/member/models.py:176 apps/member/models.py:201 | ||||
| msgid "roles" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:153 | ||||
| #: apps/member/models.py:205 | ||||
| msgid "membership starts on" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:156 | ||||
| #: apps/member/models.py:209 | ||||
| msgid "membership ends on" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:160 | ||||
| #: apps/member/models.py:214 | ||||
| msgid "fee" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:172 | ||||
| #: apps/member/models.py:226 apps/member/views.py:383 | ||||
| msgid "User is not a member of the parent club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:176 | ||||
| #: apps/member/models.py:236 apps/member/views.py:392 | ||||
| msgid "User is already a member of the club" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:271 | ||||
| #, python-brace-format | ||||
| msgid "Membership of {user} for the club {club}" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:274 | ||||
| msgid "membership" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/models.py:177 | ||||
| #: apps/member/models.py:275 | ||||
| msgid "memberships" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:76 templates/member/profile_info.html:45 | ||||
| #: apps/member/tables.py:73 | ||||
| msgid "Renew" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:80 templates/member/profile_info.html:45 | ||||
| msgid "Update Profile" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:89 | ||||
| #: apps/member/views.py:93 | ||||
| msgid "An alias with a similar name already exists." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:379 | ||||
| msgid "" | ||||
| "This user don't have enough money to join this club, and can't have a " | ||||
| "negative balance." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:396 apps/member/views.py:428 | ||||
| msgid "The membership must start after {:%m-%d-%Y}." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:401 apps/member/views.py:433 | ||||
| msgid "The membership must begin before {:%m-%d-%Y}." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:455 | ||||
| msgid "This membership is already renewed" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/admin.py:120 apps/note/models/transactions.py:94 | ||||
| msgid "source" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/admin.py:128 apps/note/admin.py:156 | ||||
| #: apps/note/admin.py:128 apps/note/admin.py:163 | ||||
| #: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107 | ||||
| msgid "destination" | ||||
| msgstr "" | ||||
| @@ -462,7 +506,7 @@ msgstr "" | ||||
| msgid "alias" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/notes.py:211 templates/member/club_info.html:33 | ||||
| #: apps/note/models/notes.py:211 templates/member/club_info.html:43 | ||||
| #: templates/member/profile_info.html:36 | ||||
| msgid "aliases" | ||||
| msgstr "" | ||||
| @@ -524,45 +568,45 @@ msgstr "" | ||||
| msgid "invalidity reason" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:146 | ||||
| #: apps/note/models/transactions.py:147 | ||||
| msgid "transaction" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:147 | ||||
| #: apps/note/models/transactions.py:148 | ||||
| msgid "transactions" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:201 templates/base.html:84 | ||||
| #: apps/note/models/transactions.py:202 templates/base.html:84 | ||||
| #: templates/note/transaction_form.html:19 | ||||
| #: templates/note/transaction_form.html:140 | ||||
| msgid "Transfer" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:221 | ||||
| #: apps/note/models/transactions.py:222 | ||||
| msgid "Template" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:236 | ||||
| #: apps/note/models/transactions.py:237 | ||||
| msgid "first_name" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:241 | ||||
| #: apps/note/models/transactions.py:242 | ||||
| msgid "bank" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24 | ||||
| #: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24 | ||||
| msgid "Credit" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28 | ||||
| #: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28 | ||||
| msgid "Debit" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268 | ||||
| #: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269 | ||||
| msgid "membership transaction" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/models/transactions.py:264 | ||||
| #: apps/note/models/transactions.py:265 | ||||
| msgid "membership transactions" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -578,29 +622,29 @@ msgstr "" | ||||
| msgid "No reason specified" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/views.py:41 | ||||
| #: apps/note/views.py:39 | ||||
| msgid "Transfer money" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/note/views.py:102 templates/base.html:79 | ||||
| #: apps/note/views.py:100 templates/base.html:79 | ||||
| msgid "Consumptions" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/permission/models.py:69 apps/permission/models.py:262 | ||||
| #: apps/permission/models.py:82 apps/permission/models.py:275 | ||||
| #, python-brace-format | ||||
| msgid "Can {type} {model}.{field} in {query}" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/permission/models.py:71 apps/permission/models.py:264 | ||||
| #: apps/permission/models.py:84 apps/permission/models.py:277 | ||||
| #, python-brace-format | ||||
| msgid "Can {type} {model} in {query}" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/permission/models.py:84 | ||||
| #: apps/permission/models.py:97 | ||||
| msgid "rank" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/permission/models.py:147 | ||||
| #: apps/permission/models.py:160 | ||||
| msgid "Specifying field applies only to view and change permission types." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -608,31 +652,32 @@ msgstr "" | ||||
| msgid "Treasury" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/treasury/forms.py:84 apps/treasury/forms.py:132 | ||||
| #: apps/treasury/forms.py:85 apps/treasury/forms.py:133 | ||||
| #: templates/activity/activity_form.html:9 | ||||
| #: templates/activity/activity_invite.html:8 | ||||
| #: templates/django_filters/rest_framework/form.html:5 | ||||
| #: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46 | ||||
| #: templates/member/add_members.html:14 templates/member/club_form.html:9 | ||||
| #: templates/treasury/invoice_form.html:46 | ||||
| msgid "Submit" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/treasury/forms.py:86 | ||||
| #: apps/treasury/forms.py:87 | ||||
| msgid "Close" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/treasury/forms.py:95 | ||||
| #: apps/treasury/forms.py:96 | ||||
| msgid "Remittance is already closed." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/treasury/forms.py:100 | ||||
| #: apps/treasury/forms.py:101 | ||||
| msgid "You can't change the type of the remittance." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/treasury/forms.py:124 templates/note/transaction_form.html:98 | ||||
| #: apps/treasury/forms.py:125 templates/note/transaction_form.html:98 | ||||
| msgid "Bank" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/treasury/forms.py:126 apps/treasury/tables.py:47 | ||||
| #: apps/treasury/forms.py:127 apps/treasury/tables.py:47 | ||||
| #: templates/note/transaction_form.html:128 | ||||
| #: templates/treasury/remittance_form.html:18 | ||||
| msgid "Amount" | ||||
| @@ -879,19 +924,23 @@ msgstr "" | ||||
| msgid "Club Parent" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_info.html:41 | ||||
| #: templates/member/club_info.html:29 | ||||
| msgid "days" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_info.html:32 | ||||
| msgid "membership fee" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_info.html:52 | ||||
| msgid "Add member" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_info.html:42 templates/note/conso_form.html:121 | ||||
| #: templates/member/club_info.html:55 templates/note/conso_form.html:121 | ||||
| msgid "Edit" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_info.html:43 | ||||
| msgid "Add roles" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_info.html:46 templates/member/profile_info.html:48 | ||||
| #: templates/member/club_info.html:59 templates/member/profile_info.html:48 | ||||
| msgid "View Profile" | ||||
| msgstr "" | ||||
|  | ||||
| @@ -900,7 +949,7 @@ msgid "search clubs" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_list.html:12 | ||||
| msgid "Créer un club" | ||||
| msgid "Create club" | ||||
| msgstr "" | ||||
|  | ||||
| #: templates/member/club_list.html:19 | ||||
|   | ||||
| @@ -3,7 +3,7 @@ msgid "" | ||||
| msgstr "" | ||||
| "Project-Id-Version: PACKAGE VERSION\n" | ||||
| "Report-Msgid-Bugs-To: \n" | ||||
| "POT-Creation-Date: 2020-03-30 17:31+0200\n" | ||||
| "POT-Creation-Date: 2020-04-01 18:39+0200\n" | ||||
| "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" | ||||
| "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" | ||||
| "Language-Team: LANGUAGE <LL@li.org>\n" | ||||
| @@ -20,7 +20,8 @@ msgstr "activité" | ||||
|  | ||||
| #: apps/activity/forms.py:45 apps/activity/models.py:217 | ||||
| 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é." | ||||
| 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:220 | ||||
| msgid "This activity is not validated yet." | ||||
| @@ -39,9 +40,9 @@ 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:64 apps/member/models.py:122 | ||||
| #: apps/member/models.py:66 apps/member/models.py:169 | ||||
| #: apps/note/models/notes.py:188 apps/note/models/transactions.py:24 | ||||
| #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231 | ||||
| #: apps/note/models/transactions.py:44 apps/note/models/transactions.py:232 | ||||
| #: templates/member/club_info.html:13 templates/member/profile_info.html:14 | ||||
| msgid "name" | ||||
| msgstr "nom" | ||||
| @@ -63,7 +64,7 @@ msgid "activity types" | ||||
| msgstr "types d'activité" | ||||
|  | ||||
| #: apps/activity/models.py:53 apps/note/models/transactions.py:69 | ||||
| #: apps/permission/models.py:90 templates/activity/activity_detail.html:16 | ||||
| #: apps/permission/models.py:103 templates/activity/activity_detail.html:16 | ||||
| msgid "description" | ||||
| msgstr "description" | ||||
|  | ||||
| @@ -73,7 +74,7 @@ msgstr "description" | ||||
| msgid "type" | ||||
| msgstr "type" | ||||
|  | ||||
| #: apps/activity/models.py:66 apps/logs/models.py:21 | ||||
| #: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:190 | ||||
| #: apps/note/models/notes.py:117 | ||||
| msgid "user" | ||||
| msgstr "utilisateur" | ||||
| @@ -164,11 +165,11 @@ msgstr "supprimer" | ||||
| msgid "Type" | ||||
| msgstr "Type" | ||||
|  | ||||
| #: apps/activity/tables.py:77 apps/treasury/forms.py:120 | ||||
| #: apps/activity/tables.py:77 apps/treasury/forms.py:121 | ||||
| msgid "Last name" | ||||
| msgstr "Nom de famille" | ||||
|  | ||||
| #: apps/activity/tables.py:79 apps/treasury/forms.py:122 | ||||
| #: apps/activity/tables.py:79 apps/treasury/forms.py:123 | ||||
| #: templates/note/transaction_form.html:92 | ||||
| msgid "First name" | ||||
| msgstr "Prénom" | ||||
| @@ -181,11 +182,11 @@ msgstr "Note" | ||||
| msgid "Balance" | ||||
| msgstr "Solde du compte" | ||||
|  | ||||
| #: apps/activity/views.py:44 templates/base.html:94 | ||||
| #: apps/activity/views.py:45 templates/base.html:94 | ||||
| msgid "Activities" | ||||
| msgstr "Activités" | ||||
|  | ||||
| #: apps/activity/views.py:149 | ||||
| #: apps/activity/views.py:153 | ||||
| msgid "Entry for activity \"{}\"" | ||||
| msgstr "Entrées pour l'activité « {} »" | ||||
|  | ||||
| @@ -246,65 +247,77 @@ msgstr "Les logs ne peuvent pas être détruits." | ||||
| msgid "member" | ||||
| msgstr "adhérent" | ||||
|  | ||||
| #: apps/member/models.py:26 | ||||
| #: apps/member/models.py:28 | ||||
| msgid "phone number" | ||||
| msgstr "numéro de téléphone" | ||||
|  | ||||
| #: apps/member/models.py:32 templates/member/profile_info.html:27 | ||||
| #: apps/member/models.py:34 templates/member/profile_info.html:27 | ||||
| msgid "section" | ||||
| msgstr "section" | ||||
|  | ||||
| #: apps/member/models.py:33 | ||||
| #: apps/member/models.py:35 | ||||
| msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" | ||||
| msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\"" | ||||
|  | ||||
| #: apps/member/models.py:39 templates/member/profile_info.html:30 | ||||
| #: apps/member/models.py:41 templates/member/profile_info.html:30 | ||||
| msgid "address" | ||||
| msgstr "adresse" | ||||
|  | ||||
| #: apps/member/models.py:45 | ||||
| #: apps/member/models.py:47 | ||||
| msgid "paid" | ||||
| msgstr "payé" | ||||
|  | ||||
| #: apps/member/models.py:50 apps/member/models.py:51 | ||||
| #: apps/member/models.py:52 apps/member/models.py:53 | ||||
| msgid "user profile" | ||||
| msgstr "profil utilisateur" | ||||
|  | ||||
| #: apps/member/models.py:69 templates/member/club_info.html:36 | ||||
| #: apps/member/models.py:71 templates/member/club_info.html:46 | ||||
| msgid "email" | ||||
| msgstr "courriel" | ||||
|  | ||||
| #: apps/member/models.py:76 | ||||
| #: apps/member/models.py:78 | ||||
| msgid "parent club" | ||||
| msgstr "club parent" | ||||
|  | ||||
| #: apps/member/models.py:81 templates/member/club_info.html:30 | ||||
| msgid "membership fee" | ||||
| msgstr "cotisation pour adhérer" | ||||
| #: apps/member/models.py:87 | ||||
| msgid "require memberships" | ||||
| msgstr "nécessite des adhésions" | ||||
|  | ||||
| #: apps/member/models.py:85 templates/member/club_info.html:27 | ||||
| #: apps/member/models.py:88 | ||||
| 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:93 templates/member/club_info.html:35 | ||||
| msgid "membership fee (paid students)" | ||||
| msgstr "cotisation pour adhérer (normalien élève)" | ||||
|  | ||||
| #: apps/member/models.py:98 templates/member/club_info.html:38 | ||||
| msgid "membership fee (unpaid students)" | ||||
| msgstr "cotisation pour adhérer (normalien étudiant)" | ||||
|  | ||||
| #: apps/member/models.py:104 templates/member/club_info.html:28 | ||||
| msgid "membership duration" | ||||
| msgstr "durée de l'adhésion" | ||||
|  | ||||
| #: apps/member/models.py:86 | ||||
| msgid "The longest time a membership can last (NULL = infinite)." | ||||
| msgstr "La durée maximale d'une adhésion (NULL = infinie)." | ||||
| #: apps/member/models.py:105 | ||||
| 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:91 templates/member/club_info.html:21 | ||||
| #: apps/member/models.py:112 templates/member/club_info.html:22 | ||||
| msgid "membership start" | ||||
| msgstr "début de l'adhésion" | ||||
|  | ||||
| #: apps/member/models.py:92 | ||||
| #: apps/member/models.py:113 | ||||
| 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:97 templates/member/club_info.html:24 | ||||
| #: apps/member/models.py:120 templates/member/club_info.html:25 | ||||
| msgid "membership end" | ||||
| msgstr "fin de l'adhésion" | ||||
|  | ||||
| #: apps/member/models.py:98 | ||||
| #: apps/member/models.py:121 | ||||
| msgid "" | ||||
| "How long the membership can last after January 1st of the next year after " | ||||
| "members can renew their membership." | ||||
| @@ -312,59 +325,91 @@ 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:104 apps/note/models/notes.py:139 | ||||
| #: apps/member/models.py:154 apps/member/models.py:196 | ||||
| #: apps/note/models/notes.py:139 | ||||
| msgid "club" | ||||
| msgstr "club" | ||||
|  | ||||
| #: apps/member/models.py:105 | ||||
| #: apps/member/models.py:155 | ||||
| msgid "clubs" | ||||
| msgstr "clubs" | ||||
|  | ||||
| #: apps/member/models.py:128 apps/permission/models.py:275 | ||||
| #: apps/member/models.py:175 apps/permission/models.py:288 | ||||
| msgid "role" | ||||
| msgstr "rôle" | ||||
|  | ||||
| #: apps/member/models.py:129 | ||||
| #: apps/member/models.py:176 apps/member/models.py:201 | ||||
| msgid "roles" | ||||
| msgstr "rôles" | ||||
|  | ||||
| #: apps/member/models.py:153 | ||||
| #: apps/member/models.py:205 | ||||
| msgid "membership starts on" | ||||
| msgstr "l'adhésion commence le" | ||||
|  | ||||
| #: apps/member/models.py:156 | ||||
| #: apps/member/models.py:209 | ||||
| msgid "membership ends on" | ||||
| msgstr "l'adhésion finie le" | ||||
| msgstr "l'adhésion finit le" | ||||
|  | ||||
| #: apps/member/models.py:160 | ||||
| #: apps/member/models.py:214 | ||||
| msgid "fee" | ||||
| msgstr "cotisation" | ||||
|  | ||||
| #: apps/member/models.py:172 | ||||
| #: apps/member/models.py:226 apps/member/views.py:383 | ||||
| msgid "User is not a member of the parent club" | ||||
| msgstr "L'utilisateur n'est pas membre du club parent" | ||||
|  | ||||
| #: apps/member/models.py:176 | ||||
| #: apps/member/models.py:236 apps/member/views.py:392 | ||||
| msgid "User is already a member of the club" | ||||
| msgstr "L'utilisateur est déjà membre du club" | ||||
|  | ||||
| #: apps/member/models.py:271 | ||||
| #, python-brace-format | ||||
| msgid "Membership of {user} for the club {club}" | ||||
| msgstr "Adhésion de {user} pour le club {club}" | ||||
|  | ||||
| #: apps/member/models.py:274 | ||||
| msgid "membership" | ||||
| msgstr "adhésion" | ||||
|  | ||||
| #: apps/member/models.py:177 | ||||
| #: apps/member/models.py:275 | ||||
| msgid "memberships" | ||||
| msgstr "adhésions" | ||||
|  | ||||
| #: apps/member/views.py:76 templates/member/profile_info.html:45 | ||||
| #: apps/member/tables.py:73 | ||||
| msgid "Renew" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:80 templates/member/profile_info.html:45 | ||||
| msgid "Update Profile" | ||||
| msgstr "Modifier le profil" | ||||
|  | ||||
| #: apps/member/views.py:89 | ||||
| #: apps/member/views.py:93 | ||||
| msgid "An alias with a similar name already exists." | ||||
| msgstr "Un alias avec un nom similaire existe déjà." | ||||
|  | ||||
| #: apps/member/views.py:379 | ||||
| msgid "" | ||||
| "This user don't have enough money to join this club, and can't have a " | ||||
| "negative balance." | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/member/views.py:396 apps/member/views.py:428 | ||||
| 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:401 apps/member/views.py:433 | ||||
| msgid "The membership must begin before {:%m-%d-%Y}." | ||||
| msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}." | ||||
|  | ||||
| #: apps/member/views.py:455 | ||||
| msgid "This membership is already renewed" | ||||
| msgstr "Cette adhésion est déjà renouvelée" | ||||
|  | ||||
| #: apps/note/admin.py:120 apps/note/models/transactions.py:94 | ||||
| msgid "source" | ||||
| msgstr "source" | ||||
|  | ||||
| #: apps/note/admin.py:128 apps/note/admin.py:156 | ||||
| #: apps/note/admin.py:128 apps/note/admin.py:163 | ||||
| #: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107 | ||||
| msgid "destination" | ||||
| msgstr "destination" | ||||
| @@ -462,7 +507,7 @@ msgstr "Alias invalide" | ||||
| msgid "alias" | ||||
| msgstr "alias" | ||||
|  | ||||
| #: apps/note/models/notes.py:211 templates/member/club_info.html:33 | ||||
| #: apps/note/models/notes.py:211 templates/member/club_info.html:43 | ||||
| #: templates/member/profile_info.html:36 | ||||
| msgid "aliases" | ||||
| msgstr "alias" | ||||
| @@ -524,45 +569,45 @@ msgstr "raison" | ||||
| msgid "invalidity reason" | ||||
| msgstr "Motif d'invalidité" | ||||
|  | ||||
| #: apps/note/models/transactions.py:146 | ||||
| #: apps/note/models/transactions.py:147 | ||||
| msgid "transaction" | ||||
| msgstr "transaction" | ||||
|  | ||||
| #: apps/note/models/transactions.py:147 | ||||
| #: apps/note/models/transactions.py:148 | ||||
| msgid "transactions" | ||||
| msgstr "transactions" | ||||
|  | ||||
| #: apps/note/models/transactions.py:201 templates/base.html:84 | ||||
| #: apps/note/models/transactions.py:202 templates/base.html:84 | ||||
| #: templates/note/transaction_form.html:19 | ||||
| #: templates/note/transaction_form.html:140 | ||||
| msgid "Transfer" | ||||
| msgstr "Virement" | ||||
|  | ||||
| #: apps/note/models/transactions.py:221 | ||||
| #: apps/note/models/transactions.py:222 | ||||
| msgid "Template" | ||||
| msgstr "Bouton" | ||||
|  | ||||
| #: apps/note/models/transactions.py:236 | ||||
| #: apps/note/models/transactions.py:237 | ||||
| msgid "first_name" | ||||
| msgstr "prénom" | ||||
|  | ||||
| #: apps/note/models/transactions.py:241 | ||||
| #: apps/note/models/transactions.py:242 | ||||
| msgid "bank" | ||||
| msgstr "banque" | ||||
|  | ||||
| #: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24 | ||||
| #: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24 | ||||
| msgid "Credit" | ||||
| msgstr "Crédit" | ||||
|  | ||||
| #: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28 | ||||
| #: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28 | ||||
| msgid "Debit" | ||||
| msgstr "Débit" | ||||
|  | ||||
| #: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268 | ||||
| #: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269 | ||||
| msgid "membership transaction" | ||||
| msgstr "transaction d'adhésion" | ||||
|  | ||||
| #: apps/note/models/transactions.py:264 | ||||
| #: apps/note/models/transactions.py:265 | ||||
| msgid "membership transactions" | ||||
| msgstr "transactions d'adhésion" | ||||
|  | ||||
| @@ -578,29 +623,29 @@ msgstr "Cliquez pour valider" | ||||
| msgid "No reason specified" | ||||
| msgstr "Pas de motif spécifié" | ||||
|  | ||||
| #: apps/note/views.py:41 | ||||
| #: apps/note/views.py:39 | ||||
| msgid "Transfer money" | ||||
| msgstr "Transférer de l'argent" | ||||
|  | ||||
| #: apps/note/views.py:102 templates/base.html:79 | ||||
| #: apps/note/views.py:100 templates/base.html:79 | ||||
| msgid "Consumptions" | ||||
| msgstr "Consommations" | ||||
|  | ||||
| #: apps/permission/models.py:69 apps/permission/models.py:262 | ||||
| #: apps/permission/models.py:82 apps/permission/models.py:275 | ||||
| #, python-brace-format | ||||
| msgid "Can {type} {model}.{field} in {query}" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/permission/models.py:71 apps/permission/models.py:264 | ||||
| #: apps/permission/models.py:84 apps/permission/models.py:277 | ||||
| #, python-brace-format | ||||
| msgid "Can {type} {model} in {query}" | ||||
| msgstr "" | ||||
|  | ||||
| #: apps/permission/models.py:84 | ||||
| #: apps/permission/models.py:97 | ||||
| msgid "rank" | ||||
| msgstr "Rang" | ||||
|  | ||||
| #: apps/permission/models.py:147 | ||||
| #: apps/permission/models.py:160 | ||||
| msgid "Specifying field applies only to view and change permission types." | ||||
| msgstr "" | ||||
|  | ||||
| @@ -608,31 +653,32 @@ msgstr "" | ||||
| msgid "Treasury" | ||||
| msgstr "Trésorerie" | ||||
|  | ||||
| #: apps/treasury/forms.py:84 apps/treasury/forms.py:132 | ||||
| #: apps/treasury/forms.py:85 apps/treasury/forms.py:133 | ||||
| #: templates/activity/activity_form.html:9 | ||||
| #: templates/activity/activity_invite.html:8 | ||||
| #: templates/django_filters/rest_framework/form.html:5 | ||||
| #: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46 | ||||
| #: templates/member/add_members.html:14 templates/member/club_form.html:9 | ||||
| #: templates/treasury/invoice_form.html:46 | ||||
| msgid "Submit" | ||||
| msgstr "Envoyer" | ||||
|  | ||||
| #: apps/treasury/forms.py:86 | ||||
| #: apps/treasury/forms.py:87 | ||||
| msgid "Close" | ||||
| msgstr "Fermer" | ||||
|  | ||||
| #: apps/treasury/forms.py:95 | ||||
| #: apps/treasury/forms.py:96 | ||||
| msgid "Remittance is already closed." | ||||
| msgstr "La remise est déjà fermée." | ||||
|  | ||||
| #: apps/treasury/forms.py:100 | ||||
| #: apps/treasury/forms.py:101 | ||||
| msgid "You can't change the type of the remittance." | ||||
| msgstr "Vous ne pouvez pas changer le type de la remise." | ||||
|  | ||||
| #: apps/treasury/forms.py:124 templates/note/transaction_form.html:98 | ||||
| #: apps/treasury/forms.py:125 templates/note/transaction_form.html:98 | ||||
| msgid "Bank" | ||||
| msgstr "Banque" | ||||
|  | ||||
| #: apps/treasury/forms.py:126 apps/treasury/tables.py:47 | ||||
| #: apps/treasury/forms.py:127 apps/treasury/tables.py:47 | ||||
| #: templates/note/transaction_form.html:128 | ||||
| #: templates/treasury/remittance_form.html:18 | ||||
| msgid "Amount" | ||||
| @@ -881,19 +927,23 @@ msgstr "Ajouter un alias" | ||||
| msgid "Club Parent" | ||||
| msgstr "Club parent" | ||||
|  | ||||
| #: templates/member/club_info.html:41 | ||||
| #: templates/member/club_info.html:29 | ||||
| msgid "days" | ||||
| msgstr "jours" | ||||
|  | ||||
| #: templates/member/club_info.html:32 | ||||
| msgid "membership fee" | ||||
| msgstr "cotisation pour adhérer" | ||||
|  | ||||
| #: templates/member/club_info.html:52 | ||||
| msgid "Add member" | ||||
| msgstr "Ajouter un membre" | ||||
|  | ||||
| #: templates/member/club_info.html:42 templates/note/conso_form.html:121 | ||||
| #: templates/member/club_info.html:55 templates/note/conso_form.html:121 | ||||
| msgid "Edit" | ||||
| msgstr "Éditer" | ||||
|  | ||||
| #: templates/member/club_info.html:43 | ||||
| msgid "Add roles" | ||||
| msgstr "Ajouter des rôles" | ||||
|  | ||||
| #: templates/member/club_info.html:46 templates/member/profile_info.html:48 | ||||
| #: templates/member/club_info.html:59 templates/member/profile_info.html:48 | ||||
| msgid "View Profile" | ||||
| msgstr "Voir le profil" | ||||
|  | ||||
| @@ -902,8 +952,8 @@ msgid "search clubs" | ||||
| msgstr "Chercher un club" | ||||
|  | ||||
| #: templates/member/club_list.html:12 | ||||
| msgid "Créer un club" | ||||
| msgstr "" | ||||
| msgid "Create club" | ||||
| msgstr "Créer un club" | ||||
|  | ||||
| #: templates/member/club_list.html:19 | ||||
| msgid "club listing " | ||||
|   | ||||
| @@ -299,4 +299,4 @@ class YearPickerInput(BasePickerInput): | ||||
|     def _link_to(self, linked_picker): | ||||
|         """Customize the options when linked with other date-time input""" | ||||
|         yformat = self.config['options']['format'].replace('-01-01', '-12-31') | ||||
|         self.config['options']['format'] = yformat | ||||
|         self.config['options']['format'] = yformat | ||||
|   | ||||
| @@ -70,7 +70,7 @@ function refreshBalance() { | ||||
|  * @param fun For each found note with the matched alias `alias`, fun(note, alias) is called. | ||||
|  */ | ||||
| function getMatchedNotes(pattern, fun) { | ||||
|     $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun); | ||||
|     $.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club|activity&ordering=normalized_name", fun); | ||||
| } | ||||
|  | ||||
| /** | ||||
|   | ||||
| @@ -25,7 +25,7 @@ | ||||
|                 <dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt> | ||||
|                 <dd class="col-xl-6">{{ activity.date_end }}</dd> | ||||
|  | ||||
|                 {% if "view_"|has_perm:activity.creater %} | ||||
|                 {% if ".view_"|has_perm:activity.creater %} | ||||
|                     <dt class="col-xl-6">{% trans 'creater'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd> | ||||
|                 {% endif %} | ||||
| @@ -53,17 +53,17 @@ | ||||
|         </div> | ||||
|  | ||||
|         <div class="card-footer text-center"> | ||||
|             {% if activity.open and "change__open"|has_perm:activity %} | ||||
|             {% if activity.open and ".change__open"|has_perm:activity %} | ||||
|                 <a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a> | ||||
|             {% endif %} | ||||
|  | ||||
|             {% if activity.valid and "change__open"|has_perm:activity %} | ||||
|             {% if activity.valid and ".change__open"|has_perm:activity %} | ||||
|                 <a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a> | ||||
|             {% endif %} | ||||
|             {% if not activity.open and "change__valid"|has_perm:activity %} | ||||
|             {% if not activity.open and ".change__valid"|has_perm:activity %} | ||||
|                 <a class="btn btn-success btn-sm my-1" id="validate_activity"> {% if activity.valid %}{% trans "invalidate"|capfirst %}{% else %}{% trans "validate"|capfirst %}{% endif %}</a> | ||||
|             {% endif %} | ||||
|             {% if "view_"|has_perm:activity %} | ||||
|             {% if ".view_"|has_perm:activity %} | ||||
|                 <a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}"> {% trans "edit"|capfirst %}</a> | ||||
|             {% endif %} | ||||
|             {% if activity.activity_type.can_invite and not activity_started %} | ||||
|   | ||||
| @@ -84,6 +84,11 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|                         <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> | ||||
|                     </li> | ||||
|                 {% endif %} | ||||
|                 {% if "auth.user"|model_list|length >= 2 %} | ||||
|                     <li class="nav-item active"> | ||||
|                         <a class="nav-link" href="{% url 'member:user_list' %}"><i class="fa fa-user"></i> {% trans 'Users' %}</a> | ||||
|                     </li> | ||||
|                 {% endif %} | ||||
|                 {% if "member.club"|not_empty_model_list %} | ||||
|                     <li class="nav-item active"> | ||||
|                         <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> | ||||
|   | ||||
| @@ -1,29 +1,21 @@ | ||||
| {% extends "member/noteowner_detail.html" %} | ||||
| {% load crispy_forms_tags %} | ||||
| {% load static %} | ||||
| {% load i18n %} | ||||
|  | ||||
| {% block profile_info %} | ||||
| {% include "member/club_info.html" %} | ||||
| {% endblock %} | ||||
| {% block profile_content %} | ||||
|  | ||||
| {% block profile_content %} | ||||
| <form method="post" action=""> | ||||
|     {% csrf_token %} | ||||
|     {% crispy formset helper %} | ||||
|     <div class="form-actions"> | ||||
|         <input type="submit" name="submit" value="Add Members" class="btn btn-primary" id="submit-save"> | ||||
|     </div> | ||||
|     {{ form|crispy }} | ||||
|     <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> | ||||
| </form> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
| <script src="{% static 'js/dynamic-formset.js' %}"></script> | ||||
| <script> | ||||
|     $('.formset-row').formset({ | ||||
|         addText: 'add another',          // Text for the add link | ||||
|         deleteText: 'remove',            // Text for the delete link | ||||
|         addCssClass: 'btn btn-primary',          // CSS class applied to the add link | ||||
|         deleteCssClass: 'btn btn-danger h-50 my-auto', | ||||
|     }); | ||||
| </script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -7,3 +7,12 @@ | ||||
| {% block profile_content %} | ||||
| {% include "member/club_tables.html" %} | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
|     <script> | ||||
|     function refreshHistory() { | ||||
|         $("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list"); | ||||
|         $("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos"); | ||||
|     } | ||||
|     </script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -9,3 +9,25 @@ | ||||
| <button class="btn btn-primary" type="submit">{% trans "Submit" %}</button> | ||||
| </form> | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
|     <script> | ||||
|         require_memberships_obj = $("#id_require_memberships"); | ||||
|  | ||||
|         if (!require_memberships_obj.is(":checked")) { | ||||
|             $("#div_id_membership_fee_paid").toggle(); | ||||
|             $("#div_id_membership_fee_unpaid").toggle(); | ||||
|             $("#div_id_membership_duration").toggle(); | ||||
|             $("#div_id_membership_start").toggle(); | ||||
|             $("#div_id_membership_end").toggle(); | ||||
|         } | ||||
|  | ||||
|         require_memberships_obj.change(function () { | ||||
|             $("#div_id_membership_fee_paid").toggle(); | ||||
|             $("#div_id_membership_fee_unpaid").toggle(); | ||||
|             $("#div_id_membership_duration").toggle(); | ||||
|             $("#div_id_membership_start").toggle(); | ||||
|             $("#div_id_membership_end").toggle(); | ||||
|         }); | ||||
|     </script> | ||||
| {% endblock %} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| {% load i18n static pretty_money %} | ||||
| {% load i18n static pretty_money perms %} | ||||
| <div class="card bg-light shadow"> | ||||
|     <div class="card-header text-center"> | ||||
|         <h4> Club {{ club.name }} </h4> | ||||
| @@ -18,31 +18,44 @@ | ||||
|                 <dd class="col-xl-6"> {{ club.parent_club.name}}</dd> | ||||
|             {% endif %} | ||||
|  | ||||
|             <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6">{{ club.membership_start }}</dd> | ||||
|             {% if club.require_memberships %} | ||||
|                 <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> | ||||
|                 <dd class="col-xl-6">{{ club.membership_start }}</dd> | ||||
|  | ||||
|             <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6">{{ club.membership_end }}</dd> | ||||
|                 <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> | ||||
|                 <dd class="col-xl-6">{{ club.membership_end }}</dd> | ||||
|  | ||||
|             <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6">{{ club.membership_duration }}</dd> | ||||
|                 <dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt> | ||||
|                 <dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd> | ||||
|  | ||||
|             <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> | ||||
|             <dd class="col-xl-6">{{ club.membership_fee|pretty_money }}</dd> | ||||
|                 {% if club.membership_fee_paid == club.membership_fee_unpaid %} | ||||
|                     <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd> | ||||
|                 {% else %} | ||||
|                     <dt class="col-xl-6">{% trans 'membership fee (paid students)'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd> | ||||
|  | ||||
|                     <dt class="col-xl-6">{% trans 'membership fee (unpaid students)'|capfirst %}</dt> | ||||
|                     <dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}</dd> | ||||
|                 {% endif %} | ||||
|             {% endif %} | ||||
|              | ||||
|             <dt class="col-xl-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> | ||||
|             <dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd> | ||||
|  | ||||
|             <dt class="col-xl-3">{% trans 'email'|capfirst %}</dt> | ||||
|             <dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email}}</a></dd> | ||||
|             <dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd> | ||||
|         </dl> | ||||
|     </div> | ||||
|     <div class="card-footer text-center"> | ||||
|         <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a> | ||||
|         <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a> | ||||
|         <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a> | ||||
|         {% if can_add_members %} | ||||
|             <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a> | ||||
|         {% endif %} | ||||
|         {% if ".change_"|has_perm:club %} | ||||
|             <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a> | ||||
|         {% endif %} | ||||
|         {% url 'member:club_detail' club.pk as club_detail_url %} | ||||
|         {%if request.get_full_path != club_detail_url %} | ||||
|         <a class="btn btn-primary btn-sm my-1" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a> | ||||
|         <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> | ||||
|         {% endif %}    </div> | ||||
| </div> | ||||
|   | ||||
| @@ -9,7 +9,7 @@ | ||||
|         </h4> | ||||
|         <input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/> | ||||
|         <hr> | ||||
|         <a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Créer un club" %}</a> | ||||
|         <a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a> | ||||
|     </div> | ||||
| </div> | ||||
| <div class="row justify-content-center">    | ||||
|   | ||||
| @@ -19,7 +19,7 @@ | ||||
|  | ||||
| {% block extrajavascript %} | ||||
|     <script> | ||||
|     function refreshhistory() { | ||||
|     function refreshHistory() { | ||||
|         $("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list"); | ||||
|         $("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos"); | ||||
|     } | ||||
|   | ||||
| @@ -2,28 +2,44 @@ | ||||
| {% load render_table from django_tables2 %} | ||||
| {% load crispy_forms_tags%} | ||||
| {% block content %} | ||||
|     <input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ..."> | ||||
|  | ||||
| <a class="btn btn-primary" href="{% url 'member:signup' %}">New User</a> | ||||
|     <hr> | ||||
|  | ||||
| <div class="row"> | ||||
| {% crispy filter.form filter.form.helper %} | ||||
| </div> | ||||
| <div class="row"> | ||||
|     <div id="replaceable-content" class="col-6"> | ||||
|         {% render_table  table %} | ||||
|     <div id="user_table"> | ||||
|         {% render_table table %} | ||||
|     </div> | ||||
| </div> | ||||
|  | ||||
| {% endblock %} | ||||
|  | ||||
| {% block extrajavascript %} | ||||
| <script type="text/javascript"> | ||||
|     $(document).ready(function() { | ||||
|         let old_pattern = null; | ||||
|         let searchbar_obj = $("#searchbar"); | ||||
|  | ||||
| $(document).ready(function($) { | ||||
|     $(".table-row").click(function() { | ||||
|         window.document.location = $(this).data("href"); | ||||
|         function reloadTable() { | ||||
|             let pattern = searchbar_obj.val(); | ||||
|  | ||||
|             if (pattern === old_pattern || pattern === "") | ||||
|                 return; | ||||
|  | ||||
|             $("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init); | ||||
|  | ||||
|             $(".table-row").click(function() { | ||||
|                 window.document.location = $(this).data("href"); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         searchbar_obj.keyup(reloadTable); | ||||
|  | ||||
|         function init() { | ||||
|             $(".table-row").click(function() { | ||||
|                 window.document.location = $(this).data("href"); | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         init(); | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| </script> | ||||
| {% endblock %} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user