From c1e48579f1d06b78b4d64b753d1a598c9c07d824 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Mon, 6 Jul 2020 23:40:56 +0200 Subject: [PATCH 1/3] BDS: UserUpdateView --- bds/forms.py | 18 ++++++++ bds/static/bds/css/bds.css | 68 +++++++++++++++++++++++++++++- bds/templates/bds/base.html | 12 ++++++ bds/templates/bds/user_update.html | 27 ++++++++++++ bds/urls.py | 1 + bds/views.py | 48 +++++++++++++++++++++ 6 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 bds/forms.py create mode 100644 bds/templates/bds/user_update.html diff --git a/bds/forms.py b/bds/forms.py new file mode 100644 index 00000000..59738e06 --- /dev/null +++ b/bds/forms.py @@ -0,0 +1,18 @@ +from django import forms +from django.contrib.auth import get_user_model + +from bds.models import BDSProfile + +User = get_user_model() + + +class UserForm(forms.ModelForm): + class Meta: + model = User + fields = ["email", "first_name", "last_name"] + + +class ProfileForm(forms.ModelForm): + class Meta: + model = BDSProfile + exclude = ["user"] diff --git a/bds/static/bds/css/bds.css b/bds/static/bds/css/bds.css index fe9b2fa2..5498c7c3 100644 --- a/bds/static/bds/css/bds.css +++ b/bds/static/bds/css/bds.css @@ -26,15 +26,16 @@ nav a, nav a img { height: 100%; } -input[type="text"] { +input[type="text"], input[type="email"] { font-size: 18px; + border: 0; + padding: 5px 5px; } #search_autocomplete { flex: 1; width: 480px; margin: 0; - border: 0; padding: 10px 10px; } @@ -80,3 +81,66 @@ input[type="text"] { .autocomplete-value, .autocomplete-new, .autocomplete-more { background: white; } + +/* --- Forms --- */ + +.form-wrapper { + margin: auto; + margin-top: 1em; + max-width: 900px; + text-align: center; +} + +table, tbody { + width: 100%; +} + +th { + width: 50%; + padding-right: 0.5em; + text-align: right; +} + +td { + width: 50%; + padding-left: 0.5em; + text-align: left; +} + +input[type="submit"] { + font-size: 1.2em; + margin-top: 1em; + width: 300px; + background: #3e2263; + color: white; + border-radius: 0.25rem; + border: solid #3e2263; + padding: 0.2em 0.5em; + cursor: pointer; +} + +input[type="submit"]:hover { + border-color: #e8554e; +} + +/* --- Message styling --- */ + +.error { + background: red; + color: white; + width: 100%; + padding: 0.5em 0; + margin: 0; + font-size: 1.2em; + text-align: center; +} + +.success { + background: green; + color: white; + width: 100%; + padding: 0.5em 0; + margin: 0; + font-size: 1.2em; + text-align: center; +} diff --git a/bds/templates/bds/base.html b/bds/templates/bds/base.html index 0bf34287..bb992b0c 100644 --- a/bds/templates/bds/base.html +++ b/bds/templates/bds/base.html @@ -18,6 +18,18 @@ {% include "bds/nav.html" %} + {% if messages %} + {% for message in messages %} +

+ {% if 'safe' in message.tags %} + {{ message|safe }} + {% else %} + {{ message }} + {% endif %} +

+ {% endfor %} + {% endif %} + {% block content %}{% endblock %} diff --git a/bds/templates/bds/user_update.html b/bds/templates/bds/user_update.html new file mode 100644 index 00000000..e922aa92 --- /dev/null +++ b/bds/templates/bds/user_update.html @@ -0,0 +1,27 @@ +{% extends "bds/base.html" %} +{% load i18n %} + + +{% block content %} + {% for error in user_form.non_field_errors %} +

{{ error }}

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

{{ error }}

+ {% endfor %} + +
+
+ {% csrf_token %} + + + + {{ user_form.as_table }} + {{ profile_form.as_table }} + +
+ + +
+
+{% endblock %} diff --git a/bds/urls.py b/bds/urls.py index b067a18f..8efe41c4 100644 --- a/bds/urls.py +++ b/bds/urls.py @@ -6,4 +6,5 @@ app_name = "bds" 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"), ] diff --git a/bds/views.py b/bds/views.py index c612d3a2..5263efe2 100644 --- a/bds/views.py +++ b/bds/views.py @@ -1,9 +1,16 @@ +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.shortcuts import get_object_or_404 +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 shared.views import AutocompleteView +User = get_user_model() + class BDSAutocompleteView(StaffRequiredMixin, AutocompleteView): template_name = "bds/search_results.html" @@ -12,3 +19,44 @@ class BDSAutocompleteView(StaffRequiredMixin, AutocompleteView): class Home(StaffRequiredMixin, TemplateView): template_name = "bds/home.html" + + +class UserUpdateView(StaffRequiredMixin, TemplateView): + template_name = "bds/user_update.html" + + def get_user(self): + return get_object_or_404(User, pk=self.kwargs["pk"]) + + def get_user_form(self, data=None): + return UserForm(prefix="u", instance=self.user, data=data) + + def get_profile_form(self, data=None): + profile = getattr(self.user, "bds", None) + return ProfileForm(prefix="p", instance=profile, data=data) + + 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(self, *args, **kwargs): + self.user = self.get_user() + return super().get(*args, **kwargs) + + 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) + + 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")) + + else: + messages.error(self.request, _("Veuillez corriger les erreurs ci-dessous")) + + return self.render_to_response(self.get_context_data(user_form, profile_form)) From 5c1e2e9cda172de7c22d4866d2bb2fb8c380daf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 10 Jul 2020 22:33:24 +0200 Subject: [PATCH 2/3] Basic tests for BDS registration views --- bds/mixins.py | 2 +- bds/tests/__init__.py | 0 bds/tests/test_views.py | 58 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 bds/tests/__init__.py create mode 100644 bds/tests/test_views.py diff --git a/bds/mixins.py b/bds/mixins.py index 14fac693..16399704 100644 --- a/bds/mixins.py +++ b/bds/mixins.py @@ -2,4 +2,4 @@ from django.contrib.auth.mixins import PermissionRequiredMixin class StaffRequiredMixin(PermissionRequiredMixin): - permission_required = "bds:is_team" + permission_required = "bds.is_team" diff --git a/bds/tests/__init__.py b/bds/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bds/tests/test_views.py b/bds/tests/test_views.py new file mode 100644 index 00000000..0ad42018 --- /dev/null +++ b/bds/tests/test_views.py @@ -0,0 +1,58 @@ +from unittest import mock + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.test import Client, TestCase +from django.urls import reverse + +User = get_user_model() + + +def give_bds_buro_permissions(user: User) -> None: + perm = Permission.objects.get(content_type__app_label="bds", codename="is_team") + user.user_permissions.add(perm) + + +class TestRegistrationView(TestCase): + @mock.patch("gestioncof.signals.messages") + def test_get_autocomplete(self, mock_messages): + user = User.objects.create_user(username="toto") + url = reverse("bds:autocomplete") + "?q=foo" + client = Client() + + # Anonymous GET + resp = client.get(url) + redirect_url = "/login?next={}".format(url) + self.assertRedirects(resp, redirect_url) + + # Logged-in but unprivileged GET + client.force_login(user) + resp = client.get(url) + self.assertEquals(resp.status_code, 403) + + # Burô user GET + give_bds_buro_permissions(user) + resp = client.get(url) + self.assertEquals(resp.status_code, 200) + + @mock.patch("gestioncof.signals.messages") + def test_get(self, mock_messages): + user = User.objects.create_user(username="toto") + url = reverse("bds:user.update", args=(user.id,)) + print(url) + client = Client() + + # Anonymous GET + resp = client.get(url) + redirect_url = "/login?next={}".format(url) + self.assertRedirects(resp, redirect_url) + + # Logged-in but unprivileged GET + client.force_login(user) + resp = client.get(url) + self.assertEquals(resp.status_code, 403) + + # Burô user GET + give_bds_buro_permissions(user) + resp = client.get(url) + self.assertEquals(resp.status_code, 200) From ac062118410e5cb37ba8ccdfa4499d5d83656e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20P=C3=A9pin?= Date: Fri, 10 Jul 2020 23:00:17 +0200 Subject: [PATCH 3/3] Make bds tests resilient to LOGIN_URL changes --- bds/tests/test_views.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bds/tests/test_views.py b/bds/tests/test_views.py index 0ad42018..8e30adf4 100644 --- a/bds/tests/test_views.py +++ b/bds/tests/test_views.py @@ -1,9 +1,10 @@ from unittest import mock +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.models import Permission from django.test import Client, TestCase -from django.urls import reverse +from django.urls import reverse, reverse_lazy User = get_user_model() @@ -13,6 +14,14 @@ def give_bds_buro_permissions(user: User) -> None: user.user_permissions.add(perm) +def login_url(next=None): + login_url = reverse_lazy(settings.LOGIN_URL) + if next is None: + return login_url + else: + return "{}?next={}".format(login_url, next) + + class TestRegistrationView(TestCase): @mock.patch("gestioncof.signals.messages") def test_get_autocomplete(self, mock_messages): @@ -22,8 +31,7 @@ class TestRegistrationView(TestCase): # Anonymous GET resp = client.get(url) - redirect_url = "/login?next={}".format(url) - self.assertRedirects(resp, redirect_url) + self.assertRedirects(resp, login_url(next=url)) # Logged-in but unprivileged GET client.force_login(user) @@ -44,8 +52,7 @@ class TestRegistrationView(TestCase): # Anonymous GET resp = client.get(url) - redirect_url = "/login?next={}".format(url) - self.assertRedirects(resp, redirect_url) + self.assertRedirects(resp, login_url(next=url)) # Logged-in but unprivileged GET client.force_login(user)