1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-11-26 18:37:12 +00:00

Full membership support

This commit is contained in:
Yohann D'ANELLO 2020-04-01 03:42:19 +02:00
parent bf9789bd9e
commit d5b010980b
14 changed files with 186 additions and 46 deletions

View File

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

View File

@ -18,6 +18,7 @@
"fields": { "fields": {
"name": "Kfet", "name": "Kfet",
"email": "tresorerie.bde@example.com", "email": "tresorerie.bde@example.com",
"parent_club": 1,
"require_memberships": true, "require_memberships": true,
"membership_fee": 3500, "membership_fee": 3500,
"membership_duration": 396, "membership_duration": 396,

View File

@ -4,6 +4,7 @@
from django import forms from django import forms
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.utils.translation import gettext_lazy as _
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
from permission.models import PermissionMask 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 MembershipForm(forms.ModelForm):
class Meta: class Meta:
model = Membership model = Membership

View File

@ -4,11 +4,14 @@
import datetime import datetime
from django.conf import settings 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.db import models
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from note.models import MembershipTransaction
class Profile(models.Model): class Profile(models.Model):
""" """
@ -91,7 +94,7 @@ class Club(models.Model):
verbose_name=_('membership fee'), verbose_name=_('membership fee'),
) )
membership_duration = models.IntegerField( membership_duration = models.PositiveIntegerField(
blank=True, blank=True,
null=True, null=True,
verbose_name=_('membership duration'), verbose_name=_('membership duration'),
@ -174,7 +177,7 @@ class Membership(models.Model):
""" """
user = models.ForeignKey( user = models.ForeignKey(
settings.AUTH_USER_MODEL, User,
on_delete=models.PROTECT, on_delete=models.PROTECT,
) )
@ -185,6 +188,7 @@ class Membership(models.Model):
roles = models.ManyToManyField( roles = models.ManyToManyField(
Role, Role,
verbose_name=_("roles"),
) )
date_start = models.DateField( date_start = models.DateField(
@ -209,17 +213,41 @@ class Membership(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.club.parent_club is not None: if self.club.parent_club is not None:
if not Membership.objects.filter(user=self.user, club=self.club.parent_club).exists(): 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 created = not self.pk
if created: 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.fee = self.club.membership_fee
if self.club.membership_duration is not None:
self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration) self.date_end = self.date_start + datetime.timedelta(days=self.club.membership_duration)
if self.date_end > self.club.membership_end: 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 self.date_end = self.club.membership_end
super().save(*args, **kwargs) 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: class Meta:
verbose_name = _('membership') verbose_name = _('membership')
verbose_name_plural = _('memberships') verbose_name_plural = _('memberships')

View File

@ -3,8 +3,14 @@
import django_tables2 as tables import django_tables2 as tables
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.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): class ClubTable(tables.Table):
@ -33,3 +39,33 @@ class UserTable(tables.Table):
template_name = 'django_tables2/bootstrap4.html' template_name = 'django_tables2/bootstrap4.html'
fields = ('last_name', 'first_name', 'username', 'email') fields = ('last_name', 'first_name', 'username', 'email')
model = User model = User
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

View File

@ -12,15 +12,16 @@ urlpatterns = [
path('club/', views.ClubListView.as_view(), name="club_list"), path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"), path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"), path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
path('club/manage_roles/<int:pk>/', views.ClubManageRolesView.as_view(), name="club_manage_roles"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"), 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/', 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>/update_pic/', views.ClubPictureUpdateView.as_view(), name="club_update_pic"),
path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"), path('club/<int:pk>/aliases/', views.ClubAliasView.as_view(), name="club_alias"),
path('user/', views.UserListView.as_view(), name="user_list"), path('user/', views.UserListView.as_view(), name="user_list"),
path('user/<int:pk>', views.UserDetailView.as_view(), name="user_detail"), path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),
path('user/<int:pk>/update', views.UserUpdateView.as_view(), name="user_update_profile"), path('user/<int:pk>/update/', views.UserUpdateView.as_view(), name="user_update_profile"),
path('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"), path('user/<int:pk>/update_pic/', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
path('user/<int:pk>/aliases', views.ProfileAliasView.as_view(), name="user_alias"), path('user/<int:pk>/aliases/', views.ProfileAliasView.as_view(), name="user_alias"),
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'), path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
] ]

View File

@ -10,6 +10,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.auth.views import LoginView from django.contrib.auth.views import LoginView
from django.db.models import Q from django.db.models import Q
from django.forms import HiddenInput
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -27,7 +28,7 @@ from permission.views import ProtectQuerysetMixin
from .filters import UserFilter, UserFilterFormHelper from .filters import UserFilter, UserFilterFormHelper
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
from .models import Club, Membership from .models import Club, Membership
from .tables import ClubTable, UserTable from .tables import ClubTable, UserTable, MembershipTable
class CustomLoginView(LoginView): class CustomLoginView(LoginView):
@ -138,7 +139,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
context['history_list'] = HistoryTable(history_list) context['history_list'] = HistoryTable(history_list)
club_list = Membership.objects.all().filter(user=user)\ club_list = Membership.objects.all().filter(user=user)\
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).only("club") .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 return context
@ -294,7 +295,19 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
date_start__lte=datetime.now().date(), date_start__lte=datetime.now().date(),
date_end__gte=datetime.now().date(), date_end__gte=datetime.now().date(),
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view")).all() ).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 return context
@ -339,7 +352,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
.get(pk=self.kwargs["pk"]) .get(pk=self.kwargs["pk"])
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
context['club'] = club context['club'] = club
context['no_cache'] = True
return context return context
@ -347,6 +359,63 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\ club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
.get(pk=self.kwargs["pk"]) .get(pk=self.kwargs["pk"])
form.instance.club = club 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) return super().form_valid(form)
def get_success_url(self): def get_success_url(self):

View File

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

View File

@ -140,6 +140,7 @@ class Transaction(PolymorphicModel):
max_length=255, max_length=255,
default=None, default=None,
null=True, null=True,
blank=True,
) )
class Meta: class Meta:

View File

@ -1,6 +1,8 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import User, AnonymousUser from django.contrib.auth.models import User, AnonymousUser
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
@ -32,7 +34,8 @@ class PermissionBackend(ModelBackend):
for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \
.filter( .filter(
rolepermissions__role__membership__user=user, 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 model__app_label=model.app_label, # For polymorphic models, we don't filter on model type
type=type, type=type,
).all(): ).all():

View File

@ -45,11 +45,13 @@ class InstancedPermission:
else: else:
oldpk = obj.pk oldpk = obj.pk
# Ensure previous models are deleted # 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 # Force insertion, no data verification, no trigger
obj._force_save = True
Model.save(obj, force_insert=True) Model.save(obj, force_insert=True)
ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists() ret = self.model.model_class().objects.filter(self.query & Q(pk=obj.pk)).exists()
# Delete testing object # Delete testing object
obj._force_delete = True
Model.delete(obj) Model.delete(obj)
# If the primary key was specified, we restore it # If the primary key was specified, we restore it

View File

@ -29,6 +29,9 @@ def pre_save_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED: if instance._meta.label_lower in EXCLUDED:
return return
if hasattr(instance, "_force_save"):
return
user = get_current_authenticated_user() user = get_current_authenticated_user()
if user is None: if user is None:
# Action performed on shell is always granted # Action performed on shell is always granted
@ -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): if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
raise PermissionDenied raise PermissionDenied
else: else:
# We check if the user can add the model
# While checking permissions, the object will be inserted in the DB, then removed.
# We disable temporary the connectors
pre_save.disconnect(pre_save_object)
pre_delete.disconnect(pre_delete_object)
# We disable also logs connectors
pre_save.disconnect(logs_signals.pre_save_object)
post_save.disconnect(logs_signals.save_object)
post_delete.disconnect(logs_signals.delete_object)
# We check if the user has right to add the object # We check if the user has right to add the object
has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance) has_perm = PermissionBackend().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: if not has_perm:
raise PermissionDenied raise PermissionDenied
def pre_delete_object(sender, instance, **kwargs): def pre_delete_object(instance, **kwargs):
""" """
Before a model get deleted, we check the permissions Before a model get deleted, we check the permissions
""" """
@ -91,6 +76,9 @@ def pre_delete_object(sender, instance, **kwargs):
if instance._meta.label_lower in EXCLUDED: if instance._meta.label_lower in EXCLUDED:
return return
if hasattr(instance, "_force_delete"):
return
user = get_current_authenticated_user() user = get_current_authenticated_user()
if user is None: if user is None:
# Action performed on shell is always granted # Action performed on shell is always granted

View File

@ -1,7 +1,6 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
import datetime
from json import dumps as json_dumps from json import dumps as json_dumps
from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput from django.forms.widgets import DateTimeBaseInput, NumberInput, TextInput

View File

@ -1,4 +1,4 @@
{% load i18n static pretty_money %} {% load i18n static pretty_money perms %}
<div class="card bg-light shadow"> <div class="card bg-light shadow">
<div class="card-header text-center"> <div class="card-header text-center">
<h4> Club {{ club.name }} </h4> <h4> Club {{ club.name }} </h4>
@ -40,9 +40,12 @@
</dl> </dl>
</div> </div>
<div class="card-footer text-center"> <div class="card-footer text-center">
{% if can_add_members %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add member" %}</a>
{% endif %}
{% if ".change_"|has_perm:club %}
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a> <a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_update' pk=club.pk %}"> {% trans "Edit" %}</a>
<a class="btn btn-primary btn-sm my-1" href="{% url 'member:club_add_member' pk=club.pk %}"> {% trans "Add roles" %}</a> {% endif %}
{% url 'member:club_detail' club.pk as club_detail_url %} {% url 'member:club_detail' club.pk as club_detail_url %}
{%if request.get_full_path != club_detail_url %} {%if request.get_full_path != club_detail_url %}
<a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a> <a class="btn btn-primary btn-sm my-1" href="{{ club_detail_url }}">{% trans 'View Profile' %}</a>