Merge branch 'Aufinal/bds_create_user' into 'master'

Création d'utilisateurs pour le BDS

See merge request klub-dev-ens/gestioCOF!433
This commit is contained in:
Martin Pepin 2020-07-27 21:41:12 +02:00
commit ae64f09869
9 changed files with 275 additions and 44 deletions

View file

@ -1,5 +1,8 @@
from urllib.parse import urlencode
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.models import Q from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from shared import autocomplete from shared import autocomplete
@ -21,7 +24,7 @@ class BDSMemberSearch(autocomplete.ModelSearch):
return user.username return user.username
def result_link(self, user): def result_link(self, user):
return "#TODO" return reverse("bds:user.update", args=(user.pk,))
class BDSOthersSearch(autocomplete.ModelSearch): class BDSOthersSearch(autocomplete.ModelSearch):
@ -38,12 +41,15 @@ class BDSOthersSearch(autocomplete.ModelSearch):
return user.username return user.username
def result_link(self, user): def result_link(self, user):
return "#TODO" return reverse("bds:user.update", args=(user.pk,))
class BDSLDAPSearch(autocomplete.LDAPSearch): class BDSLDAPSearch(autocomplete.LDAPSearch):
def result_link(self, clipper): def result_link(self, clipper):
return "#TODO" url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,))
get = {"fullname": clipper.fullname}
return "{}?{}".format(url, urlencode(get))
class BDSSearch(autocomplete.Compose): class BDSSearch(autocomplete.Compose):

View file

@ -1,5 +1,6 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm
from bds.models import BDSProfile from bds.models import BDSProfile
@ -12,7 +13,24 @@ class UserForm(forms.ModelForm):
fields = ["email", "first_name", "last_name"] fields = ["email", "first_name", "last_name"]
class UserFromClipperForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["username"].disabled = True
class Meta:
model = User
fields = ["username", "email", "first_name", "last_name"]
class UserFromScratchForm(UserCreationForm):
class Meta:
model = User
fields = ["username", "email", "first_name", "last_name"]
class ProfileForm(forms.ModelForm): class ProfileForm(forms.ModelForm):
class Meta: class Meta:
model = BDSProfile model = BDSProfile
exclude = ["user"] exclude = ["user"]
widgets = {"birthdate": forms.DateInput(attrs={"type": "date"})}

View file

@ -1,5 +1,122 @@
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponseRedirect
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
class StaffRequiredMixin(PermissionRequiredMixin): class StaffRequiredMixin(PermissionRequiredMixin):
permission_required = "bds.is_team" permission_required = "bds.is_team"
class MultipleFormMixin(ContextMixin):
""" Mixin pour gérer plusieurs formulaires dans la même vue.
Le fonctionnement est relativement identique à celui de
FormMixin, dont la documentation est disponible ici :
https://docs.djangoproject.com/en/3.0/ref/class-based-views/mixins-editing/
Les principales différences sont :
- au lieu de form_class, il faut donner comme attribut un dict de la forme
{<form_name>: <form_class>}, avec tous les formulaires à instancier. On
peut aussi redéfinir `get_form_classes`
- les données initiales se récupèrent pour chaque form via l'attribut
`<form_name>_initial` ou la fonction `get_<form_name>_initial`. De même,
si certaines forms sont des `ModelForm`s, on peut définir la fonction
`get_<form_name>_instance`.
- chaque form a un préfixe rajouté, par défaut <form_name>, mais qui peut
être customisé via `prefixes` ou `get_prefixes`.
"""
form_classes = {}
prefixes = {}
initial = {}
success_url = None
def get_form_classes(self):
return self.form_classes
def get_initial(self, form_name):
initial_attr = "%s_initial" % form_name
initial_method = "get_%s_initial" % form_name
initial_method = getattr(self, initial_method, None)
if hasattr(self, initial_attr):
return getattr(self, initial_attr)
elif callable(initial_method):
return initial_method()
else:
return self.initial.copy()
def get_prefix(self, form_name):
return self.prefixes.get(form_name, form_name)
def get_instance(self, form_name):
# Au cas où certaines des forms soient des ModelForms
instance_method = "get_%s_instance" % form_name
instance_method = getattr(self, instance_method, None)
if callable(instance_method):
return instance_method()
else:
return None
def get_form_kwargs(self, form_name):
kwargs = {
"initial": self.get_initial(form_name),
"prefix": self.get_prefix(form_name),
"instance": self.get_instance(form_name),
}
if self.request.method in ("POST", "PUT"):
kwargs.update({"data": self.request.POST, "files": self.request.FILES})
return kwargs
def get_forms(self):
form_classes = self.get_form_classes()
return {
form_name: form_class(**self.get_form_kwargs(form_name))
for form_name, form_class in form_classes.items()
}
def get_success_url(self):
if not self.success_url:
raise ImproperlyConfigured("No URL to redirect to. Provide a success_url.")
return str(self.success_url)
def form_valid(self, forms):
# on garde le nom form_valid pour l'interface avec SuccessMessageMixin
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, forms):
"""If the form is invalid, render the invalid form."""
return self.render_to_response(self.get_context_data(forms=forms))
class ProcessMultipleFormView(View):
""" Équivalent de `ProcessFormView` pour plusieurs forms.
Note : il faut que *tous* les formulaires soient valides pour
qu'ils soient sauvegardés !
"""
def get(self, request, *args, **kwargs):
forms = self.get_forms()
return self.render_to_response(self.get_context_data(forms=forms))
def post(self, request, *args, **kwargs):
forms = self.get_forms()
if all(form.is_valid() for form in forms.values()):
return self.form_valid(forms)
else:
return self.form_invalid(forms)
class BaseMultipleFormView(MultipleFormMixin, ProcessMultipleFormView):
pass
class MultipleFormView(TemplateResponseMixin, BaseMultipleFormView):
pass

View file

@ -62,6 +62,8 @@ class BDSProfile(models.Model):
null=True, null=True,
) )
is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False)
mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False) mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False)
has_certificate = models.BooleanField(_("certificat médical"), default=False) has_certificate = models.BooleanField(_("certificat médical"), default=False)
@ -77,8 +79,6 @@ class BDSProfile(models.Model):
FFSU_number = models.CharField( FFSU_number = models.CharField(
_("numéro FFSU"), max_length=50, blank=True, null=True _("numéro FFSU"), max_length=50, blank=True, null=True
) )
is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False)
cotisation_period = models.CharField( cotisation_period = models.CharField(
_("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3 _("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3
) )

View file

@ -14,7 +14,7 @@
{% endif %} {% endif %}
</li> </li>
<li class="autocomplete-new"> <li class="autocomplete-new">
<a class="autocomplete-item" href="#TODO"> <a class="autocomplete-item" href="{% url "bds:user.create" %}">
{% trans "Créer un compte" %} {% trans "Créer un compte" %}
</a> </a>
</li> </li>

View file

@ -0,0 +1,33 @@
{% extends "bds/base.html" %}
{% load i18n %}
{% block content %}
{% for form in forms.values %}
{% for error in form.non_field_errors %}
<div class="notification is-danger">
{{ error }}
</div>
{% endfor %}
{% endfor %}
<h1 class="title">{% trans "Création d'un profil" %}</h1>
<div class="container">
<form action="" method="post">
{% csrf_token %}
{% for form in forms.values %}
{% include "bds/forms/form.html" with form=form errors=False %}
{% endfor %}
<div class="field">
<p class="control">
<input class="button is-fullwidth" type="submit" value="Enregistrer">
</p>
</div>
</form>
</div>
{% endblock %}

View file

@ -4,21 +4,23 @@
{% block content %} {% block content %}
{% for error in user_form.non_field_errors %} {% for form in forms.values %}
<p class="error">{{ error }}</p> {% for error in form.non_field_errors %}
{% endfor %} <div class="notification is-danger">
{% for error in profile_form.non_field_errors %} {{ error }}
<p class="error">{{ error }}</p> </div>
{% endfor %}
{% endfor %} {% endfor %}
<h1 class="title">{% trans "Modification de l'utilisateur " %}{{user_form.instance.username}}</h1> <h1 class="title">{% trans "Modification du profil " %}{{ view.user.username }}</h1>
<div class="container"> <div class="container">
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
{% include "bds/forms/form.html" with form=user_form errors=False %} {% for form in forms.values %}
{% include "bds/forms/form.html" with form=profile_form errors=False %} {% include "bds/forms/form.html" with form=form errors=False %}
{% endfor %}
<div class="field"> <div class="field">
<p class="control"> <p class="control">

View file

@ -7,4 +7,10 @@ urlpatterns = [
path("", views.Home.as_view(), name="home"), path("", views.Home.as_view(), name="home"),
path("autocomplete", views.BDSAutocompleteView.as_view(), name="autocomplete"), path("autocomplete", views.BDSAutocompleteView.as_view(), name="autocomplete"),
path("user/update/<int:pk>", views.UserUpdateView.as_view(), name="user.update"), path("user/update/<int:pk>", views.UserUpdateView.as_view(), name="user.update"),
path("user/create/", views.UserCreateView.as_view(), name="user.create"),
path(
"user/create/<slug:clipper>",
views.UserCreateView.as_view(),
name="user.create.fromclipper",
),
] ]

View file

@ -1,12 +1,13 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from bds.autocomplete import bds_search from bds.autocomplete import bds_search
from bds.forms import ProfileForm, UserForm from bds.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm
from bds.mixins import StaffRequiredMixin from bds.mixins import MultipleFormView, StaffRequiredMixin
from shared.views import AutocompleteView from shared.views import AutocompleteView
User = get_user_model() User = get_user_model()
@ -21,42 +22,90 @@ class Home(StaffRequiredMixin, TemplateView):
template_name = "bds/home.html" template_name = "bds/home.html"
class UserUpdateView(StaffRequiredMixin, TemplateView): class UserUpdateView(StaffRequiredMixin, MultipleFormView):
template_name = "bds/user_update.html" template_name = "bds/user_update.html"
def get_user(self): form_classes = {
return get_object_or_404(User, pk=self.kwargs["pk"]) "user": UserForm,
"profile": ProfileForm,
}
def get_user_form(self, data=None): def dispatch(self, request, *args, **kwargs):
return UserForm(prefix="u", instance=self.user, data=data) self.user = get_object_or_404(User, pk=self.kwargs["pk"])
return super().dispatch(request, *args, **kwargs)
def get_profile_form(self, data=None): def get_user_instance(self):
profile = getattr(self.user, "bds", None) return self.user
return ProfileForm(prefix="p", instance=profile, data=data)
def get_context_data(self, user_form=None, profile_form=None, **kwargs): def get_profile_instance(self):
return { return getattr(self.user, "bds", None)
"user_form": user_form or self.get_user_form(),
"profile_form": profile_form or self.get_profile_form(),
}
def get(self, *args, **kwargs): def get_success_url(self):
self.user = self.get_user() return reverse("bds:user.update", args=(self.user.pk,))
return super().get(*args, **kwargs)
def post(self, request, *args, **kwargs): def form_valid(self, forms):
self.user = self.get_user() user = forms["user"].save()
user_form = self.get_user_form(data=request.POST) profile = forms["profile"].save(commit=False)
profile_form = self.get_profile_form(data=request.POST) profile.user = user
profile.save()
messages.success(self.request, _("Profil mis à jour avec succès !"))
if user_form.is_valid() and profile_form.is_valid(): return super().form_valid(forms)
self.user = user_form.save()
profile = profile_form.save(commit=False)
profile.user = self.user
profile.save()
messages.success(self.request, _("Profil mis à jour avec succès"))
def form_invalid(self, forms):
messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous"))
return super().form_invalid(forms)
class UserCreateView(StaffRequiredMixin, MultipleFormView):
template_name = "bds/user_create.html"
def get_form_classes(self):
profile_class = ProfileForm
if "clipper" in self.kwargs:
user_class = UserFromClipperForm
else: else:
messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous")) user_class = UserFromScratchForm
return self.render_to_response(self.get_context_data(user_form, profile_form)) return {"user": user_class, "profile": profile_class}
def get_user_initial(self):
if "clipper" in self.kwargs:
clipper = self.kwargs["clipper"]
email = "{}@clipper.ens.fr".format(clipper)
fullname = self.request.GET.get("fullname", None)
if fullname:
# Heuristique : le premier mot est le prénom
first_name = fullname.split()[0]
last_name = " ".join(fullname.split()[1:])
else:
first_name = ""
last_name = ""
return {
"username": clipper,
"email": email,
"first_name": first_name,
"last_name": last_name,
}
else:
return {}
def get_success_url(self):
return reverse("bds:user.update", args=(self.user.pk,))
def form_valid(self, forms):
# On redéfinit self.user pour get_success_url
self.user = forms["user"].save()
profile = forms["profile"].save(commit=False)
profile.user = self.user
profile.save()
messages.success(self.request, _("Profil créé avec succès !"))
return super().form_valid(forms)
def form_invalid(self, forms):
messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous"))
return super().form_invalid(forms)