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'),
)
note = models.ForeignKey(
'note.Note',
on_delete=models.PROTECT,
blank=True,
null=True,
related_name='+',
verbose_name=_('note'),
)
attendees_club = models.ForeignKey(
'member.Club',
on_delete=models.PROTECT,
@ -160,9 +151,7 @@ class Entry(models.Model):
if insert and self.guest:
GuestTransaction.objects.create(
source=self.note,
source_alias=self.note.user.username,
destination=self.note,
destination_alias=self.activity.organizer.name,
destination=self.activity.organizer.note,
quantity=1,
amount=self.activity.activity_type.guest_entry_fee,
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,

View File

@ -1,5 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime, timezone
from django.contrib.auth.mixins import LoginRequiredMixin
@ -11,13 +12,14 @@ from django.utils.translation import gettext_lazy as _
from django_tables2.views import SingleTableView
from note.models import NoteUser, Alias, NoteSpecial
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import ActivityForm, GuestForm
from .models import Activity, Guest, Entry
from .tables import ActivityTable, GuestTable, EntryTable
class ActivityCreateView(LoginRequiredMixin, CreateView):
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Activity
form_class = ActivityForm
@ -30,13 +32,12 @@ class ActivityCreateView(LoginRequiredMixin, CreateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.object.pk})
class ActivityListView(LoginRequiredMixin, SingleTableView):
class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
model = Activity
table_class = ActivityTable
def get_queryset(self):
return super().get_queryset()\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).reverse()
return super().get_queryset().reverse()
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
@ -50,7 +51,7 @@ class ActivityListView(LoginRequiredMixin, SingleTableView):
return ctx
class ActivityDetailView(LoginRequiredMixin, DetailView):
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Activity
context_object_name = "activity"
@ -66,7 +67,7 @@ class ActivityDetailView(LoginRequiredMixin, DetailView):
return ctx
class ActivityUpdateView(LoginRequiredMixin, UpdateView):
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Activity
form_class = ActivityForm
@ -74,18 +75,20 @@ class ActivityUpdateView(LoginRequiredMixin, UpdateView):
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
class ActivityInviteView(LoginRequiredMixin, CreateView):
class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Guest
form_class = GuestForm
template_name = "activity/activity_invite.html"
def get_form(self, form_class=None):
form = super().get_form(form_class)
form.activity = Activity.objects.get(pk=self.kwargs["pk"])
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"])
return form
def form_valid(self, form):
form.instance.activity = Activity.objects.get(pk=self.kwargs["pk"])
form.instance.activity = Activity.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
return super().form_valid(form)
def get_success_url(self, **kwargs):
@ -98,7 +101,8 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
activity = Activity.objects.get(pk=self.kwargs["pk"])
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
.get(pk=self.kwargs["pk"])
ctx["activity"] = activity
matched = []

View File

@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED:
return
if hasattr(instance, "_no_log"):
return
# noinspection PyProtectedMember
previous = instance._previous
@ -106,6 +109,9 @@ def delete_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED:
return
if hasattr(instance, "_no_log"):
return
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
user, ip = get_current_authenticated_user(), get_current_ip()

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": {
"name": "BDE",
"email": "tresorerie.bde@example.com",
"membership_fee": 500,
"membership_duration": "396 00:00:00",
"membership_start": "213 00:00:00",
"membership_end": "273 00:00:00"
"require_memberships": true,
"membership_fee_paid": 500,
"membership_fee_unpaid": 500,
"membership_duration": 396,
"membership_start": "2019-08-31",
"membership_end": "2020-09-30"
}
},
{
@ -17,10 +19,13 @@
"fields": {
"name": "Kfet",
"email": "tresorerie.bde@example.com",
"membership_fee": 3500,
"membership_duration": "396 00:00:00",
"membership_start": "213 00:00:00",
"membership_end": "273 00:00:00"
"parent_club": 1,
"require_memberships": true,
"membership_fee_paid": 3500,
"membership_fee_unpaid": 3500,
"membership_duration": 396,
"membership_start": "2019-08-31",
"membership_end": "2020-09-30"
}
}
]

View File

@ -1,13 +1,10 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from crispy_forms.bootstrap import Div
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User
from note_kfet.inputs import Autocomplete
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask
from .models import Profile, Club, Membership
@ -47,11 +44,18 @@ class ClubForm(forms.ModelForm):
class Meta:
model = Club
fields = '__all__'
class AddMembersForm(forms.Form):
class Meta:
fields = ('',)
widgets = {
"membership_fee_paid": AmountInput(),
"membership_fee_unpaid": AmountInput(),
"parent_club": Autocomplete(
Club,
attrs={
'api_url': '/api/members/club/',
}
),
"membership_start": DatePickerInput(),
"membership_end": DatePickerInput(),
}
class MembershipForm(forms.ModelForm):
@ -71,28 +75,5 @@ class MembershipForm(forms.ModelForm):
'placeholder': 'Nom ...',
},
),
'date_start': DatePickerInput(),
}
MemberFormSet = forms.modelformset_factory(
Membership,
form=MembershipForm,
extra=2,
can_delete=True,
)
class FormSetHelper(FormHelper):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_tag = False
self.form_method = 'POST'
self.form_class = 'form-inline'
# self.template = 'bootstrap/table_inline_formset.html'
self.layout = Layout(
Div(
Div('user', css_class='col-sm-2'),
Div('roles', css_class='col-sm-2'),
Div('date_start', css_class='col-sm-2'),
css_class="row formset-row",
))

View File

@ -4,10 +4,12 @@
import datetime
from django.conf import settings
from django.contrib.auth.models import User
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from note.models import MembershipTransaction
class Profile(models.Model):
@ -77,22 +79,43 @@ class Club(models.Model):
)
# Memberships
membership_fee = models.PositiveIntegerField(
verbose_name=_('membership fee'),
# When set to False, the membership system won't be used.
# Useful to create notes for activities or departments.
require_memberships = models.BooleanField(
default=True,
verbose_name=_("require memberships"),
help_text=_("Uncheck if this club don't require memberships."),
)
membership_duration = models.DurationField(
membership_fee_paid = models.PositiveIntegerField(
default=0,
verbose_name=_('membership fee (paid students)'),
)
membership_fee_unpaid = models.PositiveIntegerField(
default=0,
verbose_name=_('membership fee (unpaid students)'),
)
membership_duration = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name=_('membership duration'),
help_text=_('The longest time a membership can last '
help_text=_('The longest time (in days) a membership can last '
'(NULL = infinite).'),
)
membership_start = models.DurationField(
membership_start = models.DateField(
blank=True,
null=True,
verbose_name=_('membership start'),
help_text=_('How long after January 1st the members can renew '
'their membership.'),
)
membership_end = models.DurationField(
membership_end = models.DateField(
blank=True,
null=True,
verbose_name=_('membership end'),
help_text=_('How long the membership can last after January 1st '
@ -100,6 +123,33 @@ class Club(models.Model):
'membership.'),
)
def update_membership_dates(self):
"""
This function is called each time the club detail view is displayed.
Update the year of the membership dates.
"""
if not self.membership_start:
return
today = datetime.date.today()
if (today - self.membership_start).days >= 365:
self.membership_start = datetime.date(self.membership_start.year + 1,
self.membership_start.month, self.membership_start.day)
self.membership_end = datetime.date(self.membership_end.year + 1,
self.membership_end.month, self.membership_end.day)
self.save(force_update=True)
def save(self, force_insert=False, force_update=False, using=None,
update_fields=None):
if not self.require_memberships:
self.membership_fee_paid = 0
self.membership_fee_unpaid = 0
self.membership_duration = None
self.membership_start = None
self.membership_end = None
super().save(force_insert, force_update, update_fields)
class Meta:
verbose_name = _("club")
verbose_name_plural = _("clubs")
@ -114,9 +164,6 @@ class Club(models.Model):
class Role(models.Model):
"""
Role that an :model:`auth.User` can have in a :model:`member.Club`
TODO: Integrate the right management, and create some standard Roles at the
creation of the club.
"""
name = models.CharField(
verbose_name=_('name'),
@ -138,24 +185,31 @@ class Membership(models.Model):
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
User,
on_delete=models.PROTECT,
verbose_name=_("user"),
)
club = models.ForeignKey(
Club,
on_delete=models.PROTECT,
verbose_name=_("club"),
)
roles = models.ForeignKey(
roles = models.ManyToManyField(
Role,
on_delete=models.PROTECT,
verbose_name=_("roles"),
)
date_start = models.DateField(
verbose_name=_('membership starts on'),
)
date_end = models.DateField(
verbose_name=_('membership ends on'),
null=True,
)
fee = models.PositiveIntegerField(
verbose_name=_('fee'),
)
@ -168,10 +222,54 @@ class Membership(models.Model):
def save(self, *args, **kwargs):
if self.club.parent_club is not None:
if not Membership.objects.filter(user=self.user, club=self.club.parent_club):
raise ValidationError(_('User is not a member of the parent club'))
if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists():
raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
created = not self.pk
if created:
if Membership.objects.filter(
user=self.user,
club=self.club,
date_start__lte=self.date_start,
date_end__gte=self.date_start,
).exists():
raise ValidationError(_('User is already a member of the club'))
if self.user.profile.paid:
self.fee = self.club.membership_fee_paid
else:
self.fee = self.club.membership_fee_unpaid
if self.club.membership_duration is not None:
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration)
else:
self.date_end = self.date_start + datetime.timedelta(days=424242)
if self.club.membership_end is not None and self.date_end > self.club.membership_end:
self.date_end = self.club.membership_end
super().save(*args, **kwargs)
self.make_transaction()
def make_transaction(self):
if not self.fee or MembershipTransaction.objects.filter(membership=self).exists():
return
if self.fee:
transaction = MembershipTransaction(
membership=self,
source=self.user.note,
destination=self.club.note,
quantity=1,
amount=self.fee,
reason="Adhésion " + self.club.name,
)
transaction._force_save = True
transaction.save(force_insert=True)
def __str__(self):
return _("Membership of {user} for the club {club}").format(user=self.user.username, club=self.club.name, )
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')

View File

@ -1,10 +1,17 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from datetime import datetime
import django_tables2 as tables
from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from django.urls import reverse_lazy
from django.utils.html import format_html
from note.templatetags.pretty_money import pretty_money
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
from .models import Club
from .models import Club, Membership
class ClubTable(tables.Table):
@ -24,7 +31,11 @@ class ClubTable(tables.Table):
class UserTable(tables.Table):
section = tables.Column(accessor='profile.section')
solde = tables.Column(accessor='note.balance')
balance = tables.Column(accessor='note.balance', verbose_name=_("Balance"))
def render_balance(self, value):
return pretty_money(value)
class Meta:
attrs = {
@ -33,3 +44,68 @@ class UserTable(tables.Table):
template_name = 'django_tables2/bootstrap4.html'
fields = ('last_name', 'first_name', 'username', 'email')
model = User
row_attrs = {
'class': 'table-row',
'data-href': lambda record: record.pk
}
class MembershipTable(tables.Table):
roles = tables.Column(
attrs={
"td": {
"class": "text-truncate",
}
}
)
def render_club(self, value):
s = value.name
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
s = format_html("<a href={url}>{name}</a>",
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
return s
def render_fee(self, value, record):
t = pretty_money(value)
# If it is required and if the user has the right, the renew button is displayed.
if record.club.membership_start is not None:
if record.date_start < record.club.membership_start: # If the renew is available
if not Membership.objects.filter(
club=record.club,
user=record.user,
date_start__gte=record.club.membership_start,
date_end__lte=record.club.membership_end,
).exists(): # If the renew is not yet performed
empty_membership = Membership(
club=record.club,
user=record.user,
date_start=datetime.now().date(),
date_end=datetime.now().date(),
fee=0,
)
if PermissionBackend.check_perm(get_current_authenticated_user(),
"member:add_membership", empty_membership): # If the user has right
t = format_html(t + ' <a class="btn btn-warning" href="{url}">{text}</a>',
url=reverse_lazy('member:club_renew_membership',
kwargs={"pk": record.pk}), text=_("Renew"))
return t
def render_roles(self, record):
roles = record.roles.all()
s = ", ".join(str(role) for role in roles)
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
+ "'>" + s + "</a>")
return s
class Meta:
attrs = {
'class': 'table table-condensed table-striped table-hover',
'style': 'table-layout: fixed;'
}
template_name = 'django_tables2/bootstrap4.html'
fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
model = Membership

View File

@ -8,17 +8,21 @@ from . import views
app_name = 'member'
urlpatterns = [
path('signup/', views.UserCreateView.as_view(), name="signup"),
path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
path('club/<int:pk>/update', views.ClubUpdateView.as_view(), name="club_update"),
path('club/<int:pk>/update_pic', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
path('club/<int:pk>/aliases', views.ClubAliasView.as_view(), name="club_alias"),
path('club/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"),
path('club/renew_membership/<int:pk>/', views.ClubRenewMembershipView.as_view(), name="club_renew_membership"),
path('club/<int:pk>/update/', views.ClubUpdateView.as_view(), name="club_update"),
path('club/<int:pk>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
path('user/', views.UserListView.as_view(), name="user_list"),
path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"),
path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"),
path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),
path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
]

View File

@ -2,17 +2,21 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import io
from datetime import datetime, timedelta
from PIL import Image
from django.conf import settings
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView
from django.core.exceptions import ValidationError
from django.db.models import Q
from django.forms import HiddenInput
from django.shortcuts import redirect
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
from django.views.generic.base import View
from django.views.generic.edit import FormMixin
from django_tables2.views import SingleTableView
from rest_framework.authtoken.models import Token
@ -21,12 +25,11 @@ from note.models import Alias, NoteUser
from note.models.transactions import Transaction
from note.tables import HistoryTable, AliasTable
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .filters import UserFilter, UserFilterFormHelper
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \
CustomAuthenticationForm
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
from .models import Club, Membership
from .tables import ClubTable, UserTable
from .tables import ClubTable, UserTable, MembershipTable
class CustomLoginView(LoginView):
@ -63,7 +66,7 @@ class UserCreateView(CreateView):
return super().form_valid(form)
class UserUpdateView(LoginRequiredMixin, UpdateView):
class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = User
fields = ['first_name', 'last_name', 'username', 'email']
template_name = 'member/profile_update.html'
@ -97,7 +100,8 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
if form.is_valid() and profile_form.is_valid():
new_username = form.data['username']
alias = Alias.objects.filter(name=new_username)
# Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer
# Si le nouveau pseudo n'est pas un de nos alias,
# on supprime éventuellement un alias similaire pour le remplacer
if not alias.exists():
similar = Alias.objects.filter(
normalized_name=Alias.normalize(new_username))
@ -119,7 +123,7 @@ class UserUpdateView(LoginRequiredMixin, UpdateView):
return reverse_lazy('member:user_detail', args=(self.object.id,))
class UserDetailView(LoginRequiredMixin, DetailView):
class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
"""
Affiche les informations sur un utilisateur, sa note, ses clubs...
"""
@ -127,44 +131,56 @@ class UserDetailView(LoginRequiredMixin, DetailView):
context_object_name = "user_object"
template_name = "member/profile_detail.html"
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = context['user_object']
history_list = \
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note)).order_by("-id")\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
context['history_list'] = HistoryTable(history_list)
club_list = \
Membership.objects.all().filter(user=user).only("club")
context['club_list'] = ClubTable(club_list)
club_list = Membership.objects.filter(user=user, date_end__gte=datetime.today())\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
context['club_list'] = MembershipTable(data=club_list)
return context
class UserListView(LoginRequiredMixin, SingleTableView):
class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
Affiche la liste des utilisateurs, avec une fonction de recherche statique
"""
model = User
table_class = UserTable
template_name = 'member/user_list.html'
filter_class = UserFilter
formhelper_class = UserFilterFormHelper
def get_queryset(self, **kwargs):
qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))
self.filter = self.filter_class(self.request.GET, queryset=qs)
self.filter.form.helper = self.formhelper_class()
return self.filter.qs
qs = super().get_queryset()
if "search" in self.request.GET:
pattern = self.request.GET["search"]
if not pattern:
return qs.none()
qs = qs.filter(
Q(first_name__iregex=pattern)
| Q(last_name__iregex=pattern)
| Q(profile__section__iregex=pattern)
| Q(note__alias__name__iregex="^" + pattern)
| Q(note__alias__normalized_name__iregex=Alias.normalize("^" + pattern))
)
else:
qs = qs.none()
return qs[:20]
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filter"] = self.filter
context["title"] = _("Search user")
return context
class ProfileAliasView(LoginRequiredMixin, DetailView):
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = User
template_name = 'member/profile_alias.html'
context_object_name = 'user_object'
@ -176,11 +192,11 @@ class ProfileAliasView(LoginRequiredMixin, DetailView):
return context
class PictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, DetailView):
form_class = ImageForm
def get_context_data(self, *args, **kwargs):
context = super().get_context_data(*args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['form'] = self.form_class(self.request.POST, self.request.FILES)
return context
@ -237,8 +253,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
template_name = "member/manage_auth_tokens.html"
def get(self, request, *args, **kwargs):
if 'regenerate' in request.GET and Token.objects.filter(
user=request.user).exists():
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
Token.objects.get(user=self.request.user).delete()
return redirect(reverse_lazy('member:auth_token') + "?show",
permanent=True)
@ -247,8 +262,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['token'] = Token.objects.get_or_create(
user=self.request.user)[0]
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
return context
@ -257,7 +271,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
# ******************************* #
class ClubCreateView(LoginRequiredMixin, CreateView):
class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Create Club
"""
@ -269,38 +283,49 @@ class ClubCreateView(LoginRequiredMixin, CreateView):
return super().form_valid(form)
class ClubListView(LoginRequiredMixin, SingleTableView):
class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List existing Clubs
"""
model = Club
table_class = ClubTable
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
class ClubDetailView(LoginRequiredMixin, DetailView):
class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Club
context_object_name = "club"
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = context["club"]
club_transactions = \
Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
club.update_membership_dates()
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")).order_by('-id')
context['history_list'] = HistoryTable(club_transactions)
club_member = \
Membership.objects.all().filter(club=club)
# TODO: consider only valid Membership
context['member_list'] = club_member
club_member = Membership.objects.filter(
club=club,
date_end__gte=datetime.today(),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
context['member_list'] = MembershipTable(data=club_member)
empty_membership = Membership(
club=club,
user=User.objects.first(),
date_start=datetime.now().date(),
date_end=datetime.now().date(),
fee=0,
)
context["can_add_members"] = PermissionBackend()\
.has_perm(self.request.user, "member.add_membership", empty_membership)
return context
class ClubAliasView(LoginRequiredMixin, DetailView):
class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
model = Club
template_name = 'member/club_alias.html'
context_object_name = 'club'
@ -312,12 +337,14 @@ class ClubAliasView(LoginRequiredMixin, DetailView):
return context
class ClubUpdateView(LoginRequiredMixin, UpdateView):
class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Club
context_object_name = "club"
form_class = ClubForm
template_name = "member/club_form.html"
success_url = reverse_lazy("member:club_detail")
def get_success_url(self):
return reverse_lazy("member:club_detail", kwargs={"pk": self.object.pk})
class ClubPictureUpdateView(PictureUpdateView):
@ -329,35 +356,123 @@ class ClubPictureUpdateView(PictureUpdateView):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.id})
class ClubAddMemberView(LoginRequiredMixin, CreateView):
class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
def get_queryset(self, **kwargs):
return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")
| PermissionBackend.filter_queryset(self.request.user, Membership,
"change"))
def get_context_data(self, **kwargs):
club = Club.objects.get(pk=self.kwargs["pk"])
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["pk"])
context = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet()
context['helper'] = FormSetHelper()
context['club'] = club
context['no_cache'] = True
return context
def post(self, request, *args, **kwargs):
return
# TODO: Implement POST
# formset = MembershipFormset(request.POST)
# if formset.is_valid():
# return self.form_valid(formset)
# else:
# return self.form_invalid(formset)
def form_valid(self, form):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["pk"])
user = self.request.user
form.instance.club = club
def form_valid(self, formset):
formset.save()
return super().form_valid(formset)
if user.profile.paid:
fee = club.membership_fee_paid
else:
fee = club.membership_fee_unpaid
if user.note.balance < fee and not Membership.objects.filter(
club__name="Kfet",
user=user,
date_start__lte=datetime.now().date(),
date_end__gte=datetime.now().date(),
).exists():
# Users without a valid Kfet membership can't have a negative balance.
# Club 2 = Kfet (hard-code :'( )
# TODO Send a notification to the user (with a mail?) to tell her/him to credit her/his note
form.add_error('user',
_("This user don't have enough money to join this club, and can't have a negative balance."))
if club.parent_club is not None:
if not Membership.objects.filter(user=form.instance.user, club=club.parent_club).exists():
form.add_error('user', _('User is not a member of the parent club') + ' ' + club.parent_club.name)
return super().form_invalid(form)
if Membership.objects.filter(
user=form.instance.user,
club=club,
date_start__lte=form.instance.date_start,
date_end__gte=form.instance.date_start,
).exists():
form.add_error('user', _('User is already a member of the club'))
return super().form_invalid(form)
if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
return super().form_invalid(form)
if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end:
form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
return super().form_invalid(form)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = self.object.club
context['club'] = club
form = context['form']
form.fields['user'].disabled = True
form.fields['date_start'].widget = HiddenInput()
return context
def form_valid(self, form):
if form.instance.club.membership_start and form.instance.date_start < form.instance.club.membership_start:
form.add_error('user', _("The membership must start after {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
return super().form_invalid(form)
if form.instance.club.membership_end and form.instance.date_start > form.instance.club.membership_end:
form.add_error('user', _("The membership must begin before {:%m-%d-%Y}.")
.format(form.instance.club.membership_start))
return super().form_invalid(form)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
class ClubRenewMembershipView(ProtectQuerysetMixin, LoginRequiredMixin, View):
def get(self, *args, **kwargs):
user = self.request.user
membership = Membership.objects.filter(PermissionBackend.filter_queryset(user, Membership, "change"))\
.filter(pk=self.kwargs["pk"]).get()
if Membership.objects.filter(
club=membership.club,
user=membership.user,
date_start__gte=membership.club.membership_start,
date_end__lte=membership.club.membership_end,
).exists():
raise ValidationError(_("This membership is already renewed"))
new_membership = Membership.objects.create(
user=user,
club=membership.club,
date_start=membership.date_end + timedelta(days=1),
)
new_membership.roles.set(membership.roles.all())
new_membership.save()
return redirect(reverse_lazy('member:club_detail', kwargs={'pk': membership.club.pk}))

View File

@ -138,6 +138,13 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
return []
@admin.register(MembershipTransaction)
class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
"""
Admin customisation for Transaction
"""
@admin.register(TransactionTemplate)
class TransactionTemplateAdmin(admin.ModelAdmin):
"""

View File

@ -90,7 +90,7 @@ class NotePolymorphicSerializer(PolymorphicSerializer):
Note: NoteSerializer,
NoteUser: NoteUserSerializer,
NoteClub: NoteClubSerializer,
NoteSpecial: NoteSpecialSerializer
NoteSpecial: NoteSpecialSerializer,
}
class Meta:

View File

@ -46,12 +46,14 @@ class TransactionTemplate(models.Model):
unique=True,
error_messages={'unique': _("A template with this name already exist")},
)
destination = models.ForeignKey(
NoteClub,
on_delete=models.PROTECT,
related_name='+', # no reverse
verbose_name=_('destination'),
)
amount = models.PositiveIntegerField(
verbose_name=_('amount'),
help_text=_('in centimes'),
@ -62,9 +64,12 @@ class TransactionTemplate(models.Model):
verbose_name=_('type'),
max_length=31,
)
display = models.BooleanField(
default=True,
verbose_name=_("display"),
)
description = models.CharField(
verbose_name=_('description'),
max_length=255,
@ -140,6 +145,7 @@ class Transaction(PolymorphicModel):
max_length=255,
default=None,
null=True,
blank=True,
)
class Meta:

View File

@ -118,7 +118,8 @@ class AliasTable(tables.Table):
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}})
attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"),)
class ButtonTable(tables.Table):
@ -134,17 +135,20 @@ class ButtonTable(tables.Table):
}
model = TransactionTemplate
exclude = ('id',)
edit = tables.LinkColumn('note:template_update',
args=[A('pk')],
attrs={'td': {'class': 'col-sm-1'},
'a': {'class': 'btn btn-sm btn-primary'}},
text=_('edit'),
accessor='pk')
accessor='pk',
verbose_name=_("Edit"),)
delete_col = tables.TemplateColumn(template_code=DELETE_TEMPLATE,
extra_context={"delete_trans": _('delete')},
attrs={'td': {'class': 'col-sm-1'}})
attrs={'td': {'class': 'col-sm-1'}},
verbose_name=_("Delete"),)
def render_amount(self, value):
return pretty_money(value)

View File

@ -9,6 +9,7 @@ from django_tables2 import SingleTableView
from django.urls import reverse_lazy
from note_kfet.inputs import AmountInput
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import TransactionTemplateForm
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
@ -16,7 +17,7 @@ from .models.transactions import SpecialTransaction
from .tables import HistoryTable, ButtonTable
class TransactionCreateView(LoginRequiredMixin, SingleTableView):
class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
View for the creation of Transaction between two note which are not :models:`transactions.RecurrentTransaction`.
e.g. for donation/transfer between people and clubs or for credit/debit with :models:`note.NoteSpecial`
@ -26,12 +27,9 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
model = Transaction
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 50}
def get_queryset(self):
return Transaction.objects.filter(PermissionBackend.filter_queryset(
self.request.user, Transaction, "view")
).order_by("-id").all()[:50]
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:50]
def get_context_data(self, **kwargs):
"""
@ -42,12 +40,14 @@ class TransactionCreateView(LoginRequiredMixin, SingleTableView):
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
context['special_types'] = NoteSpecial.objects.order_by("special_type").all()
context['special_types'] = NoteSpecial.objects\
.filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
.order_by("special_type").all()
return context
class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Create TransactionTemplate
"""
@ -56,7 +56,7 @@ class TransactionTemplateCreateView(LoginRequiredMixin, CreateView):
success_url = reverse_lazy('note:template_list')
class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List TransactionsTemplates
"""
@ -64,7 +64,7 @@ class TransactionTemplateListView(LoginRequiredMixin, SingleTableView):
table_class = ButtonTable
class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
"""
model = TransactionTemplate
@ -72,21 +72,19 @@ class TransactionTemplateUpdateView(LoginRequiredMixin, UpdateView):
success_url = reverse_lazy('note:template_list')
class ConsoView(LoginRequiredMixin, SingleTableView):
class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
The Magic View that make people pay their beer and burgers.
(Most of the magic happens in the dark world of Javascript see consos.js)
"""
model = Transaction
template_name = "note/conso_form.html"
# Transaction history table
table_class = HistoryTable
table_pagination = {"per_page": 50}
def get_queryset(self):
return Transaction.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
).order_by("-id").all()[:50]
def get_queryset(self, **kwargs):
return super().get_queryset(**kwargs).order_by("-id").all()[:50]
def get_context_data(self, **kwargs):
"""

View File

@ -3,7 +3,7 @@
from rest_framework import serializers
from ..models import Permission
from ..models import Permission, RolePermissions
class PermissionSerializer(serializers.ModelSerializer):
@ -15,3 +15,14 @@ class PermissionSerializer(serializers.ModelSerializer):
class Meta:
model = Permission
fields = '__all__'
class RolePermissionsSerializer(serializers.ModelSerializer):
"""
REST API Serializer for RolePermissions types.
The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API.
"""
class Meta:
model = RolePermissions
fields = '__all__'

View File

@ -1,11 +1,12 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from .views import PermissionViewSet
from .views import PermissionViewSet, RolePermissionsViewSet
def register_permission_urls(router, path):
"""
Configure router for permission REST API.
"""
router.register(path, PermissionViewSet)
router.register(path + "/permission", PermissionViewSet)
router.register(path + "/roles", RolePermissionsViewSet)

View File

@ -4,17 +4,29 @@
from django_filters.rest_framework import DjangoFilterBackend
from api.viewsets import ReadOnlyProtectedModelViewSet
from .serializers import PermissionSerializer
from ..models import Permission
from .serializers import PermissionSerializer, RolePermissionsSerializer
from ..models import Permission, RolePermissions
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
then render it on /api/logs/
The djangorestframework plugin will get all `Permission` objects, serialize it to JSON with the given serializer,
then render it on /api/permission/permission/
"""
queryset = Permission.objects.all()
serializer_class = PermissionSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['model', 'type', ]
class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet):
"""
REST API View set.
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
then render it on /api/permission/roles/
"""
queryset = RolePermissions.objects.all()
serializer_class = RolePermissionsSerializer
filter_backends = [DjangoFilterBackend]
filterset_fields = ['role', ]

View File

@ -1,6 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import datetime
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.contenttypes.models import ContentType
@ -9,6 +11,7 @@ from note.models import Note, NoteUser, NoteClub, NoteSpecial
from note_kfet.middlewares import get_current_session
from member.models import Membership, Club
from .decorators import memoize
from .models import Permission
@ -20,6 +23,28 @@ class PermissionBackend(ModelBackend):
supports_anonymous_user = False
supports_inactive_user = False
@staticmethod
@memoize
def get_raw_permissions(user, t):
"""
Query permissions of a certain type for a user, then memoize it.
:param user: The owner of the permissions
:param t: The type of the permissions: view, change, add or delete
:return: The queryset of the permissions of the user (memoized) grouped by clubs
"""
if isinstance(user, AnonymousUser):
# Unauthenticated users have no permissions
return Permission.objects.none()
return Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
.filter(
rolepermissions__role__membership__user=user,
rolepermissions__role__membership__date_start__lte=datetime.date.today(),
rolepermissions__role__membership__date_end__gte=datetime.date.today(),
type=t,
mask__rank__lte=get_current_session().get("permission_mask", 0),
).distinct('club', 'pk',)
@staticmethod
def permissions(user, model, type):
"""
@ -29,16 +54,16 @@ class PermissionBackend(ModelBackend):
:param type: The type of the permissions: view, change, add or delete
:return: A generator of the requested permissions
"""
for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
.filter(
rolepermissions__role__membership__user=user,
model__app_label=model.app_label, # For polymorphic models, we don't filter on model type
type=type,
).all():
if not isinstance(model, permission.model.__class__):
clubs = {}
for permission in PermissionBackend.get_raw_permissions(user, type):
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club:
continue
club = Club.objects.get(pk=permission.club)
if permission.club not in clubs:
clubs[permission.club] = club = Club.objects.get(pk=permission.club)
else:
club = clubs[permission.club]
permission = permission.about(
user=user,
club=club,
@ -52,10 +77,10 @@ class PermissionBackend(ModelBackend):
F=F,
Q=Q
)
if permission.mask.rank <= get_current_session().get("permission_mask", 0):
yield permission
yield permission
@staticmethod
@memoize
def filter_queryset(user, model, t, field=None):
"""
Filter a queryset by considering the permissions of a given user.
@ -89,10 +114,23 @@ class PermissionBackend(ModelBackend):
query = query | perm.query
return query
def has_perm(self, user_obj, perm, obj=None):
@staticmethod
@memoize
def check_perm(user_obj, perm, obj=None):
"""
Check is the given user has the permission over a given object.
The result is then memoized.
Exception: for add permissions, since the object is not hashable since it doesn't have any
primary key, the result is not memoized. Moreover, the right could change
(e.g. for a transaction, the balance of the user could change)
"""
if user_obj is None or isinstance(user_obj, AnonymousUser):
return False
sess = get_current_session()
if sess is not None and sess.session_key is None:
return Permission.objects.none()
if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42:
return True
@ -104,10 +142,13 @@ class PermissionBackend(ModelBackend):
perm_field = perm[2] if len(perm) == 3 else None
ct = ContentType.objects.get_for_model(obj)
if any(permission.applies(obj, perm_type, perm_field)
for permission in self.permissions(user_obj, ct, perm_type)):
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
return True
return False
def has_perm(self, user_obj, perm, obj=None):
return PermissionBackend.check_perm(user_obj, perm, obj)
def has_module_perms(self, user_obj, app_label):
return False

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",
"transaction"
],
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]",
"query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], [\"OR\", {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}, {\"valid\": false}]]",
"type": "add",
"mask": 2,
"field": "",
@ -783,6 +783,66 @@
"description": "Validate invitation transactions"
}
},
{
"model": "permission.permission",
"pk": 47,
"fields": {
"model": [
"member",
"club"
],
"query": "{\"pk\": [\"club\", \"pk\"]}",
"type": "change",
"mask": 1,
"field": "",
"description": "Update club"
}
},
{
"model": "permission.permission",
"pk": 48,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"user\": [\"user\"]}",
"type": "view",
"mask": 1,
"field": "",
"description": "View our memberships"
}
},
{
"model": "permission.permission",
"pk": 49,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club\": [\"club\"]}",
"type": "view",
"mask": 1,
"field": "",
"description": "View club's memberships"
}
},
{
"model": "permission.permission",
"pk": 50,
"fields": {
"model": [
"member",
"membership"
],
"query": "{\"club\": [\"club\"]}",
"type": "add",
"mask": 2,
"field": "",
"description": "Add a membership to a club"
}
},
{
"model": "permission.rolepermissions",
"pk": 1,
@ -795,7 +855,8 @@
8,
9,
10,
11
11,
48
]
}
},
@ -880,5 +941,75 @@
46
]
}
},
{
"model": "permission.rolepermissions",
"pk": 6,
"fields": {
"role": 7,
"permissions": [
22,
47
]
}
},
{
"model": "permission.rolepermissions",
"pk": 7,
"fields": {
"role": 5,
"permissions": [
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
38,
39,
40,
41,
42,
43,
44,
45,
46,
47,
48,
49,
50
]
}
}
]

View File

@ -38,20 +38,33 @@ class InstancedPermission:
if permission_type == self.type:
self.update_query()
# Don't increase indexes
obj.pk = 0
# Don't increase indexes, if the primary key is an AutoField
if not hasattr(obj, "pk") or not obj.pk:
obj.pk = 0
oldpk = None
else:
oldpk = obj.pk
# Ensure previous models are deleted
self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete()
# Force insertion, no data verification, no trigger
obj._force_save = True
Model.save(obj, force_insert=True)
ret = obj in self.model.model_class().objects.filter(self.query).all()
# We don't want log anything
obj._no_log = True
ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists()
# Delete testing object
obj._force_delete = True
Model.delete(obj)
# If the primary key was specified, we restore it
obj.pk = oldpk
return ret
if permission_type == self.type:
if self.field and field_name != self.field:
return False
self.update_query()
return obj in self.model.model_class().objects.filter(self.query).all()
return self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists()
else:
return False

View File

@ -44,7 +44,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not all(PermissionBackend().has_perm(user, perm, obj) for perm in perms):
if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms):
# If the user does not have permissions we need to determine if
# they have read permissions to see 403, or not, and simply see
# a 404 response.

View File

@ -2,8 +2,6 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from django.core.exceptions import PermissionDenied
from django.db.models.signals import pre_save, pre_delete, post_save, post_delete
from logs import signals as logs_signals
from note_kfet.middlewares import get_current_authenticated_user
from permission.backends import PermissionBackend
@ -29,6 +27,9 @@ def pre_save_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED:
return
if hasattr(instance, "_force_save"):
return
user = get_current_authenticated_user()
if user is None:
# Action performed on shell is always granted
@ -43,7 +44,7 @@ def pre_save_object(sender, instance, **kwargs):
# We check if the user can change the model
# If the user has all right on a model, then OK
if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance):
if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance):
return
# In the other case, we check if he/she has the right to change one field
@ -55,35 +56,17 @@ def pre_save_object(sender, instance, **kwargs):
# If the field wasn't modified, no need to check the permissions
if old_value == new_value:
continue
if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
raise PermissionDenied
else:
# We check if the user can add the model
# While checking permissions, the object will be inserted in the DB, then removed.
# We disable temporary the connectors
pre_save.disconnect(pre_save_object)
pre_delete.disconnect(pre_delete_object)
# We disable also logs connectors
pre_save.disconnect(logs_signals.pre_save_object)
post_save.disconnect(logs_signals.save_object)
post_delete.disconnect(logs_signals.delete_object)
# We check if the user has right to add the object
has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance)
# Then we reconnect all
pre_save.connect(pre_save_object)
pre_delete.connect(pre_delete_object)
pre_save.connect(logs_signals.pre_save_object)
post_save.connect(logs_signals.save_object)
post_delete.connect(logs_signals.delete_object)
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
if not has_perm:
raise PermissionDenied
def pre_delete_object(sender, instance, **kwargs):
def pre_delete_object(instance, **kwargs):
"""
Before a model get deleted, we check the permissions
"""
@ -91,6 +74,9 @@ def pre_delete_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED:
return
if hasattr(instance, "_force_delete"):
return
user = get_current_authenticated_user()
if user is None:
# Action performed on shell is always granted
@ -101,5 +87,5 @@ def pre_delete_object(sender, instance, **kwargs):
model_name = model_name_full[1]
# We check if the user has rights to delete the object
if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance):
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
raise PermissionDenied

View File

@ -4,6 +4,7 @@
from django.contrib.contenttypes.models import ContentType
from django.template.defaultfilters import stringfilter
from django import template
from note.models import Transaction
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
from permission.backends import PermissionBackend
@ -19,13 +20,8 @@ def not_empty_model_list(model_name):
return False
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
return True
if session.get("not_empty_model_list_" + model_name, None):
return session.get("not_empty_model_list_" + model_name, None) == 1
spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "view")).all()
session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2
return session.get("not_empty_model_list_" + model_name) == 1
qs = model_list(model_name)
return qs.exists()
@stringfilter
@ -39,20 +35,54 @@ def not_empty_model_change_list(model_name):
return False
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
return True
if session.get("not_empty_model_change_list_" + model_name, None):
return session.get("not_empty_model_change_list_" + model_name, None) == 1
qs = model_list(model_name, "change")
return qs.exists()
@stringfilter
def model_list(model_name, t="view"):
"""
Return the queryset of all visible instances of the given model.
"""
user = get_current_authenticated_user()
if user is None:
return False
spl = model_name.split(".")
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, "change"))
session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2
return session.get("not_empty_model_change_list_" + model_name) == 1
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all()
return qs
def has_perm(perm, obj):
return PermissionBackend().has_perm(get_current_authenticated_user(), perm, obj)
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
def can_create_transaction():
"""
:return: True iff the authenticated user can create a transaction.
"""
user = get_current_authenticated_user()
session = get_current_session()
if user is None:
return False
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
return True
if session.get("can_create_transaction", None):
return session.get("can_create_transaction", None) == 1
empty_transaction = Transaction(
source=user.note,
destination=user.note,
quantity=1,
amount=0,
reason="Check permissions",
)
session["can_create_transaction"] = PermissionBackend.check_perm(user, "note.add_transaction", empty_transaction)
return session.get("can_create_transaction") == 1
register = template.Library()
register.filter('not_empty_model_list', not_empty_model_list)
register.filter('not_empty_model_change_list', not_empty_model_change_list)
register.filter('model_list', model_list)
register.filter('has_perm', has_perm)

11
apps/permission/views.py Normal file
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.utils.translation import gettext_lazy as _
from note_kfet.inputs import DatePickerInput, AmountInput
from permission.backends import PermissionBackend
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
@ -131,7 +132,8 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
# Add submit button
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
def clean_last_name(self):
"""

View File

@ -19,13 +19,15 @@ from django.views.generic.base import View, TemplateView
from django_tables2 import SingleTableView
from note.models import SpecialTransaction, NoteSpecial
from note_kfet.settings.base import BASE_DIR
from permission.backends import PermissionBackend
from permission.views import ProtectQuerysetMixin
from .forms import InvoiceForm, ProductFormSet, ProductFormSetHelper, RemittanceForm, LinkTransactionToRemittanceForm
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
from .tables import InvoiceTable, RemittanceTable, SpecialTransactionTable
class InvoiceCreateView(LoginRequiredMixin, CreateView):
class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Create Invoice
"""
@ -67,7 +69,7 @@ class InvoiceCreateView(LoginRequiredMixin, CreateView):
return reverse_lazy('treasury:invoice_list')
class InvoiceListView(LoginRequiredMixin, SingleTableView):
class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
"""
List existing Invoices
"""
@ -75,7 +77,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
table_class = InvoiceTable
class InvoiceUpdateView(LoginRequiredMixin, UpdateView):
class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Create Invoice
"""
@ -130,7 +132,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
def get(self, request, **kwargs):
pk = kwargs["pk"]
invoice = Invoice.objects.get(pk=pk)
invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk)
products = Product.objects.filter(invoice=invoice).all()
# Informations of the BDE. Should be updated when the school will move.
@ -188,7 +190,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
return response
class RemittanceCreateView(LoginRequiredMixin, CreateView):
class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
"""
Create Remittance
"""
@ -201,7 +203,9 @@ class RemittanceCreateView(LoginRequiredMixin, CreateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["table"] = RemittanceTable(data=Remittance.objects.all())
ctx["table"] = RemittanceTable(data=Remittance.objects
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
.all())
ctx["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
return ctx
@ -216,22 +220,28 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["opened_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=False).all())
ctx["closed_remittances"] = RemittanceTable(data=Remittance.objects.filter(closed=True).reverse().all())
ctx["opened_remittances"] = RemittanceTable(
data=Remittance.objects.filter(closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
ctx["closed_remittances"] = RemittanceTable(
data=Remittance.objects.filter(closed=True).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all())
ctx["special_transactions_no_remittance"] = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance=None).all(),
specialtransactionproxy__remittance=None).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
exclude=('remittance_remove', ))
ctx["special_transactions_with_remittance"] = SpecialTransactionTable(
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
specialtransactionproxy__remittance__closed=False).all(),
specialtransactionproxy__remittance__closed=False).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
exclude=('remittance_add', ))
return ctx
class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Update Remittance
"""
@ -244,8 +254,10 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx["table"] = RemittanceTable(data=Remittance.objects.all())
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).all()
ctx["table"] = RemittanceTable(data=Remittance.objects.filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()
ctx["special_transactions"] = SpecialTransactionTable(
data=data,
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
@ -253,7 +265,7 @@ class RemittanceUpdateView(LoginRequiredMixin, UpdateView):
return ctx
class LinkTransactionToRemittanceView(LoginRequiredMixin, UpdateView):
class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
"""
Attach a special transaction to a remittance
"""

View File

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-30 17:31+0200\n"
"POT-Creation-Date: 2020-04-01 18:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -44,9 +44,9 @@ msgid "You can't invite more than 3 people to this activity."
msgstr ""
#: apps/activity/models.py:23 apps/activity/models.py:48
#: apps/member/models.py:64 apps/member/models.py:122
#: apps/member/models.py:66 apps/member/models.py:169
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:232
#: templates/member/club_info.html:13 templates/member/profile_info.html:14
msgid "name"
msgstr ""
@ -68,7 +68,7 @@ msgid "activity types"
msgstr ""
#: apps/activity/models.py:53 apps/note/models/transactions.py:69
#: apps/permission/models.py:90 templates/activity/activity_detail.html:16
#: apps/permission/models.py:103 templates/activity/activity_detail.html:16
msgid "description"
msgstr ""
@ -78,7 +78,7 @@ msgstr ""
msgid "type"
msgstr ""
#: apps/activity/models.py:66 apps/logs/models.py:21
#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:190
#: apps/note/models/notes.py:117
msgid "user"
msgstr ""
@ -169,11 +169,11 @@ msgstr ""
msgid "Type"
msgstr ""
#: apps/activity/tables.py:77 apps/treasury/forms.py:120
#: apps/activity/tables.py:77 apps/treasury/forms.py:121
msgid "Last name"
msgstr ""
#: apps/activity/tables.py:79 apps/treasury/forms.py:122
#: apps/activity/tables.py:79 apps/treasury/forms.py:123
#: templates/note/transaction_form.html:92
msgid "First name"
msgstr ""
@ -186,11 +186,11 @@ msgstr ""
msgid "Balance"
msgstr ""
#: apps/activity/views.py:44 templates/base.html:94
#: apps/activity/views.py:45 templates/base.html:94
msgid "Activities"
msgstr ""
#: apps/activity/views.py:149
#: apps/activity/views.py:153
msgid "Entry for activity \"{}\""
msgstr ""
@ -251,121 +251,165 @@ msgstr ""
msgid "member"
msgstr ""
#: apps/member/models.py:26
#: apps/member/models.py:28
msgid "phone number"
msgstr ""
#: apps/member/models.py:32 templates/member/profile_info.html:27
#: apps/member/models.py:34 templates/member/profile_info.html:27
msgid "section"
msgstr ""
#: apps/member/models.py:33
#: apps/member/models.py:35
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr ""
#: apps/member/models.py:39 templates/member/profile_info.html:30
#: apps/member/models.py:41 templates/member/profile_info.html:30
msgid "address"
msgstr ""
#: apps/member/models.py:45
#: apps/member/models.py:47
msgid "paid"
msgstr ""
#: apps/member/models.py:50 apps/member/models.py:51
#: apps/member/models.py:52 apps/member/models.py:53
msgid "user profile"
msgstr ""
#: apps/member/models.py:69 templates/member/club_info.html:36
#: apps/member/models.py:71 templates/member/club_info.html:46
msgid "email"
msgstr ""
#: apps/member/models.py:76
#: apps/member/models.py:78
msgid "parent club"
msgstr ""
#: apps/member/models.py:81 templates/member/club_info.html:30
msgid "membership fee"
#: apps/member/models.py:87
msgid "require memberships"
msgstr ""
#: apps/member/models.py:85 templates/member/club_info.html:27
#: apps/member/models.py:88
msgid "Uncheck if this club don't require memberships."
msgstr ""
#: apps/member/models.py:93 templates/member/club_info.html:35
msgid "membership fee (paid students)"
msgstr ""
#: apps/member/models.py:98 templates/member/club_info.html:38
msgid "membership fee (unpaid students)"
msgstr ""
#: apps/member/models.py:104 templates/member/club_info.html:28
msgid "membership duration"
msgstr ""
#: apps/member/models.py:86
msgid "The longest time a membership can last (NULL = infinite)."
#: apps/member/models.py:105
msgid "The longest time (in days) a membership can last (NULL = infinite)."
msgstr ""
#: apps/member/models.py:91 templates/member/club_info.html:21
#: apps/member/models.py:112 templates/member/club_info.html:22
msgid "membership start"
msgstr ""
#: apps/member/models.py:92
#: apps/member/models.py:113
msgid "How long after January 1st the members can renew their membership."
msgstr ""
#: apps/member/models.py:97 templates/member/club_info.html:24
#: apps/member/models.py:120 templates/member/club_info.html:25
msgid "membership end"
msgstr ""
#: apps/member/models.py:98
#: apps/member/models.py:121
msgid ""
"How long the membership can last after January 1st of the next year after "
"members can renew their membership."
msgstr ""
#: apps/member/models.py:104 apps/note/models/notes.py:139
#: apps/member/models.py:154 apps/member/models.py:196
#: apps/note/models/notes.py:139
msgid "club"
msgstr ""
#: apps/member/models.py:105
#: apps/member/models.py:155
msgid "clubs"
msgstr ""
#: apps/member/models.py:128 apps/permission/models.py:275
#: apps/member/models.py:175 apps/permission/models.py:288
msgid "role"
msgstr ""
#: apps/member/models.py:129
#: apps/member/models.py:176 apps/member/models.py:201
msgid "roles"
msgstr ""
#: apps/member/models.py:153
#: apps/member/models.py:205
msgid "membership starts on"
msgstr ""
#: apps/member/models.py:156
#: apps/member/models.py:209
msgid "membership ends on"
msgstr ""
#: apps/member/models.py:160
#: apps/member/models.py:214
msgid "fee"
msgstr ""
#: apps/member/models.py:172
#: apps/member/models.py:226 apps/member/views.py:383
msgid "User is not a member of the parent club"
msgstr ""
#: apps/member/models.py:176
#: apps/member/models.py:236 apps/member/views.py:392
msgid "User is already a member of the club"
msgstr ""
#: apps/member/models.py:271
#, python-brace-format
msgid "Membership of {user} for the club {club}"
msgstr ""
#: apps/member/models.py:274
msgid "membership"
msgstr ""
#: apps/member/models.py:177
#: apps/member/models.py:275
msgid "memberships"
msgstr ""
#: apps/member/views.py:76 templates/member/profile_info.html:45
#: apps/member/tables.py:73
msgid "Renew"
msgstr ""
#: apps/member/views.py:80 templates/member/profile_info.html:45
msgid "Update Profile"
msgstr ""
#: apps/member/views.py:89
#: apps/member/views.py:93
msgid "An alias with a similar name already exists."
msgstr ""
#: apps/member/views.py:379
msgid ""
"This user don't have enough money to join this club, and can't have a "
"negative balance."
msgstr ""
#: apps/member/views.py:396 apps/member/views.py:428
msgid "The membership must start after {:%m-%d-%Y}."
msgstr ""
#: apps/member/views.py:401 apps/member/views.py:433
msgid "The membership must begin before {:%m-%d-%Y}."
msgstr ""
#: apps/member/views.py:455
msgid "This membership is already renewed"
msgstr ""
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source"
msgstr ""
#: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/admin.py:128 apps/note/admin.py:163
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107
msgid "destination"
msgstr ""
@ -462,7 +506,7 @@ msgstr ""
msgid "alias"
msgstr ""
#: apps/note/models/notes.py:211 templates/member/club_info.html:33
#: apps/note/models/notes.py:211 templates/member/club_info.html:43
#: templates/member/profile_info.html:36
msgid "aliases"
msgstr ""
@ -524,45 +568,45 @@ msgstr ""
msgid "invalidity reason"
msgstr ""
#: apps/note/models/transactions.py:146
#: apps/note/models/transactions.py:147
msgid "transaction"
msgstr ""
#: apps/note/models/transactions.py:147
#: apps/note/models/transactions.py:148
msgid "transactions"
msgstr ""
#: apps/note/models/transactions.py:201 templates/base.html:84
#: apps/note/models/transactions.py:202 templates/base.html:84
#: templates/note/transaction_form.html:19
#: templates/note/transaction_form.html:140
msgid "Transfer"
msgstr ""
#: apps/note/models/transactions.py:221
#: apps/note/models/transactions.py:222
msgid "Template"
msgstr ""
#: apps/note/models/transactions.py:236
#: apps/note/models/transactions.py:237
msgid "first_name"
msgstr ""
#: apps/note/models/transactions.py:241
#: apps/note/models/transactions.py:242
msgid "bank"
msgstr ""
#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24
#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24
msgid "Credit"
msgstr ""
#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28
#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28
msgid "Debit"
msgstr ""
#: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268
#: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269
msgid "membership transaction"
msgstr ""
#: apps/note/models/transactions.py:264
#: apps/note/models/transactions.py:265
msgid "membership transactions"
msgstr ""
@ -578,29 +622,29 @@ msgstr ""
msgid "No reason specified"
msgstr ""
#: apps/note/views.py:41
#: apps/note/views.py:39
msgid "Transfer money"
msgstr ""
#: apps/note/views.py:102 templates/base.html:79
#: apps/note/views.py:100 templates/base.html:79
msgid "Consumptions"
msgstr ""
#: apps/permission/models.py:69 apps/permission/models.py:262
#: apps/permission/models.py:82 apps/permission/models.py:275
#, python-brace-format
msgid "Can {type} {model}.{field} in {query}"
msgstr ""
#: apps/permission/models.py:71 apps/permission/models.py:264
#: apps/permission/models.py:84 apps/permission/models.py:277
#, python-brace-format
msgid "Can {type} {model} in {query}"
msgstr ""
#: apps/permission/models.py:84
#: apps/permission/models.py:97
msgid "rank"
msgstr ""
#: apps/permission/models.py:147
#: apps/permission/models.py:160
msgid "Specifying field applies only to view and change permission types."
msgstr ""
@ -608,31 +652,32 @@ msgstr ""
msgid "Treasury"
msgstr ""
#: apps/treasury/forms.py:84 apps/treasury/forms.py:132
#: apps/treasury/forms.py:85 apps/treasury/forms.py:133
#: templates/activity/activity_form.html:9
#: templates/activity/activity_invite.html:8
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46
#: templates/member/add_members.html:14 templates/member/club_form.html:9
#: templates/treasury/invoice_form.html:46
msgid "Submit"
msgstr ""
#: apps/treasury/forms.py:86
#: apps/treasury/forms.py:87
msgid "Close"
msgstr ""
#: apps/treasury/forms.py:95
#: apps/treasury/forms.py:96
msgid "Remittance is already closed."
msgstr ""
#: apps/treasury/forms.py:100
#: apps/treasury/forms.py:101
msgid "You can't change the type of the remittance."
msgstr ""
#: apps/treasury/forms.py:124 templates/note/transaction_form.html:98
#: apps/treasury/forms.py:125 templates/note/transaction_form.html:98
msgid "Bank"
msgstr ""
#: apps/treasury/forms.py:126 apps/treasury/tables.py:47
#: apps/treasury/forms.py:127 apps/treasury/tables.py:47
#: templates/note/transaction_form.html:128
#: templates/treasury/remittance_form.html:18
msgid "Amount"
@ -879,19 +924,23 @@ msgstr ""
msgid "Club Parent"
msgstr ""
#: templates/member/club_info.html:41
#: templates/member/club_info.html:29
msgid "days"
msgstr ""
#: templates/member/club_info.html:32
msgid "membership fee"
msgstr ""
#: templates/member/club_info.html:52
msgid "Add member"
msgstr ""
#: templates/member/club_info.html:42 templates/note/conso_form.html:121
#: templates/member/club_info.html:55 templates/note/conso_form.html:121
msgid "Edit"
msgstr ""
#: templates/member/club_info.html:43
msgid "Add roles"
msgstr ""
#: templates/member/club_info.html:46 templates/member/profile_info.html:48
#: templates/member/club_info.html:59 templates/member/profile_info.html:48
msgid "View Profile"
msgstr ""
@ -900,7 +949,7 @@ msgid "search clubs"
msgstr ""
#: templates/member/club_list.html:12
msgid "Créer un club"
msgid "Create club"
msgstr ""
#: templates/member/club_list.html:19

View File

@ -3,7 +3,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-03-30 17:31+0200\n"
"POT-Creation-Date: 2020-04-01 18:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -20,7 +20,8 @@ msgstr "activité"
#: apps/activity/forms.py:45 apps/activity/models.py:217
msgid "You can't invite someone once the activity is started."
msgstr "Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
msgstr ""
"Vous ne pouvez pas inviter quelqu'un une fois que l'activité a démarré."
#: apps/activity/forms.py:48 apps/activity/models.py:220
msgid "This activity is not validated yet."
@ -39,9 +40,9 @@ msgid "You can't invite more than 3 people to this activity."
msgstr "Vous ne pouvez pas inviter plus de 3 personnes à cette activité."
#: apps/activity/models.py:23 apps/activity/models.py:48
#: apps/member/models.py:64 apps/member/models.py:122
#: apps/member/models.py:66 apps/member/models.py:169
#: apps/note/models/notes.py:188 apps/note/models/transactions.py:24
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:231
#: apps/note/models/transactions.py:44 apps/note/models/transactions.py:232
#: templates/member/club_info.html:13 templates/member/profile_info.html:14
msgid "name"
msgstr "nom"
@ -63,7 +64,7 @@ msgid "activity types"
msgstr "types d'activité"
#: apps/activity/models.py:53 apps/note/models/transactions.py:69
#: apps/permission/models.py:90 templates/activity/activity_detail.html:16
#: apps/permission/models.py:103 templates/activity/activity_detail.html:16
msgid "description"
msgstr "description"
@ -73,7 +74,7 @@ msgstr "description"
msgid "type"
msgstr "type"
#: apps/activity/models.py:66 apps/logs/models.py:21
#: apps/activity/models.py:66 apps/logs/models.py:21 apps/member/models.py:190
#: apps/note/models/notes.py:117
msgid "user"
msgstr "utilisateur"
@ -164,11 +165,11 @@ msgstr "supprimer"
msgid "Type"
msgstr "Type"
#: apps/activity/tables.py:77 apps/treasury/forms.py:120
#: apps/activity/tables.py:77 apps/treasury/forms.py:121
msgid "Last name"
msgstr "Nom de famille"
#: apps/activity/tables.py:79 apps/treasury/forms.py:122
#: apps/activity/tables.py:79 apps/treasury/forms.py:123
#: templates/note/transaction_form.html:92
msgid "First name"
msgstr "Prénom"
@ -181,11 +182,11 @@ msgstr "Note"
msgid "Balance"
msgstr "Solde du compte"
#: apps/activity/views.py:44 templates/base.html:94
#: apps/activity/views.py:45 templates/base.html:94
msgid "Activities"
msgstr "Activités"
#: apps/activity/views.py:149
#: apps/activity/views.py:153
msgid "Entry for activity \"{}\""
msgstr "Entrées pour l'activité « {} »"
@ -246,65 +247,77 @@ msgstr "Les logs ne peuvent pas être détruits."
msgid "member"
msgstr "adhérent"
#: apps/member/models.py:26
#: apps/member/models.py:28
msgid "phone number"
msgstr "numéro de téléphone"
#: apps/member/models.py:32 templates/member/profile_info.html:27
#: apps/member/models.py:34 templates/member/profile_info.html:27
msgid "section"
msgstr "section"
#: apps/member/models.py:33
#: apps/member/models.py:35
msgid "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
msgstr "e.g. \"1A0\", \"9A♥\", \"SAPHIRE\""
#: apps/member/models.py:39 templates/member/profile_info.html:30
#: apps/member/models.py:41 templates/member/profile_info.html:30
msgid "address"
msgstr "adresse"
#: apps/member/models.py:45
#: apps/member/models.py:47
msgid "paid"
msgstr "payé"
#: apps/member/models.py:50 apps/member/models.py:51
#: apps/member/models.py:52 apps/member/models.py:53
msgid "user profile"
msgstr "profil utilisateur"
#: apps/member/models.py:69 templates/member/club_info.html:36
#: apps/member/models.py:71 templates/member/club_info.html:46
msgid "email"
msgstr "courriel"
#: apps/member/models.py:76
#: apps/member/models.py:78
msgid "parent club"
msgstr "club parent"
#: apps/member/models.py:81 templates/member/club_info.html:30
msgid "membership fee"
msgstr "cotisation pour adhérer"
#: apps/member/models.py:87
msgid "require memberships"
msgstr "nécessite des adhésions"
#: apps/member/models.py:85 templates/member/club_info.html:27
#: apps/member/models.py:88
msgid "Uncheck if this club don't require memberships."
msgstr "Décochez si ce club n'utilise pas d'adhésions."
#: apps/member/models.py:93 templates/member/club_info.html:35
msgid "membership fee (paid students)"
msgstr "cotisation pour adhérer (normalien élève)"
#: apps/member/models.py:98 templates/member/club_info.html:38
msgid "membership fee (unpaid students)"
msgstr "cotisation pour adhérer (normalien étudiant)"
#: apps/member/models.py:104 templates/member/club_info.html:28
msgid "membership duration"
msgstr "durée de l'adhésion"
#: apps/member/models.py:86
msgid "The longest time a membership can last (NULL = infinite)."
msgstr "La durée maximale d'une adhésion (NULL = infinie)."
#: apps/member/models.py:105
msgid "The longest time (in days) a membership can last (NULL = infinite)."
msgstr "La durée maximale (en jours) d'une adhésion (NULL = infinie)."
#: apps/member/models.py:91 templates/member/club_info.html:21
#: apps/member/models.py:112 templates/member/club_info.html:22
msgid "membership start"
msgstr "début de l'adhésion"
#: apps/member/models.py:92
#: apps/member/models.py:113
msgid "How long after January 1st the members can renew their membership."
msgstr ""
"Combien de temps après le 1er Janvier les adhérents peuvent renouveler leur "
"adhésion."
#: apps/member/models.py:97 templates/member/club_info.html:24
#: apps/member/models.py:120 templates/member/club_info.html:25
msgid "membership end"
msgstr "fin de l'adhésion"
#: apps/member/models.py:98
#: apps/member/models.py:121
msgid ""
"How long the membership can last after January 1st of the next year after "
"members can renew their membership."
@ -312,59 +325,91 @@ msgstr ""
"Combien de temps l'adhésion peut durer après le 1er Janvier de l'année "
"suivante avant que les adhérents peuvent renouveler leur adhésion."
#: apps/member/models.py:104 apps/note/models/notes.py:139
#: apps/member/models.py:154 apps/member/models.py:196
#: apps/note/models/notes.py:139
msgid "club"
msgstr "club"
#: apps/member/models.py:105
#: apps/member/models.py:155
msgid "clubs"
msgstr "clubs"
#: apps/member/models.py:128 apps/permission/models.py:275
#: apps/member/models.py:175 apps/permission/models.py:288
msgid "role"
msgstr "rôle"
#: apps/member/models.py:129
#: apps/member/models.py:176 apps/member/models.py:201
msgid "roles"
msgstr "rôles"
#: apps/member/models.py:153
#: apps/member/models.py:205
msgid "membership starts on"
msgstr "l'adhésion commence le"
#: apps/member/models.py:156
#: apps/member/models.py:209
msgid "membership ends on"
msgstr "l'adhésion finie le"
msgstr "l'adhésion finit le"
#: apps/member/models.py:160
#: apps/member/models.py:214
msgid "fee"
msgstr "cotisation"
#: apps/member/models.py:172
#: apps/member/models.py:226 apps/member/views.py:383
msgid "User is not a member of the parent club"
msgstr "L'utilisateur n'est pas membre du club parent"
#: apps/member/models.py:176
#: apps/member/models.py:236 apps/member/views.py:392
msgid "User is already a member of the club"
msgstr "L'utilisateur est déjà membre du club"
#: apps/member/models.py:271
#, python-brace-format
msgid "Membership of {user} for the club {club}"
msgstr "Adhésion de {user} pour le club {club}"
#: apps/member/models.py:274
msgid "membership"
msgstr "adhésion"
#: apps/member/models.py:177
#: apps/member/models.py:275
msgid "memberships"
msgstr "adhésions"
#: apps/member/views.py:76 templates/member/profile_info.html:45
#: apps/member/tables.py:73
msgid "Renew"
msgstr ""
#: apps/member/views.py:80 templates/member/profile_info.html:45
msgid "Update Profile"
msgstr "Modifier le profil"
#: apps/member/views.py:89
#: apps/member/views.py:93
msgid "An alias with a similar name already exists."
msgstr "Un alias avec un nom similaire existe déjà."
#: apps/member/views.py:379
msgid ""
"This user don't have enough money to join this club, and can't have a "
"negative balance."
msgstr ""
#: apps/member/views.py:396 apps/member/views.py:428
msgid "The membership must start after {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer après le {:%d/%m/%Y}."
#: apps/member/views.py:401 apps/member/views.py:433
msgid "The membership must begin before {:%m-%d-%Y}."
msgstr "L'adhésion doit commencer avant le {:%d/%m/%Y}."
#: apps/member/views.py:455
msgid "This membership is already renewed"
msgstr "Cette adhésion est déjà renouvelée"
#: apps/note/admin.py:120 apps/note/models/transactions.py:94
msgid "source"
msgstr "source"
#: apps/note/admin.py:128 apps/note/admin.py:156
#: apps/note/admin.py:128 apps/note/admin.py:163
#: apps/note/models/transactions.py:53 apps/note/models/transactions.py:107
msgid "destination"
msgstr "destination"
@ -462,7 +507,7 @@ msgstr "Alias invalide"
msgid "alias"
msgstr "alias"
#: apps/note/models/notes.py:211 templates/member/club_info.html:33
#: apps/note/models/notes.py:211 templates/member/club_info.html:43
#: templates/member/profile_info.html:36
msgid "aliases"
msgstr "alias"
@ -524,45 +569,45 @@ msgstr "raison"
msgid "invalidity reason"
msgstr "Motif d'invalidité"
#: apps/note/models/transactions.py:146
#: apps/note/models/transactions.py:147
msgid "transaction"
msgstr "transaction"
#: apps/note/models/transactions.py:147
#: apps/note/models/transactions.py:148
msgid "transactions"
msgstr "transactions"
#: apps/note/models/transactions.py:201 templates/base.html:84
#: apps/note/models/transactions.py:202 templates/base.html:84
#: templates/note/transaction_form.html:19
#: templates/note/transaction_form.html:140
msgid "Transfer"
msgstr "Virement"
#: apps/note/models/transactions.py:221
#: apps/note/models/transactions.py:222
msgid "Template"
msgstr "Bouton"
#: apps/note/models/transactions.py:236
#: apps/note/models/transactions.py:237
msgid "first_name"
msgstr "prénom"
#: apps/note/models/transactions.py:241
#: apps/note/models/transactions.py:242
msgid "bank"
msgstr "banque"
#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:24
#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:24
msgid "Credit"
msgstr "Crédit"
#: apps/note/models/transactions.py:247 templates/note/transaction_form.html:28
#: apps/note/models/transactions.py:248 templates/note/transaction_form.html:28
msgid "Debit"
msgstr "Débit"
#: apps/note/models/transactions.py:263 apps/note/models/transactions.py:268
#: apps/note/models/transactions.py:264 apps/note/models/transactions.py:269
msgid "membership transaction"
msgstr "transaction d'adhésion"
#: apps/note/models/transactions.py:264
#: apps/note/models/transactions.py:265
msgid "membership transactions"
msgstr "transactions d'adhésion"
@ -578,29 +623,29 @@ msgstr "Cliquez pour valider"
msgid "No reason specified"
msgstr "Pas de motif spécifié"
#: apps/note/views.py:41
#: apps/note/views.py:39
msgid "Transfer money"
msgstr "Transférer de l'argent"
#: apps/note/views.py:102 templates/base.html:79
#: apps/note/views.py:100 templates/base.html:79
msgid "Consumptions"
msgstr "Consommations"
#: apps/permission/models.py:69 apps/permission/models.py:262
#: apps/permission/models.py:82 apps/permission/models.py:275
#, python-brace-format
msgid "Can {type} {model}.{field} in {query}"
msgstr ""
#: apps/permission/models.py:71 apps/permission/models.py:264
#: apps/permission/models.py:84 apps/permission/models.py:277
#, python-brace-format
msgid "Can {type} {model} in {query}"
msgstr ""
#: apps/permission/models.py:84
#: apps/permission/models.py:97
msgid "rank"
msgstr "Rang"
#: apps/permission/models.py:147
#: apps/permission/models.py:160
msgid "Specifying field applies only to view and change permission types."
msgstr ""
@ -608,31 +653,32 @@ msgstr ""
msgid "Treasury"
msgstr "Trésorerie"
#: apps/treasury/forms.py:84 apps/treasury/forms.py:132
#: apps/treasury/forms.py:85 apps/treasury/forms.py:133
#: templates/activity/activity_form.html:9
#: templates/activity/activity_invite.html:8
#: templates/django_filters/rest_framework/form.html:5
#: templates/member/club_form.html:9 templates/treasury/invoice_form.html:46
#: templates/member/add_members.html:14 templates/member/club_form.html:9
#: templates/treasury/invoice_form.html:46
msgid "Submit"
msgstr "Envoyer"
#: apps/treasury/forms.py:86
#: apps/treasury/forms.py:87
msgid "Close"
msgstr "Fermer"
#: apps/treasury/forms.py:95
#: apps/treasury/forms.py:96
msgid "Remittance is already closed."
msgstr "La remise est déjà fermée."
#: apps/treasury/forms.py:100
#: apps/treasury/forms.py:101
msgid "You can't change the type of the remittance."
msgstr "Vous ne pouvez pas changer le type de la remise."
#: apps/treasury/forms.py:124 templates/note/transaction_form.html:98
#: apps/treasury/forms.py:125 templates/note/transaction_form.html:98
msgid "Bank"
msgstr "Banque"
#: apps/treasury/forms.py:126 apps/treasury/tables.py:47
#: apps/treasury/forms.py:127 apps/treasury/tables.py:47
#: templates/note/transaction_form.html:128
#: templates/treasury/remittance_form.html:18
msgid "Amount"
@ -881,19 +927,23 @@ msgstr "Ajouter un alias"
msgid "Club Parent"
msgstr "Club parent"
#: templates/member/club_info.html:41
#: templates/member/club_info.html:29
msgid "days"
msgstr "jours"
#: templates/member/club_info.html:32
msgid "membership fee"
msgstr "cotisation pour adhérer"
#: templates/member/club_info.html:52
msgid "Add member"
msgstr "Ajouter un membre"
#: templates/member/club_info.html:42 templates/note/conso_form.html:121
#: templates/member/club_info.html:55 templates/note/conso_form.html:121
msgid "Edit"
msgstr "Éditer"
#: templates/member/club_info.html:43
msgid "Add roles"
msgstr "Ajouter des rôles"
#: templates/member/club_info.html:46 templates/member/profile_info.html:48
#: templates/member/club_info.html:59 templates/member/profile_info.html:48
msgid "View Profile"
msgstr "Voir le profil"
@ -902,8 +952,8 @@ msgid "search clubs"
msgstr "Chercher un club"
#: templates/member/club_list.html:12
msgid "Créer un club"
msgstr ""
msgid "Create club"
msgstr "Créer un club"
#: templates/member/club_list.html:19
msgid "club listing "

View File

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

View File

@ -70,7 +70,7 @@ function refreshBalance() {
* @param fun For each found note with the matched alias `alias`, fun(note, alias) is called.
*/
function getMatchedNotes(pattern, fun) {
$.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club&ordering=normalized_name", fun);
$.getJSON("/api/note/alias/?format=json&alias=" + pattern + "&search=user|club|activity&ordering=normalized_name", fun);
}
/**

View File

@ -25,7 +25,7 @@
<dt class="col-xl-6">{% trans 'end date'|capfirst %}</dt>
<dd class="col-xl-6">{{ activity.date_end }}</dd>
{% if "view_"|has_perm:activity.creater %}
{% if ".view_"|has_perm:activity.creater %}
<dt class="col-xl-6">{% trans 'creater'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "member:user_detail" pk=activity.creater.pk %}">{{ activity.creater }}</a></dd>
{% endif %}
@ -53,17 +53,17 @@
</div>
<div class="card-footer text-center">
{% if activity.open and "change__open"|has_perm:activity %}
{% if activity.open and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" href="{% url 'activity:activity_entry' pk=activity.pk %}"> {% trans "Entry page" %}</a>
{% endif %}
{% if activity.valid and "change__open"|has_perm:activity %}
{% if activity.valid and ".change__open"|has_perm:activity %}
<a class="btn btn-warning btn-sm my-1" id="open_activity"> {% if activity.open %}{% trans "close"|capfirst %}{% else %}{% trans "open"|capfirst %}{% endif %}</a>
{% endif %}
{% if not activity.open and "change__valid"|has_perm:activity %}
{% if not activity.open and ".change__valid"|has_perm:activity %}
<a class="btn btn-success btn-sm my-1" id="validate_activity"> {% if activity.valid %}{% trans "invalidate"|capfirst %}{% else %}{% trans "validate"|capfirst %}{% endif %}</a>
{% endif %}
{% if "view_"|has_perm:activity %}
{% if ".view_"|has_perm:activity %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'activity:activity_update' pk=activity.pk %}"> {% trans "edit"|capfirst %}</a>
{% endif %}
{% if activity.activity_type.can_invite and not activity_started %}

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>
</li>
{% endif %}
{% if "auth.user"|model_list|length >= 2 %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'member:user_list' %}"><i class="fa fa-user"></i> {% trans 'Users' %}</a>
</li>
{% endif %}
{% if "member.club"|not_empty_model_list %}
<li class="nav-item active">
<a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a>

View File

@ -1,29 +1,21 @@
{% extends "member/noteowner_detail.html" %}
{% load crispy_forms_tags %}
{% load static %}
{% load i18n %}
{% block profile_info %}
{% include "member/club_info.html" %}
{% endblock %}
{% block profile_content %}
{% block profile_content %}
<form method="post" action="">
{% csrf_token %}
{% crispy formset helper %}
<div class="form-actions">
<input type="submit" name="submit" value="Add Members" class="btn btn-primary" id="submit-save">
</div>
{{ form|crispy }}
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}
{% block extrajavascript %}
<script src="{% static 'js/dynamic-formset.js' %}"></script>
<script>
$('.formset-row').formset({
addText: 'add another', // Text for the add link
deleteText: 'remove', // Text for the delete link
addCssClass: 'btn btn-primary', // CSS class applied to the add link
deleteCssClass: 'btn btn-danger h-50 my-auto',
});
</script>
{% endblock %}

View File

@ -7,3 +7,12 @@
{% block profile_content %}
{% include "member/club_tables.html" %}
{% endblock %}
{% block extrajavascript %}
<script>
function refreshHistory() {
$("#history_list").load("{% url 'member:club_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:club_detail' pk=object.pk %} #profile_infos");
}
</script>
{% endblock %}

View File

@ -9,3 +9,25 @@
<button class="btn btn-primary" type="submit">{% trans "Submit" %}</button>
</form>
{% endblock %}
{% block extrajavascript %}
<script>
require_memberships_obj = $("#id_require_memberships");
if (!require_memberships_obj.is(":checked")) {
$("#div_id_membership_fee_paid").toggle();
$("#div_id_membership_fee_unpaid").toggle();
$("#div_id_membership_duration").toggle();
$("#div_id_membership_start").toggle();
$("#div_id_membership_end").toggle();
}
require_memberships_obj.change(function () {
$("#div_id_membership_fee_paid").toggle();
$("#div_id_membership_fee_unpaid").toggle();
$("#div_id_membership_duration").toggle();
$("#div_id_membership_start").toggle();
$("#div_id_membership_end").toggle();
});
</script>
{% endblock %}

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-header text-center">
<h4> Club {{ club.name }} </h4>
@ -18,31 +18,44 @@
<dd class="col-xl-6"> {{ club.parent_club.name}}</dd>
{% endif %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd>
{% if club.require_memberships %}
<dt class="col-xl-6">{% trans 'membership start'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_start }}</dd>
<dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_end }}</dd>
<dt class="col-xl-6">{% trans 'membership end'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_end }}</dd>
<dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_duration }}</dd>
<dt class="col-xl-6">{% trans 'membership duration'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_duration }} {% trans "days" %}</dd>
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee|pretty_money }}</dd>
{% if club.membership_fee_paid == club.membership_fee_unpaid %}
<dt class="col-xl-6">{% trans 'membership fee'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
{% else %}
<dt class="col-xl-6">{% trans 'membership fee (paid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_paid|pretty_money }}</dd>
<dt class="col-xl-6">{% trans 'membership fee (unpaid students)'|capfirst %}</dt>
<dd class="col-xl-6">{{ club.membership_fee_unpaid|pretty_money }}</dd>
{% endif %}
{% endif %}
<dt class="col-xl-6"><a href="{% url 'member:club_alias' club.pk %}">{% trans 'aliases'|capfirst %}</a></dt>
<dd class="col-xl-6 text-truncate">{{ object.note.alias_set.all|join:", " }}</dd>
<dt class="col-xl-3">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email}}</a></dd>
<dd class="col-xl-9"><a href="mailto:{{ club.email }}">{{ club.email }}</a></dd>
</dl>
</div>
<div class="card-footer text-center">
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a>
{% if can_add_members %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a>
{% endif %}
{% if ".change_"|has_perm:club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
{% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %}
{%if request.get_full_path != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ user_profile_url }}">{% trans 'View Profile' %}</a>
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>
{% endif %} </div>
</div>

View File

@ -9,7 +9,7 @@
</h4>
<input class="form-control mx-auto w-25" type="text" onkeyup="search_field_moved();return(false);" id="search_field"/>
<hr>
<a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Créer un club" %}</a>
<a class="btn btn-primary text-center my-4" href="{% url 'member:club_create' %}">{% trans "Create club" %}</a>
</div>
</div>
<div class="row justify-content-center">

View File

@ -19,7 +19,7 @@
{% block extrajavascript %}
<script>
function refreshhistory() {
function refreshHistory() {
$("#history_list").load("{% url 'member:user_detail' pk=object.pk %} #history_list");
$("#profile_infos").load("{% url 'member:user_detail' pk=object.pk %} #profile_infos");
}

View File

@ -2,28 +2,44 @@
{% load render_table from django_tables2 %}
{% load crispy_forms_tags%}
{% block content %}
<input id="searchbar" type="text" class="form-control" placeholder="Nom/prénom/note/section ...">
<a class="btn btn-primary" href="{% url 'member:signup' %}">New User</a>
<hr>
<div class="row">
{% crispy filter.form filter.form.helper %}
</div>
<div class="row">
<div id="replaceable-content" class="col-6">
{% render_table table %}
<div id="user_table">
{% render_table table %}
</div>
</div>
{% endblock %}
{% block extrajavascript %}
<script type="text/javascript">
$(document).ready(function() {
let old_pattern = null;
let searchbar_obj = $("#searchbar");
$(document).ready(function($) {
$(".table-row").click(function() {
window.document.location = $(this).data("href");
function reloadTable() {
let pattern = searchbar_obj.val();
if (pattern === old_pattern || pattern === "")
return;
$("#user_table").load(location.href + "?search=" + pattern.replace(" ", "%20") + " #user_table", init);
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
}
searchbar_obj.keyup(reloadTable);
function init() {
$(".table-row").click(function() {
window.document.location = $(this).data("href");
});
}
init();
});
});
</script>
{% endblock %}