mirror of
https://gitlab.crans.org/bde/nk20
synced 2025-06-21 01:48:21 +02:00
Merge branch 'master' into rights
# Conflicts: # note_kfet/settings/base.py
This commit is contained in:
@ -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'
|
||||
|
@ -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):
|
||||
|
0
apps/member/api/__init__.py
Normal file
0
apps/member/api/__init__.py
Normal file
46
apps/member/api/serializers.py
Normal file
46
apps/member/api/serializers.py
Normal 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
14
apps/member/api/urls.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .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
47
apps/member/api/views.py
Normal 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
|
@ -1,11 +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.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from django.db.models.signals import post_save
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .signals import save_user_profile
|
||||
|
||||
|
||||
class MemberConfig(AppConfig):
|
||||
name = 'member'
|
||||
verbose_name = _('member')
|
||||
|
||||
def ready(self):
|
||||
"""
|
||||
Define app internal signals to interact with other apps
|
||||
"""
|
||||
post_save.connect(
|
||||
save_user_profile,
|
||||
sender=settings.AUTH_USER_MODEL,
|
||||
)
|
||||
|
33
apps/member/filters.py
Normal file
33
apps/member/filters.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
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
|
||||
|
||||
|
||||
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'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserFilterFormHelper(FormHelper):
|
||||
form_method = 'GET'
|
||||
layout = Layout(
|
||||
'last_name',
|
||||
'first_name',
|
||||
'username',
|
||||
'profile__section',
|
||||
Submit('Submit', 'Apply Filter'),
|
||||
)
|
26
apps/member/fixtures/initial.json
Normal file
26
apps/member/fixtures/initial.json
Normal file
@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"model": "member.club",
|
||||
"pk": 1,
|
||||
"fields": {
|
||||
"name": "BDE",
|
||||
"email": "tresorerie.bde@example.com",
|
||||
"membership_fee": 5,
|
||||
"membership_duration": "396 00:00:00",
|
||||
"membership_start": "213 00:00:00",
|
||||
"membership_end": "273 00:00:00"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "member.club",
|
||||
"pk": 2,
|
||||
"fields": {
|
||||
"name": "Kfet",
|
||||
"email": "tresorerie.bde@example.com",
|
||||
"membership_fee": 35,
|
||||
"membership_duration": "396 00:00:00",
|
||||
"membership_start": "213 00:00:00",
|
||||
"membership_end": "273 00:00:00"
|
||||
}
|
||||
}
|
||||
]
|
@ -1,61 +1,88 @@
|
||||
# -*- 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):
|
||||
def __init__(self,*args,**kwargs):
|
||||
super().__init__(*args,**kwargs)
|
||||
self.fields['username'].widget.attrs.pop("autofocus", None)
|
||||
self.fields['first_name'].widget.attrs.update({"autofocus":"autofocus"})
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['first_name', 'last_name', 'username', 'email']
|
||||
|
||||
|
||||
class ProfileForm(forms.ModelForm):
|
||||
"""
|
||||
Forms pour la création d'un profile utilisateur.
|
||||
A form for the extras field provided by the :model:`member.Profile` model.
|
||||
"""
|
||||
class Meta:
|
||||
model = Profile
|
||||
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",
|
||||
)
|
||||
)
|
||||
))
|
||||
|
27
apps/member/hashers.py
Normal file
27
apps/member/hashers.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import hashlib
|
||||
|
||||
from django.contrib.auth.hashers import PBKDF2PasswordHasher
|
||||
from django.utils.crypto import constant_time_compare
|
||||
|
||||
|
||||
class CustomNK15Hasher(PBKDF2PasswordHasher):
|
||||
"""
|
||||
Permet d'importer les mots de passe depuis la Note KFet 2015.
|
||||
Si un hash de mot de passe est de la forme :
|
||||
`custom_nk15$<NB>$<ENCODED>`
|
||||
où <NB> est un entier quelconque (symbolisant normalement un nombre d'itérations)
|
||||
et <ENCODED> le hash du mot de passe dans la Note Kfet 2015,
|
||||
alors ce hasher va vérifier le mot de passe.
|
||||
N'ayant pas la priorité (cf note_kfet/settings/base.py), le mot de passe sera
|
||||
converti automatiquement avec l'algorithme PBKDF2.
|
||||
"""
|
||||
algorithm = "custom_nk15"
|
||||
|
||||
def verify(self, password, encoded):
|
||||
if '|' in encoded:
|
||||
salt, db_hashed_pass = encoded.split('$')[2].split('|')
|
||||
return constant_time_compare(hashlib.sha256((salt + password).encode("utf-8")).hexdigest(), db_hashed_pass)
|
||||
return super().verify(password, encoded)
|
@ -1,13 +1,10 @@
|
||||
# -*- 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
|
||||
|
||||
import datetime
|
||||
|
||||
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
|
||||
|
||||
@ -16,8 +13,9 @@ class Profile(models.Model):
|
||||
"""
|
||||
An user profile
|
||||
|
||||
We do not want to patch the Django Contrib Auth User class
|
||||
We do not want to patch the Django Contrib :model:`auth.User`model;
|
||||
so this model add an user profile with additional information.
|
||||
|
||||
"""
|
||||
user = models.OneToOneField(
|
||||
settings.AUTH_USER_MODEL,
|
||||
@ -52,12 +50,14 @@ 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):
|
||||
"""
|
||||
A student club
|
||||
A club is a group of people, whose membership is handle by their
|
||||
:model:`member.Membership`, and gives access to right defined by a :model:`member.Role`.
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
@ -100,12 +100,15 @@ 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):
|
||||
"""
|
||||
Role that an user can have in a club
|
||||
Role that an :model:`auth.User` can have in a :model:`member.Club`
|
||||
|
||||
TODO: Integrate the right management, and create some standard Roles at the
|
||||
creation of the club.
|
||||
"""
|
||||
name = models.CharField(
|
||||
verbose_name=_('name'),
|
||||
@ -117,22 +120,26 @@ class Role(models.Model):
|
||||
verbose_name = _('role')
|
||||
verbose_name_plural = _('roles')
|
||||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
"""
|
||||
Register the membership of a user to a club, including roles and membership duration.
|
||||
|
||||
"""
|
||||
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'),
|
||||
|
@ -1,6 +1,15 @@
|
||||
#!/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
|
||||
|
||||
def save_user_profile(instance, created, raw, **_kwargs):
|
||||
"""
|
||||
Hook to create and save a profile when an user is updated if it is not registered with the signup form
|
||||
"""
|
||||
if raw:
|
||||
# When provisionning data, do not try to autocreate
|
||||
return
|
||||
|
||||
if created:
|
||||
from .models import Profile
|
||||
Profile.objects.get_or_create(user=instance)
|
||||
instance.profile.save()
|
||||
|
@ -1,13 +1,34 @@
|
||||
#!/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 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):
|
||||
section = tables.Column(accessor='profile.section')
|
||||
solde = tables.Column(accessor='note.balance')
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
fields = ('last_name', 'first_name', 'username', 'email')
|
||||
model = User
|
||||
|
@ -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,10 +7,18 @@ 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/<int:pk>',views.UserDetailView.as_view(),name="user_detail")
|
||||
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('user/<int:pk>/update_pic', views.ProfilePictureUpdateView.as_view(), name="user_update_pic"),
|
||||
path('user/<int:pk>/aliases', views.AliasView.as_view(), name="user_alias"),
|
||||
path('user/aliases/delete/<int:pk>', views.DeleteAliasView.as_view(), name="user_alias_delete"),
|
||||
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"),
|
||||
]
|
||||
|
@ -1,88 +1,333 @@
|
||||
#!/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 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
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django.views.generic import CreateView, DetailView, UpdateView, TemplateView,DeleteView
|
||||
from django.views.generic.edit import FormMixin
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib import messages
|
||||
from django.urls import reverse_lazy
|
||||
from django.http import HttpResponseRedirect
|
||||
from django.db.models import Q
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
from django_tables2.views import SingleTableView
|
||||
from rest_framework.authtoken.models import Token
|
||||
from dal import autocomplete
|
||||
from PIL import Image
|
||||
import io
|
||||
|
||||
from note.models import Alias, NoteUser
|
||||
from note.models.transactions import Transaction
|
||||
from note.tables import HistoryTable, AliasTable
|
||||
from note.forms import AliasForm, ImageForm
|
||||
|
||||
from .models import Profile, Club, Membership
|
||||
from .forms import ProfileForm, ClubForm,MembershipForm, MemberFormSet,FormSetHelper
|
||||
from .tables import ClubTable
|
||||
from note.models.transactions import Transaction
|
||||
from note.tables import HistoryTable
|
||||
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
|
||||
|
||||
"""
|
||||
form_class = ProfileForm
|
||||
success_url = reverse_lazy('login')
|
||||
template_name ='member/signup.html'
|
||||
second_form = UserCreationForm
|
||||
|
||||
def get_context_data(self,**kwargs):
|
||||
form_class = SignUpForm
|
||||
success_url = reverse_lazy('login')
|
||||
template_name = 'member/signup.html'
|
||||
second_form = ProfileForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["user_form"] = self.second_form
|
||||
context["profile_form"] = self.second_form()
|
||||
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
user_form = UserCreationForm(self.request.POST)
|
||||
if user_form.is_valid():
|
||||
user = user_form.save()
|
||||
user_profile = form.save(commit=False) # do not save to db
|
||||
user_profile.user = user
|
||||
user_profile.save()
|
||||
profile_form = ProfileForm(self.request.POST)
|
||||
if form.is_valid() and profile_form.is_valid():
|
||||
user = form.save()
|
||||
profile = profile_form.save(commit=False)
|
||||
profile.user = user
|
||||
profile.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
class UserDetailView(LoginRequiredMixin,DetailView):
|
||||
model = Profile
|
||||
context_object_name = "profile"
|
||||
def get_context_data(slef,**kwargs):
|
||||
class UserUpdateView(LoginRequiredMixin, UpdateView):
|
||||
model = User
|
||||
fields = ['first_name', 'last_name', 'username', 'email']
|
||||
template_name = 'member/profile_update.html'
|
||||
context_object_name = 'user_object'
|
||||
profile_form = ProfileForm
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = context['profile'].user
|
||||
|
||||
context['profile_form'] = self.profile_form(instance=context['user_object'].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.object:
|
||||
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.object.profile,
|
||||
)
|
||||
if form.is_valid() and profile_form.is_valid():
|
||||
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']})
|
||||
else:
|
||||
return reverse_lazy('member:user_detail', args=(self.object.id, ))
|
||||
|
||||
|
||||
class UserDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
Affiche les informations sur un utilisateur, sa note, ses clubs...
|
||||
"""
|
||||
model = User
|
||||
context_object_name = "user_object"
|
||||
template_name = "member/profile_detail.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
user = context['user_object']
|
||||
history_list = \
|
||||
Transaction.objects.all().filter(Q(source=user.note) | Q(destination=user.note))
|
||||
context['history_list'] = HistoryTable(history_list)
|
||||
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 ClubCreateView(LoginRequiredMixin,CreateView):
|
||||
class UserListView(LoginRequiredMixin, SingleTableView):
|
||||
"""
|
||||
Affiche la liste des utilisateurs, avec une fonction de recherche statique
|
||||
"""
|
||||
model = User
|
||||
table_class = UserTable
|
||||
template_name = 'member/user_list.html'
|
||||
filter_class = UserFilter
|
||||
formhelper_class = UserFilterFormHelper
|
||||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset()
|
||||
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):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context["filter"] = self.filter
|
||||
return context
|
||||
|
||||
class AliasView(LoginRequiredMixin,FormMixin,DetailView):
|
||||
model = User
|
||||
template_name = 'member/profile_alias.html'
|
||||
context_object_name = 'user_object'
|
||||
form_class = AliasForm
|
||||
|
||||
def get_context_data(self,**kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
note = context['user_object'].note
|
||||
context["aliases"] = AliasTable(note.alias_set.all())
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:user_alias', kwargs={'pk': self.object.id})
|
||||
|
||||
def post(self,request,*args,**kwargs):
|
||||
self.object = self.get_object()
|
||||
form = self.get_form()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self, form):
|
||||
alias = form.save(commit=False)
|
||||
alias.note = self.object.note
|
||||
alias.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
class DeleteAliasView(LoginRequiredMixin, DeleteView):
|
||||
model = Alias
|
||||
|
||||
def delete(self,request,*args,**kwargs):
|
||||
try:
|
||||
self.object = self.get_object()
|
||||
self.object.delete()
|
||||
except ValidationError as e:
|
||||
# TODO: pass message to redirected view.
|
||||
messages.error(self.request,str(e))
|
||||
else:
|
||||
messages.success(self.request,_("Alias successfully deleted"))
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self):
|
||||
print(self.request)
|
||||
return reverse_lazy('member:user_alias',kwargs={'pk':self.object.note.user.pk})
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return self.post(request, *args, **kwargs)
|
||||
|
||||
class ProfilePictureUpdateView(LoginRequiredMixin, FormMixin, DetailView):
|
||||
model = User
|
||||
template_name = 'member/profile_picture_update.html'
|
||||
context_object_name = 'user_object'
|
||||
form_class = ImageForm
|
||||
def get_context_data(self,*args,**kwargs):
|
||||
context = super().get_context_data(*args,**kwargs)
|
||||
context['form'] = self.form_class(self.request.POST,self.request.FILES)
|
||||
return context
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:user_detail', kwargs={'pk': self.object.id})
|
||||
|
||||
def post(self,request,*args,**kwargs):
|
||||
form = self.get_form()
|
||||
self.object = self.get_object()
|
||||
if form.is_valid():
|
||||
return self.form_valid(form)
|
||||
else:
|
||||
print('is_invalid')
|
||||
print(form)
|
||||
return self.form_invalid(form)
|
||||
|
||||
def form_valid(self,form):
|
||||
image_field = form.cleaned_data['image']
|
||||
x = form.cleaned_data['x']
|
||||
y = form.cleaned_data['y']
|
||||
w = form.cleaned_data['width']
|
||||
h = form.cleaned_data['height']
|
||||
# image crop and resize
|
||||
image_file = io.BytesIO(image_field.read())
|
||||
ext = image_field.name.split('.')[-1]
|
||||
image = Image.open(image_file)
|
||||
image = image.crop((x, y, x+w, y+h))
|
||||
image_clean = image.resize((settings.PIC_WIDTH,
|
||||
settings.PIC_RATIO*settings.PIC_WIDTH),
|
||||
Image.ANTIALIAS)
|
||||
image_file = io.BytesIO()
|
||||
image_clean.save(image_file,ext)
|
||||
image_field.file = image_file
|
||||
# renaming
|
||||
filename = "{}_pic.{}".format(self.object.note.pk, ext)
|
||||
image_field.name = filename
|
||||
self.object.note.display_image = image_field
|
||||
self.object.note.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
|
||||
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 #
|
||||
# ******************************* #
|
||||
|
||||
|
||||
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 tables
|
||||
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 = \
|
||||
@ -93,24 +338,31 @@ class ClubDetailView(LoginRequiredMixin,DetailView):
|
||||
# TODO: consider only valid Membership
|
||||
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()
|
||||
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 form_valid(self,formset):
|
||||
context['no_cache'] = True
|
||||
|
||||
return context
|
||||
|
||||
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):
|
||||
formset.save()
|
||||
return super().form_valid(formset)
|
||||
|
Reference in New Issue
Block a user