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..ae365000 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 @@ -262,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/" 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; 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..06d04079 --- /dev/null +++ b/umap/templates/auth/user_form.html @@ -0,0 +1,50 @@ +{% extends "umap/content.html" %} +{% load i18n %} +{% block maincontent %} +
+

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

+
+
+
+ {% if form.non_field_errors %} + + {% endif %} +
+ {% csrf_token %} + {{ form }} + +
+
+ {% if backends.backends|length %} +
+

{% 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 %} +

+
+ +
+
+ {% endif %} +
+{% 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..6e6b905e 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,24 @@ 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) + + 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() + + class UserMaps(PaginatorMixin, DetailView): model = User slug_url_kwarg = "identifier"