1
0
mirror of https://gitlab.crans.org/bde/nk20 synced 2025-06-21 01:48:21 +02:00

Merge branch 'master' into import_nk15

This commit is contained in:
Pierre-antoine Comby
2020-02-22 16:34:17 +01:00
68 changed files with 2147 additions and 830 deletions

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
default_app_config = 'member.apps.MemberConfig'

View File

@ -1,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib import admin
@ -19,9 +18,9 @@ class ProfileInline(admin.StackedInline):
class CustomUserAdmin(UserAdmin):
inlines = (ProfileInline,)
inlines = (ProfileInline, )
list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff')
list_select_related = ('profile',)
list_select_related = ('profile', )
form = ProfileForm
def get_inline_instances(self, request, obj=None):

View File

View File

@ -0,0 +1,46 @@
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from rest_framework import serializers
from ..models import Profile, Club, Role, Membership
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__'

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

@ -0,0 +1,14 @@
# 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)

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

@ -0,0 +1,47 @@
# 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,5 +1,4 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.apps import AppConfig

View File

@ -1,31 +1,33 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django_filters import FilterSet, CharFilter,NumberFilter
from django_filters import FilterSet, CharFilter
from django.contrib.auth.models import User
from django.db.models import CharField
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, Submit
from .models import Club
class UserFilter(FilterSet):
class Meta:
model = User
fields = ['last_name','first_name','username','profile__section']
filter_overrides={
CharField:{
'filter_class':CharFilter,
'extra': lambda f:{
'lookup_expr':'icontains'
fields = ['last_name', 'first_name', 'username', 'profile__section']
filter_overrides = {
CharField: {
'filter_class': CharFilter,
'extra': lambda f: {
'lookup_expr': 'icontains'
}
}
}
class UserFilterFormHelper(FormHelper):
form_method = 'GET'
layout = Layout(
'last_name','first_name','username','profile__section',
Submit('Submit','Apply Filter'),
'last_name',
'first_name',
'username',
'profile__section',
Submit('Submit', 'Apply Filter'),
)

View File

@ -1,26 +1,23 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from dal import autocomplete
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from django import forms
from .models import Profile, Club, Membership
from django.utils.translation import gettext_lazy as _
from crispy_forms.helper import FormHelper
from crispy_forms import layout, bootstrap
from crispy_forms.bootstrap import InlineField, FormActions, StrictButton, Div, Field
from crispy_forms.bootstrap import Div
from crispy_forms.layout import Layout
class SignUpForm(UserCreationForm):
class Meta:
model = User
fields = ['first_name','last_name','username','email']
fields = ['first_name', 'last_name', 'username', 'email']
class ProfileForm(forms.ModelForm):
"""
@ -31,37 +28,56 @@ class ProfileForm(forms.ModelForm):
fields = '__all__'
exclude = ['user']
class ClubForm(forms.ModelForm):
class Meta:
model = Club
fields ='__all__'
fields = '__all__'
class AddMembersForm(forms.Form):
class Meta:
fields = ('',)
fields = ('', )
class MembershipForm(forms.ModelForm):
class Meta:
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,
form=MembershipForm,
extra=2,
can_delete=True,
)
MemberFormSet = forms.modelformset_factory(Membership,
form=MembershipForm,
extra=2,
can_delete=True)
class FormSetHelper(FormHelper):
def __init__(self,*args,**kwargs):
super().__init__(*args,**kwargs)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_tag = False
self.form_method = 'POST'
self.form_class='form-inline'
self.form_class = 'form-inline'
# self.template = 'bootstrap/table_inline_formset.html'
self.layout = Layout(
Div(
Div('user',css_class='col-sm-2'),
Div('roles',css_class='col-sm-2'),
Div('date_start',css_class='col-sm-2'),
Div('user', css_class='col-sm-2'),
Div('roles', css_class='col-sm-2'),
Div('date_start', css_class='col-sm-2'),
css_class="row formset-row",
)
)
))

View File

@ -1,14 +1,12 @@
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.conf import settings
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils.translation import gettext_lazy as _
from django.urls import reverse, reverse_lazy
class Profile(models.Model):
"""
An user profile
@ -50,7 +48,8 @@ class Profile(models.Model):
verbose_name_plural = _('user profile')
def get_absolute_url(self):
return reverse('user_detail',args=(self.pk,))
return reverse('user_detail', args=(self.pk, ))
class Club(models.Model):
"""
@ -98,7 +97,7 @@ class Club(models.Model):
return self.name
def get_absolute_url(self):
return reverse_lazy('member:club_detail', args=(self.pk,))
return reverse_lazy('member:club_detail', args=(self.pk, ))
class Role(models.Model):
@ -118,6 +117,9 @@ class Role(models.Model):
verbose_name = _('role')
verbose_name_plural = _('roles')
def __str__(self):
return str(self.name)
class Membership(models.Model):
"""
@ -126,15 +128,15 @@ class Membership(models.Model):
"""
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.PROTECT
on_delete=models.PROTECT,
)
club = models.ForeignKey(
Club,
on_delete=models.PROTECT
on_delete=models.PROTECT,
)
roles = models.ForeignKey(
Role,
on_delete=models.PROTECT
on_delete=models.PROTECT,
)
date_start = models.DateField(
verbose_name=_('membership starts on'),

View File

@ -1,6 +1,2 @@
#!/usr/bin/env python
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later

View File

@ -1,19 +1,24 @@
#!/usr/bin/env python
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
import django_tables2 as tables
from .models import Club
from django.conf import settings
from django.contrib.auth.models import User
from .models import Club
class ClubTable(tables.Table):
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
model = Club
template_name = 'django_tables2/bootstrap.html'
fields = ('id','name','email')
row_attrs = {'class':'table-row',
'data-href': lambda record: record.pk }
template_name = 'django_tables2/bootstrap4.html'
fields = ('id', 'name', 'email')
row_attrs = {
'class': 'table-row',
'data-href': lambda record: record.pk
}
class UserTable(tables.Table):
@ -21,7 +26,9 @@ class UserTable(tables.Table):
solde = tables.Column(accessor='note.balance')
class Meta:
attrs = {'class':'table table-bordered table-condensed table-striped table-hover'}
template_name = 'django_tables2/bootstrap.html'
fields = ('last_name','first_name','username','email')
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
template_name = 'django_tables2/bootstrap4.html'
fields = ('last_name', 'first_name', 'username', 'email')
model = User

View File

@ -1,7 +1,4 @@
#!/usr/bin/env python
# -*- mode: python; coding: utf-8 -*-
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
# SPDX-License-Identifier: GPL-3.0-or-later
from django.urls import path
@ -10,12 +7,16 @@ from . import views
app_name = 'member'
urlpatterns = [
path('signup/',views.UserCreateView.as_view(),name="signup"),
path('club/',views.ClubListView.as_view(),name="club_list"),
path('club/<int:pk>/',views.ClubDetailView.as_view(),name="club_detail"),
path('club/<int:pk>/add_member/',views.ClubAddMemberView.as_view(),name="club_add_member"),
path('club/create/',views.ClubCreateView.as_view(),name="club_create"),
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('signup/', views.UserCreateView.as_view(), name="signup"),
path('club/', views.ClubListView.as_view(), name="club_list"),
path('club/<int:pk>/', views.ClubDetailView.as_view(), name="club_detail"),
path('club/<int:pk>/add_member/', views.ClubAddMemberView.as_view(), name="club_add_member"),
path('club/create/', views.ClubCreateView.as_view(), name="club_create"),
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"),
]

View File

@ -1,29 +1,26 @@
#!/usr/bin/env python
# Copyright (C) 2018-2019 by BDE ENS Paris-Saclay
# Copyright (C) 2018-2020 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, DetailView, UpdateView, 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 .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper
from .tables import ClubTable,UserTable
from .filters import UserFilter, UserFilterFormHelper
from rest_framework.authtoken.models import Token
from note.models import Alias, NoteUser
from note.models.transactions import Transaction
from note.tables import HistoryTable
from .models import Profile, Club, Membership
from .forms import SignUpForm, ProfileForm, ClubForm, MembershipForm, MemberFormSet, FormSetHelper
from .tables import ClubTable, UserTable
from .filters import UserFilter, UserFilterFormHelper
class UserCreateView(CreateView):
"""
Une vue pour inscrire un utilisateur et lui créer un profile
@ -31,10 +28,10 @@ class UserCreateView(CreateView):
form_class = SignUpForm
success_url = reverse_lazy('login')
template_name ='member/signup.html'
template_name = 'member/signup.html'
second_form = ProfileForm
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["profile_form"] = self.second_form()
@ -49,40 +46,78 @@ class UserCreateView(CreateView):
profile.save()
return super().form_valid(form)
class UserUpdateView(LoginRequiredMixin,UpdateView):
class UserUpdateView(LoginRequiredMixin, UpdateView):
model = User
fields = ['first_name','last_name','username','email']
fields = ['first_name', 'last_name', 'username', 'email']
template_name = 'member/profile_update.html'
second_form = ProfileForm
def get_context_data(self,**kwargs):
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)
context['title'] = _("Update 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)
profile_form = ProfileForm(
data=self.request.POST,
instance=self.request.user.profile,
)
if form.is_valid() and profile_form.is_valid():
user = form.save()
profile = profile_form.save(commit=False)
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):
if kwargs:
return reverse_lazy('member:user_detail', kwargs = {'pk': kwargs['id']})
if kwargs:
return reverse_lazy('member:user_detail',
kwargs={'pk': kwargs['id']})
else:
return reverse_lazy('member:user_detail', args = (self.object.id,))
return reverse_lazy('member:user_detail', args=(self.object.id, ))
class UserDetailView(LoginRequiredMixin,DetailView):
class UserDetailView(LoginRequiredMixin, DetailView):
"""
Affiche les informations sur un utilisateur, sa note, ses clubs ...
Affiche les informations sur un utilisateur, sa note, ses clubs...
"""
model = Profile
context_object_name = "profile"
def get_context_data(slef,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
user = context['profile'].user
history_list = \
@ -91,9 +126,14 @@ class UserDetailView(LoginRequiredMixin,DetailView):
club_list = \
Membership.objects.all().filter(user=user).only("club")
context['club_list'] = ClubTable(club_list)
context['title'] = _("Account #%(id)s: %(username)s") % {
'id': user.pk,
'username': user.username,
}
return context
class UserListView(LoginRequiredMixin,SingleTableView):
class UserListView(LoginRequiredMixin, SingleTableView):
"""
Affiche la liste des utilisateurs, avec une fonction de recherche statique
"""
@ -103,44 +143,91 @@ class UserListView(LoginRequiredMixin,SingleTableView):
filter_class = UserFilter
formhelper_class = UserFilterFormHelper
def get_queryset(self,**kwargs):
def get_queryset(self, **kwargs):
qs = super().get_queryset()
self.filter = self.filter_class(self.request.GET,queryset=qs)
self.filter = self.filter_class(self.request.GET, queryset=qs)
self.filter.form.helper = self.formhelper_class()
return self.filter.qs
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["filter"] = self.filter
return context
###################################
############## CLUB ###############
###################################
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"
class ClubCreateView(LoginRequiredMixin,CreateView):
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 #
# ******************************* #
class ClubCreateView(LoginRequiredMixin, CreateView):
"""
Create Club
"""
model = Club
form_class = ClubForm
def form_valid(self,form):
def form_valid(self, form):
return super().form_valid(form)
class ClubListView(LoginRequiredMixin,SingleTableView):
class ClubListView(LoginRequiredMixin, SingleTableView):
"""
List existing Clubs
"""
model = Club
table_class = ClubTable
class ClubDetailView(LoginRequiredMixin,DetailView):
model = Club
context_object_name="club"
def get_context_data(self,**kwargs):
class ClubDetailView(LoginRequiredMixin, DetailView):
model = Club
context_object_name = "club"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
club = context["club"]
club_transactions = \
@ -152,23 +239,30 @@ class ClubDetailView(LoginRequiredMixin,DetailView):
context['member_list'] = club_member
return context
class ClubAddMemberView(LoginRequiredMixin,CreateView):
class ClubAddMemberView(LoginRequiredMixin, CreateView):
model = Membership
form_class = MembershipForm
template_name = 'member/add_members.html'
def get_context_data(self,**kwargs):
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['formset'] = MemberFormSet()
context['helper'] = FormSetHelper()
context['no_cache'] = True
return context
def post(self,request,*args,**kwargs):
formset = MembershipFormset(request.POST)
if formset.is_valid():
return self.form_valid(formset)
else:
return self.form_invalid(formset)
def post(self, request, *args, **kwargs):
return
# TODO: Implement POST
# formset = MembershipFormset(request.POST)
# if formset.is_valid():
# return self.form_valid(formset)
# else:
# return self.form_invalid(formset)
def form_valid(self,formset):
def form_valid(self, formset):
formset.save()
return super().form_valid(formset)