mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-11-03 17:08:47 +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