mirror of
				https://gitlab.crans.org/bde/nk20
				synced 2025-10-31 15:50:03 +01:00 
			
		
		
		
	Merge branch 'rights' into 'master'
Système de droits See merge request bde/nk20!10
This commit is contained in:
		| @@ -1,14 +1,15 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework import viewsets | ||||
| from rest_framework.filters import SearchFilter | ||||
| from api.viewsets import ReadProtectedModelViewSet | ||||
|  | ||||
| from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer | ||||
| from ..models import ActivityType, Activity, Guest | ||||
|  | ||||
|  | ||||
| class ActivityTypeViewSet(viewsets.ModelViewSet): | ||||
| class ActivityTypeViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer, | ||||
| @@ -20,7 +21,7 @@ class ActivityTypeViewSet(viewsets.ModelViewSet): | ||||
|     filterset_fields = ['name', 'can_invite', ] | ||||
|  | ||||
|  | ||||
| class ActivityViewSet(viewsets.ModelViewSet): | ||||
| class ActivityViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer, | ||||
| @@ -32,7 +33,7 @@ class ActivityViewSet(viewsets.ModelViewSet): | ||||
|     filterset_fields = ['name', 'description', 'activity_type', ] | ||||
|  | ||||
|  | ||||
| class GuestViewSet(viewsets.ModelViewSet): | ||||
| class GuestViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer, | ||||
|   | ||||
| @@ -5,12 +5,15 @@ from django.conf.urls import url, include | ||||
| from django.contrib.auth.models import User | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework import routers, serializers, viewsets | ||||
| from rest_framework import routers, serializers | ||||
| from rest_framework.filters import SearchFilter | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||||
| from activity.api.urls import register_activity_urls | ||||
| from api.viewsets import ReadProtectedModelViewSet | ||||
| from member.api.urls import register_members_urls | ||||
| from note.api.urls import register_note_urls | ||||
| from logs.api.urls import register_logs_urls | ||||
| from permission.api.urls import register_permission_urls | ||||
|  | ||||
|  | ||||
| class UserSerializer(serializers.ModelSerializer): | ||||
| @@ -39,7 +42,7 @@ class ContentTypeSerializer(serializers.ModelSerializer): | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class UserViewSet(viewsets.ModelViewSet): | ||||
| class UserViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, | ||||
| @@ -52,7 +55,8 @@ class UserViewSet(viewsets.ModelViewSet): | ||||
|     search_fields = ['$username', '$first_name', '$last_name', ] | ||||
|  | ||||
|  | ||||
| class ContentTypeViewSet(viewsets.ReadOnlyModelViewSet): | ||||
| # This ViewSet is the only one that is accessible from all authenticated users! | ||||
| class ContentTypeViewSet(ReadOnlyModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer, | ||||
| @@ -70,6 +74,7 @@ router.register('user', UserViewSet) | ||||
| register_members_urls(router, 'members') | ||||
| register_activity_urls(router, 'activity') | ||||
| register_note_urls(router, 'note') | ||||
| register_permission_urls(router, 'permission') | ||||
| register_logs_urls(router, 'logs') | ||||
|  | ||||
| app_name = 'api' | ||||
|   | ||||
							
								
								
									
										31
									
								
								apps/api/viewsets.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/api/viewsets.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from permission.backends import PermissionBackend | ||||
| from rest_framework import viewsets | ||||
| from note_kfet.middlewares import get_current_authenticated_user | ||||
|  | ||||
|  | ||||
| class ReadProtectedModelViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     Protect a ModelViewSet by filtering the objects that the user cannot see. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() | ||||
|         user = get_current_authenticated_user() | ||||
|         self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view")) | ||||
|  | ||||
|  | ||||
| class ReadOnlyProtectedModelViewSet(viewsets.ReadOnlyModelViewSet): | ||||
|     """ | ||||
|     Protect a ReadOnlyModelViewSet by filtering the objects that the user cannot see. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|         model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class() | ||||
|         user = get_current_authenticated_user() | ||||
|         self.queryset = model.objects.filter(PermissionBackend.filter_queryset(user, model, "view")) | ||||
| @@ -2,14 +2,14 @@ | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework import viewsets | ||||
| from rest_framework.filters import OrderingFilter | ||||
| from api.viewsets import ReadOnlyProtectedModelViewSet | ||||
|  | ||||
| from .serializers import ChangelogSerializer | ||||
| from ..models import Changelog | ||||
|  | ||||
|  | ||||
| class ChangelogViewSet(viewsets.ReadOnlyModelViewSet): | ||||
| class ChangelogViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, | ||||
|   | ||||
| @@ -1,77 +0,0 @@ | ||||
| # 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.models import AnonymousUser | ||||
|  | ||||
| from threading import local | ||||
|  | ||||
|  | ||||
| USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') | ||||
| IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') | ||||
|  | ||||
| _thread_locals = local() | ||||
|  | ||||
|  | ||||
| def _set_current_user_and_ip(user=None, ip=None): | ||||
|     """ | ||||
|     Store current user and IP address in the local thread. | ||||
|     """ | ||||
|     setattr(_thread_locals, USER_ATTR_NAME, user) | ||||
|     setattr(_thread_locals, IP_ATTR_NAME, ip) | ||||
|  | ||||
|  | ||||
| def get_current_user(): | ||||
|     """ | ||||
|     :return: The user that performed a request (may be anonymous) | ||||
|     """ | ||||
|     return getattr(_thread_locals, USER_ATTR_NAME, None) | ||||
|  | ||||
|  | ||||
| def get_current_ip(): | ||||
|     """ | ||||
|     :return: The IP address of the user that has performed a request | ||||
|     """ | ||||
|     return getattr(_thread_locals, IP_ATTR_NAME, None) | ||||
|  | ||||
|  | ||||
| def get_current_authenticated_user(): | ||||
|     """ | ||||
|     :return: The user that performed a request (must be authenticated, return None if anonymous) | ||||
|     """ | ||||
|     current_user = get_current_user() | ||||
|     if isinstance(current_user, AnonymousUser): | ||||
|         return None | ||||
|     return current_user | ||||
|  | ||||
|  | ||||
| class LogsMiddleware(object): | ||||
|     """ | ||||
|     This middleware gets the current user with his or her IP address on each request. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, get_response): | ||||
|         self.get_response = get_response | ||||
|  | ||||
|     def __call__(self, request): | ||||
|         """ | ||||
|         This function is called on each request. | ||||
|         :param request: The HTTP Request | ||||
|         :return: The HTTP Response | ||||
|         """ | ||||
|         user = request.user | ||||
|         # Get request IP from the headers | ||||
|         # The `REMOTE_ADDR` field may not contain the true IP, if there is a proxy | ||||
|         if 'HTTP_X_FORWARDED_FOR' in request.META: | ||||
|             ip = request.META.get('HTTP_X_FORWARDED_FOR') | ||||
|         else: | ||||
|             ip = request.META.get('REMOTE_ADDR') | ||||
|  | ||||
|         # The user and the IP address are stored in the current thread | ||||
|         _set_current_user_and_ip(user, ip) | ||||
|         # The request is then analysed, and the response is generated | ||||
|         response = self.get_response(request) | ||||
|         # We flush the connected user and the IP address for the next requests | ||||
|         _set_current_user_and_ip(None, None) | ||||
|  | ||||
|         return response | ||||
| @@ -4,14 +4,13 @@ | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from rest_framework.renderers import JSONRenderer | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from note.models import NoteUser, Alias | ||||
| from note_kfet.middlewares import get_current_authenticated_user, get_current_ip | ||||
|  | ||||
| from .models import Changelog | ||||
|  | ||||
| import getpass | ||||
|  | ||||
| from note.models import NoteUser, Alias | ||||
|  | ||||
| from .middlewares import get_current_authenticated_user, get_current_ip | ||||
| from .models import Changelog | ||||
|  | ||||
|  | ||||
| # Ces modèles ne nécessitent pas de logs | ||||
| EXCLUDED = [ | ||||
|   | ||||
| @@ -15,6 +15,7 @@ class ProfileSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Profile | ||||
|         fields = '__all__' | ||||
|         read_only_fields = ('user', ) | ||||
|  | ||||
|  | ||||
| class ClubSerializer(serializers.ModelSerializer): | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import viewsets | ||||
| 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 | ||||
|  | ||||
|  | ||||
| class ProfileViewSet(viewsets.ModelViewSet): | ||||
| class ProfileViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer, | ||||
| @@ -18,7 +18,7 @@ class ProfileViewSet(viewsets.ModelViewSet): | ||||
|     serializer_class = ProfileSerializer | ||||
|  | ||||
|  | ||||
| class ClubViewSet(viewsets.ModelViewSet): | ||||
| class ClubViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer, | ||||
| @@ -30,7 +30,7 @@ class ClubViewSet(viewsets.ModelViewSet): | ||||
|     search_fields = ['$name', ] | ||||
|  | ||||
|  | ||||
| class RoleViewSet(viewsets.ModelViewSet): | ||||
| class RoleViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer, | ||||
| @@ -42,7 +42,7 @@ class RoleViewSet(viewsets.ModelViewSet): | ||||
|     search_fields = ['$name', ] | ||||
|  | ||||
|  | ||||
| class MembershipViewSet(viewsets.ModelViewSet): | ||||
| class MembershipViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer, | ||||
|   | ||||
| @@ -6,12 +6,21 @@ from crispy_forms.helper import FormHelper | ||||
| from crispy_forms.layout import Layout | ||||
| from dal import autocomplete | ||||
| from django import forms | ||||
| from django.contrib.auth.forms import UserCreationForm | ||||
| from django.contrib.auth.forms import UserCreationForm, AuthenticationForm | ||||
| from django.contrib.auth.models import User | ||||
| from permission.models import PermissionMask | ||||
|  | ||||
| from .models import Profile, Club, Membership | ||||
|  | ||||
|  | ||||
| class CustomAuthenticationForm(AuthenticationForm): | ||||
|     permission_mask = forms.ModelChoiceField( | ||||
|         label="Masque de permissions", | ||||
|         queryset=PermissionMask.objects.order_by("rank"), | ||||
|         empty_label=None, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class SignUpForm(UserCreationForm): | ||||
|     def __init__(self, *args, **kwargs): | ||||
|         super().__init__(*args, **kwargs) | ||||
|   | ||||
| @@ -1,6 +1,8 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import datetime | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.db import models | ||||
| from django.urls import reverse, reverse_lazy | ||||
| @@ -150,16 +152,13 @@ class Membership(models.Model): | ||||
|         verbose_name=_('fee'), | ||||
|     ) | ||||
|  | ||||
|     def valid(self): | ||||
|         if self.date_end is not None: | ||||
|             return self.date_start.toordinal() <= datetime.datetime.now().toordinal() < self.date_end.toordinal() | ||||
|         else: | ||||
|             return self.date_start.toordinal() <= datetime.datetime.now().toordinal() | ||||
|  | ||||
|     class Meta: | ||||
|         verbose_name = _('membership') | ||||
|         verbose_name_plural = _('memberships') | ||||
|         indexes = [models.Index(fields=['user'])] | ||||
|  | ||||
| # @receiver(post_save, sender=settings.AUTH_USER_MODEL) | ||||
| # def save_user_profile(instance, created, **_kwargs): | ||||
| #     """ | ||||
| #     Hook to save an user profile when an user is updated | ||||
| #     """ | ||||
| #     if created: | ||||
| #         Profile.objects.create(user=instance) | ||||
| #     instance.profile.save() | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from django.conf import settings | ||||
| from django.contrib import messages | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.models import User | ||||
| from django.contrib.auth.views import LoginView | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db.models import Q | ||||
| from django.http import HttpResponseRedirect | ||||
| @@ -23,13 +24,23 @@ from note.forms import AliasForm, ImageForm | ||||
| from note.models import Alias, NoteUser | ||||
| from note.models.transactions import Transaction | ||||
| from note.tables import HistoryTable, AliasTable | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
| from .filters import UserFilter, UserFilterFormHelper | ||||
| from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper | ||||
| from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper, \ | ||||
|     CustomAuthenticationForm | ||||
| from .models import Club, Membership | ||||
| from .tables import ClubTable, UserTable | ||||
|  | ||||
|  | ||||
| class CustomLoginView(LoginView): | ||||
|     form_class = CustomAuthenticationForm | ||||
|  | ||||
|     def form_valid(self, form): | ||||
|         self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank | ||||
|         return super().form_valid(form) | ||||
|  | ||||
|  | ||||
| class UserCreateView(CreateView): | ||||
|     """ | ||||
|     Une vue pour inscrire un utilisateur et lui créer un profile | ||||
| @@ -120,6 +131,9 @@ class UserDetailView(LoginRequiredMixin, DetailView): | ||||
|     context_object_name = "user_object" | ||||
|     template_name = "member/profile_detail.html" | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         user = context['user_object'] | ||||
| @@ -147,7 +161,7 @@ class UserListView(LoginRequiredMixin, SingleTableView): | ||||
|     formhelper_class = UserFilterFormHelper | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         qs = super().get_queryset() | ||||
|         qs = super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, User, "view")) | ||||
|         self.filter = self.filter_class(self.request.GET, queryset=qs) | ||||
|         self.filter.form.helper = self.formhelper_class() | ||||
|         return self.filter.qs | ||||
| @@ -203,7 +217,6 @@ class DeleteAliasView(LoginRequiredMixin, DeleteView): | ||||
|         return HttpResponseRedirect(self.get_success_url()) | ||||
|  | ||||
|     def get_success_url(self): | ||||
|         print(self.request) | ||||
|         return reverse_lazy('member:user_alias', kwargs={'pk': self.object.note.user.pk}) | ||||
|  | ||||
|     def get(self, request, *args, **kwargs): | ||||
| @@ -297,7 +310,7 @@ class UserAutocomplete(autocomplete.Select2QuerySetView): | ||||
|         if not self.request.user.is_authenticated: | ||||
|             return User.objects.none() | ||||
|  | ||||
|         qs = User.objects.all() | ||||
|         qs = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view")).all() | ||||
|  | ||||
|         if self.q: | ||||
|             qs = qs.filter(username__regex="^" + self.q) | ||||
| @@ -328,11 +341,17 @@ class ClubListView(LoginRequiredMixin, SingleTableView): | ||||
|     model = Club | ||||
|     table_class = ClubTable | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) | ||||
|  | ||||
|  | ||||
| class ClubDetailView(LoginRequiredMixin, DetailView): | ||||
|     model = Club | ||||
|     context_object_name = "club" | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         club = context["club"] | ||||
| @@ -351,6 +370,11 @@ class ClubAddMemberView(LoginRequiredMixin, CreateView): | ||||
|     form_class = MembershipForm | ||||
|     template_name = 'member/add_members.html' | ||||
|  | ||||
|     def get_queryset(self, **kwargs): | ||||
|         return super().get_queryset().filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view") | ||||
|                                              | PermissionBackend.filter_queryset(self.request.user, Membership, | ||||
|                                                                                  "change")) | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         context = super().get_context_data(**kwargs) | ||||
|         context['formset'] = MemberFormSet() | ||||
|   | ||||
| @@ -8,7 +8,7 @@ from polymorphic.admin import PolymorphicChildModelAdmin, \ | ||||
|  | ||||
| from .models.notes import Alias, Note, NoteClub, NoteSpecial, NoteUser | ||||
| from .models.transactions import Transaction, TemplateCategory, TransactionTemplate, \ | ||||
|     TemplateTransaction, MembershipTransaction | ||||
|     RecurrentTransaction, MembershipTransaction | ||||
|  | ||||
|  | ||||
| class AliasInlines(admin.TabularInline): | ||||
| @@ -102,7 +102,7 @@ class TransactionAdmin(PolymorphicParentModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for Transaction | ||||
|     """ | ||||
|     child_models = (TemplateTransaction, MembershipTransaction) | ||||
|     child_models = (RecurrentTransaction, MembershipTransaction) | ||||
|     list_display = ('created_at', 'poly_source', 'poly_destination', | ||||
|                     'quantity', 'amount', 'valid') | ||||
|     list_filter = ('valid',) | ||||
|   | ||||
| @@ -6,7 +6,7 @@ from rest_polymorphic.serializers import PolymorphicSerializer | ||||
|  | ||||
| from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias | ||||
| from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction, TemplateCategory, \ | ||||
|     TemplateTransaction, SpecialTransaction | ||||
|     RecurrentTransaction, SpecialTransaction | ||||
|  | ||||
|  | ||||
| class NoteSerializer(serializers.ModelSerializer): | ||||
| @@ -18,6 +18,7 @@ class NoteSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = Note | ||||
|         fields = '__all__' | ||||
|         read_only_fields = [f.name for f in model._meta.get_fields()]  # Notes are read-only protected | ||||
|  | ||||
|  | ||||
| class NoteClubSerializer(serializers.ModelSerializer): | ||||
| @@ -30,6 +31,7 @@ class NoteClubSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = NoteClub | ||||
|         fields = '__all__' | ||||
|         read_only_fields = ('note', 'club', ) | ||||
|  | ||||
|     def get_name(self, obj): | ||||
|         return str(obj) | ||||
| @@ -45,6 +47,7 @@ class NoteSpecialSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = NoteSpecial | ||||
|         fields = '__all__' | ||||
|         read_only_fields = ('note', ) | ||||
|  | ||||
|     def get_name(self, obj): | ||||
|         return str(obj) | ||||
| @@ -60,6 +63,7 @@ class NoteUserSerializer(serializers.ModelSerializer): | ||||
|     class Meta: | ||||
|         model = NoteUser | ||||
|         fields = '__all__' | ||||
|         read_only_fields = ('note', 'user', ) | ||||
|  | ||||
|     def get_name(self, obj): | ||||
|         return str(obj) | ||||
| @@ -70,14 +74,11 @@ class AliasSerializer(serializers.ModelSerializer): | ||||
|     REST API Serializer for Aliases. | ||||
|     The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API. | ||||
|     """ | ||||
|     note = serializers.SerializerMethodField() | ||||
|  | ||||
|     class Meta: | ||||
|         model = Alias | ||||
|         fields = '__all__' | ||||
|  | ||||
|     def get_note(self, alias): | ||||
|         return NotePolymorphicSerializer().to_representation(alias.note) | ||||
|         read_only_fields = ('note', ) | ||||
|  | ||||
|  | ||||
| class NotePolymorphicSerializer(PolymorphicSerializer): | ||||
| @@ -88,6 +89,9 @@ class NotePolymorphicSerializer(PolymorphicSerializer): | ||||
|         NoteSpecial: NoteSpecialSerializer | ||||
|     } | ||||
|  | ||||
|     class Meta: | ||||
|         model = Note | ||||
|  | ||||
|  | ||||
| class TemplateCategorySerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
| @@ -122,14 +126,14 @@ class TransactionSerializer(serializers.ModelSerializer): | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| class TemplateTransactionSerializer(serializers.ModelSerializer): | ||||
| class RecurrentTransactionSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Transactions. | ||||
|     The djangorestframework plugin will analyse the model `TemplateTransaction` and parse all fields in the API. | ||||
|     The djangorestframework plugin will analyse the model `RecurrentTransaction` and parse all fields in the API. | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = TemplateTransaction | ||||
|         model = RecurrentTransaction | ||||
|         fields = '__all__' | ||||
|  | ||||
|  | ||||
| @@ -158,7 +162,10 @@ class SpecialTransactionSerializer(serializers.ModelSerializer): | ||||
| class TransactionPolymorphicSerializer(PolymorphicSerializer): | ||||
|     model_serializer_mapping = { | ||||
|         Transaction: TransactionSerializer, | ||||
|         TemplateTransaction: TemplateTransactionSerializer, | ||||
|         RecurrentTransaction: RecurrentTransactionSerializer, | ||||
|         MembershipTransaction: MembershipTransactionSerializer, | ||||
|         SpecialTransaction: SpecialTransactionSerializer, | ||||
|     } | ||||
|  | ||||
|     class Meta: | ||||
|         model = Transaction | ||||
|   | ||||
| @@ -3,57 +3,16 @@ | ||||
|  | ||||
| from django.db.models import Q | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
| from rest_framework import viewsets | ||||
| from rest_framework.filters import OrderingFilter, SearchFilter | ||||
| from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet | ||||
|  | ||||
| from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \ | ||||
|     NoteUserSerializer, AliasSerializer, \ | ||||
|     TemplateCategorySerializer, TransactionTemplateSerializer, TransactionPolymorphicSerializer | ||||
| from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias | ||||
| from .serializers import NotePolymorphicSerializer, AliasSerializer, TemplateCategorySerializer, \ | ||||
|     TransactionTemplateSerializer, TransactionPolymorphicSerializer | ||||
| from ..models.notes import Note, Alias | ||||
| from ..models.transactions import TransactionTemplate, Transaction, TemplateCategory | ||||
|  | ||||
|  | ||||
| class NoteViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/note/ | ||||
|     """ | ||||
|     queryset = Note.objects.all() | ||||
|     serializer_class = NoteSerializer | ||||
|  | ||||
|  | ||||
| class NoteClubViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/club/ | ||||
|     """ | ||||
|     queryset = NoteClub.objects.all() | ||||
|     serializer_class = NoteClubSerializer | ||||
|  | ||||
|  | ||||
| class NoteSpecialViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/special/ | ||||
|     """ | ||||
|     queryset = NoteSpecial.objects.all() | ||||
|     serializer_class = NoteSpecialSerializer | ||||
|  | ||||
|  | ||||
| class NoteUserViewSet(viewsets.ModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/note/user/ | ||||
|     """ | ||||
|     queryset = NoteUser.objects.all() | ||||
|     serializer_class = NoteUserSerializer | ||||
|  | ||||
|  | ||||
| class NotePolymorphicViewSet(viewsets.ModelViewSet): | ||||
| class NotePolymorphicViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer, | ||||
| @@ -70,29 +29,18 @@ class NotePolymorphicViewSet(viewsets.ModelViewSet): | ||||
|         Parse query and apply filters. | ||||
|         :return: The filtered set of requested notes | ||||
|         """ | ||||
|         queryset = Note.objects.all() | ||||
|         queryset = super().get_queryset() | ||||
|  | ||||
|         alias = self.request.query_params.get("alias", ".*") | ||||
|         queryset = queryset.filter( | ||||
|             Q(alias__name__regex="^" + alias) | ||||
|             | Q(alias__normalized_name__regex="^" + Alias.normalize(alias)) | ||||
|             | Q(alias__normalized_name__regex="^" + alias.lower())) | ||||
|  | ||||
|         note_type = self.request.query_params.get("type", None) | ||||
|         if note_type: | ||||
|             types = str(note_type).lower() | ||||
|             if "user" in types: | ||||
|                 queryset = queryset.filter(polymorphic_ctype__model="noteuser") | ||||
|             elif "club" in types: | ||||
|                 queryset = queryset.filter(polymorphic_ctype__model="noteclub") | ||||
|             elif "special" in types: | ||||
|                 queryset = queryset.filter(polymorphic_ctype__model="notespecial") | ||||
|             else: | ||||
|                 queryset = queryset.none() | ||||
|  | ||||
|         return queryset.distinct() | ||||
|  | ||||
|  | ||||
| class AliasViewSet(viewsets.ModelViewSet): | ||||
| class AliasViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer, | ||||
| @@ -110,35 +58,18 @@ class AliasViewSet(viewsets.ModelViewSet): | ||||
|         :return: The filtered set of requested aliases | ||||
|         """ | ||||
|  | ||||
|         queryset = Alias.objects.all() | ||||
|         queryset = super().get_queryset() | ||||
|  | ||||
|         alias = self.request.query_params.get("alias", ".*") | ||||
|         queryset = queryset.filter( | ||||
|             Q(name__regex="^" + alias) | Q(normalized_name__regex="^" + alias.lower())) | ||||
|  | ||||
|         note_id = self.request.query_params.get("note", None) | ||||
|         if note_id: | ||||
|             queryset = queryset.filter(id=note_id) | ||||
|  | ||||
|         note_type = self.request.query_params.get("type", None) | ||||
|         if note_type: | ||||
|             types = str(note_type).lower() | ||||
|             if "user" in types: | ||||
|                 queryset = queryset.filter( | ||||
|                     note__polymorphic_ctype__model="noteuser") | ||||
|             elif "club" in types: | ||||
|                 queryset = queryset.filter( | ||||
|                     note__polymorphic_ctype__model="noteclub") | ||||
|             elif "special" in types: | ||||
|                 queryset = queryset.filter( | ||||
|                     note__polymorphic_ctype__model="notespecial") | ||||
|             else: | ||||
|                 queryset = queryset.none() | ||||
|             Q(name__regex="^" + alias) | ||||
|             | Q(normalized_name__regex="^" + Alias.normalize(alias)) | ||||
|             | Q(normalized_name__regex="^" + alias.lower())) | ||||
|  | ||||
|         return queryset | ||||
|  | ||||
|  | ||||
| class TemplateCategoryViewSet(viewsets.ModelViewSet): | ||||
| class TemplateCategoryViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `TemplateCategory` objects, serialize it to JSON with the given serializer, | ||||
| @@ -150,7 +81,7 @@ class TemplateCategoryViewSet(viewsets.ModelViewSet): | ||||
|     search_fields = ['$name', ] | ||||
|  | ||||
|  | ||||
| class TransactionTemplateViewSet(viewsets.ModelViewSet): | ||||
| class TransactionTemplateViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer, | ||||
| @@ -162,7 +93,7 @@ class TransactionTemplateViewSet(viewsets.ModelViewSet): | ||||
|     filterset_fields = ['name', 'amount', 'display', 'category', ] | ||||
|  | ||||
|  | ||||
| class TransactionViewSet(viewsets.ModelViewSet): | ||||
| class TransactionViewSet(ReadProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer, | ||||
|   | ||||
| @@ -3,12 +3,12 @@ | ||||
|  | ||||
| from .notes import Alias, Note, NoteClub, NoteSpecial, NoteUser | ||||
| from .transactions import MembershipTransaction, Transaction, \ | ||||
|     TemplateCategory, TransactionTemplate, TemplateTransaction | ||||
|     TemplateCategory, TransactionTemplate, RecurrentTransaction | ||||
|  | ||||
| __all__ = [ | ||||
|     # Notes | ||||
|     'Alias', 'Note', 'NoteClub', 'NoteSpecial', 'NoteUser', | ||||
|     # Transactions | ||||
|     'MembershipTransaction', 'Transaction', 'TemplateCategory', 'TransactionTemplate', | ||||
|     'TemplateTransaction', | ||||
|     'RecurrentTransaction', | ||||
| ] | ||||
|   | ||||
| @@ -152,10 +152,12 @@ class Transaction(PolymorphicModel): | ||||
|             self.source.balance -= to_transfer | ||||
|             self.destination.balance += to_transfer | ||||
|  | ||||
|         # We save first the transaction, in case of the user has no right to transfer money | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|         # Save notes | ||||
|         self.source.save() | ||||
|         self.destination.save() | ||||
|         super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def total(self): | ||||
| @@ -166,7 +168,7 @@ class Transaction(PolymorphicModel): | ||||
|         return _('Transfer') | ||||
|  | ||||
|  | ||||
| class TemplateTransaction(Transaction): | ||||
| class RecurrentTransaction(Transaction): | ||||
|     """ | ||||
|     Special type of :model:`note.Transaction` associated to a :model:`note.TransactionTemplate`. | ||||
|     """ | ||||
|   | ||||
| @@ -8,9 +8,10 @@ from django.db.models import Q | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from django.views.generic import CreateView, ListView, UpdateView | ||||
| from django_tables2 import SingleTableView | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
| from .forms import TransactionTemplateForm | ||||
| from .models import Transaction, TransactionTemplate, Alias, TemplateTransaction, NoteSpecial | ||||
| from .models import Transaction, TransactionTemplate, Alias, RecurrentTransaction, NoteSpecial | ||||
| from .models.transactions import SpecialTransaction | ||||
| from .tables import HistoryTable | ||||
|  | ||||
| @@ -18,16 +19,18 @@ from .tables import HistoryTable | ||||
| class TransactionCreate(LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     Show transfer page | ||||
|  | ||||
|     TODO: If user have sufficient rights, they can transfer from an other note | ||||
|     """ | ||||
|     queryset = Transaction.objects.order_by("-id").all()[:50] | ||||
|     template_name = "note/transaction_form.html" | ||||
|  | ||||
|     # Transaction history table | ||||
|     table_class = HistoryTable | ||||
|     table_pagination = {"per_page": 50} | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         return Transaction.objects.filter(PermissionBackend.filter_queryset( | ||||
|             self.request.user, Transaction, "view") | ||||
|         ).order_by("-id").all()[:50] | ||||
|  | ||||
|     def get_context_data(self, **kwargs): | ||||
|         """ | ||||
|         Add some context variables in template such as page title | ||||
| @@ -117,25 +120,30 @@ class ConsoView(LoginRequiredMixin, SingleTableView): | ||||
|     """ | ||||
|     Consume | ||||
|     """ | ||||
|     queryset = Transaction.objects.order_by("-id").all()[:50] | ||||
|     template_name = "note/conso_form.html" | ||||
|  | ||||
|     # Transaction history table | ||||
|     table_class = HistoryTable | ||||
|     table_pagination = {"per_page": 50} | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         return Transaction.objects.filter( | ||||
|             PermissionBackend.filter_queryset(self.request.user, Transaction, "view") | ||||
|         ).order_by("-id").all()[:50] | ||||
|  | ||||
|     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(display=True) \ | ||||
|             .annotate(clicks=Count('templatetransaction')).order_by('category__name', 'name') | ||||
|         buttons = TransactionTemplate.objects.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") | ||||
|         context['polymorphic_ctype'] = ContentType.objects.get_for_model(TemplateTransaction).pk | ||||
|         context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk | ||||
|  | ||||
|         # select2 compatibility | ||||
|         context['no_cache'] = True | ||||
|   | ||||
							
								
								
									
										4
									
								
								apps/permission/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								apps/permission/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| default_app_config = 'permission.apps.PermissionConfig' | ||||
							
								
								
									
										31
									
								
								apps/permission/admin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								apps/permission/admin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-lateré | ||||
|  | ||||
| from django.contrib import admin | ||||
|  | ||||
| from .models import Permission, PermissionMask, RolePermissions | ||||
|  | ||||
|  | ||||
| @admin.register(PermissionMask) | ||||
| class PermissionMaskAdmin(admin.ModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for PermissionMask | ||||
|     """ | ||||
|     list_display = ('description', 'rank', ) | ||||
|  | ||||
|  | ||||
| @admin.register(Permission) | ||||
| class PermissionAdmin(admin.ModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for Permission | ||||
|     """ | ||||
|     list_display = ('type', 'model', 'field', 'mask', 'description', ) | ||||
|  | ||||
|  | ||||
| @admin.register(RolePermissions) | ||||
| class RolePermissionsAdmin(admin.ModelAdmin): | ||||
|     """ | ||||
|     Admin customisation for RolePermissions | ||||
|     """ | ||||
|     list_display = ('role', ) | ||||
|  | ||||
							
								
								
									
										0
									
								
								apps/permission/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/permission/api/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										17
									
								
								apps/permission/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								apps/permission/api/serializers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,17 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework import serializers | ||||
|  | ||||
| from ..models import Permission | ||||
|  | ||||
|  | ||||
| class PermissionSerializer(serializers.ModelSerializer): | ||||
|     """ | ||||
|     REST API Serializer for Permission types. | ||||
|     The djangorestframework plugin will analyse the model `Permission` and parse all fields in the API. | ||||
|     """ | ||||
|  | ||||
|     class Meta: | ||||
|         model = Permission | ||||
|         fields = '__all__' | ||||
							
								
								
									
										11
									
								
								apps/permission/api/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								apps/permission/api/urls.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from .views import PermissionViewSet | ||||
|  | ||||
|  | ||||
| def register_permission_urls(router, path): | ||||
|     """ | ||||
|     Configure router for permission REST API. | ||||
|     """ | ||||
|     router.register(path, PermissionViewSet) | ||||
							
								
								
									
										20
									
								
								apps/permission/api/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								apps/permission/api/views.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django_filters.rest_framework import DjangoFilterBackend | ||||
|  | ||||
| from api.viewsets import ReadOnlyProtectedModelViewSet | ||||
| from .serializers import PermissionSerializer | ||||
| from ..models import Permission | ||||
|  | ||||
|  | ||||
| class PermissionViewSet(ReadOnlyProtectedModelViewSet): | ||||
|     """ | ||||
|     REST API View set. | ||||
|     The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer, | ||||
|     then render it on /api/logs/ | ||||
|     """ | ||||
|     queryset = Permission.objects.all() | ||||
|     serializer_class = PermissionSerializer | ||||
|     filter_backends = [DjangoFilterBackend] | ||||
|     filterset_fields = ['model', 'type', ] | ||||
							
								
								
									
										14
									
								
								apps/permission/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								apps/permission/apps.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.apps import AppConfig | ||||
| from django.db.models.signals import pre_save, pre_delete | ||||
|  | ||||
|  | ||||
| class PermissionConfig(AppConfig): | ||||
|     name = 'permission' | ||||
|  | ||||
|     def ready(self): | ||||
|         from . import signals | ||||
|         pre_save.connect(signals.pre_save_object) | ||||
|         pre_delete.connect(signals.pre_delete_object) | ||||
							
								
								
									
										116
									
								
								apps/permission/backends.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								apps/permission/backends.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.auth.backends import ModelBackend | ||||
| from django.contrib.auth.models import User, AnonymousUser | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.db.models import Q, F | ||||
| from note.models import Note, NoteUser, NoteClub, NoteSpecial | ||||
| from note_kfet.middlewares import get_current_session | ||||
| from member.models import Membership, Club | ||||
|  | ||||
| from .models import Permission | ||||
|  | ||||
|  | ||||
| class PermissionBackend(ModelBackend): | ||||
|     """ | ||||
|     Manage permissions of users | ||||
|     """ | ||||
|     supports_object_permissions = True | ||||
|     supports_anonymous_user = False | ||||
|     supports_inactive_user = False | ||||
|  | ||||
|     @staticmethod | ||||
|     def permissions(user, model, type): | ||||
|         """ | ||||
|         List all permissions of the given user that applies to a given model and a give type | ||||
|         :param user: The owner of the permissions | ||||
|         :param model: The model that the permissions shoud apply | ||||
|         :param type: The type of the permissions: view, change, add or delete | ||||
|         :return: A generator of the requested permissions | ||||
|         """ | ||||
|         for permission in Permission.objects.annotate(club=F("rolepermissions__role__membership__club")) \ | ||||
|                 .filter( | ||||
|             rolepermissions__role__membership__user=user, | ||||
|             model__app_label=model.app_label,  # For polymorphic models, we don't filter on model type | ||||
|             type=type, | ||||
|         ).all(): | ||||
|             if not isinstance(model, permission.model.__class__): | ||||
|                 continue | ||||
|  | ||||
|             club = Club.objects.get(pk=permission.club) | ||||
|             permission = permission.about( | ||||
|                 user=user, | ||||
|                 club=club, | ||||
|                 User=User, | ||||
|                 Club=Club, | ||||
|                 Membership=Membership, | ||||
|                 Note=Note, | ||||
|                 NoteUser=NoteUser, | ||||
|                 NoteClub=NoteClub, | ||||
|                 NoteSpecial=NoteSpecial, | ||||
|                 F=F, | ||||
|                 Q=Q | ||||
|             ) | ||||
|             if permission.mask.rank <= get_current_session().get("permission_mask", 0): | ||||
|                 yield permission | ||||
|  | ||||
|     @staticmethod | ||||
|     def filter_queryset(user, model, t, field=None): | ||||
|         """ | ||||
|         Filter a queryset by considering the permissions of a given user. | ||||
|         :param user: The owner of the permissions that are fetched | ||||
|         :param model: The concerned model of the queryset | ||||
|         :param t: The type of modification (view, add, change, delete) | ||||
|         :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", 0) >= 42: | ||||
|             # Superusers have all rights | ||||
|             return Q() | ||||
|  | ||||
|         if not isinstance(model, ContentType): | ||||
|             model = ContentType.objects.get_for_model(model) | ||||
|  | ||||
|         # Never satisfied | ||||
|         query = Q(pk=-1) | ||||
|         perms = PermissionBackend.permissions(user, model, t) | ||||
|         for perm in perms: | ||||
|             if perm.field and field != perm.field: | ||||
|                 continue | ||||
|             if perm.type != t or perm.model != model: | ||||
|                 continue | ||||
|             perm.update_query() | ||||
|             query = query | perm.query | ||||
|         return query | ||||
|  | ||||
|     def has_perm(self, user_obj, perm, obj=None): | ||||
|         if user_obj is None or isinstance(user_obj, AnonymousUser): | ||||
|             return False | ||||
|  | ||||
|         if user_obj.is_superuser and get_current_session().get("permission_mask", 0) >= 42: | ||||
|             return True | ||||
|  | ||||
|         if obj is None: | ||||
|             return True | ||||
|  | ||||
|         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 self.permissions(user_obj, ct, perm_type)): | ||||
|             return True | ||||
|         return False | ||||
|  | ||||
|     def has_module_perms(self, user_obj, app_label): | ||||
|         return False | ||||
|  | ||||
|     def get_all_permissions(self, user_obj, obj=None): | ||||
|         ct = ContentType.objects.get_for_model(obj) | ||||
|         return list(self.permissions(user_obj, ct, "view")) | ||||
							
								
								
									
										554
									
								
								apps/permission/fixtures/initial.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										554
									
								
								apps/permission/fixtures/initial.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,554 @@ | ||||
| [ | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 1, | ||||
|     "fields": { | ||||
|       "name": "Adh\u00e9rent BDE" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 2, | ||||
|     "fields": { | ||||
|       "name": "Adh\u00e9rent Kfet" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 3, | ||||
|     "fields": { | ||||
|       "name": "Pr\u00e9sident\u00b7e BDE" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 4, | ||||
|     "fields": { | ||||
|       "name": "Tr\u00e9sorier\u00b7\u00e8re BDE" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 5, | ||||
|     "fields": { | ||||
|       "name": "Respo info" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 6, | ||||
|     "fields": { | ||||
|       "name": "GC Kfet" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 7, | ||||
|     "fields": { | ||||
|       "name": "Pr\u00e9sident\u00b7e de club" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "member.role", | ||||
|     "pk": 8, | ||||
|     "fields": { | ||||
|       "name": "Tr\u00e9sorier\u00b7\u00e8re de club" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permissionmask", | ||||
|     "pk": 1, | ||||
|     "fields": { | ||||
|       "rank": 0, | ||||
|       "description": "Droits basiques" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permissionmask", | ||||
|     "pk": 2, | ||||
|     "fields": { | ||||
|       "rank": 1, | ||||
|       "description": "Droits note seulement" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permissionmask", | ||||
|     "pk": 3, | ||||
|     "fields": { | ||||
|       "rank": 42, | ||||
|       "description": "Tous mes droits" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 1, | ||||
|     "fields": { | ||||
|       "model": 21, | ||||
|       "query": "{\"pk\": [\"user\", \"pk\"]}", | ||||
|       "type": "view", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "View our User object" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 2, | ||||
|     "fields": { | ||||
|       "model": 31, | ||||
|       "query": "{\"user\": [\"user\"]}", | ||||
|       "type": "view", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "View our profile" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 3, | ||||
|     "fields": { | ||||
|       "model": 34, | ||||
|       "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", | ||||
|       "type": "view", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "View our own note" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 4, | ||||
|     "fields": { | ||||
|       "model": 25, | ||||
|       "query": "{\"user\": [\"user\"]}", | ||||
|       "type": "view", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "View our API token" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 5, | ||||
|     "fields": { | ||||
|       "model": 36, | ||||
|       "query": "[\"OR\", {\"source\": [\"user\", \"note\"]}, {\"destination\": [\"user\", \"note\"]}]", | ||||
|       "type": "view", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "View our own transactions" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 6, | ||||
|     "fields": { | ||||
|       "model": 33, | ||||
|       "query": "[\"OR\", {\"note__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club__name\": \"Kfet\"}], [\"all\"]]}, {\"note__in\": [\"NoteClub\", \"objects\", [\"all\"]]}]", | ||||
|       "type": "view", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "View aliases of clubs and members of Kfet club" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 7, | ||||
|     "fields": { | ||||
|       "model": 21, | ||||
|       "query": "{\"pk\": [\"user\", \"pk\"]}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "last_login", | ||||
|       "description": "Change myself's last login" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 8, | ||||
|     "fields": { | ||||
|       "model": 21, | ||||
|       "query": "{\"pk\": [\"user\", \"pk\"]}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "username", | ||||
|       "description": "Change myself's username" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 9, | ||||
|     "fields": { | ||||
|       "model": 21, | ||||
|       "query": "{\"pk\": [\"user\", \"pk\"]}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "first_name", | ||||
|       "description": "Change myself's first name" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 10, | ||||
|     "fields": { | ||||
|       "model": 21, | ||||
|       "query": "{\"pk\": [\"user\", \"pk\"]}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "last_name", | ||||
|       "description": "Change myself's last name" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 11, | ||||
|     "fields": { | ||||
|       "model": 21, | ||||
|       "query": "{\"pk\": [\"user\", \"pk\"]}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "email", | ||||
|       "description": "Change myself's email" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 12, | ||||
|     "fields": { | ||||
|       "model": 25, | ||||
|       "query": "{\"user\": [\"user\"]}", | ||||
|       "type": "delete", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "Delete API Token" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 13, | ||||
|     "fields": { | ||||
|       "model": 25, | ||||
|       "query": "{\"user\": [\"user\"]}", | ||||
|       "type": "add", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "Create API Token" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 14, | ||||
|     "fields": { | ||||
|       "model": 33, | ||||
|       "query": "{\"note\": [\"user\", \"note\"]}", | ||||
|       "type": "delete", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "Remove alias" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 15, | ||||
|     "fields": { | ||||
|       "model": 33, | ||||
|       "query": "{\"note\": [\"user\", \"note\"]}", | ||||
|       "type": "add", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "Add alias" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 16, | ||||
|     "fields": { | ||||
|       "model": 34, | ||||
|       "query": "{\"pk\": [\"user\", \"note\", \"pk\"]}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "display_image", | ||||
|       "description": "Change myself's display image" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 17, | ||||
|     "fields": { | ||||
|       "model": 36, | ||||
|       "query": "[\"AND\", {\"source\": [\"user\", \"note\"]}, {\"amount__lte\": [\"user\", \"note\", \"balance\"]}]", | ||||
|       "type": "add", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "Transfer from myself's note" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 18, | ||||
|     "fields": { | ||||
|       "model": 34, | ||||
|       "query": "{}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "balance", | ||||
|       "description": "Update a note balance with a transaction" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 19, | ||||
|     "fields": { | ||||
|       "model": 34, | ||||
|       "query": "[\"OR\", {\"pk\": [\"club\", \"note\", \"pk\"]}, {\"pk__in\": [\"NoteUser\", \"objects\", [\"filter\", {\"user__membership__club\": [\"club\"]}], [\"all\"]]}]", | ||||
|       "type": "view", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "View notes of club members" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 20, | ||||
|     "fields": { | ||||
|       "model": 36, | ||||
|       "query": "[\"AND\", [\"OR\", {\"source\": [\"club\", \"note\"]}, {\"destination\": [\"club\", \"note\"]}], {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]", | ||||
|       "type": "add", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "Create transactions with a club" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 21, | ||||
|     "fields": { | ||||
|       "model": 42, | ||||
|       "query": "[\"AND\", {\"destination\": [\"club\", \"note\"]}, {\"amount__lte\": {\"F\": [\"ADD\", [\"F\", \"source__balance\"], 5000]}}]", | ||||
|       "type": "add", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "Create transactions from buttons with a club" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 22, | ||||
|     "fields": { | ||||
|       "model": 29, | ||||
|       "query": "{\"pk\": [\"club\", \"pk\"]}", | ||||
|       "type": "view", | ||||
|       "mask": 1, | ||||
|       "field": "", | ||||
|       "description": "View club infos" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 23, | ||||
|     "fields": { | ||||
|       "model": 36, | ||||
|       "query": "{}", | ||||
|       "type": "change", | ||||
|       "mask": 1, | ||||
|       "field": "valid", | ||||
|       "description": "Update validation status of a transaction" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 24, | ||||
|     "fields": { | ||||
|       "model": 36, | ||||
|       "query": "{}", | ||||
|       "type": "view", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "View all transactions" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 25, | ||||
|     "fields": { | ||||
|       "model": 40, | ||||
|       "query": "{}", | ||||
|       "type": "view", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "Display credit/debit interface" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 26, | ||||
|     "fields": { | ||||
|       "model": 43, | ||||
|       "query": "{}", | ||||
|       "type": "add", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "Create credit/debit transaction" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 27, | ||||
|     "fields": { | ||||
|       "model": 35, | ||||
|       "query": "{}", | ||||
|       "type": "view", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "View button categories" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 28, | ||||
|     "fields": { | ||||
|       "model": 35, | ||||
|       "query": "{}", | ||||
|       "type": "change", | ||||
|       "mask": 3, | ||||
|       "field": "", | ||||
|       "description": "Change button category" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 29, | ||||
|     "fields": { | ||||
|       "model": 35, | ||||
|       "query": "{}", | ||||
|       "type": "add", | ||||
|       "mask": 3, | ||||
|       "field": "", | ||||
|       "description": "Add button category" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 30, | ||||
|     "fields": { | ||||
|       "model": 37, | ||||
|       "query": "{}", | ||||
|       "type": "view", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "View buttons" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 31, | ||||
|     "fields": { | ||||
|       "model": 37, | ||||
|       "query": "{}", | ||||
|       "type": "add", | ||||
|       "mask": 3, | ||||
|       "field": "", | ||||
|       "description": "Add buttons" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 32, | ||||
|     "fields": { | ||||
|       "model": 37, | ||||
|       "query": "{}", | ||||
|       "type": "change", | ||||
|       "mask": 3, | ||||
|       "field": "", | ||||
|       "description": "Update buttons" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.permission", | ||||
|     "pk": 33, | ||||
|     "fields": { | ||||
|       "model": 36, | ||||
|       "query": "{}", | ||||
|       "type": "add", | ||||
|       "mask": 2, | ||||
|       "field": "", | ||||
|       "description": "Create any transaction" | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.rolepermissions", | ||||
|     "pk": 1, | ||||
|     "fields": { | ||||
|       "role": 1, | ||||
|       "permissions": [ | ||||
|         1, | ||||
|         2, | ||||
|         7, | ||||
|         8, | ||||
|         9, | ||||
|         10, | ||||
|         11 | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.rolepermissions", | ||||
|     "pk": 2, | ||||
|     "fields": { | ||||
|       "role": 2, | ||||
|       "permissions": [ | ||||
|         1, | ||||
|         2, | ||||
|         3, | ||||
|         4, | ||||
|         5, | ||||
|         6, | ||||
|         7, | ||||
|         8, | ||||
|         9, | ||||
|         10, | ||||
|         11, | ||||
|         12, | ||||
|         13, | ||||
|         14, | ||||
|         15, | ||||
|         16, | ||||
|         17, | ||||
|         18 | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.rolepermissions", | ||||
|     "pk": 3, | ||||
|     "fields": { | ||||
|       "role": 8, | ||||
|       "permissions": [ | ||||
|         19, | ||||
|         20, | ||||
|         21, | ||||
|         22 | ||||
|       ] | ||||
|     } | ||||
|   }, | ||||
|   { | ||||
|     "model": "permission.rolepermissions", | ||||
|     "pk": 4, | ||||
|     "fields": { | ||||
|       "role": 4, | ||||
|       "permissions": [ | ||||
|         23, | ||||
|         24, | ||||
|         25, | ||||
|         26, | ||||
|         27, | ||||
|         28, | ||||
|         29, | ||||
|         30, | ||||
|         31, | ||||
|         32, | ||||
|         33 | ||||
|       ] | ||||
|     } | ||||
|   } | ||||
| ] | ||||
							
								
								
									
										0
									
								
								apps/permission/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/permission/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										284
									
								
								apps/permission/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								apps/permission/models.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,284 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| import functools | ||||
| import json | ||||
| import operator | ||||
|  | ||||
| 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: | ||||
|  | ||||
|     def __init__(self, model, query, type, field, mask, **kwargs): | ||||
|         self.model = model | ||||
|         self.raw_query = query | ||||
|         self.query = None | ||||
|         self.type = type | ||||
|         self.field = field | ||||
|         self.mask = mask | ||||
|         self.kwargs = kwargs | ||||
|  | ||||
|     def applies(self, obj, permission_type, field_name=None): | ||||
|         """ | ||||
|         Returns True if the permission applies to | ||||
|         the field `field_name` object `obj` | ||||
|         """ | ||||
|  | ||||
|         if not isinstance(obj, self.model.model_class()): | ||||
|             # The permission does not apply to the model | ||||
|             return False | ||||
|  | ||||
|         if self.type == 'add': | ||||
|             if permission_type == self.type: | ||||
|                 self.update_query() | ||||
|  | ||||
|                 # Don't increase indexes | ||||
|                 obj.pk = 0 | ||||
|                 # Force insertion, no data verification, no trigger | ||||
|                 Model.save(obj, force_insert=True) | ||||
|                 ret = obj in self.model.model_class().objects.filter(self.query).all() | ||||
|                 # Delete testing object | ||||
|                 Model.delete(obj) | ||||
|                 return ret | ||||
|  | ||||
|         if permission_type == self.type: | ||||
|             if self.field and field_name != self.field: | ||||
|                 return False | ||||
|             self.update_query() | ||||
|             return obj in self.model.model_class().objects.filter(self.query).all() | ||||
|         else: | ||||
|             return False | ||||
|  | ||||
|     def update_query(self): | ||||
|         """ | ||||
|         The query is not analysed in a first time. It is analysed at most once if needed. | ||||
|         :return: | ||||
|         """ | ||||
|         if not self.query: | ||||
|             # noinspection PyProtectedMember | ||||
|             self.query = Permission._about(self.raw_query, **self.kwargs) | ||||
|  | ||||
|     def __repr__(self): | ||||
|         if self.field: | ||||
|             return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query) | ||||
|         else: | ||||
|             return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.__repr__() | ||||
|  | ||||
|  | ||||
| class PermissionMask(models.Model): | ||||
|     """ | ||||
|     Permissions that are hidden behind a mask | ||||
|     """ | ||||
|  | ||||
|     rank = models.PositiveSmallIntegerField( | ||||
|         unique=True, | ||||
|         verbose_name=_('rank'), | ||||
|     ) | ||||
|  | ||||
|     description = models.CharField( | ||||
|         max_length=255, | ||||
|         unique=True, | ||||
|         verbose_name=_('description'), | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return self.description | ||||
|  | ||||
|  | ||||
| class Permission(models.Model): | ||||
|  | ||||
|     PERMISSION_TYPES = [ | ||||
|         ('add', 'add'), | ||||
|         ('view', 'view'), | ||||
|         ('change', 'change'), | ||||
|         ('delete', 'delete') | ||||
|     ] | ||||
|  | ||||
|     model = models.ForeignKey(ContentType, on_delete=models.CASCADE, related_name='+') | ||||
|  | ||||
|     # A json encoded Q object with the following grammar | ||||
|     #  query -> [] | {}  (the empty query representing all objects) | ||||
|     #  query -> ["AND", query, …]            AND multiple queries | ||||
|     #         | ["OR", query, …]             OR multiple queries | ||||
|     #         | ["NOT", query]               Opposite of query | ||||
|     #  query -> {key: value, …}              A list of fields and values of a Q object | ||||
|     #  key   -> string                       A field name | ||||
|     #  value -> int | string | bool | null   Literal values | ||||
|     #         | [parameter, …]               A parameter. See compute_param for more details. | ||||
|     #         | {"F": oper}                  An F object | ||||
|     #  oper  -> [string, …]                  A parameter. See compute_param for more details. | ||||
|     #         | ["ADD", oper, …]             Sum multiple F objects or literal | ||||
|     #         | ["SUB", oper, oper]          Substract two F objects or literal | ||||
|     #         | ["MUL", oper, …]             Multiply F objects or literals | ||||
|     #         | int | string | bool | null   Literal values | ||||
|     #         | ["F", string]                A field | ||||
|     # | ||||
|     # Examples: | ||||
|     #  Q(is_superuser=True)  := {"is_superuser": true} | ||||
|     #  ~Q(is_superuser=True) := ["NOT", {"is_superuser": true}] | ||||
|     query = models.TextField() | ||||
|  | ||||
|     type = models.CharField(max_length=15, choices=PERMISSION_TYPES) | ||||
|  | ||||
|     mask = models.ForeignKey( | ||||
|         PermissionMask, | ||||
|         on_delete=models.PROTECT, | ||||
|     ) | ||||
|  | ||||
|     field = models.CharField(max_length=255, blank=True) | ||||
|  | ||||
|     description = models.CharField(max_length=255, blank=True) | ||||
|  | ||||
|     class Meta: | ||||
|         unique_together = ('model', 'query', 'type', 'field') | ||||
|  | ||||
|     def clean(self): | ||||
|         self.query = json.dumps(json.loads(self.query)) | ||||
|         if self.field and self.type not in {'view', 'change'}: | ||||
|             raise ValidationError(_("Specifying field applies only to view and change permission types.")) | ||||
|  | ||||
|     def save(self, **kwargs): | ||||
|         self.full_clean() | ||||
|         super().save() | ||||
|  | ||||
|     @staticmethod | ||||
|     def compute_f(oper, **kwargs): | ||||
|         if isinstance(oper, list): | ||||
|             if oper[0] == 'ADD': | ||||
|                 return functools.reduce(operator.add, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]]) | ||||
|             elif oper[0] == 'SUB': | ||||
|                 return Permission.compute_f(oper[1], **kwargs) - Permission.compute_f(oper[2], **kwargs) | ||||
|             elif oper[0] == 'MUL': | ||||
|                 return functools.reduce(operator.mul, [Permission.compute_f(oper, **kwargs) for oper in oper[1:]]) | ||||
|             elif oper[0] == 'F': | ||||
|                 return F(oper[1]) | ||||
|             else: | ||||
|                 field = kwargs[oper[0]] | ||||
|                 for i in range(1, len(oper)): | ||||
|                     field = getattr(field, oper[i]) | ||||
|                 return field | ||||
|         else: | ||||
|             return oper | ||||
|  | ||||
|     @staticmethod | ||||
|     def compute_param(value, **kwargs): | ||||
|         """ | ||||
|         A parameter is given by a list. The first argument is the name of the parameter. | ||||
|         The parameters are the user, the club, and some classes (Note, ...) | ||||
|         If there are more arguments in the list, then attributes are queried. | ||||
|         For example, ["user", "note", "balance"] will return the balance of the note of the user. | ||||
|         If an argument is a list, then this is interpreted with a function call: | ||||
|             First argument is the name of the function, next arguments are parameters, and if there is a dict, | ||||
|             then the dict is given as kwargs. | ||||
|             For example: NoteUser.objects.filter(user__memberships__club__name="Kfet").all() is translated by: | ||||
|             ["NoteUser", "objects", ["filter", {"user__memberships__club__name": "Kfet"}], ["all"]] | ||||
|         """ | ||||
|  | ||||
|         if not isinstance(value, list): | ||||
|             return value | ||||
|  | ||||
|         field = kwargs[value[0]] | ||||
|         for i in range(1, len(value)): | ||||
|             if isinstance(value[i], list): | ||||
|                 if value[i][0] in kwargs: | ||||
|                     field = Permission.compute_param(value[i], **kwargs) | ||||
|                     continue | ||||
|  | ||||
|                 field = getattr(field, value[i][0]) | ||||
|                 params = [] | ||||
|                 call_kwargs = {} | ||||
|                 for j in range(1, len(value[i])): | ||||
|                     param = Permission.compute_param(value[i][j], **kwargs) | ||||
|                     if isinstance(param, dict): | ||||
|                         for key in param: | ||||
|                             val = Permission.compute_param(param[key], **kwargs) | ||||
|                             call_kwargs[key] = val | ||||
|                     else: | ||||
|                         params.append(param) | ||||
|                 field = field(*params, **call_kwargs) | ||||
|             else: | ||||
|                 field = getattr(field, value[i]) | ||||
|         return field | ||||
|  | ||||
|     @staticmethod | ||||
|     def _about(query, **kwargs): | ||||
|         """ | ||||
|         Translate JSON query into a Q query. | ||||
|         :param query: The JSON query | ||||
|         :param kwargs: Additional params | ||||
|         :return: A Q object | ||||
|         """ | ||||
|         if len(query) == 0: | ||||
|             # The query is either [] or {} and | ||||
|             # applies to all objects of the model | ||||
|             # to represent this we return a trivial request | ||||
|             return Q(pk=F("pk")) | ||||
|         if isinstance(query, list): | ||||
|             if query[0] == 'AND': | ||||
|                 return functools.reduce(operator.and_, [Permission._about(query, **kwargs) for query in query[1:]]) | ||||
|             elif query[0] == 'OR': | ||||
|                 return functools.reduce(operator.or_, [Permission._about(query, **kwargs) for query in query[1:]]) | ||||
|             elif query[0] == 'NOT': | ||||
|                 return ~Permission._about(query[1], **kwargs) | ||||
|             else: | ||||
|                 return Q(pk=F("pk")) | ||||
|         elif isinstance(query, dict): | ||||
|             q_kwargs = {} | ||||
|             for key in query: | ||||
|                 value = query[key] | ||||
|                 if isinstance(value, list): | ||||
|                     # It is a parameter we query its return value | ||||
|                     q_kwargs[key] = Permission.compute_param(value, **kwargs) | ||||
|                 elif isinstance(value, dict): | ||||
|                     # It is an F object | ||||
|                     q_kwargs[key] = Permission.compute_f(value['F'], **kwargs) | ||||
|                 else: | ||||
|                     q_kwargs[key] = value | ||||
|             return Q(**q_kwargs) | ||||
|         else: | ||||
|             # TODO: find a better way to crash here | ||||
|             raise Exception("query {} is wrong".format(query)) | ||||
|  | ||||
|     def about(self, **kwargs): | ||||
|         """ | ||||
|         Return an InstancedPermission with the parameters | ||||
|         replaced by their values and the query interpreted | ||||
|         """ | ||||
|         query = json.loads(self.query) | ||||
|         # query = self._about(query, **kwargs) | ||||
|         return InstancedPermission(self.model, query, self.type, self.field, self.mask, **kwargs) | ||||
|  | ||||
|     def __str__(self): | ||||
|         if self.field: | ||||
|             return _("Can {type} {model}.{field} in {query}").format(type=self.type, model=self.model, field=self.field, query=self.query) | ||||
|         else: | ||||
|             return _("Can {type} {model} in {query}").format(type=self.type, model=self.model, query=self.query) | ||||
|  | ||||
|  | ||||
| class RolePermissions(models.Model): | ||||
|     """ | ||||
|     Permissions associated with a Role | ||||
|     """ | ||||
|     role = models.ForeignKey( | ||||
|         Role, | ||||
|         on_delete=models.PROTECT, | ||||
|         related_name='+', | ||||
|         verbose_name=_('role'), | ||||
|     ) | ||||
|     permissions = models.ManyToManyField( | ||||
|         Permission, | ||||
|     ) | ||||
|  | ||||
|     def __str__(self): | ||||
|         return str(self.role) | ||||
|  | ||||
							
								
								
									
										63
									
								
								apps/permission/permissions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								apps/permission/permissions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from rest_framework.permissions import DjangoObjectPermissions | ||||
|  | ||||
| SAFE_METHODS = ('HEAD', 'OPTIONS', ) | ||||
|  | ||||
|  | ||||
| class StrongDjangoObjectPermissions(DjangoObjectPermissions): | ||||
|     """ | ||||
|     Default DjangoObjectPermissions grant view permission to all. | ||||
|     This is a simple patch of this class that controls view access. | ||||
|     """ | ||||
|  | ||||
|     perms_map = { | ||||
|         'GET': ['%(app_label)s.view_%(model_name)s'], | ||||
|         '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'], | ||||
|         'DELETE': ['%(app_label)s.delete_%(model_name)s'], | ||||
|     } | ||||
|  | ||||
|     def get_required_object_permissions(self, method, model_cls): | ||||
|         kwargs = { | ||||
|             'app_label': model_cls._meta.app_label, | ||||
|             'model_name': model_cls._meta.model_name | ||||
|         } | ||||
|  | ||||
|         if method not in self.perms_map: | ||||
|             from rest_framework import exceptions | ||||
|             raise exceptions.MethodNotAllowed(method) | ||||
|  | ||||
|         return [perm % kwargs for perm in self.perms_map[method]] | ||||
|  | ||||
|     def has_object_permission(self, request, view, obj): | ||||
|         # authentication checks have already executed via has_permission | ||||
|         queryset = self._queryset(view) | ||||
|         model_cls = queryset.model | ||||
|         user = request.user | ||||
|  | ||||
|         perms = self.get_required_object_permissions(request.method, model_cls) | ||||
|  | ||||
|         if not user.has_perms(perms, obj): | ||||
|             # If the user does not have permissions we need to determine if | ||||
|             # they have read permissions to see 403, or not, and simply see | ||||
|             # a 404 response. | ||||
|             from django.http import Http404 | ||||
|  | ||||
|             if request.method in SAFE_METHODS: | ||||
|                 # Read permissions already checked and failed, no need | ||||
|                 # to make another lookup. | ||||
|                 raise Http404 | ||||
|  | ||||
|             read_perms = self.get_required_object_permissions('GET', model_cls) | ||||
|             if not user.has_perms(read_perms, obj): | ||||
|                 raise Http404 | ||||
|  | ||||
|             # Has read permissions. | ||||
|             return False | ||||
|  | ||||
|         return True | ||||
							
								
								
									
										106
									
								
								apps/permission/signals.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								apps/permission/signals.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,106 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.core.exceptions import PermissionDenied | ||||
| from django.db.models.signals import pre_save, pre_delete, post_save, post_delete | ||||
|  | ||||
| from logs import signals as logs_signals | ||||
| from permission.backends import PermissionBackend | ||||
| from note_kfet.middlewares import get_current_authenticated_user | ||||
|  | ||||
|  | ||||
| EXCLUDED = [ | ||||
|     'cas_server.proxygrantingticket', | ||||
|     'cas_server.proxyticket', | ||||
|     'cas_server.serviceticket', | ||||
|     'cas_server.user', | ||||
|     'cas_server.userattributes', | ||||
|     'contenttypes.contenttype', | ||||
|     'logs.changelog', | ||||
|     'migrations.migration', | ||||
|     'sessions.session', | ||||
| ] | ||||
|  | ||||
|  | ||||
| def pre_save_object(sender, instance, **kwargs): | ||||
|     """ | ||||
|     Before a model get saved, we check the permissions | ||||
|     """ | ||||
|     # noinspection PyProtectedMember | ||||
|     if instance._meta.label_lower in EXCLUDED: | ||||
|         return | ||||
|  | ||||
|     user = get_current_authenticated_user() | ||||
|     if user is None: | ||||
|         # Action performed on shell is always granted | ||||
|         return | ||||
|  | ||||
|     qs = sender.objects.filter(pk=instance.pk).all() | ||||
|     model_name_full = instance._meta.label_lower.split(".") | ||||
|     app_label = model_name_full[0] | ||||
|     model_name = model_name_full[1] | ||||
|  | ||||
|     if qs.exists(): | ||||
|         # We check if the user can change the model | ||||
|  | ||||
|         # If the user has all right on a model, then OK | ||||
|         if PermissionBackend().has_perm(user, app_label + ".change_" + model_name, instance): | ||||
|             return | ||||
|  | ||||
|         # 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) | ||||
|             new_value = getattr(instance, field.name) | ||||
|             # If the field wasn't modified, no need to check the permissions | ||||
|             if old_value == new_value: | ||||
|                 continue | ||||
|             if not PermissionBackend().has_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance): | ||||
|                 raise PermissionDenied | ||||
|     else: | ||||
|         # We check if the user can add the model | ||||
|  | ||||
|         # While checking permissions, the object will be inserted in the DB, then removed. | ||||
|         # We disable temporary the connectors | ||||
|         pre_save.disconnect(pre_save_object) | ||||
|         pre_delete.disconnect(pre_delete_object) | ||||
|         # We disable also logs connectors | ||||
|         pre_save.disconnect(logs_signals.pre_save_object) | ||||
|         post_save.disconnect(logs_signals.save_object) | ||||
|         post_delete.disconnect(logs_signals.delete_object) | ||||
|  | ||||
|         # We check if the user has right to add the object | ||||
|         has_perm = PermissionBackend().has_perm(user, app_label + ".add_" + model_name, instance) | ||||
|  | ||||
|         # Then we reconnect all | ||||
|         pre_save.connect(pre_save_object) | ||||
|         pre_delete.connect(pre_delete_object) | ||||
|         pre_save.connect(logs_signals.pre_save_object) | ||||
|         post_save.connect(logs_signals.save_object) | ||||
|         post_delete.connect(logs_signals.delete_object) | ||||
|  | ||||
|         if not has_perm: | ||||
|             raise PermissionDenied | ||||
|  | ||||
|  | ||||
| def pre_delete_object(sender, instance, **kwargs): | ||||
|     """ | ||||
|     Before a model get deleted, we check the permissions | ||||
|     """ | ||||
|     # noinspection PyProtectedMember | ||||
|     if instance._meta.label_lower in EXCLUDED: | ||||
|         return | ||||
|  | ||||
|     user = get_current_authenticated_user() | ||||
|     if user is None: | ||||
|         # Action performed on shell is always granted | ||||
|         return | ||||
|  | ||||
|     model_name_full = instance._meta.label_lower.split(".") | ||||
|     app_label = model_name_full[0] | ||||
|     model_name = model_name_full[1] | ||||
|  | ||||
|     # We check if the user has rights to delete the object | ||||
|     if not PermissionBackend().has_perm(user, app_label + ".delete_" + model_name, instance): | ||||
|         raise PermissionDenied | ||||
							
								
								
									
										0
									
								
								apps/permission/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								apps/permission/templatetags/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										55
									
								
								apps/permission/templatetags/perms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								apps/permission/templatetags/perms.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| # Copyright (C) 2018-2020 by BDE ENS Paris-Saclay | ||||
| # SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.template.defaultfilters import stringfilter | ||||
|  | ||||
| from note_kfet.middlewares import get_current_authenticated_user, get_current_session | ||||
| from django import template | ||||
|  | ||||
| from permission.backends import PermissionBackend | ||||
|  | ||||
|  | ||||
| @stringfilter | ||||
| def not_empty_model_list(model_name): | ||||
|     """ | ||||
|     Return True if and only if the current user has right to see any object of the given model. | ||||
|     """ | ||||
|     user = get_current_authenticated_user() | ||||
|     session = get_current_session() | ||||
|     if user is None: | ||||
|         return False | ||||
|     elif user.is_superuser and session.get("permission_mask", 0) >= 42: | ||||
|         return True | ||||
|     if session.get("not_empty_model_list_" + model_name, None): | ||||
|         return session.get("not_empty_model_list_" + model_name, None) == 1 | ||||
|     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, "view")).all() | ||||
|     session["not_empty_model_list_" + model_name] = 1 if qs.exists() else 2 | ||||
|     return session.get("not_empty_model_list_" + model_name) == 1 | ||||
|  | ||||
|  | ||||
| @stringfilter | ||||
| def not_empty_model_change_list(model_name): | ||||
|     """ | ||||
|     Return True if and only if the current user has right to change any object of the given model. | ||||
|     """ | ||||
|     user = get_current_authenticated_user() | ||||
|     session = get_current_session() | ||||
|     if user is None: | ||||
|         return False | ||||
|     elif user.is_superuser and session.get("permission_mask", 0) >= 42: | ||||
|         return True | ||||
|     if session.get("not_empty_model_change_list_" + model_name, None): | ||||
|         return session.get("not_empty_model_change_list_" + model_name, None) == 1 | ||||
|     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, "change")) | ||||
|     session["not_empty_model_change_list_" + model_name] = 1 if qs.exists() else 2 | ||||
|     return session.get("not_empty_model_change_list_" + model_name) == 1 | ||||
|  | ||||
|  | ||||
| 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) | ||||
| @@ -7,7 +7,7 @@ if [ -z ${NOTE_URL+x} ]; then | ||||
| else | ||||
|   sed -i -e "s/example.com/$DOMAIN/g" /code/apps/member/fixtures/initial.json | ||||
|   sed -i -e "s/localhost/$NOTE_URL/g" /code/note_kfet/fixtures/initial.json | ||||
|   sed -i -e "s/\.\*/https?:\/\/$NOTE_URL\/.*/g" /code/note_kfet/fixtures/cas.json | ||||
|   sed -i -e "s/\"\.\*\"/\"https?:\/\/$NOTE_URL\/.*\"/g" /code/note_kfet/fixtures/cas.json | ||||
|   sed -i -e "s/REPLACEME/La Note Kfet \\\\ud83c\\\\udf7b/g" /code/note_kfet/fixtures/cas.json | ||||
| fi | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,66 @@ | ||||
| # 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.models import AnonymousUser, User | ||||
|  | ||||
| from threading import local | ||||
|  | ||||
| from django.contrib.sessions.backends.db import SessionStore | ||||
|  | ||||
| USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user') | ||||
| SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session') | ||||
| IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip') | ||||
|  | ||||
| _thread_locals = local() | ||||
|  | ||||
|  | ||||
| def _set_current_user_and_ip(user=None, session=None, ip=None): | ||||
|     setattr(_thread_locals, USER_ATTR_NAME, user) | ||||
|     setattr(_thread_locals, SESSION_ATTR_NAME, session) | ||||
|     setattr(_thread_locals, IP_ATTR_NAME, ip) | ||||
|  | ||||
|  | ||||
| def get_current_user() -> User: | ||||
|     return getattr(_thread_locals, USER_ATTR_NAME, None) | ||||
|  | ||||
|  | ||||
| def get_current_session() -> SessionStore: | ||||
|     return getattr(_thread_locals, SESSION_ATTR_NAME, None) | ||||
|  | ||||
|  | ||||
| def get_current_ip() -> str: | ||||
|     return getattr(_thread_locals, IP_ATTR_NAME, None) | ||||
|  | ||||
|  | ||||
| def get_current_authenticated_user(): | ||||
|     current_user = get_current_user() | ||||
|     if isinstance(current_user, AnonymousUser): | ||||
|         return None | ||||
|     return current_user | ||||
|  | ||||
|  | ||||
| class SessionMiddleware(object): | ||||
|     """ | ||||
|     This middleware get the current user with his or her IP address on each request. | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, get_response): | ||||
|         self.get_response = get_response | ||||
|  | ||||
|     def __call__(self, request): | ||||
|         user = request.user | ||||
|         if 'HTTP_X_FORWARDED_FOR' in request.META: | ||||
|             ip = request.META.get('HTTP_X_FORWARDED_FOR') | ||||
|         else: | ||||
|             ip = request.META.get('REMOTE_ADDR') | ||||
|  | ||||
|         _set_current_user_and_ip(user, request.session, ip) | ||||
|         response = self.get_response(request) | ||||
|         _set_current_user_and_ip(None, None, None) | ||||
|  | ||||
|         return response | ||||
|  | ||||
|  | ||||
| class TurbolinksMiddleware(object): | ||||
|     """ | ||||
|   | ||||
| @@ -76,7 +76,7 @@ if "cas" in INSTALLED_APPS: | ||||
|  | ||||
|  | ||||
| if "logs" in INSTALLED_APPS: | ||||
|     MIDDLEWARE += ('logs.middlewares.LogsMiddleware',) | ||||
|     MIDDLEWARE += ('note_kfet.middlewares.SessionMiddleware',) | ||||
|  | ||||
| if "debug_toolbar" in INSTALLED_APPS: | ||||
|     MIDDLEWARE.insert(1, "debug_toolbar.middleware.DebugToolbarMiddleware") | ||||
|   | ||||
| @@ -59,6 +59,7 @@ INSTALLED_APPS = [ | ||||
|     'activity', | ||||
|     'member', | ||||
|     'note', | ||||
|     'permission', | ||||
|     'api', | ||||
|     'logs', | ||||
| ] | ||||
| @@ -124,18 +125,15 @@ PASSWORD_HASHERS = [ | ||||
|     'member.hashers.CustomNK15Hasher', | ||||
| ] | ||||
|  | ||||
| # Django Guardian object permissions | ||||
|  | ||||
| AUTHENTICATION_BACKENDS = ( | ||||
|     'django.contrib.auth.backends.ModelBackend',  # this is default | ||||
|     'permission.backends.PermissionBackend',  # Custom role-based permission system | ||||
|     'cas.backends.CASBackend',  # For CAS connections | ||||
| ) | ||||
|  | ||||
| REST_FRAMEWORK = { | ||||
|     # Use Django's standard `django.contrib.auth` permissions, | ||||
|     # or allow read-only access for unauthenticated users. | ||||
|     'DEFAULT_PERMISSION_CLASSES': [ | ||||
|         # TODO Maybe replace it with our custom permissions system | ||||
|         'rest_framework.permissions.DjangoModelPermissions', | ||||
|         # Control API access with our role-based permission system | ||||
|         'permission.permissions.StrongDjangoObjectPermissions', | ||||
|     ], | ||||
|     'DEFAULT_AUTHENTICATION_CLASSES': [ | ||||
|         'rest_framework.authentication.SessionAuthentication', | ||||
|   | ||||
| @@ -7,6 +7,8 @@ from django.contrib import admin | ||||
| from django.urls import path, include | ||||
| from django.views.generic import RedirectView | ||||
|  | ||||
| from member.views import CustomLoginView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     # Dev so redirect to something random | ||||
|     path('', RedirectView.as_view(pattern_name='note:transfer'), name='index'), | ||||
| @@ -16,10 +18,11 @@ urlpatterns = [ | ||||
|  | ||||
|     # Include Django Contrib and Core routers | ||||
|     path('i18n/', include('django.conf.urls.i18n')), | ||||
|     path('accounts/', include('member.urls')), | ||||
|     path('accounts/', include('django.contrib.auth.urls')), | ||||
|     path('admin/doc/', include('django.contrib.admindocs.urls')), | ||||
|     path('admin/', admin.site.urls), | ||||
|     path('accounts/', include('member.urls')), | ||||
|     path('accounts/login/', CustomLoginView.as_view()), | ||||
|     path('accounts/', include('django.contrib.auth.urls')), | ||||
|     path('api/', include('api.urls')), | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -61,13 +61,23 @@ function li(id, text) { | ||||
|  * @param profile_pic_field | ||||
|  */ | ||||
| function displayNote(note, alias, user_note_field=null, profile_pic_field=null) { | ||||
|     let img = note == null ? null : note.display_image; | ||||
|     if (img == null) | ||||
|         img = '/media/pic/default.png'; | ||||
|     if (note !== null && alias !== note.name) | ||||
|     if (!note.display_image) { | ||||
|         note.display_image = 'https://nk20.ynerant.fr/media/pic/default.png'; | ||||
|         $.getJSON("/api/note/note/" + note.id + "/?format=json", function(new_note) { | ||||
|             note.display_image = new_note.display_image.replace("http:", "https:"); | ||||
|             note.name = new_note.name; | ||||
|             note.balance = new_note.balance; | ||||
|  | ||||
|             displayNote(note, alias, user_note_field, profile_pic_field); | ||||
|         }); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     let img = note.display_image; | ||||
|     if (alias !== note.name) | ||||
|         alias += " (aka. " + note.name + ")"; | ||||
|     if (note !== null && user_note_field !== null) | ||||
|         $("#" + user_note_field).text(alias + " : " + pretty_money(note.balance)); | ||||
|     if (user_note_field !== null) | ||||
|         $("#" + user_note_field).text(alias + (note.balance == null ? "" : (" : " + pretty_money(note.balance)))); | ||||
|     if (profile_pic_field != null) | ||||
|         $("#" + profile_pic_field).attr('src', img); | ||||
| } | ||||
| @@ -173,8 +183,13 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes | ||||
|  | ||||
|             aliases.results.forEach(function (alias) { | ||||
|                 let note = alias.note; | ||||
|                 note = { | ||||
|                     id: note, | ||||
|                     name: alias.name, | ||||
|                     alias: alias, | ||||
|                     balance: null | ||||
|                 }; | ||||
|                 aliases_matched_html += li(alias_prefix + "_" + alias.id, alias.name); | ||||
|                 note.alias = alias; | ||||
|                 notes.push(note); | ||||
|             }); | ||||
|  | ||||
| @@ -192,6 +207,7 @@ function autoCompleteNote(field_id, alias_matched_id, note_list_id, notes, notes | ||||
|                 // When the user click on an alias, the associated note is added to the emitters | ||||
|                 alias_obj.click(function () { | ||||
|                     field.val(""); | ||||
|                     old_pattern = ""; | ||||
|                     // If the note is already an emitter, we increase the quantity | ||||
|                     var disp = null; | ||||
|                     notes_display.forEach(function (d) { | ||||
| @@ -258,7 +274,7 @@ function de_validate(id, validated) { | ||||
|             "X-CSRFTOKEN": CSRF_TOKEN | ||||
|         }, | ||||
|         data: { | ||||
|             "resourcetype": "TemplateTransaction", | ||||
|             "resourcetype": "RecurrentTransaction", | ||||
|             valid: !validated | ||||
|         }, | ||||
|         success: function () { | ||||
|   | ||||
| @@ -97,7 +97,7 @@ autoCompleteNote("note", "alias_matched", "note_list", notes, notes_display, | ||||
|  * Add a transaction from a button. | ||||
|  * @param dest Where the money goes | ||||
|  * @param amount The price of the item | ||||
|  * @param type The type of the transaction (content type id for TemplateTransaction) | ||||
|  * @param type The type of the transaction (content type id for RecurrentTransaction) | ||||
|  * @param category_id The category identifier | ||||
|  * @param category_name The category name | ||||
|  * @param template_id The identifier of the button | ||||
| @@ -154,7 +154,8 @@ function reset() { | ||||
|     $("#note_list").html(""); | ||||
|     $("#alias_matched").html(""); | ||||
|     $("#consos_list").html(""); | ||||
|     displayNote(null, ""); | ||||
|     $("#user_note").text(""); | ||||
|     $("#profile_pic").attr("src", "/media/pic/default.png"); | ||||
|     refreshHistory(); | ||||
|     refreshBalance(); | ||||
| } | ||||
| @@ -179,7 +180,7 @@ function consumeAll() { | ||||
|  * @param quantity The quantity sold (type: int) | ||||
|  * @param amount The price of one item, in cents (type: int) | ||||
|  * @param reason The transaction details (type: str) | ||||
|  * @param type The type of the transaction (content type id for TemplateTransaction) | ||||
|  * @param type The type of the transaction (content type id for RecurrentTransaction) | ||||
|  * @param category The category id of the button (type: int) | ||||
|  * @param template The button id (type: int) | ||||
|  */ | ||||
| @@ -192,7 +193,7 @@ function consume(source, dest, quantity, amount, reason, type, category, templat | ||||
|             "reason": reason, | ||||
|             "valid": true, | ||||
|             "polymorphic_ctype": type, | ||||
|             "resourcetype": "TemplateTransaction", | ||||
|             "resourcetype": "RecurrentTransaction", | ||||
|             "source": source, | ||||
|             "destination": dest, | ||||
|             "category": category, | ||||
|   | ||||
| @@ -21,6 +21,8 @@ function reset() { | ||||
|     $("#last_name").val(""); | ||||
|     $("#first_name").val(""); | ||||
|     $("#bank").val(""); | ||||
|     $("#user_note").val(""); | ||||
|     $("#profile_pic").attr("src", "/media/pic/default.png"); | ||||
|     refreshBalance(); | ||||
|     refreshHistory(); | ||||
| } | ||||
| @@ -30,16 +32,18 @@ $(document).ready(function() { | ||||
|         "source_alias", "source_note", "user_note", "profile_pic"); | ||||
|     autoCompleteNote("dest_note", "dest_alias_matched", "dest_note_list", dests, dests_notes_display, | ||||
|         "dest_alias", "dest_note", "user_note", "profile_pic", function() { | ||||
|             let last = dests_notes_display[dests_notes_display.length - 1]; | ||||
|             dests_notes_display.length = 0; | ||||
|             dests_notes_display.push(last); | ||||
|             if ($("#type_credit").is(":checked") || $("#type_debit").is(":checked")) { | ||||
|                 let last = dests_notes_display[dests_notes_display.length - 1]; | ||||
|                 dests_notes_display.length = 0; | ||||
|                 dests_notes_display.push(last); | ||||
|  | ||||
|             last.quantity = 1; | ||||
|                 last.quantity = 1; | ||||
|  | ||||
|             $.getJSON("/api/user/" + last.note.user + "/", function(user) { | ||||
|                 $("#last_name").val(user.last_name); | ||||
|                 $("#first_name").val(user.first_name); | ||||
|             }); | ||||
|                 $.getJSON("/api/user/" + last.note.user + "/", function(user) { | ||||
|                     $("#last_name").val(user.last_name); | ||||
|                     $("#first_name").val(user.first_name); | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             return true; | ||||
|        }); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| {% load static i18n pretty_money static getenv %} | ||||
| {% load static i18n pretty_money static getenv perms %} | ||||
| {% comment %} | ||||
| SPDX-License-Identifier: GPL-3.0-or-later | ||||
| {% endcomment %} | ||||
| @@ -74,21 +74,29 @@ SPDX-License-Identifier: GPL-3.0-or-later | ||||
|         </button> | ||||
|         <div class="collapse navbar-collapse" id="navbarNavDropdown"> | ||||
|             <ul class="navbar-nav"> | ||||
|                 <li class="nav-item active"> | ||||
|                     <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> | ||||
|                 </li> | ||||
|                 <li class="nav-item active"> | ||||
|                     <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> | ||||
|                 </li> | ||||
|                 <li class="nav-item active"> | ||||
|                     <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> | ||||
|                 </li> | ||||
|                 <li class="nav-item active"> | ||||
|                     <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a> | ||||
|                 </li> | ||||
|                 <li class="nav-item active"> | ||||
|                     <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> | ||||
|                 </li> | ||||
|                 {% if "note.transactiontemplate"|not_empty_model_list %} | ||||
|                     <li class="nav-item active"> | ||||
|                         <a class="nav-link" href="{% url 'note:consos' %}"><i class="fa fa-coffee"></i> {% trans 'Consumptions' %}</a> | ||||
|                     </li> | ||||
|                 {% endif %} | ||||
|                 {% if "member.club"|not_empty_model_list %} | ||||
|                     <li class="nav-item active"> | ||||
|                         <a class="nav-link" href="{% url 'member:club_list' %}"><i class="fa fa-users"></i> {% trans 'Clubs' %}</a> | ||||
|                     </li> | ||||
|                 {% endif %} | ||||
|                 {% if "activity.activity"|not_empty_model_list %} | ||||
|                     <li class="nav-item active"> | ||||
|                         <a class="nav-link" href="#"><i class="fa fa-calendar"></i> {% trans 'Activities' %}</a> | ||||
|                     </li> | ||||
|                 {% endif %} | ||||
|                 {% if "note.transactiontemplate"|not_empty_model_change_list %} | ||||
|                     <li class="nav-item active"> | ||||
|                         <a class="nav-link" href="{% url 'note:template_list' %}"><i class="fa fa-coffee"></i> {% trans 'Buttons' %}</a> | ||||
|                     </li> | ||||
|                 {% endif %} | ||||
|                     <li class="nav-item active"> | ||||
|                         <a class="nav-link" href="{% url 'note:transfer' %}"><i class="fa fa-exchange"></i>{% trans 'Transfer' %} </a> | ||||
|                     </li> | ||||
|             </ul> | ||||
|             <ul class="navbar-nav ml-auto"> | ||||
|                 {% if user.is_authenticated %} | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
| SPDX-License-Identifier: GPL-2.0-or-later | ||||
| {% endcomment %} | ||||
|  | ||||
| {% load i18n static django_tables2 %} | ||||
| {% load i18n static django_tables2 perms %} | ||||
|  | ||||
| {% block content %} | ||||
|  | ||||
| @@ -18,14 +18,16 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|                     <input type="radio" name="transaction_type" id="type_transfer"> | ||||
|                     {% trans "Transfer" %} | ||||
|                 </label> | ||||
|                 <label for="type_credit" class="btn btn-sm btn-outline-primary"> | ||||
|                     <input type="radio" name="transaction_type" id="type_credit"> | ||||
|                     {% trans "Credit" %} | ||||
|                 </label> | ||||
|                 <label type="type_debit" class="btn btn-sm btn-outline-primary"> | ||||
|                     <input type="radio" name="transaction_type" id="type_debit"> | ||||
|                     {% trans "Debit" %} | ||||
|                 </label> | ||||
|                 {% if "note.notespecial"|not_empty_model_list %} | ||||
|                     <label for="type_credit" class="btn btn-sm btn-outline-primary"> | ||||
|                         <input type="radio" name="transaction_type" id="type_credit"> | ||||
|                         {% trans "Credit" %} | ||||
|                     </label> | ||||
|                     <label type="type_debit" class="btn btn-sm btn-outline-primary"> | ||||
|                         <input type="radio" name="transaction_type" id="type_debit"> | ||||
|                         {% trans "Debit" %} | ||||
|                     </label> | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| @@ -58,47 +60,49 @@ SPDX-License-Identifier: GPL-2.0-or-later | ||||
|             </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="col-md-4" id="external_div" style="display: none;"> | ||||
|             <div class="card border-success shadow mb-4"> | ||||
|                 <div class="card-header"> | ||||
|                     <p class="card-text font-weight-bold"> | ||||
|                         {% trans "External payment" %} | ||||
|                     </p> | ||||
|                 </div> | ||||
|                 <ul class="list-group list-group-flush" id="source_note_list"> | ||||
|                 </ul> | ||||
|                 <div class="card-body"> | ||||
|                     <div class="form-row"> | ||||
|                         <div class="col-md-12"> | ||||
|                             <label for="credit_type">{% trans "Transfer type" %} :</label> | ||||
|                             <select id="credit_type" class="custom-select"> | ||||
|                                 {% for special_type in special_types %} | ||||
|                                     <option value="{{ special_type.id }}">{{ special_type.special_type }}</option> | ||||
|                                 {% endfor %} | ||||
|                             </select> | ||||
|                         </div> | ||||
|         {% if "note.notespecial"|not_empty_model_list %} | ||||
|             <div class="col-md-4" id="external_div" style="display: none;"> | ||||
|                 <div class="card border-success shadow mb-4"> | ||||
|                     <div class="card-header"> | ||||
|                         <p class="card-text font-weight-bold"> | ||||
|                             {% trans "External payment" %} | ||||
|                         </p> | ||||
|                     </div> | ||||
|                     <div class="form-row"> | ||||
|                         <div class="col-md-12"> | ||||
|                             <label for="last_name">{% trans "Name" %} :</label> | ||||
|                             <input type="text" id="last_name" class="form-control" /> | ||||
|                     <ul class="list-group list-group-flush" id="source_note_list"> | ||||
|                     </ul> | ||||
|                     <div class="card-body"> | ||||
|                         <div class="form-row"> | ||||
|                             <div class="col-md-12"> | ||||
|                                 <label for="credit_type">{% trans "Transfer type" %} :</label> | ||||
|                                 <select id="credit_type" class="custom-select"> | ||||
|                                     {% for special_type in special_types %} | ||||
|                                         <option value="{{ special_type.id }}">{{ special_type.special_type }}</option> | ||||
|                                     {% endfor %} | ||||
|                                 </select> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-row"> | ||||
|                         <div class="col-md-12"> | ||||
|                             <label for="first_name">{% trans "First name" %} :</label> | ||||
|                             <input type="text" id="first_name" class="form-control" /> | ||||
|                         <div class="form-row"> | ||||
|                             <div class="col-md-12"> | ||||
|                                 <label for="last_name">{% trans "Name" %} :</label> | ||||
|                                 <input type="text" id="last_name" class="form-control" /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                     <div class="form-row"> | ||||
|                         <div class="col-md-12"> | ||||
|                             <label for="bank">{% trans "Bank" %} :</label> | ||||
|                             <input type="text" id="bank" class="form-control" /> | ||||
|                         <div class="form-row"> | ||||
|                             <div class="col-md-12"> | ||||
|                                 <label for="first_name">{% trans "First name" %} :</label> | ||||
|                                 <input type="text" id="first_name" class="form-control" /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                         <div class="form-row"> | ||||
|                             <div class="col-md-12"> | ||||
|                                 <label for="bank">{% trans "Bank" %} :</label> | ||||
|                                 <input type="text" id="bank" class="form-control" /> | ||||
|                             </div> | ||||
|                         </div> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|  | ||||
|         <div class="col-md-8" id="dests_div"> | ||||
|             <div class="card border-info shadow mb-4"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user