mirror of
https://gitlab.crans.org/bde/nk20
synced 2024-11-26 18:37:12 +00:00
Merge branch 'beta' into 'master'
OAuth2, tests WEI See merge request bde/nk20!174
This commit is contained in:
commit
5eb3ffca66
@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from member.models import Club
|
from member.models import Club
|
||||||
from note.models import Note, NoteUser
|
from note.models import Note, NoteUser
|
||||||
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
from note_kfet.inputs import Autocomplete, DateTimePickerInput
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models import Activity, Guest
|
from .models import Activity, Guest
|
||||||
@ -24,7 +24,7 @@ class ActivityForm(forms.ModelForm):
|
|||||||
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
|
self.fields["attendees_club"].initial = Club.objects.get(name="Kfet")
|
||||||
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
|
self.fields["attendees_club"].widget.attrs["placeholder"] = "Kfet"
|
||||||
clubs = list(Club.objects.filter(PermissionBackend
|
clubs = list(Club.objects.filter(PermissionBackend
|
||||||
.filter_queryset(get_current_authenticated_user(), Club, "view")).all())
|
.filter_queryset(get_current_request(), Club, "view")).all())
|
||||||
shuffle(clubs)
|
shuffle(clubs)
|
||||||
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
self.fields["organizer"].widget.attrs["placeholder"] = ", ".join(club.name for club in clubs[:4]) + ", ..."
|
||||||
|
|
||||||
|
@ -74,12 +74,12 @@ class ActivityListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView
|
|||||||
|
|
||||||
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
upcoming_activities = Activity.objects.filter(date_end__gt=timezone.now())
|
||||||
context['upcoming'] = ActivityTable(
|
context['upcoming'] = ActivityTable(
|
||||||
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")),
|
data=upcoming_activities.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")),
|
||||||
prefix='upcoming-',
|
prefix='upcoming-',
|
||||||
)
|
)
|
||||||
|
|
||||||
started_activities = Activity.objects\
|
started_activities = Activity.objects\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
.filter(open=True, valid=True).all()
|
.filter(open=True, valid=True).all()
|
||||||
context["started_activities"] = started_activities
|
context["started_activities"] = started_activities
|
||||||
|
|
||||||
@ -98,7 +98,7 @@ class ActivityDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context = super().get_context_data()
|
context = super().get_context_data()
|
||||||
|
|
||||||
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
table = GuestTable(data=Guest.objects.filter(activity=self.object)
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view")))
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view")))
|
||||||
context["guests"] = table
|
context["guests"] = table
|
||||||
|
|
||||||
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
context["activity_started"] = timezone.now() > timezone.localtime(self.object.date_start)
|
||||||
@ -144,7 +144,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
def get_form(self, form_class=None):
|
def get_form(self, form_class=None):
|
||||||
form = super().get_form(form_class)
|
form = super().get_form(form_class)
|
||||||
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
form.activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
.get(pk=self.kwargs["pk"])
|
.get(pk=self.kwargs["pk"])
|
||||||
form.fields["inviter"].initial = self.request.user.note
|
form.fields["inviter"].initial = self.request.user.note
|
||||||
return form
|
return form
|
||||||
@ -152,7 +152,7 @@ class ActivityInviteView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
form.instance.activity = Activity.objects\
|
form.instance.activity = Activity.objects\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view")).get(pk=self.kwargs["pk"])
|
.filter(PermissionBackend.filter_queryset(self.request, Activity, "view")).get(pk=self.kwargs["pk"])
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
def get_success_url(self, **kwargs):
|
def get_success_url(self, **kwargs):
|
||||||
@ -173,7 +173,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
activity = Activity.objects.get(pk=self.kwargs["pk"])
|
||||||
|
|
||||||
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
sample_entry = Entry(activity=activity, note=self.request.user.note)
|
||||||
if not PermissionBackend.check_perm(self.request.user, "activity.add_entry", sample_entry):
|
if not PermissionBackend.check_perm(self.request, "activity.add_entry", sample_entry):
|
||||||
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
|
raise PermissionDenied(_("You are not allowed to display the entry interface for this activity."))
|
||||||
|
|
||||||
if not activity.activity_type.manage_entries:
|
if not activity.activity_type.manage_entries:
|
||||||
@ -191,7 +191,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
guest_qs = Guest.objects\
|
guest_qs = Guest.objects\
|
||||||
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
|
.annotate(balance=F("inviter__balance"), note_name=F("inviter__user__username"))\
|
||||||
.filter(activity=activity)\
|
.filter(activity=activity)\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Guest, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Guest, "view"))\
|
||||||
.order_by('last_name', 'first_name').distinct()
|
.order_by('last_name', 'first_name').distinct()
|
||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
@ -230,7 +230,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Filter with permission backend
|
# Filter with permission backend
|
||||||
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view"))
|
note_qs = note_qs.filter(PermissionBackend.filter_queryset(self.request, Alias, "view"))
|
||||||
|
|
||||||
if "search" in self.request.GET and self.request.GET["search"]:
|
if "search" in self.request.GET and self.request.GET["search"]:
|
||||||
pattern = self.request.GET["search"]
|
pattern = self.request.GET["search"]
|
||||||
@ -256,7 +256,7 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
"""
|
"""
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request.user, Activity, "view"))\
|
activity = Activity.objects.filter(PermissionBackend.filter_queryset(self.request, Activity, "view"))\
|
||||||
.distinct().get(pk=self.kwargs["pk"])
|
.distinct().get(pk=self.kwargs["pk"])
|
||||||
context["activity"] = activity
|
context["activity"] = activity
|
||||||
|
|
||||||
@ -281,9 +281,9 @@ class ActivityEntryView(LoginRequiredMixin, TemplateView):
|
|||||||
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
context["notespecial_ctype"] = ContentType.objects.get_for_model(NoteSpecial).pk
|
||||||
|
|
||||||
activities_open = Activity.objects.filter(open=True).filter(
|
activities_open = Activity.objects.filter(open=True).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
|
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||||
context["activities_open"] = [a for a in activities_open
|
context["activities_open"] = [a for a in activities_open
|
||||||
if PermissionBackend.check_perm(self.request.user,
|
if PermissionBackend.check_perm(self.request,
|
||||||
"activity.add_entry",
|
"activity.add_entry",
|
||||||
Entry(activity=a, note=self.request.user.note,))]
|
Entry(activity=a, note=self.request.user.note,))]
|
||||||
|
|
||||||
|
@ -9,7 +9,6 @@ from django.contrib.auth.models import User
|
|||||||
from rest_framework.filters import SearchFilter
|
from rest_framework.filters import SearchFilter
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from note_kfet.middlewares import get_current_session
|
|
||||||
from note.models import Alias
|
from note.models import Alias
|
||||||
|
|
||||||
from .serializers import UserSerializer, ContentTypeSerializer
|
from .serializers import UserSerializer, ContentTypeSerializer
|
||||||
@ -25,9 +24,7 @@ class ReadProtectedModelViewSet(ModelViewSet):
|
|||||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
|
||||||
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
|
||||||
|
|
||||||
|
|
||||||
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
||||||
@ -40,9 +37,7 @@ class ReadOnlyProtectedModelViewSet(ReadOnlyModelViewSet):
|
|||||||
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
self.model = ContentType.objects.get_for_model(self.serializer_class.Meta.model).model_class()
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
return self.queryset.filter(PermissionBackend.filter_queryset(self.request, self.model, "view")).distinct()
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
|
||||||
return self.queryset.filter(PermissionBackend.filter_queryset(user, self.model, "view")).distinct()
|
|
||||||
|
|
||||||
|
|
||||||
class UserViewSet(ReadProtectedModelViewSet):
|
class UserViewSet(ReadProtectedModelViewSet):
|
||||||
|
@ -5,7 +5,7 @@ from django.contrib.contenttypes.models import ContentType
|
|||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.serializers import ModelSerializer
|
from rest_framework.serializers import ModelSerializer
|
||||||
from note.models import NoteUser, Alias
|
from note.models import NoteUser, Alias
|
||||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_ip
|
from note_kfet.middlewares import get_current_request
|
||||||
|
|
||||||
from .models import Changelog
|
from .models import Changelog
|
||||||
|
|
||||||
@ -57,9 +57,9 @@ def save_object(sender, instance, **kwargs):
|
|||||||
previous = instance._previous
|
previous = instance._previous
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
request = get_current_request()
|
||||||
|
|
||||||
if user is None:
|
if request is None:
|
||||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||||
@ -71,9 +71,23 @@ def save_object(sender, instance, **kwargs):
|
|||||||
# else:
|
# else:
|
||||||
if note.exists():
|
if note.exists():
|
||||||
user = note.get().user
|
user = note.get().user
|
||||||
|
else:
|
||||||
|
user = None
|
||||||
|
else:
|
||||||
|
user = request.user
|
||||||
|
if 'HTTP_X_REAL_IP' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_REAL_IP')
|
||||||
|
elif 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
# For registration and OAuth2 purposes
|
||||||
|
user = None
|
||||||
|
|
||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
if user is not None and instance._meta.label_lower == "auth.user" and previous:
|
if request is not None and instance._meta.label_lower == "auth.user" and previous:
|
||||||
# On n'enregistre pas les connexions
|
# On n'enregistre pas les connexions
|
||||||
if instance.last_login != previous.last_login:
|
if instance.last_login != previous.last_login:
|
||||||
return
|
return
|
||||||
@ -121,9 +135,9 @@ def delete_object(sender, instance, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
request = get_current_request()
|
||||||
|
|
||||||
if user is None:
|
if request is None:
|
||||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||||
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
# IMPORTANT : l'utilisateur dans la VM doit être un des alias note du respo info
|
||||||
@ -135,6 +149,20 @@ def delete_object(sender, instance, **kwargs):
|
|||||||
# else:
|
# else:
|
||||||
if note.exists():
|
if note.exists():
|
||||||
user = note.get().user
|
user = note.get().user
|
||||||
|
else:
|
||||||
|
user = None
|
||||||
|
else:
|
||||||
|
user = request.user
|
||||||
|
if 'HTTP_X_REAL_IP' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_REAL_IP')
|
||||||
|
elif 'HTTP_X_FORWARDED_FOR' in request.META:
|
||||||
|
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
|
||||||
|
else:
|
||||||
|
ip = request.META.get('REMOTE_ADDR')
|
||||||
|
|
||||||
|
if not user.is_authenticated:
|
||||||
|
# For registration and OAuth2 purposes
|
||||||
|
user = None
|
||||||
|
|
||||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
||||||
class CustomSerializer(ModelSerializer):
|
class CustomSerializer(ModelSerializer):
|
||||||
|
@ -6,7 +6,7 @@ import hashlib
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||||
from django.utils.crypto import constant_time_compare
|
from django.utils.crypto import constant_time_compare
|
||||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
from note_kfet.middlewares import get_current_request
|
||||||
|
|
||||||
|
|
||||||
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||||
@ -24,16 +24,22 @@ class CustomNK15Hasher(PBKDF2PasswordHasher):
|
|||||||
|
|
||||||
def must_update(self, encoded):
|
def must_update(self, encoded):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
current_user = get_current_authenticated_user()
|
# Small hack to let superusers to impersonate people.
|
||||||
|
# Don't change their password.
|
||||||
|
request = get_current_request()
|
||||||
|
current_user = request.user
|
||||||
if current_user is not None and current_user.is_superuser:
|
if current_user is not None and current_user.is_superuser:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def verify(self, password, encoded):
|
def verify(self, password, encoded):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
current_user = get_current_authenticated_user()
|
# Small hack to let superusers to impersonate people.
|
||||||
|
# If a superuser is already connected, let him/her log in as another person.
|
||||||
|
request = get_current_request()
|
||||||
|
current_user = request.user
|
||||||
if current_user is not None and current_user.is_superuser\
|
if current_user is not None and current_user.is_superuser\
|
||||||
and get_current_session().get("permission_mask", -1) >= 42:
|
and request.session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if '|' in encoded:
|
if '|' in encoded:
|
||||||
@ -51,8 +57,11 @@ class DebugSuperuserBackdoor(PBKDF2PasswordHasher):
|
|||||||
|
|
||||||
def verify(self, password, encoded):
|
def verify(self, password, encoded):
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
current_user = get_current_authenticated_user()
|
# Small hack to let superusers to impersonate people.
|
||||||
|
# If a superuser is already connected, let him/her log in as another person.
|
||||||
|
request = get_current_request()
|
||||||
|
current_user = request.user
|
||||||
if current_user is not None and current_user.is_superuser\
|
if current_user is not None and current_user.is_superuser\
|
||||||
and get_current_session().get("permission_mask", -1) >= 42:
|
and request.session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
return super().verify(password, encoded)
|
return super().verify(password, encoded)
|
||||||
|
@ -9,7 +9,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from note.templatetags.pretty_money import pretty_money
|
from note.templatetags.pretty_money import pretty_money
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models import Club, Membership
|
from .models import Club, Membership
|
||||||
@ -51,19 +51,19 @@ class UserTable(tables.Table):
|
|||||||
def render_email(self, record, value):
|
def render_email(self, record, value):
|
||||||
# Replace the email by a dash if the user can't see the profile detail
|
# Replace the email by a dash if the user can't see the profile detail
|
||||||
# Replace also the URL
|
# Replace also the URL
|
||||||
if not PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile):
|
if not PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile):
|
||||||
value = "—"
|
value = "—"
|
||||||
record.email = value
|
record.email = value
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def render_section(self, record, value):
|
def render_section(self, record, value):
|
||||||
return value \
|
return value \
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_profile", record.profile) \
|
if PermissionBackend.check_perm(get_current_request(), "member.view_profile", record.profile) \
|
||||||
else "—"
|
else "—"
|
||||||
|
|
||||||
def render_balance(self, record, value):
|
def render_balance(self, record, value):
|
||||||
return pretty_money(value)\
|
return pretty_money(value)\
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", record.note) else "—"
|
if PermissionBackend.check_perm(get_current_request(), "note.view_note", record.note) else "—"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
attrs = {
|
attrs = {
|
||||||
@ -93,7 +93,7 @@ class MembershipTable(tables.Table):
|
|||||||
def render_user(self, value):
|
def render_user(self, value):
|
||||||
# If the user has the right, link the displayed user with the page of its detail.
|
# If the user has the right, link the displayed user with the page of its detail.
|
||||||
s = value.username
|
s = value.username
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
@ -102,7 +102,7 @@ class MembershipTable(tables.Table):
|
|||||||
def render_club(self, value):
|
def render_club(self, value):
|
||||||
# If the user has the right, link the displayed club with the page of its detail.
|
# If the user has the right, link the displayed club with the page of its detail.
|
||||||
s = value.name
|
s = value.name
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
|
if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
@ -127,7 +127,7 @@ class MembershipTable(tables.Table):
|
|||||||
date_end=date.today(),
|
date_end=date.today(),
|
||||||
fee=0,
|
fee=0,
|
||||||
)
|
)
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
if PermissionBackend.check_perm(get_current_request(),
|
||||||
"member.add_membership", empty_membership): # If the user has right
|
"member.add_membership", empty_membership): # If the user has right
|
||||||
renew_url = reverse_lazy('member:club_renew_membership',
|
renew_url = reverse_lazy('member:club_renew_membership',
|
||||||
kwargs={"pk": record.pk})
|
kwargs={"pk": record.pk})
|
||||||
@ -142,7 +142,7 @@ class MembershipTable(tables.Table):
|
|||||||
# If the user has the right to manage the roles, display the link to manage them
|
# If the user has the right to manage the roles, display the link to manage them
|
||||||
roles = record.roles.all()
|
roles = record.roles.all()
|
||||||
s = ", ".join(str(role) for role in roles)
|
s = ", ".join(str(role) for role in roles)
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
|
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
|
||||||
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
|
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
|
||||||
+ "'>" + s + "</a>")
|
+ "'>" + s + "</a>")
|
||||||
return s
|
return s
|
||||||
@ -165,7 +165,7 @@ class ClubManagerTable(tables.Table):
|
|||||||
def render_user(self, value):
|
def render_user(self, value):
|
||||||
# If the user has the right, link the displayed user with the page of its detail.
|
# If the user has the right, link the displayed user with the page of its detail.
|
||||||
s = value.username
|
s = value.username
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
|
@ -5,32 +5,98 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="alert alert-info">
|
<div class="row mt-4">
|
||||||
<h4>À quoi sert un jeton d'authentification ?</h4>
|
<div class="col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "Token authentication" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h4>À quoi sert un jeton d'authentification ?</h4>
|
||||||
|
|
||||||
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a>.<br />
|
Un jeton vous permet de vous connecter à <a href="/api/">l'API de la Note Kfet</a> via votre propre compte
|
||||||
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token <TOKEN></code>
|
depuis un client externe.<br />
|
||||||
pour pouvoir vous identifier.<br /><br />
|
Il suffit pour cela d'ajouter en en-tête de vos requêtes <code>Authorization: Token <TOKEN></code>
|
||||||
|
pour pouvoir vous identifier.<br /><br />
|
||||||
|
|
||||||
Une documentation de l'API arrivera ultérieurement.
|
La documentation de l'API est disponible ici :
|
||||||
|
<a href="/doc/api/">{{ request.scheme }}://{{ request.get_host }}/doc/api/</a>.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<strong>{%trans 'Token' %} :</strong>
|
||||||
|
{% if 'show' in request.GET %}
|
||||||
|
{{ token.key }} (<a href="?">cacher</a>)
|
||||||
|
{% else %}
|
||||||
|
<em>caché</em> (<a href="?show">montrer</a>)
|
||||||
|
{% endif %}
|
||||||
|
<br />
|
||||||
|
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
<strong>{% trans "Warning" %} :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<a href="?regenerate">
|
||||||
|
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "OAuth2 authentication" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<p>
|
||||||
|
La Note Kfet implémente également le protocole <a href="https://oauth.net/2/">OAuth2</a>, afin de
|
||||||
|
permettre à des applications tierces d'interagir avec la Note en récoltant des informations
|
||||||
|
(de connexion par exemple) voir en permettant des modifications à distance, par exemple lorsqu'il
|
||||||
|
s'agit d'avoir un site marchand sur lequel faire des transactions via la Note Kfet.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
L'usage de ce protocole est recommandé pour tout usage non personnel, car permet de mieux cibler
|
||||||
|
les droits dont on a besoin, en restreignant leur usage par jeton généré.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
La documentation vis-à-vis de l'usage de ce protocole est disponible ici :
|
||||||
|
<a href="/doc/external_services/oauth2/">{{ request.scheme }}://{{ request.get_host }}/doc/external_services/oauth2/</a>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Liste des URL à communiquer à votre application :
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
{% trans "Authorization:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% trans "Token:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:token' %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% trans "Revoke Token:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:revoke-token' %}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{% trans "Introspect Token:" %}
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:introspect' %}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<a class="btn btn-primary" href="{% url 'oauth2_provider:list' %}">{% trans "Show my applications" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<strong>{%trans 'Token' %} :</strong>
|
|
||||||
{% if 'show' in request.GET %}
|
|
||||||
{{ token.key }} (<a href="?">cacher</a>)
|
|
||||||
{% else %}
|
|
||||||
<em>caché</em> (<a href="?show">montrer</a>)
|
|
||||||
{% endif %}
|
|
||||||
<br />
|
|
||||||
<strong>{%trans 'Created' %} :</strong> {{ token.created }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<strong>Attention :</strong> regénérer le jeton va révoquer tout accès autorisé à l'API via ce jeton !
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<a href="?regenerate">
|
|
||||||
<button class="btn btn-primary">{% trans 'Regenerate token' %}</button>
|
|
||||||
</a>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
@ -21,7 +21,7 @@ from rest_framework.authtoken.models import Token
|
|||||||
from note.models import Alias, NoteUser
|
from note.models import Alias, NoteUser
|
||||||
from note.models.transactions import Transaction, SpecialTransaction
|
from note.models.transactions import Transaction, SpecialTransaction
|
||||||
from note.tables import HistoryTable, AliasTable
|
from note.tables import HistoryTable, AliasTable
|
||||||
from note_kfet.middlewares import _set_current_user_and_ip
|
from note_kfet.middlewares import _set_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from permission.models import Role
|
from permission.models import Role
|
||||||
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
from permission.views import ProtectQuerysetMixin, ProtectedCreateView
|
||||||
@ -41,7 +41,8 @@ class CustomLoginView(LoginView):
|
|||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def form_valid(self, form):
|
def form_valid(self, form):
|
||||||
logout(self.request)
|
logout(self.request)
|
||||||
_set_current_user_and_ip(form.get_user(), self.request.session, None)
|
self.request.user = form.get_user()
|
||||||
|
_set_current_request(self.request)
|
||||||
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
self.request.session['permission_mask'] = form.cleaned_data['permission_mask'].rank
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|
||||||
@ -70,7 +71,7 @@ class UserUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView):
|
|||||||
form.fields['email'].required = True
|
form.fields['email'].required = True
|
||||||
form.fields['email'].help_text = _("This address must be valid.")
|
form.fields['email'].help_text = _("This address must be valid.")
|
||||||
|
|
||||||
if PermissionBackend.check_perm(self.request.user, "member.change_profile", context['user_object'].profile):
|
if PermissionBackend.check_perm(self.request, "member.change_profile", context['user_object'].profile):
|
||||||
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
context['profile_form'] = self.profile_form(instance=context['user_object'].profile,
|
||||||
data=self.request.POST if self.request.POST else None)
|
data=self.request.POST if self.request.POST else None)
|
||||||
if not self.object.profile.report_frequency:
|
if not self.object.profile.report_frequency:
|
||||||
@ -153,13 +154,13 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
history_list = \
|
history_list = \
|
||||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))\
|
||||||
.order_by("-created_at")\
|
.order_by("-created_at")\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))
|
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))
|
||||||
history_table = HistoryTable(history_list, prefix='transaction-')
|
history_table = HistoryTable(history_list, prefix='transaction-')
|
||||||
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
history_table.paginate(per_page=20, page=self.request.GET.get("transaction-page", 1))
|
||||||
context['history_list'] = history_table
|
context['history_list'] = history_table
|
||||||
|
|
||||||
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
|
club_list = Membership.objects.filter(user=user, date_end__gte=date.today() - timedelta(days=15))\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
|
||||||
.order_by("club__name", "-date_start")
|
.order_by("club__name", "-date_start")
|
||||||
# Display only the most recent membership
|
# Display only the most recent membership
|
||||||
club_list = club_list.distinct("club__name")\
|
club_list = club_list.distinct("club__name")\
|
||||||
@ -176,21 +177,20 @@ class UserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
modified_note.is_active = True
|
modified_note.is_active = True
|
||||||
modified_note.inactivity_reason = 'manual'
|
modified_note.inactivity_reason = 'manual'
|
||||||
context["can_lock_note"] = user.note.is_active and PermissionBackend\
|
context["can_lock_note"] = user.note.is_active and PermissionBackend\
|
||||||
.check_perm(self.request.user, "note.change_noteuser_is_active",
|
.check_perm(self.request, "note.change_noteuser_is_active", modified_note)
|
||||||
modified_note)
|
|
||||||
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
|
old_note = NoteUser.objects.select_for_update().get(pk=user.note.pk)
|
||||||
modified_note.inactivity_reason = 'forced'
|
modified_note.inactivity_reason = 'forced'
|
||||||
modified_note._force_save = True
|
modified_note._force_save = True
|
||||||
modified_note.save()
|
modified_note.save()
|
||||||
context["can_force_lock"] = user.note.is_active and PermissionBackend\
|
context["can_force_lock"] = user.note.is_active and PermissionBackend\
|
||||||
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
|
.check_perm(self.request, "note.change_note_is_active", modified_note)
|
||||||
old_note._force_save = True
|
old_note._force_save = True
|
||||||
old_note._no_signal = True
|
old_note._no_signal = True
|
||||||
old_note.save()
|
old_note.save()
|
||||||
modified_note.refresh_from_db()
|
modified_note.refresh_from_db()
|
||||||
modified_note.is_active = True
|
modified_note.is_active = True
|
||||||
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
|
context["can_unlock_note"] = not user.note.is_active and PermissionBackend\
|
||||||
.check_perm(self.request.user, "note.change_note_is_active", modified_note)
|
.check_perm(self.request, "note.change_note_is_active", modified_note)
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -237,7 +237,7 @@ class UserListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request.user, User, "view"))\
|
pre_registered_users = User.objects.filter(PermissionBackend.filter_queryset(self.request, User, "view"))\
|
||||||
.filter(profile__registration_valid=False)
|
.filter(profile__registration_valid=False)
|
||||||
context["can_manage_registrations"] = pre_registered_users.exists()
|
context["can_manage_registrations"] = pre_registered_users.exists()
|
||||||
return context
|
return context
|
||||||
@ -256,8 +256,8 @@ class ProfileAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
note = context['object'].note
|
||||||
context["aliases"] = AliasTable(
|
context["aliases"] = AliasTable(
|
||||||
note.alias.filter(PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
|
note.alias.filter(PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
normalized_name="",
|
normalized_name="",
|
||||||
@ -382,7 +382,7 @@ class ClubListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["can_add_club"] = PermissionBackend.check_perm(self.request.user, "member.add_club", Club(
|
context["can_add_club"] = PermissionBackend.check_perm(self.request, "member.add_club", Club(
|
||||||
name="",
|
name="",
|
||||||
email="club@example.com",
|
email="club@example.com",
|
||||||
))
|
))
|
||||||
@ -404,7 +404,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
club = context["club"]
|
club = context["club"]
|
||||||
if PermissionBackend.check_perm(self.request.user, "member.change_club_membership_start", club):
|
if PermissionBackend.check_perm(self.request, "member.change_club_membership_start", club):
|
||||||
club.update_membership_dates()
|
club.update_membership_dates()
|
||||||
# managers list
|
# managers list
|
||||||
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
|
managers = Membership.objects.filter(club=self.object, roles__name="Bureau de club",
|
||||||
@ -413,7 +413,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
context["managers"] = ClubManagerTable(data=managers, prefix="managers-")
|
||||||
# transaction history
|
# transaction history
|
||||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note))\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
||||||
.order_by('-created_at')
|
.order_by('-created_at')
|
||||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||||
@ -422,7 +422,7 @@ class ClubDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
club_member = Membership.objects.filter(
|
club_member = Membership.objects.filter(
|
||||||
club=club,
|
club=club,
|
||||||
date_end__gte=date.today() - timedelta(days=15),
|
date_end__gte=date.today() - timedelta(days=15),
|
||||||
).filter(PermissionBackend.filter_queryset(self.request.user, Membership, "view"))\
|
).filter(PermissionBackend.filter_queryset(self.request, Membership, "view"))\
|
||||||
.order_by("user__username", "-date_start")
|
.order_by("user__username", "-date_start")
|
||||||
# Display only the most recent membership
|
# Display only the most recent membership
|
||||||
club_member = club_member.distinct("user__username")\
|
club_member = club_member.distinct("user__username")\
|
||||||
@ -459,8 +459,8 @@ class ClubAliasView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
note = context['object'].note
|
note = context['object'].note
|
||||||
context["aliases"] = AliasTable(note.alias.filter(
|
context["aliases"] = AliasTable(note.alias.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Alias, "view")).distinct().all())
|
PermissionBackend.filter_queryset(self.request, Alias, "view")).distinct().all())
|
||||||
context["can_create"] = PermissionBackend.check_perm(self.request.user, "note.add_alias", Alias(
|
context["can_create"] = PermissionBackend.check_perm(self.request, "note.add_alias", Alias(
|
||||||
note=context["object"].note,
|
note=context["object"].note,
|
||||||
name="",
|
name="",
|
||||||
normalized_name="",
|
normalized_name="",
|
||||||
@ -535,7 +535,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
form = context['form']
|
form = context['form']
|
||||||
|
|
||||||
if "club_pk" in self.kwargs: # We create a new membership.
|
if "club_pk" in self.kwargs: # We create a new membership.
|
||||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view"))\
|
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view"))\
|
||||||
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
.get(pk=self.kwargs["club_pk"], weiclub=None)
|
||||||
form.fields['credit_amount'].initial = club.membership_fee_paid
|
form.fields['credit_amount'].initial = club.membership_fee_paid
|
||||||
# Ensure that the user is member of the parent club and all its the family tree.
|
# Ensure that the user is member of the parent club and all its the family tree.
|
||||||
@ -683,7 +683,7 @@ class ClubAddMemberView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
"""
|
"""
|
||||||
# Get the club that is concerned by the membership
|
# Get the club that is concerned by the membership
|
||||||
if "club_pk" in self.kwargs: # get from url of new membership
|
if "club_pk" in self.kwargs: # get from url of new membership
|
||||||
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request.user, Club, "view")) \
|
club = Club.objects.filter(PermissionBackend.filter_queryset(self.request, Club, "view")) \
|
||||||
.get(pk=self.kwargs["club_pk"])
|
.get(pk=self.kwargs["club_pk"])
|
||||||
user = form.instance.user
|
user = form.instance.user
|
||||||
old_membership = None
|
old_membership = None
|
||||||
@ -867,7 +867,7 @@ class ClubMembersListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableV
|
|||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
club = Club.objects.filter(
|
club = Club.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Club, "view")
|
PermissionBackend.filter_queryset(self.request, Club, "view")
|
||||||
).get(pk=self.kwargs["pk"])
|
).get(pk=self.kwargs["pk"])
|
||||||
context["club"] = club
|
context["club"] = club
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ from rest_framework.exceptions import ValidationError
|
|||||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||||
from member.api.serializers import MembershipSerializer
|
from member.api.serializers import MembershipSerializer
|
||||||
from member.models import Membership
|
from member.models import Membership
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
from rest_framework.utils import model_meta
|
from rest_framework.utils import model_meta
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
|||||||
"""
|
"""
|
||||||
# If the user has no right to see the note, then we only display the note identifier
|
# If the user has no right to see the note, then we only display the note identifier
|
||||||
return NotePolymorphicSerializer().to_representation(obj.note)\
|
return NotePolymorphicSerializer().to_representation(obj.note)\
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "note.view_note", obj.note)\
|
if PermissionBackend.check_perm(get_current_request(), "note.view_note", obj.note)\
|
||||||
else dict(
|
else dict(
|
||||||
id=obj.note.id,
|
id=obj.note.id,
|
||||||
name=str(obj.note),
|
name=str(obj.note),
|
||||||
@ -142,7 +142,7 @@ class ConsumerSerializer(serializers.ModelSerializer):
|
|||||||
def get_membership(self, obj):
|
def get_membership(self, obj):
|
||||||
if isinstance(obj.note, NoteUser):
|
if isinstance(obj.note, NoteUser):
|
||||||
memberships = Membership.objects.filter(
|
memberships = Membership.objects.filter(
|
||||||
PermissionBackend.filter_queryset(get_current_authenticated_user(), Membership, "view")).filter(
|
PermissionBackend.filter_queryset(get_current_request(), Membership, "view")).filter(
|
||||||
user=obj.note.user,
|
user=obj.note.user,
|
||||||
club=2, # Kfet
|
club=2, # Kfet
|
||||||
).order_by("-date_start")
|
).order_by("-date_start")
|
||||||
|
@ -10,7 +10,6 @@ from rest_framework import viewsets
|
|||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework import status
|
from rest_framework import status
|
||||||
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
from api.viewsets import ReadProtectedModelViewSet, ReadOnlyProtectedModelViewSet
|
||||||
from note_kfet.middlewares import get_current_session
|
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
from .serializers import NotePolymorphicSerializer, AliasSerializer, ConsumerSerializer,\
|
||||||
@ -40,12 +39,11 @@ class NotePolymorphicViewSet(ReadProtectedModelViewSet):
|
|||||||
Parse query and apply filters.
|
Parse query and apply filters.
|
||||||
:return: The filtered set of requested notes
|
:return: The filtered set of requested notes
|
||||||
"""
|
"""
|
||||||
user = self.request.user
|
queryset = self.queryset.filter(PermissionBackend.filter_queryset(self.request, Note, "view")
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
| PermissionBackend.filter_queryset(self.request, NoteUser, "view")
|
||||||
queryset = self.queryset.filter(PermissionBackend.filter_queryset(user, Note, "view")
|
| PermissionBackend.filter_queryset(self.request, NoteClub, "view")
|
||||||
| PermissionBackend.filter_queryset(user, NoteUser, "view")
|
| PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
|
||||||
| PermissionBackend.filter_queryset(user, NoteClub, "view")
|
.distinct()
|
||||||
| PermissionBackend.filter_queryset(user, NoteSpecial, "view")).distinct()
|
|
||||||
|
|
||||||
alias = self.request.query_params.get("alias", ".*")
|
alias = self.request.query_params.get("alias", ".*")
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
@ -67,7 +65,8 @@ class AliasViewSet(ReadProtectedModelViewSet):
|
|||||||
serializer_class = AliasSerializer
|
serializer_class = AliasSerializer
|
||||||
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
filter_backends = [SearchFilter, DjangoFilterBackend, OrderingFilter]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
ordering_fields = ['name', 'normalized_name', ]
|
ordering_fields = ['name', 'normalized_name', ]
|
||||||
|
|
||||||
def get_serializer_class(self):
|
def get_serializer_class(self):
|
||||||
@ -118,7 +117,8 @@ class ConsumerViewSet(ReadOnlyProtectedModelViewSet):
|
|||||||
serializer_class = ConsumerSerializer
|
serializer_class = ConsumerSerializer
|
||||||
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
filter_backends = [SearchFilter, OrderingFilter, DjangoFilterBackend]
|
||||||
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
search_fields = ['$normalized_name', '$name', '$note__polymorphic_ctype__model', ]
|
||||||
filterset_fields = ['note', 'note__noteuser__user', 'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
filterset_fields = ['name', 'normalized_name', 'note', 'note__noteuser__user',
|
||||||
|
'note__noteclub__club', 'note__polymorphic_ctype__model', ]
|
||||||
ordering_fields = ['name', 'normalized_name', ]
|
ordering_fields = ['name', 'normalized_name', ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -205,7 +205,5 @@ class TransactionViewSet(ReadProtectedModelViewSet):
|
|||||||
ordering_fields = ['created_at', 'amount', ]
|
ordering_fields = ['created_at', 'amount', ]
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
user = self.request.user
|
return self.model.objects.filter(PermissionBackend.filter_queryset(self.request, self.model, "view"))\
|
||||||
get_current_session().setdefault("permission_mask", 42)
|
|
||||||
return self.model.objects.filter(PermissionBackend.filter_queryset(user, self.model, "view"))\
|
|
||||||
.order_by("created_at", "id")
|
.order_by("created_at", "id")
|
||||||
|
@ -7,7 +7,7 @@ import django_tables2 as tables
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django_tables2.utils import A
|
from django_tables2.utils import A
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models.notes import Alias
|
from .models.notes import Alias
|
||||||
@ -88,16 +88,16 @@ class HistoryTable(tables.Table):
|
|||||||
"class": lambda record:
|
"class": lambda record:
|
||||||
str(record.valid).lower()
|
str(record.valid).lower()
|
||||||
+ (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
|
+ (' validate' if record.source.is_active and record.destination.is_active and PermissionBackend
|
||||||
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
|
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
|
||||||
else ''),
|
else ''),
|
||||||
"data-toggle": "tooltip",
|
"data-toggle": "tooltip",
|
||||||
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
|
"title": lambda record: (_("Click to invalidate") if record.valid else _("Click to validate"))
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
if PermissionBackend.check_perm(get_current_request(),
|
||||||
"note.change_transaction_invalidity_reason", record)
|
"note.change_transaction_invalidity_reason", record)
|
||||||
and record.source.is_active and record.destination.is_active else None,
|
and record.source.is_active and record.destination.is_active else None,
|
||||||
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
|
"onclick": lambda record: 'de_validate(' + str(record.id) + ', ' + str(record.valid).lower()
|
||||||
+ ', "' + str(record.__class__.__name__) + '")'
|
+ ', "' + str(record.__class__.__name__) + '")'
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(),
|
if PermissionBackend.check_perm(get_current_request(),
|
||||||
"note.change_transaction_invalidity_reason", record)
|
"note.change_transaction_invalidity_reason", record)
|
||||||
and record.source.is_active and record.destination.is_active else None,
|
and record.source.is_active and record.destination.is_active else None,
|
||||||
"onmouseover": lambda record: '$("#invalidity_reason_'
|
"onmouseover": lambda record: '$("#invalidity_reason_'
|
||||||
@ -126,7 +126,7 @@ class HistoryTable(tables.Table):
|
|||||||
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
When the validation status is hovered, an input field is displayed to let the user specify an invalidity reason
|
||||||
"""
|
"""
|
||||||
has_perm = PermissionBackend \
|
has_perm = PermissionBackend \
|
||||||
.check_perm(get_current_authenticated_user(), "note.change_transaction_invalidity_reason", record)
|
.check_perm(get_current_request(), "note.change_transaction_invalidity_reason", record)
|
||||||
|
|
||||||
val = "✔" if value else "✖"
|
val = "✔" if value else "✖"
|
||||||
|
|
||||||
@ -165,7 +165,7 @@ class AliasTable(tables.Table):
|
|||||||
extra_context={"delete_trans": _('delete')},
|
extra_context={"delete_trans": _('delete')},
|
||||||
attrs={'td': {'class': lambda record: 'col-sm-1' + (
|
attrs={'td': {'class': lambda record: 'col-sm-1' + (
|
||||||
' d-none' if not PermissionBackend.check_perm(
|
' d-none' if not PermissionBackend.check_perm(
|
||||||
get_current_authenticated_user(), "note.delete_alias",
|
get_current_request(), "note.delete_alias",
|
||||||
record) else '')}}, verbose_name=_("Delete"), )
|
record) else '')}}, verbose_name=_("Delete"), )
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
|||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
# retrieves only Transaction that user has the right to see.
|
# retrieves only Transaction that user has the right to see.
|
||||||
return Transaction.objects.filter(
|
return Transaction.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
PermissionBackend.filter_queryset(self.request, Transaction, "view")
|
||||||
).order_by("-created_at").all()[:20]
|
).order_by("-created_at").all()[:20]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -47,16 +47,16 @@ class TransactionCreateView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTabl
|
|||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(Transaction).pk
|
||||||
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
context['special_polymorphic_ctype'] = ContentType.objects.get_for_model(SpecialTransaction).pk
|
||||||
context['special_types'] = NoteSpecial.objects\
|
context['special_types'] = NoteSpecial.objects\
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, NoteSpecial, "view"))\
|
.filter(PermissionBackend.filter_queryset(self.request, NoteSpecial, "view"))\
|
||||||
.order_by("special_type").all()
|
.order_by("special_type").all()
|
||||||
|
|
||||||
# Add a shortcut for entry page for open activities
|
# Add a shortcut for entry page for open activities
|
||||||
if "activity" in settings.INSTALLED_APPS:
|
if "activity" in settings.INSTALLED_APPS:
|
||||||
from activity.models import Activity
|
from activity.models import Activity
|
||||||
activities_open = Activity.objects.filter(open=True).filter(
|
activities_open = Activity.objects.filter(open=True).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Activity, "view")).distinct().all()
|
PermissionBackend.filter_queryset(self.request, Activity, "view")).distinct().all()
|
||||||
context["activities_open"] = [a for a in activities_open
|
context["activities_open"] = [a for a in activities_open
|
||||||
if PermissionBackend.check_perm(self.request.user,
|
if PermissionBackend.check_perm(self.request,
|
||||||
"activity.add_entry",
|
"activity.add_entry",
|
||||||
Entry(activity=a,
|
Entry(activity=a,
|
||||||
note=self.request.user.note, ))]
|
note=self.request.user.note, ))]
|
||||||
@ -159,7 +159,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
templates = TransactionTemplate.objects.filter(
|
templates = TransactionTemplate.objects.filter(
|
||||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||||
)
|
)
|
||||||
if not templates.exists():
|
if not templates.exists():
|
||||||
raise PermissionDenied(_("You can't see any button."))
|
raise PermissionDenied(_("You can't see any button."))
|
||||||
@ -170,7 +170,7 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
restrict to the transaction history the user can see.
|
restrict to the transaction history the user can see.
|
||||||
"""
|
"""
|
||||||
return Transaction.objects.filter(
|
return Transaction.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view")
|
PermissionBackend.filter_queryset(self.request, Transaction, "view")
|
||||||
).order_by("-created_at").all()[:20]
|
).order_by("-created_at").all()[:20]
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -180,13 +180,13 @@ class ConsoView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
# for each category, find which transaction templates the user can see.
|
# for each category, find which transaction templates the user can see.
|
||||||
for category in categories:
|
for category in categories:
|
||||||
category.templates_filtered = category.templates.filter(
|
category.templates_filtered = category.templates.filter(
|
||||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||||
).filter(display=True).order_by('name').all()
|
).filter(display=True).order_by('name').all()
|
||||||
|
|
||||||
context['categories'] = [cat for cat in categories if cat.templates_filtered]
|
context['categories'] = [cat for cat in categories if cat.templates_filtered]
|
||||||
# some transactiontemplate are put forward to find them easily
|
# some transactiontemplate are put forward to find them easily
|
||||||
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
|
context['highlighted'] = TransactionTemplate.objects.filter(highlighted=True).filter(
|
||||||
PermissionBackend().filter_queryset(self.request.user, TransactionTemplate, "view")
|
PermissionBackend().filter_queryset(self.request, TransactionTemplate, "view")
|
||||||
).order_by('name').all()
|
).order_by('name').all()
|
||||||
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
context['polymorphic_ctype'] = ContentType.objects.get_for_model(RecurrentTransaction).pk
|
||||||
|
|
||||||
@ -209,7 +209,7 @@ class TransactionSearchView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView
|
|||||||
data = form.cleaned_data if form.is_valid() else {}
|
data = form.cleaned_data if form.is_valid() else {}
|
||||||
|
|
||||||
transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
|
transactions = Transaction.objects.annotate(total_amount=F("quantity") * F("amount")).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Transaction, "view"))\
|
PermissionBackend.filter_queryset(self.request, Transaction, "view"))\
|
||||||
.filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
|
.filter(Q(source=self.object) | Q(destination=self.object)).order_by('-created_at')
|
||||||
|
|
||||||
if "source" in data and data["source"]:
|
if "source" in data and data["source"]:
|
||||||
|
@ -4,12 +4,12 @@
|
|||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.contrib.auth.backends import ModelBackend
|
from django.contrib.auth.backends import ModelBackend
|
||||||
from django.contrib.auth.models import User, AnonymousUser
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Q, F
|
from django.db.models import Q, F
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from note.models import Note, NoteUser, NoteClub, NoteSpecial
|
from note.models import Note, NoteUser, NoteClub, NoteSpecial
|
||||||
from note_kfet.middlewares import get_current_session
|
from note_kfet.middlewares import get_current_request
|
||||||
from member.models import Membership, Club
|
from member.models import Membership, Club
|
||||||
|
|
||||||
from .decorators import memoize
|
from .decorators import memoize
|
||||||
@ -26,14 +26,31 @@ class PermissionBackend(ModelBackend):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@memoize
|
@memoize
|
||||||
def get_raw_permissions(user, t):
|
def get_raw_permissions(request, t):
|
||||||
"""
|
"""
|
||||||
Query permissions of a certain type for a user, then memoize it.
|
Query permissions of a certain type for a user, then memoize it.
|
||||||
:param user: The owner of the permissions
|
:param request: The current request
|
||||||
:param t: The type of the permissions: view, change, add or delete
|
:param t: The type of the permissions: view, change, add or delete
|
||||||
:return: The queryset of the permissions of the user (memoized) grouped by clubs
|
:return: The queryset of the permissions of the user (memoized) grouped by clubs
|
||||||
"""
|
"""
|
||||||
if isinstance(user, AnonymousUser):
|
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||||
|
# OAuth2 Authentication
|
||||||
|
user = request.auth.user
|
||||||
|
|
||||||
|
def permission_filter(membership_obj):
|
||||||
|
query = Q(pk=-1)
|
||||||
|
for scope in request.auth.scope.split(' '):
|
||||||
|
permission_id, club_id = scope.split('_')
|
||||||
|
if int(club_id) == membership_obj.club_id:
|
||||||
|
query |= Q(pk=permission_id)
|
||||||
|
return query
|
||||||
|
else:
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
def permission_filter(membership_obj):
|
||||||
|
return Q(mask__rank__lte=request.session.get("permission_mask", 42))
|
||||||
|
|
||||||
|
if user.is_anonymous:
|
||||||
# Unauthenticated users have no permissions
|
# Unauthenticated users have no permissions
|
||||||
return Permission.objects.none()
|
return Permission.objects.none()
|
||||||
|
|
||||||
@ -43,7 +60,7 @@ class PermissionBackend(ModelBackend):
|
|||||||
|
|
||||||
for membership in memberships:
|
for membership in memberships:
|
||||||
for role in membership.roles.all():
|
for role in membership.roles.all():
|
||||||
for perm in role.permissions.filter(type=t, mask__rank__lte=get_current_session().get("permission_mask", -1)).all():
|
for perm in role.permissions.filter(permission_filter(membership), type=t).all():
|
||||||
if not perm.permanent:
|
if not perm.permanent:
|
||||||
if membership.date_start > date.today() or membership.date_end < date.today():
|
if membership.date_start > date.today() or membership.date_end < date.today():
|
||||||
continue
|
continue
|
||||||
@ -52,16 +69,22 @@ class PermissionBackend(ModelBackend):
|
|||||||
return perms
|
return perms
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def permissions(user, model, type):
|
def permissions(request, model, type):
|
||||||
"""
|
"""
|
||||||
List all permissions of the given user that applies to a given model and a give 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 request: The current request
|
||||||
:param model: The model that the permissions shoud apply
|
:param model: The model that the permissions shoud apply
|
||||||
:param type: The type of the permissions: view, change, add or delete
|
:param type: The type of the permissions: view, change, add or delete
|
||||||
:return: A generator of the requested permissions
|
:return: A generator of the requested permissions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for permission in PermissionBackend.get_raw_permissions(user, type):
|
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||||
|
# OAuth2 Authentication
|
||||||
|
user = request.auth.user
|
||||||
|
else:
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
for permission in PermissionBackend.get_raw_permissions(request, type):
|
||||||
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
|
if not isinstance(model.model_class()(), permission.model.model_class()) or not permission.membership:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@ -88,20 +111,26 @@ class PermissionBackend(ModelBackend):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@memoize
|
@memoize
|
||||||
def filter_queryset(user, model, t, field=None):
|
def filter_queryset(request, model, t, field=None):
|
||||||
"""
|
"""
|
||||||
Filter a queryset by considering the permissions of a given user.
|
Filter a queryset by considering the permissions of a given user.
|
||||||
:param user: The owner of the permissions that are fetched
|
:param request: The current request
|
||||||
:param model: The concerned model of the queryset
|
:param model: The concerned model of the queryset
|
||||||
:param t: The type of modification (view, add, change, delete)
|
:param t: The type of modification (view, add, change, delete)
|
||||||
:param field: The field of the model to test, if concerned
|
:param field: The field of the model to test, if concerned
|
||||||
:return: A query that corresponds to the filter to give to a queryset
|
:return: A query that corresponds to the filter to give to a queryset
|
||||||
"""
|
"""
|
||||||
if user is None or isinstance(user, AnonymousUser):
|
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||||
|
# OAuth2 Authentication
|
||||||
|
user = request.auth.user
|
||||||
|
else:
|
||||||
|
user = request.user
|
||||||
|
|
||||||
|
if user is None or user.is_anonymous:
|
||||||
# Anonymous users can't do anything
|
# Anonymous users can't do anything
|
||||||
return Q(pk=-1)
|
return Q(pk=-1)
|
||||||
|
|
||||||
if user.is_superuser and get_current_session().get("permission_mask", -1) >= 42:
|
if user.is_superuser and request.session.get("permission_mask", -1) >= 42:
|
||||||
# Superusers have all rights
|
# Superusers have all rights
|
||||||
return Q()
|
return Q()
|
||||||
|
|
||||||
@ -110,7 +139,7 @@ class PermissionBackend(ModelBackend):
|
|||||||
|
|
||||||
# Never satisfied
|
# Never satisfied
|
||||||
query = Q(pk=-1)
|
query = Q(pk=-1)
|
||||||
perms = PermissionBackend.permissions(user, model, t)
|
perms = PermissionBackend.permissions(request, model, t)
|
||||||
for perm in perms:
|
for perm in perms:
|
||||||
if perm.field and field != perm.field:
|
if perm.field and field != perm.field:
|
||||||
continue
|
continue
|
||||||
@ -122,7 +151,7 @@ class PermissionBackend(ModelBackend):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@memoize
|
@memoize
|
||||||
def check_perm(user_obj, perm, obj=None):
|
def check_perm(request, perm, obj=None):
|
||||||
"""
|
"""
|
||||||
Check is the given user has the permission over a given object.
|
Check is the given user has the permission over a given object.
|
||||||
The result is then memoized.
|
The result is then memoized.
|
||||||
@ -130,10 +159,15 @@ class PermissionBackend(ModelBackend):
|
|||||||
primary key, the result is not memoized. Moreover, the right could change
|
primary key, the result is not memoized. Moreover, the right could change
|
||||||
(e.g. for a transaction, the balance of the user could change)
|
(e.g. for a transaction, the balance of the user could change)
|
||||||
"""
|
"""
|
||||||
if user_obj is None or isinstance(user_obj, AnonymousUser):
|
user_obj = request.user
|
||||||
return False
|
sess = request.session
|
||||||
|
|
||||||
sess = get_current_session()
|
if hasattr(request, 'auth') and request.auth is not None and hasattr(request.auth, 'scope'):
|
||||||
|
# OAuth2 Authentication
|
||||||
|
user_obj = request.auth.user
|
||||||
|
|
||||||
|
if user_obj is None or user_obj.is_anonymous:
|
||||||
|
return False
|
||||||
|
|
||||||
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
if user_obj.is_superuser and sess.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
@ -147,16 +181,19 @@ class PermissionBackend(ModelBackend):
|
|||||||
|
|
||||||
ct = ContentType.objects.get_for_model(obj)
|
ct = ContentType.objects.get_for_model(obj)
|
||||||
if any(permission.applies(obj, perm_type, perm_field)
|
if any(permission.applies(obj, perm_type, perm_field)
|
||||||
for permission in PermissionBackend.permissions(user_obj, ct, perm_type)):
|
for permission in PermissionBackend.permissions(request, ct, perm_type)):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def has_perm(self, user_obj, perm, obj=None):
|
def has_perm(self, user_obj, perm, obj=None):
|
||||||
return PermissionBackend.check_perm(user_obj, perm, obj)
|
# Warning: this does not check that user_obj has the permission,
|
||||||
|
# but if the current request has the permission.
|
||||||
|
# This function is implemented for backward compatibility, and should not be used.
|
||||||
|
return PermissionBackend.check_perm(get_current_request(), perm, obj)
|
||||||
|
|
||||||
def has_module_perms(self, user_obj, app_label):
|
def has_module_perms(self, user_obj, app_label):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_all_permissions(self, user_obj, obj=None):
|
def get_all_permissions(self, user_obj, obj=None):
|
||||||
ct = ContentType.objects.get_for_model(obj)
|
ct = ContentType.objects.get_for_model(obj)
|
||||||
return list(self.permissions(user_obj, ct, "view"))
|
return list(self.permissions(get_current_request(), ct, "view"))
|
||||||
|
@ -5,7 +5,7 @@ from functools import lru_cache
|
|||||||
from time import time
|
from time import time
|
||||||
|
|
||||||
from django.contrib.sessions.models import Session
|
from django.contrib.sessions.models import Session
|
||||||
from note_kfet.middlewares import get_current_session
|
from note_kfet.middlewares import get_current_request
|
||||||
|
|
||||||
|
|
||||||
def memoize(f):
|
def memoize(f):
|
||||||
@ -48,11 +48,11 @@ def memoize(f):
|
|||||||
last_collect = time()
|
last_collect = time()
|
||||||
|
|
||||||
# If there is no session, then we don't memoize anything.
|
# If there is no session, then we don't memoize anything.
|
||||||
sess = get_current_session()
|
request = get_current_request()
|
||||||
if sess is None or sess.session_key is None:
|
if request is None or request.session is None or request.session.session_key is None:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
sess_key = sess.session_key
|
sess_key = request.session.session_key
|
||||||
if sess_key not in sess_funs:
|
if sess_key not in sess_funs:
|
||||||
# lru_cache makes the job of memoization
|
# lru_cache makes the job of memoization
|
||||||
# We store only the 512 latest data per session. It has to be enough.
|
# We store only the 512 latest data per session. It has to be enough.
|
||||||
|
@ -45,7 +45,7 @@ class StrongDjangoObjectPermissions(DjangoObjectPermissions):
|
|||||||
|
|
||||||
perms = self.get_required_object_permissions(request.method, model_cls)
|
perms = self.get_required_object_permissions(request.method, model_cls)
|
||||||
# if not user.has_perms(perms, obj):
|
# if not user.has_perms(perms, obj):
|
||||||
if not all(PermissionBackend.check_perm(user, perm, obj) for perm in perms):
|
if not all(PermissionBackend.check_perm(request, perm, obj) for perm in perms):
|
||||||
# If the user does not have permissions we need to determine if
|
# If the user does not have permissions we need to determine if
|
||||||
# they have read permissions to see 403, or not, and simply see
|
# they have read permissions to see 403, or not, and simply see
|
||||||
# a 404 response.
|
# a 404 response.
|
||||||
|
34
apps/permission/scopes.py
Normal file
34
apps/permission/scopes.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from oauth2_provider.scopes import BaseScopes
|
||||||
|
from member.models import Club
|
||||||
|
from note_kfet.middlewares import get_current_request
|
||||||
|
|
||||||
|
from .backends import PermissionBackend
|
||||||
|
from .models import Permission
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionScopes(BaseScopes):
|
||||||
|
"""
|
||||||
|
An OAuth2 scope is defined by a permission object and a club.
|
||||||
|
A token will have a subset of permissions from the owner of the application,
|
||||||
|
and can be useful to make queries through the API with limited privileges.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_all_scopes(self):
|
||||||
|
return {f"{p.id}_{club.id}": f"{p.description} (club {club.name})"
|
||||||
|
for p in Permission.objects.all() for club in Club.objects.all()}
|
||||||
|
|
||||||
|
def get_available_scopes(self, application=None, request=None, *args, **kwargs):
|
||||||
|
if not application:
|
||||||
|
return []
|
||||||
|
return [f"{p.id}_{p.membership.club.id}"
|
||||||
|
for t in Permission.PERMISSION_TYPES
|
||||||
|
for p in PermissionBackend.get_raw_permissions(get_current_request(), t[0])]
|
||||||
|
|
||||||
|
def get_default_scopes(self, application=None, request=None, *args, **kwargs):
|
||||||
|
if not application:
|
||||||
|
return []
|
||||||
|
return [f"{p.id}_{p.membership.club.id}"
|
||||||
|
for p in PermissionBackend.get_raw_permissions(get_current_request(), 'view')]
|
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
from django.core.exceptions import PermissionDenied
|
from django.core.exceptions import PermissionDenied
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
|
|
||||||
@ -16,6 +16,9 @@ EXCLUDED = [
|
|||||||
'contenttypes.contenttype',
|
'contenttypes.contenttype',
|
||||||
'logs.changelog',
|
'logs.changelog',
|
||||||
'migrations.migration',
|
'migrations.migration',
|
||||||
|
'oauth2_provider.accesstoken',
|
||||||
|
'oauth2_provider.grant',
|
||||||
|
'oauth2_provider.refreshtoken',
|
||||||
'sessions.session',
|
'sessions.session',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -31,8 +34,8 @@ def pre_save_object(sender, instance, **kwargs):
|
|||||||
if hasattr(instance, "_force_save") or hasattr(instance, "_no_signal"):
|
if hasattr(instance, "_force_save") or hasattr(instance, "_no_signal"):
|
||||||
return
|
return
|
||||||
|
|
||||||
user = get_current_authenticated_user()
|
request = get_current_request()
|
||||||
if user is None:
|
if request is None:
|
||||||
# Action performed on shell is always granted
|
# Action performed on shell is always granted
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -45,7 +48,7 @@ def pre_save_object(sender, instance, **kwargs):
|
|||||||
# We check if the user can change the model
|
# We check if the user can change the model
|
||||||
|
|
||||||
# If the user has all right on a model, then OK
|
# If the user has all right on a model, then OK
|
||||||
if PermissionBackend.check_perm(user, app_label + ".change_" + model_name, instance):
|
if PermissionBackend.check_perm(request, app_label + ".change_" + model_name, instance):
|
||||||
return
|
return
|
||||||
|
|
||||||
# In the other case, we check if he/she has the right to change one field
|
# In the other case, we check if he/she has the right to change one field
|
||||||
@ -58,7 +61,8 @@ def pre_save_object(sender, instance, **kwargs):
|
|||||||
# If the field wasn't modified, no need to check the permissions
|
# If the field wasn't modified, no need to check the permissions
|
||||||
if old_value == new_value:
|
if old_value == new_value:
|
||||||
continue
|
continue
|
||||||
if not PermissionBackend.check_perm(user, app_label + ".change_" + model_name + "_" + field_name, instance):
|
if not PermissionBackend.check_perm(request, app_label + ".change_" + model_name + "_" + field_name,
|
||||||
|
instance):
|
||||||
raise PermissionDenied(
|
raise PermissionDenied(
|
||||||
_("You don't have the permission to change the field {field} on this instance of model"
|
_("You don't have the permission to change the field {field} on this instance of model"
|
||||||
" {app_label}.{model_name}.")
|
" {app_label}.{model_name}.")
|
||||||
@ -66,7 +70,7 @@ def pre_save_object(sender, instance, **kwargs):
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# We check if the user has right to add the object
|
# We check if the user has right to add the object
|
||||||
has_perm = PermissionBackend.check_perm(user, app_label + ".add_" + model_name, instance)
|
has_perm = PermissionBackend.check_perm(request, app_label + ".add_" + model_name, instance)
|
||||||
|
|
||||||
if not has_perm:
|
if not has_perm:
|
||||||
raise PermissionDenied(
|
raise PermissionDenied(
|
||||||
@ -87,8 +91,8 @@ def pre_delete_object(instance, **kwargs):
|
|||||||
# Don't check permissions on force-deleted objects
|
# Don't check permissions on force-deleted objects
|
||||||
return
|
return
|
||||||
|
|
||||||
user = get_current_authenticated_user()
|
request = get_current_request()
|
||||||
if user is None:
|
if request is None:
|
||||||
# Action performed on shell is always granted
|
# Action performed on shell is always granted
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -97,7 +101,7 @@ def pre_delete_object(instance, **kwargs):
|
|||||||
model_name = model_name_full[1]
|
model_name = model_name_full[1]
|
||||||
|
|
||||||
# We check if the user has rights to delete the object
|
# We check if the user has rights to delete the object
|
||||||
if not PermissionBackend.check_perm(user, app_label + ".delete_" + model_name, instance):
|
if not PermissionBackend.check_perm(request, app_label + ".delete_" + model_name, instance):
|
||||||
raise PermissionDenied(
|
raise PermissionDenied(
|
||||||
_("You don't have the permission to delete this instance of model {app_label}.{model_name}.")
|
_("You don't have the permission to delete this instance of model {app_label}.{model_name}.")
|
||||||
.format(app_label=app_label, model_name=model_name))
|
.format(app_label=app_label, model_name=model_name))
|
||||||
|
@ -8,7 +8,7 @@ from django.urls import reverse_lazy
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django_tables2 import A
|
from django_tables2 import A
|
||||||
from member.models import Membership
|
from member.models import Membership
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
|
|
||||||
@ -20,7 +20,7 @@ class RightsTable(tables.Table):
|
|||||||
def render_user(self, value):
|
def render_user(self, value):
|
||||||
# If the user has the right, link the displayed user with the page of its detail.
|
# If the user has the right, link the displayed user with the page of its detail.
|
||||||
s = value.username
|
s = value.username
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "auth.view_user", value):
|
if PermissionBackend.check_perm(get_current_request(), "auth.view_user", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:user_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
return s
|
return s
|
||||||
@ -28,7 +28,7 @@ class RightsTable(tables.Table):
|
|||||||
def render_club(self, value):
|
def render_club(self, value):
|
||||||
# If the user has the right, link the displayed user with the page of its detail.
|
# If the user has the right, link the displayed user with the page of its detail.
|
||||||
s = value.name
|
s = value.name
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.view_club", value):
|
if PermissionBackend.check_perm(get_current_request(), "member.view_club", value):
|
||||||
s = format_html("<a href={url}>{name}</a>",
|
s = format_html("<a href={url}>{name}</a>",
|
||||||
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
|
url=reverse_lazy('member:club_detail', kwargs={"pk": value.pk}), name=s)
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ class RightsTable(tables.Table):
|
|||||||
| Q(name="Bureau de club"))
|
| Q(name="Bureau de club"))
|
||||||
& Q(weirole__isnull=True))).all()
|
& Q(weirole__isnull=True))).all()
|
||||||
s = ", ".join(str(role) for role in roles)
|
s = ", ".join(str(role) for role in roles)
|
||||||
if PermissionBackend.check_perm(get_current_authenticated_user(), "member.change_membership_roles", record):
|
if PermissionBackend.check_perm(get_current_request(), "member.change_membership_roles", record):
|
||||||
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
|
s = format_html("<a href='" + str(reverse_lazy("member:club_manage_roles", kwargs={"pk": record.pk}))
|
||||||
+ "'>" + s + "</a>")
|
+ "'>" + s + "</a>")
|
||||||
return s
|
return s
|
||||||
|
74
apps/permission/templates/permission/scopes.html
Normal file
74
apps/permission/templates/permission/scopes.html
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h2>{% trans "Available scopes" %}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="accordion" id="accordionApps">
|
||||||
|
{% for app, app_scopes in scopes.items %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header" id="app-{{ app.name.lower }}-title">
|
||||||
|
<a class="text-decoration-none collapsed" href="#" data-toggle="collapse"
|
||||||
|
data-target="#app-{{ app.name.lower }}" aria-expanded="false"
|
||||||
|
aria-controls="app-{{ app.name.lower }}">
|
||||||
|
{{ app.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="collapse" id="app-{{ app.name.lower }}" aria-labelledby="app-{{ app.name.lower }}" data-target="#accordionApps">
|
||||||
|
<div class="card-body">
|
||||||
|
{% for scope_id, scope_desc in app_scopes.items %}
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-check-label" for="scope-{{ app.name.lower }}-{{ scope_id }}">
|
||||||
|
<input type="checkbox" id="scope-{{ app.name.lower }}-{{ scope_id }}"
|
||||||
|
name="scope-{{ app.name.lower }}" class="checkboxinput form-check-input" value="{{ scope_id }}">
|
||||||
|
{{ scope_desc }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<p id="url-{{ app.name.lower }}">
|
||||||
|
<a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code" target="_blank">
|
||||||
|
{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% empty %}
|
||||||
|
<p>
|
||||||
|
{% trans "No applications defined" %}.
|
||||||
|
<a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}.
|
||||||
|
</p>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
{% for app in scopes.keys %}
|
||||||
|
let elements = document.getElementsByName("scope-{{ app.name.lower }}");
|
||||||
|
for (let element of elements) {
|
||||||
|
element.onchange = function (event) {
|
||||||
|
let scope = ""
|
||||||
|
for (let element of elements) {
|
||||||
|
if (element.checked) {
|
||||||
|
scope += element.value + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
scope = scope.substr(0, scope.length - 1)
|
||||||
|
|
||||||
|
document.getElementById("url-{{ app.name.lower }}").innerHTML = 'Scopes : ' + scope
|
||||||
|
+ '<br><a href="{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='+ scope.replaceAll(' ', '%20')
|
||||||
|
+ '" target="_blank">{{ request.scheme }}://{{ request.get_host }}{% url 'oauth2_provider:authorize' %}?client_id={{ app.client_id }}&response_type=code&scope='
|
||||||
|
+ scope.replaceAll(' ', '%20') + '</a>'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{% endfor %}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
@ -1,12 +1,12 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.contrib.auth.models import AnonymousUser
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.template.defaultfilters import stringfilter
|
from django.template.defaultfilters import stringfilter
|
||||||
from django import template
|
from django import template
|
||||||
from note_kfet.middlewares import get_current_authenticated_user, get_current_session
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
|
||||||
|
from ..backends import PermissionBackend
|
||||||
|
|
||||||
|
|
||||||
@stringfilter
|
@stringfilter
|
||||||
@ -14,9 +14,10 @@ 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.
|
Return True if and only if the current user has right to see any object of the given model.
|
||||||
"""
|
"""
|
||||||
user = get_current_authenticated_user()
|
request = get_current_request()
|
||||||
session = get_current_session()
|
user = request.user
|
||||||
if user is None or isinstance(user, AnonymousUser):
|
session = request.session
|
||||||
|
if user is None or not user.is_authenticated:
|
||||||
return False
|
return False
|
||||||
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
elif user.is_superuser and session.get("permission_mask", -1) >= 42:
|
||||||
return True
|
return True
|
||||||
@ -29,11 +30,12 @@ def model_list(model_name, t="view", fetch=True):
|
|||||||
"""
|
"""
|
||||||
Return the queryset of all visible instances of the given model.
|
Return the queryset of all visible instances of the given model.
|
||||||
"""
|
"""
|
||||||
user = get_current_authenticated_user()
|
request = get_current_request()
|
||||||
|
user = request.user
|
||||||
spl = model_name.split(".")
|
spl = model_name.split(".")
|
||||||
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
ct = ContentType.objects.get(app_label=spl[0], model=spl[1])
|
||||||
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(user, ct, t))
|
qs = ct.model_class().objects.filter(PermissionBackend.filter_queryset(request, ct, t))
|
||||||
if user is None or isinstance(user, AnonymousUser):
|
if user is None or not user.is_authenticated:
|
||||||
return qs.none()
|
return qs.none()
|
||||||
if fetch:
|
if fetch:
|
||||||
qs = qs.all()
|
qs = qs.all()
|
||||||
@ -49,7 +51,7 @@ def model_list_length(model_name, t="view"):
|
|||||||
|
|
||||||
|
|
||||||
def has_perm(perm, obj):
|
def has_perm(perm, obj):
|
||||||
return PermissionBackend.check_perm(get_current_authenticated_user(), perm, obj)
|
return PermissionBackend.check_perm(get_current_request(), perm, obj)
|
||||||
|
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
94
apps/permission/tests/test_oauth2.py
Normal file
94
apps/permission/tests/test_oauth2.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
from django.utils.crypto import get_random_string
|
||||||
|
from member.models import Membership, Club
|
||||||
|
from note.models import NoteUser
|
||||||
|
from oauth2_provider.models import Application, AccessToken
|
||||||
|
|
||||||
|
from ..models import Role, Permission
|
||||||
|
|
||||||
|
|
||||||
|
class OAuth2TestCase(TestCase):
|
||||||
|
fixtures = ('initial', )
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.user = User.objects.create(
|
||||||
|
username="toto",
|
||||||
|
)
|
||||||
|
self.application = Application.objects.create(
|
||||||
|
name="Test",
|
||||||
|
client_type=Application.CLIENT_PUBLIC,
|
||||||
|
authorization_grant_type=Application.GRANT_AUTHORIZATION_CODE,
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_oauth2_access(self):
|
||||||
|
"""
|
||||||
|
Create a simple OAuth2 access token that only has the right to see data of the current user
|
||||||
|
and check that this token has required access, and nothing more.
|
||||||
|
"""
|
||||||
|
|
||||||
|
bde = Club.objects.get(name="BDE")
|
||||||
|
view_user_perm = Permission.objects.get(pk=1) # View own user detail
|
||||||
|
|
||||||
|
# Create access token that has access to our own user detail
|
||||||
|
token = AccessToken.objects.create(
|
||||||
|
user=self.user,
|
||||||
|
application=self.application,
|
||||||
|
scope=f"{view_user_perm.pk}_{bde.pk}",
|
||||||
|
token=get_random_string(64),
|
||||||
|
expires=timezone.now() + timedelta(days=365),
|
||||||
|
)
|
||||||
|
|
||||||
|
# No access without token
|
||||||
|
resp = self.client.get(f'/api/user/{self.user.pk}/')
|
||||||
|
self.assertEqual(resp.status_code, 403)
|
||||||
|
|
||||||
|
# Valid token but user has no membership, so the query is not returning the user object
|
||||||
|
resp = self.client.get(f'/api/user/{self.user.pk}/', **{'Authorization': f'Bearer {token.token}'})
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
# Create membership to validate permissions
|
||||||
|
NoteUser.objects.create(user=self.user)
|
||||||
|
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
|
||||||
|
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||||
|
membership.save()
|
||||||
|
|
||||||
|
# User is now a member and can now see its own user detail
|
||||||
|
resp = self.client.get(f'/api/user/{self.user.pk}/', **{'Authorization': f'Bearer {token.token}'})
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
|
# Token is not granted to see profile detail
|
||||||
|
resp = self.client.get(f'/api/members/profile/{self.user.profile.pk}/',
|
||||||
|
**{'Authorization': f'Bearer {token.token}'})
|
||||||
|
self.assertEqual(resp.status_code, 404)
|
||||||
|
|
||||||
|
def test_scopes(self):
|
||||||
|
"""
|
||||||
|
Ensure that the scopes page is loading.
|
||||||
|
"""
|
||||||
|
self.client.force_login(self.user)
|
||||||
|
|
||||||
|
resp = self.client.get(reverse('permission:scopes'))
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertIn(self.application, resp.context['scopes'])
|
||||||
|
self.assertNotIn('1_1', resp.context['scopes'][self.application]) # The user has not this permission
|
||||||
|
|
||||||
|
# Create membership to validate permissions
|
||||||
|
bde = Club.objects.get(name="BDE")
|
||||||
|
NoteUser.objects.create(user=self.user)
|
||||||
|
membership = Membership.objects.create(user=self.user, club_id=bde.pk)
|
||||||
|
membership.roles.add(Role.objects.get(name="Adhérent BDE"))
|
||||||
|
membership.save()
|
||||||
|
|
||||||
|
resp = self.client.get(reverse('permission:scopes'))
|
||||||
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
self.assertIn(self.application, resp.context['scopes'])
|
||||||
|
self.assertIn('1_1', resp.context['scopes'][self.application]) # Now the user has this permission
|
@ -1,10 +1,17 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from permission.views import RightsView
|
|
||||||
|
from .views import RightsView, ScopesView
|
||||||
|
|
||||||
app_name = 'permission'
|
app_name = 'permission'
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('rights', RightsView.as_view(), name="rights"),
|
path('rights/', RightsView.as_view(), name="rights"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if "oauth2_provider" in settings.INSTALLED_APPS:
|
||||||
|
urlpatterns += [
|
||||||
|
path('scopes/', ScopesView.as_view(), name="scopes"),
|
||||||
|
]
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
from collections import OrderedDict
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
@ -28,7 +28,7 @@ class ProtectQuerysetMixin:
|
|||||||
"""
|
"""
|
||||||
def get_queryset(self, filter_permissions=True, **kwargs):
|
def get_queryset(self, filter_permissions=True, **kwargs):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
return qs.filter(PermissionBackend.filter_queryset(self.request.user, qs.model, "view")).distinct()\
|
return qs.filter(PermissionBackend.filter_queryset(self.request, qs.model, "view")).distinct()\
|
||||||
if filter_permissions else qs
|
if filter_permissions else qs
|
||||||
|
|
||||||
def get_object(self, queryset=None):
|
def get_object(self, queryset=None):
|
||||||
@ -53,7 +53,7 @@ class ProtectQuerysetMixin:
|
|||||||
# We could also delete the field, but some views might be affected.
|
# We could also delete the field, but some views might be affected.
|
||||||
meta = form.instance._meta
|
meta = form.instance._meta
|
||||||
for key in form.base_fields:
|
for key in form.base_fields:
|
||||||
if not PermissionBackend.check_perm(self.request.user,
|
if not PermissionBackend.check_perm(self.request,
|
||||||
f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
|
f"{meta.app_label}.change_{meta.model_name}_" + key, self.object):
|
||||||
form.fields[key].widget = HiddenInput()
|
form.fields[key].widget = HiddenInput()
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ class ProtectedCreateView(LoginRequiredMixin, CreateView):
|
|||||||
# noinspection PyProtectedMember
|
# noinspection PyProtectedMember
|
||||||
app_label, model_name = model_class._meta.app_label, model_class._meta.model_name.lower()
|
app_label, model_name = model_class._meta.app_label, model_class._meta.model_name.lower()
|
||||||
perm = app_label + ".add_" + model_name
|
perm = app_label + ".add_" + model_name
|
||||||
if not PermissionBackend.check_perm(request.user, perm, self.get_sample_object()):
|
if not PermissionBackend.check_perm(request, perm, self.get_sample_object()):
|
||||||
raise PermissionDenied(_("You don't have the permission to add an instance of model "
|
raise PermissionDenied(_("You don't have the permission to add an instance of model "
|
||||||
"{app_label}.{model_name}.").format(app_label=app_label, model_name=model_name))
|
"{app_label}.{model_name}.").format(app_label=app_label, model_name=model_name))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@ -143,3 +143,26 @@ class RightsView(TemplateView):
|
|||||||
prefix="superusers-")
|
prefix="superusers-")
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
class ScopesView(LoginRequiredMixin, TemplateView):
|
||||||
|
template_name = "permission/scopes.html"
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs):
|
||||||
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
from oauth2_provider.models import Application
|
||||||
|
from .scopes import PermissionScopes
|
||||||
|
|
||||||
|
scopes = PermissionScopes()
|
||||||
|
context["scopes"] = {}
|
||||||
|
all_scopes = scopes.get_all_scopes()
|
||||||
|
for app in Application.objects.filter(user=self.request.user).all():
|
||||||
|
available_scopes = scopes.get_available_scopes(app)
|
||||||
|
context["scopes"][app] = OrderedDict()
|
||||||
|
items = [(k, v) for (k, v) in all_scopes.items() if k in available_scopes]
|
||||||
|
items.sort(key=lambda x: (int(x[0].split("_")[1]), int(x[0].split("_")[0])))
|
||||||
|
for k, v in items:
|
||||||
|
context["scopes"][app][k] = v
|
||||||
|
|
||||||
|
return context
|
||||||
|
@ -66,9 +66,11 @@ class UserCreateView(CreateView):
|
|||||||
profile_form.instance.user = user
|
profile_form.instance.user = user
|
||||||
profile = profile_form.save(commit=False)
|
profile = profile_form.save(commit=False)
|
||||||
user.profile = profile
|
user.profile = profile
|
||||||
|
user._force_save = True
|
||||||
user.save()
|
user.save()
|
||||||
user.refresh_from_db()
|
user.refresh_from_db()
|
||||||
profile.user = user
|
profile.user = user
|
||||||
|
profile._force_save = True
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
user.profile.send_email_validation_link()
|
user.profile.send_email_validation_link()
|
||||||
@ -110,7 +112,9 @@ class UserValidateView(TemplateView):
|
|||||||
self.validlink = True
|
self.validlink = True
|
||||||
user.is_active = user.profile.registration_valid or user.is_superuser
|
user.is_active = user.profile.registration_valid or user.is_superuser
|
||||||
user.profile.email_confirmed = True
|
user.profile.email_confirmed = True
|
||||||
|
user._force_save = True
|
||||||
user.save()
|
user.save()
|
||||||
|
user.profile._force_save = True
|
||||||
user.profile.save()
|
user.profile.save()
|
||||||
return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
|
return self.render_to_response(self.get_context_data(), status=200 if self.validlink else 400)
|
||||||
|
|
||||||
@ -230,9 +234,6 @@ class FutureUserDetailView(ProtectQuerysetMixin, LoginRequiredMixin, FormMixin,
|
|||||||
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
fee += bde.membership_fee_paid if user.profile.paid else bde.membership_fee_unpaid
|
||||||
kfet = Club.objects.get(name="Kfet")
|
kfet = Club.objects.get(name="Kfet")
|
||||||
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
fee += kfet.membership_fee_paid if user.profile.paid else kfet.membership_fee_unpaid
|
||||||
# In 2020, for COVID-19 reasons, the BDE offered 80 € to each new member that opens a Sogé account,
|
|
||||||
# since there is no WEI.
|
|
||||||
fee += 8000
|
|
||||||
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
|
ctx["total_fee"] = "{:.02f}".format(fee / 100, )
|
||||||
|
|
||||||
ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
|
ctx["declare_soge_account"] = SogeCredit.objects.filter(user=user).exists()
|
||||||
@ -387,7 +388,7 @@ class FutureUserInvalidateView(ProtectQuerysetMixin, LoginRequiredMixin, View):
|
|||||||
Delete the pre-registered user which id is given in the URL.
|
Delete the pre-registered user which id is given in the URL.
|
||||||
"""
|
"""
|
||||||
user = User.objects.filter(profile__registration_valid=False)\
|
user = User.objects.filter(profile__registration_valid=False)\
|
||||||
.filter(PermissionBackend.filter_queryset(request.user, User, "change", "is_valid"))\
|
.filter(PermissionBackend.filter_queryset(request, User, "change", "is_valid"))\
|
||||||
.get(pk=self.kwargs["pk"])
|
.get(pk=self.kwargs["pk"])
|
||||||
# Delete associated soge credits before
|
# Delete associated soge credits before
|
||||||
SogeCredit.objects.filter(user=user).delete()
|
SogeCredit.objects.filter(user=user).delete()
|
||||||
|
@ -303,7 +303,7 @@ class SogeCredit(models.Model):
|
|||||||
@property
|
@property
|
||||||
def amount(self):
|
def amount(self):
|
||||||
return self.credit_transaction.total if self.valid \
|
return self.credit_transaction.total if self.valid \
|
||||||
else sum(transaction.total for transaction in self.transactions.all()) + 8000
|
else sum(transaction.total for transaction in self.transactions.all())
|
||||||
|
|
||||||
def invalidate(self):
|
def invalidate(self):
|
||||||
"""
|
"""
|
||||||
|
@ -107,7 +107,7 @@ class InvoiceListView(LoginRequiredMixin, SingleTableView):
|
|||||||
name="",
|
name="",
|
||||||
address="",
|
address="",
|
||||||
)
|
)
|
||||||
if not PermissionBackend.check_perm(self.request.user, "treasury.add_invoice", sample_invoice):
|
if not PermissionBackend.check_perm(self.request, "treasury.add_invoice", sample_invoice):
|
||||||
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -194,7 +194,7 @@ class InvoiceRenderView(LoginRequiredMixin, View):
|
|||||||
|
|
||||||
def get(self, request, **kwargs):
|
def get(self, request, **kwargs):
|
||||||
pk = kwargs["pk"]
|
pk = kwargs["pk"]
|
||||||
invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request.user, Invoice, "view")).get(pk=pk)
|
invoice = Invoice.objects.filter(PermissionBackend.filter_queryset(request, Invoice, "view")).get(pk=pk)
|
||||||
tex = invoice.tex
|
tex = invoice.tex
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -259,7 +259,7 @@ class RemittanceCreateView(ProtectQuerysetMixin, ProtectedCreateView):
|
|||||||
|
|
||||||
context["table"] = RemittanceTable(
|
context["table"] = RemittanceTable(
|
||||||
data=Remittance.objects.filter(
|
data=Remittance.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all())
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all())
|
||||||
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
|
context["special_transactions"] = SpecialTransactionTable(data=SpecialTransaction.objects.none())
|
||||||
|
|
||||||
return context
|
return context
|
||||||
@ -281,7 +281,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
|||||||
remittance_type_id=1,
|
remittance_type_id=1,
|
||||||
comment="",
|
comment="",
|
||||||
)
|
)
|
||||||
if not PermissionBackend.check_perm(self.request.user, "treasury.add_remittance", sample_remittance):
|
if not PermissionBackend.check_perm(self.request, "treasury.add_remittance", sample_remittance):
|
||||||
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
raise PermissionDenied(_("You are not able to see the treasury interface."))
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|
||||||
@ -290,7 +290,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
opened_remittances = RemittanceTable(
|
opened_remittances = RemittanceTable(
|
||||||
data=Remittance.objects.filter(closed=False).filter(
|
data=Remittance.objects.filter(closed=False).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
prefix="opened-remittances-",
|
prefix="opened-remittances-",
|
||||||
)
|
)
|
||||||
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
|
opened_remittances.paginate(page=self.request.GET.get("opened-remittances-page", 1), per_page=10)
|
||||||
@ -298,7 +298,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
|||||||
|
|
||||||
closed_remittances = RemittanceTable(
|
closed_remittances = RemittanceTable(
|
||||||
data=Remittance.objects.filter(closed=True).filter(
|
data=Remittance.objects.filter(closed=True).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
prefix="closed-remittances-",
|
prefix="closed-remittances-",
|
||||||
)
|
)
|
||||||
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
|
closed_remittances.paginate(page=self.request.GET.get("closed-remittances-page", 1), per_page=10)
|
||||||
@ -307,7 +307,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
|||||||
no_remittance_tr = SpecialTransactionTable(
|
no_remittance_tr = SpecialTransactionTable(
|
||||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||||
specialtransactionproxy__remittance=None).filter(
|
specialtransactionproxy__remittance=None).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
exclude=('remittance_remove', ),
|
exclude=('remittance_remove', ),
|
||||||
prefix="no-remittance-",
|
prefix="no-remittance-",
|
||||||
)
|
)
|
||||||
@ -317,7 +317,7 @@ class RemittanceListView(LoginRequiredMixin, TemplateView):
|
|||||||
with_remittance_tr = SpecialTransactionTable(
|
with_remittance_tr = SpecialTransactionTable(
|
||||||
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
data=SpecialTransaction.objects.filter(source__in=NoteSpecial.objects.filter(~Q(remittancetype=None)),
|
||||||
specialtransactionproxy__remittance__closed=False).filter(
|
specialtransactionproxy__remittance__closed=False).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all(),
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all(),
|
||||||
exclude=('remittance_add', ),
|
exclude=('remittance_add', ),
|
||||||
prefix="with-remittance-",
|
prefix="with-remittance-",
|
||||||
)
|
)
|
||||||
@ -342,7 +342,7 @@ class RemittanceUpdateView(ProtectQuerysetMixin, LoginRequiredMixin, UpdateView)
|
|||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
|
data = SpecialTransaction.objects.filter(specialtransactionproxy__remittance=self.object).filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, Remittance, "view")).all()
|
PermissionBackend.filter_queryset(self.request, Remittance, "view")).all()
|
||||||
context["special_transactions"] = SpecialTransactionTable(
|
context["special_transactions"] = SpecialTransactionTable(
|
||||||
data=data,
|
data=data,
|
||||||
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
|
exclude=('remittance_add', 'remittance_remove', ) if self.object.closed else ('remittance_add', ))
|
||||||
|
@ -53,7 +53,8 @@ class WEIBusInformation:
|
|||||||
def free_seats(self, surveys: List["WEISurvey"] = None):
|
def free_seats(self, surveys: List["WEISurvey"] = None):
|
||||||
size = self.bus.size
|
size = self.bus.size
|
||||||
already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
|
already_occupied = WEIMembership.objects.filter(bus=self.bus).count()
|
||||||
valid_surveys = sum(1 for survey in surveys if survey.information.valid) if surveys else 0
|
valid_surveys = sum(1 for survey in surveys if survey.information.valid
|
||||||
|
and survey.information.get_selected_bus() == self.bus) if surveys else 0
|
||||||
return size - already_occupied - valid_surveys
|
return size - already_occupied - valid_surveys
|
||||||
|
|
||||||
def has_free_seats(self, surveys=None):
|
def has_free_seats(self, surveys=None):
|
||||||
|
@ -16,7 +16,7 @@ WORDS = [
|
|||||||
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
|
'Fanfare', 'Fracassage', 'Féria', 'Hard rock', 'Hoeggarden', 'House', 'Huit-six', 'IPA', 'Inclusif', 'Inferno',
|
||||||
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
|
'Introverti', 'Jager bomb', 'Jazz', 'Jeux d\'alcool', 'Jeux de rôles', 'Jeux vidéo', 'Jul', 'Jus de fruit',
|
||||||
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
|
'Karaoké', 'LGBTQI+', 'Lady Gaga', 'Loup garou', 'Morning beer', 'Métal', 'Nuit blanche', 'Ovalie', 'Psychedelic',
|
||||||
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
|
'Pétanque', 'Rave', 'Reggae', 'Rhum', 'Ricard', 'Rock', 'Rosé', 'Rétro', 'Séducteur', 'Techno', 'Thérapie taxi',
|
||||||
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
|
'Théâtre', 'Trap', 'Turn up', 'Underground', 'Volley', 'Wati B', 'Zinédine Zidane',
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -45,9 +45,9 @@ class WEISurveyForm2021(forms.Form):
|
|||||||
rng = Random(information.seed)
|
rng = Random(information.seed)
|
||||||
|
|
||||||
words = []
|
words = []
|
||||||
for _ in range(information.step + 1):
|
for _ignored in range(information.step + 1):
|
||||||
# Generate N times words
|
# Generate N times words
|
||||||
words = [rng.choice(WORDS) for _ in range(10)]
|
words = [rng.choice(WORDS) for _ignored2 in range(10)]
|
||||||
words = [(w, w) for w in words]
|
words = [(w, w) for w in words]
|
||||||
if self.data:
|
if self.data:
|
||||||
self.fields["word"].choices = [(w, w) for w in WORDS]
|
self.fields["word"].choices = [(w, w) for w in WORDS]
|
||||||
@ -162,7 +162,7 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
|||||||
while free_surveys: # Some students are not affected
|
while free_surveys: # Some students are not affected
|
||||||
survey = free_surveys[0]
|
survey = free_surveys[0]
|
||||||
buses = survey.ordered_buses() # Preferences of the student
|
buses = survey.ordered_buses() # Preferences of the student
|
||||||
for bus, _ in buses:
|
for bus, _ignored in buses:
|
||||||
if self.get_bus_information(bus).has_free_seats(surveys):
|
if self.get_bus_information(bus).has_free_seats(surveys):
|
||||||
# Selected bus has free places. Put student in the bus
|
# Selected bus has free places. Put student in the bus
|
||||||
survey.select_bus(bus)
|
survey.select_bus(bus)
|
||||||
@ -190,6 +190,9 @@ class WEISurveyAlgorithm2021(WEISurveyAlgorithm):
|
|||||||
# If it does not exist, choose the next bus.
|
# If it does not exist, choose the next bus.
|
||||||
least_preferred_survey.free()
|
least_preferred_survey.free()
|
||||||
least_preferred_survey.save()
|
least_preferred_survey.save()
|
||||||
|
free_surveys.append(least_preferred_survey)
|
||||||
survey.select_bus(bus)
|
survey.select_bus(bus)
|
||||||
survey.save()
|
survey.save()
|
||||||
break
|
break
|
||||||
|
else:
|
||||||
|
raise ValueError(f"User {survey.registration.user} has no free seat")
|
||||||
|
@ -5,7 +5,7 @@ from argparse import ArgumentParser, FileType
|
|||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from wei.forms import CurrentSurvey
|
from ...forms import CurrentSurvey
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
@ -8,7 +8,7 @@ from django.urls import reverse_lazy
|
|||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django_tables2 import A
|
from django_tables2 import A
|
||||||
from note_kfet.middlewares import get_current_authenticated_user
|
from note_kfet.middlewares import get_current_request
|
||||||
from permission.backends import PermissionBackend
|
from permission.backends import PermissionBackend
|
||||||
|
|
||||||
from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership
|
from .models import WEIClub, WEIRegistration, Bus, BusTeam, WEIMembership
|
||||||
@ -85,7 +85,7 @@ class WEIRegistrationTable(tables.Table):
|
|||||||
|
|
||||||
def render_validate(self, record):
|
def render_validate(self, record):
|
||||||
hasperm = PermissionBackend.check_perm(
|
hasperm = PermissionBackend.check_perm(
|
||||||
get_current_authenticated_user(), "wei.add_weimembership", WEIMembership(
|
get_current_request(), "wei.add_weimembership", WEIMembership(
|
||||||
club=record.wei,
|
club=record.wei,
|
||||||
user=record.user,
|
user=record.user,
|
||||||
date_start=date.today(),
|
date_start=date.today(),
|
||||||
@ -110,7 +110,7 @@ class WEIRegistrationTable(tables.Table):
|
|||||||
f"title=\"{tooltip}\" href=\"{url}\">{text}</a>")
|
f"title=\"{tooltip}\" href=\"{url}\">{text}</a>")
|
||||||
|
|
||||||
def render_delete(self, record):
|
def render_delete(self, record):
|
||||||
hasperm = PermissionBackend.check_perm(get_current_authenticated_user(), "wei.delete_weimembership", record)
|
hasperm = PermissionBackend.check_perm(get_current_request(), "wei.delete_weimembership", record)
|
||||||
return _("Delete") if hasperm else format_html("<span class='no-perm'></span>")
|
return _("Delete") if hasperm else format_html("<span class='no-perm'></span>")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
109
apps/wei/tests/test_wei_algorithm_2021.py
Normal file
109
apps/wei/tests/test_wei_algorithm_2021.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from ..forms.surveys.wei2021 import WEIBusInformation2021, WEISurvey2021, WORDS, WEISurveyInformation2021
|
||||||
|
from ..models import Bus, WEIClub, WEIRegistration
|
||||||
|
|
||||||
|
|
||||||
|
class TestWEIAlgorithm(TestCase):
|
||||||
|
"""
|
||||||
|
Run some tests to ensure that the WEI algorithm is working well.
|
||||||
|
"""
|
||||||
|
fixtures = ('initial',)
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""
|
||||||
|
Create some test data, with one WEI and 10 buses with random score attributions.
|
||||||
|
"""
|
||||||
|
self.wei = WEIClub.objects.create(
|
||||||
|
name="WEI 2021",
|
||||||
|
email="wei2021@example.com",
|
||||||
|
date_start='2021-09-17',
|
||||||
|
date_end='2021-09-19',
|
||||||
|
)
|
||||||
|
|
||||||
|
self.buses = []
|
||||||
|
for i in range(10):
|
||||||
|
bus = Bus.objects.create(wei=self.wei, name=f"Bus {i}", size=10)
|
||||||
|
self.buses.append(bus)
|
||||||
|
information = WEIBusInformation2021(bus)
|
||||||
|
for word in WORDS:
|
||||||
|
information.scores[word] = random.randint(0, 101)
|
||||||
|
information.save()
|
||||||
|
bus.save()
|
||||||
|
|
||||||
|
def test_survey_algorithm_small(self):
|
||||||
|
"""
|
||||||
|
There are only a few people in each bus, ensure that each person has its best bus
|
||||||
|
"""
|
||||||
|
# Add a few users
|
||||||
|
for i in range(10):
|
||||||
|
user = User.objects.create(username=f"user{i}")
|
||||||
|
registration = WEIRegistration.objects.create(
|
||||||
|
user=user,
|
||||||
|
wei=self.wei,
|
||||||
|
first_year=True,
|
||||||
|
birth_date='2000-01-01',
|
||||||
|
)
|
||||||
|
information = WEISurveyInformation2021(registration)
|
||||||
|
for j in range(1, 21):
|
||||||
|
setattr(information, f'word{j}', random.choice(WORDS))
|
||||||
|
information.step = 20
|
||||||
|
information.save(registration)
|
||||||
|
registration.save()
|
||||||
|
|
||||||
|
# Run algorithm
|
||||||
|
WEISurvey2021.get_algorithm_class()().run_algorithm()
|
||||||
|
|
||||||
|
# Ensure that everyone has its first choice
|
||||||
|
for r in WEIRegistration.objects.filter(wei=self.wei).all():
|
||||||
|
survey = WEISurvey2021(r)
|
||||||
|
preferred_bus = survey.ordered_buses()[0][0]
|
||||||
|
chosen_bus = survey.information.get_selected_bus()
|
||||||
|
self.assertEqual(preferred_bus, chosen_bus)
|
||||||
|
|
||||||
|
def test_survey_algorithm_full(self):
|
||||||
|
"""
|
||||||
|
Buses are full of first year people, ensure that they are happy
|
||||||
|
"""
|
||||||
|
# Add a lot of users
|
||||||
|
for i in range(95):
|
||||||
|
user = User.objects.create(username=f"user{i}")
|
||||||
|
registration = WEIRegistration.objects.create(
|
||||||
|
user=user,
|
||||||
|
wei=self.wei,
|
||||||
|
first_year=True,
|
||||||
|
birth_date='2000-01-01',
|
||||||
|
)
|
||||||
|
information = WEISurveyInformation2021(registration)
|
||||||
|
for j in range(1, 21):
|
||||||
|
setattr(information, f'word{j}', random.choice(WORDS))
|
||||||
|
information.step = 20
|
||||||
|
information.save(registration)
|
||||||
|
registration.save()
|
||||||
|
|
||||||
|
# Run algorithm
|
||||||
|
WEISurvey2021.get_algorithm_class()().run_algorithm()
|
||||||
|
|
||||||
|
penalty = 0
|
||||||
|
# Ensure that everyone seems to be happy
|
||||||
|
# We attribute a penalty for each user that didn't have its first choice
|
||||||
|
# The penalty is the square of the distance between the score of the preferred bus
|
||||||
|
# and the score of the attributed bus
|
||||||
|
# We consider it acceptable if the mean of this distance is lower than 5 %
|
||||||
|
for r in WEIRegistration.objects.filter(wei=self.wei).all():
|
||||||
|
survey = WEISurvey2021(r)
|
||||||
|
chosen_bus = survey.information.get_selected_bus()
|
||||||
|
buses = survey.ordered_buses()
|
||||||
|
score = min(v for bus, v in buses if bus == chosen_bus)
|
||||||
|
max_score = buses[0][1]
|
||||||
|
penalty += (max_score - score) ** 2
|
||||||
|
|
||||||
|
self.assertLessEqual(max_score - score, 25) # Always less than 25 % of tolerance
|
||||||
|
|
||||||
|
self.assertLessEqual(penalty / 100, 25) # Tolerance of 5 %
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import timedelta, date
|
from datetime import timedelta, date
|
||||||
from unittest import skip
|
|
||||||
|
|
||||||
from api.tests import TestAPI
|
from api.tests import TestAPI
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -813,10 +812,6 @@ class TestWEISurveyAlgorithm(TestCase):
|
|||||||
)
|
)
|
||||||
CurrentSurvey(self.registration).save()
|
CurrentSurvey(self.registration).save()
|
||||||
|
|
||||||
@skip # FIXME Write good unit tests
|
|
||||||
def test_survey_algorithm(self):
|
|
||||||
CurrentSurvey.get_algorithm_class()().run_algorithm()
|
|
||||||
|
|
||||||
|
|
||||||
class TestWeiAPI(TestAPI):
|
class TestWeiAPI(TestAPI):
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
|
@ -57,7 +57,7 @@ class WEIListView(ProtectQuerysetMixin, LoginRequiredMixin, SingleTableView):
|
|||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context["can_create_wei"] = PermissionBackend.check_perm(self.request.user, "wei.add_weiclub", WEIClub(
|
context["can_create_wei"] = PermissionBackend.check_perm(self.request, "wei.add_weiclub", WEIClub(
|
||||||
name="",
|
name="",
|
||||||
email="weiclub@example.com",
|
email="weiclub@example.com",
|
||||||
year=0,
|
year=0,
|
||||||
@ -112,7 +112,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
club = context["club"]
|
club = context["club"]
|
||||||
|
|
||||||
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
|
club_transactions = Transaction.objects.all().filter(Q(source=club.note) | Q(destination=club.note)) \
|
||||||
.filter(PermissionBackend.filter_queryset(self.request.user, Transaction, "view")) \
|
.filter(PermissionBackend.filter_queryset(self.request, Transaction, "view")) \
|
||||||
.order_by('-created_at', '-id')
|
.order_by('-created_at', '-id')
|
||||||
history_table = HistoryTable(club_transactions, prefix="history-")
|
history_table = HistoryTable(club_transactions, prefix="history-")
|
||||||
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
history_table.paginate(per_page=20, page=self.request.GET.get('history-page', 1))
|
||||||
@ -121,13 +121,13 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
club_member = WEIMembership.objects.filter(
|
club_member = WEIMembership.objects.filter(
|
||||||
club=club,
|
club=club,
|
||||||
date_end__gte=date.today(),
|
date_end__gte=date.today(),
|
||||||
).filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
|
).filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
|
||||||
membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
|
membership_table = WEIMembershipTable(data=club_member, prefix="membership-")
|
||||||
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
membership_table.paginate(per_page=20, page=self.request.GET.get('membership-page', 1))
|
||||||
context['member_list'] = membership_table
|
context['member_list'] = membership_table
|
||||||
|
|
||||||
pre_registrations = WEIRegistration.objects.filter(
|
pre_registrations = WEIRegistration.objects.filter(
|
||||||
PermissionBackend.filter_queryset(self.request.user, WEIRegistration, "view")).filter(
|
PermissionBackend.filter_queryset(self.request, WEIRegistration, "view")).filter(
|
||||||
membership=None,
|
membership=None,
|
||||||
wei=club
|
wei=club
|
||||||
)
|
)
|
||||||
@ -142,7 +142,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
my_registration = None
|
my_registration = None
|
||||||
context["my_registration"] = my_registration
|
context["my_registration"] = my_registration
|
||||||
|
|
||||||
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request.user, Bus, "view")) \
|
buses = Bus.objects.filter(PermissionBackend.filter_queryset(self.request, Bus, "view")) \
|
||||||
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
|
.filter(wei=self.object).annotate(count=Count("memberships")).order_by("name")
|
||||||
bus_table = BusTable(data=buses, prefix="bus-")
|
bus_table = BusTable(data=buses, prefix="bus-")
|
||||||
context['buses'] = bus_table
|
context['buses'] = bus_table
|
||||||
@ -167,7 +167,7 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
emergency_contact_phone="No",
|
emergency_contact_phone="No",
|
||||||
)
|
)
|
||||||
context["can_add_first_year_member"] = PermissionBackend \
|
context["can_add_first_year_member"] = PermissionBackend \
|
||||||
.check_perm(self.request.user, "wei.add_weiregistration", empty_fy_registration)
|
.check_perm(self.request, "wei.add_weiregistration", empty_fy_registration)
|
||||||
|
|
||||||
# Check if the user has the right to create a registration of a random old member.
|
# Check if the user has the right to create a registration of a random old member.
|
||||||
empty_old_registration = WEIRegistration(
|
empty_old_registration = WEIRegistration(
|
||||||
@ -180,13 +180,13 @@ class WEIDetailView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
emergency_contact_phone="No",
|
emergency_contact_phone="No",
|
||||||
)
|
)
|
||||||
context["can_add_any_member"] = PermissionBackend \
|
context["can_add_any_member"] = PermissionBackend \
|
||||||
.check_perm(self.request.user, "wei.add_weiregistration", empty_old_registration)
|
.check_perm(self.request, "wei.add_weiregistration", empty_old_registration)
|
||||||
|
|
||||||
empty_bus = Bus(
|
empty_bus = Bus(
|
||||||
wei=club,
|
wei=club,
|
||||||
name="",
|
name="",
|
||||||
)
|
)
|
||||||
context["can_add_bus"] = PermissionBackend.check_perm(self.request.user, "wei.add_bus", empty_bus)
|
context["can_add_bus"] = PermissionBackend.check_perm(self.request, "wei.add_bus", empty_bus)
|
||||||
|
|
||||||
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
|
context["not_first_year"] = WEIMembership.objects.filter(user=self.request.user).exists()
|
||||||
|
|
||||||
@ -370,13 +370,13 @@ class BusManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context["club"] = self.object.wei
|
context["club"] = self.object.wei
|
||||||
|
|
||||||
bus = self.object
|
bus = self.object
|
||||||
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request.user, BusTeam, "view")) \
|
teams = BusTeam.objects.filter(PermissionBackend.filter_queryset(self.request, BusTeam, "view")) \
|
||||||
.filter(bus=bus).annotate(count=Count("memberships")).order_by("name")
|
.filter(bus=bus).annotate(count=Count("memberships")).order_by("name")
|
||||||
teams_table = BusTeamTable(data=teams, prefix="team-")
|
teams_table = BusTeamTable(data=teams, prefix="team-")
|
||||||
context["teams"] = teams_table
|
context["teams"] = teams_table
|
||||||
|
|
||||||
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
||||||
self.request.user, WEIMembership, "view")).filter(bus=bus)
|
self.request, WEIMembership, "view")).filter(bus=bus)
|
||||||
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
||||||
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
||||||
context["memberships"] = memberships_table
|
context["memberships"] = memberships_table
|
||||||
@ -469,7 +469,7 @@ class BusTeamManageView(ProtectQuerysetMixin, LoginRequiredMixin, DetailView):
|
|||||||
context["club"] = self.object.bus.wei
|
context["club"] = self.object.bus.wei
|
||||||
|
|
||||||
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
memberships = WEIMembership.objects.filter(PermissionBackend.filter_queryset(
|
||||||
self.request.user, WEIMembership, "view")).filter(team=self.object)
|
self.request, WEIMembership, "view")).filter(team=self.object)
|
||||||
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
memberships_table = WEIMembershipTable(data=memberships, prefix="membership-")
|
||||||
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
memberships_table.paginate(per_page=20, page=self.request.GET.get("membership-page", 1))
|
||||||
context["memberships"] = memberships_table
|
context["memberships"] = memberships_table
|
||||||
@ -659,7 +659,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
|||||||
data=self.request.POST if self.request.POST else None)
|
data=self.request.POST if self.request.POST else None)
|
||||||
for field_name, field in membership_form.fields.items():
|
for field_name, field in membership_form.fields.items():
|
||||||
if not PermissionBackend.check_perm(
|
if not PermissionBackend.check_perm(
|
||||||
self.request.user, "wei.change_membership_" + field_name, self.object.membership):
|
self.request, "wei.change_membership_" + field_name, self.object.membership):
|
||||||
field.widget = HiddenInput()
|
field.widget = HiddenInput()
|
||||||
del membership_form.fields["credit_type"]
|
del membership_form.fields["credit_type"]
|
||||||
del membership_form.fields["credit_amount"]
|
del membership_form.fields["credit_amount"]
|
||||||
@ -668,7 +668,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
|||||||
del membership_form.fields["bank"]
|
del membership_form.fields["bank"]
|
||||||
context["membership_form"] = membership_form
|
context["membership_form"] = membership_form
|
||||||
elif not self.object.first_year and PermissionBackend.check_perm(
|
elif not self.object.first_year and PermissionBackend.check_perm(
|
||||||
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
self.request, "wei.change_weiregistration_information_json", self.object):
|
||||||
choose_bus_form = WEIChooseBusForm(
|
choose_bus_form = WEIChooseBusForm(
|
||||||
self.request.POST if self.request.POST else dict(
|
self.request.POST if self.request.POST else dict(
|
||||||
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
|
bus=Bus.objects.filter(pk__in=self.object.information["preferred_bus_pk"]).all(),
|
||||||
@ -704,7 +704,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
|||||||
membership_form.save()
|
membership_form.save()
|
||||||
# If it is not validated and if this is an old member, then we update the choices
|
# If it is not validated and if this is an old member, then we update the choices
|
||||||
elif not form.instance.first_year and PermissionBackend.check_perm(
|
elif not form.instance.first_year and PermissionBackend.check_perm(
|
||||||
self.request.user, "wei.change_weiregistration_information_json", self.object):
|
self.request, "wei.change_weiregistration_information_json", self.object):
|
||||||
choose_bus_form = WEIChooseBusForm(self.request.POST)
|
choose_bus_form = WEIChooseBusForm(self.request.POST)
|
||||||
if not choose_bus_form.is_valid():
|
if not choose_bus_form.is_valid():
|
||||||
return self.form_invalid(form)
|
return self.form_invalid(form)
|
||||||
@ -726,7 +726,7 @@ class WEIUpdateRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Update
|
|||||||
survey = CurrentSurvey(self.object)
|
survey = CurrentSurvey(self.object)
|
||||||
if not survey.is_complete():
|
if not survey.is_complete():
|
||||||
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
return reverse_lazy("wei:wei_survey", kwargs={"pk": self.object.pk})
|
||||||
if PermissionBackend.check_perm(self.request.user, "wei.add_weimembership", WEIMembership(
|
if PermissionBackend.check_perm(self.request, "wei.add_weimembership", WEIMembership(
|
||||||
club=self.object.wei,
|
club=self.object.wei,
|
||||||
user=self.object.user,
|
user=self.object.user,
|
||||||
date_start=date.today(),
|
date_start=date.today(),
|
||||||
@ -753,7 +753,7 @@ class WEIDeleteRegistrationView(ProtectQuerysetMixin, LoginRequiredMixin, Delete
|
|||||||
if today > wei.membership_end:
|
if today > wei.membership_end:
|
||||||
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
return redirect(reverse_lazy('wei:wei_closed', args=(wei.pk,)))
|
||||||
|
|
||||||
if not PermissionBackend.check_perm(self.request.user, "wei.delete_weiregistration", object):
|
if not PermissionBackend.check_perm(self.request, "wei.delete_weiregistration", object):
|
||||||
raise PermissionDenied(_("You don't have the right to delete this WEI registration."))
|
raise PermissionDenied(_("You don't have the right to delete this WEI registration."))
|
||||||
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
@ -1049,7 +1049,7 @@ class MemberListRenderView(LoginRequiredMixin, View):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request.user, WEIMembership, "view"))
|
qs = WEIMembership.objects.filter(PermissionBackend.filter_queryset(self.request, WEIMembership, "view"))
|
||||||
qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by(
|
qs = qs.filter(club__pk=self.kwargs["wei_pk"]).order_by(
|
||||||
Lower('bus__name'),
|
Lower('bus__name'),
|
||||||
Lower('team__name'),
|
Lower('team__name'),
|
||||||
|
@ -5,19 +5,10 @@ L'authentification `OAuth2 <https://fr.wikipedia.org/wiki/OAuth>`_ est supporté
|
|||||||
Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateurs, mais aussi
|
Note Kfet. Elle offre l'avantage non seulement d'identifier les utilisateurs, mais aussi
|
||||||
de transmettre des informations à un service tiers tels que des informations personnelles,
|
de transmettre des informations à un service tiers tels que des informations personnelles,
|
||||||
le solde de la note ou encore les adhésions de l'utilisateur, en l'avertissant sur
|
le solde de la note ou encore les adhésions de l'utilisateur, en l'avertissant sur
|
||||||
quelles données sont effectivement collectées.
|
quelles données sont effectivement collectées. Ainsi, il est possible de développer des
|
||||||
|
appplications tierces qui peuvent se baser sur les données de la Note Kfet ou encore
|
||||||
|
faire des transactions.
|
||||||
|
|
||||||
.. danger::
|
|
||||||
L'implémentation actuelle ne permet pas de choisir quels droits on offre. Se connecter
|
|
||||||
par OAuth2 offre actuellement exactement les mêmes permissions que l'on n'aurait
|
|
||||||
normalement, avec le masque le plus haut, y compris en écriture.
|
|
||||||
|
|
||||||
Faites alors très attention lorsque vous vous connectez à un service tiers via OAuth2,
|
|
||||||
et contrôlez bien exactement ce que l'application fait de vos données, à savoir si
|
|
||||||
elle ignore bien tout ce dont elle n'a pas besoin.
|
|
||||||
|
|
||||||
À l'avenir, la fenêtre d'authentification pourra vous indiquer clairement quels
|
|
||||||
paramètres sont collectés.
|
|
||||||
|
|
||||||
Configuration du serveur
|
Configuration du serveur
|
||||||
------------------------
|
------------------------
|
||||||
@ -44,7 +35,15 @@ l'authentification OAuth2. On adapte alors la configuration pour permettre cela
|
|||||||
...
|
...
|
||||||
}
|
}
|
||||||
|
|
||||||
On ajoute les routes dans ``urls.py`` :
|
On a ensuite besoin de définir nos propres scopes afin d'avoir des permissions fines :
|
||||||
|
|
||||||
|
.. code:: python
|
||||||
|
|
||||||
|
OAUTH2_PROVIDER = {
|
||||||
|
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
|
||||||
|
}
|
||||||
|
|
||||||
|
On ajoute enfin les routes dans ``urls.py`` :
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
@ -58,8 +57,7 @@ L'OAuth2 est désormais prêt à être utilisé.
|
|||||||
Configuration client
|
Configuration client
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
Contrairement au `CAS <cas>`_, n'importe qui peut en théorie créer une application OAuth2.
|
Contrairement au `CAS <cas>`_, n'importe qui peut créer une application OAuth2.
|
||||||
En théorie, car pour l'instant les permissions ne leur permettent pas.
|
|
||||||
|
|
||||||
Pour créer une application, il faut se rendre à la page
|
Pour créer une application, il faut se rendre à la page
|
||||||
`/o/applications/ <https://note.crans.org/o/applications/>`_. Dans ``client type``,
|
`/o/applications/ <https://note.crans.org/o/applications/>`_. Dans ``client type``,
|
||||||
@ -72,14 +70,30 @@ Il vous suffit de donner à votre application :
|
|||||||
|
|
||||||
* L'identifiant client (client-ID)
|
* L'identifiant client (client-ID)
|
||||||
* La clé secrète
|
* La clé secrète
|
||||||
* Les scopes : sous-ensemble de ``[read, write]`` (ignoré pour l'instant, cf premier paragraphe)
|
* Les scopes, qui peuvent être récupérées sur cette page : `<https://note.crans.org/permission/scopes/>`_
|
||||||
* L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_
|
* L'URL d'autorisation : `<https://note.crans.org/o/authorize/>`_
|
||||||
* L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_
|
* L'URL d'obtention de jeton : `<https://note.crans.org/o/token/>`_
|
||||||
* L'URL de récupération des informations de l'utilisateur : `<https://note.crans.org/api/me/>`_
|
* Si besoin, l'URL de récupération des informations de l'utilisateur : `<https://note.crans.org/api/me/>`_
|
||||||
|
|
||||||
N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner
|
N'hésitez pas à consulter la page `<https://note.crans.org/api/me/>`_ pour s'imprégner
|
||||||
du format renvoyé.
|
du format renvoyé.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Un petit mot sur les scopes : tel qu'implémenté, une scope est une permission unitaire
|
||||||
|
(telle que décrite dans le modèle ``Permission``) associée à un club. Ainsi, un jeton
|
||||||
|
a accès à une scope si et seulement si le/la propriétaire du jeton dispose d'une adhésion
|
||||||
|
courante dans le club lié à la scope qui lui octroie cette permission.
|
||||||
|
|
||||||
|
Par exemple, un jeton pourra avoir accès à la permission de créer des transactions en lien
|
||||||
|
avec un club si et seulement si le propriétaire du jeton est trésorier du club.
|
||||||
|
|
||||||
|
La vérification des droits du propriétaire est faite systématiquement, afin de ne pas
|
||||||
|
faire confiance au jeton en cas de droits révoqués à son propriétaire.
|
||||||
|
|
||||||
|
Vous pouvez donc contrôler le plus finement possible les permissions octroyées à vos
|
||||||
|
jetons.
|
||||||
|
|
||||||
Avec Django-allauth
|
Avec Django-allauth
|
||||||
###################
|
###################
|
||||||
|
|
||||||
@ -131,3 +145,97 @@ alors autant le faire via un shell python :
|
|||||||
Si vous avez bien configuré ``django-allauth``, vous êtes désormais prêts par à vous
|
Si vous avez bien configuré ``django-allauth``, vous êtes désormais prêts par à vous
|
||||||
connecter via la note :) Par défaut, nom, prénom, pseudo et adresse e-mail sont
|
connecter via la note :) Par défaut, nom, prénom, pseudo et adresse e-mail sont
|
||||||
récupérés. Les autres données sont stockées mais inutilisées.
|
récupérés. Les autres données sont stockées mais inutilisées.
|
||||||
|
|
||||||
|
|
||||||
|
Application personnalisée
|
||||||
|
#########################
|
||||||
|
|
||||||
|
Ce modèle vous permet de créer vos propres applications à interfacer avec la Note Kfet.
|
||||||
|
|
||||||
|
Commencez par créer une application : `<https://note.crans.org/o/applications/register>`_.
|
||||||
|
Dans ``Client type``, choisissez ``Confidential`` si des informations confidentielles sont
|
||||||
|
amenées à transiter, sinon ``public``. Choisissez ``Authorization code`` dans
|
||||||
|
``Authorization grant type``.
|
||||||
|
|
||||||
|
Dans ``Redirect uris``, vous devez insérer l'ensemble des URL autorisées à être redirigées
|
||||||
|
à la suite d'une autorisation OAuth2. La première URL entrée sera l'URL par défaut dans le
|
||||||
|
cas où elle n'est pas explicitement indiquée lors de l'autorisation.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
À des fins de tests, il est possible de laisser `<http://localhost/>`_ pour faire des
|
||||||
|
appels à la main en récupérant le jeton d'autorisation.
|
||||||
|
|
||||||
|
Lorsqu'un client veut s'authentifier via la Note Kfet, il va devoir accéder à une page
|
||||||
|
d'authentification. La page d'autorisation est `<https://note.crans.org/o/authorize/>`_,
|
||||||
|
c'est sur cette page qu'il faut rediriger les utilisateurs. Il faut mettre en paramètre GET :
|
||||||
|
|
||||||
|
* ``client_id`` : l'identifiant client de l'application (public) ;
|
||||||
|
* ``response_type`` : mettre ``code`` ;
|
||||||
|
* ``scope`` : l'ensemble des scopes demandés, séparés par des espaces. Ces scopes peuvent
|
||||||
|
être récupérés sur la page `<https://note.crans.org/permission/scopes/>`_.
|
||||||
|
* ``redirect_uri`` : l'URL sur laquelle rediriger qui récupérera le code d'accès. Doit être
|
||||||
|
autorisée par l'application. À des fins de test, peut être `<http://localhost/>`_.
|
||||||
|
* ``state`` : optionnel, peut être utilisé pour permettre au client de détecter des requêtes
|
||||||
|
provenant d'autres sites.
|
||||||
|
|
||||||
|
Sur cette page, les permissions demandées seront listées, et l'utilisateur aura le choix
|
||||||
|
d'accepter ou non. Dans les deux cas, l'utilisateur sera redirigée vers ``redirect_uri``,
|
||||||
|
avec pour paramètre GET soit le message d'erreur, soit un paramètre ``code`` correspondant
|
||||||
|
au code d'autorisation.
|
||||||
|
|
||||||
|
Une fois ce code d'autorisation récupéré, il faut désormais récupérer le jeton d'accès.
|
||||||
|
Il faut pour cela aller sur l'URL `<https://note.crans.org/o/token/>`_, effectuer une
|
||||||
|
requête POST avec pour arguments :
|
||||||
|
|
||||||
|
* ``client_id`` ;
|
||||||
|
* ``client_secret`` ;
|
||||||
|
* ``grant_type`` : mettre ``authorization_code`` ;
|
||||||
|
* ``code`` : le code généré.
|
||||||
|
|
||||||
|
À noter que le code fourni n'est disponible que pendant quelques secondes.
|
||||||
|
|
||||||
|
À des fins de tests, on peut envoyer la requête avec ``curl`` :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
curl -X POST https://note.crans.org/o/token/ -d "client_id=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&client_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&grant_type=authorization_code&code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
|
||||||
|
Le serveur renverra si tout se passe bien une réponse JSON :
|
||||||
|
|
||||||
|
.. code:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"access_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||||
|
"expires_in": 36000,
|
||||||
|
"token_type": "Bearer",
|
||||||
|
"scope": "1_1 1_2",
|
||||||
|
"refresh_token": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
}
|
||||||
|
|
||||||
|
On note donc 2 jetons différents : un d'accès et un de rafraîchissement. Le jeton d'accès
|
||||||
|
est celui qui sera donné à l'API pour s'authentifier, et qui expire au bout de quelques
|
||||||
|
heures.
|
||||||
|
|
||||||
|
Il suffit désormais d'ajouter l'en-tête ``Authorization: Bearer ACCESS_TOKEN`` pour se
|
||||||
|
connecter à la note grâce à ce jeton d'accès.
|
||||||
|
|
||||||
|
Pour tester :
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
curl https://note.crans.org/api/me -H "Authorization: Bearer XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
|
||||||
|
|
||||||
|
En cas d'expiration de ce jeton d'accès, il est possible de le renouveler grâce au jeton
|
||||||
|
de rafraichissement à usage unique. Il suffit pour cela de refaire une requête sur la page
|
||||||
|
`<https://note.crans.org/o/token/>`_ avec pour paramètres :
|
||||||
|
|
||||||
|
* ``client_id`` ;
|
||||||
|
* ``client_secret`` ;
|
||||||
|
* ``grant_type`` : mettre ``refresh_token`` ;
|
||||||
|
* ``refresh_token`` : le jeton de rafraîchissement.
|
||||||
|
|
||||||
|
Le serveur vous fournira alors une nouvelle paire de jetons, comme précédemment.
|
||||||
|
À noter qu'un jeton de rafraîchissement est à usage unique.
|
||||||
|
|
||||||
|
N'hésitez pas à vous renseigner sur OAuth2 pour plus d'informations.
|
||||||
|
@ -7,7 +7,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: \n"
|
"Project-Id-Version: \n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2021-08-29 14:06+0200\n"
|
"POT-Creation-Date: 2021-06-15 21:17+0200\n"
|
||||||
"PO-Revision-Date: 2020-11-16 20:02+0000\n"
|
"PO-Revision-Date: 2020-11-16 20:02+0000\n"
|
||||||
"Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
|
"Last-Translator: Yohann D'ANELLO <ynerant@crans.org>\n"
|
||||||
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
|
"Language-Team: French <http://translate.ynerant.fr/projects/nk20/nk20/fr/>\n"
|
||||||
@ -540,8 +540,8 @@ msgstr "Taille maximale : 2 Mo"
|
|||||||
msgid "This image cannot be loaded."
|
msgid "This image cannot be loaded."
|
||||||
msgstr "Cette image ne peut pas être chargée."
|
msgstr "Cette image ne peut pas être chargée."
|
||||||
|
|
||||||
#: apps/member/forms.py:141 apps/member/views.py:101
|
#: apps/member/forms.py:141 apps/member/views.py:100
|
||||||
#: apps/registration/forms.py:33 apps/registration/views.py:258
|
#: apps/registration/forms.py:33 apps/registration/views.py:254
|
||||||
msgid "An alias with a similar name already exists."
|
msgid "An alias with a similar name already exists."
|
||||||
msgstr "Un alias avec un nom similaire existe déjà."
|
msgstr "Un alias avec un nom similaire existe déjà."
|
||||||
|
|
||||||
@ -900,7 +900,7 @@ msgid "Account #"
|
|||||||
msgstr "Compte n°"
|
msgstr "Compte n°"
|
||||||
|
|
||||||
#: apps/member/templates/member/base.html:48
|
#: apps/member/templates/member/base.html:48
|
||||||
#: apps/member/templates/member/base.html:62 apps/member/views.py:58
|
#: apps/member/templates/member/base.html:62 apps/member/views.py:59
|
||||||
#: apps/registration/templates/registration/future_profile_detail.html:48
|
#: apps/registration/templates/registration/future_profile_detail.html:48
|
||||||
#: apps/wei/templates/wei/weimembership_form.html:117
|
#: apps/wei/templates/wei/weimembership_form.html:117
|
||||||
msgid "Update Profile"
|
msgid "Update Profile"
|
||||||
@ -932,7 +932,7 @@ msgid ""
|
|||||||
"Are you sure you want to lock this note? This will prevent any transaction "
|
"Are you sure you want to lock this note? This will prevent any transaction "
|
||||||
"that would be performed, until the note is unlocked."
|
"that would be performed, until the note is unlocked."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Êtes-vous sûr de vouloir verrouiller cette note ? Cela empêchera toute "
|
"Êtes-vous sûr⋅e de vouloir verrouiller cette note ? Cela empêchera toute "
|
||||||
"transaction qui devrait être faite, jusqu'à ce qu'elle soit déverrouillée."
|
"transaction qui devrait être faite, jusqu'à ce qu'elle soit déverrouillée."
|
||||||
|
|
||||||
#: apps/member/templates/member/base.html:104
|
#: apps/member/templates/member/base.html:104
|
||||||
@ -956,8 +956,8 @@ msgstr "Verrouiller de force"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure you want to unlock this note? Transactions will be re-enabled."
|
"Are you sure you want to unlock this note? Transactions will be re-enabled."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Êtes-vous sûr de vouloir déverrouiller cette note ? Les transactions seront "
|
"Êtes-vous sûr⋅e de vouloir déverrouiller cette note ? Les transactions "
|
||||||
"à nouveau possible."
|
"seront à nouveau possible."
|
||||||
|
|
||||||
#: apps/member/templates/member/club_alias.html:10
|
#: apps/member/templates/member/club_alias.html:10
|
||||||
#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:253
|
#: apps/member/templates/member/profile_alias.html:10 apps/member/views.py:253
|
||||||
@ -1053,18 +1053,50 @@ msgstr "Changer le mot de passe"
|
|||||||
msgid "API token"
|
msgid "API token"
|
||||||
msgstr "Accès API"
|
msgstr "Accès API"
|
||||||
|
|
||||||
#: apps/member/templates/member/manage_auth_tokens.html:19
|
#: apps/member/templates/member/manage_auth_tokens.html:12
|
||||||
|
msgid "Token authentication"
|
||||||
|
msgstr "Authentification par jeton"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:28
|
||||||
msgid "Token"
|
msgid "Token"
|
||||||
msgstr "Jeton"
|
msgstr "Jeton"
|
||||||
|
|
||||||
#: apps/member/templates/member/manage_auth_tokens.html:26
|
#: apps/member/templates/member/manage_auth_tokens.html:35
|
||||||
msgid "Created"
|
msgid "Created"
|
||||||
msgstr "Créé le"
|
msgstr "Créé le"
|
||||||
|
|
||||||
#: apps/member/templates/member/manage_auth_tokens.html:34
|
#: apps/member/templates/member/manage_auth_tokens.html:39
|
||||||
|
msgid "Warning"
|
||||||
|
msgstr "Attention"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:44
|
||||||
msgid "Regenerate token"
|
msgid "Regenerate token"
|
||||||
msgstr "Regénérer le jeton"
|
msgstr "Regénérer le jeton"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:53
|
||||||
|
msgid "OAuth2 authentication"
|
||||||
|
msgstr "Authentification OAuth2"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:79
|
||||||
|
msgid "Authorization:"
|
||||||
|
msgstr "Autorisation :"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:83
|
||||||
|
msgid "Token:"
|
||||||
|
msgstr "Jeton :"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:87
|
||||||
|
msgid "Revoke Token:"
|
||||||
|
msgstr "Révoquer le jeton :"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:91
|
||||||
|
msgid "Introspect Token:"
|
||||||
|
msgstr "Introspection :"
|
||||||
|
|
||||||
|
#: apps/member/templates/member/manage_auth_tokens.html:97
|
||||||
|
msgid "Show my applications"
|
||||||
|
msgstr "Voir mes applications"
|
||||||
|
|
||||||
#: apps/member/templates/member/picture_update.html:35
|
#: apps/member/templates/member/picture_update.html:35
|
||||||
msgid "Nevermind"
|
msgid "Nevermind"
|
||||||
msgstr "Annuler"
|
msgstr "Annuler"
|
||||||
@ -1097,7 +1129,7 @@ msgstr "Sauvegarder les changements"
|
|||||||
msgid "Registrations"
|
msgid "Registrations"
|
||||||
msgstr "Inscriptions"
|
msgstr "Inscriptions"
|
||||||
|
|
||||||
#: apps/member/views.py:71 apps/registration/forms.py:23
|
#: apps/member/views.py:72 apps/registration/forms.py:23
|
||||||
msgid "This address must be valid."
|
msgid "This address must be valid."
|
||||||
msgstr "Cette adresse doit être valide."
|
msgstr "Cette adresse doit être valide."
|
||||||
|
|
||||||
@ -1481,6 +1513,9 @@ msgstr "Pas de motif spécifié"
|
|||||||
#: apps/treasury/templates/treasury/sogecredit_detail.html:65
|
#: apps/treasury/templates/treasury/sogecredit_detail.html:65
|
||||||
#: apps/wei/tables.py:74 apps/wei/tables.py:114
|
#: apps/wei/tables.py:74 apps/wei/tables.py:114
|
||||||
#: apps/wei/templates/wei/weiregistration_confirm_delete.html:31
|
#: apps/wei/templates/wei/weiregistration_confirm_delete.html:31
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:18
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:39
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorized-token-delete.html:12
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr "Supprimer"
|
msgstr "Supprimer"
|
||||||
|
|
||||||
@ -1490,6 +1525,7 @@ msgstr "Supprimer"
|
|||||||
#: apps/wei/templates/wei/bus_detail.html:20
|
#: apps/wei/templates/wei/bus_detail.html:20
|
||||||
#: apps/wei/templates/wei/busteam_detail.html:20
|
#: apps/wei/templates/wei/busteam_detail.html:20
|
||||||
#: apps/wei/templates/wei/busteam_detail.html:40
|
#: apps/wei/templates/wei/busteam_detail.html:40
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:38
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr "Éditer"
|
msgstr "Éditer"
|
||||||
|
|
||||||
@ -1731,7 +1767,7 @@ msgstr "s'applique au club"
|
|||||||
msgid "role permissions"
|
msgid "role permissions"
|
||||||
msgstr "permissions par rôles"
|
msgstr "permissions par rôles"
|
||||||
|
|
||||||
#: apps/permission/signals.py:63
|
#: apps/permission/signals.py:67
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You don't have the permission to change the field {field} on this instance "
|
"You don't have the permission to change the field {field} on this instance "
|
||||||
@ -1740,7 +1776,7 @@ msgstr ""
|
|||||||
"Vous n'avez pas la permission de modifier le champ {field} sur l'instance du "
|
"Vous n'avez pas la permission de modifier le champ {field} sur l'instance du "
|
||||||
"modèle {app_label}.{model_name}."
|
"modèle {app_label}.{model_name}."
|
||||||
|
|
||||||
#: apps/permission/signals.py:73 apps/permission/views.py:105
|
#: apps/permission/signals.py:77 apps/permission/views.py:105
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You don't have the permission to add an instance of model {app_label}."
|
"You don't have the permission to add an instance of model {app_label}."
|
||||||
@ -1749,7 +1785,7 @@ msgstr ""
|
|||||||
"Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}."
|
"Vous n'avez pas la permission d'ajouter une instance du modèle {app_label}."
|
||||||
"{model_name}."
|
"{model_name}."
|
||||||
|
|
||||||
#: apps/permission/signals.py:102
|
#: apps/permission/signals.py:106
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
"You don't have the permission to delete this instance of model {app_label}."
|
"You don't have the permission to delete this instance of model {app_label}."
|
||||||
@ -1799,6 +1835,25 @@ msgstr "Requête :"
|
|||||||
msgid "No associated permission"
|
msgid "No associated permission"
|
||||||
msgstr "Pas de permission associée"
|
msgstr "Pas de permission associée"
|
||||||
|
|
||||||
|
#: apps/permission/templates/permission/scopes.html:8
|
||||||
|
msgid "Available scopes"
|
||||||
|
msgstr "Scopes disponibles"
|
||||||
|
|
||||||
|
#: apps/permission/templates/permission/scopes.html:42
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_list.html:24
|
||||||
|
msgid "No applications defined"
|
||||||
|
msgstr "Pas d'application définie"
|
||||||
|
|
||||||
|
#: apps/permission/templates/permission/scopes.html:43
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_list.html:25
|
||||||
|
msgid "Click here"
|
||||||
|
msgstr "Cliquez ici"
|
||||||
|
|
||||||
|
#: apps/permission/templates/permission/scopes.html:43
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_list.html:25
|
||||||
|
msgid "if you want to register a new one"
|
||||||
|
msgstr "si vous voulez en enregistrer une nouvelle"
|
||||||
|
|
||||||
#: apps/permission/views.py:72
|
#: apps/permission/views.py:72
|
||||||
#, python-brace-format
|
#, python-brace-format
|
||||||
msgid ""
|
msgid ""
|
||||||
@ -1977,35 +2032,35 @@ msgstr "L'équipe de la Note Kfet."
|
|||||||
msgid "Register new user"
|
msgid "Register new user"
|
||||||
msgstr "Enregistrer un nouvel utilisateur"
|
msgstr "Enregistrer un nouvel utilisateur"
|
||||||
|
|
||||||
#: apps/registration/views.py:93
|
#: apps/registration/views.py:95
|
||||||
msgid "Email validation"
|
msgid "Email validation"
|
||||||
msgstr "Validation de l'adresse mail"
|
msgstr "Validation de l'adresse mail"
|
||||||
|
|
||||||
#: apps/registration/views.py:95
|
#: apps/registration/views.py:97
|
||||||
msgid "Validate email"
|
msgid "Validate email"
|
||||||
msgstr "Valider l'adresse e-mail"
|
msgstr "Valider l'adresse e-mail"
|
||||||
|
|
||||||
#: apps/registration/views.py:137
|
#: apps/registration/views.py:141
|
||||||
msgid "Email validation unsuccessful"
|
msgid "Email validation unsuccessful"
|
||||||
msgstr "La validation de l'adresse mail a échoué"
|
msgstr "La validation de l'adresse mail a échoué"
|
||||||
|
|
||||||
#: apps/registration/views.py:148
|
#: apps/registration/views.py:152
|
||||||
msgid "Email validation email sent"
|
msgid "Email validation email sent"
|
||||||
msgstr "L'email de vérification de l'adresse email a bien été envoyé"
|
msgstr "L'email de vérification de l'adresse email a bien été envoyé"
|
||||||
|
|
||||||
#: apps/registration/views.py:156
|
#: apps/registration/views.py:160
|
||||||
msgid "Resend email validation link"
|
msgid "Resend email validation link"
|
||||||
msgstr "Renvoyer le lien de validation"
|
msgstr "Renvoyer le lien de validation"
|
||||||
|
|
||||||
#: apps/registration/views.py:174
|
#: apps/registration/views.py:178
|
||||||
msgid "Pre-registered users list"
|
msgid "Pre-registered users list"
|
||||||
msgstr "Liste des utilisateurs en attente d'inscription"
|
msgstr "Liste des utilisateurs en attente d'inscription"
|
||||||
|
|
||||||
#: apps/registration/views.py:198
|
#: apps/registration/views.py:202
|
||||||
msgid "Unregistered users"
|
msgid "Unregistered users"
|
||||||
msgstr "Utilisateurs en attente d'inscription"
|
msgstr "Utilisateurs en attente d'inscription"
|
||||||
|
|
||||||
#: apps/registration/views.py:211
|
#: apps/registration/views.py:215
|
||||||
msgid "Registration detail"
|
msgid "Registration detail"
|
||||||
msgstr "Détails de l'inscription"
|
msgstr "Détails de l'inscription"
|
||||||
|
|
||||||
@ -2225,7 +2280,7 @@ msgstr "Cette facture est verrouillée et ne peut pas être supprimée."
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure you want to delete this invoice? This action can't be undone."
|
"Are you sure you want to delete this invoice? This action can't be undone."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Êtes-vous sûr de vouloir supprimer cette facture ? Cette action ne pourra "
|
"Êtes-vous sûr⋅e de vouloir supprimer cette facture ? Cette action ne pourra "
|
||||||
"pas être annulée."
|
"pas être annulée."
|
||||||
|
|
||||||
#: apps/treasury/templates/treasury/invoice_confirm_delete.html:28
|
#: apps/treasury/templates/treasury/invoice_confirm_delete.html:28
|
||||||
@ -2863,7 +2918,7 @@ msgid ""
|
|||||||
"Are you sure you want to delete the registration of %(user)s for the WEI "
|
"Are you sure you want to delete the registration of %(user)s for the WEI "
|
||||||
"%(wei_name)s? This action can't be undone."
|
"%(wei_name)s? This action can't be undone."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Êtes-vous sûr de vouloir supprimer l'inscription de %(user)s pour le WEI "
|
"Êtes-vous sûr⋅e de vouloir supprimer l'inscription de %(user)s pour le WEI "
|
||||||
"%(wei_name)s ? Cette action ne pourra pas être annulée."
|
"%(wei_name)s ? Cette action ne pourra pas être annulée."
|
||||||
|
|
||||||
#: apps/wei/templates/wei/weiregistration_list.html:19
|
#: apps/wei/templates/wei/weiregistration_list.html:19
|
||||||
@ -3127,6 +3182,113 @@ msgstr "Chercher par un attribut tel que le nom …"
|
|||||||
msgid "There is no results."
|
msgid "There is no results."
|
||||||
msgstr "Il n'y a pas de résultat."
|
msgstr "Il n'y a pas de résultat."
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:8
|
||||||
|
msgid "Are you sure to delete the application"
|
||||||
|
msgstr "Êtes-vous sûr⋅e de vouloir supprimer l'application"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_confirm_delete.html:17
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorize.html:28
|
||||||
|
msgid "Cancel"
|
||||||
|
msgstr "Annuler"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:11
|
||||||
|
msgid "Client id"
|
||||||
|
msgstr "ID client"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:14
|
||||||
|
msgid "Client secret"
|
||||||
|
msgstr "Secret client"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:17
|
||||||
|
msgid "Client type"
|
||||||
|
msgstr "Type de client"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:20
|
||||||
|
msgid "Authorization Grant Type"
|
||||||
|
msgstr "Type d'autorisation"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:23
|
||||||
|
msgid "Redirect Uris"
|
||||||
|
msgstr "URIs de redirection"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:29
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"You can go <a href=\"%(scopes_url)s\">here</a> to generate authorization "
|
||||||
|
"link templates and convert permissions to scope numbers with the permissions "
|
||||||
|
"that you want to grant for your application."
|
||||||
|
msgstr ""
|
||||||
|
"Vous pouvez aller <a href=\"%(scopes_url)s\">ici</a> pour générer des modèles "
|
||||||
|
"de liens d'autorisation et convertir des permissions en identifiants de "
|
||||||
|
"scopes avec les permissions que vous souhaitez attribuer à votre application."
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_detail.html:37
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_form.html:23
|
||||||
|
msgid "Go Back"
|
||||||
|
msgstr "Retour en arrière"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_form.html:12
|
||||||
|
msgid "Edit application"
|
||||||
|
msgstr "Modifier l'application"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_list.html:7
|
||||||
|
msgid "Your applications"
|
||||||
|
msgstr "Vos applications"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_list.html:11
|
||||||
|
msgid ""
|
||||||
|
"You can find on this page the list of the applications that you already "
|
||||||
|
"registered."
|
||||||
|
msgstr ""
|
||||||
|
"Vous pouvez trouver sur cette page la liste des applications que vous avez "
|
||||||
|
"déjà enregistrées."
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_list.html:30
|
||||||
|
msgid "New Application"
|
||||||
|
msgstr "Nouvelle application"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_list.html:31
|
||||||
|
msgid "Authorized Tokens"
|
||||||
|
msgstr "Jetons autorisés"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/application_registration_form.html:5
|
||||||
|
msgid "Register a new application"
|
||||||
|
msgstr "Enregistrer une nouvelle application"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorize.html:9
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorize.html:29
|
||||||
|
msgid "Authorize"
|
||||||
|
msgstr "Autoriser"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorize.html:14
|
||||||
|
msgid "Application requires following permissions:"
|
||||||
|
msgstr "L'application requiert les permissions suivantes :"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorize.html:36
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorized-oob.html:15
|
||||||
|
msgid "Error:"
|
||||||
|
msgstr "Erreur :"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorized-oob.html:13
|
||||||
|
msgid "Success"
|
||||||
|
msgstr "Succès"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorized-oob.html:21
|
||||||
|
msgid "Please return to your application and enter this code:"
|
||||||
|
msgstr "Merci de retourner à votre application et entrez ce code :"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorized-token-delete.html:9
|
||||||
|
msgid "Are you sure you want to delete this token?"
|
||||||
|
msgstr "Êtes-vous sûr⋅e de vouloir supprimer ce jeton ?"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorized-tokens.html:7
|
||||||
|
msgid "Tokens"
|
||||||
|
msgstr "Jetons"
|
||||||
|
|
||||||
|
#: note_kfet/templates/oauth2_provider/authorized-tokens.html:22
|
||||||
|
msgid "There are no authorized tokens yet."
|
||||||
|
msgstr "Il n'y a pas encore de jeton autorisé."
|
||||||
|
|
||||||
#: note_kfet/templates/registration/logged_out.html:13
|
#: note_kfet/templates/registration/logged_out.html:13
|
||||||
msgid "Thanks for spending some quality time with the Web site today."
|
msgid "Thanks for spending some quality time with the Web site today."
|
||||||
msgstr "Merci d'avoir utilisé la Note Kfet."
|
msgstr "Merci d'avoir utilisé la Note Kfet."
|
||||||
@ -3168,7 +3330,7 @@ msgid ""
|
|||||||
"password twice so we can verify you typed it in correctly."
|
"password twice so we can verify you typed it in correctly."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Veuillez entrer votre ancien mot de passe pour des raisons de sécurité, puis "
|
"Veuillez entrer votre ancien mot de passe pour des raisons de sécurité, puis "
|
||||||
"renseigner votre nouveau mot de passe à deux reprises, pour être sûr de "
|
"renseigner votre nouveau mot de passe à deux reprises, pour être sûr⋅e de "
|
||||||
"l'avoir tapé correctement."
|
"l'avoir tapé correctement."
|
||||||
|
|
||||||
#: note_kfet/templates/registration/password_change_form.html:16
|
#: note_kfet/templates/registration/password_change_form.html:16
|
||||||
|
@ -6,7 +6,6 @@ from django.contrib.admin import AdminSite
|
|||||||
from django.contrib.sites.admin import Site, SiteAdmin
|
from django.contrib.sites.admin import Site, SiteAdmin
|
||||||
|
|
||||||
from member.views import CustomLoginView
|
from member.views import CustomLoginView
|
||||||
from .middlewares import get_current_session
|
|
||||||
|
|
||||||
|
|
||||||
class StrongAdminSite(AdminSite):
|
class StrongAdminSite(AdminSite):
|
||||||
@ -14,8 +13,7 @@ class StrongAdminSite(AdminSite):
|
|||||||
"""
|
"""
|
||||||
Authorize only staff that have the correct permission mask
|
Authorize only staff that have the correct permission mask
|
||||||
"""
|
"""
|
||||||
session = get_current_session()
|
return request.user.is_active and request.user.is_staff and request.session.get("permission_mask", -1) >= 42
|
||||||
return request.user.is_active and request.user.is_staff and session.get("permission_mask", -1) >= 42
|
|
||||||
|
|
||||||
def login(self, request, extra_context=None):
|
def login(self, request, extra_context=None):
|
||||||
return CustomLoginView.as_view()(request)
|
return CustomLoginView.as_view()(request)
|
||||||
|
@ -1,43 +1,23 @@
|
|||||||
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
# Copyright (C) 2018-2021 by BDE ENS Paris-Saclay
|
||||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.contrib.auth import login
|
|
||||||
from django.contrib.auth.models import AnonymousUser, User
|
|
||||||
from django.contrib.sessions.backends.db import SessionStore
|
|
||||||
|
|
||||||
from threading import local
|
from threading import local
|
||||||
|
|
||||||
USER_ATTR_NAME = getattr(settings, 'LOCAL_USER_ATTR_NAME', '_current_user')
|
from django.conf import settings
|
||||||
SESSION_ATTR_NAME = getattr(settings, 'LOCAL_SESSION_ATTR_NAME', '_current_session')
|
from django.contrib.auth import login
|
||||||
IP_ATTR_NAME = getattr(settings, 'LOCAL_IP_ATTR_NAME', '_current_ip')
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
|
REQUEST_ATTR_NAME = getattr(settings, 'LOCAL_REQUEST_ATTR_NAME', '_current_request')
|
||||||
|
|
||||||
_thread_locals = local()
|
_thread_locals = local()
|
||||||
|
|
||||||
|
|
||||||
def _set_current_user_and_ip(user=None, session=None, ip=None):
|
def _set_current_request(request=None):
|
||||||
setattr(_thread_locals, USER_ATTR_NAME, user)
|
setattr(_thread_locals, REQUEST_ATTR_NAME, request)
|
||||||
setattr(_thread_locals, SESSION_ATTR_NAME, session)
|
|
||||||
setattr(_thread_locals, IP_ATTR_NAME, ip)
|
|
||||||
|
|
||||||
|
|
||||||
def get_current_user() -> User:
|
def get_current_request():
|
||||||
return getattr(_thread_locals, USER_ATTR_NAME, None)
|
return getattr(_thread_locals, REQUEST_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):
|
class SessionMiddleware(object):
|
||||||
@ -49,8 +29,6 @@ class SessionMiddleware(object):
|
|||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request):
|
||||||
user = request.user
|
|
||||||
|
|
||||||
# If we authenticate through a token to connect to the API, then we query the good user
|
# If we authenticate through a token to connect to the API, then we query the good user
|
||||||
if 'HTTP_AUTHORIZATION' in request.META and request.path.startswith("/api"):
|
if 'HTTP_AUTHORIZATION' in request.META and request.path.startswith("/api"):
|
||||||
token = request.META.get('HTTP_AUTHORIZATION')
|
token = request.META.get('HTTP_AUTHORIZATION')
|
||||||
@ -60,20 +38,14 @@ class SessionMiddleware(object):
|
|||||||
if Token.objects.filter(key=token).exists():
|
if Token.objects.filter(key=token).exists():
|
||||||
token_obj = Token.objects.get(key=token)
|
token_obj = Token.objects.get(key=token)
|
||||||
user = token_obj.user
|
user = token_obj.user
|
||||||
|
request.user = user
|
||||||
session = request.session
|
session = request.session
|
||||||
session["permission_mask"] = 42
|
session["permission_mask"] = 42
|
||||||
session.save()
|
session.save()
|
||||||
|
|
||||||
if 'HTTP_X_REAL_IP' in request.META:
|
_set_current_request(request)
|
||||||
ip = request.META.get('HTTP_X_REAL_IP')
|
|
||||||
elif 'HTTP_X_FORWARDED_FOR' in request.META:
|
|
||||||
ip = request.META.get('HTTP_X_FORWARDED_FOR').split(', ')[0]
|
|
||||||
else:
|
|
||||||
ip = request.META.get('REMOTE_ADDR')
|
|
||||||
|
|
||||||
_set_current_user_and_ip(user, request.session, ip)
|
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
_set_current_user_and_ip(None, None, None)
|
_set_current_request(None)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
@ -245,6 +245,11 @@ REST_FRAMEWORK = {
|
|||||||
'PAGE_SIZE': 20,
|
'PAGE_SIZE': 20,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# OAuth2 Provider
|
||||||
|
OAUTH2_PROVIDER = {
|
||||||
|
'SCOPES_BACKEND_CLASS': 'permission.scopes.PermissionScopes',
|
||||||
|
}
|
||||||
|
|
||||||
# Take control on how widget templates are sourced
|
# Take control on how widget templates are sourced
|
||||||
# See https://docs.djangoproject.com/en/2.2/ref/forms/renderers/#templatessetting
|
# See https://docs.djangoproject.com/en/2.2/ref/forms/renderers/#templatessetting
|
||||||
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
|
FORM_RENDERER = 'django.forms.renderers.TemplatesSetting'
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "Are you sure to delete the application" %} {{ application.name }}?</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
|
||||||
|
<form method="post" action="{% url 'oauth2_provider:delete' application.pk %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<a class="btn btn-secondary btn-large" href="{% url "oauth2_provider:list" %}">{% trans "Cancel" %}</a>
|
||||||
|
<input type="submit" class="btn btn-large btn-danger" name="allow" value="{% trans "Delete" %}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
42
note_kfet/templates/oauth2_provider/application_detail.html
Normal file
42
note_kfet/templates/oauth2_provider/application_detail.html
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{{ application.name }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<dl class="row">
|
||||||
|
<dt class="col-xl-6">{% trans "Client id" %}</dt>
|
||||||
|
<dd class="col-xl-6"><input class="form-control" type="text" value="{{ application.client_id }}" readonly></dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans "Client secret" %}</dt>
|
||||||
|
<dd class="col-xl-6"><input class="form-control" type="text" value="****************************************************************" readonly></dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans "Client type" %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ application.client_type }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans "Authorization Grant Type" %}</dt>
|
||||||
|
<dd class="col-xl-6">{{ application.authorization_grant_type }}</dd>
|
||||||
|
|
||||||
|
<dt class="col-xl-6">{% trans "Redirect Uris" %}</dt>
|
||||||
|
<dd class="col-xl-6"><textarea class="form-control" readonly>{{ application.redirect_uris }}</textarea></dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% url 'permission:scopes' as scopes_url %}
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can go <a href="{{ scopes_url }}">here</a> to generate authorization link templates and convert
|
||||||
|
permissions to scope numbers with the permissions that you want to grant for your application.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<a class="btn btn-secondary" href="{% url "oauth2_provider:list" %}">{% trans "Go Back" %}</a>
|
||||||
|
<a class="btn btn-primary" href="{% url "oauth2_provider:update" application.id %}">{% trans "Edit" %}</a>
|
||||||
|
<a class="btn btn-danger" href="{% url "oauth2_provider:delete" application.id %}">{% trans "Delete" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
30
note_kfet/templates/oauth2_provider/application_form.html
Normal file
30
note_kfet/templates/oauth2_provider/application_form.html
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<form class="form-horizontal" method="post" action="{% block app-form-action-url %}{% url 'oauth2_provider:update' application.id %}{% endblock app-form-action-url %}">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3 class="block-center-heading">
|
||||||
|
{% block app-form-title %}
|
||||||
|
{% trans "Edit application" %} {{ application.name }}
|
||||||
|
{% endblock app-form-title %}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<a class="btn btn-secondary" href="{% block app-form-back-url %}{% url "oauth2_provider:detail" application.id %}{% endblock app-form-back-url %}">
|
||||||
|
{% trans "Go Back" %}
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
34
note_kfet/templates/oauth2_provider/application_list.html
Normal file
34
note_kfet/templates/oauth2_provider/application_list.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "Your applications" %}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
{% blocktrans trimmed %}
|
||||||
|
You can find on this page the list of the applications that you already registered.
|
||||||
|
{% endblocktrans %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if applications %}
|
||||||
|
<ul>
|
||||||
|
{% for application in applications %}
|
||||||
|
<li><a href="{{ application.get_absolute_url }}">{{ application.name }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% trans "No applications defined" %}.
|
||||||
|
<a href="{% url 'oauth2_provider:register' %}">{% trans "Click here" %}</a> {% trans "if you want to register a new one" %}.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<a class="btn btn-success" href="{% url "oauth2_provider:register" %}">{% trans "New Application" %}</a>
|
||||||
|
<a class="btn btn-secondary" href="{% url "oauth2_provider:authorized-token-list" %}">{% trans "Authorized Tokens" %}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
@ -0,0 +1,9 @@
|
|||||||
|
{% extends "oauth2_provider/application_form.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block app-form-title %}{% trans "Register a new application" %}{% endblock app-form-title %}
|
||||||
|
|
||||||
|
{% block app-form-action-url %}{% url 'oauth2_provider:register' %}{% endblock app-form-action-url %}
|
||||||
|
|
||||||
|
{% block app-form-back-url %}{% url "oauth2_provider:list" %}"{% endblock app-form-back-url %}
|
49
note_kfet/templates/oauth2_provider/authorize.html
Normal file
49
note_kfet/templates/oauth2_provider/authorize.html
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% load crispy_forms_filters %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header text-center">
|
||||||
|
<h3>{% trans "Authorize" %} {{ application.name }} ?</h3>
|
||||||
|
</div>
|
||||||
|
{% if not error %}
|
||||||
|
<form id="authorizationForm" method="post">
|
||||||
|
<div class="card-body">
|
||||||
|
<p>{% trans "Application requires following permissions:" %}</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{% for scope in scopes_descriptions %}
|
||||||
|
<li>{{ scope }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form|crispy }}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<div class="control-group">
|
||||||
|
<div class="controls">
|
||||||
|
<input type="submit" class="btn btn-large btn-danger" value="{% trans "Cancel" %}"/>
|
||||||
|
<input type="submit" class="btn btn-large btn-primary" name="allow" value="{% trans "Authorize" %}"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<div class="card-body">
|
||||||
|
<h2>{% trans "Error:" %} {{ error.error }}</h2>
|
||||||
|
<p>{{ error.description }}</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrajavascript %}
|
||||||
|
<script>
|
||||||
|
{# Small hack to have the remove the allow checkbox and replace it with the button #}
|
||||||
|
{# Django oauth toolkit does simply not render the wdiget since it is not hidden, and create directly the button #}
|
||||||
|
document.getElementById('div_id_allow').parentElement.remove()
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
29
note_kfet/templates/oauth2_provider/authorized-oob.html
Normal file
29
note_kfet/templates/oauth2_provider/authorized-oob.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Success code={{code}}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% if not error %}
|
||||||
|
{% trans "Success" %}
|
||||||
|
{% else %}
|
||||||
|
{% trans "Error:" %} {{ error.error }}
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
{% if not error %}
|
||||||
|
<p>{% trans "Please return to your application and enter this code:" %}</p>
|
||||||
|
|
||||||
|
<p><code>{{ code }}</code></p>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ error.description }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -0,0 +1,16 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<form action="" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% trans "Are you sure you want to delete this token?" %}
|
||||||
|
</h3>
|
||||||
|
<div class="card-footer text-center">
|
||||||
|
<input type="submit" value="{% trans "Delete" %}" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
27
note_kfet/templates/oauth2_provider/authorized-tokens.html
Normal file
27
note_kfet/templates/oauth2_provider/authorized-tokens.html
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% load i18n %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card">
|
||||||
|
<h3 class="card-header text-center">
|
||||||
|
{% trans "Tokens" %}
|
||||||
|
</h3>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul>
|
||||||
|
{% for authorized_token in authorized_tokens %}
|
||||||
|
<li>
|
||||||
|
{{ authorized_token.application }}
|
||||||
|
(<a href="{% url 'oauth2_provider:authorized-token-delete' authorized_token.pk %}">revoke</a>)
|
||||||
|
</li>
|
||||||
|
<ul>
|
||||||
|
{% for scope_name, scope_description in authorized_token.scopes.items %}
|
||||||
|
<li>{{ scope_name }}: {{ scope_description }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% empty %}
|
||||||
|
<li>{% trans "There are no authorized tokens yet." %}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -19,11 +19,11 @@ class IndexView(LoginRequiredMixin, RedirectView):
|
|||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
|
||||||
# The account note will have the consumption page as default page
|
# The account note will have the consumption page as default page
|
||||||
if not PermissionBackend.check_perm(user, "auth.view_user", user):
|
if not PermissionBackend.check_perm(self.request, "auth.view_user", user):
|
||||||
return reverse("note:consos")
|
return reverse("note:consos")
|
||||||
|
|
||||||
# People that can see the alias BDE are Kfet members
|
# People that can see the alias BDE are Kfet members
|
||||||
if PermissionBackend.check_perm(user, "alias.view_alias", Alias.objects.get(name="BDE")):
|
if PermissionBackend.check_perm(self.request, "alias.view_alias", Alias.objects.get(name="BDE")):
|
||||||
return reverse("note:transfer")
|
return reverse("note:transfer")
|
||||||
|
|
||||||
# Non-Kfet members will don't see the transfer page, but their profile page
|
# Non-Kfet members will don't see the transfer page, but their profile page
|
||||||
|
Loading…
Reference in New Issue
Block a user