diff --git a/apps/logs/signals.py b/apps/logs/signals.py
index 43fc1e13..37e9ba79 100644
--- a/apps/logs/signals.py
+++ b/apps/logs/signals.py
@@ -50,6 +50,9 @@ def save_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED:
return
+ if hasattr(instance, "_force_save"):
+ 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, "_force_delete"):
+ return
+
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
user, ip = get_current_authenticated_user(), get_current_ip()
diff --git a/apps/member/fixtures/initial.json b/apps/member/fixtures/initial.json
index 094d2c3f..d72377f8 100644
--- a/apps/member/fixtures/initial.json
+++ b/apps/member/fixtures/initial.json
@@ -18,6 +18,7 @@
"fields": {
"name": "Kfet",
"email": "tresorerie.bde@example.com",
+ "parent_club": 1,
"require_memberships": true,
"membership_fee": 3500,
"membership_duration": 396,
diff --git a/apps/member/forms.py b/apps/member/forms.py
index 9ac4ffa9..87d5322e 100644
--- a/apps/member/forms.py
+++ b/apps/member/forms.py
@@ -4,6 +4,7 @@
from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User
+from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask
@@ -57,11 +58,6 @@ class ClubForm(forms.ModelForm):
}
-class AddMembersForm(forms.Form):
- class Meta:
- fields = ('',)
-
-
class MembershipForm(forms.ModelForm):
class Meta:
model = Membership
diff --git a/apps/member/models.py b/apps/member/models.py
index 8906af9c..180839d8 100644
--- a/apps/member/models.py
+++ b/apps/member/models.py
@@ -4,11 +4,14 @@
import datetime
from django.conf import settings
-from django.core.exceptions import ValidationError
+from django.contrib.auth.models import User
+from django.core.exceptions import ValidationError, PermissionDenied
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):
"""
@@ -91,7 +94,7 @@ class Club(models.Model):
verbose_name=_('membership fee'),
)
- membership_duration = models.IntegerField(
+ membership_duration = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name=_('membership duration'),
@@ -174,7 +177,7 @@ class Membership(models.Model):
"""
user = models.ForeignKey(
- settings.AUTH_USER_MODEL,
+ User,
on_delete=models.PROTECT,
)
@@ -185,6 +188,7 @@ class Membership(models.Model):
roles = models.ManyToManyField(
Role,
+ verbose_name=_("roles"),
)
date_start = models.DateField(
@@ -209,17 +213,41 @@ 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).exists():
- raise ValidationError(_('User is not a member of the parent club'))
+ raise ValidationError(_('User is not a member of the parent club') + ' ' + self.club.parent_club.name)
created = not self.pk
if created:
+ if Membership.objects.filter(
+ user=self.user,
+ club=self.club,
+ date_start__lte=datetime.datetime.now().date(),
+ date_end__gte=datetime.datetime.now().date(),
+ ).exists():
+ raise ValidationError(_('User is already a member of the club'))
+
self.fee = self.club.membership_fee
- self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration)
- if self.date_end > self.club.membership_end:
+ 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=0x7FFFFFFF)
+ 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)
+ if created and self.fee:
+ try:
+ MembershipTransaction.objects.create(
+ membership=self,
+ source=self.user.note,
+ destination=self.club.note,
+ quantity=1,
+ amount=self.fee,
+ reason="Adhésion",
+ )
+ except PermissionDenied:
+ self.delete()
+
class Meta:
verbose_name = _('membership')
verbose_name_plural = _('memberships')
diff --git a/apps/member/tables.py b/apps/member/tables.py
index d0c37a6e..18a2a555 100644
--- a/apps/member/tables.py
+++ b/apps/member/tables.py
@@ -3,8 +3,14 @@
import django_tables2 as tables
from django.contrib.auth.models import User
+from django.urls import reverse_lazy
+from django.utils.html import format_html
+from django_tables2 import A
-from .models import Club
+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, Membership
class ClubTable(tables.Table):
@@ -33,3 +39,33 @@ class UserTable(tables.Table):
template_name = 'django_tables2/bootstrap4.html'
fields = ('last_name', 'first_name', 'username', 'email')
model = User
+
+
+class MembershipTable(tables.Table):
+ roles = tables.Column(
+ attrs={
+ "td": {
+ "class": "text-truncate",
+ }
+ }
+ )
+
+ def render_fee(self, value):
+ return pretty_money(value)
+
+ def render_roles(self, record):
+ roles = record.roles.all()
+ s = ", ".join(str(role) for role in roles)
+ if PermissionBackend().has_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
+ s = format_html("" + s + "")
+ return s
+
+ class Meta:
+ attrs = {
+ 'class': 'table table-condensed table-striped table-hover',
+ 'style': 'table-layout: fixed;'
+ }
+ template_name = 'django_tables2/bootstrap4.html'
+ fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
+ model = Membership
diff --git a/apps/member/urls.py b/apps/member/urls.py
index 7f4bf0d5..2000a6e4 100644
--- a/apps/member/urls.py
+++ b/apps/member/urls.py
@@ -12,15 +12,16 @@ urlpatterns = [
path('club/', views.ClubListView.as_view(), name="club_list"),
path('club//', views.ClubDetailView.as_view(), name="club_detail"),
path('club//add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
+ path('club/manage_roles//', views.ClubManageRolesView.as_view(), name="club_manage_roles"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
path('club//update/', views.ClubUpdateView.as_view(), name="club_update"),
path('club//update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
path('club//aliases/', views.ClubAliasView.as_view(), name="club_alias"),
path('user/', views.UserListView.as_view(), name="user_list"),
- path('user/', views.UserDetailView.as_view(), name="user_detail"),
- path('user//update', views.UserUpdateView.as_view(), name="user_update_profile"),
- path('user//update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
- path('user//aliases', views.ProfileAliasView.as_view(), name="user_alias"),
+ path('user//', views.UserDetailView.as_view(), name="user_detail"),
+ path('user//update/', views.UserUpdateView.as_view(), name="user_update_profile"),
+ path('user//update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
+ path('user//aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
]
diff --git a/apps/member/views.py b/apps/member/views.py
index 651dfc35..ad01e2a2 100644
--- a/apps/member/views.py
+++ b/apps/member/views.py
@@ -10,6 +10,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView
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 _
@@ -27,7 +28,7 @@ from permission.views import ProtectQuerysetMixin
from .filters import UserFilter, UserFilterFormHelper
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):
@@ -138,7 +139,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context['history_list'] = HistoryTable(history_list)
club_list = Membership.objects.all().filter(user=user)\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).only("club")
- context['club_list'] = ClubTable(club_list)
+ context['club_list'] = MembershipTable(data=club_list)
return context
@@ -294,7 +295,19 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
date_start__lte=datetime.now().date(),
date_end__gte=datetime.now().date(),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).all()
- context['member_list'] = club_member
+
+ 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
@@ -339,7 +352,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
.get(pk=self.kwargs["pk"])
context = super().get_context_data(**kwargs)
context['club'] = club
- context['no_cache'] = True
return context
@@ -347,6 +359,63 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["pk"])
form.instance.club = club
+
+ 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=datetime.now().date(),
+ date_end__gte=datetime.now().date(),
+ ).exists():
+ form.add_error('user', _('User is already a member of the club'))
+ return super().form_invalid(form)
+
+ if 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.date_start > form.instance.club.membership_end:
+ form.add_error('user', _("The membership must end 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.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.date_start > form.instance.club.membership_end:
+ form.add_error('user', _("The membership must end 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):
diff --git a/apps/note/admin.py b/apps/note/admin.py
index 702d3350..7b4ba870 100644
--- a/apps/note/admin.py
+++ b/apps/note/admin.py
@@ -138,6 +138,13 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
return []
+@admin.register(MembershipTransaction)
+class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
+ """
+ Admin customisation for Transaction
+ """
+
+
@admin.register(TransactionTemplate)
class TransactionTemplateAdmin(admin.ModelAdmin):
"""
diff --git a/apps/note/models/transactions.py b/apps/note/models/transactions.py
index d1dcd788..d9b860ae 100644
--- a/apps/note/models/transactions.py
+++ b/apps/note/models/transactions.py
@@ -140,6 +140,7 @@ class Transaction(PolymorphicModel):
max_length=255,
default=None,
null=True,
+ blank=True,
)
class Meta:
diff --git a/apps/permission/backends.py b/apps/permission/backends.py
index f478cd5d..b95f8203 100644
--- a/apps/permission/backends.py
+++ b/apps/permission/backends.py
@@ -1,6 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
+import datetime
+
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User, AnonymousUser
from django.contrib.contenttypes.models import ContentType
@@ -32,7 +34,8 @@ class PermissionBackend(ModelBackend):
for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
.filter(
rolepermissions__role__membership__user=user,
- rolepermissions__role__membership__valid=True,
+ rolepermissions__role__membership__date_start__lte=datetime.date.today(),
+ rolepermissions__role__membership__date_end__gte=datetime.date.today(),
model__app_label=model.app_label, # For polymorphic models, we don't filter on model type
type=type,
).all():
diff --git a/apps/permission/models.py b/apps/permission/models.py
index d1b55090..c8df07c0 100644
--- a/apps/permission/models.py
+++ b/apps/permission/models.py
@@ -45,11 +45,13 @@ class InstancedPermission:
else:
oldpk = obj.pk
# Ensure previous models are deleted
- self.model.model_class().objects.filter(pk=obj.pk).delete()
+ self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk") + 1).delete()
# Force insertion, no data verification, no trigger
+ obj._force_save = True
Model.save(obj, force_insert=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
diff --git a/apps/permission/signals.py b/apps/permission/signals.py
index 1e30f56f..5ccb9c0b 100644
--- a/apps/permission/signals.py
+++ b/apps/permission/signals.py
@@ -29,6 +29,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
@@ -58,32 +61,14 @@ def pre_save_object(sender, instance, **kwargs):
if not PermissionBackend().has_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)
-
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 +76,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
diff --git a/note_kfet/inputs.py b/note_kfet/inputs.py
index 81f55dee..ecd758e0 100644
--- a/note_kfet/inputs.py
+++ b/note_kfet/inputs.py
@@ -1,7 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
-import datetime
from json import dumps as json_dumps
from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput
diff --git a/templates/member/club_info.html b/templates/member/club_info.html
index da747ccd..23a41c1c 100644
--- a/templates/member/club_info.html
+++ b/templates/member/club_info.html
@@ -1,4 +1,4 @@
-{% load i18n static pretty_money %}
+{% load i18n static pretty_money perms %}