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 %}
+
+
+
+ {% if form.non_field_errors %}
+
+ {% endif %}
+
+
+ {% if backends.backends|length %}
+
+
{% 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 %}
+
+
+
+ {% for name in backends.backends %}
+ {% if name not in providers %}
+ -
+
+
+ {% endif %}
+ {% endfor %}
+
+
+
+ {% 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" %}
+
{% 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"