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