From 2b471be1713473759ffa3d94906f2d15328066fe Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 21 Aug 2023 15:09:30 +0200 Subject: [PATCH 1/5] Very lite "My Profile" page to allow changing username --- umap/forms.py | 7 ++++ umap/settings/base.py | 2 + umap/static/umap/content.css | 7 ++++ umap/templates/auth/user_form.html | 23 ++++++++++++ umap/templates/umap/user_dashboard.html | 4 +- umap/tests/test_views.py | 49 ++++++++++++++++++++++++- umap/urls.py | 5 +++ umap/views.py | 13 +++++++ 8 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 umap/templates/auth/user_form.html diff --git a/umap/forms.py b/umap/forms.py index cff7150b..0a665b5c 100644 --- a/umap/forms.py +++ b/umap/forms.py @@ -89,3 +89,10 @@ class MapSettingsForm(forms.ModelForm): class Meta: fields = ('settings', 'name', 'center', 'slug') model = Map + + +class UserProfileForm(forms.ModelForm): + + class Meta: + model = User + fields = ('username', 'first_name', 'last_name') diff --git a/umap/settings/base.py b/umap/settings/base.py index cb1c2fcf..d8573b49 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -129,6 +129,8 @@ DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" FROM_EMAIL = None +# https://docs.djangoproject.com/en/4.2/releases/4.1/#forms +FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer" # ============================================================================= # Calculation of directories relative to the project module location diff --git a/umap/static/umap/content.css b/umap/static/umap/content.css index 3f51571b..bbee8364 100644 --- a/umap/static/umap/content.css +++ b/umap/static/umap/content.css @@ -133,6 +133,13 @@ h2.section { text-align: center; padding-top: 28px; } +h2.tabs a { + font-weight: normal; + color: #666; +} +h2.tabs a:hover { + text-decoration: underline; +} .showcase-map .map_fragment { height: 400px; } diff --git a/umap/templates/auth/user_form.html b/umap/templates/auth/user_form.html new file mode 100644 index 00000000..a777854e --- /dev/null +++ b/umap/templates/auth/user_form.html @@ -0,0 +1,23 @@ +{% extends "umap/content.html" %} +{% load i18n %} +{% block maincontent %} +
+

+ {% trans "My dashboard" %} | {% trans "My profile" %} +

+
+
+
+ {% if form.non_field_errors %} +
    + {% for error in form.non_field_errors %}
  • {{ error }}
  • {% endfor %} +
+ {% endif %} +
+ {% csrf_token %} + {{ form }} + +
+
+
+{% endblock maincontent %} diff --git a/umap/templates/umap/user_dashboard.html b/umap/templates/umap/user_dashboard.html index e29331ed..c2fd214c 100644 --- a/umap/templates/umap/user_dashboard.html +++ b/umap/templates/umap/user_dashboard.html @@ -6,7 +6,9 @@ {% block maincontent %} {% trans "Search my maps" as placeholder %}
-

{% trans "My dashboard" %}

+

+ {% trans "My dashboard" %} | {% trans "My profile" %} +

{% include "umap/search_bar.html" with action=request.get_full_path placeholder=placeholder %}
diff --git a/umap/tests/test_views.py b/umap/tests/test_views.py index f2d1e78c..493d7cda 100644 --- a/umap/tests/test_views.py +++ b/umap/tests/test_views.py @@ -11,6 +11,8 @@ from django.test import RequestFactory from umap import VERSION from umap.views import validate_url +User = get_user_model() + def get(target="http://osm.org/georss.xml", verb="get", **kwargs): defaults = { @@ -141,7 +143,6 @@ def test_login_contains_form_if_enabled(client, settings): @pytest.mark.django_db def test_can_login_with_username_and_password_if_enabled(client, settings): settings.ENABLE_ACCOUNT_LOGIN = True - User = get_user_model() user = User.objects.create(username="test") user.set_password("test") user.save() @@ -279,3 +280,49 @@ def test_logout_should_return_redirect(client, user, settings): response = client.get(reverse("logout")) assert response.status_code == 302 assert response["Location"] == "/" + + +@pytest.mark.django_db +def test_user_profile_is_restricted_to_logged_in(client): + response = client.get(reverse("user_profile")) + assert response.status_code == 302 + assert response["Location"] == "/en/login/?next=/en/me/profile" + + +@pytest.mark.django_db +def test_user_profile_allows_to_edit_username(client, map): + client.login(username=map.owner.username, password="123123") + new_name = "newname" + response = client.post( + reverse("user_profile"), data={"username": new_name}, follow=True + ) + assert response.status_code == 200 + user = User.objects.get(pk=map.owner.pk) + assert user.username == new_name + + +@pytest.mark.django_db +def test_user_profile_cannot_set_to_existing_username(client, map, user2): + client.login(username=map.owner.username, password="123123") + response = client.post( + reverse("user_profile"), data={"username": user2.username}, follow=True + ) + assert response.status_code == 200 + user = User.objects.get(pk=map.owner.pk) + assert user.username == map.owner.username + assert user.username != user2.username + + +@pytest.mark.django_db +def test_user_profile_does_not_allow_to_edit_other_fields(client, map): + client.login(username=map.owner.username, password="123123") + new_email = "foo@bar.com" + response = client.post( + reverse("user_profile"), + data={"username": new_email, "is_superuser": True}, + follow=True, + ) + assert response.status_code == 200 + user = User.objects.get(pk=map.owner.pk) + assert user.email != new_email + assert user.is_superuser is False diff --git a/umap/urls.py b/umap/urls.py index 10e111dc..ed5dec3c 100644 --- a/umap/urls.py +++ b/umap/urls.py @@ -103,6 +103,11 @@ i18n_urls += decorated_patterns( views.user_dashboard, name="user_dashboard", ), + re_path( + r"^me/profile$", + views.user_profile, + name="user_profile", + ), ) map_urls = [ re_path( diff --git a/umap/views.py b/umap/views.py index d7477cbc..e3acfe63 100644 --- a/umap/views.py +++ b/umap/views.py @@ -50,6 +50,7 @@ from .forms import ( MapSettingsForm, SendLinkForm, UpdateMapPermissionsForm, + UserProfileForm, ) from .models import DataLayer, Licence, Map, Pictogram, Star, TileLayer from .utils import get_uri_template, gzip_file, is_ajax @@ -164,6 +165,18 @@ class About(Home): about = About.as_view() +class UserProfile(UpdateView): + model = User + form_class = UserProfileForm + success_url = reverse_lazy('user_profile') + + def get_object(self): + return self.get_queryset().get(pk=self.request.user.pk) + + +user_profile = UserProfile.as_view() + + class UserMaps(PaginatorMixin, DetailView): model = User slug_url_kwarg = "identifier" From 15e9cf0b5fbb9c8ca941feedd7e23fd97c5e0a8a Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 21 Aug 2023 17:05:01 +0200 Subject: [PATCH 2/5] Protect back username field from being modified by social auth login --- umap/settings/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/umap/settings/base.py b/umap/settings/base.py index d8573b49..ae365000 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -264,8 +264,6 @@ LEAFLET_ZOOM = env.int('LEAFLET_ZOOM', default=6) COMPRESS_ENABLED = True COMPRESS_OFFLINE = True -SOCIAL_AUTH_NO_DEFAULT_PROTECTED_USER_FIELDS = True -SOCIAL_AUTH_PROTECTED_USER_FIELDS = ("id", ) LOGIN_URL = "login" SOCIAL_AUTH_LOGIN_REDIRECT_URL = "/login/popup/end/" From 25b3a6635104da546308ceb1e5f912a567e71b87 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 21 Aug 2023 17:08:57 +0200 Subject: [PATCH 3/5] Allow to add more than one OAuth provider --- umap/templates/auth/user_form.html | 25 +++++++++++++++++++++++++ umap/views.py | 8 +++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/umap/templates/auth/user_form.html b/umap/templates/auth/user_form.html index a777854e..c5401399 100644 --- a/umap/templates/auth/user_form.html +++ b/umap/templates/auth/user_form.html @@ -19,5 +19,30 @@
+
+

{% trans "Your current providers" %}

+ +
+
+

{% trans "Connect to another provider" %}

+

+ {% blocktrans %}It's a good habit to connect your account to more than one provider, in case one provider becomes unavailable, temporarily or even permanently.{% endblocktrans %} +

+
+ +
+
{% endblock maincontent %} diff --git a/umap/views.py b/umap/views.py index e3acfe63..6e6b905e 100644 --- a/umap/views.py +++ b/umap/views.py @@ -168,11 +168,17 @@ about = About.as_view() class UserProfile(UpdateView): model = User form_class = UserProfileForm - success_url = reverse_lazy('user_profile') + success_url = reverse_lazy("user_profile") def get_object(self): return self.get_queryset().get(pk=self.request.user.pk) + def get_context_data(self, **kwargs): + kwargs.update( + {"providers": self.object.social_auth.values_list("provider", flat=True)} + ) + return super().get_context_data(**kwargs) + user_profile = UserProfile.as_view() From 7814702721b13c470215056eaf007ad6255dfffc Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 21 Aug 2023 17:10:12 +0200 Subject: [PATCH 4/5] Lite styling of user profile form --- umap/static/umap/base.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/umap/static/umap/base.css b/umap/static/umap/base.css index 6e3828f7..f1419015 100644 --- a/umap/static/umap/base.css +++ b/umap/static/umap/base.css @@ -194,6 +194,10 @@ select[multiple="multiple"] { font-size: 10px; border-radius: 0 2px; } +.content .helptext { + background-color: #eee; + color: #000; +} input + .help-text { margin-top: -14px; } @@ -214,6 +218,9 @@ label { line-height: 21px; width: 100%; } +.content label { + font-weight: bold; +} input[type="checkbox"] + label { display: inline; padding: 0 14px; From a89e6622eda51af6065f48aa332f88da30da546e Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Mon, 21 Aug 2023 17:16:34 +0200 Subject: [PATCH 5/5] User profile page: only display providers if there are some Someone uses uMap with classic auth username/password, and they don't want those empty HTML elements --- umap/templates/auth/user_form.html | 48 ++++++++++++++++-------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/umap/templates/auth/user_form.html b/umap/templates/auth/user_form.html index c5401399..06d04079 100644 --- a/umap/templates/auth/user_form.html +++ b/umap/templates/auth/user_form.html @@ -19,30 +19,32 @@ -
-

{% trans "Your current providers" %}

-
    - {% for name in providers %}
  • {{ name|title }}
  • {% endfor %} -
-
-
-

{% trans "Connect to another provider" %}

-

- {% blocktrans %}It's a good habit to connect your account to more than one provider, in case one provider becomes unavailable, temporarily or even permanently.{% endblocktrans %} -

-
-
+
+

{% trans "Connect to another provider" %}

+

+ {% blocktrans %}It's a good habit to connect your account to more than one provider, in case one provider becomes unavailable, temporarily or even permanently.{% endblocktrans %} +

+
+ +
+
+ {% endif %}
{% endblock maincontent %}