Merge branch 'api' into 'master'

API & autocomplétion

See merge request bde/nk20!8
This commit is contained in:
erdnaxe 2020-02-18 12:06:53 +01:00
commit 0d7db68372
27 changed files with 817 additions and 15 deletions

View File

View File

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

14
apps/activity/api/urls.py Normal file
View File

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

View File

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

51
apps/api/urls.py Normal file
View File

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

View File

View File

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

15
apps/member/api/urls.py Normal file
View File

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

48
apps/member/api/views.py Normal file
View File

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

View File

@ -1,7 +1,7 @@
# -*- mode: python; coding: utf-8 -*- # -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django.contrib.auth.forms import UserChangeForm, UserCreationForm from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django import forms from django import forms
@ -44,6 +44,17 @@ class MembershipForm(forms.ModelForm):
class Meta: class Meta:
model = Membership model = Membership
fields = ('user','roles','date_start') 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, MemberFormSet = forms.modelformset_factory(Membership,
form=MembershipForm, form=MembershipForm,

View File

@ -18,4 +18,8 @@ urlpatterns = [
path('user/',views.UserListView.as_view(),name="user_list"), path('user/',views.UserListView.as_view(),name="user_list"),
path('user/<int:pk>',views.UserDetailView.as_view(),name="user_detail"), 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('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"),
] ]

View File

@ -2,19 +2,19 @@
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import redirect
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, DetailView, UpdateView from django.views.generic import CreateView, ListView, DetailView, UpdateView, RedirectView, TemplateView
from django.http import HttpResponseRedirect
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.db.models import Q from django.db.models import Q
from django_tables2.views import SingleTableView 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 .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper from .forms import SignUpForm, ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper
from .tables import ClubTable,UserTable from .tables import ClubTable,UserTable
@ -57,17 +57,43 @@ class UserUpdateView(LoginRequiredMixin,UpdateView):
second_form = ProfileForm second_form = ProfileForm
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["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 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): def form_valid(self, form):
profile_form = ProfileForm(data=self.request.POST,instance=self.request.user.profile) profile_form = ProfileForm(data=self.request.POST,instance=self.request.user.profile)
if form.is_valid() and profile_form.is_valid(): 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 = profile_form.save(commit=False)
profile.user = user profile.user = user
profile.save() profile.save()
user.save()
return super().form_valid(form) return super().form_valid(form)
def get_success_url(self, **kwargs): def get_success_url(self, **kwargs):
@ -115,6 +141,46 @@ class UserListView(LoginRequiredMixin,SingleTableView):
return context 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 ############### ############## CLUB ###############
################################### ###################################

View File

View File

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

18
apps/note/api/urls.py Normal file
View File

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

155
apps/note/api/views.py Normal file
View File

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

View File

@ -1,14 +1,54 @@
#!/usr/bin/env python #!/usr/bin/env python
from dal import autocomplete, forward
from django import forms from django import forms
from .models import TransactionTemplate, Transaction from .models import Transaction, TransactionTemplate
class TransactionTemplateForm(forms.ModelForm): class TransactionTemplateForm(forms.ModelForm):
class Meta: class Meta:
model = TransactionTemplate model = TransactionTemplate
fields ='__all__' 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): class ConsoForm(forms.ModelForm):
def save(self, commit=True): def save(self, commit=True):
button: TransactionTemplate = TransactionTemplate.objects.filter(name=self.data['button']).get() button: TransactionTemplate = TransactionTemplate.objects.filter(name=self.data['button']).get()
self.instance.destination = button.destination self.instance.destination = button.destination
@ -20,3 +60,14 @@ class ConsoForm(forms.ModelForm):
class Meta: class Meta:
model = Transaction model = Transaction
fields = ('source',) 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,
}),
}

View File

@ -85,7 +85,7 @@ class Note(PolymorphicModel):
""" """
Verify alias (simulate save) Verify alias (simulate save)
""" """
aliases = Alias.objects.filter(name=str(self)) aliases = Alias.objects.filter(normalized_name=Alias.normalize(str(self)))
if aliases.exists(): if aliases.exists():
# Alias exists, so check if it is linked to this note # Alias exists, so check if it is linked to this note
if aliases.first().note != self: if aliases.first().note != self:
@ -233,3 +233,8 @@ class Alias(models.Model):
'already exists.')) 'already exists.'))
except Alias.DoesNotExist: except Alias.DoesNotExist:
pass 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)

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import django_tables2 as tables import django_tables2 as tables
from django.db.models import F
from .models.transactions import Transaction from .models.transactions import Transaction

View File

@ -5,6 +5,7 @@
from django.urls import path from django.urls import path
from . import views from . import views
from .models import Note
app_name = 'note' app_name = 'note'
urlpatterns = [ urlpatterns = [
@ -14,4 +15,7 @@ urlpatterns = [
path('buttons/',views.TransactionTemplateListView.as_view(),name='template_list'), path('buttons/',views.TransactionTemplateListView.as_view(),name='template_list'),
path('consos/<str:template_type>/',views.ConsoView.as_view(),name='consos'), path('consos/<str:template_type>/',views.ConsoView.as_view(),name='consos'),
path('consos/',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'),
] ]

View File

@ -2,13 +2,15 @@
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay # Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later # SPDX-License-Identifier: GPL-3.0-or-later
from dal import autocomplete
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.urls import reverse_lazy, reverse from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import CreateView, ListView, DetailView, UpdateView from django.views.generic import CreateView, ListView, DetailView, UpdateView
from .models import Transaction,TransactionCategory,TransactionTemplate from .models import Note, Transaction, TransactionCategory, TransactionTemplate, Alias
from .forms import TransactionTemplateForm, ConsoForm from .forms import TransactionForm, TransactionTemplateForm, ConsoForm
class TransactionCreate(LoginRequiredMixin, CreateView): 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 TODO: If user have sufficient rights, they can transfer from an other note
""" """
model = Transaction model = Transaction
fields = ('destination', 'amount', 'reason') form_class = TransactionForm
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
""" """
@ -28,6 +30,77 @@ class TransactionCreate(LoginRequiredMixin, CreateView):
'to one or others') 'to one or others')
return context 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): class TransactionTemplateCreateView(LoginRequiredMixin,CreateView):
""" """
Create TransactionTemplate Create TransactionTemplate

View File

@ -50,11 +50,18 @@ INSTALLED_APPS = [
'django.contrib.sites', 'django.contrib.sites',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# API
'rest_framework',
'rest_framework.authtoken',
# Autocomplete
'dal',
'dal_select2',
# Note apps # Note apps
'activity', 'activity',
'member', 'member',
'note', 'note',
'api',
] ]
LOGIN_REDIRECT_URL = '/note/transfer/' LOGIN_REDIRECT_URL = '/note/transfer/'
@ -117,6 +124,18 @@ AUTHENTICATION_BACKENDS = (
'guardian.backends.ObjectPermissionBackend', '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 ANONYMOUS_USER_NAME = None # Disable guardian anonymous user
GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type' GUARDIAN_GET_CONTENT_TYPE = 'polymorphic.contrib.guardian.get_polymorphic_base_content_type'

View File

@ -19,4 +19,7 @@ urlpatterns = [
path('accounts/', include('django.contrib.auth.urls')), path('accounts/', include('django.contrib.auth.urls')),
path('admin/doc/', include('django.contrib.admindocs.urls')), path('admin/doc/', include('django.contrib.admindocs.urls')),
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
# Include Django REST API
path('api/', include('api.urls')),
] ]

View File

@ -3,11 +3,14 @@ chardet==3.0.4
defusedxml==0.6.0 defusedxml==0.6.0
Django==2.2.3 Django==2.2.3
django-allauth==0.39.1 django-allauth==0.39.1
django-autocomplete-light==3.3.0
django-crispy-forms==1.7.2 django-crispy-forms==1.7.2
django-extensions==2.1.9 django-extensions==2.1.9
django-filter==2.2.0 django-filter==2.2.0
django-guardian==2.1.0 django-guardian==2.1.0
django-polymorphic==2.0.3 django-polymorphic==2.0.3
djangorestframework==3.9.0
django-rest-polymorphic==0.1.8
django-reversion==3.0.3 django-reversion==3.0.3
django-tables2==2.1.0 django-tables2==2.1.0
docutils==0.14 docutils==0.14

View File

@ -31,6 +31,12 @@ SPDX-License-Identifier: GPL-3.0-or-later
crossorigin="anonymous"> crossorigin="anonymous">
<link rel="stylesheet" <link rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css"> 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 %} {% block extracss %}{% endblock %}
</head> </head>
<body> <body>

View File

@ -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 &lt;TOKEN&gt;</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 %}

View File

@ -8,13 +8,13 @@
<dl class="row"> <dl class="row">
<dt class="col-6 col-md-3">{% trans 'name'|capfirst %}</dt> <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> <dt class="col-6 col-md-3">{% trans 'first name'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.user.first_name }}</dd> <dd class="col-6 col-md-3">{{ object.user.first_name }}</dd>
<dt class="col-6 col-md-3">{% trans 'username'|capfirst %}</dt> <dt class="col-6 col-md-3">{% trans 'username'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.user.username }}</dd> <dd class="col-6 col-md-3">{{ object.user.username }}</dd>
<dt class="col-6 col-md-3">Aliases</dt> <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> <dt class="col-6 col-md-3">{% trans 'section'|capfirst %}</dt>
<dd class="col-6 col-md-3">{{ object.section }}</dd> <dd class="col-6 col-md-3">{{ object.section }}</dd>
<dt class="col-6 col-md-3">{% trans 'address'|capfirst %}</dt> <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> <dd class="col-6 col-md-3">{{ object.user.note.balance | pretty_money }}</dd>
</dl> </dl>
<center> <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 'member:user_update_profile' object.pk %}">{% trans 'Update Profile' %}</a>
<a class="btn btn-primary" href="{% url 'password_change' %}">{% trans 'Change password' %}</a> <a class="btn btn-primary" href="{% url 'password_change' %}">{% trans 'Change password' %}</a>
</center> </center>