Commit 6cb6d99a authored by Hamza Dely's avatar Hamza Dely
Browse files

Merge branch 'cleanup'

parents b1a1bc0f c0f55d89
......@@ -11,6 +11,7 @@ from django.db import transaction
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.forms import PasswordChangeForm
from note_kfet.environnement import ACCREDITATIONS_DEFAUT
......@@ -28,6 +29,7 @@ class AdherentInscriptionForm(UserCreationForm):
required=False,
queryset=Section.objects.filter(ferme=False),
)
class Meta:
model = Adherent
fields = [
......@@ -36,35 +38,38 @@ class AdherentInscriptionForm(UserCreationForm):
'password2', 'remunere', 'pbsante',
]
def clean(self):
def clean_pseudo(self):
"""
Vérifie que le pseudo demandé n'est pas déjà pris, que
la section du nouvel adhérent n'est pas fermée et que le
formulaire est complet relativement au type de compte.
Vérifie que le pseudo demandé n'est pas déjà pris
"""
cleaned_data = super().clean()
pseudo = Adherent.normalize_username(cleaned_data['pseudo'])
pseudo = self._meta.model.normalize_username(self.cleaned_data['pseudo'])
qs = Alias.objects.filter(alias=pseudo)
if qs.filter(proprietaire__isnull=False).exists():
self.add_error(
'pseudo',
ValidationError(
"Le pseudo %s est déjà pris." % cleaned_data['pseudo']
),
)
if cleaned_data.get('section', None) and cleaned_data['section'].ferme:
self.add_error(
'section',
ValidationError("Cette section est fermée."),
)
if (cleaned_data['type'] != Adherent.DEBIT and
not cleaned_data.get('nom', '')):
raise ValidationError("Le pseudo %s est déjà pris." % self.cleaned_data['pseudo'])
return pseudo
def clean_section(self):
"""
Vérifie que la section demandée n'est pas fermée
"""
section = self.cleaned_data.get('section', None)
if section and section.ferme:
raise ValidationError("Cette section est fermée.")
return section
def clean(self):
"""
Vérifie que le formulaire est complet relativement au type de compte.
"""
cleaned_data = super().clean()
if (cleaned_data['type'] != Adherent.DEBIT and not cleaned_data.get('nom', '')):
self.add_error(
'nom',
ValidationError("Veuillez rensigner votre nom."),
)
if (cleaned_data['type'] == Adherent.PERSONNE and
not cleaned_data.get('prenom', '')):
if (cleaned_data['type'] == Adherent.PERSONNE and not cleaned_data.get('prenom', '')):
self.add_error(
'prenom',
ValidationError("Veuillez rensigner votre prénom."),
......@@ -80,25 +85,16 @@ class AdherentInscriptionForm(UserCreationForm):
Toute l'opération est effectuée de manière atomique dans
la base de données.
"""
with transaction.atomic():
adh = super().save(*args, **kwargs)
pseudo, _ = Alias.objects.get_or_create(alias=adh.pseudo)
pseudo.proprietaire = adh
pseudo.save()
Historique.objects.create(alias=pseudo, suivant=adh)
Adhesion.objects.create(
adherent=adh,
fin=timezone.now() + timedelta(days=365),
section=self.cleaned_data['section'],
)
for codename, niveau, meta in ACCREDITATIONS_DEFAUT.get(adh.type, []):
perm = Accreditation.objects.create(
droit=Droit.objects.get(codename=codename, niveau=niveau),
adherent=adh,
meta=meta,
)
# TODO : Envoyer un mail de bienvenue à l'adhérent nouvellement inscrit
return adh
self.cleaned_data.pop('password2')
self.cleaned_data['password'] = self.cleaned_data.pop('password1')
return self._meta.model.objects.create(**self.cleaned_data)
class AdherentPasswordChangeForm(PasswordChangeForm):
"""
Formulaire de changement de mot de passe pour un adhérent
"""
def __init__(self, instance=None, **kwargs):
super().__init__(instance, **kwargs)
class AdherentDroitForm(forms.Form):
"""
......
......@@ -3,6 +3,7 @@
"""
import uuid
from datetime import timedelta
from django.db import models, transaction, IntegrityError
from django.db.models import Q
......@@ -12,9 +13,12 @@ from django.core.mail import send_mail
from django.core.validators import RegexValidator
from django.core.exceptions import ValidationError, PermissionDenied
from django.template import loader
from django.urls import reverse
from django.utils import timezone
from note_kfet import droits as m_droits
from note_kfet.utils import CumulativeDict
from note_kfet.environnement import ACCREDITATIONS_DEFAUT
## Regex utilisées pour la vérification des champs de modèles
......@@ -357,19 +361,25 @@ class AdherentManager(BaseUserManager):
"""
Manager pour la création d'adhérent
"""
def create_user(self, pseudo, password=None, **kwargs):
def create(self, *args, **kwargs):
"""
Crée un utilisateur dans la base de données.
"""
return self.create_user(*args, **kwargs)
def create_user(self, pseudo=None, password=None, **kwargs):
"""
Crée un utilisateur non privilégié dans la base de données
"""
kwargs['is_superuser'] = False
self.__create(pseudo, password, **kwargs)
return self.__create(pseudo, password, **kwargs)
def create_superuser(self, pseudo, password, **kwargs):
"""
Crée un utilisateur privilégié dans la base de données
"""
kwargs['is_superuser'] = True
self.__create(pseudo, password, **kwargs)
return self.__create(pseudo, password, **kwargs)
def __create(self, pseudo, password, **kwargs):
"""
......@@ -385,15 +395,41 @@ class AdherentManager(BaseUserManager):
)
)
if pseudo is None:
raise ValueError("Un pseudo doit être spécifié")
if password is None:
password = self.make_random_password()
# Construction du dico avec les informations du nouvel adhérent
# On uniformise l'aspect des données
kwargs['pseudo'] = self.model.normalize_username(pseudo)
section = kwargs.pop('section')
if not isinstance(section, Section):
section = Section.objects.get(sigle=section)
new_adherent = self.model(**kwargs)
new_adherent.set_password(password)
new_adherent.save()
# Création d'un adhérent et de tous les objets associés en un bloc
with transaction.atomic():
new_adh = self.model(**kwargs)
new_adh.set_password(password)
new_adh.save()
pseudo, _ = Alias.objects.update_or_create(
defaults={'proprietaire': new_adh},
alias=new_adh.pseudo,
proprietaire=None,
)
Historique.objects.create(alias=pseudo, suivant=new_adh)
Adhesion.objects.create(
adherent=new_adh,
fin=timezone.now() + timedelta(days=365),
section=section,
)
for codename, niveau, meta in ACCREDITATIONS_DEFAUT.get(new_adh.type, []):
perm = Accreditation.objects.create(
droit=Droit.objects.get(codename=codename, niveau=niveau),
adherent=new_adh,
meta=meta,
)
return new_adh
def upload_to_avatars(instance, _):
"""
......@@ -419,11 +455,13 @@ class Adherent(AbstractBaseUser):
- remarque [varchar] -> Remarque sur l'adhérent
- uuid [uuid] -> Identifiant anonyme secondaire.
- supprime [boolean] -> Indique si le compte a été supprimé.
- droits[0-N] [droits*] -> Droits attribués à l'adhérent
===== Couche de compatibilité Django =====
- is_staff [boolean] -> L'adhérent est MA.
- is_active [boolean] -> Le compte est actif
- is_superuser[boolean] -> Super-utilisateur
"""
USERNAME_FIELD = 'pseudo'
REQUIRED_FIELDS = ['nom', 'prenom', 'email', 'sexe', 'type']
......@@ -651,7 +689,7 @@ class Adherent(AbstractBaseUser):
if not hasattr(backend, 'has_perm'):
continue
try:
if backend.has_perm(self, perm, (obj,meta)):
if backend.has_perm(self, perm, (obj, meta)):
return True
except PermissionDenied:
return False
......@@ -737,6 +775,12 @@ class Adherent(AbstractBaseUser):
message = template.render(context=context, request=request)
self.email_user(subject, message, from_email, **kwargs)
def get_absolute_url(self):
"""
Renvoie l'URL associée au profil d'un adhérent
"""
return reverse('comptes:detail', kwargs={'pk': self.id})
# Propriétés
@property
......
......@@ -2,18 +2,11 @@
Sérialiseurs de l'app « Comptes »
"""
from datetime import timedelta
from django.db import transaction
from django.contrib.auth.models import Permission
from django.contrib.auth.password_validation import validate_password
from django.utils import timezone
from django.contrib.auth.password_validation import validate_password as django_validate_password
from rest_framework import serializers
from note_kfet.droits import Acl
from note_kfet.environnement import ACCREDITATIONS_DEFAUT
from note_kfet.mixins import DynamicFieldsMixin
from comptes.models import (
Section, Alias, Historique, Droit, Accreditation, Adhesion, Adherent
)
......@@ -27,14 +20,24 @@ class SectionSerializer(serializers.ModelSerializer):
fields = ['sigle', 'nom']
read_only_fields = ['sigle', 'nom']
class AccreditationSerializer(serializers.ModelSerializer):
class DroitSerializer(serializers.ModelSerializer):
"""
Sérialiseur pour le modèle Droit
"""
class Meta:
model = Droit
fields = ['id', 'description', 'niveau']
read_only_fields = ['id', 'description', 'niveau']
class AccreditationSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
"""
Sérialiseur pour le modèle Accréditation
"""
droit = DroitSerializer(read_only=True)
class Meta:
model = Accreditation
fields = ['id', 'niveau', 'description']
read_only_fields = ['id', 'niveau', 'description']
fields = ['id', 'adherent', 'droit', 'meta']
read_only_fields = ['id', 'adherent', 'droit']
class AdhesionSerializer(serializers.ModelSerializer):
"""
......@@ -68,20 +71,57 @@ class AliasSerializer(serializers.ModelSerializer):
### Sérialiseurs pour le modèle Adhérent
class AdherentCreationSerializer(serializers.ModelSerializer):
class AdherentSerializer(DynamicFieldsMixin, serializers.ModelSerializer):
"""
Sérialiseur pour le modèle Adhérent conçu pour
l'inscription de nouveaux adhérents.
Sérialiseur pour le modèle Adhérent
"""
password = serializers.CharField()
accreditations = AccreditationSerializer(many=True, read_only=True, fields=['droit', 'meta'])
aliases = AliasSerializer(many=True, read_only=True)
adhesions = AdhesionSerializer(many=True, read_only=True)
last_adhesion = AdhesionSerializer(read_only=True)
section = serializers.CharField(max_length=10)
class Meta:
model = Adherent
fields = [
'pseudo', 'nom', 'prenom', 'email', 'password',
'sexe', 'type', 'telephone', 'adresse', 'remunere',
'pbsante', 'remarque', 'section',
'id', 'pseudo', 'nom', 'prenom', 'email', 'sexe',
'type', 'telephone', 'adresse', 'remunere', 'pbsante', 'remarque',
'supprime', 'is_staff', 'is_active', 'aliases', 'adhesions', 'last_adhesion',
'accreditations', 'password', 'section',
]
read_only_fields = ['id']
extra_kwargs = {'password': {'write_only': True}}
def validate_section(self, value):
"""
Vérifie que la section demandée n'est pas fermée
"""
qs = Section.objects.filter(sigle=value)
if not qs.exists():
raise serializers.ValidationError("Cette section n'existe pas")
elif qs.get().ferme:
raise serializers.ValidationError("Cette section est fermée.")
else:
return value
def validate_pseudo(self, value):
"""
Vérifie que le pseudo demandé n'est pas déjà pris
"""
pseudo = Adherent.normalize_username(value)
qs = Alias.objects.filter(alias=pseudo)
if not self.instance and qs.filter(proprietaire__isnull=False).exists():
raise serializers.ValidationError("Le pseudo %s est déjà pris." % pseudo)
elif self.instance and not qs.filter(proprietaire=self.instance).exists():
raise serializers.ValidationError("%s n'est pas l'un de vos aliases" % pseudo)
else:
return pseudo
def validate_password(self, value):
"""
Vérifie que le mot de passe fourni répond aux exigences imposées
"""
django_validate_password(value)
return value
def validate(self, data):
"""
......@@ -89,26 +129,6 @@ class AdherentCreationSerializer(serializers.ModelSerializer):
la section du nouvel adhérent n'est pas fermée et que le
formulaire est complet relativement au type de compte.
"""
pseudo = Adherent.normalize_username(data['pseudo'])
qs = Alias.objects.filter(alias=pseudo)
if qs.filter(proprietaire__isnull=False).exists():
self.add_error(
'pseudo',
serializers.ValidationError(
"Le pseudo %s est déjà pris." % data['pseudo']
),
)
try:
if Section.objects.get(sigle=data['section']).ferme:
self.add_error(
'section',
serializers.ValidationError("Cette section est fermée."),
)
except Section.objects.DoesNotExist:
self.add_error(
'section',
serializers.ValidationError("Cette section n'existe pas"),
)
if data['type'] != Adherent.DEBIT and not data.get('nom', ''):
self.add_error(
'nom',
......@@ -119,68 +139,5 @@ class AdherentCreationSerializer(serializers.ModelSerializer):
'prenom',
serializers.ValidationError("Veuillez rensigner votre prénom."),
)
try:
validate_password(data['password'])
except serializers.ValidationError as e:
self.add_error('password', e)
return data
def create(self, validated_data):
"""
Crée l'adhérent, l'alias allant avec le pseudo, l'historique
associé, l'adhésion qui va avec et met en place les droits de
base pour le type de compte demandé.
Toute l'opération est effectuée de manière atomique dans
la base de données.
"""
with transaction.atomic():
section = validated_data.pop('section')
print(section)
adh = Adherent(**validated_data)
adh.set_password(validated_data['password'])
adh.save()
pseudo, _ = Alias.objects.get_or_create(alias=adh.pseudo)
pseudo.proprietaire = adh
pseudo.save()
Historique.objects.create(alias=pseudo, suivant=adh)
Adhesion.objects.create(
adherent=adh,
fin=timezone.now() + timedelta(days=365),
section=Section.objects.get(sigle=section),
)
for codename, acl, meta in DROITS_DEFAUT.get(adh.type, []):
perm = Permission.objects.get(codename=codename)
Attribution.objects.create(
adherent=adh,
droit=perm,
accreditation=Accreditation.objects.get(droit=perm, niveau=acl),
meta=meta,
)
# TODO : Envoyer un mail de bienvenue à l'adhérent nouvellement inscrit
return adh
class AdherentListSerializer(serializers.ModelSerializer):
"""
Sérialiseur pour le modèle Adhérent.
Permet de lister les différentes instances
"""
aliases = AliasSerializer(many=True, read_only=True)
adhesion = AdhesionSerializer(source='last_adhesion', read_only=True)
class Meta:
model = Adherent
fields = ['id', 'pseudo', 'nom', 'prenom', 'type', 'aliases', 'adhesion']
read_only_fields = fields
class AdherentSerializer(serializers.ModelSerializer):
"""
Sérialiseur pour le modèle Adhérent
"""
accreditations = AccreditationSerializer(many=True, read_only=True)
aliases = AliasSerializer(many=True, read_only=True)
adhesions = AdhesionSerializer(many=True, read_only=True)
class Meta:
model = Adherent
fields = Adherent.CHAMPS_VISIBLES[Acl.TOTAL]
read_only_fields = ['id']
{% extends "base.html" %}
{% from "forms.html" import display_grid %}
{% block app %}Gestion des comptes{% endblock %}
{% block fonction %}Changement du mot de passe du compte n°{{ adherent.id }}{% endblock %}
{% block fonction %}Changement du mot de passe du compte n°{{ object.id }}{% endblock %}
{% block contenu %}
<div class="row">
{{ display_grid(form, csrf_input, submit="Changer") }}
......
......@@ -100,7 +100,7 @@ var adherent_result_generator = [
(result) => result.pseudo,
(result) => result.type.capitalize(),
(result) => extractFromDictList(result.aliases, 'alias').remove(result.pseudo).join(', '),
(result) => extractFromDictList(extractFromDictList([result.adhesion], 'section'), 'nom').join(', '),
(result) => extractFromDictList(extractFromDictList([result.last_adhesion], 'section'), 'nom').join(', '),
];
function adherentLookup(event) {
......
......@@ -14,10 +14,9 @@ from django.shortcuts import redirect, get_object_or_404
from django.core.exceptions import PermissionDenied, ValidationError
from django.views.generic.detail import DetailView
from django.views.generic.edit import FormView, UpdateView, DeleteView
from django.views.generic.edit import FormView, CreateView, UpdateView, DeleteView
from django.contrib import messages
from django.contrib.auth.forms import PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.contrib.auth.views import LoginView, LogoutView, PasswordResetDoneView, PasswordResetCompleteView
from django.contrib.auth.models import Permission
......@@ -42,12 +41,12 @@ from comptes.models import (
Section, Alias, Droit, Accreditation, Adhesion, Adherent,
)
from comptes.forms import (
AdherentInscriptionForm, AdherentDroitForm, AdherentSuppressionForm, AliasAjoutForm
AdherentInscriptionForm, AdherentPasswordChangeForm, AdherentDroitForm,
AdherentSuppressionForm, AliasAjoutForm,
)
from comptes.filters import AdherentRechercheFilter
from comptes.serializers import (
AdherentCreationSerializer, AdherentListSerializer, AdherentSerializer, AliasSerializer,
AccreditationSerializer,
AdherentSerializer, AliasSerializer, AccreditationSerializer,
)
#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
......@@ -82,7 +81,7 @@ class NotePasswordResetCompleteView(NoteMixin, PasswordResetCompleteView):
### Vues concernant le modèle Adhérent
class AdherentInscriptionView(NoteMixin, LoginRequiredMixin, PermissionRequiredMixin, FormView):
class AdherentInscriptionView(NoteMixin, LoginRequiredMixin, PermissionRequiredMixin, CreateView):
"""
Vue pour l'inscription d'un nouvel adhérent dans la base de données.
"""
......@@ -94,7 +93,6 @@ class AdherentInscriptionView(NoteMixin, LoginRequiredMixin, PermissionRequiredM
form_class = AdherentInscriptionForm
def form_valid(self, form):
self.adherent = form.save()
user = self.request.user
if (form.cleaned_data['type'] in [Adherent.CLUB, Adherent.SECTION] and
not user.has_perm("comptes.adherent_inscrire", Acl.ETENDU)):
......@@ -114,9 +112,6 @@ class AdherentInscriptionView(NoteMixin, LoginRequiredMixin, PermissionRequiredM
pass
return super().form_valid(form)
def get_success_url(self, *args, **kwargs):
return reverse_lazy('comptes:detail', kwargs={'pk' : self.adherent.id})
class AdherentDetailView(NoteMixin, LoginRequiredMixin, PermissionRequiredMixin, DetailView):
"""
Vue pour l'affichage des données d'un adhérent
......@@ -202,9 +197,6 @@ class AdherentUpdateView(NoteMixin, LoginRequiredMixin, PermissionRequiredMixin,
del form.fields[key]
return form
def get_success_url(self, *args, **kwargs):
return reverse_lazy('comptes:detail', kwargs={'pk' : self.object.id})
class AdherentDroitChangeView(NoteMixin, LoginRequiredMixin, FormView):
"""
Une vue permettant de modifier les droits d'un adhérent.
......@@ -275,7 +267,7 @@ class AdherentDroitChangeView(NoteMixin, LoginRequiredMixin, FormView):
def get_success_url(self):
return reverse_lazy("comptes:detail", kwargs={'pk' : self.target_user.id})
class AdherentPasswordChangeView(NoteMixin, LoginRequiredMixin, PermissionRequiredMixin, FormView):
class AdherentPasswordChangeView(NoteMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Une vue pour changer le mot de passe d'un utilisateur.
"""
......@@ -284,27 +276,17 @@ class AdherentPasswordChangeView(NoteMixin, LoginRequiredMixin, PermissionRequir
permission_required = D("comptes.adherent_change_pw", Acl.LIMITE)
template_name = "adherent_change_password.html"
form_class = PasswordChangeForm
def get_form(self, form_class=None):
self.adherent = get_object_or_404(Adherent, pk=self.kwargs['pk'])
return self.form_class(self.adherent, **self.get_form_kwargs())
form_class = AdherentPasswordChangeForm
model = Adherent
def form_valid(self, form):
user = self.request.user
if self.adherent == user:
form.save()
messages.success(self.request, "Mot de passe changé avec succès")
elif (self.adherent.type == Adherent.PERSONNE and
user.has_perm("comptes.adherent_change_pw", Acl.BASIQUE)):
form.save()
messages.success(self.request, "Mot de passe changé avec succès")
elif (self.adherent.type in [Adherent.CLUB, Adherent.SECTION] and
user.has_perm("comptes.adherent_change_pw", Acl.ETENDU)):
form.save()
messages.success(self.request, "Mot de passe changé avec succès")
elif user.has_perm("comptes.adherent_change_pw", Acl.TOTAL):
form.save()
if (self.object == user
or user.has_perm("comptes.adherent_change_pw", Acl.TOTAL)
or (self.object.type == Adherent.PERSONNE
and user.has_perm("comptes.adherent_change_pw", Acl.BASIQUE))
or (self.object.type in [Adherent.CLUB, Adherent.SECTION]
and user.has_perm("comptes.adherent_change_pw", Acl.ETENDU))):
messages.success(self.request, "Mot de passe changé avec succès")
else:
messages.error(
......@@ -313,14 +295,6 @@ class AdherentPasswordChangeView(NoteMixin, LoginRequiredMixin, PermissionRequir
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context.update({'adherent' : self.adherent})
return context
def get_success_url(self):
return reverse_lazy('comptes:detail', kwargs={'pk' : self.adherent.id})
class AdherentAvatarView(NoteMixin, LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
"""
Vue pour la modification de l'avatar d'un adhérent.
......@@ -508,6 +482,7 @@ class AdherentViewSet(viewsets.GenericViewSet):
La liste des paramètres utilisables est disponible avec une requête OPTIONS.
"""
user = request.user
fields = ['id', 'pseudo', 'nom', 'prenom', 'type', 'aliases', 'last_adhesion']
if not user.has_perm("comptes.adherent_chercher", Acl.BASIQUE):
raise PermissionDenied
......@@ -620,7 +595,9 @@ class AdherentViewSet(viewsets.GenericViewSet):
)
try:
if qs.exists():
return Response(AdherentListSerializer(qs, many=True).data)
return Response(
self.get_serializer(qs, many=True, read_only=True, fields=fields).data
)
else:
return Response(
{'detail' : 'Aucun résultat'},
......@@ -639,23 +616,22 @@ class AdherentViewSet(viewsets.GenericViewSet):
- être envoyées via un requête POST
- ne contenir que des champs indiqués en réponse à une requête OPTIONS
"""
if not request.user.has_perm("comptes.adherent_inscrire", Acl.BASIQUE):
raise PermissionDenied
fields = [
'pseudo', 'nom', 'prenom', 'email', 'password', 'sexe', 'type',
'telephone', 'adresse', 'remunere', 'pbsante', 'remarque', 'section',
]
serializer = AdherentCreationSerializer(data=request.data)
serializer = self.get_serializer(data=request.data, fields=fields)
if not serializer.is_valid():