diff --git a/apps/activity/models.py b/apps/activity/models.py index e3ff0c2f..ed2d94c9 100644 --- a/apps/activity/models.py +++ b/apps/activity/models.py @@ -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, diff --git a/apps/activity/views.py b/apps/activity/views.py index feb7591d..51e2ebf5 100644 --- a/apps/activity/views.py +++ b/apps/activity/views.py @@ -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 = [] diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 43fc1e13..68bf95c0 100644 --- a/apps/logs/signals.py +++ b/apps/logs/signals.py @@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_no_log"): + return + # noinspection PyProtectedMember previous = instance._previous @@ -106,6 +109,9 @@ def delete_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_no_log"): + return + # Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP user, ip = get_current_authenticated_user(), get_current_ip() diff --git a/apps/member/filters.py b/apps/member/filters.py deleted file mode 100644 index 951723e8..00000000 --- a/apps/member/filters.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay -# SPDX-License-Identifier: GPL-3.0-or-later - -from crispy_forms.helper import FormHelper -from crispy_forms.layout import Layout, Submit -from django.contrib.auth.models import User -from django.db.models import CharField -from django_filters import FilterSet, CharFilter - - -class UserFilter(FilterSet): - class Meta: - model = User - fields = ['last_name', 'first_name', 'username', 'profile__section'] - filter_overrides = { - CharField: { - 'filter_class': CharFilter, - 'extra': lambda f: { - 'lookup_expr': 'icontains' - } - } - } - - -class UserFilterFormHelper(FormHelper): - form_method = 'GET' - layout = Layout( - 'last_name', - 'first_name', - 'username', - 'profile__section', - Submit('Submit', 'Apply Filter'), - ) diff --git a/apps/member/fixtures/initial.json b/apps/member/fixtures/initial.json index bba1e7ac..e27eb72d 100644 --- a/apps/member/fixtures/initial.json +++ b/apps/member/fixtures/initial.json @@ -5,10 +5,12 @@ "fields": { "name": "BDE", "email": "tresorerie.bde@example.com", - "membership_fee": 500, - "membership_duration": "396 00:00:00", - "membership_start": "213 00:00:00", - "membership_end": "273 00:00:00" + "require_memberships": true, + "membership_fee_paid": 500, + "membership_fee_unpaid": 500, + "membership_duration": 396, + "membership_start": "2019-08-31", + "membership_end": "2020-09-30" } }, { @@ -17,10 +19,13 @@ "fields": { "name": "Kfet", "email": "tresorerie.bde@example.com", - "membership_fee": 3500, - "membership_duration": "396 00:00:00", - "membership_start": "213 00:00:00", - "membership_end": "273 00:00:00" + "parent_club": 1, + "require_memberships": true, + "membership_fee_paid": 3500, + "membership_fee_unpaid": 3500, + "membership_duration": 396, + "membership_start": "2019-08-31", + "membership_end": "2020-09-30" } } ] diff --git a/apps/member/forms.py b/apps/member/forms.py index 20f0acfe..a37d143e 100644 --- a/apps/member/forms.py +++ b/apps/member/forms.py @@ -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", - )) diff --git a/apps/member/models.py b/apps/member/models.py index d0051e59..693854af 100644 --- a/apps/member/models.py +++ b/apps/member/models.py @@ -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') diff --git a/apps/member/tables.py b/apps/member/tables.py index d0c37a6e..c8a510ff 100644 --- a/apps/member/tables.py +++ b/apps/member/tables.py @@ -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("{name}", + url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s) + + return s + + def render_fee(self, value, record): + t = pretty_money(value) + + # If it is required and if the user has the right, the renew button is displayed. + if record.club.membership_start is not None: + if record.date_start < record.club.membership_start: # If the renew is available + if not Membership.objects.filter( + club=record.club, + user=record.user, + date_start__gte=record.club.membership_start, + date_end__lte=record.club.membership_end, + ).exists(): # If the renew is not yet performed + empty_membership = Membership( + club=record.club, + user=record.user, + date_start=datetime.now().date(), + date_end=datetime.now().date(), + fee=0, + ) + if PermissionBackend.check_perm(get_current_authenticated_user(), + "member:add_membership", empty_membership): # If the user has right + t = format_html(t + ' {text}', + url=reverse_lazy('member:club_renew_membership', + kwargs={"pk": record.pk}), text=_("Renew")) + return t + + def render_roles(self, record): + roles = record.roles.all() + s = ", ".join(str(role) for role in roles) + if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record): + s = format_html("" + s + "") + return s + + class Meta: + attrs = { + 'class': 'table table-condensed table-striped table-hover', + 'style': 'table-layout: fixed;' + } + template_name = 'django_tables2/bootstrap4.html' + fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', ) + model = Membership diff --git a/apps/member/urls.py b/apps/member/urls.py index 085a3fec..1214f024 100644 --- a/apps/member/urls.py +++ b/apps/member/urls.py @@ -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//', views.ClubDetailView.as_view(), name="club_detail"), path('club//add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), - path('club/create/', views.ClubCreateView.as_view(), name="club_create"), - path('club//update', views.ClubUpdateView.as_view(), name="club_update"), - path('club//update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), - path('club//aliases', views.ClubAliasView.as_view(), name="club_alias"), + path('club/manage_roles//', views.ClubManageRolesView.as_view(), name="club_manage_roles"), + path('club/renew_membership//', views.ClubRenewMembershipView.as_view(), name="club_renew_membership"), + path('club//update/', views.ClubUpdateView.as_view(), name="club_update"), + path('club//update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"), + path('club//aliases/', views.ClubAliasView.as_view(), name="club_alias"), + path('user/', views.UserListView.as_view(), name="user_list"), - path('user/', views.UserDetailView.as_view(), name="user_detail"), - path('user//update', views.UserUpdateView.as_view(), name="user_update_profile"), - path('user//update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), - path('user//aliases', views.ProfileAliasView.as_view(), name="user_alias"), + path('user//', views.UserDetailView.as_view(), name="user_detail"), + path('user//update/', views.UserUpdateView.as_view(), name="user_update_profile"), + path('user//update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), + path('user//aliases/', views.ProfileAliasView.as_view(), name="user_alias"), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), ] diff --git a/apps/member/views.py b/apps/member/views.py index 8145b5e9..f695002f 100644 --- a/apps/member/views.py +++ b/apps/member/views.py @@ -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})) diff --git a/apps/note/admin.py b/apps/note/admin.py index 702d3350..7b4ba870 100644 --- a/apps/note/admin.py +++ b/apps/note/admin.py @@ -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): """ diff --git a/apps/note/api/serializers.py b/apps/note/api/serializers.py index fbd12038..5625e5b5 100644 --- a/apps/note/api/serializers.py +++ b/apps/note/api/serializers.py @@ -90,7 +90,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer): Note: NoteSerializer, NoteUser: NoteUserSerializer, NoteClub: NoteClubSerializer, - NoteSpecial: NoteSpecialSerializer + NoteSpecial: NoteSpecialSerializer, } class Meta: diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py index d1dcd788..83f8f914 100644 --- a/apps/note/models/transactions.py +++ b/apps/note/models/transactions.py @@ -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: diff --git a/apps/note/tables.py b/apps/note/tables.py index 0d83e3cc..a38beb9a 100644 --- a/apps/note/tables.py +++ b/apps/note/tables.py @@ -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) diff --git a/apps/note/views.py b/apps/note/views.py index 25279281..ac9b3e40 100644 --- a/apps/note/views.py +++ b/apps/note/views.py @@ -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): """ diff --git a/apps/permission/api/serializers.py b/apps/permission/api/serializers.py index 0a52f4fe..e30ed7dc 100644 --- a/apps/permission/api/serializers.py +++ b/apps/permission/api/serializers.py @@ -3,7 +3,7 @@ from rest_framework import serializers -from ..models import Permission +from ..models import Permission, RolePermissions class PermissionSerializer(serializers.ModelSerializer): @@ -15,3 +15,14 @@ class PermissionSerializer(serializers.ModelSerializer): class Meta: model = Permission fields = '__all__' + + +class RolePermissionsSerializer(serializers.ModelSerializer): + """ + REST API Serializer for RolePermissions types. + The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API. + """ + + class Meta: + model = RolePermissions + fields = '__all__' diff --git a/apps/permission/api/urls.py b/apps/permission/api/urls.py index d50344ea..b5d53466 100644 --- a/apps/permission/api/urls.py +++ b/apps/permission/api/urls.py @@ -1,11 +1,12 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later -from .views import PermissionViewSet +from .views import PermissionViewSet, RolePermissionsViewSet def register_permission_urls(router, path): """ Configure router for permission REST API. """ - router.register(path, PermissionViewSet) + router.register(path + "/permission", PermissionViewSet) + router.register(path + "/roles", RolePermissionsViewSet) diff --git a/apps/permission/api/views.py b/apps/permission/api/views.py index 965e82c9..6a068225 100644 --- a/apps/permission/api/views.py +++ b/apps/permission/api/views.py @@ -4,17 +4,29 @@ from django_filters.rest_framework import DjangoFilterBackend from api.viewsets import ReadOnlyProtectedModelViewSet -from .serializers import PermissionSerializer -from ..models import Permission +from .serializers import PermissionSerializer, RolePermissionsSerializer +from ..models import Permission, RolePermissions class PermissionViewSet(ReadOnlyProtectedModelViewSet): """ REST API View set. - The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, - then render it on /api/logs/ + The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer, + then render it on /api/permission/permission/ """ queryset = Permission.objects.all() serializer_class = PermissionSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ['model', 'type', ] + + +class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet): + """ + REST API View set. + The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer + then render it on /api/permission/roles/ + """ + queryset = RolePermissions.objects.all() + serializer_class = RolePermissionsSerializer + filter_backends = [DjangoFilterBackend] + filterset_fields = ['role', ] diff --git a/apps/permission/backends.py b/apps/permission/backends.py index e61b0719..4fb7b577 100644 --- a/apps/permission/backends.py +++ b/apps/permission/backends.py @@ -1,6 +1,8 @@ # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # SPDX-License-Identifier: GPL-3.0-or-later +import datetime + from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, AnonymousUser from django.contrib.contenttypes.models import ContentType @@ -9,6 +11,7 @@ from note.models import Note, NoteUser, NoteClub, NoteSpecial from note_kfet.middlewares import get_current_session from member.models import Membership, Club +from .decorators import memoize from .models import Permission @@ -20,6 +23,28 @@ class PermissionBackend(ModelBackend): supports_anonymous_user = False supports_inactive_user = False + @staticmethod + @memoize + def get_raw_permissions(user, t): + """ + Query permissions of a certain type for a user, then memoize it. + :param user: The owner of the permissions + :param t: The type of the permissions: view, change, add or delete + :return: The queryset of the permissions of the user (memoized) grouped by clubs + """ + if isinstance(user, AnonymousUser): + # Unauthenticated users have no permissions + return Permission.objects.none() + + return Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ + .filter( + rolepermissions__role__membership__user=user, + rolepermissions__role__membership__date_start__lte=datetime.date.today(), + rolepermissions__role__membership__date_end__gte=datetime.date.today(), + type=t, + mask__rank__lte=get_current_session().get("permission_mask", 0), + ).distinct('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 diff --git a/apps/permission/decorators.py b/apps/permission/decorators.py new file mode 100644 index 00000000..f144935a --- /dev/null +++ b/apps/permission/decorators.py @@ -0,0 +1,59 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from functools import lru_cache +from time import time + +from django.contrib.sessions.models import Session +from note_kfet.middlewares import get_current_session + + +def memoize(f): + """ + Memoize results and store in sessions + + This decorator is useful for permissions: they are loaded once needed, then stored for next calls. + The storage is contained with sessions since it depends on the selected mask. + """ + sess_funs = {} + last_collect = time() + + def collect(): + """ + Clear cache of results when sessions are invalid, to flush useless data. + This function is called every minute. + """ + nonlocal sess_funs + + new_sess_funs = {} + for sess_key in sess_funs: + if Session.objects.filter(session_key=sess_key).exists(): + new_sess_funs[sess_key] = sess_funs[sess_key] + sess_funs = new_sess_funs + + def func(*args, **kwargs): + nonlocal last_collect + + if time() - last_collect > 60: + # Clear cache + collect() + last_collect = time() + + # If there is no session, then we don't memoize anything. + sess = get_current_session() + if sess is None or sess.session_key is None: + return f(*args, **kwargs) + + sess_key = sess.session_key + if sess_key not in sess_funs: + # lru_cache makes the job of memoization + # We store only the 512 latest data per session. It has to be enough. + sess_funs[sess_key] = lru_cache(512)(f) + try: + return sess_funs[sess_key](*args, **kwargs) + except TypeError: # For add permissions, objects are not hashable (not yet created). Don't memoize this case. + return f(*args, **kwargs) + + func.func_name = f.__name__ + + return func diff --git a/apps/permission/fixtures/initial.json b/apps/permission/fixtures/initial.json index 31b59069..4cf3ecfa 100644 --- a/apps/permission/fixtures/initial.json +++ b/apps/permission/fixtures/initial.json @@ -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 + ] + } } ] diff --git a/apps/permission/models.py b/apps/permission/models.py index 205f5b41..8aaf416c 100644 --- a/apps/permission/models.py +++ b/apps/permission/models.py @@ -38,20 +38,33 @@ class InstancedPermission: if permission_type == self.type: self.update_query() - # Don't increase indexes - obj.pk = 0 + # Don't increase indexes, if the primary key is an AutoField + if not hasattr(obj, "pk") or not obj.pk: + obj.pk = 0 + oldpk = None + else: + oldpk = obj.pk + # Ensure previous models are deleted + self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete() # Force insertion, no data verification, no trigger + obj._force_save = True Model.save(obj, force_insert=True) - ret = obj in self.model.model_class().objects.filter(self.query).all() + # We don't want log anything + obj._no_log = True + ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() # Delete testing object + obj._force_delete = True Model.delete(obj) + + # If the primary key was specified, we restore it + obj.pk = oldpk return ret if permission_type == self.type: if self.field and field_name != self.field: return False self.update_query() - return obj in self.model.model_class().objects.filter(self.query).all() + return self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() else: return False diff --git a/apps/permission/permissions.py b/apps/permission/permissions.py index 7097085f..40321567 100644 --- a/apps/permission/permissions.py +++ b/apps/permission/permissions.py @@ -44,7 +44,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions): perms = self.get_required_object_permissions(request.method, model_cls) # if not user.has_perms(perms, obj): - if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms): + if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms): # If the user does not have permissions we need to determine if # they have read permissions to see 403, or not, and simply see # a 404 response. diff --git a/apps/permission/signals.py b/apps/permission/signals.py index 1e30f56f..bf54b72f 100644 --- a/apps/permission/signals.py +++ b/apps/permission/signals.py @@ -2,8 +2,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later from django.core.exceptions import PermissionDenied -from django.db.models.signals import pre_save, pre_delete, post_save, post_delete -from logs import signals as logs_signals from note_kfet.middlewares import get_current_authenticated_user from permission.backends import PermissionBackend @@ -29,6 +27,9 @@ def pre_save_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_force_save"): + return + user = get_current_authenticated_user() if user is None: # Action performed on shell is always granted @@ -43,7 +44,7 @@ def pre_save_object(sender, instance, **kwargs): # We check if the user can change the model # If the user has all right on a model, then OK - if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance): + if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance): return # In the other case, we check if he/she has the right to change one field @@ -55,35 +56,17 @@ def pre_save_object(sender, instance, **kwargs): # If the field wasn't modified, no need to check the permissions if old_value == new_value: continue - if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): + if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): raise PermissionDenied else: - # We check if the user can add the model - - # While checking permissions, the object will be inserted in the DB, then removed. - # We disable temporary the connectors - pre_save.disconnect(pre_save_object) - pre_delete.disconnect(pre_delete_object) - # We disable also logs connectors - pre_save.disconnect(logs_signals.pre_save_object) - post_save.disconnect(logs_signals.save_object) - post_delete.disconnect(logs_signals.delete_object) - # We check if the user has right to add the object - has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance) - - # Then we reconnect all - pre_save.connect(pre_save_object) - pre_delete.connect(pre_delete_object) - pre_save.connect(logs_signals.pre_save_object) - post_save.connect(logs_signals.save_object) - post_delete.connect(logs_signals.delete_object) + has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance) if not has_perm: raise PermissionDenied -def pre_delete_object(sender, instance, **kwargs): +def pre_delete_object(instance, **kwargs): """ Before a model get deleted, we check the permissions """ @@ -91,6 +74,9 @@ def pre_delete_object(sender, instance, **kwargs): if instance._meta.label_lower in EXCLUDED: return + if hasattr(instance, "_force_delete"): + return + user = get_current_authenticated_user() if user is None: # Action performed on shell is always granted @@ -101,5 +87,5 @@ def pre_delete_object(sender, instance, **kwargs): model_name = model_name_full[1] # We check if the user has rights to delete the object - if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance): + if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance): raise PermissionDenied diff --git a/apps/permission/templatetags/perms.py b/apps/permission/templatetags/perms.py index aa2feeca..a89c7f49 100644 --- a/apps/permission/templatetags/perms.py +++ b/apps/permission/templatetags/perms.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.models import ContentType from django.template.defaultfilters import stringfilter from django import template +from note.models import Transaction from note_kfet.middlewares import get_current_authenticated_user, get_current_session from permission.backends import PermissionBackend @@ -19,13 +20,8 @@ def not_empty_model_list(model_name): return False elif user.is_superuser and session.get("permission_mask", 0) >= 42: return True - if session.get("not_empty_model_list_" + model_name, None): - return session.get("not_empty_model_list_" + model_name, None) == 1 - spl = model_name.split(".") - ct = ContentType.objects.get(app_label=spl[0], model=spl[1]) - qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")).all() - session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2 - return session.get("not_empty_model_list_" + model_name) == 1 + qs = model_list(model_name) + return qs.exists() @stringfilter @@ -39,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) diff --git a/apps/permission/views.py b/apps/permission/views.py new file mode 100644 index 00000000..bbd9872f --- /dev/null +++ b/apps/permission/views.py @@ -0,0 +1,11 @@ +# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay +# SPDX-License-Identifier: GPL-3.0-or-later + +from permission.backends import PermissionBackend + + +class ProtectQuerysetMixin: + def get_queryset(self, **kwargs): + qs = super().get_queryset(**kwargs) + + return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")) diff --git a/apps/treasury/forms.py b/apps/treasury/forms.py index 7fe7de4c..ad479e14 100644 --- a/apps/treasury/forms.py +++ b/apps/treasury/forms.py @@ -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): """ diff --git a/apps/treasury/views.py b/apps/treasury/views.py index c374ced1..f564ccb2 100644 --- a/apps/treasury/views.py +++ b/apps/treasury/views.py @@ -19,13 +19,15 @@ from django.views.generic.base import View, TemplateView from django_tables2 import SingleTableView from note.models import SpecialTransaction, NoteSpecial from note_kfet.settings.base import BASE_DIR +from permission.backends import PermissionBackend +from permission.views import ProtectQuerysetMixin from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm from .models import Invoice, Product, Remittance, SpecialTransactionProxy from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable -class InvoiceCreateView(LoginRequiredMixin, CreateView): +class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView): """ Create Invoice """ @@ -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 """ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index e2c8075e..16c73f35 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-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 \n" "Language-Team: LANGUAGE \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 diff --git a/locale/fr/LC_MESSAGES/django.po b/locale/fr/LC_MESSAGES/django.po index 08c1b174..9754fa9c 100644 --- a/locale/fr/LC_MESSAGES/django.po +++ b/locale/fr/LC_MESSAGES/django.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-03-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 \n" "Language-Team: LANGUAGE \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 " diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py index a3170007..ecd758e0 100644 --- a/note_kfet/inputs.py +++ b/note_kfet/inputs.py @@ -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 \ No newline at end of file + self.config['options']['format'] = yformat diff --git a/static/js/base.js b/static/js/base.js index 22d1366a..7febd3d6 100644 --- a/static/js/base.js +++ b/static/js/base.js @@ -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); } /** diff --git a/templates/activity/activity_detail.html b/templates/activity/activity_detail.html index 0ed3c719..84182065 100644 --- a/templates/activity/activity_detail.html +++ b/templates/activity/activity_detail.html @@ -25,7 +25,7 @@
{% trans 'end date'|capfirst %}
{{ activity.date_end }}
- {% if "view_"|has_perm:activity.creater %} + {% if ".view_"|has_perm:activity.creater %}
{% trans 'creater'|capfirst %}
{{ activity.creater }}
{% endif %} @@ -53,17 +53,17 @@
diff --git a/templates/member/noteowner_detail.html b/templates/member/noteowner_detail.html index ad329aee..fc781549 100644 --- a/templates/member/noteowner_detail.html +++ b/templates/member/noteowner_detail.html @@ -19,7 +19,7 @@ {% block extrajavascript %} {% endblock %}