mirror of https://gitlab.crans.org/bde/nk20
Merge branch 'api' into 'master'
API & autocomplétion See merge request bde/nk20!8
This commit is contained in:
commit
0d7db68372
|
@ -0,0 +1,35 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from ..models import ActivityType, Activity, Guest
|
||||
from rest_framework import serializers
|
||||
|
||||
class ActivityTypeSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Activity types.
|
||||
The djangorestframework plugin will analyse the model `ActivityType` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = ActivityType
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ActivitySerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Activities.
|
||||
The djangorestframework plugin will analyse the model `Activity` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Activity
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class GuestSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Guests.
|
||||
The djangorestframework plugin will analyse the model `Guest` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Guest
|
||||
fields = '__all__'
|
|
@ -0,0 +1,14 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ActivityTypeViewSet, ActivityViewSet, GuestViewSet
|
||||
|
||||
|
||||
def register_activity_urls(router, path):
|
||||
"""
|
||||
Configure router for Activity REST API.
|
||||
"""
|
||||
router.register(path + '/activity', ActivityViewSet)
|
||||
router.register(path + '/type', ActivityTypeViewSet)
|
||||
router.register(path + '/guest', GuestViewSet)
|
|
@ -0,0 +1,38 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
from ..models import ActivityType, Activity, Guest
|
||||
from .serializers import ActivityTypeSerializer, ActivitySerializer, GuestSerializer
|
||||
|
||||
|
||||
class ActivityTypeViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `ActivityType` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/activity/type/
|
||||
"""
|
||||
queryset = ActivityType.objects.all()
|
||||
serializer_class = ActivityTypeSerializer
|
||||
|
||||
|
||||
class ActivityViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Activity` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/activity/activity/
|
||||
"""
|
||||
queryset = Activity.objects.all()
|
||||
serializer_class = ActivitySerializer
|
||||
|
||||
|
||||
class GuestViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Guest` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/activity/guest/
|
||||
"""
|
||||
queryset = Guest.objects.all()
|
||||
serializer_class = GuestSerializer
|
|
@ -0,0 +1,51 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf.urls import url, include
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import routers, serializers, viewsets
|
||||
from rest_framework.authtoken import views as token_views
|
||||
|
||||
from activity.api.urls import register_activity_urls
|
||||
from member.api.urls import register_members_urls
|
||||
from note.api.urls import register_note_urls
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Users.
|
||||
The djangorestframework plugin will analyse the model `User` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
exclude = ('password', 'groups', 'user_permissions',)
|
||||
|
||||
|
||||
class UserViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `User` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/users/
|
||||
"""
|
||||
queryset = User.objects.all()
|
||||
serializer_class = UserSerializer
|
||||
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf.
|
||||
# Register each app API router and user viewset
|
||||
router = routers.DefaultRouter()
|
||||
router.register('user', UserViewSet)
|
||||
register_members_urls(router, 'members')
|
||||
register_activity_urls(router, 'activity')
|
||||
register_note_urls(router, 'note')
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
# Wire up our API using automatic URL routing.
|
||||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url('^', include(router.urls)),
|
||||
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
|
||||
]
|
|
@ -0,0 +1,46 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from ..models import Profile, Club, Role, Membership
|
||||
from rest_framework import serializers
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Profiles.
|
||||
The djangorestframework plugin will analyse the model `Profile` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Profile
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class ClubSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Clubs.
|
||||
The djangorestframework plugin will analyse the model `Club` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Club
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class RoleSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Roles.
|
||||
The djangorestframework plugin will analyse the model `Role` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Role
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MembershipSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Memberships.
|
||||
The djangorestframework plugin will analyse the model `Memberships` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = '__all__'
|
|
@ -0,0 +1,15 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ProfileViewSet, ClubViewSet, RoleViewSet, MembershipViewSet
|
||||
|
||||
|
||||
def register_members_urls(router, path):
|
||||
"""
|
||||
Configure router for Member REST API.
|
||||
"""
|
||||
router.register(path + '/profile', ProfileViewSet)
|
||||
router.register(path + '/club', ClubViewSet)
|
||||
router.register(path + '/role', RoleViewSet)
|
||||
router.register(path + '/membership', MembershipViewSet)
|
|
@ -0,0 +1,48 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import viewsets
|
||||
|
||||
from ..models import Profile, Club, Role, Membership
|
||||
from .serializers import ProfileSerializer, ClubSerializer, RoleSerializer, MembershipSerializer
|
||||
|
||||
|
||||
class ProfileViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Profile` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/profile/
|
||||
"""
|
||||
queryset = Profile.objects.all()
|
||||
serializer_class = ProfileSerializer
|
||||
|
||||
|
||||
class ClubViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Club` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/club/
|
||||
"""
|
||||
queryset = Club.objects.all()
|
||||
serializer_class = ClubSerializer
|
||||
|
||||
|
||||
class RoleViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Role` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/role/
|
||||
"""
|
||||
queryset = Role.objects.all()
|
||||
serializer_class = RoleSerializer
|
||||
|
||||
|
||||
class MembershipViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Membership` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/members/membership/
|
||||
"""
|
||||
queryset = Membership.objects.all()
|
||||
serializer_class = MembershipSerializer
|
|
@ -1,7 +1,7 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from dal import autocomplete
|
||||
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
||||
from django.contrib.auth.models import User
|
||||
from django import forms
|
||||
|
@ -44,6 +44,17 @@ class MembershipForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = Membership
|
||||
fields = ('user','roles','date_start')
|
||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les noms d'utilisateur valides
|
||||
widgets = {
|
||||
'user': autocomplete.ModelSelect2(url='member:user_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Nom ...',
|
||||
'data-minimum-input-length': 1,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
MemberFormSet = forms.modelformset_factory(Membership,
|
||||
form=MembershipForm,
|
||||
|
|
|
@ -18,4 +18,8 @@ urlpatterns = [
|
|||
path('user/',views.UserListView.as_view(),name="user_list"),
|
||||
path('user/<int:pk>',views.UserDetailView.as_view(),name="user_detail"),
|
||||
path('user/<int:pk>/update',views.UserUpdateView.as_view(),name="user_update_profile"),
|
||||
path('manage-auth-token/', views.ManageAuthTokens.as_view(), name='auth_token'),
|
||||
|
||||
# API for the user autocompleter
|
||||
path('user/user-autocomplete',views.UserAutocomplete.as_view(),name="user_autocomplete"),
|
||||
]
|
||||
|
|
|
@ -2,19 +2,19 @@
|
|||
|
||||
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from dal import autocomplete
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.shortcuts import redirect
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, ListView, DetailView, UpdateView
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.views.generic import CreateView, ListView, DetailView, UpdateView, RedirectView, TemplateView
|
||||
from django.contrib.auth.models import User
|
||||
from django.urls import reverse_lazy
|
||||
from django.db.models import Q
|
||||
|
||||
from django_tables2.views import SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
|
||||
from note.models import Alias, Note, NoteUser
|
||||
from .models import Profile, Club, Membership
|
||||
from .forms import SignUpForm, ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper
|
||||
from .tables import ClubTable,UserTable
|
||||
|
@ -57,17 +57,43 @@ class UserUpdateView(LoginRequiredMixin,UpdateView):
|
|||
second_form = ProfileForm
|
||||
def get_context_data(self,**kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["profile_form"] = self.second_form(instance=context['user'].profile)
|
||||
context['user_modified'] = context['user']
|
||||
context['user'] = self.request.user
|
||||
context["profile_form"] = self.second_form(instance=context['user_modified'].profile)
|
||||
|
||||
return context
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
form = super().get_form(form_class)
|
||||
if 'username' not in form.data:
|
||||
return form
|
||||
|
||||
new_username = form.data['username']
|
||||
|
||||
# Si l'utilisateur cherche à modifier son pseudo, le nouveau pseudo ne doit pas être proche d'un alias existant
|
||||
note = NoteUser.objects.filter(alias__normalized_name=Alias.normalize(new_username))
|
||||
if note.exists() and note.get().user != self.request.user:
|
||||
form.add_error('username', _("An alias with a similar name already exists."))
|
||||
|
||||
return form
|
||||
|
||||
|
||||
def form_valid(self, form):
|
||||
profile_form = ProfileForm(data=self.request.POST,instance=self.request.user.profile)
|
||||
if form.is_valid() and profile_form.is_valid():
|
||||
user = form.save()
|
||||
new_username = form.data['username']
|
||||
alias = Alias.objects.filter(name=new_username)
|
||||
# Si le nouveau pseudo n'est pas un de nos alias, on supprime éventuellement un alias similaire pour le remplacer
|
||||
if not alias.exists():
|
||||
similar = Alias.objects.filter(normalized_name=Alias.normalize(new_username))
|
||||
if similar.exists():
|
||||
similar.delete()
|
||||
|
||||
user = form.save(commit=False)
|
||||
profile = profile_form.save(commit=False)
|
||||
profile.user = user
|
||||
profile.save()
|
||||
user.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self, **kwargs):
|
||||
|
@ -115,6 +141,46 @@ class UserListView(LoginRequiredMixin,SingleTableView):
|
|||
return context
|
||||
|
||||
|
||||
class ManageAuthTokens(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Affiche le jeton d'authentification, et permet de le regénérer
|
||||
"""
|
||||
model = Token
|
||||
template_name = "member/manage_auth_tokens.html"
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if 'regenerate' in request.GET and Token.objects.filter(user=request.user).exists():
|
||||
Token.objects.get(user=self.request.user).delete()
|
||||
return redirect(reverse_lazy('member:auth_token') + "?show", permanent=True)
|
||||
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['token'] = Token.objects.get_or_create(user=self.request.user)[0]
|
||||
return context
|
||||
|
||||
class UserAutocomplete(autocomplete.Select2QuerySetView):
|
||||
"""
|
||||
Auto complete users by usernames
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Quand une personne cherche un utilisateur par pseudo, une requête est envoyée sur l'API dédiée à l'auto-complétion.
|
||||
Cette fonction récupère la requête, et renvoie la liste filtrée des utilisateurs par pseudos.
|
||||
"""
|
||||
# Un utilisateur non connecté n'a accès à aucune information
|
||||
if not self.request.user.is_authenticated:
|
||||
return User.objects.none()
|
||||
|
||||
qs = User.objects.all()
|
||||
|
||||
if self.q:
|
||||
qs = qs.filter(username__regex=self.q)
|
||||
|
||||
return qs
|
||||
|
||||
###################################
|
||||
############## CLUB ###############
|
||||
###################################
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
|
||||
from rest_framework import serializers
|
||||
from rest_polymorphic.serializers import PolymorphicSerializer
|
||||
|
||||
|
||||
class NoteSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Notes.
|
||||
The djangorestframework plugin will analyse the model `Note` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = '__all__'
|
||||
extra_kwargs = {
|
||||
'url': {'view_name': 'project-detail', 'lookup_field': 'pk'},
|
||||
}
|
||||
|
||||
|
||||
class NoteClubSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Club's notes.
|
||||
The djangorestframework plugin will analyse the model `NoteClub` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = NoteClub
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class NoteSpecialSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for special notes.
|
||||
The djangorestframework plugin will analyse the model `NoteSpecial` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = NoteSpecial
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class NoteUserSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for User's notes.
|
||||
The djangorestframework plugin will analyse the model `NoteUser` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = NoteUser
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class AliasSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Aliases.
|
||||
The djangorestframework plugin will analyse the model `Alias` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Alias
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class NotePolymorphicSerializer(PolymorphicSerializer):
|
||||
model_serializer_mapping = {
|
||||
Note: NoteSerializer,
|
||||
NoteUser: NoteUserSerializer,
|
||||
NoteClub: NoteClubSerializer,
|
||||
NoteSpecial: NoteSpecialSerializer
|
||||
}
|
||||
|
||||
class TransactionTemplateSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Transaction templates.
|
||||
The djangorestframework plugin will analyse the model `TransactionTemplate` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = TransactionTemplate
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class TransactionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Transactions.
|
||||
The djangorestframework plugin will analyse the model `Transaction` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class MembershipTransactionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Membership transactions.
|
||||
The djangorestframework plugin will analyse the model `MembershipTransaction` and parse all fields in the API.
|
||||
"""
|
||||
class Meta:
|
||||
model = MembershipTransaction
|
||||
fields = '__all__'
|
|
@ -0,0 +1,18 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import NotePolymorphicViewSet, AliasViewSet, \
|
||||
TransactionViewSet, TransactionTemplateViewSet, MembershipTransactionViewSet
|
||||
|
||||
|
||||
def register_note_urls(router, path):
|
||||
"""
|
||||
Configure router for Note REST API.
|
||||
"""
|
||||
router.register(path + '/note', NotePolymorphicViewSet)
|
||||
router.register(path + '/alias', AliasViewSet)
|
||||
|
||||
router.register(path + '/transaction/transaction', TransactionViewSet)
|
||||
router.register(path + '/transaction/template', TransactionTemplateViewSet)
|
||||
router.register(path + '/transaction/membership', MembershipTransactionViewSet)
|
|
@ -0,0 +1,155 @@
|
|||
# -*- mode: python; coding: utf-8 -*-
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.db.models import Q
|
||||
from rest_framework import viewsets
|
||||
|
||||
from ..models.notes import Note, NoteClub, NoteSpecial, NoteUser, Alias
|
||||
from ..models.transactions import TransactionTemplate, Transaction, MembershipTransaction
|
||||
from .serializers import NoteSerializer, NotePolymorphicSerializer, NoteClubSerializer, NoteSpecialSerializer, \
|
||||
NoteUserSerializer, AliasSerializer, \
|
||||
TransactionTemplateSerializer, TransactionSerializer, MembershipTransactionSerializer
|
||||
|
||||
|
||||
class NoteViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Note` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/note/
|
||||
"""
|
||||
queryset = Note.objects.all()
|
||||
serializer_class = NoteSerializer
|
||||
|
||||
|
||||
class NoteClubViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteClub` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/club/
|
||||
"""
|
||||
queryset = NoteClub.objects.all()
|
||||
serializer_class = NoteClubSerializer
|
||||
|
||||
|
||||
class NoteSpecialViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteSpecial` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/special/
|
||||
"""
|
||||
queryset = NoteSpecial.objects.all()
|
||||
serializer_class = NoteSpecialSerializer
|
||||
|
||||
|
||||
class NoteUserViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `NoteUser` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/user/
|
||||
"""
|
||||
queryset = NoteUser.objects.all()
|
||||
serializer_class = NoteUserSerializer
|
||||
|
||||
|
||||
class NotePolymorphicViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Note` objects (with polymorhism), serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/note/
|
||||
"""
|
||||
queryset = Note.objects.all()
|
||||
serializer_class = NotePolymorphicSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Parse query and apply filters.
|
||||
:return: The filtered set of requested notes
|
||||
"""
|
||||
queryset = Note.objects.all()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(Q(alias__name__regex=alias) | Q(alias__normalized_name__regex=alias.lower()))
|
||||
|
||||
note_type = self.request.query_params.get("type", None)
|
||||
if note_type:
|
||||
l = str(note_type).lower()
|
||||
if "user" in l:
|
||||
queryset = queryset.filter(polymorphic_ctype__model="noteuser")
|
||||
elif "club" in l:
|
||||
queryset = queryset.filter(polymorphic_ctype__model="noteclub")
|
||||
elif "special" in l:
|
||||
queryset = queryset.filter(polymorphic_ctype__model="notespecial")
|
||||
else:
|
||||
queryset = queryset.none()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class AliasViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Alias` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/aliases/
|
||||
"""
|
||||
queryset = Alias.objects.all()
|
||||
serializer_class = AliasSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Parse query and apply filters.
|
||||
:return: The filtered set of requested aliases
|
||||
"""
|
||||
|
||||
queryset = Alias.objects.all()
|
||||
|
||||
alias = self.request.query_params.get("alias", ".*")
|
||||
queryset = queryset.filter(Q(name__regex=alias) | Q(normalized_name__regex=alias.lower()))
|
||||
|
||||
note_id = self.request.query_params.get("note", None)
|
||||
if note_id:
|
||||
queryset = queryset.filter(id=note_id)
|
||||
|
||||
note_type = self.request.query_params.get("type", None)
|
||||
if note_type:
|
||||
l = str(note_type).lower()
|
||||
if "user" in l:
|
||||
queryset = queryset.filter(note__polymorphic_ctype__model="noteuser")
|
||||
elif "club" in l:
|
||||
queryset = queryset.filter(note__polymorphic_ctype__model="noteclub")
|
||||
elif "special" in l:
|
||||
queryset = queryset.filter(note__polymorphic_ctype__model="notespecial")
|
||||
else:
|
||||
queryset = queryset.none()
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class TransactionTemplateViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `TransactionTemplate` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/template/
|
||||
"""
|
||||
queryset = TransactionTemplate.objects.all()
|
||||
serializer_class = TransactionTemplateSerializer
|
||||
|
||||
|
||||
class TransactionViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Transaction` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/transaction/
|
||||
"""
|
||||
queryset = Transaction.objects.all()
|
||||
serializer_class = TransactionSerializer
|
||||
|
||||
|
||||
class MembershipTransactionViewSet(viewsets.ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `MembershipTransaction` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/note/transaction/membership/
|
||||
"""
|
||||
queryset = MembershipTransaction.objects.all()
|
||||
serializer_class = MembershipTransactionSerializer
|
|
@ -1,14 +1,54 @@
|
|||
#!/usr/bin/env python
|
||||
|
||||
from dal import autocomplete, forward
|
||||
from django import forms
|
||||
from .models import TransactionTemplate, Transaction
|
||||
from .models import Transaction, TransactionTemplate
|
||||
|
||||
class TransactionTemplateForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = TransactionTemplate
|
||||
fields ='__all__'
|
||||
|
||||
# Le champ de destination est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les aliases valides
|
||||
# Pour force le type d'une note, il faut rajouter le paramètre :
|
||||
# forward=(forward.Const('TYPE', 'note_type') où TYPE est dans {user, club, special}
|
||||
widgets = {
|
||||
'destination': autocomplete.ModelSelect2(url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class TransactionForm(forms.ModelForm):
|
||||
def save(self, commit=True):
|
||||
self.instance.transaction_type = 'transfert'
|
||||
|
||||
super().save(commit)
|
||||
|
||||
class Meta:
|
||||
model = Transaction
|
||||
fields = ('source', 'destination', 'reason', 'amount',)
|
||||
|
||||
# Voir ci-dessus
|
||||
widgets = {
|
||||
'source': autocomplete.ModelSelect2(url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},),
|
||||
'destination': autocomplete.ModelSelect2(url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
},),
|
||||
}
|
||||
|
||||
class ConsoForm(forms.ModelForm):
|
||||
|
||||
def save(self, commit=True):
|
||||
button: TransactionTemplate = TransactionTemplate.objects.filter(name=self.data['button']).get()
|
||||
self.instance.destination = button.destination
|
||||
|
@ -20,3 +60,14 @@ class ConsoForm(forms.ModelForm):
|
|||
class Meta:
|
||||
model = Transaction
|
||||
fields = ('source',)
|
||||
|
||||
# Le champ d'utilisateur est remplacé par un champ d'auto-complétion.
|
||||
# Quand des lettres sont tapées, une requête est envoyée sur l'API d'auto-complétion
|
||||
# et récupère les aliases de note valides
|
||||
widgets = {
|
||||
'source': autocomplete.ModelSelect2(url='note:note_autocomplete',
|
||||
attrs={
|
||||
'data-placeholder': 'Note ...',
|
||||
'data-minimum-input-length': 1,
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -85,7 +85,7 @@ class Note(PolymorphicModel):
|
|||
"""
|
||||
Verify alias (simulate save)
|
||||
"""
|
||||
aliases = Alias.objects.filter(name=str(self))
|
||||
aliases = Alias.objects.filter(normalized_name=Alias.normalize(str(self)))
|
||||
if aliases.exists():
|
||||
# Alias exists, so check if it is linked to this note
|
||||
if aliases.first().note != self:
|
||||
|
@ -233,3 +233,8 @@ class Alias(models.Model):
|
|||
'already exists.'))
|
||||
except Alias.DoesNotExist:
|
||||
pass
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
if self.name == str(self.note):
|
||||
raise ValidationError(_("You can't delete your main alias."))
|
||||
return super().delete(using, keep_parents)
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#!/usr/bin/env python
|
||||
import django_tables2 as tables
|
||||
from django.db.models import F
|
||||
|
||||
from .models.transactions import Transaction
|
||||
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
from django.urls import path
|
||||
|
||||
from . import views
|
||||
from .models import Note
|
||||
|
||||
app_name = 'note'
|
||||
urlpatterns = [
|
||||
|
@ -14,4 +15,7 @@ urlpatterns = [
|
|||
path('buttons/',views.TransactionTemplateListView.as_view(),name='template_list'),
|
||||
path('consos/<str:template_type>/',views.ConsoView.as_view(),name='consos'),
|
||||
path('consos/',views.ConsoView.as_view(),name='consos'),
|
||||
|
||||
# API for the note autocompleter
|
||||
path('note-autocomplete/', views.NoteAutocomplete.as_view(model=Note),name='note_autocomplete'),
|
||||
]
|
||||
|
|
|
@ -2,13 +2,15 @@
|
|||
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from dal import autocomplete
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic import CreateView, ListView, DetailView, UpdateView
|
||||
|
||||
from .models import Transaction,TransactionCategory,TransactionTemplate
|
||||
from .forms import TransactionTemplateForm, ConsoForm
|
||||
from .models import Note, Transaction, TransactionCategory, TransactionTemplate, Alias
|
||||
from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
|
||||
|
||||
class TransactionCreate(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
|
@ -17,7 +19,7 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
|
|||
TODO: If user have sufficient rights, they can transfer from an other note
|
||||
"""
|
||||
model = Transaction
|
||||
fields = ('destination', 'amount', 'reason')
|
||||
form_class = TransactionForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
"""
|
||||
|
@ -28,6 +30,77 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
|
|||
'to one or others')
|
||||
return context
|
||||
|
||||
|
||||
def get_form(self, form_class=None):
|
||||
"""
|
||||
If the user has no right to transfer funds, then it won't have the choice of the source of the transfer.
|
||||
"""
|
||||
form = super().get_form(form_class)
|
||||
|
||||
if False: # TODO: fix it with "if %user has no right to transfer funds"
|
||||
del form.fields['source']
|
||||
|
||||
return form
|
||||
|
||||
def form_valid(self, form):
|
||||
"""
|
||||
If the user has no right to transfer funds, then it will be the source of the transfer by default.
|
||||
"""
|
||||
if False: # TODO: fix it with "if %user has no right to transfer funds"
|
||||
form.instance.source = self.request.user.note
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class NoteAutocomplete(autocomplete.Select2QuerySetView):
|
||||
"""
|
||||
Auto complete note by aliases
|
||||
"""
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Quand une personne cherche un alias, une requête est envoyée sur l'API dédiée à l'auto-complétion.
|
||||
Cette fonction récupère la requête, et renvoie la liste filtrée des aliases.
|
||||
"""
|
||||
# Un utilisateur non connecté n'a accès à aucune information
|
||||
if not self.request.user.is_authenticated:
|
||||
return Alias.objects.none()
|
||||
|
||||
qs = Alias.objects.all()
|
||||
|
||||
# self.q est le paramètre de la recherche
|
||||
if self.q:
|
||||
qs = qs.filter(Q(name__regex=self.q) | Q(normalized_name__regex=Alias.normalize(self.q)))\
|
||||
.order_by('normalized_name').distinct()
|
||||
|
||||
# Filtrage par type de note (user, club, special)
|
||||
note_type = self.forwarded.get("note_type", None)
|
||||
if note_type:
|
||||
l = str(note_type).lower()
|
||||
if "user" in l:
|
||||
qs = qs.filter(note__polymorphic_ctype__model="noteuser")
|
||||
elif "club" in l:
|
||||
qs = qs.filter(note__polymorphic_ctype__model="noteclub")
|
||||
elif "special" in l:
|
||||
qs = qs.filter(note__polymorphic_ctype__model="notespecial")
|
||||
else:
|
||||
qs = qs.none()
|
||||
|
||||
return qs
|
||||
|
||||
def get_result_label(self, result):
|
||||
# Gère l'affichage de l'alias dans la recherche
|
||||
res = result.name
|
||||
note_name = str(result.note)
|
||||
if res != note_name:
|
||||
res += " (aka. " + note_name + ")"
|
||||
return res
|
||||
|
||||
def get_result_value(self, result):
|
||||
# Le résultat renvoyé doit être l'identifiant de la note, et non de l'alias
|
||||
return str(result.note.pk)
|
||||
|
||||
|
||||
class TransactionTemplateCreateView(LoginRequiredMixin,CreateView):
|
||||
"""
|
||||
Create TransactionTemplate
|
||||
|
|
|
@ -50,11 +50,18 @@ INSTALLED_APPS = [
|
|||
'django.contrib.sites',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
# API
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
# Autocomplete
|
||||
'dal',
|
||||
'dal_select2',
|
||||
|
||||
# Note apps
|
||||
'activity',
|
||||
'member',
|
||||
'note',
|
||||
'api',
|
||||
]
|
||||
LOGIN_REDIRECT_URL = '/note/transfer/'
|
||||
|
||||
|
@ -117,6 +124,18 @@ AUTHENTICATION_BACKENDS = (
|
|||
'guardian.backends.ObjectPermissionBackend',
|
||||
)
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
# Use Django's standard `django.contrib.auth` permissions,
|
||||
# or allow read-only access for unauthenticated users.
|
||||
'DEFAULT_PERMISSION_CLASSES': [
|
||||
# TODO Maybe replace it with our custom permissions system
|
||||
'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
|
||||
],
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': [
|
||||
'rest_framework.authentication.TokenAuthentication',
|
||||
]
|
||||
}
|
||||
|
||||
ANONYMOUS_USER_NAME = None # Disable guardian anonymous user
|
||||
|
||||
GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'
|
||||
|
|
|
@ -19,4 +19,7 @@ urlpatterns = [
|
|||
path('accounts/', include('django.contrib.auth.urls')),
|
||||
path('admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
# Include Django REST API
|
||||
path('api/', include('api.urls')),
|
||||
]
|
||||
|
|
|
@ -3,11 +3,14 @@ chardet==3.0.4
|
|||
defusedxml==0.6.0
|
||||
Django==2.2.3
|
||||
django-allauth==0.39.1
|
||||
django-autocomplete-light==3.3.0
|
||||
django-crispy-forms==1.7.2
|
||||
django-extensions==2.1.9
|
||||
django-filter==2.2.0
|
||||
django-guardian==2.1.0
|
||||
django-polymorphic==2.0.3
|
||||
djangorestframework==3.9.0
|
||||
django-rest-polymorphic==0.1.8
|
||||
django-reversion==3.0.3
|
||||
django-tables2==2.1.0
|
||||
docutils==0.14
|
||||
|
|
|
@ -31,6 +31,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
|
|||
crossorigin="anonymous">
|
||||
<link rel="stylesheet"
|
||||
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
|
||||
|
||||
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
|
||||
{% if form.media %}
|
||||
{{ form.media }}
|
||||
{% endif %}
|
||||
|
||||
{% block extracss %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
{% extends "base.html" %}
|
||||
{% load i18n static pretty_money django_tables2 %}
|
||||
|
||||
{% block content %}
|
||||
<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 />
|
||||
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.
|
||||
</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 %}
|
|
@ -8,13 +8,13 @@
|
|||
|
||||
<dl class="row">
|
||||
<dt class="col-6 col-md-3">{% trans 'name'|capfirst %}</dt>
|
||||
<dd class="col-6 col-md-3">{{ object.user.name }}</dd>
|
||||
<dd class="col-6 col-md-3">{{ object.user.last_name }}</dd>
|
||||
<dt class="col-6 col-md-3">{% trans 'first name'|capfirst %}</dt>
|
||||
<dd class="col-6 col-md-3">{{ object.user.first_name }}</dd>
|
||||
<dt class="col-6 col-md-3">{% trans 'username'|capfirst %}</dt>
|
||||
<dd class="col-6 col-md-3">{{ object.user.username }}</dd>
|
||||
<dt class="col-6 col-md-3">Aliases</dt>
|
||||
<dd class="col-6 col-md-3">{{ object.user.note.aliases_set.all }}</dd>
|
||||
<dd class="col-6 col-md-3">{{ object.user.note.alias_set.all }}</dd>
|
||||
<dt class="col-6 col-md-3">{% trans 'section'|capfirst %}</dt>
|
||||
<dd class="col-6 col-md-3">{{ object.section }}</dd>
|
||||
<dt class="col-6 col-md-3">{% trans 'address'|capfirst %}</dt>
|
||||
|
@ -23,6 +23,9 @@
|
|||
<dd class="col-6 col-md-3">{{ object.user.note.balance | pretty_money }}</dd>
|
||||
</dl>
|
||||
<center>
|
||||
{% if object.user.pk == user.pk %}
|
||||
<a class="btn btn-primary" href="{% url 'member:auth_token' %}">{% trans 'Manage auth token' %}</a>
|
||||
{% endif %}
|
||||
<a class="btn btn-primary" href="{% url 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a>
|
||||
<a class="btn btn-primary" href="{% url 'password_change' %}">{% trans 'Change password' %}</a>
|
||||
</center>
|
||||
|
|
Loading…
Reference in New Issue