mirror of https://gitlab.crans.org/bde/nk20
Full membership support
This commit is contained in:
parent
bf9789bd9e
commit
d5b010980b
|
@ -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()
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
"fields": {
|
||||
"name": "Kfet",
|
||||
"email": "tresorerie.bde@example.com",
|
||||
"parent_club": 1,
|
||||
"require_memberships": true,
|
||||
"membership_fee": 3500,
|
||||
"membership_duration": 396,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'),
|
||||
]
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -140,6 +140,7 @@ class Transaction(PolymorphicModel):
|
|||
max_length=255,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue