1
0
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:
ynerant
2020-08-01 16:12:09 +02:00
100 changed files with 5020 additions and 2196 deletions

View File

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

View File

@ -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"))\

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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"),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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="",

View File

@ -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"),

View File

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