1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2024-12-22 23:42:25 +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:
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()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

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