mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-20 17:41:55 +02:00
Merge branch 'beta-soon' into 'master'
Pre-beta fixes Closes #51 See merge request bde/nk20!86
This commit is contained in:
@ -163,7 +163,7 @@ class Entry(models.Model):
|
||||
amount=self.activity.activity_type.guest_entry_fee,
|
||||
reason="Invitation " + self.activity.name + " " + self.guest.first_name + " " + self.guest.last_name,
|
||||
valid=True,
|
||||
guest=self.guest,
|
||||
entry=self,
|
||||
).save()
|
||||
|
||||
return ret
|
||||
@ -240,8 +240,8 @@ class Guest(models.Model):
|
||||
|
||||
|
||||
class GuestTransaction(Transaction):
|
||||
guest = models.OneToOneField(
|
||||
Guest,
|
||||
entry = models.OneToOneField(
|
||||
Entry,
|
||||
on_delete=models.PROTECT,
|
||||
)
|
||||
|
||||
|
@ -23,6 +23,7 @@ from .tables import ActivityTable, GuestTable, EntryTable
|
||||
class ActivityCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
model = Activity
|
||||
form_class = ActivityForm
|
||||
extra_context = {"title": _("Create new activity")}
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.creater = self.request.user
|
||||
@ -37,12 +38,14 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||
model = Activity
|
||||
table_class = ActivityTable
|
||||
ordering = ('-date_start',)
|
||||
extra_context = {"title": _("Activities")}
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context['title'] = _("Activities")
|
||||
|
||||
upcoming_activities = Activity.objects.filter(date_end__gt=datetime.now())
|
||||
context['upcoming'] = ActivityTable(
|
||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
|
||||
@ -55,6 +58,7 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
||||
class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
model = Activity
|
||||
context_object_name = "activity"
|
||||
extra_context = {"title": _("Activity detail")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data()
|
||||
@ -71,6 +75,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
class ActivityUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
model = Activity
|
||||
form_class = ActivityForm
|
||||
extra_context = {"title": _("Update activity")}
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
return reverse_lazy('activity:activity_detail', kwargs={"pk": self.kwargs["pk"]})
|
||||
@ -81,6 +86,12 @@ class ActivityInviteView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
form_class = GuestForm
|
||||
template_name = "activity/activity_invite.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
activity = context["form"].activity
|
||||
context["title"] = _('Invite guest to the activity "{}"').format(activity.name)
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
||||
|
@ -14,9 +14,11 @@ class ReadProtectedModelViewSet(viewsets.ModelViewSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
|
||||
def get_queryset(self):
|
||||
user = get_current_authenticated_user()
|
||||
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
|
||||
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
|
||||
|
||||
|
||||
class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
@ -26,6 +28,8 @@ class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||
|
||||
def get_queryset(self):
|
||||
user = get_current_authenticated_user()
|
||||
self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view"))
|
||||
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))
|
||||
|
@ -4,9 +4,12 @@
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .forms import ProfileForm
|
||||
from .models import Club, Membership, Profile, Role
|
||||
from .models import Club, Membership, Profile
|
||||
|
||||
|
||||
class ProfileInline(admin.StackedInline):
|
||||
@ -17,6 +20,7 @@ class ProfileInline(admin.StackedInline):
|
||||
can_delete = False
|
||||
|
||||
|
||||
@admin.register(User, site=admin_site)
|
||||
class CustomUserAdmin(UserAdmin):
|
||||
inlines = (ProfileInline,)
|
||||
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
|
||||
@ -32,11 +36,33 @@ class CustomUserAdmin(UserAdmin):
|
||||
return super().get_inline_instances(request, obj)
|
||||
|
||||
|
||||
# Update Django User with profile
|
||||
admin.site.unregister(User)
|
||||
admin.site.register(User, CustomUserAdmin)
|
||||
@admin.register(Club, site=admin_site)
|
||||
class ClubAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'parent_club', 'email', 'require_memberships', 'pretty_fee_paid',
|
||||
'pretty_fee_unpaid', 'membership_start', 'membership_end',)
|
||||
ordering = ('name',)
|
||||
search_fields = ('name', 'email',)
|
||||
|
||||
# Add other models
|
||||
admin.site.register(Club)
|
||||
admin.site.register(Membership)
|
||||
admin.site.register(Role)
|
||||
def pretty_fee_paid(self, obj):
|
||||
return pretty_money(obj.membership_fee_paid)
|
||||
|
||||
def pretty_fee_unpaid(self, obj):
|
||||
return pretty_money(obj.membership_fee_unpaid)
|
||||
|
||||
pretty_fee_paid.short_description = _("membership fee (paid students)")
|
||||
pretty_fee_unpaid.short_description = _("membership fee (unpaid students)")
|
||||
|
||||
|
||||
@admin.register(Membership, site=admin_site)
|
||||
class MembershipAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'club', 'date_start', 'date_end', 'view_roles', 'pretty_fee',)
|
||||
ordering = ('-date_start', 'club')
|
||||
|
||||
def view_roles(self, obj):
|
||||
return ", ".join(role.name for role in obj.roles.all())
|
||||
|
||||
def pretty_fee(self, obj):
|
||||
return pretty_money(obj.fee)
|
||||
|
||||
view_roles.short_description = _("roles")
|
||||
pretty_fee.short_description = _("fee")
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Profile, Club, Role, Membership
|
||||
from ..models import Profile, Club, Membership
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
@ -29,17 +29,6 @@ class ClubSerializer(serializers.ModelSerializer):
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Roles.
|
||||
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MembershipSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Memberships.
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet
|
||||
from .views import ProfileViewSet, ClubViewSet, MembershipViewSet
|
||||
|
||||
|
||||
def register_members_urls(router, path):
|
||||
@ -10,5 +10,4 @@ def register_members_urls(router, path):
|
||||
"""
|
||||
router.register(path + '/profile', ProfileViewSet)
|
||||
router.register(path + '/club', ClubViewSet)
|
||||
router.register(path + '/role', RoleViewSet)
|
||||
router.register(path + '/membership', MembershipViewSet)
|
||||
|
@ -4,8 +4,8 @@
|
||||
from rest_framework.filters import SearchFilter
|
||||
from api.viewsets import ReadProtectedModelViewSet
|
||||
|
||||
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
|
||||
from ..models import Profile, Club, Role, Membership
|
||||
from .serializers import ProfileSerializer, ClubSerializer, MembershipSerializer
|
||||
from ..models import Profile, Club, Membership
|
||||
|
||||
|
||||
class ProfileViewSet(ReadProtectedModelViewSet):
|
||||
@ -30,18 +30,6 @@ class ClubViewSet(ReadProtectedModelViewSet):
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class RoleViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/role/
|
||||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
filter_backends = [SearchFilter]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class MembershipViewSet(ReadProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
|
@ -5,11 +5,11 @@ from django import forms
|
||||
from django.contrib.auth.forms import AuthenticationForm
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note.models import NoteSpecial
|
||||
from note.models import NoteSpecial, Alias
|
||||
from note_kfet.inputs import Autocomplete, AmountInput, DatePickerInput
|
||||
from permission.models import PermissionMask
|
||||
from permission.models import PermissionMask, Role
|
||||
|
||||
from .models import Profile, Club, Membership, Role
|
||||
from .models import Profile, Club, Membership
|
||||
|
||||
|
||||
class CustomAuthenticationForm(AuthenticationForm):
|
||||
@ -20,6 +20,18 @@ class CustomAuthenticationForm(AuthenticationForm):
|
||||
)
|
||||
|
||||
|
||||
class UserForm(forms.ModelForm):
|
||||
def _get_validation_exclusions(self):
|
||||
# Django usernames can only contain letters, numbers, @, ., +, - and _.
|
||||
# We want to allow users to have uncommon and unpractical usernames:
|
||||
# That is their problem, and we have normalized aliases for us.
|
||||
return super()._get_validation_exclusions() + ["username"]
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ('first_name', 'last_name', 'username', 'email',)
|
||||
|
||||
|
||||
class ProfileForm(forms.ModelForm):
|
||||
"""
|
||||
A form for the extras field provided by the :model:`member.Profile` model.
|
||||
@ -38,6 +50,15 @@ class ProfileForm(forms.ModelForm):
|
||||
|
||||
|
||||
class ClubForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if not self.instance.pk: # Creating a club
|
||||
if Alias.objects.filter(normalized_name=Alias.normalize(self.cleaned_data["name"])).exists():
|
||||
self.add_error('name', _("An alias with a similar name already exists."))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = '__all__'
|
||||
@ -56,8 +77,6 @@ class ClubForm(forms.ModelForm):
|
||||
|
||||
|
||||
class MembershipForm(forms.ModelForm):
|
||||
roles = forms.ModelMultipleChoiceField(queryset=Role.objects.filter(weirole=None).all())
|
||||
|
||||
soge = forms.BooleanField(
|
||||
label=_("Inscription paid by Société Générale"),
|
||||
required=False,
|
||||
@ -96,7 +115,7 @@ class MembershipForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ('user', 'roles', 'date_start')
|
||||
fields = ('user', 'date_start')
|
||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les noms d'utilisateur valides
|
||||
@ -112,3 +131,28 @@ class MembershipForm(forms.ModelForm):
|
||||
),
|
||||
'date_start': DatePickerInput(),
|
||||
}
|
||||
|
||||
|
||||
class MembershipRolesForm(forms.ModelForm):
|
||||
user = forms.ModelChoiceField(
|
||||
queryset=User.objects,
|
||||
label=_("User"),
|
||||
disabled=True,
|
||||
widget=Autocomplete(
|
||||
User,
|
||||
attrs={
|
||||
'api_url': '/api/user/',
|
||||
'name_field': 'username',
|
||||
'placeholder': 'Nom ...',
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
roles = forms.ModelMultipleChoiceField(
|
||||
queryset=Role.objects.filter(weirole=None).all(),
|
||||
label=_("Roles"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = ('user', 'roles')
|
||||
|
@ -3,8 +3,10 @@
|
||||
|
||||
import hashlib
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
||||
|
||||
|
||||
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||
@ -20,8 +22,37 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||
"""
|
||||
algorithm = "custom_nk15"
|
||||
|
||||
def must_update(self, encoded):
|
||||
if settings.DEBUG:
|
||||
current_user = get_current_authenticated_user()
|
||||
if current_user is not None and current_user.is_superuser:
|
||||
return False
|
||||
return True
|
||||
|
||||
def verify(self, password, encoded):
|
||||
if settings.DEBUG:
|
||||
current_user = get_current_authenticated_user()
|
||||
if current_user is not None and current_user.is_superuser\
|
||||
and get_current_session().get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
|
||||
if '|' in encoded:
|
||||
salt, db_hashed_pass = encoded.split('$')[2].split('|')
|
||||
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
|
||||
return super().verify(password, encoded)
|
||||
|
||||
|
||||
class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
||||
"""
|
||||
In debug mode and during the beta, superusers can login into other accounts for tests.
|
||||
"""
|
||||
def must_update(self, encoded):
|
||||
return False
|
||||
|
||||
def verify(self, password, encoded):
|
||||
if settings.DEBUG:
|
||||
current_user = get_current_authenticated_user()
|
||||
if current_user is not None and current_user.is_superuser\
|
||||
and get_current_session().get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
return super().verify(password, encoded)
|
||||
|
@ -131,7 +131,7 @@ class Profile(models.Model):
|
||||
return reverse('user_detail', args=(self.pk,))
|
||||
|
||||
def send_email_validation_link(self):
|
||||
subject = "Activate your Note Kfet account"
|
||||
subject = _("Activate your Note Kfet account")
|
||||
message = loader.render_to_string('registration/mails/email_validation_email.html',
|
||||
{
|
||||
'user': self.user,
|
||||
@ -247,24 +247,6 @@ class Club(models.Model):
|
||||
return reverse_lazy('member:club_detail', args=(self.pk,))
|
||||
|
||||
|
||||
class Role(models.Model):
|
||||
"""
|
||||
Role that an :model:`auth.User` can have in a :model:`member.Club`
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
max_length=255,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
"""
|
||||
Register the membership of a user to a club, including roles and membership duration.
|
||||
@ -284,7 +266,7 @@ class Membership(models.Model):
|
||||
)
|
||||
|
||||
roles = models.ManyToManyField(
|
||||
Role,
|
||||
"permission.Role",
|
||||
verbose_name=_("roles"),
|
||||
)
|
||||
|
||||
@ -302,6 +284,7 @@ class Membership(models.Model):
|
||||
verbose_name=_('fee'),
|
||||
)
|
||||
|
||||
@property
|
||||
def valid(self):
|
||||
"""
|
||||
A membership is valid if today is between the start and the end date.
|
||||
@ -319,6 +302,14 @@ class Membership(models.Model):
|
||||
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') + ' ' + self.club.parent_club.name)
|
||||
|
||||
if self.pk:
|
||||
for role in self.roles.all():
|
||||
club = role.for_club
|
||||
if club is not None:
|
||||
if club.pk != self.club_id:
|
||||
raise ValidationError(_('The role {role} does not apply to the club {club}.')
|
||||
.format(role=role.name, club=club.name))
|
||||
|
||||
created = not self.pk
|
||||
if created:
|
||||
if Membership.objects.filter(
|
||||
|
@ -131,3 +131,31 @@ class MembershipTable(tables.Table):
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('user', 'club', 'date_start', 'date_end', 'roles', 'fee', )
|
||||
model = Membership
|
||||
|
||||
|
||||
class ClubManagerTable(tables.Table):
|
||||
"""
|
||||
List managers of a club.
|
||||
"""
|
||||
|
||||
def render_user(self, value):
|
||||
# If the user has the right, link the displayed user with the page of its detail.
|
||||
s = value.username
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
||||
s = format_html("<a href={url}>{name}</a>",
|
||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||
|
||||
return s
|
||||
|
||||
def render_roles(self, record):
|
||||
roles = record.roles.all()
|
||||
return ", ".join(str(role) for role in roles)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover',
|
||||
'style': 'table-layout: fixed;'
|
||||
}
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('user', 'user.first_name', 'user.last_name', 'roles', )
|
||||
model = Membership
|
||||
|
@ -16,6 +16,7 @@ urlpatterns = [
|
||||
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('club/<int:pk>/members/', views.ClubMembersListView.as_view(), name="club_members"),
|
||||
|
||||
path('user/', views.UserListView.as_view(), name="user_list"),
|
||||
path('user/<int:pk>/', views.UserDetailView.as_view(), name="user_detail"),
|
||||
|
@ -6,12 +6,14 @@ from datetime import datetime, timedelta
|
||||
|
||||
from PIL import Image
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import logout
|
||||
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.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView
|
||||
from django.views.generic.edit import FormMixin
|
||||
@ -21,12 +23,14 @@ from note.forms import ImageForm
|
||||
from note.models import Alias, NoteUser
|
||||
from note.models.transactions import Transaction, SpecialTransaction
|
||||
from note.tables import HistoryTable, AliasTable
|
||||
from note_kfet.middlewares import _set_current_user_and_ip
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm
|
||||
from .models import Club, Membership, Role
|
||||
from .tables import ClubTable, UserTable, MembershipTable
|
||||
from .forms import ProfileForm, ClubForm, MembershipForm, CustomAuthenticationForm, UserForm, MembershipRolesForm
|
||||
from .models import Club, Membership
|
||||
from .tables import ClubTable, UserTable, MembershipTable, ClubManagerTable
|
||||
|
||||
|
||||
class CustomLoginView(LoginView):
|
||||
@ -36,6 +40,8 @@ class CustomLoginView(LoginView):
|
||||
form_class = CustomAuthenticationForm
|
||||
|
||||
def form_valid(self, form):
|
||||
logout(self.request)
|
||||
_set_current_user_and_ip(form.get_user(), self.request.session, None)
|
||||
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
||||
return super().form_valid(form)
|
||||
|
||||
@ -45,9 +51,11 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
Update the user information.
|
||||
"""
|
||||
model = User
|
||||
fields = ['first_name', 'last_name', 'username', 'email']
|
||||
form_class = UserForm
|
||||
template_name = 'member/profile_update.html'
|
||||
context_object_name = 'user_object'
|
||||
extra_context = {"title": _("Update Profile")}
|
||||
|
||||
profile_form = ProfileForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -62,7 +70,6 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
form.fields['email'].help_text = _("This address must be valid.")
|
||||
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile)
|
||||
context['title'] = _("Update Profile")
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -101,6 +108,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
if olduser.email != user.email:
|
||||
# If the user changed her/his email, then it is unvalidated and a confirmation link is sent.
|
||||
user.profile.email_confirmed = False
|
||||
user.profile.save()
|
||||
user.profile.send_email_validation_link()
|
||||
|
||||
return super().form_valid(form)
|
||||
@ -117,6 +125,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
model = User
|
||||
context_object_name = "user_object"
|
||||
template_name = "member/profile_detail.html"
|
||||
extra_context = {"title": _("Profile detail")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
@ -129,7 +138,7 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
user = context['user_object']
|
||||
history_list = \
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
||||
.order_by("-created_at", "-id")\
|
||||
.order_by("-created_at")\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
||||
history_table = HistoryTable(history_list, prefix='transaction-')
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
||||
@ -150,12 +159,13 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
model = User
|
||||
table_class = UserTable
|
||||
template_name = 'member/user_list.html'
|
||||
extra_context = {"title": _("Search user")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
Filter the user list with the given pattern.
|
||||
"""
|
||||
qs = super().get_queryset().filter(profile__registration_valid=True)
|
||||
qs = super().get_queryset().distinct().filter(profile__registration_valid=True)
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
@ -175,13 +185,6 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
|
||||
return qs[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = _("Search user")
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
@ -190,6 +193,7 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'member/profile_alias.html'
|
||||
context_object_name = 'user_object'
|
||||
extra_context = {"title": _("Note aliases")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -203,6 +207,7 @@ class PictureUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin, Det
|
||||
Update profile picture of the user note.
|
||||
"""
|
||||
form_class = ImageForm
|
||||
extra_context = {"title": _("Update note picture")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -260,6 +265,7 @@ class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
model = Token
|
||||
template_name = "member/manage_auth_tokens.html"
|
||||
extra_context = {"title": _("Manage auth token")}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
||||
@ -287,6 +293,7 @@ class ClubCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
model = Club
|
||||
form_class = ClubForm
|
||||
success_url = reverse_lazy('member:club_list')
|
||||
extra_context = {"title": _("Create new club")}
|
||||
|
||||
def form_valid(self, form):
|
||||
return super().form_valid(form)
|
||||
@ -298,12 +305,13 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
model = Club
|
||||
table_class = ClubTable
|
||||
extra_context = {"title": _("Search club")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
Filter the user list with the given pattern.
|
||||
"""
|
||||
qs = super().get_queryset().filter()
|
||||
qs = super().get_queryset().distinct()
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
@ -322,6 +330,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
model = Club
|
||||
context_object_name = "club"
|
||||
extra_context = {"title": _("Club detail")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -330,9 +339,13 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
|
||||
club.update_membership_dates()
|
||||
|
||||
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club")\
|
||||
.order_by('user__last_name').all()
|
||||
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
||||
|
||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
||||
.order_by('-created_at', '-id')
|
||||
.order_by('-created_at')
|
||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||
context['history_list'] = history_table
|
||||
@ -342,7 +355,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))
|
||||
|
||||
membership_table = MembershipTable(data=club_member, prefix="membership-")
|
||||
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
||||
membership_table.paginate(per_page=5, page=self.request.GET.get('membership-page', 1))
|
||||
context['member_list'] = membership_table
|
||||
|
||||
# Check if the user has the right to create a membership, to display the button.
|
||||
@ -366,6 +379,7 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
model = Club
|
||||
template_name = 'member/club_alias.html'
|
||||
context_object_name = 'club'
|
||||
extra_context = {"title": _("Note aliases")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -382,6 +396,7 @@ class ClubUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
context_object_name = "club"
|
||||
form_class = ClubForm
|
||||
template_name = "member/club_form.html"
|
||||
extra_context = {"title": _("Update club")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
@ -415,6 +430,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
model = Membership
|
||||
form_class = MembershipForm
|
||||
template_name = 'member/add_members.html'
|
||||
extra_context = {"title": _("Add new member to the club")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -425,7 +441,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
||||
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
||||
form.fields['credit_amount'].initial = club.membership_fee_paid
|
||||
form.fields['roles'].initial = Role.objects.filter(name="Membre de club").all()
|
||||
|
||||
# If the concerned club is the BDE, then we add the option that Société générale pays the membership.
|
||||
if club.name != "BDE":
|
||||
@ -444,7 +459,6 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
user = old_membership.user
|
||||
form.fields['user'].initial = user
|
||||
form.fields['user'].disabled = True
|
||||
form.fields['roles'].initial = old_membership.roles.all()
|
||||
form.fields['date_start'].initial = old_membership.date_end + timedelta(days=1)
|
||||
form.fields['credit_amount'].initial = club.membership_fee_paid if user.profile.paid \
|
||||
else club.membership_fee_unpaid
|
||||
@ -560,7 +574,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
form.add_error('bank', _("This field is required."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
SpecialTransaction.objects.create(
|
||||
transaction = SpecialTransaction(
|
||||
source=credit_type,
|
||||
destination=user.note,
|
||||
quantity=1,
|
||||
@ -571,9 +585,16 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
bank=bank,
|
||||
valid=True,
|
||||
)
|
||||
transaction._force_save = True
|
||||
transaction.save()
|
||||
|
||||
ret = super().form_valid(form)
|
||||
|
||||
member_role = Role.objects.filter(name="Membre de club").all()
|
||||
form.instance.roles.set(member_role)
|
||||
form.instance._force_save = True
|
||||
form.instance.save()
|
||||
|
||||
# If Société générale pays, then we assume that this is the BDE membership, and we auto-renew the
|
||||
# Kfet membership.
|
||||
if soge:
|
||||
@ -595,6 +616,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
date_start=old_membership.get().date_end + timedelta(days=1)
|
||||
if old_membership.exists() else form.instance.date_start,
|
||||
)
|
||||
membership._force_save = True
|
||||
membership._soge = True
|
||||
membership.save()
|
||||
membership.refresh_from_db()
|
||||
@ -615,8 +637,9 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
Manage the roles of a user in a club
|
||||
"""
|
||||
model = Membership
|
||||
form_class = MembershipForm
|
||||
form_class = MembershipRolesForm
|
||||
template_name = 'member/add_members.html'
|
||||
extra_context = {"title": _("Manage roles of an user in the club")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -626,15 +649,61 @@ class ClubManageRolesView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
# We don't create a full membership, we only update one field
|
||||
form.fields['user'].disabled = True
|
||||
del form.fields['date_start']
|
||||
del form.fields['credit_type']
|
||||
del form.fields['credit_amount']
|
||||
del form.fields['last_name']
|
||||
del form.fields['first_name']
|
||||
del form.fields['bank']
|
||||
|
||||
club = self.object.club
|
||||
form.fields['roles'].queryset = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
|
||||
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
|
||||
|
||||
return form
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:club_detail', kwargs={'pk': self.object.club.id})
|
||||
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.user.id})
|
||||
|
||||
|
||||
class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
model = Membership
|
||||
table_class = MembershipTable
|
||||
template_name = "member/club_members.html"
|
||||
extra_context = {"title": _("Members of the club")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset().filter(club_id=self.kwargs["pk"])
|
||||
|
||||
if 'search' in self.request.GET:
|
||||
pattern = self.request.GET['search']
|
||||
qs = qs.filter(
|
||||
Q(user__first_name__iregex='^' + pattern)
|
||||
| Q(user__last_name__iregex='^' + pattern)
|
||||
| Q(user__note__alias__normalized_name__iregex='^' + Alias.normalize(pattern))
|
||||
)
|
||||
|
||||
only_active = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||
|
||||
if only_active:
|
||||
qs = qs.filter(date_start__lte=timezone.now().today(), date_end__gte=timezone.now().today())
|
||||
|
||||
if "roles" in self.request.GET:
|
||||
if not self.request.GET["roles"]:
|
||||
return qs.none()
|
||||
roles_str = self.request.GET["roles"].replace(' ', '').split(',')
|
||||
roles_int = map(int, roles_str)
|
||||
qs = qs.filter(roles__in=roles_int)
|
||||
|
||||
qs = qs.order_by('-date_start', 'user__username')
|
||||
|
||||
return qs.distinct()
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
club = Club.objects.filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Club, "view")
|
||||
).get(pk=self.kwargs["pk"])
|
||||
context["club"] = club
|
||||
|
||||
applicable_roles = Role.objects.filter(Q(weirole__isnull=not hasattr(club, 'weiclub'))
|
||||
& (Q(for_club__isnull=True) | Q(for_club=club))).all()
|
||||
context["applicable_roles"] = applicable_roles
|
||||
|
||||
context["only_active"] = "only_active" not in self.request.GET or self.request.GET["only_active"] != '0'
|
||||
|
||||
return context
|
||||
|
@ -5,10 +5,12 @@ from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from polymorphic.admin import PolymorphicChildModelAdmin, \
|
||||
PolymorphicChildModelFilter, PolymorphicParentModelAdmin
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser
|
||||
from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \
|
||||
RecurrentTransaction, MembershipTransaction, SpecialTransaction
|
||||
from .templatetags.pretty_money import pretty_money
|
||||
|
||||
|
||||
class AliasInlines(admin.TabularInline):
|
||||
@ -19,7 +21,7 @@ class AliasInlines(admin.TabularInline):
|
||||
model = Alias
|
||||
|
||||
|
||||
@admin.register(Note)
|
||||
@admin.register(Note, site=admin_site)
|
||||
class NoteAdmin(PolymorphicParentModelAdmin):
|
||||
"""
|
||||
Parent regrouping all note types as children
|
||||
@ -36,13 +38,12 @@ class NoteAdmin(PolymorphicParentModelAdmin):
|
||||
|
||||
# Organize notes by registration date
|
||||
date_hierarchy = 'created_at'
|
||||
ordering = ['-created_at']
|
||||
|
||||
# Search by aliases
|
||||
search_fields = ['alias__name']
|
||||
|
||||
|
||||
@admin.register(NoteClub)
|
||||
@admin.register(NoteClub, site=admin_site)
|
||||
class NoteClubAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Child for a club note, see NoteAdmin
|
||||
@ -66,15 +67,27 @@ class NoteClubAdmin(PolymorphicChildModelAdmin):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(NoteSpecial)
|
||||
@admin.register(NoteSpecial, site=admin_site)
|
||||
class NoteSpecialAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Child for a special note, see NoteAdmin
|
||||
"""
|
||||
readonly_fields = ('balance',)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
"""
|
||||
A club note should not be manually added
|
||||
"""
|
||||
return False
|
||||
|
||||
@admin.register(NoteUser)
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
"""
|
||||
A club note should not be manually removed
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(NoteUser, site=admin_site)
|
||||
class NoteUserAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Child for an user note, see NoteAdmin
|
||||
@ -97,16 +110,16 @@ class NoteUserAdmin(PolymorphicChildModelAdmin):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(Transaction)
|
||||
@admin.register(Transaction, site=admin_site)
|
||||
class TransactionAdmin(PolymorphicParentModelAdmin):
|
||||
"""
|
||||
Admin customisation for Transaction
|
||||
"""
|
||||
child_models = (RecurrentTransaction, MembershipTransaction, SpecialTransaction)
|
||||
child_models = (Transaction, RecurrentTransaction, MembershipTransaction, SpecialTransaction)
|
||||
list_display = ('created_at', 'poly_source', 'poly_destination',
|
||||
'quantity', 'amount', 'valid')
|
||||
list_filter = ('valid',)
|
||||
autocomplete_fields = (
|
||||
readonly_fields = (
|
||||
'source',
|
||||
'destination',
|
||||
)
|
||||
@ -138,27 +151,35 @@ class TransactionAdmin(PolymorphicParentModelAdmin):
|
||||
return []
|
||||
|
||||
|
||||
@admin.register(MembershipTransaction)
|
||||
@admin.register(MembershipTransaction, site=admin_site)
|
||||
class MembershipTransactionAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Admin customisation for MembershipTransaction
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(SpecialTransaction)
|
||||
@admin.register(RecurrentTransaction, site=admin_site)
|
||||
class RecurrentTransactionAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Admin customisation for RecurrentTransaction
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(SpecialTransaction, site=admin_site)
|
||||
class SpecialTransactionAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Admin customisation for SpecialTransaction
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(TransactionTemplate)
|
||||
@admin.register(TransactionTemplate, site=admin_site)
|
||||
class TransactionTemplateAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for TransactionTemplate
|
||||
"""
|
||||
list_display = ('name', 'poly_destination', 'amount', 'category', 'display',)
|
||||
list_filter = ('category', 'display')
|
||||
list_display = ('name', 'poly_destination', 'pretty_amount', 'category', 'display', 'highlighted',)
|
||||
list_filter = ('category', 'display', 'highlighted',)
|
||||
search_fields = ('name', 'destination__club__name', 'amount',)
|
||||
autocomplete_fields = ('destination',)
|
||||
|
||||
def poly_destination(self, obj):
|
||||
@ -169,11 +190,15 @@ class TransactionTemplateAdmin(admin.ModelAdmin):
|
||||
|
||||
poly_destination.short_description = _('destination')
|
||||
|
||||
def pretty_amount(self, obj):
|
||||
return pretty_money(obj.amount)
|
||||
|
||||
@admin.register(TemplateCategory)
|
||||
pretty_amount.short_description = _("amount")
|
||||
|
||||
|
||||
@admin.register(TemplateCategory, site=admin_site)
|
||||
class TemplateCategoryAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for TransactionTemplate
|
||||
"""
|
||||
list_display = ('name',)
|
||||
list_filter = ('name',)
|
||||
|
@ -118,9 +118,8 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
# If the user has no right to see the note, then we only display the note identifier
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note):
|
||||
print(obj.pk)
|
||||
return NotePolymorphicSerializer().to_representation(obj.note)
|
||||
return dict(id=obj.id)
|
||||
return dict(id=obj.note.id, name=str(obj.note))
|
||||
|
||||
def get_email_confirmed(self, obj):
|
||||
if isinstance(obj.note, NoteUser):
|
||||
|
@ -109,7 +109,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
||||
queryset = queryset.filter(
|
||||
Q(name__regex="^" + alias)
|
||||
| Q(normalized_name__regex="^" + Alias.normalize(alias))
|
||||
| Q(normalized_name__regex="^" + alias.lower()))
|
||||
| Q(normalized_name__regex="^" + alias.lower()))\
|
||||
.order_by('name').prefetch_related('note')
|
||||
|
||||
return queryset
|
||||
|
||||
|
@ -62,6 +62,7 @@ class TransactionTemplate(models.Model):
|
||||
category = models.ForeignKey(
|
||||
TemplateCategory,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='templates',
|
||||
verbose_name=_('type'),
|
||||
max_length=31,
|
||||
)
|
||||
@ -71,6 +72,11 @@ class TransactionTemplate(models.Model):
|
||||
verbose_name=_("display"),
|
||||
)
|
||||
|
||||
highlighted = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("highlighted"),
|
||||
)
|
||||
|
||||
description = models.CharField(
|
||||
verbose_name=_('description'),
|
||||
max_length=255,
|
||||
@ -202,7 +208,9 @@ class Transaction(PolymorphicModel):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
# Save notes
|
||||
self.source._force_save = True
|
||||
self.source.save()
|
||||
self.destination._force_save = True
|
||||
self.destination.save()
|
||||
|
||||
def delete(self, **kwargs):
|
||||
|
@ -8,6 +8,8 @@ from django.db.models import F
|
||||
from django.utils.html import format_html
|
||||
from django_tables2.utils import A
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.middlewares import get_current_authenticated_user
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models.notes import Alias
|
||||
from .models.transactions import Transaction, TransactionTemplate
|
||||
@ -52,14 +54,26 @@ class HistoryTable(tables.Table):
|
||||
attrs={
|
||||
"td": {
|
||||
"id": lambda record: "validate_" + str(record.id),
|
||||
"class": lambda record: str(record.valid).lower() + ' validate',
|
||||
"class": lambda record:
|
||||
str(record.valid).lower()
|
||||
+ (' validate' if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
"note.change_transaction_invalidity_reason",
|
||||
record) else ''),
|
||||
"data-toggle": "tooltip",
|
||||
"title": lambda record: _("Click to invalidate") if record.valid else _("Click to validate"),
|
||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')',
|
||||
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
"note.change_transaction_invalidity_reason", record) else None,
|
||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower() + ')'
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
"note.change_transaction_invalidity_reason", record) else None,
|
||||
"onmouseover": lambda record: '$("#invalidity_reason_'
|
||||
+ str(record.id) + '").show();$("#invalidity_reason_'
|
||||
+ str(record.id) + '").focus();',
|
||||
"onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()',
|
||||
+ str(record.id) + '").focus();'
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
"note.change_transaction_invalidity_reason", record) else None,
|
||||
"onmouseout": lambda record: '$("#invalidity_reason_' + str(record.id) + '").hide()'
|
||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
||||
"note.change_transaction_invalidity_reason", record) else None,
|
||||
}
|
||||
}
|
||||
)
|
||||
@ -88,6 +102,10 @@ class HistoryTable(tables.Table):
|
||||
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
||||
"""
|
||||
val = "✔" if value else "✖"
|
||||
if not PermissionBackend\
|
||||
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record):
|
||||
return val
|
||||
|
||||
val += "<input type='text' class='form-control' id='invalidity_reason_" + str(record.id) \
|
||||
+ "' value='" + (html.escape(record.invalidity_reason)
|
||||
if record.invalidity_reason else ("" if value else str(_("No reason specified")))) \
|
||||
@ -131,12 +149,10 @@ class ButtonTable(tables.Table):
|
||||
row_attrs = {
|
||||
'class': lambda record: 'table-row ' + ('table-success' if record.display else 'table-danger'),
|
||||
'id': lambda record: "row-" + str(record.pk),
|
||||
'data-href': lambda record: record.pk
|
||||
}
|
||||
|
||||
model = TransactionTemplate
|
||||
exclude = ('id',)
|
||||
order_by = ('type', '-display', 'destination__name', 'name',)
|
||||
|
||||
edit = tables.LinkColumn('note:template_update',
|
||||
args=[A('pk')],
|
||||
|
@ -5,6 +5,7 @@ import json
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, UpdateView
|
||||
from django_tables2 import SingleTableView
|
||||
@ -14,7 +15,7 @@ from permission.backends import PermissionBackend
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .forms import TransactionTemplateForm
|
||||
from .models import Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
|
||||
from .models import TemplateCategory, Transaction, TransactionTemplate, RecurrentTransaction, NoteSpecial
|
||||
from .models.transactions import SpecialTransaction
|
||||
from .tables import HistoryTable, ButtonTable
|
||||
|
||||
@ -29,16 +30,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
||||
model = Transaction
|
||||
# Transaction history table
|
||||
table_class = HistoryTable
|
||||
extra_context = {"title": _("Transfer money")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).order_by("-created_at", "-id").all()[:20]
|
||||
return super().get_queryset(**kwargs).order_by("-created_at").all()[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Add some context variables in template such as page title
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['title'] = _('Transfer money')
|
||||
context['amount_widget'] = AmountInput(attrs={"id": "amount"})
|
||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||
@ -63,6 +64,7 @@ class TransactionTemplateCreateView(ProtectQuerysetMixin, LoginRequiredMixin, Cr
|
||||
model = TransactionTemplate
|
||||
form_class = TransactionTemplateForm
|
||||
success_url = reverse_lazy('note:template_list')
|
||||
extra_context = {"title": _("Create new button")}
|
||||
|
||||
|
||||
class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
@ -71,6 +73,20 @@ class TransactionTemplateListView(ProtectQuerysetMixin, LoginRequiredMixin, Sing
|
||||
"""
|
||||
model = TransactionTemplate
|
||||
table_class = ButtonTable
|
||||
extra_context = {"title": _("Search button")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
Filter the user list with the given pattern.
|
||||
"""
|
||||
qs = super().get_queryset().distinct()
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
qs = qs.filter(Q(name__iregex="^" + pattern) | Q(destination__club__name__iregex="^" + pattern))
|
||||
|
||||
qs = qs.order_by('-display', 'category__name', 'destination__club__name', 'name')
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
@ -80,6 +96,7 @@ class TransactionTemplateUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, Up
|
||||
model = TransactionTemplate
|
||||
form_class = TransactionTemplateForm
|
||||
success_url = reverse_lazy('note:template_list')
|
||||
extra_context = {"title": _("Update button")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -116,25 +133,28 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
model = Transaction
|
||||
template_name = "note/conso_form.html"
|
||||
extra_context = {"title": _("Consumptions")}
|
||||
|
||||
# Transaction history table
|
||||
table_class = HistoryTable
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return super().get_queryset(**kwargs).order_by("-created_at", "-id")[:20]
|
||||
return super().get_queryset(**kwargs).order_by("-created_at")[:20]
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
Add some context variables in template such as page title
|
||||
"""
|
||||
context = super().get_context_data(**kwargs)
|
||||
from django.db.models import Count
|
||||
buttons = TransactionTemplate.objects.filter(
|
||||
categories = TemplateCategory.objects.order_by('name').all()
|
||||
for category in categories:
|
||||
category.templates_filtered = category.templates.filter(
|
||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||
).filter(display=True).order_by('name').all()
|
||||
context['categories'] = [cat for cat in categories if cat.templates_filtered]
|
||||
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
|
||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
||||
).filter(display=True).annotate(clicks=Count('recurrenttransaction')).order_by('category__name', 'name')
|
||||
context['transaction_templates'] = buttons
|
||||
context['most_used'] = buttons.order_by('-clicks', 'name')[:10]
|
||||
context['title'] = _("Consumptions")
|
||||
).order_by('name').all()
|
||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
||||
|
||||
# select2 compatibility
|
||||
|
@ -2,11 +2,12 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||
|
||||
from django.contrib import admin
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .models import Permission, PermissionMask, RolePermissions
|
||||
from .models import Permission, PermissionMask, Role
|
||||
|
||||
|
||||
@admin.register(PermissionMask)
|
||||
@admin.register(PermissionMask, site=admin_site)
|
||||
class PermissionMaskAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for PermissionMask
|
||||
@ -14,17 +15,19 @@ class PermissionMaskAdmin(admin.ModelAdmin):
|
||||
list_display = ('description', 'rank', )
|
||||
|
||||
|
||||
@admin.register(Permission)
|
||||
@admin.register(Permission, site=admin_site)
|
||||
class PermissionAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for Permission
|
||||
"""
|
||||
list_display = ('type', 'model', 'field', 'mask', 'description', )
|
||||
list_display = ('description', 'type', 'model', 'field', 'mask', )
|
||||
list_filter = ('type', 'mask', 'model',)
|
||||
search_fields = ('description', 'field',)
|
||||
|
||||
|
||||
@admin.register(RolePermissions)
|
||||
class RolePermissionsAdmin(admin.ModelAdmin):
|
||||
@admin.register(Role, site=admin_site)
|
||||
class RoleAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for RolePermissions
|
||||
Admin customisation for Role
|
||||
"""
|
||||
list_display = ('role', )
|
||||
list_display = ('name', )
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Permission, RolePermissions
|
||||
from ..models import Permission, Role
|
||||
|
||||
|
||||
class PermissionSerializer(serializers.ModelSerializer):
|
||||
@ -17,12 +17,12 @@ class PermissionSerializer(serializers.ModelSerializer):
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RolePermissionsSerializer(serializers.ModelSerializer):
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for RolePermissions types.
|
||||
The djangorestframework plugin will analyse the model `RolePermissions` and parse all fields in the API.
|
||||
REST API Serializer for Role types.
|
||||
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = RolePermissions
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import PermissionViewSet, RolePermissionsViewSet
|
||||
from .views import PermissionViewSet, RoleViewSet
|
||||
|
||||
|
||||
def register_permission_urls(router, path):
|
||||
@ -9,4 +9,4 @@ def register_permission_urls(router, path):
|
||||
Configure router for permission REST API.
|
||||
"""
|
||||
router.register(path + "/permission", PermissionViewSet)
|
||||
router.register(path + "/roles", RolePermissionsViewSet)
|
||||
router.register(path + "/roles", RoleViewSet)
|
||||
|
@ -4,8 +4,8 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from api.viewsets import ReadOnlyProtectedModelViewSet
|
||||
|
||||
from .serializers import PermissionSerializer, RolePermissionsSerializer
|
||||
from ..models import Permission, RolePermissions
|
||||
from .serializers import PermissionSerializer, RoleSerializer
|
||||
from ..models import Permission, Role
|
||||
|
||||
|
||||
class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||
@ -20,13 +20,13 @@ class PermissionViewSet(ReadOnlyProtectedModelViewSet):
|
||||
filterset_fields = ['model', 'type', ]
|
||||
|
||||
|
||||
class RolePermissionsViewSet(ReadOnlyProtectedModelViewSet):
|
||||
class RoleViewSet(ReadOnlyProtectedModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `RolePermission` objects, serialize it to JSON with the given serializer
|
||||
then render it on /api/permission/roles/
|
||||
"""
|
||||
queryset = RolePermissions.objects.all()
|
||||
serializer_class = RolePermissionsSerializer
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['role', ]
|
||||
|
@ -1,7 +1,6 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
@ -36,27 +35,19 @@ class PermissionBackend(ModelBackend):
|
||||
# Unauthenticated users have no permissions
|
||||
return Permission.objects.none()
|
||||
|
||||
qs = Permission.objects.annotate(
|
||||
club=F("rolepermissions__role__membership__club"),
|
||||
membership=F("rolepermissions__role__membership"),
|
||||
).filter(
|
||||
(
|
||||
Q(
|
||||
rolepermissions__role__membership__date_start__lte=timezone.now().today(),
|
||||
rolepermissions__role__membership__date_end__gte=timezone.now().today(),
|
||||
)
|
||||
| Q(permanent=True)
|
||||
)
|
||||
& Q(rolepermissions__role__membership__user=user)
|
||||
& Q(type=t)
|
||||
& Q(mask__rank__lte=get_current_session().get("permission_mask", 0))
|
||||
)
|
||||
memberships = Membership.objects.filter(user=user).all()
|
||||
|
||||
if settings.DATABASES[qs.db]["ENGINE"] == 'django.db.backends.postgresql_psycopg2':
|
||||
qs = qs.distinct('pk', 'club')
|
||||
else: # SQLite doesn't support distinct fields.
|
||||
qs = qs.distinct()
|
||||
return qs
|
||||
perms = []
|
||||
|
||||
for membership in memberships:
|
||||
for role in membership.roles.all():
|
||||
for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all():
|
||||
if not perm.permanent:
|
||||
if membership.date_start > timezone.now().date() or membership.date_end < timezone.now().date():
|
||||
continue
|
||||
perm.membership = membership
|
||||
perms.append(perm)
|
||||
return perms
|
||||
|
||||
@staticmethod
|
||||
def permissions(user, model, type):
|
||||
@ -67,22 +58,13 @@ class PermissionBackend(ModelBackend):
|
||||
:param type: The type of the permissions: view, change, add or delete
|
||||
:return: A generator of the requested permissions
|
||||
"""
|
||||
clubs = {}
|
||||
memberships = {}
|
||||
|
||||
for permission in PermissionBackend.get_raw_permissions(user, type):
|
||||
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.club:
|
||||
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
|
||||
continue
|
||||
|
||||
if permission.club not in clubs:
|
||||
clubs[permission.club] = club = Club.objects.get(pk=permission.club)
|
||||
else:
|
||||
club = clubs[permission.club]
|
||||
|
||||
if permission.membership not in memberships:
|
||||
memberships[permission.membership] = membership = Membership.objects.get(pk=permission.membership)
|
||||
else:
|
||||
membership = memberships[permission.membership]
|
||||
membership = permission.membership
|
||||
club = membership.club
|
||||
|
||||
permission = permission.about(
|
||||
user=user,
|
||||
@ -113,12 +95,11 @@ class PermissionBackend(ModelBackend):
|
||||
:param field: The field of the model to test, if concerned
|
||||
:return: A query that corresponds to the filter to give to a queryset
|
||||
"""
|
||||
|
||||
if user is None or isinstance(user, AnonymousUser):
|
||||
# Anonymous users can't do anything
|
||||
return Q(pk=-1)
|
||||
|
||||
if user.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
|
||||
if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42:
|
||||
# Superusers have all rights
|
||||
return Q()
|
||||
|
||||
@ -154,7 +135,7 @@ class PermissionBackend(ModelBackend):
|
||||
if sess is not None and sess.session_key is None:
|
||||
return False
|
||||
|
||||
if user_obj.is_superuser and get_current_session().get("permission_mask", 42) >= 42:
|
||||
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
|
||||
if obj is None:
|
||||
@ -163,6 +144,7 @@ class PermissionBackend(ModelBackend):
|
||||
perm = perm.split('.')[-1].split('_', 2)
|
||||
perm_type = perm[0]
|
||||
perm_field = perm[2] if len(perm) == 3 else None
|
||||
|
||||
ct = ContentType.objects.get_for_model(obj)
|
||||
if any(permission.applies(obj, perm_type, perm_field)
|
||||
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
|
||||
|
@ -4,6 +4,7 @@
|
||||
from functools import lru_cache
|
||||
from time import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.sessions.models import Session
|
||||
from note_kfet.middlewares import get_current_session
|
||||
|
||||
@ -32,6 +33,10 @@ def memoize(f):
|
||||
sess_funs = new_sess_funs
|
||||
|
||||
def func(*args, **kwargs):
|
||||
if settings.DEBUG:
|
||||
# Don't memoize in DEBUG mode
|
||||
return f(*args, **kwargs)
|
||||
|
||||
nonlocal last_collect
|
||||
|
||||
if time() - last_collect > 60:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,13 +4,13 @@
|
||||
import functools
|
||||
import json
|
||||
import operator
|
||||
from time import sleep
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.db.models import F, Q, Model
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Role
|
||||
|
||||
|
||||
class InstancedPermission:
|
||||
@ -45,7 +45,17 @@ class InstancedPermission:
|
||||
else:
|
||||
oldpk = obj.pk
|
||||
# Ensure previous models are deleted
|
||||
self.model.model_class().objects.filter(pk=obj.pk).annotate(_force_delete=F("pk")).delete()
|
||||
count = 0
|
||||
while count < 1000:
|
||||
if self.model.model_class().objects.filter(pk=obj.pk).exists():
|
||||
# If the object exists, that means that one permission is currently checked.
|
||||
# We wait before the other permission, at most 1 second.
|
||||
sleep(1)
|
||||
continue
|
||||
break
|
||||
for o in self.model.model_class().objects.filter(pk=obj.pk).all():
|
||||
o._force_delete = True
|
||||
Model.delete(o)
|
||||
# Force insertion, no data verification, no trigger
|
||||
obj._force_save = True
|
||||
Model.save(obj, force_insert=True)
|
||||
@ -114,10 +124,10 @@ class PermissionMask(models.Model):
|
||||
class Permission(models.Model):
|
||||
|
||||
PERMISSION_TYPES = [
|
||||
('add', 'add'),
|
||||
('view', 'view'),
|
||||
('change', 'change'),
|
||||
('delete', 'delete')
|
||||
('add', _('add')),
|
||||
('view', _('view')),
|
||||
('change', _('change')),
|
||||
('delete', _('delete'))
|
||||
]
|
||||
|
||||
model = models.ForeignKey(
|
||||
@ -239,6 +249,9 @@ class Permission(models.Model):
|
||||
field = Permission.compute_param(value[i], **kwargs)
|
||||
continue
|
||||
|
||||
if not hasattr(field, value[i][0]):
|
||||
return False
|
||||
|
||||
field = getattr(field, value[i][0])
|
||||
params = []
|
||||
call_kwargs = {}
|
||||
@ -252,6 +265,9 @@ class Permission(models.Model):
|
||||
params.append(param)
|
||||
field = field(*params, **call_kwargs)
|
||||
else:
|
||||
if not hasattr(field, value[i]):
|
||||
return False
|
||||
|
||||
field = getattr(field, value[i])
|
||||
return field
|
||||
|
||||
@ -276,7 +292,7 @@ class Permission(models.Model):
|
||||
elif query[0] == 'NOT':
|
||||
return ~Permission._about(query[1], **kwargs)
|
||||
else:
|
||||
return Q(pk=F("pk"))
|
||||
return Q(pk=F("pk")) if Permission.compute_param(query, **kwargs) else ~Q(pk=F("pk"))
|
||||
elif isinstance(query, dict):
|
||||
q_kwargs = {}
|
||||
for key in query:
|
||||
@ -307,23 +323,30 @@ class Permission(models.Model):
|
||||
return self.description
|
||||
|
||||
|
||||
class RolePermissions(models.Model):
|
||||
class Role(models.Model):
|
||||
"""
|
||||
Permissions associated with a Role
|
||||
"""
|
||||
role = models.OneToOneField(
|
||||
Role,
|
||||
on_delete=models.PROTECT,
|
||||
related_name='permissions',
|
||||
verbose_name=_('role'),
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
)
|
||||
|
||||
permissions = models.ManyToManyField(
|
||||
Permission,
|
||||
verbose_name=_("permissions"),
|
||||
)
|
||||
|
||||
for_club = models.ForeignKey(
|
||||
"member.Club",
|
||||
verbose_name=_("for club"),
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return str(self.role)
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("role permissions")
|
||||
|
@ -19,8 +19,8 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
|
||||
'OPTIONS': [],
|
||||
'HEAD': [],
|
||||
'POST': ['%(app_label)s.add_%(model_name)s'],
|
||||
'PUT': ['%(app_label)s.change_%(model_name)s'],
|
||||
'PATCH': ['%(app_label)s.change_%(model_name)s'],
|
||||
'PUT': [], # ['%(app_label)s.change_%(model_name)s'],
|
||||
'PATCH': [], # ['%(app_label)s.change_%(model_name)s'],
|
||||
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,7 @@ def pre_save_object(sender, instance, **kwargs):
|
||||
|
||||
# In the other case, we check if he/she has the right to change one field
|
||||
previous = qs.get()
|
||||
|
||||
for field in instance._meta.fields:
|
||||
field_name = field.name
|
||||
old_value = getattr(previous, field.name)
|
||||
@ -81,7 +82,8 @@ def pre_delete_object(instance, **kwargs):
|
||||
if instance._meta.label_lower in EXCLUDED:
|
||||
return
|
||||
|
||||
if hasattr(instance, "_force_delete"):
|
||||
if hasattr(instance, "_force_delete") or hasattr(instance, "pk") and instance.pk == 0:
|
||||
# Don't check permissions on force-deleted objects
|
||||
return
|
||||
|
||||
user = get_current_authenticated_user()
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.template.defaultfilters import stringfilter
|
||||
from django import template
|
||||
@ -16,9 +17,9 @@ def not_empty_model_list(model_name):
|
||||
"""
|
||||
user = get_current_authenticated_user()
|
||||
session = get_current_session()
|
||||
if user is None:
|
||||
if user is None or isinstance(user, AnonymousUser):
|
||||
return False
|
||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
||||
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
qs = model_list(model_name)
|
||||
return qs.exists()
|
||||
@ -31,28 +32,38 @@ def not_empty_model_change_list(model_name):
|
||||
"""
|
||||
user = get_current_authenticated_user()
|
||||
session = get_current_session()
|
||||
if user is None:
|
||||
if user is None or isinstance(user, AnonymousUser):
|
||||
return False
|
||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
||||
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
qs = model_list(model_name, "change")
|
||||
return qs.exists()
|
||||
|
||||
|
||||
@stringfilter
|
||||
def model_list(model_name, t="view"):
|
||||
def model_list(model_name, t="view", fetch=True):
|
||||
"""
|
||||
Return the queryset of all visible instances of the given model.
|
||||
"""
|
||||
user = get_current_authenticated_user()
|
||||
if user is None:
|
||||
return False
|
||||
spl = model_name.split(".")
|
||||
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
||||
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t)).all()
|
||||
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t))
|
||||
if user is None or isinstance(user, AnonymousUser):
|
||||
return qs.none()
|
||||
if fetch:
|
||||
qs = qs.all()
|
||||
return qs
|
||||
|
||||
|
||||
@stringfilter
|
||||
def model_list_length(model_name, t="view"):
|
||||
"""
|
||||
Return the length of queryset of all visible instances of the given model.
|
||||
"""
|
||||
return model_list(model_name, t, False).count()
|
||||
|
||||
|
||||
def has_perm(perm, obj):
|
||||
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
|
||||
|
||||
@ -63,9 +74,9 @@ def can_create_transaction():
|
||||
"""
|
||||
user = get_current_authenticated_user()
|
||||
session = get_current_session()
|
||||
if user is None:
|
||||
if user is None or isinstance(user, AnonymousUser):
|
||||
return False
|
||||
elif user.is_superuser and session.get("permission_mask", 0) >= 42:
|
||||
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
||||
return True
|
||||
if session.get("can_create_transaction", None):
|
||||
return session.get("can_create_transaction", None) == 1
|
||||
@ -85,4 +96,5 @@ register = template.Library()
|
||||
register.filter('not_empty_model_list', not_empty_model_list)
|
||||
register.filter('not_empty_model_change_list', not_empty_model_change_list)
|
||||
register.filter('model_list', model_list)
|
||||
register.filter('model_list_length', model_list_length)
|
||||
register.filter('has_perm', has_perm)
|
||||
|
@ -76,7 +76,7 @@ class PermissionQueryTestCase(TestCase):
|
||||
model = perm.model.model_class()
|
||||
model.objects.filter(query).all()
|
||||
# print("Good query for permission", perm)
|
||||
except (FieldError, AttributeError, ValueError):
|
||||
except (FieldError, AttributeError, ValueError, TypeError):
|
||||
print("Query error for permission", perm)
|
||||
print("Query:", perm.query)
|
||||
if instanced.query:
|
||||
|
@ -5,9 +5,10 @@ from datetime import date
|
||||
from django.forms import HiddenInput
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import UpdateView, TemplateView
|
||||
from member.models import Role, Membership
|
||||
from member.models import Membership
|
||||
|
||||
from .backends import PermissionBackend
|
||||
from .models import Role
|
||||
|
||||
|
||||
class ProtectQuerysetMixin:
|
||||
@ -19,7 +20,7 @@ class ProtectQuerysetMixin:
|
||||
"""
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")).distinct()
|
||||
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view"))
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
@ -40,6 +41,7 @@ class ProtectQuerysetMixin:
|
||||
|
||||
class RightsView(TemplateView):
|
||||
template_name = "permission/all_rights.html"
|
||||
extra_context = {"title": _("Rights")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
@ -24,7 +24,8 @@ class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
|
||||
# Truncate microseconds so that tokens are consistent even if the
|
||||
# database doesn't support microseconds.
|
||||
login_timestamp = '' if user.last_login is None else user.last_login.replace(microsecond=0, tzinfo=None)
|
||||
return str(user.pk) + str(user.profile.email_confirmed) + str(login_timestamp) + str(timestamp)
|
||||
return str(user.pk) + str(user.email) + str(user.profile.email_confirmed)\
|
||||
+ str(login_timestamp) + str(timestamp)
|
||||
|
||||
|
||||
email_validation_token = AccountActivationTokenGenerator()
|
||||
|
@ -15,10 +15,11 @@ from django.views.generic import CreateView, TemplateView, DetailView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django_tables2 import SingleTableView
|
||||
from member.forms import ProfileForm
|
||||
from member.models import Membership, Club, Role
|
||||
from member.models import Membership, Club
|
||||
from note.models import SpecialTransaction
|
||||
from note.templatetags.pretty_money import pretty_money
|
||||
from permission.backends import PermissionBackend
|
||||
from permission.models import Role
|
||||
from permission.views import ProtectQuerysetMixin
|
||||
|
||||
from .forms import SignUpForm, ValidationForm
|
||||
@ -34,6 +35,7 @@ class UserCreateView(CreateView):
|
||||
form_class = SignUpForm
|
||||
template_name = 'registration/signup.html'
|
||||
second_form = ProfileForm
|
||||
extra_context = {"title": _("Register new user")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -77,6 +79,7 @@ class UserValidateView(TemplateView):
|
||||
"""
|
||||
title = _("Email validation")
|
||||
template_name = 'registration/email_validation_complete.html'
|
||||
extra_context = {"title": _("Validate email")}
|
||||
|
||||
def get(self, *args, **kwargs):
|
||||
"""
|
||||
@ -90,16 +93,13 @@ class UserValidateView(TemplateView):
|
||||
|
||||
# Validate the token
|
||||
if user is not None and email_validation_token.check_token(user, token):
|
||||
self.validlink = True
|
||||
# The user must wait that someone validates the account before the user can be active and login.
|
||||
self.validlink = True
|
||||
user.is_active = user.profile.registration_valid or user.is_superuser
|
||||
user.profile.email_confirmed = True
|
||||
user.save()
|
||||
user.profile.save()
|
||||
return super().dispatch(*args, **kwargs)
|
||||
else:
|
||||
# Display the "Email validation unsuccessful" page.
|
||||
return self.render_to_response(self.get_context_data())
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def get_user(self, uidb64):
|
||||
"""
|
||||
@ -132,7 +132,7 @@ class UserValidationEmailSentView(TemplateView):
|
||||
Display the information that the validation link has been sent.
|
||||
"""
|
||||
template_name = 'registration/email_validation_email_sent.html'
|
||||
title = _('Email validation email sent')
|
||||
extra_context = {"title": _('Email validation email sent')}
|
||||
|
||||
|
||||
class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, DetailView):
|
||||
@ -140,6 +140,7 @@ class UserResendValidationEmailView(LoginRequiredMixin, ProtectQuerysetMixin, De
|
||||
Rensend the email validation link.
|
||||
"""
|
||||
model = User
|
||||
extra_context = {"title": _("Resend email validation link")}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user = self.get_object()
|
||||
@ -157,6 +158,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||
model = User
|
||||
table_class = FutureUserTable
|
||||
template_name = 'registration/future_user_list.html'
|
||||
extra_context = {"title": _("Pre-registered users list")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
@ -164,7 +166,7 @@ class FutureUserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||
:param kwargs:
|
||||
:return:
|
||||
"""
|
||||
qs = super().get_queryset().filter(profile__registration_valid=False)
|
||||
qs = super().get_queryset().distinct().filter(profile__registration_valid=False)
|
||||
if "search" in self.request.GET:
|
||||
pattern = self.request.GET["search"]
|
||||
|
||||
@ -198,6 +200,7 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
||||
form_class = ValidationForm
|
||||
context_object_name = "user_object"
|
||||
template_name = "registration/future_profile_detail.html"
|
||||
extra_context = {"title": _("Registration detail")}
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
form = self.get_form()
|
||||
@ -354,6 +357,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
|
||||
"""
|
||||
Delete a pre-registered user.
|
||||
"""
|
||||
extra_context = {"title": _("Invalidate pre-registration")}
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
"""
|
||||
|
Submodule apps/scripts updated: ee54fca89e...dd8b48c31d
@ -2,11 +2,12 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-lateré
|
||||
|
||||
from django.contrib import admin
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .models import RemittanceType, Remittance, SogeCredit
|
||||
|
||||
|
||||
@admin.register(RemittanceType)
|
||||
@admin.register(RemittanceType, site=admin_site)
|
||||
class RemittanceTypeAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for RemiitanceType
|
||||
@ -14,7 +15,7 @@ class RemittanceTypeAdmin(admin.ModelAdmin):
|
||||
list_display = ('note', )
|
||||
|
||||
|
||||
@admin.register(Remittance)
|
||||
@admin.register(Remittance, site=admin_site)
|
||||
class RemittanceAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for Remittance
|
||||
@ -27,4 +28,14 @@ class RemittanceAdmin(admin.ModelAdmin):
|
||||
return not obj.closed and super().has_change_permission(request, obj)
|
||||
|
||||
|
||||
admin.site.register(SogeCredit)
|
||||
@admin.register(SogeCredit, site=admin_site)
|
||||
class SogeCreditAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Admin customisation for Remittance
|
||||
"""
|
||||
list_display = ('user', 'valid',)
|
||||
readonly_fields = ('transactions', 'credit_transaction',)
|
||||
|
||||
def has_add_permission(self, request):
|
||||
# Don't create a credit manually
|
||||
return False
|
||||
|
@ -8,7 +8,6 @@ from crispy_forms.layout import Submit
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from note_kfet.inputs import DatePickerInput, AmountInput
|
||||
from permission.backends import PermissionBackend
|
||||
|
||||
from .models import Invoice, Product, Remittance, SpecialTransactionProxy
|
||||
|
||||
@ -132,8 +131,7 @@ class LinkTransactionToRemittanceForm(forms.ModelForm):
|
||||
# Add submit button
|
||||
self.helper.add_input(Submit('submit', _("Submit"), attr={'class': 'btn btn-block btn-primary'}))
|
||||
|
||||
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)\
|
||||
.filter(PermissionBackend.filter_queryset(self.request.user, Remittance, "view"))
|
||||
self.fields["remittance"].queryset = Remittance.objects.filter(closed=False)
|
||||
|
||||
def clean_last_name(self):
|
||||
"""
|
||||
|
@ -15,6 +15,7 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, UpdateView, DetailView
|
||||
from django.views.generic.base import View, TemplateView
|
||||
from django.views.generic.edit import BaseFormView
|
||||
@ -35,6 +36,7 @@ class InvoiceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
model = Invoice
|
||||
form_class = InvoiceForm
|
||||
extra_context = {"title": _("Create new invoice")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -77,6 +79,7 @@ class InvoiceListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView)
|
||||
"""
|
||||
model = Invoice
|
||||
table_class = InvoiceTable
|
||||
extra_context = {"title": _("Invoices list")}
|
||||
|
||||
|
||||
class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
@ -85,6 +88,7 @@ class InvoiceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
model = Invoice
|
||||
form_class = InvoiceForm
|
||||
extra_context = {"title": _("Update an invoice")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -167,7 +171,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
|
||||
del tex
|
||||
|
||||
# The file has to be rendered twice
|
||||
for _ in range(2):
|
||||
for ignored in range(2):
|
||||
error = subprocess.Popen(
|
||||
["pdflatex", "invoice-{}.tex".format(pk)],
|
||||
cwd=tmp_dir,
|
||||
@ -198,6 +202,7 @@ class RemittanceCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView)
|
||||
"""
|
||||
model = Remittance
|
||||
form_class = RemittanceForm
|
||||
extra_context = {"title": _("Create a new remittance")}
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:remittance_list')
|
||||
@ -218,27 +223,46 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
||||
List existing Remittances
|
||||
"""
|
||||
template_name = "treasury/remittance_list.html"
|
||||
extra_context = {"title": _("Remittances list")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["opened_remittances"] = RemittanceTable(
|
||||
opened_remittances = RemittanceTable(
|
||||
data=Remittance.objects.filter(closed=False).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
|
||||
context["closed_remittances"] = RemittanceTable(
|
||||
data=Remittance.objects.filter(closed=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all())
|
||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
||||
prefix="opened-remittances-",
|
||||
)
|
||||
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
|
||||
context["opened_remittances"] = opened_remittances
|
||||
|
||||
context["special_transactions_no_remittance"] = SpecialTransactionTable(
|
||||
closed_remittances = RemittanceTable(
|
||||
data=Remittance.objects.filter(closed=True).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).reverse().all(),
|
||||
prefix="closed-remittances-",
|
||||
)
|
||||
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
|
||||
context["closed_remittances"] = closed_remittances
|
||||
|
||||
no_remittance_tr = SpecialTransactionTable(
|
||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||
specialtransactionproxy__remittance=None).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
||||
exclude=('remittance_remove', ))
|
||||
context["special_transactions_with_remittance"] = SpecialTransactionTable(
|
||||
exclude=('remittance_remove', ),
|
||||
prefix="no-remittance-",
|
||||
)
|
||||
no_remittance_tr.paginate(page=self.request.GET.get("no-remittance-page", 1), per_page=10)
|
||||
context["special_transactions_no_remittance"] = no_remittance_tr
|
||||
|
||||
with_remittance_tr = SpecialTransactionTable(
|
||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||
specialtransactionproxy__remittance__closed=False).filter(
|
||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
||||
exclude=('remittance_add', ))
|
||||
exclude=('remittance_add', ),
|
||||
prefix="with-remittance-",
|
||||
)
|
||||
with_remittance_tr.paginate(page=self.request.GET.get("with-remittance-page", 1), per_page=10)
|
||||
context["special_transactions_with_remittance"] = with_remittance_tr
|
||||
|
||||
return context
|
||||
|
||||
@ -249,6 +273,7 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView)
|
||||
"""
|
||||
model = Remittance
|
||||
form_class = RemittanceForm
|
||||
extra_context = {"title": _("Update a remittance")}
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:remittance_list')
|
||||
@ -271,9 +296,9 @@ class LinkTransactionToRemittanceView(ProtectQuerysetMixin, LoginRequiredMixin,
|
||||
"""
|
||||
Attach a special transaction to a remittance
|
||||
"""
|
||||
|
||||
model = SpecialTransactionProxy
|
||||
form_class = LinkTransactionToRemittanceForm
|
||||
extra_context = {"title": _("Attach a transaction to a remittance")}
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('treasury:remittance_list')
|
||||
@ -317,6 +342,7 @@ class SogeCreditListView(LoginRequiredMixin, ProtectQuerysetMixin, SingleTableVi
|
||||
"""
|
||||
model = SogeCredit
|
||||
table_class = SogeCreditTable
|
||||
extra_context = {"title": _("List of credits from the Société générale")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
"""
|
||||
@ -355,6 +381,7 @@ class SogeCreditManageView(LoginRequiredMixin, ProtectQuerysetMixin, BaseFormVie
|
||||
"""
|
||||
model = SogeCredit
|
||||
form_class = Form
|
||||
extra_context = {"title": _("Manage credits from the Société générale")}
|
||||
|
||||
def form_valid(self, form):
|
||||
if "validate" in form.data:
|
||||
|
@ -1,13 +1,13 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from note_kfet.admin import admin_site
|
||||
|
||||
from .models import WEIClub, WEIRegistration, WEIMembership, WEIRole, Bus, BusTeam
|
||||
|
||||
admin.site.register(WEIClub)
|
||||
admin.site.register(WEIRegistration)
|
||||
admin.site.register(WEIMembership)
|
||||
admin.site.register(WEIRole)
|
||||
admin.site.register(Bus)
|
||||
admin.site.register(BusTeam)
|
||||
admin_site.register(WEIClub)
|
||||
admin_site.register(WEIRegistration)
|
||||
admin_site.register(WEIMembership)
|
||||
admin_site.register(WEIRole)
|
||||
admin_site.register(Bus)
|
||||
admin_site.register(BusTeam)
|
||||
|
@ -96,7 +96,7 @@ class WEIMembershipForm(forms.ModelForm):
|
||||
class BusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Bus
|
||||
fields = '__all__'
|
||||
exclude = ('information_json',)
|
||||
widgets = {
|
||||
"wei": Autocomplete(
|
||||
WEIClub,
|
||||
|
@ -8,8 +8,9 @@ from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from member.models import Role, Club, Membership
|
||||
from member.models import Club, Membership
|
||||
from note.models import MembershipTransaction
|
||||
from permission.models import Role
|
||||
|
||||
|
||||
class WEIClub(Club):
|
||||
@ -113,6 +114,7 @@ class BusTeam(models.Model):
|
||||
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
)
|
||||
|
||||
color = models.PositiveIntegerField( # Use a color picker to get the hexa code
|
||||
@ -188,6 +190,28 @@ class WEIRegistration(models.Model):
|
||||
verbose_name=_("gender"),
|
||||
)
|
||||
|
||||
clothing_cut = models.CharField(
|
||||
max_length=16,
|
||||
choices=(
|
||||
('male', _("Male")),
|
||||
('female', _("Female")),
|
||||
),
|
||||
verbose_name=_("clothing cut"),
|
||||
)
|
||||
|
||||
clothing_size = models.CharField(
|
||||
max_length=4,
|
||||
choices=(
|
||||
('XS', "XS"),
|
||||
('S', "S"),
|
||||
('M', "M"),
|
||||
('L', "L"),
|
||||
('XL', "XL"),
|
||||
('XXL', "XXL"),
|
||||
),
|
||||
verbose_name=_("clothing size"),
|
||||
)
|
||||
|
||||
health_issues = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
|
@ -103,7 +103,7 @@ class WEIMembershipTable(tables.Table):
|
||||
|
||||
team = tables.LinkColumn(
|
||||
'wei:manage_bus_team',
|
||||
args=[A('bus.pk')],
|
||||
args=[A('team.pk')],
|
||||
)
|
||||
|
||||
def render_year(self, record):
|
||||
@ -144,10 +144,10 @@ class BusTable(tables.Table):
|
||||
)
|
||||
|
||||
def render_teams(self, value):
|
||||
return ", ".join(team.name for team in value.all())
|
||||
return ", ".join(team.name for team in value.order_by('name').all())
|
||||
|
||||
def render_count(self, value):
|
||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
||||
return str(value) + " " + (str(_("members")) if value > 1 else str(_("member")))
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
@ -178,7 +178,7 @@ class BusTeamTable(tables.Table):
|
||||
)
|
||||
|
||||
def render_count(self, value):
|
||||
return str(value) + " " + (str(_("members")) if value > 0 else str(_("member")))
|
||||
return str(value) + " " + (str(_("members")) if value > 1 else str(_("member")))
|
||||
|
||||
count = tables.Column(
|
||||
verbose_name=_("Members count"),
|
||||
|
@ -17,6 +17,7 @@ from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.template.loader import render_to_string
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.views import View
|
||||
from django.views.generic import DetailView, UpdateView, CreateView, RedirectView, TemplateView
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -52,6 +53,16 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
||||
model = WEIClub
|
||||
table_class = WEITable
|
||||
ordering = '-year'
|
||||
extra_context = {"title": _("Search WEI")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub(
|
||||
year=0,
|
||||
date_start=timezone.now().date(),
|
||||
date_end=timezone.now().date(),
|
||||
))
|
||||
return context
|
||||
|
||||
|
||||
class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
@ -60,6 +71,7 @@ class WEICreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
model = WEIClub
|
||||
form_class = WEIForm
|
||||
extra_context = {"title": _("Create WEI")}
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.requires_membership = True
|
||||
@ -79,6 +91,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
model = WEIClub
|
||||
context_object_name = "club"
|
||||
extra_context = {"title": _("WEI Detail")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -132,6 +145,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
else:
|
||||
# Check if the user has the right to create a registration of a random first year member.
|
||||
empty_fy_registration = WEIRegistration(
|
||||
wei=club,
|
||||
user=random_user,
|
||||
first_year=True,
|
||||
birth_date="1970-01-01",
|
||||
@ -144,6 +158,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
|
||||
# Check if the user has the right to create a registration of a random old member.
|
||||
empty_old_registration = WEIRegistration(
|
||||
wei=club,
|
||||
user=User.objects.filter(~Q(wei__wei__in=[club])).first(),
|
||||
first_year=False,
|
||||
birth_date="1970-01-01",
|
||||
@ -171,13 +186,14 @@ class WEIMembershipsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableVi
|
||||
"""
|
||||
model = WEIMembership
|
||||
table_class = WEIMembershipTable
|
||||
extra_context = {"title": _("View members of the WEI")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs).filter(club=self.club)
|
||||
qs = super().get_queryset(**kwargs).filter(club=self.club).distinct()
|
||||
|
||||
pattern = self.request.GET.get("search", "")
|
||||
|
||||
@ -208,13 +224,14 @@ class WEIRegistrationsView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTable
|
||||
"""
|
||||
model = WEIRegistration
|
||||
table_class = WEIRegistrationTable
|
||||
extra_context = {"title": _("View registrations to the WEI")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
self.club = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None)
|
||||
qs = super().get_queryset(**kwargs).filter(wei=self.club, membership=None).distinct()
|
||||
|
||||
pattern = self.request.GET.get("search", "")
|
||||
|
||||
@ -244,6 +261,7 @@ class WEIUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
model = WEIClub
|
||||
context_object_name = "club"
|
||||
form_class = WEIForm
|
||||
extra_context = {"title": _("Update the WEI")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object()
|
||||
@ -264,6 +282,7 @@ class BusCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
model = Bus
|
||||
form_class = BusForm
|
||||
extra_context = {"title": _("Create new bus")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
@ -294,6 +313,7 @@ class BusUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
model = Bus
|
||||
form_class = BusForm
|
||||
extra_context = {"title": _("Update bus")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().wei
|
||||
@ -323,6 +343,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
Manage Bus
|
||||
"""
|
||||
model = Bus
|
||||
extra_context = {"title": _("Manage bus")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -330,7 +351,7 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
|
||||
bus = self.object
|
||||
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
|
||||
.filter(bus=bus).annotate(count=Count("memberships"))
|
||||
.filter(bus=bus).annotate(count=Count("memberships")).order_by("name")
|
||||
teams_table = BusTeamTable(data=teams, prefix="team-")
|
||||
context["teams"] = teams_table
|
||||
|
||||
@ -349,6 +370,7 @@ class BusTeamCreateView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
model = BusTeam
|
||||
form_class = BusTeamForm
|
||||
extra_context = {"title": _("Create new team")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(buses__pk=self.kwargs["pk"])
|
||||
@ -380,6 +402,7 @@ class BusTeamUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
model = BusTeam
|
||||
form_class = BusTeamForm
|
||||
extra_context = {"title": _("Update team")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = self.get_object().bus.wei
|
||||
@ -410,6 +433,7 @@ class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
||||
Manage Bus team
|
||||
"""
|
||||
model = BusTeam
|
||||
extra_context = {"title": _("Manage WEI team")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
@ -431,6 +455,7 @@ class WEIRegister1AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
extra_context = {"title": _("Register first year student to the WEI")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
@ -485,6 +510,7 @@ class WEIRegister2AView(ProtectQuerysetMixin, LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
extra_context = {"title": _("Register old student to the WEI")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIClub.objects.get(pk=self.kwargs["wei_pk"])
|
||||
@ -562,6 +588,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
||||
"""
|
||||
model = WEIRegistration
|
||||
form_class = WEIRegistrationForm
|
||||
extra_context = {"title": _("Update WEI Registration")}
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
return WEIRegistration.objects
|
||||
@ -651,6 +678,7 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete
|
||||
Delete a non-validated WEI registration
|
||||
"""
|
||||
model = WEIRegistration
|
||||
extra_context = {"title": _("Delete WEI registration")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
object = self.get_object()
|
||||
@ -680,6 +708,7 @@ class WEIValidateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Crea
|
||||
"""
|
||||
model = WEIMembership
|
||||
form_class = WEIMembershipForm
|
||||
extra_context = {"title": _("Validate WEI registration")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
wei = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||
@ -797,6 +826,7 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||
model = WEIRegistration
|
||||
template_name = "wei/survey.html"
|
||||
survey = None
|
||||
extra_context = {"title": _("Survey WEI")}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
@ -834,7 +864,6 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = self.object.wei
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
@ -850,21 +879,21 @@ class WEISurveyView(LoginRequiredMixin, BaseFormView, DetailView):
|
||||
|
||||
class WEISurveyEndView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "wei/survey_end.html"
|
||||
extra_context = {"title": _("Survey WEI")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIRegistration.objects.get(pk=self.kwargs["pk"]).wei
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
|
||||
class WEIClosedView(LoginRequiredMixin, TemplateView):
|
||||
template_name = "wei/survey_closed.html"
|
||||
extra_context = {"title": _("Survey WEI")}
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["club"] = WEIClub.objects.get(pk=self.kwargs["pk"])
|
||||
context["title"] = _("Survey WEI")
|
||||
return context
|
||||
|
||||
|
||||
|
Reference in New Issue
Block a user