diff --git a/bds/autocomplete.py b/bds/autocomplete.py index 9c9d8e59..b6575fa4 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,15 @@ 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" + 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/forms.py b/bds/forms.py index 59738e06..557f7c83 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,7 +13,24 @@ 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 exclude = ["user"] + widgets = {"birthdate": forms.DateInput(attrs={"type": "date"})} diff --git a/bds/mixins.py b/bds/mixins.py index 16399704..4f1f8038 100644 --- a/bds/mixins.py +++ b/bds/mixins.py @@ -1,5 +1,122 @@ 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, 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 = {} + 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 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/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" %}
  • diff --git a/bds/templates/bds/user_create.html b/bds/templates/bds/user_create.html new file mode 100644 index 00000000..a36874cb --- /dev/null +++ b/bds/templates/bds/user_create.html @@ -0,0 +1,33 @@ +{% extends "bds/base.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/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/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", + ), ] diff --git a/bds/views.py b/bds/views.py index 5263efe2..2226c7fe 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.forms import ProfileForm, UserForm, UserFromClipperForm, UserFromScratchForm +from bds.mixins import MultipleFormView, StaffRequiredMixin from shared.views import AutocompleteView User = get_user_model() @@ -21,42 +22,90 @@ 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) + + +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: - 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)