Merge branch 'new-note-type' into 'master'

Memberships

Closes #43 and #16

See merge request bde/nk20!71
This commit is contained in:
ynerant 2020-04-05 02:06:19 +02:00
commit 72e5df0cf5
41 changed files with 1208 additions and 483 deletions

View File

@ -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,

View File

@ -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 = []

View File

@ -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()

View File

@ -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'),
)

View File

@ -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"
} }
} }
] ]

View File

@ -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",
))

View File

@ -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')

View File

@ -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

View File

@ -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'),
] ]

View File

@ -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}))

View File

@ -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):
""" """

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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):
""" """

View File

@ -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__'

View File

@ -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)

View File

@ -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', ]

View File

@ -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

View 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

View File

@ -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
]
}
} }
] ]

View File

@ -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
obj.pk = 0 if not hasattr(obj, "pk") or not obj.pk:
obj.pk = 0
oldpk = None
else:
oldpk = obj.pk
# Ensure previous models are deleted
self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete()
# Force insertion, no data verification, no trigger # 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

View File

@ -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.

View File

@ -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

View File

@ -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
View 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"))

View File

@ -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):
""" """

View File

@ -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
""" """

View File

@ -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

View File

@ -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 "

View File

@ -299,4 +299,4 @@ class YearPickerInput(BasePickerInput):
def _link_to(self, linked_picker): def _link_to(self, linked_picker):
"""Customize the options when linked with other date-time input""" """Customize the options when linked with other date-time input"""
yformat = self.config['options']['format'].replace('-01-01', '-12-31') yformat = self.config['options']['format'].replace('-01-01', '-12-31')
self.config['options']['format'] = yformat self.config['options']['format'] = yformat

View File

@ -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);
} }
/** /**

View File

@ -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 %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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,31 +18,44 @@
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd> <dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
{% endif %} {% endif %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt> {% if club.require_memberships %}
<dd class="col-xl-6">{{ club.membership_start }}</dd> <dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd>
<dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt> <dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt>
<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>
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt> {% if club.membership_fee_paid == club.membership_fee_unpaid %}
<dd class="col-xl-6">{{ club.membership_fee|pretty_money }}</dd> <dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
{% else %}
<dt class="col-xl-6">{% trans 'membership fee (paid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
<dt class="col-xl-6">{% trans 'membership fee (unpaid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}</dd>
{% endif %}
{% endif %}
<dt class="col-xl-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt> <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">
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a> {% if can_add_members %}
<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 member" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a> {% endif %}
{% if ".change_"|has_perm:club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
{% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %} {% 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>

View File

@ -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">

View File

@ -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");
} }

View File

@ -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 %} {% render_table table %}
</div>
<div class="row">
<div id="replaceable-content" class="col-6">
{% 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");
$(document).ready(function($) { function reloadTable() {
$(".table-row").click(function() { let pattern = searchbar_obj.val();
window.document.location = $(this).data("href");
if (pattern === old_pattern || pattern === "")
return;
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
}
searchbar_obj.keyup(reloadTable);
function init() {
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
}
init();
}); });
});
</script> </script>
{% endblock %} {% endblock %}