diff --git a/apps/logs/signals.py b/apps/logs/signals.py index 43fc1e13a92d23e746fbb5cab92bbbdaa6d70cf4..37e9ba79dc3176db2b529b799adeb5b457791014 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 094d2c3f9306f4eed438ec9607d9755151842874..d72377f8afb514df348bd0548ab7ad61bab3fcbc 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 9ac4ffa99810f9e3548107dac363d0ac16766a7a..87d5322e793fffede7dee092b7ff8d7de93e92d2 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 8906af9cd790f6aca30d6a173447c992611d688e..180839d8d950d00162e8fd28d02a9c37527c928d 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 d0c37a6e2e47baaa3e4b2f3eef1eebe152fdc5d1..18a2a555701fd69a06ca68503cbdb3f571aa49fc 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("<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 diff --git a/apps/member/urls.py b/apps/member/urls.py index 7f4bf0d51de37a5b62987c66fc56c0ab9c37f108..2000a6e4f47f531770d278087a2c53dc315d5ddd 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/<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/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"), 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('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'), ] diff --git a/apps/member/views.py b/apps/member/views.py index 651dfc35298d66519649ee550d70c4607f1f64a8..ad01e2a2bbdf2884a42839642acd5e1d5fff7895 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 702d3350e7933ac7141b80bdd1ab884ce515e1b7..7b4ba870c64331eb10501ba13ef68df493418127 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 d1dcd7887eb62c36f7235e90d0b12454c80ce58e..d9b860ae823486c41c3c9e49d8da44ccfde12b53 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 f478cd5dcc4f5deaf298c369e25078c98838e08d..b95f8203ab7c3c14dadb45434800920491bc09df 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 d1b55090f04b82af44e7dc80258f50440b3694d0..c8df07c0cfa9f75539edc9414fdbde590380a06e 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 1e30f56f5793296e929b195231bb4a4eb4660d26..5ccb9c0bbeb897f535bf3ba9d2b5a64beae8e0e6 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 81f55deead00755acd5938547939afd3c8e3ed5a..ecd758e058cfa995f54974bec65e12503a80660b 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 da747ccd88e4866b6941d837b39a69d7c2057ee5..23a41c1c3ab4c59b3656465ab17e5aa410976162 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 %} <div class="card bg-light shadow"> <div class="card-header text-center"> <h4> Club {{ club.name }} </h4> @@ -40,9 +40,12 @@ </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="{{ club_detail_url }}">{% trans 'View Profile' %}</a>