mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-11-26 18:37:12 +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'),
|
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(
|
attendees_club = models.ForeignKey(
|
||||||
'member.Club',
|
'member.Club',
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
@ -160,9 +151,7 @@ class Entry(models.Model):
|
|||||||
if insert and self.guest:
|
if insert and self.guest:
|
||||||
GuestTransaction.objects.create(
|
GuestTransaction.objects.create(
|
||||||
source=self.note,
|
source=self.note,
|
||||||
source_alias=self.note.user.username,
|
destination=self.activity.organizer.note,
|
||||||
destination=self.note,
|
|
||||||
destination_alias=self.activity.organizer.name,
|
|
||||||
quantity=1,
|
quantity=1,
|
||||||
amount=self.activity.activity_type.guest_entry_fee,
|
amount=self.activity.activity_type.guest_entry_fee,
|
||||||
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
|
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
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
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 django_tables2.views import SingleTableView
|
||||||
from note.models import NoteUser, Alias, NoteSpecial
|
from note.models import NoteUser, Alias, NoteSpecial
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
from permission.views import ProtectQuerysetMixin
|
||||||
|
|
||||||
from .forms import ActivityForm, GuestForm
|
from .forms import ActivityForm, GuestForm
|
||||||
from .models import Activity, Guest, Entry
|
from .models import Activity, Guest, Entry
|
||||||
from .tables import ActivityTable, GuestTable, EntryTable
|
from .tables import ActivityTable, GuestTable, EntryTable
|
||||||
|
|
||||||
|
|
||||||
class ActivityCreateView(LoginRequiredMixin, CreateView):
|
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
|
||||||
@ -30,13 +32,12 @@ class ActivityCreateView(LoginRequiredMixin, CreateView):
|
|||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
class ActivityListView(LoginRequiredMixin, SingleTableView):
|
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
model = Activity
|
model = Activity
|
||||||
table_class = ActivityTable
|
table_class = ActivityTable
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset()\
|
return super().get_queryset().reverse()
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).reverse()
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
@ -50,7 +51,7 @@ class ActivityListView(LoginRequiredMixin, SingleTableView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class ActivityDetailView(LoginRequiredMixin, DetailView):
|
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
model = Activity
|
model = Activity
|
||||||
context_object_name = "activity"
|
context_object_name = "activity"
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class ActivityUpdateView(LoginRequiredMixin, UpdateView):
|
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = Activity
|
model = Activity
|
||||||
form_class = ActivityForm
|
form_class = ActivityForm
|
||||||
|
|
||||||
@ -74,18 +75,20 @@ class ActivityUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
||||||
|
|
||||||
|
|
||||||
class ActivityInviteView(LoginRequiredMixin, CreateView):
|
class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
model = Guest
|
model = Guest
|
||||||
form_class = GuestForm
|
form_class = GuestForm
|
||||||
template_name = "activity/activity_invite.html"
|
template_name = "activity/activity_invite.html"
|
||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
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
|
return form
|
||||||
|
|
||||||
def form_valid(self, 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)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
@ -98,7 +101,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**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
|
ctx["activity"] = activity
|
||||||
|
|
||||||
matched = []
|
matched = []
|
||||||
|
@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs):
|
|||||||
if instance._meta.label_lower in EXCLUDED:
|
if instance._meta.label_lower in EXCLUDED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if hasattr(instance, "_no_log"):
|
||||||
|
return
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
previous = instance._previous
|
previous = instance._previous
|
||||||
|
|
||||||
@ -106,6 +109,9 @@ def delete_object(sender, instance, **kwargs):
|
|||||||
if instance._meta.label_lower in EXCLUDED:
|
if instance._meta.label_lower in EXCLUDED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if hasattr(instance, "_no_log"):
|
||||||
|
return
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# 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()
|
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": {
|
"fields": {
|
||||||
"name": "BDE",
|
"name": "BDE",
|
||||||
"email": "tresorerie.bde@example.com",
|
"email": "tresorerie.bde@example.com",
|
||||||
"membership_fee": 500,
|
"require_memberships": true,
|
||||||
"membership_duration": "396 00:00:00",
|
"membership_fee_paid": 500,
|
||||||
"membership_start": "213 00:00:00",
|
"membership_fee_unpaid": 500,
|
||||||
"membership_end": "273 00:00:00"
|
"membership_duration": 396,
|
||||||
|
"membership_start": "2019-08-31",
|
||||||
|
"membership_end": "2020-09-30"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -17,10 +19,13 @@
|
|||||||
"fields": {
|
"fields": {
|
||||||
"name": "Kfet",
|
"name": "Kfet",
|
||||||
"email": "tresorerie.bde@example.com",
|
"email": "tresorerie.bde@example.com",
|
||||||
"membership_fee": 3500,
|
"parent_club": 1,
|
||||||
"membership_duration": "396 00:00:00",
|
"require_memberships": true,
|
||||||
"membership_start": "213 00:00:00",
|
"membership_fee_paid": 3500,
|
||||||
"membership_end": "273 00:00:00"
|
"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
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# 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 import forms
|
||||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||||
from django.contrib.auth.models import User
|
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 permission.models import PermissionMask
|
||||||
|
|
||||||
from .models import Profile, Club, Membership
|
from .models import Profile, Club, Membership
|
||||||
@ -47,11 +44,18 @@ class ClubForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Club
|
model = Club
|
||||||
fields = '__all__'
|
fields = '__all__'
|
||||||
|
widgets = {
|
||||||
|
"membership_fee_paid": AmountInput(),
|
||||||
class AddMembersForm(forms.Form):
|
"membership_fee_unpaid": AmountInput(),
|
||||||
class Meta:
|
"parent_club": Autocomplete(
|
||||||
fields = ('',)
|
Club,
|
||||||
|
attrs={
|
||||||
|
'api_url': '/api/members/club/',
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"membership_start": DatePickerInput(),
|
||||||
|
"membership_end": DatePickerInput(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class MembershipForm(forms.ModelForm):
|
class MembershipForm(forms.ModelForm):
|
||||||
@ -71,28 +75,5 @@ class MembershipForm(forms.ModelForm):
|
|||||||
'placeholder': 'Nom ...',
|
'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
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.urls import reverse, reverse_lazy
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from note.models import MembershipTransaction
|
||||||
|
|
||||||
|
|
||||||
class Profile(models.Model):
|
class Profile(models.Model):
|
||||||
@ -77,22 +79,43 @@ class Club(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Memberships
|
# 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,
|
null=True,
|
||||||
verbose_name=_('membership duration'),
|
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).'),
|
'(NULL = infinite).'),
|
||||||
)
|
)
|
||||||
membership_start = models.DurationField(
|
|
||||||
|
membership_start = models.DateField(
|
||||||
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('membership start'),
|
verbose_name=_('membership start'),
|
||||||
help_text=_('How long after January 1st the members can renew '
|
help_text=_('How long after January 1st the members can renew '
|
||||||
'their membership.'),
|
'their membership.'),
|
||||||
)
|
)
|
||||||
membership_end = models.DurationField(
|
|
||||||
|
membership_end = models.DateField(
|
||||||
|
blank=True,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name=_('membership end'),
|
verbose_name=_('membership end'),
|
||||||
help_text=_('How long the membership can last after January 1st '
|
help_text=_('How long the membership can last after January 1st '
|
||||||
@ -100,6 +123,33 @@ class Club(models.Model):
|
|||||||
'membership.'),
|
'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:
|
class Meta:
|
||||||
verbose_name = _("club")
|
verbose_name = _("club")
|
||||||
verbose_name_plural = _("clubs")
|
verbose_name_plural = _("clubs")
|
||||||
@ -114,9 +164,6 @@ class Club(models.Model):
|
|||||||
class Role(models.Model):
|
class Role(models.Model):
|
||||||
"""
|
"""
|
||||||
Role that an :model:`auth.User` can have in a :model:`member.Club`
|
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(
|
name = models.CharField(
|
||||||
verbose_name=_('name'),
|
verbose_name=_('name'),
|
||||||
@ -138,24 +185,31 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
user = models.ForeignKey(
|
user = models.ForeignKey(
|
||||||
settings.AUTH_USER_MODEL,
|
User,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
verbose_name=_("user"),
|
||||||
)
|
)
|
||||||
|
|
||||||
club = models.ForeignKey(
|
club = models.ForeignKey(
|
||||||
Club,
|
Club,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
|
verbose_name=_("club"),
|
||||||
)
|
)
|
||||||
roles = models.ForeignKey(
|
|
||||||
|
roles = models.ManyToManyField(
|
||||||
Role,
|
Role,
|
||||||
on_delete=models.PROTECT,
|
verbose_name=_("roles"),
|
||||||
)
|
)
|
||||||
|
|
||||||
date_start = models.DateField(
|
date_start = models.DateField(
|
||||||
verbose_name=_('membership starts on'),
|
verbose_name=_('membership starts on'),
|
||||||
)
|
)
|
||||||
|
|
||||||
date_end = models.DateField(
|
date_end = models.DateField(
|
||||||
verbose_name=_('membership ends on'),
|
verbose_name=_('membership ends on'),
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
fee = models.PositiveIntegerField(
|
fee = models.PositiveIntegerField(
|
||||||
verbose_name=_('fee'),
|
verbose_name=_('fee'),
|
||||||
)
|
)
|
||||||
@ -168,10 +222,54 @@ class Membership(models.Model):
|
|||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.club.parent_club is not None:
|
if self.club.parent_club is not None:
|
||||||
if not Membership.objects.filter(user=self.user, club=self.club.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'))
|
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)
|
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:
|
class Meta:
|
||||||
verbose_name = _('membership')
|
verbose_name = _('membership')
|
||||||
verbose_name_plural = _('memberships')
|
verbose_name_plural = _('memberships')
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
import django_tables2 as tables
|
import django_tables2 as tables
|
||||||
from django.contrib.auth.models import User
|
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):
|
class ClubTable(tables.Table):
|
||||||
@ -24,7 +31,11 @@ class ClubTable(tables.Table):
|
|||||||
|
|
||||||
class UserTable(tables.Table):
|
class UserTable(tables.Table):
|
||||||
section = tables.Column(accessor='profile.section')
|
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:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
@ -33,3 +44,68 @@ class UserTable(tables.Table):
|
|||||||
template_name = 'django_tables2/bootstrap4.html'
|
template_name = 'django_tables2/bootstrap4.html'
|
||||||
fields = ('last_name', 'first_name', 'username', 'email')
|
fields = ('last_name', 'first_name', 'username', 'email')
|
||||||
model = User
|
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'
|
app_name = 'member'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('signup/', views.UserCreateView.as_view(), name="signup"),
|
path('signup/', views.UserCreateView.as_view(), name="signup"),
|
||||||
|
|
||||||
path('club/', views.ClubListView.as_view(), name="club_list"),
|
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>/', views.ClubDetailView.as_view(), name="club_detail"),
|
||||||
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
|
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/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"),
|
||||||
path('club/<int:pk>/update', views.ClubUpdateView.as_view(), name="club_update"),
|
path('club/renew_membership/<int:pk>/', views.ClubRenewMembershipView.as_view(), name="club_renew_membership"),
|
||||||
path('club/<int:pk>/update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
|
path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
|
||||||
path('club/<int:pk>/aliases', views.ClubAliasView.as_view(), name="club_alias"),
|
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/', views.UserListView.as_view(), name="user_list"),
|
||||||
path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
|
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/', 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>/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>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
|
||||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||||
]
|
]
|
||||||
|
@ -2,17 +2,21 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.auth.views import LoginView
|
from django.contrib.auth.views import LoginView
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
|
from django.forms import HiddenInput
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
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.views.generic.edit import FormMixin
|
||||||
from django_tables2.views import SingleTableView
|
from django_tables2.views import SingleTableView
|
||||||
from rest_framework.authtoken.models import Token
|
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.models.transactions import Transaction
|
||||||
from note.tables import HistoryTable, AliasTable
|
from note.tables import HistoryTable, AliasTable
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
from permission.views import ProtectQuerysetMixin
|
||||||
|
|
||||||
from .filters import UserFilter, UserFilterFormHelper
|
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
|
||||||
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \
|
|
||||||
CustomAuthenticationForm
|
|
||||||
from .models import Club, Membership
|
from .models import Club, Membership
|
||||||
from .tables import ClubTable, UserTable
|
from .tables import ClubTable, UserTable, MembershipTable
|
||||||
|
|
||||||
|
|
||||||
class CustomLoginView(LoginView):
|
class CustomLoginView(LoginView):
|
||||||
@ -63,7 +66,7 @@ class UserCreateView(CreateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class UserUpdateView(LoginRequiredMixin, UpdateView):
|
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = User
|
model = User
|
||||||
fields = ['first_name', 'last_name', 'username', 'email']
|
fields = ['first_name', 'last_name', 'username', 'email']
|
||||||
template_name = 'member/profile_update.html'
|
template_name = 'member/profile_update.html'
|
||||||
@ -97,7 +100,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
if form.is_valid() and profile_form.is_valid():
|
if form.is_valid() and profile_form.is_valid():
|
||||||
new_username = form.data['username']
|
new_username = form.data['username']
|
||||||
alias = Alias.objects.filter(name=new_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():
|
if not alias.exists():
|
||||||
similar = Alias.objects.filter(
|
similar = Alias.objects.filter(
|
||||||
normalized_name=Alias.normalize(new_username))
|
normalized_name=Alias.normalize(new_username))
|
||||||
@ -119,7 +123,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return reverse_lazy('member:user_detail', args=(self.object.id,))
|
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...
|
Affiche les informations sur un utilisateur, sa note, ses clubs...
|
||||||
"""
|
"""
|
||||||
@ -127,44 +131,56 @@ class UserDetailView(LoginRequiredMixin, DetailView):
|
|||||||
context_object_name = "user_object"
|
context_object_name = "user_object"
|
||||||
template_name = "member/profile_detail.html"
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
user = context['user_object']
|
user = context['user_object']
|
||||||
history_list = \
|
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)
|
context['history_list'] = HistoryTable(history_list)
|
||||||
club_list = \
|
club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\
|
||||||
Membership.objects.all().filter(user=user).only("club")
|
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
|
||||||
context['club_list'] = ClubTable(club_list)
|
context['club_list'] = MembershipTable(data=club_list)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class UserListView(LoginRequiredMixin, SingleTableView):
|
class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
Affiche la liste des utilisateurs, avec une fonction de recherche statique
|
Affiche la liste des utilisateurs, avec une fonction de recherche statique
|
||||||
"""
|
"""
|
||||||
model = User
|
model = User
|
||||||
table_class = UserTable
|
table_class = UserTable
|
||||||
template_name = 'member/user_list.html'
|
template_name = 'member/user_list.html'
|
||||||
filter_class = UserFilter
|
|
||||||
formhelper_class = UserFilterFormHelper
|
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
|
qs = super().get_queryset()
|
||||||
self.filter = self.filter_class(self.request.GET, queryset=qs)
|
if "search" in self.request.GET:
|
||||||
self.filter.form.helper = self.formhelper_class()
|
pattern = self.request.GET["search"]
|
||||||
return self.filter.qs
|
|
||||||
|
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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["filter"] = self.filter
|
|
||||||
|
context["title"] = _("Search user")
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ProfileAliasView(LoginRequiredMixin, DetailView):
|
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
model = User
|
model = User
|
||||||
template_name = 'member/profile_alias.html'
|
template_name = 'member/profile_alias.html'
|
||||||
context_object_name = 'user_object'
|
context_object_name = 'user_object'
|
||||||
@ -176,11 +192,11 @@ class ProfileAliasView(LoginRequiredMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
|
class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
|
||||||
form_class = ImageForm
|
form_class = ImageForm
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(*args, **kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['form'] = self.form_class(self.request.POST, self.request.FILES)
|
context['form'] = self.form_class(self.request.POST, self.request.FILES)
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -237,8 +253,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
|||||||
template_name = "member/manage_auth_tokens.html"
|
template_name = "member/manage_auth_tokens.html"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
if 'regenerate' in request.GET and Token.objects.filter(
|
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
||||||
user=request.user).exists():
|
|
||||||
Token.objects.get(user=self.request.user).delete()
|
Token.objects.get(user=self.request.user).delete()
|
||||||
return redirect(reverse_lazy('member:auth_token') + "?show",
|
return redirect(reverse_lazy('member:auth_token') + "?show",
|
||||||
permanent=True)
|
permanent=True)
|
||||||
@ -247,8 +262,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['token'] = Token.objects.get_or_create(
|
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
|
||||||
user=self.request.user)[0]
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
@ -257,7 +271,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
|||||||
# ******************************* #
|
# ******************************* #
|
||||||
|
|
||||||
|
|
||||||
class ClubCreateView(LoginRequiredMixin, CreateView):
|
class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create Club
|
Create Club
|
||||||
"""
|
"""
|
||||||
@ -269,38 +283,49 @@ class ClubCreateView(LoginRequiredMixin, CreateView):
|
|||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
class ClubListView(LoginRequiredMixin, SingleTableView):
|
class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
List existing Clubs
|
List existing Clubs
|
||||||
"""
|
"""
|
||||||
model = Club
|
model = Club
|
||||||
table_class = ClubTable
|
table_class = ClubTable
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
|
||||||
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
|
|
||||||
|
|
||||||
|
class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
class ClubDetailView(LoginRequiredMixin, DetailView):
|
|
||||||
model = Club
|
model = Club
|
||||||
context_object_name = "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):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
club = context["club"]
|
club = context["club"]
|
||||||
club_transactions = \
|
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
|
||||||
Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))
|
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)
|
context['history_list'] = HistoryTable(club_transactions)
|
||||||
club_member = \
|
club_member = Membership.objects.filter(
|
||||||
Membership.objects.all().filter(club=club)
|
club=club,
|
||||||
# TODO: consider only valid Membership
|
date_end__gte=datetime.today(),
|
||||||
context['member_list'] = club_member
|
).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
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ClubAliasView(LoginRequiredMixin, DetailView):
|
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||||
model = Club
|
model = Club
|
||||||
template_name = 'member/club_alias.html'
|
template_name = 'member/club_alias.html'
|
||||||
context_object_name = 'club'
|
context_object_name = 'club'
|
||||||
@ -312,12 +337,14 @@ class ClubAliasView(LoginRequiredMixin, DetailView):
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
class ClubUpdateView(LoginRequiredMixin, UpdateView):
|
class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
model = Club
|
model = Club
|
||||||
context_object_name = "club"
|
context_object_name = "club"
|
||||||
form_class = ClubForm
|
form_class = ClubForm
|
||||||
template_name = "member/club_form.html"
|
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):
|
class ClubPictureUpdateView(PictureUpdateView):
|
||||||
@ -329,35 +356,123 @@ class ClubPictureUpdateView(PictureUpdateView):
|
|||||||
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id})
|
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id})
|
||||||
|
|
||||||
|
|
||||||
class ClubAddMemberView(LoginRequiredMixin, CreateView):
|
class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
model = Membership
|
model = Membership
|
||||||
form_class = MembershipForm
|
form_class = MembershipForm
|
||||||
template_name = 'member/add_members.html'
|
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):
|
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 = super().get_context_data(**kwargs)
|
||||||
context['formset'] = MemberFormSet()
|
|
||||||
context['helper'] = FormSetHelper()
|
|
||||||
context['club'] = club
|
context['club'] = club
|
||||||
context['no_cache'] = True
|
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def post(self, request, *args, **kwargs):
|
def form_valid(self, form):
|
||||||
return
|
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
||||||
# TODO: Implement POST
|
.get(pk=self.kwargs["pk"])
|
||||||
# formset = MembershipFormset(request.POST)
|
user = self.request.user
|
||||||
# if formset.is_valid():
|
form.instance.club = club
|
||||||
# return self.form_valid(formset)
|
|
||||||
# else:
|
|
||||||
# return self.form_invalid(formset)
|
|
||||||
|
|
||||||
def form_valid(self, formset):
|
if user.profile.paid:
|
||||||
formset.save()
|
fee = club.membership_fee_paid
|
||||||
return super().form_valid(formset)
|
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 []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(MembershipTransaction)
|
||||||
|
class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
|
||||||
|
"""
|
||||||
|
Admin customisation for Transaction
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
@admin.register(TransactionTemplate)
|
@admin.register(TransactionTemplate)
|
||||||
class TransactionTemplateAdmin(admin.ModelAdmin):
|
class TransactionTemplateAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
|
@ -90,7 +90,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
|
|||||||
Note: NoteSerializer,
|
Note: NoteSerializer,
|
||||||
NoteUser: NoteUserSerializer,
|
NoteUser: NoteUserSerializer,
|
||||||
NoteClub: NoteClubSerializer,
|
NoteClub: NoteClubSerializer,
|
||||||
NoteSpecial: NoteSpecialSerializer
|
NoteSpecial: NoteSpecialSerializer,
|
||||||
}
|
}
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -46,12 +46,14 @@ class TransactionTemplate(models.Model):
|
|||||||
unique=True,
|
unique=True,
|
||||||
error_messages={'unique': _("A template with this name already exist")},
|
error_messages={'unique': _("A template with this name already exist")},
|
||||||
)
|
)
|
||||||
|
|
||||||
destination = models.ForeignKey(
|
destination = models.ForeignKey(
|
||||||
NoteClub,
|
NoteClub,
|
||||||
on_delete=models.PROTECT,
|
on_delete=models.PROTECT,
|
||||||
related_name='+', # no reverse
|
related_name='+', # no reverse
|
||||||
verbose_name=_('destination'),
|
verbose_name=_('destination'),
|
||||||
)
|
)
|
||||||
|
|
||||||
amount = models.PositiveIntegerField(
|
amount = models.PositiveIntegerField(
|
||||||
verbose_name=_('amount'),
|
verbose_name=_('amount'),
|
||||||
help_text=_('in centimes'),
|
help_text=_('in centimes'),
|
||||||
@ -62,9 +64,12 @@ class TransactionTemplate(models.Model):
|
|||||||
verbose_name=_('type'),
|
verbose_name=_('type'),
|
||||||
max_length=31,
|
max_length=31,
|
||||||
)
|
)
|
||||||
|
|
||||||
display = models.BooleanField(
|
display = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
|
verbose_name=_("display"),
|
||||||
)
|
)
|
||||||
|
|
||||||
description = models.CharField(
|
description = models.CharField(
|
||||||
verbose_name=_('description'),
|
verbose_name=_('description'),
|
||||||
max_length=255,
|
max_length=255,
|
||||||
@ -140,6 +145,7 @@ class Transaction(PolymorphicModel):
|
|||||||
max_length=255,
|
max_length=255,
|
||||||
default=None,
|
default=None,
|
||||||
null=True,
|
null=True,
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -118,7 +118,8 @@ class AliasTable(tables.Table):
|
|||||||
|
|
||||||
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
||||||
extra_context={"delete_trans": _('delete')},
|
extra_context={"delete_trans": _('delete')},
|
||||||
attrs={'td': {'class': 'col-sm-1'}})
|
attrs={'td': {'class': 'col-sm-1'}},
|
||||||
|
verbose_name=_("Delete"),)
|
||||||
|
|
||||||
|
|
||||||
class ButtonTable(tables.Table):
|
class ButtonTable(tables.Table):
|
||||||
@ -134,17 +135,20 @@ class ButtonTable(tables.Table):
|
|||||||
}
|
}
|
||||||
|
|
||||||
model = TransactionTemplate
|
model = TransactionTemplate
|
||||||
|
exclude = ('id',)
|
||||||
|
|
||||||
edit = tables.LinkColumn('note:template_update',
|
edit = tables.LinkColumn('note:template_update',
|
||||||
args=[A('pk')],
|
args=[A('pk')],
|
||||||
attrs={'td': {'class': 'col-sm-1'},
|
attrs={'td': {'class': 'col-sm-1'},
|
||||||
'a': {'class': 'btn btn-sm btn-primary'}},
|
'a': {'class': 'btn btn-sm btn-primary'}},
|
||||||
text=_('edit'),
|
text=_('edit'),
|
||||||
accessor='pk')
|
accessor='pk',
|
||||||
|
verbose_name=_("Edit"),)
|
||||||
|
|
||||||
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
|
||||||
extra_context={"delete_trans": _('delete')},
|
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):
|
def render_amount(self, value):
|
||||||
return pretty_money(value)
|
return pretty_money(value)
|
||||||
|
@ -9,6 +9,7 @@ from django_tables2 import SingleTableView
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from note_kfet.inputs import AmountInput
|
from note_kfet.inputs import AmountInput
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
from permission.views import ProtectQuerysetMixin
|
||||||
|
|
||||||
from .forms import TransactionTemplateForm
|
from .forms import TransactionTemplateForm
|
||||||
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
|
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
|
||||||
@ -16,7 +17,7 @@ from .models.transactions import SpecialTransaction
|
|||||||
from .tables import HistoryTable, ButtonTable
|
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`.
|
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`
|
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
|
model = Transaction
|
||||||
# Transaction history table
|
# Transaction history table
|
||||||
table_class = HistoryTable
|
table_class = HistoryTable
|
||||||
table_pagination = {"per_page": 50}
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self, **kwargs):
|
||||||
return Transaction.objects.filter(PermissionBackend.filter_queryset(
|
return super().get_queryset(**kwargs).order_by("-id").all()[:50]
|
||||||
self.request.user, Transaction, "view")
|
|
||||||
).order_by("-id").all()[:50]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
@ -42,12 +40,14 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
|
|||||||
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
|
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
|
||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).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
|
return context
|
||||||
|
|
||||||
|
|
||||||
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
|
class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create TransactionTemplate
|
Create TransactionTemplate
|
||||||
"""
|
"""
|
||||||
@ -56,7 +56,7 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
|
|||||||
success_url = reverse_lazy('note:template_list')
|
success_url = reverse_lazy('note:template_list')
|
||||||
|
|
||||||
|
|
||||||
class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
|
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
List TransactionsTemplates
|
List TransactionsTemplates
|
||||||
"""
|
"""
|
||||||
@ -64,7 +64,7 @@ class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
|
|||||||
table_class = ButtonTable
|
table_class = ButtonTable
|
||||||
|
|
||||||
|
|
||||||
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
|
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
"""
|
"""
|
||||||
model = TransactionTemplate
|
model = TransactionTemplate
|
||||||
@ -72,21 +72,19 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
success_url = reverse_lazy('note:template_list')
|
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.
|
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)
|
(Most of the magic happens in the dark world of Javascript see consos.js)
|
||||||
"""
|
"""
|
||||||
|
model = Transaction
|
||||||
template_name = "note/conso_form.html"
|
template_name = "note/conso_form.html"
|
||||||
|
|
||||||
# Transaction history table
|
# Transaction history table
|
||||||
table_class = HistoryTable
|
table_class = HistoryTable
|
||||||
table_pagination = {"per_page": 50}
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self, **kwargs):
|
||||||
return Transaction.objects.filter(
|
return super().get_queryset(**kwargs).order_by("-id").all()[:50]
|
||||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
|
||||||
).order_by("-id").all()[:50]
|
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
from ..models import Permission
|
from ..models import Permission, RolePermissions
|
||||||
|
|
||||||
|
|
||||||
class PermissionSerializer(serializers.ModelSerializer):
|
class PermissionSerializer(serializers.ModelSerializer):
|
||||||
@ -15,3 +15,14 @@ class PermissionSerializer(serializers.ModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Permission
|
model = Permission
|
||||||
fields = '__all__'
|
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
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from .views import PermissionViewSet
|
from .views import PermissionViewSet, RolePermissionsViewSet
|
||||||
|
|
||||||
|
|
||||||
def register_permission_urls(router, path):
|
def register_permission_urls(router, path):
|
||||||
"""
|
"""
|
||||||
Configure router for permission REST API.
|
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 django_filters.rest_framework import DjangoFilterBackend
|
||||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||||
|
|
||||||
from .serializers import PermissionSerializer
|
from .serializers import PermissionSerializer, RolePermissionsSerializer
|
||||||
from ..models import Permission
|
from ..models import Permission, RolePermissions
|
||||||
|
|
||||||
|
|
||||||
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||||
"""
|
"""
|
||||||
REST API View set.
|
REST API View set.
|
||||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer,
|
||||||
then render it on /api/logs/
|
then render it on /api/permission/permission/
|
||||||
"""
|
"""
|
||||||
queryset = Permission.objects.all()
|
queryset = Permission.objects.all()
|
||||||
serializer_class = PermissionSerializer
|
serializer_class = PermissionSerializer
|
||||||
filter_backends = [DjangoFilterBackend]
|
filter_backends = [DjangoFilterBackend]
|
||||||
filterset_fields = ['model', 'type', ]
|
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
|
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from django.contrib.auth.models import User, AnonymousUser
|
from django.contrib.auth.models import User, AnonymousUser
|
||||||
from django.contrib.contenttypes.models import ContentType
|
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 note_kfet.middlewares import get_current_session
|
||||||
from member.models import Membership, Club
|
from member.models import Membership, Club
|
||||||
|
|
||||||
|
from .decorators import memoize
|
||||||
from .models import Permission
|
from .models import Permission
|
||||||
|
|
||||||
|
|
||||||
@ -20,6 +23,28 @@ class PermissionBackend(ModelBackend):
|
|||||||
supports_anonymous_user = False
|
supports_anonymous_user = False
|
||||||
supports_inactive_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
|
@staticmethod
|
||||||
def permissions(user, model, type):
|
def permissions(user, model, type):
|
||||||
"""
|
"""
|
||||||
@ -29,16 +54,16 @@ class PermissionBackend(ModelBackend):
|
|||||||
:param type: The type of the permissions: view, change, add or delete
|
:param type: The type of the permissions: view, change, add or delete
|
||||||
:return: A generator of the requested permissions
|
:return: A generator of the requested permissions
|
||||||
"""
|
"""
|
||||||
for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
|
clubs = {}
|
||||||
.filter(
|
|
||||||
rolepermissions__role__membership__user=user,
|
for permission in PermissionBackend.get_raw_permissions(user, type):
|
||||||
model__app_label=model.app_label, # For polymorphic models, we don't filter on model type
|
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club:
|
||||||
type=type,
|
|
||||||
).all():
|
|
||||||
if not isinstance(model, permission.model.__class__):
|
|
||||||
continue
|
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(
|
permission = permission.about(
|
||||||
user=user,
|
user=user,
|
||||||
club=club,
|
club=club,
|
||||||
@ -52,10 +77,10 @@ class PermissionBackend(ModelBackend):
|
|||||||
F=F,
|
F=F,
|
||||||
Q=Q
|
Q=Q
|
||||||
)
|
)
|
||||||
if permission.mask.rank <= get_current_session().get("permission_mask", 0):
|
|
||||||
yield permission
|
yield permission
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@memoize
|
||||||
def filter_queryset(user, model, t, field=None):
|
def filter_queryset(user, model, t, field=None):
|
||||||
"""
|
"""
|
||||||
Filter a queryset by considering the permissions of a given user.
|
Filter a queryset by considering the permissions of a given user.
|
||||||
@ -89,10 +114,23 @@ class PermissionBackend(ModelBackend):
|
|||||||
query = query | perm.query
|
query = query | perm.query
|
||||||
return 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):
|
if user_obj is None or isinstance(user_obj, AnonymousUser):
|
||||||
return False
|
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:
|
if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -104,10 +142,13 @@ class PermissionBackend(ModelBackend):
|
|||||||
perm_field = perm[2] if len(perm) == 3 else None
|
perm_field = perm[2] if len(perm) == 3 else None
|
||||||
ct = ContentType.objects.get_for_model(obj)
|
ct = ContentType.objects.get_for_model(obj)
|
||||||
if any(permission.applies(obj, perm_type, perm_field)
|
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 True
|
||||||
return False
|
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):
|
def has_module_perms(self, user_obj, app_label):
|
||||||
return False
|
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",
|
"note",
|
||||||
"transaction"
|
"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",
|
"type": "add",
|
||||||
"mask": 2,
|
"mask": 2,
|
||||||
"field": "",
|
"field": "",
|
||||||
@ -783,6 +783,66 @@
|
|||||||
"description": "Validate invitation transactions"
|
"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",
|
"model": "permission.rolepermissions",
|
||||||
"pk": 1,
|
"pk": 1,
|
||||||
@ -795,7 +855,8 @@
|
|||||||
8,
|
8,
|
||||||
9,
|
9,
|
||||||
10,
|
10,
|
||||||
11
|
11,
|
||||||
|
48
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -880,5 +941,75 @@
|
|||||||
46
|
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:
|
if permission_type == self.type:
|
||||||
self.update_query()
|
self.update_query()
|
||||||
|
|
||||||
# Don't increase indexes
|
# Don't increase indexes, if the primary key is an AutoField
|
||||||
|
if not hasattr(obj, "pk") or not obj.pk:
|
||||||
obj.pk = 0
|
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
|
# Force insertion, no data verification, no trigger
|
||||||
|
obj._force_save = True
|
||||||
Model.save(obj, force_insert=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
|
# Delete testing object
|
||||||
|
obj._force_delete = True
|
||||||
Model.delete(obj)
|
Model.delete(obj)
|
||||||
|
|
||||||
|
# If the primary key was specified, we restore it
|
||||||
|
obj.pk = oldpk
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
if permission_type == self.type:
|
if permission_type == self.type:
|
||||||
if self.field and field_name != self.field:
|
if self.field and field_name != self.field:
|
||||||
return False
|
return False
|
||||||
self.update_query()
|
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:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
|
|||||||
|
|
||||||
perms = self.get_required_object_permissions(request.method, model_cls)
|
perms = self.get_required_object_permissions(request.method, model_cls)
|
||||||
# if not user.has_perms(perms, obj):
|
# 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
|
# If the user does not have permissions we need to determine if
|
||||||
# they have read permissions to see 403, or not, and simply see
|
# they have read permissions to see 403, or not, and simply see
|
||||||
# a 404 response.
|
# a 404 response.
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
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 note_kfet.middlewares import get_current_authenticated_user
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
@ -29,6 +27,9 @@ def pre_save_object(sender, instance, **kwargs):
|
|||||||
if instance._meta.label_lower in EXCLUDED:
|
if instance._meta.label_lower in EXCLUDED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if hasattr(instance, "_force_save"):
|
||||||
|
return
|
||||||
|
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
if user is None:
|
if user is None:
|
||||||
# Action performed on shell is always granted
|
# 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
|
# We check if the user can change the model
|
||||||
|
|
||||||
# If the user has all right on a model, then OK
|
# 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
|
return
|
||||||
|
|
||||||
# In the other case, we check if he/she has the right to change one field
|
# 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 the field wasn't modified, no need to check the permissions
|
||||||
if old_value == new_value:
|
if old_value == new_value:
|
||||||
continue
|
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
|
raise PermissionDenied
|
||||||
else:
|
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
|
# We check if the user has right to add the object
|
||||||
has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance)
|
has_perm = PermissionBackend.check_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)
|
|
||||||
|
|
||||||
if not has_perm:
|
if not has_perm:
|
||||||
raise PermissionDenied
|
raise PermissionDenied
|
||||||
|
|
||||||
|
|
||||||
def pre_delete_object(sender, instance, **kwargs):
|
def pre_delete_object(instance, **kwargs):
|
||||||
"""
|
"""
|
||||||
Before a model get deleted, we check the permissions
|
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:
|
if instance._meta.label_lower in EXCLUDED:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if hasattr(instance, "_force_delete"):
|
||||||
|
return
|
||||||
|
|
||||||
user = get_current_authenticated_user()
|
user = get_current_authenticated_user()
|
||||||
if user is None:
|
if user is None:
|
||||||
# Action performed on shell is always granted
|
# Action performed on shell is always granted
|
||||||
@ -101,5 +87,5 @@ def pre_delete_object(sender, instance, **kwargs):
|
|||||||
model_name = model_name_full[1]
|
model_name = model_name_full[1]
|
||||||
|
|
||||||
# We check if the user has rights to delete the object
|
# 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
|
raise PermissionDenied
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django import template
|
from django import template
|
||||||
|
from note.models import Transaction
|
||||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
@ -19,13 +20,8 @@ def not_empty_model_list(model_name):
|
|||||||
return False
|
return False
|
||||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
||||||
return True
|
return True
|
||||||
if session.get("not_empty_model_list_" + model_name, None):
|
qs = model_list(model_name)
|
||||||
return session.get("not_empty_model_list_" + model_name, None) == 1
|
return qs.exists()
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
@stringfilter
|
@stringfilter
|
||||||
@ -39,20 +35,54 @@ def not_empty_model_change_list(model_name):
|
|||||||
return False
|
return False
|
||||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
||||||
return True
|
return True
|
||||||
if session.get("not_empty_model_change_list_" + model_name, None):
|
qs = model_list(model_name, "change")
|
||||||
return session.get("not_empty_model_change_list_" + model_name, None) == 1
|
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(".")
|
spl = model_name.split(".")
|
||||||
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
||||||
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change"))
|
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all()
|
||||||
session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2
|
return qs
|
||||||
return session.get("not_empty_model_change_list_" + model_name) == 1
|
|
||||||
|
|
||||||
|
|
||||||
def has_perm(perm, obj):
|
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 = template.Library()
|
||||||
register.filter('not_empty_model_list', not_empty_model_list)
|
register.filter('not_empty_model_list', not_empty_model_list)
|
||||||
register.filter('not_empty_model_change_list', not_empty_model_change_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)
|
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 import forms
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.inputs import DatePickerInput, AmountInput
|
from note_kfet.inputs import DatePickerInput, AmountInput
|
||||||
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||||
|
|
||||||
@ -131,7 +132,8 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
|
|||||||
# Add submit button
|
# Add submit button
|
||||||
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
|
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):
|
def clean_last_name(self):
|
||||||
"""
|
"""
|
||||||
|
@ -19,13 +19,15 @@ from django.views.generic.base import View, TemplateView
|
|||||||
from django_tables2 import SingleTableView
|
from django_tables2 import SingleTableView
|
||||||
from note.models import SpecialTransaction, NoteSpecial
|
from note.models import SpecialTransaction, NoteSpecial
|
||||||
from note_kfet.settings.base import BASE_DIR
|
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 .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
|
||||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||||
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
|
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
|
||||||
|
|
||||||
|
|
||||||
class InvoiceCreateView(LoginRequiredMixin, CreateView):
|
class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create Invoice
|
Create Invoice
|
||||||
"""
|
"""
|
||||||
@ -67,7 +69,7 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
|
|||||||
return reverse_lazy('treasury:invoice_list')
|
return reverse_lazy('treasury:invoice_list')
|
||||||
|
|
||||||
|
|
||||||
class InvoiceListView(LoginRequiredMixin, SingleTableView):
|
class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||||
"""
|
"""
|
||||||
List existing Invoices
|
List existing Invoices
|
||||||
"""
|
"""
|
||||||
@ -75,7 +77,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
|
|||||||
table_class = InvoiceTable
|
table_class = InvoiceTable
|
||||||
|
|
||||||
|
|
||||||
class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
|
class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Create Invoice
|
Create Invoice
|
||||||
"""
|
"""
|
||||||
@ -130,7 +132,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
pk = kwargs["pk"]
|
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()
|
products = Product.objects.filter(invoice=invoice).all()
|
||||||
|
|
||||||
# Informations of the BDE. Should be updated when the school will move.
|
# Informations of the BDE. Should be updated when the school will move.
|
||||||
@ -188,7 +190,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
class RemittanceCreateView(LoginRequiredMixin, CreateView):
|
class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||||
"""
|
"""
|
||||||
Create Remittance
|
Create Remittance
|
||||||
"""
|
"""
|
||||||
@ -201,7 +203,9 @@ class RemittanceCreateView(LoginRequiredMixin, CreateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**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())
|
ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
@ -216,22 +220,28 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all())
|
ctx["opened_remittances"] = RemittanceTable(
|
||||||
ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all())
|
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(
|
ctx["special_transactions_no_remittance"] = SpecialTransactionTable(
|
||||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
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', ))
|
exclude=('remittance_remove', ))
|
||||||
ctx["special_transactions_with_remittance"] = SpecialTransactionTable(
|
ctx["special_transactions_with_remittance"] = SpecialTransactionTable(
|
||||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
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', ))
|
exclude=('remittance_add', ))
|
||||||
|
|
||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
|
class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Update Remittance
|
Update Remittance
|
||||||
"""
|
"""
|
||||||
@ -244,8 +254,10 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
ctx = super().get_context_data(**kwargs)
|
ctx = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
ctx["table"] = RemittanceTable(data=Remittance.objects.all())
|
ctx["table"] = RemittanceTable(data=Remittance.objects.filter(
|
||||||
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all()
|
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(
|
ctx["special_transactions"] = SpecialTransactionTable(
|
||||||
data=data,
|
data=data,
|
||||||
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
|
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
|
||||||
@ -253,7 +265,7 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return ctx
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
|
class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||||
"""
|
"""
|
||||||
Attach a special transaction to a remittance
|
Attach a special transaction to a remittance
|
||||||
"""
|
"""
|
||||||
|
@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/activity/models.py:23 apps/activity/models.py:48
|
#: 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/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
|
#: templates/member/club_info.html:13 templates/member/profile_info.html:14
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -68,7 +68,7 @@ msgid "activity types"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/activity/models.py:53 apps/note/models/transactions.py:69
|
#: 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"
|
msgid "description"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ msgstr ""
|
|||||||
msgid "type"
|
msgid "type"
|
||||||
msgstr ""
|
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
|
#: apps/note/models/notes.py:117
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -169,11 +169,11 @@ msgstr ""
|
|||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/activity/tables.py:77 apps/treasury/forms.py:120
|
#: apps/activity/tables.py:77 apps/treasury/forms.py:121
|
||||||
msgid "Last name"
|
msgid "Last name"
|
||||||
msgstr ""
|
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
|
#: templates/note/transaction_form.html:92
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -186,11 +186,11 @@ msgstr ""
|
|||||||
msgid "Balance"
|
msgid "Balance"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/activity/views.py:44 templates/base.html:94
|
#: apps/activity/views.py:45 templates/base.html:94
|
||||||
msgid "Activities"
|
msgid "Activities"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/activity/views.py:149
|
#: apps/activity/views.py:153
|
||||||
msgid "Entry for activity \"{}\""
|
msgid "Entry for activity \"{}\""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -251,121 +251,165 @@ msgstr ""
|
|||||||
msgid "member"
|
msgid "member"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:26
|
#: apps/member/models.py:28
|
||||||
msgid "phone number"
|
msgid "phone number"
|
||||||
msgstr ""
|
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"
|
msgid "section"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:33
|
#: apps/member/models.py:35
|
||||||
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
|
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
|
||||||
msgstr ""
|
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"
|
msgid "address"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:45
|
#: apps/member/models.py:47
|
||||||
msgid "paid"
|
msgid "paid"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:50 apps/member/models.py:51
|
#: apps/member/models.py:52 apps/member/models.py:53
|
||||||
msgid "user profile"
|
msgid "user profile"
|
||||||
msgstr ""
|
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"
|
msgid "email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:76
|
#: apps/member/models.py:78
|
||||||
msgid "parent club"
|
msgid "parent club"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:81 templates/member/club_info.html:30
|
#: apps/member/models.py:87
|
||||||
msgid "membership fee"
|
msgid "require memberships"
|
||||||
msgstr ""
|
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"
|
msgid "membership duration"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:86
|
#: apps/member/models.py:105
|
||||||
msgid "The longest time a membership can last (NULL = infinite)."
|
msgid "The longest time (in days) a membership can last (NULL = infinite)."
|
||||||
msgstr ""
|
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"
|
msgid "membership start"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:92
|
#: apps/member/models.py:113
|
||||||
msgid "How long after January 1st the members can renew their membership."
|
msgid "How long after January 1st the members can renew their membership."
|
||||||
msgstr ""
|
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"
|
msgid "membership end"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:98
|
#: apps/member/models.py:121
|
||||||
msgid ""
|
msgid ""
|
||||||
"How long the membership can last after January 1st of the next year after "
|
"How long the membership can last after January 1st of the next year after "
|
||||||
"members can renew their membership."
|
"members can renew their membership."
|
||||||
msgstr ""
|
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"
|
msgid "club"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:105
|
#: apps/member/models.py:155
|
||||||
msgid "clubs"
|
msgid "clubs"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:128 apps/permission/models.py:275
|
#: apps/member/models.py:175 apps/permission/models.py:288
|
||||||
msgid "role"
|
msgid "role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:129
|
#: apps/member/models.py:176 apps/member/models.py:201
|
||||||
msgid "roles"
|
msgid "roles"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:153
|
#: apps/member/models.py:205
|
||||||
msgid "membership starts on"
|
msgid "membership starts on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:156
|
#: apps/member/models.py:209
|
||||||
msgid "membership ends on"
|
msgid "membership ends on"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:160
|
#: apps/member/models.py:214
|
||||||
msgid "fee"
|
msgid "fee"
|
||||||
msgstr ""
|
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"
|
msgid "User is not a member of the parent club"
|
||||||
msgstr ""
|
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"
|
msgid "membership"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/models.py:177
|
#: apps/member/models.py:275
|
||||||
msgid "memberships"
|
msgid "memberships"
|
||||||
msgstr ""
|
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"
|
msgid "Update Profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/member/views.py:89
|
#: apps/member/views.py:93
|
||||||
msgid "An alias with a similar name already exists."
|
msgid "An alias with a similar name already exists."
|
||||||
msgstr ""
|
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
|
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
|
||||||
msgid "source"
|
msgid "source"
|
||||||
msgstr ""
|
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
|
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107
|
||||||
msgid "destination"
|
msgid "destination"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -462,7 +506,7 @@ msgstr ""
|
|||||||
msgid "alias"
|
msgid "alias"
|
||||||
msgstr ""
|
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
|
#: templates/member/profile_info.html:36
|
||||||
msgid "aliases"
|
msgid "aliases"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@ -524,45 +568,45 @@ msgstr ""
|
|||||||
msgid "invalidity reason"
|
msgid "invalidity reason"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:146
|
#: apps/note/models/transactions.py:147
|
||||||
msgid "transaction"
|
msgid "transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:147
|
#: apps/note/models/transactions.py:148
|
||||||
msgid "transactions"
|
msgid "transactions"
|
||||||
msgstr ""
|
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:19
|
||||||
#: templates/note/transaction_form.html:140
|
#: templates/note/transaction_form.html:140
|
||||||
msgid "Transfer"
|
msgid "Transfer"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:221
|
#: apps/note/models/transactions.py:222
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:236
|
#: apps/note/models/transactions.py:237
|
||||||
msgid "first_name"
|
msgid "first_name"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:241
|
#: apps/note/models/transactions.py:242
|
||||||
msgid "bank"
|
msgid "bank"
|
||||||
msgstr ""
|
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"
|
msgid "Credit"
|
||||||
msgstr ""
|
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"
|
msgid "Debit"
|
||||||
msgstr ""
|
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"
|
msgid "membership transaction"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:264
|
#: apps/note/models/transactions.py:265
|
||||||
msgid "membership transactions"
|
msgid "membership transactions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -578,29 +622,29 @@ msgstr ""
|
|||||||
msgid "No reason specified"
|
msgid "No reason specified"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/views.py:41
|
#: apps/note/views.py:39
|
||||||
msgid "Transfer money"
|
msgid "Transfer money"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/note/views.py:102 templates/base.html:79
|
#: apps/note/views.py:100 templates/base.html:79
|
||||||
msgid "Consumptions"
|
msgid "Consumptions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/permission/models.py:69 apps/permission/models.py:262
|
#: apps/permission/models.py:82 apps/permission/models.py:275
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Can {type} {model}.{field} in {query}"
|
msgid "Can {type} {model}.{field} in {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/permission/models.py:71 apps/permission/models.py:264
|
#: apps/permission/models.py:84 apps/permission/models.py:277
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Can {type} {model} in {query}"
|
msgid "Can {type} {model} in {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/permission/models.py:84
|
#: apps/permission/models.py:97
|
||||||
msgid "rank"
|
msgid "rank"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/permission/models.py:147
|
#: apps/permission/models.py:160
|
||||||
msgid "Specifying field applies only to view and change permission types."
|
msgid "Specifying field applies only to view and change permission types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -608,31 +652,32 @@ msgstr ""
|
|||||||
msgid "Treasury"
|
msgid "Treasury"
|
||||||
msgstr ""
|
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_form.html:9
|
||||||
#: templates/activity/activity_invite.html:8
|
#: templates/activity/activity_invite.html:8
|
||||||
#: templates/django_filters/rest_framework/form.html:5
|
#: 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"
|
msgid "Submit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/treasury/forms.py:86
|
#: apps/treasury/forms.py:87
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/treasury/forms.py:95
|
#: apps/treasury/forms.py:96
|
||||||
msgid "Remittance is already closed."
|
msgid "Remittance is already closed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/treasury/forms.py:100
|
#: apps/treasury/forms.py:101
|
||||||
msgid "You can't change the type of the remittance."
|
msgid "You can't change the type of the remittance."
|
||||||
msgstr ""
|
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"
|
msgid "Bank"
|
||||||
msgstr ""
|
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/note/transaction_form.html:128
|
||||||
#: templates/treasury/remittance_form.html:18
|
#: templates/treasury/remittance_form.html:18
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
@ -879,19 +924,23 @@ msgstr ""
|
|||||||
msgid "Club Parent"
|
msgid "Club Parent"
|
||||||
msgstr ""
|
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"
|
msgid "Add member"
|
||||||
msgstr ""
|
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"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/member/club_info.html:43
|
#: templates/member/club_info.html:59 templates/member/profile_info.html:48
|
||||||
msgid "Add roles"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
#: templates/member/club_info.html:46 templates/member/profile_info.html:48
|
|
||||||
msgid "View Profile"
|
msgid "View Profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -900,7 +949,7 @@ msgid "search clubs"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/member/club_list.html:12
|
#: templates/member/club_list.html:12
|
||||||
msgid "Créer un club"
|
msgid "Create club"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: templates/member/club_list.html:19
|
#: templates/member/club_list.html:19
|
||||||
|
@ -3,7 +3,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \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"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -20,7 +20,8 @@ msgstr "activité"
|
|||||||
|
|
||||||
#: apps/activity/forms.py:45 apps/activity/models.py:217
|
#: apps/activity/forms.py:45 apps/activity/models.py:217
|
||||||
msgid "You can't invite someone once the activity is started."
|
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
|
#: apps/activity/forms.py:48 apps/activity/models.py:220
|
||||||
msgid "This activity is not validated yet."
|
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é."
|
msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
|
||||||
|
|
||||||
#: apps/activity/models.py:23 apps/activity/models.py:48
|
#: 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/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
|
#: templates/member/club_info.html:13 templates/member/profile_info.html:14
|
||||||
msgid "name"
|
msgid "name"
|
||||||
msgstr "nom"
|
msgstr "nom"
|
||||||
@ -63,7 +64,7 @@ msgid "activity types"
|
|||||||
msgstr "types d'activité"
|
msgstr "types d'activité"
|
||||||
|
|
||||||
#: apps/activity/models.py:53 apps/note/models/transactions.py:69
|
#: 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"
|
msgid "description"
|
||||||
msgstr "description"
|
msgstr "description"
|
||||||
|
|
||||||
@ -73,7 +74,7 @@ msgstr "description"
|
|||||||
msgid "type"
|
msgid "type"
|
||||||
msgstr "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
|
#: apps/note/models/notes.py:117
|
||||||
msgid "user"
|
msgid "user"
|
||||||
msgstr "utilisateur"
|
msgstr "utilisateur"
|
||||||
@ -164,11 +165,11 @@ msgstr "supprimer"
|
|||||||
msgid "Type"
|
msgid "Type"
|
||||||
msgstr "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"
|
msgid "Last name"
|
||||||
msgstr "Nom de famille"
|
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
|
#: templates/note/transaction_form.html:92
|
||||||
msgid "First name"
|
msgid "First name"
|
||||||
msgstr "Prénom"
|
msgstr "Prénom"
|
||||||
@ -181,11 +182,11 @@ msgstr "Note"
|
|||||||
msgid "Balance"
|
msgid "Balance"
|
||||||
msgstr "Solde du compte"
|
msgstr "Solde du compte"
|
||||||
|
|
||||||
#: apps/activity/views.py:44 templates/base.html:94
|
#: apps/activity/views.py:45 templates/base.html:94
|
||||||
msgid "Activities"
|
msgid "Activities"
|
||||||
msgstr "Activités"
|
msgstr "Activités"
|
||||||
|
|
||||||
#: apps/activity/views.py:149
|
#: apps/activity/views.py:153
|
||||||
msgid "Entry for activity \"{}\""
|
msgid "Entry for activity \"{}\""
|
||||||
msgstr "Entrées pour l'activité « {} »"
|
msgstr "Entrées pour l'activité « {} »"
|
||||||
|
|
||||||
@ -246,65 +247,77 @@ msgstr "Les logs ne peuvent pas être détruits."
|
|||||||
msgid "member"
|
msgid "member"
|
||||||
msgstr "adhérent"
|
msgstr "adhérent"
|
||||||
|
|
||||||
#: apps/member/models.py:26
|
#: apps/member/models.py:28
|
||||||
msgid "phone number"
|
msgid "phone number"
|
||||||
msgstr "numéro de téléphone"
|
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"
|
msgid "section"
|
||||||
msgstr "section"
|
msgstr "section"
|
||||||
|
|
||||||
#: apps/member/models.py:33
|
#: apps/member/models.py:35
|
||||||
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
|
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
|
||||||
msgstr "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"
|
msgid "address"
|
||||||
msgstr "adresse"
|
msgstr "adresse"
|
||||||
|
|
||||||
#: apps/member/models.py:45
|
#: apps/member/models.py:47
|
||||||
msgid "paid"
|
msgid "paid"
|
||||||
msgstr "payé"
|
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"
|
msgid "user profile"
|
||||||
msgstr "profil utilisateur"
|
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"
|
msgid "email"
|
||||||
msgstr "courriel"
|
msgstr "courriel"
|
||||||
|
|
||||||
#: apps/member/models.py:76
|
#: apps/member/models.py:78
|
||||||
msgid "parent club"
|
msgid "parent club"
|
||||||
msgstr "club parent"
|
msgstr "club parent"
|
||||||
|
|
||||||
#: apps/member/models.py:81 templates/member/club_info.html:30
|
#: apps/member/models.py:87
|
||||||
msgid "membership fee"
|
msgid "require memberships"
|
||||||
msgstr "cotisation pour adhérer"
|
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"
|
msgid "membership duration"
|
||||||
msgstr "durée de l'adhésion"
|
msgstr "durée de l'adhésion"
|
||||||
|
|
||||||
#: apps/member/models.py:86
|
#: apps/member/models.py:105
|
||||||
msgid "The longest time a membership can last (NULL = infinite)."
|
msgid "The longest time (in days) a membership can last (NULL = infinite)."
|
||||||
msgstr "La durée maximale d'une adhésion (NULL = infinie)."
|
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"
|
msgid "membership start"
|
||||||
msgstr "début de l'adhésion"
|
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."
|
msgid "How long after January 1st the members can renew their membership."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
|
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
|
||||||
"adhésion."
|
"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"
|
msgid "membership end"
|
||||||
msgstr "fin de l'adhésion"
|
msgstr "fin de l'adhésion"
|
||||||
|
|
||||||
#: apps/member/models.py:98
|
#: apps/member/models.py:121
|
||||||
msgid ""
|
msgid ""
|
||||||
"How long the membership can last after January 1st of the next year after "
|
"How long the membership can last after January 1st of the next year after "
|
||||||
"members can renew their membership."
|
"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 "
|
"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."
|
"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"
|
msgid "club"
|
||||||
msgstr "club"
|
msgstr "club"
|
||||||
|
|
||||||
#: apps/member/models.py:105
|
#: apps/member/models.py:155
|
||||||
msgid "clubs"
|
msgid "clubs"
|
||||||
msgstr "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"
|
msgid "role"
|
||||||
msgstr "rôle"
|
msgstr "rôle"
|
||||||
|
|
||||||
#: apps/member/models.py:129
|
#: apps/member/models.py:176 apps/member/models.py:201
|
||||||
msgid "roles"
|
msgid "roles"
|
||||||
msgstr "rôles"
|
msgstr "rôles"
|
||||||
|
|
||||||
#: apps/member/models.py:153
|
#: apps/member/models.py:205
|
||||||
msgid "membership starts on"
|
msgid "membership starts on"
|
||||||
msgstr "l'adhésion commence le"
|
msgstr "l'adhésion commence le"
|
||||||
|
|
||||||
#: apps/member/models.py:156
|
#: apps/member/models.py:209
|
||||||
msgid "membership ends on"
|
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"
|
msgid "fee"
|
||||||
msgstr "cotisation"
|
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"
|
msgid "User is not a member of the parent club"
|
||||||
msgstr "L'utilisateur n'est pas membre du club parent"
|
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"
|
msgid "membership"
|
||||||
msgstr "adhésion"
|
msgstr "adhésion"
|
||||||
|
|
||||||
#: apps/member/models.py:177
|
#: apps/member/models.py:275
|
||||||
msgid "memberships"
|
msgid "memberships"
|
||||||
msgstr "adhésions"
|
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"
|
msgid "Update Profile"
|
||||||
msgstr "Modifier le profil"
|
msgstr "Modifier le profil"
|
||||||
|
|
||||||
#: apps/member/views.py:89
|
#: apps/member/views.py:93
|
||||||
msgid "An alias with a similar name already exists."
|
msgid "An alias with a similar name already exists."
|
||||||
msgstr "Un alias avec un nom similaire existe déjà."
|
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
|
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
|
||||||
msgid "source"
|
msgid "source"
|
||||||
msgstr "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
|
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107
|
||||||
msgid "destination"
|
msgid "destination"
|
||||||
msgstr "destination"
|
msgstr "destination"
|
||||||
@ -462,7 +507,7 @@ msgstr "Alias invalide"
|
|||||||
msgid "alias"
|
msgid "alias"
|
||||||
msgstr "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
|
#: templates/member/profile_info.html:36
|
||||||
msgid "aliases"
|
msgid "aliases"
|
||||||
msgstr "alias"
|
msgstr "alias"
|
||||||
@ -524,45 +569,45 @@ msgstr "raison"
|
|||||||
msgid "invalidity reason"
|
msgid "invalidity reason"
|
||||||
msgstr "Motif d'invalidité"
|
msgstr "Motif d'invalidité"
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:146
|
#: apps/note/models/transactions.py:147
|
||||||
msgid "transaction"
|
msgid "transaction"
|
||||||
msgstr "transaction"
|
msgstr "transaction"
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:147
|
#: apps/note/models/transactions.py:148
|
||||||
msgid "transactions"
|
msgid "transactions"
|
||||||
msgstr "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:19
|
||||||
#: templates/note/transaction_form.html:140
|
#: templates/note/transaction_form.html:140
|
||||||
msgid "Transfer"
|
msgid "Transfer"
|
||||||
msgstr "Virement"
|
msgstr "Virement"
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:221
|
#: apps/note/models/transactions.py:222
|
||||||
msgid "Template"
|
msgid "Template"
|
||||||
msgstr "Bouton"
|
msgstr "Bouton"
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:236
|
#: apps/note/models/transactions.py:237
|
||||||
msgid "first_name"
|
msgid "first_name"
|
||||||
msgstr "prénom"
|
msgstr "prénom"
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:241
|
#: apps/note/models/transactions.py:242
|
||||||
msgid "bank"
|
msgid "bank"
|
||||||
msgstr "banque"
|
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"
|
msgid "Credit"
|
||||||
msgstr "Crédit"
|
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"
|
msgid "Debit"
|
||||||
msgstr "Débit"
|
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"
|
msgid "membership transaction"
|
||||||
msgstr "transaction d'adhésion"
|
msgstr "transaction d'adhésion"
|
||||||
|
|
||||||
#: apps/note/models/transactions.py:264
|
#: apps/note/models/transactions.py:265
|
||||||
msgid "membership transactions"
|
msgid "membership transactions"
|
||||||
msgstr "transactions d'adhésion"
|
msgstr "transactions d'adhésion"
|
||||||
|
|
||||||
@ -578,29 +623,29 @@ msgstr "Cliquez pour valider"
|
|||||||
msgid "No reason specified"
|
msgid "No reason specified"
|
||||||
msgstr "Pas de motif spécifié"
|
msgstr "Pas de motif spécifié"
|
||||||
|
|
||||||
#: apps/note/views.py:41
|
#: apps/note/views.py:39
|
||||||
msgid "Transfer money"
|
msgid "Transfer money"
|
||||||
msgstr "Transférer de l'argent"
|
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"
|
msgid "Consumptions"
|
||||||
msgstr "Consommations"
|
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
|
#, python-brace-format
|
||||||
msgid "Can {type} {model}.{field} in {query}"
|
msgid "Can {type} {model}.{field} in {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/permission/models.py:71 apps/permission/models.py:264
|
#: apps/permission/models.py:84 apps/permission/models.py:277
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid "Can {type} {model} in {query}"
|
msgid "Can {type} {model} in {query}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: apps/permission/models.py:84
|
#: apps/permission/models.py:97
|
||||||
msgid "rank"
|
msgid "rank"
|
||||||
msgstr "Rang"
|
msgstr "Rang"
|
||||||
|
|
||||||
#: apps/permission/models.py:147
|
#: apps/permission/models.py:160
|
||||||
msgid "Specifying field applies only to view and change permission types."
|
msgid "Specifying field applies only to view and change permission types."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -608,31 +653,32 @@ msgstr ""
|
|||||||
msgid "Treasury"
|
msgid "Treasury"
|
||||||
msgstr "Trésorerie"
|
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_form.html:9
|
||||||
#: templates/activity/activity_invite.html:8
|
#: templates/activity/activity_invite.html:8
|
||||||
#: templates/django_filters/rest_framework/form.html:5
|
#: 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"
|
msgid "Submit"
|
||||||
msgstr "Envoyer"
|
msgstr "Envoyer"
|
||||||
|
|
||||||
#: apps/treasury/forms.py:86
|
#: apps/treasury/forms.py:87
|
||||||
msgid "Close"
|
msgid "Close"
|
||||||
msgstr "Fermer"
|
msgstr "Fermer"
|
||||||
|
|
||||||
#: apps/treasury/forms.py:95
|
#: apps/treasury/forms.py:96
|
||||||
msgid "Remittance is already closed."
|
msgid "Remittance is already closed."
|
||||||
msgstr "La remise est déjà fermée."
|
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."
|
msgid "You can't change the type of the remittance."
|
||||||
msgstr "Vous ne pouvez pas changer le type de la remise."
|
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"
|
msgid "Bank"
|
||||||
msgstr "Banque"
|
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/note/transaction_form.html:128
|
||||||
#: templates/treasury/remittance_form.html:18
|
#: templates/treasury/remittance_form.html:18
|
||||||
msgid "Amount"
|
msgid "Amount"
|
||||||
@ -881,19 +927,23 @@ msgstr "Ajouter un alias"
|
|||||||
msgid "Club Parent"
|
msgid "Club Parent"
|
||||||
msgstr "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"
|
msgid "Add member"
|
||||||
msgstr "Ajouter un membre"
|
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"
|
msgid "Edit"
|
||||||
msgstr "Éditer"
|
msgstr "Éditer"
|
||||||
|
|
||||||
#: templates/member/club_info.html:43
|
#: templates/member/club_info.html:59 templates/member/profile_info.html:48
|
||||||
msgid "Add roles"
|
|
||||||
msgstr "Ajouter des rôles"
|
|
||||||
|
|
||||||
#: templates/member/club_info.html:46 templates/member/profile_info.html:48
|
|
||||||
msgid "View Profile"
|
msgid "View Profile"
|
||||||
msgstr "Voir le profil"
|
msgstr "Voir le profil"
|
||||||
|
|
||||||
@ -902,8 +952,8 @@ msgid "search clubs"
|
|||||||
msgstr "Chercher un club"
|
msgstr "Chercher un club"
|
||||||
|
|
||||||
#: templates/member/club_list.html:12
|
#: templates/member/club_list.html:12
|
||||||
msgid "Créer un club"
|
msgid "Create club"
|
||||||
msgstr ""
|
msgstr "Créer un club"
|
||||||
|
|
||||||
#: templates/member/club_list.html:19
|
#: templates/member/club_list.html:19
|
||||||
msgid "club listing "
|
msgid "club listing "
|
||||||
|
@ -70,7 +70,7 @@ function refreshBalance() {
|
|||||||
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
|
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
|
||||||
*/
|
*/
|
||||||
function getMatchedNotes(pattern, fun) {
|
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>
|
<dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ activity.date_end }}</dd>
|
<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>
|
<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>
|
<dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -53,17 +53,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-footer text-center">
|
<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>
|
<a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
|
||||||
{% endif %}
|
{% 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>
|
<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 %}
|
{% 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>
|
<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 %}
|
{% 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>
|
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}"> {% trans "edit"|capfirst %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if activity.activity_type.can_invite and not activity_started %}
|
{% 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>
|
<a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% 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 %}
|
{% if "member.club"|not_empty_model_list %}
|
||||||
<li class="nav-item active">
|
<li class="nav-item active">
|
||||||
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>
|
<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" %}
|
{% extends "member/noteowner_detail.html" %}
|
||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
{% block profile_info %}
|
{% block profile_info %}
|
||||||
{% include "member/club_info.html" %}
|
{% include "member/club_info.html" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block profile_content %}
|
|
||||||
|
|
||||||
|
{% block profile_content %}
|
||||||
<form method="post" action="">
|
<form method="post" action="">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% crispy formset helper %}
|
{{ form|crispy }}
|
||||||
<div class="form-actions">
|
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
|
||||||
<input type="submit" name="submit" value="Add Members" class="btn btn-primary" id="submit-save">
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script src="{% static 'js/dynamic-formset.js' %}"></script>
|
|
||||||
<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>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -7,3 +7,12 @@
|
|||||||
{% block profile_content %}
|
{% block profile_content %}
|
||||||
{% include "member/club_tables.html" %}
|
{% include "member/club_tables.html" %}
|
||||||
{% endblock %}
|
{% 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>
|
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
|
||||||
</form>
|
</form>
|
||||||
{% endblock %}
|
{% 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 bg-light shadow">
|
||||||
<div class="card-header text-center">
|
<div class="card-header text-center">
|
||||||
<h4> Club {{ club.name }} </h4>
|
<h4> Club {{ club.name }} </h4>
|
||||||
@ -18,6 +18,7 @@
|
|||||||
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
|
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if club.require_memberships %}
|
||||||
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ club.membership_start }}</dd>
|
<dd class="col-xl-6">{{ club.membership_start }}</dd>
|
||||||
|
|
||||||
@ -25,24 +26,36 @@
|
|||||||
<dd class="col-xl-6">{{ club.membership_end }}</dd>
|
<dd class="col-xl-6">{{ club.membership_end }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ club.membership_duration }}</dd>
|
<dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd>
|
||||||
|
|
||||||
|
{% if club.membership_fee_paid == club.membership_fee_unpaid %}
|
||||||
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
|
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
|
||||||
<dd class="col-xl-6">{{ club.membership_fee|pretty_money }}</dd>
|
<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>
|
<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>
|
<dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd>
|
||||||
|
|
||||||
<dt class="col-xl-3">{% trans 'email'|capfirst %}</dt>
|
<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>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer text-center">
|
<div class="card-footer text-center">
|
||||||
|
{% 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>
|
<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>
|
<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>
|
{% endif %}
|
||||||
{% url 'member:club_detail' club.pk as club_detail_url %}
|
{% url 'member:club_detail' club.pk as club_detail_url %}
|
||||||
{%if request.get_full_path != 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>
|
{% endif %} </div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
</h4>
|
</h4>
|
||||||
<input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/>
|
<input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/>
|
||||||
<hr>
|
<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>
|
</div>
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script>
|
<script>
|
||||||
function refreshhistory() {
|
function refreshHistory() {
|
||||||
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
|
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
|
||||||
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
|
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
|
||||||
}
|
}
|
||||||
|
@ -2,28 +2,44 @@
|
|||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
{% load crispy_forms_tags%}
|
{% load crispy_forms_tags%}
|
||||||
{% block content %}
|
{% 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">
|
<div id="user_table">
|
||||||
{% crispy filter.form filter.form.helper %}
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div id="replaceable-content" class="col-6">
|
|
||||||
{% render_table table %}
|
{% render_table table %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extrajavascript %}
|
{% block extrajavascript %}
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
let old_pattern = null;
|
||||||
|
let searchbar_obj = $("#searchbar");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
$(document).ready(function($) {
|
|
||||||
$(".table-row").click(function() {
|
$(".table-row").click(function() {
|
||||||
window.document.location = $(this).data("href");
|
window.document.location = $(this).data("href");
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
|
searchbar_obj.keyup(reloadTable);
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
$(".table-row").click(function() {
|
||||||
|
window.document.location = $(this).data("href");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Loading…
Reference in New Issue
Block a user