From b6626093e5ad80fc6da6fa691f661343816e6c55 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 21 Jul 2020 14:47:07 +0200 Subject: [PATCH 01/10] Mixin pour forms multiples --- bds/mixins.py | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/bds/mixins.py b/bds/mixins.py index 16399704..30334e9f 100644 --- a/bds/mixins.py +++ b/bds/mixins.py @@ -1,5 +1,103 @@ 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): 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. + """ + + 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): + 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 From f33416b71206b179a5fc2781ed138e88f285feea Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 21 Jul 2020 14:49:10 +0200 Subject: [PATCH 02/10] Use mixin in UserUpdateView --- bds/templates/bds/user_update.html | 18 +++++----- bds/views.py | 53 +++++++++++++++--------------- 2 files changed, 36 insertions(+), 35 deletions(-) diff --git a/bds/templates/bds/user_update.html b/bds/templates/bds/user_update.html index 663ed91f..0704c9f0 100644 --- a/bds/templates/bds/user_update.html +++ b/bds/templates/bds/user_update.html @@ -4,21 +4,23 @@ {% block content %} -{% for error in user_form.non_field_errors %} -

{{ error }}

-{% endfor %} -{% for error in profile_form.non_field_errors %} -

{{ error }}

+{% for form in forms.values %} + {% for error in form.non_field_errors %} +
+ {{ error }} +
+ {% endfor %} {% endfor %} -

{% trans "Modification de l'utilisateur " %}{{user_form.instance.username}}

+

{% trans "Modification du profil " %}{{ view.user.username }}

{% csrf_token %} - {% include "bds/forms/form.html" with form=user_form errors=False %} - {% include "bds/forms/form.html" with form=profile_form errors=False %} + {% for form in forms.values %} + {% include "bds/forms/form.html" with form=form errors=False %} + {% endfor %}

diff --git a/bds/views.py b/bds/views.py index 5263efe2..67c1f50c 100644 --- a/bds/views.py +++ b/bds/views.py @@ -1,12 +1,13 @@ from django.contrib import messages from django.contrib.auth import get_user_model from django.shortcuts import get_object_or_404 +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView from bds.autocomplete import bds_search from bds.forms import ProfileForm, UserForm -from bds.mixins import StaffRequiredMixin +from bds.mixins import MultipleFormView, StaffRequiredMixin from shared.views import AutocompleteView User = get_user_model() @@ -21,41 +22,39 @@ class Home(StaffRequiredMixin, TemplateView): template_name = "bds/home.html" -class UserUpdateView(StaffRequiredMixin, TemplateView): +class UserUpdateView(StaffRequiredMixin, MultipleFormView): template_name = "bds/user_update.html" - def get_user(self): - return get_object_or_404(User, pk=self.kwargs["pk"]) + form_classes = { + "user": UserForm, + "profile": ProfileForm, + } - def get_user_form(self, data=None): - return UserForm(prefix="u", instance=self.user, data=data) + def dispatch(self, request, *args, **kwargs): + self.user = get_object_or_404(User, pk=self.kwargs["pk"]) + return super().dispatch(request, *args, **kwargs) - def get_profile_form(self, data=None): - profile = getattr(self.user, "bds", None) - return ProfileForm(prefix="p", instance=profile, data=data) + def get_user_instance(self): + return self.user - def get_context_data(self, user_form=None, profile_form=None, **kwargs): - return { - "user_form": user_form or self.get_user_form(), - "profile_form": profile_form or self.get_profile_form(), - } + def get_profile_instance(self): + return getattr(self.user, "bds", None) - def get(self, *args, **kwargs): - self.user = self.get_user() - return super().get(*args, **kwargs) + def get_success_url(self): + return reverse("bds:user.update", args=(self.user.pk,)) - def post(self, request, *args, **kwargs): - self.user = self.get_user() - user_form = self.get_user_form(data=request.POST) - profile_form = self.get_profile_form(data=request.POST) + def form_valid(self, forms): + user = forms["user"].save() + profile = forms["profile"].save(commit=False) + 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(): - 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")) + 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) else: messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous")) From 5e5b224f89028d0099a67c711c3bd4e9981be3e1 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 21 Jul 2020 14:49:37 +0200 Subject: [PATCH 03/10] User creation views --- bds/forms.py | 17 +++++++++ bds/templates/bds/user_create.html | 33 +++++++++++++++++ bds/views.py | 58 +++++++++++++++++++++++++++--- 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 bds/templates/bds/user_create.html diff --git a/bds/forms.py b/bds/forms.py index 59738e06..e54f17d4 100644 --- a/bds/forms.py +++ b/bds/forms.py @@ -1,5 +1,6 @@ from django import forms from django.contrib.auth import get_user_model +from django.contrib.auth.forms import UserCreationForm from bds.models import BDSProfile @@ -12,6 +13,22 @@ class UserForm(forms.ModelForm): 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 Meta: model = BDSProfile diff --git a/bds/templates/bds/user_create.html b/bds/templates/bds/user_create.html new file mode 100644 index 00000000..25d97e79 --- /dev/null +++ b/bds/templates/bds/user_create.html @@ -0,0 +1,33 @@ +{% extends "bds/base_layout.html" %} +{% load i18n %} + + +{% block content %} + +{% for form in forms.values %} + {% for error in form.non_field_errors %} +

+ {{ error }} +
+ {% endfor %} +{% endfor %} + +

{% trans "Création d'un profil" %}

+ +
+ + {% csrf_token %} + + {% for form in forms.values %} + {% include "bds/forms/form.html" with form=form errors=False %} + {% endfor %} + +
+

+ +

+
+ +
+ +{% endblock %} diff --git a/bds/views.py b/bds/views.py index 67c1f50c..47651458 100644 --- a/bds/views.py +++ b/bds/views.py @@ -6,7 +6,7 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView from bds.autocomplete import bds_search -from bds.forms import ProfileForm, UserForm +from bds.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm from bds.mixins import MultipleFormView, StaffRequiredMixin from shared.views import AutocompleteView @@ -55,7 +55,57 @@ class UserUpdateView(StaffRequiredMixin, MultipleFormView): def form_invalid(self, forms): messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous")) return super().form_invalid(forms) - else: - messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous")) - return self.render_to_response(self.get_context_data(user_form, profile_form)) + +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: + user_class = UserFromScratchForm + + 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 dict( + 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) From ee1d158f2d445492bdbc37d755304b746b10240f Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 21 Jul 2020 14:50:00 +0200 Subject: [PATCH 04/10] Plug into autocomplete and urls --- bds/autocomplete.py | 13 ++++++++++--- bds/models.py | 4 ++-- bds/urls.py | 6 ++++++ 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/bds/autocomplete.py b/bds/autocomplete.py index 9c9d8e59..27719a7c 100644 --- a/bds/autocomplete.py +++ b/bds/autocomplete.py @@ -1,5 +1,8 @@ +from urllib.parse import urlencode + from django.contrib.auth import get_user_model from django.db.models import Q +from django.urls import reverse from django.utils.translation import gettext_lazy as _ from shared import autocomplete @@ -21,7 +24,7 @@ class BDSMemberSearch(autocomplete.ModelSearch): return user.username def result_link(self, user): - return "#TODO" + return reverse("bds:user.update", args=(user.pk,)) class BDSOthersSearch(autocomplete.ModelSearch): @@ -38,12 +41,16 @@ class BDSOthersSearch(autocomplete.ModelSearch): return user.username def result_link(self, user): - return "#TODO" + return reverse("bds:user.update", args=(user.pk,)) class BDSLDAPSearch(autocomplete.LDAPSearch): def result_link(self, clipper): - return "#TODO" + print(clipper.fullname) + url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,)) + get = {"fullname": clipper.fullname} + + return "{}?{}".format(url, urlencode(get)) class BDSSearch(autocomplete.Compose): diff --git a/bds/models.py b/bds/models.py index 1d0072a6..f597cc94 100644 --- a/bds/models.py +++ b/bds/models.py @@ -62,6 +62,8 @@ class BDSProfile(models.Model): null=True, ) + is_member = models.BooleanField(_("adhérent⋅e du BDS"), default=False) + mails_bds = models.BooleanField(_("recevoir les mails du BDS"), default=False) has_certificate = models.BooleanField(_("certificat médical"), default=False) @@ -77,8 +79,6 @@ class BDSProfile(models.Model): FFSU_number = models.CharField( _("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( _("inscription"), default="NO", choices=COTIZ_DURATION_CHOICES, max_length=3 ) diff --git a/bds/urls.py b/bds/urls.py index 8efe41c4..4ddcbf61 100644 --- a/bds/urls.py +++ b/bds/urls.py @@ -7,4 +7,10 @@ urlpatterns = [ path("", views.Home.as_view(), name="home"), path("autocomplete", views.BDSAutocompleteView.as_view(), name="autocomplete"), path("user/update/", views.UserUpdateView.as_view(), name="user.update"), + path("user/create/", views.UserCreateView.as_view(), name="user.create"), + path( + "user/create/", + views.UserCreateView.as_view(), + name="user.create.fromclipper", + ), ] From 26fa9dc898d2995d57bd32727782517375e94804 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Tue, 21 Jul 2020 15:00:21 +0200 Subject: [PATCH 05/10] Add create user from scratch --- bds/autocomplete.py | 1 - bds/templates/bds/search_results.html | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/bds/autocomplete.py b/bds/autocomplete.py index 27719a7c..b6575fa4 100644 --- a/bds/autocomplete.py +++ b/bds/autocomplete.py @@ -46,7 +46,6 @@ class BDSOthersSearch(autocomplete.ModelSearch): class BDSLDAPSearch(autocomplete.LDAPSearch): def result_link(self, clipper): - print(clipper.fullname) url = reverse("bds:user.create.fromclipper", args=(clipper.clipper,)) get = {"fullname": clipper.fullname} diff --git a/bds/templates/bds/search_results.html b/bds/templates/bds/search_results.html index 4c5471ff..fec629df 100644 --- a/bds/templates/bds/search_results.html +++ b/bds/templates/bds/search_results.html @@ -14,7 +14,7 @@ {% endif %}
  • - + {% trans "Créer un compte" %}
  • From a6c9cf11bd3156d5c5d35a309b4bc8f55c567e8e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 26 Jul 2020 22:02:14 +0200 Subject: [PATCH 06/10] Meilleure doc --- bds/mixins.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/bds/mixins.py b/bds/mixins.py index 30334e9f..473bb0b4 100644 --- a/bds/mixins.py +++ b/bds/mixins.py @@ -11,7 +11,21 @@ class StaffRequiredMixin(PermissionRequiredMixin): class MultipleFormMixin(ContextMixin): """ Mixin pour gérer plusieurs formulaires dans la même vue. Le fonctionnement est relativement identique à celui de - FormMixin. + 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 + {: }, 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 + `_initial` ou la fonction `get__initial`. De même, + si certaines forms sont des `ModelForm`s, on peut définir la fonction + `get__instance`. + + - chaque form a un préfixe rajouté, par défaut , mais qui peut + être customisé via `prefixes` ou `get_prefixes`. """ form_classes = {} From efbb9c2be3e8f8431f9212ae4b5fa32a94c1a9d6 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 26 Jul 2020 22:09:52 +0200 Subject: [PATCH 07/10] Encore plus de doc --- bds/mixins.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bds/mixins.py b/bds/mixins.py index 473bb0b4..4f1f8038 100644 --- a/bds/mixins.py +++ b/bds/mixins.py @@ -97,6 +97,11 @@ class MultipleFormMixin(ContextMixin): 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)) From effed1b5c5fbbb124064d15347a3c82e86d6ab45 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 26 Jul 2020 22:12:38 +0200 Subject: [PATCH 08/10] Fix template --- bds/templates/bds/user_create.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bds/templates/bds/user_create.html b/bds/templates/bds/user_create.html index 25d97e79..a36874cb 100644 --- a/bds/templates/bds/user_create.html +++ b/bds/templates/bds/user_create.html @@ -1,4 +1,4 @@ -{% extends "bds/base_layout.html" %} +{% extends "bds/base.html" %} {% load i18n %} From f990934425a9f97b37a02f0c119b1a2bb204351e Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 26 Jul 2020 22:24:41 +0200 Subject: [PATCH 09/10] On utilise un vrai dict --- bds/views.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bds/views.py b/bds/views.py index 47651458..2226c7fe 100644 --- a/bds/views.py +++ b/bds/views.py @@ -84,12 +84,12 @@ class UserCreateView(StaffRequiredMixin, MultipleFormView): first_name = "" last_name = "" - return dict( - username=clipper, - email=email, - first_name=first_name, - last_name=last_name, - ) + return { + "username": clipper, + "email": email, + "first_name": first_name, + "last_name": last_name, + } else: return {} From 422e2f7b42fec0768cbf96430a6f847a052bd3d7 Mon Sep 17 00:00:00 2001 From: Ludovic Stephan Date: Sun, 26 Jul 2020 22:34:56 +0200 Subject: [PATCH 10/10] Fix date input --- bds/forms.py | 1 + 1 file changed, 1 insertion(+) diff --git a/bds/forms.py b/bds/forms.py index e54f17d4..557f7c83 100644 --- a/bds/forms.py +++ b/bds/forms.py @@ -33,3 +33,4 @@ class ProfileForm(forms.ModelForm): class Meta: model = BDSProfile exclude = ["user"] + widgets = {"birthdate": forms.DateInput(attrs={"type": "date"})}