From 453a7b5616fb3d84add76d2204f925bf25540951 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 16 Jun 2023 14:52:12 +0200 Subject: [PATCH 1/3] Remove unused settings from social auth I cannot find any reference of those settings while looking at social-core code. --- umap/settings/base.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/umap/settings/base.py b/umap/settings/base.py index f4e5f192..17b8d813 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -252,8 +252,6 @@ LEAFLET_ZOOM = env.int('LEAFLET_ZOOM', default=6) COMPRESS_ENABLED = True COMPRESS_OFFLINE = True -SOCIAL_AUTH_DEFAULT_USERNAME = lambda u: slugify(u) -SOCIAL_AUTH_ASSOCIATE_BY_EMAIL = True SOCIAL_AUTH_NO_DEFAULT_PROTECTED_USER_FIELDS = True SOCIAL_AUTH_PROTECTED_USER_FIELDS = ("id", ) LOGIN_URL = "login" From 81fcc080d98ed33c8785f7319a29803ebfcc5891 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Fri, 16 Jun 2023 14:59:59 +0200 Subject: [PATCH 2/3] Allow to customize user display name and URL slug --- docs/custom.md | 20 +++++++++++++++++++- umap/autocomplete.py | 6 ++---- umap/models.py | 24 ++++++++++++++++++++++++ umap/settings/base.py | 5 +++++ umap/templates/umap/login_popup_end.html | 2 +- umap/templates/umap/map_list.html | 2 +- umap/templates/umap/navigation.html | 4 ++-- umap/urls.py | 4 ++-- umap/views.py | 21 ++++++++------------- 9 files changed, 64 insertions(+), 24 deletions(-) diff --git a/docs/custom.md b/docs/custom.md index 60fe9730..50d8204a 100644 --- a/docs/custom.md +++ b/docs/custom.md @@ -82,4 +82,22 @@ And so on! See also [https://github.com/etalab/cartes.data.gouv.fr](https://github.com/etalab/cartes.data.gouv.fr) -for an example of customization. +for an example of theme customization. + + +## Custom user display name + +In some situation, you may want to customize the display name of users, which +is by default the username. + +There are three settings you can play with to control that: + + # The display name itself, could be for example "{first_name} {last_name}" + USER_DISPLAY_NAME = "{username}" + # Which field to search for when autocompleting users (for permissions) + # See https://django-agnocomplete.readthedocs.io/en/latest/autocomplete-definition.html#agnocompletemode + USER_AUTOCOMPLETE_FIELDS = ["^username"] + # Which field to use in the URL, may also be for example "pk" to use the + # primary key and not expose the username (which may be private or may change too + # often for URL persistance) + USER_URL_FIELD = "username" diff --git a/umap/autocomplete.py b/umap/autocomplete.py index 6a485182..860c74f1 100644 --- a/umap/autocomplete.py +++ b/umap/autocomplete.py @@ -1,6 +1,5 @@ from django.conf import settings from django.contrib.auth import get_user_model -from django.urls import reverse from agnocomplete.register import register @@ -10,10 +9,9 @@ from agnocomplete.core import AgnocompleteModel @register class AutocompleteUser(AgnocompleteModel): model = get_user_model() - fields = ['^username'] + fields = settings.USER_AUTOCOMPLETE_FIELDS def item(self, current_item): data = super().item(current_item) - data['url'] = reverse(settings.USER_MAPS_URL, - args=(current_item.get_username(), )) + data['url'] = current_item.get_url() return data diff --git a/umap/models.py b/umap/models.py index fc9b8c40..719a6845 100644 --- a/umap/models.py +++ b/umap/models.py @@ -1,6 +1,7 @@ import os import time +from django.contrib.auth.models import User from django.contrib.gis.db import models from django.conf import settings from django.urls import reverse @@ -12,6 +13,29 @@ from django.core.files.base import File from .managers import PublicManager +# Did not find a clean way to do this in Django +# - creating a Proxy model would mean replacing get_user_model by this proxy model +# in every template +# - extending User model woulc mean a non trivial migration +def display_name(self): + return settings.USER_DISPLAY_NAME.format(**self.__dict__) + + +def get_user_url(self): + identifier = getattr(self, settings.USER_URL_FIELD) + return reverse(settings.USER_MAPS_URL, kwargs={"identifier": identifier}) + + +def get_user_stars_url(self): + identifier = getattr(self, settings.USER_URL_FIELD) + return reverse("user_stars", kwargs={"identifier": identifier}) + + +User.add_to_class("__str__", display_name) +User.add_to_class("get_url", get_user_url) +User.add_to_class("get_stars_url", get_user_stars_url) + + class NamedModel(models.Model): name = models.CharField(max_length=200, verbose_name=_("name")) diff --git a/umap/settings/base.py b/umap/settings/base.py index 17b8d813..e1c009c3 100644 --- a/umap/settings/base.py +++ b/umap/settings/base.py @@ -211,6 +211,11 @@ MIDDLEWARE = ( # Set to True if login into django account should be possible. Default is to # only use OAuth flow. ENABLE_ACCOUNT_LOGIN = env.bool("ENABLE_ACCOUNT_LOGIN", default=False) +USER_DISPLAY_NAME = "{username}" +# For use by Agnocomplete +# See https://django-agnocomplete.readthedocs.io/en/latest/autocomplete-definition.html#agnocompletemode +USER_AUTOCOMPLETE_FIELDS = ["^username"] +USER_URL_FIELD = "username" # ============================================================================= # Miscellaneous project settings diff --git a/umap/templates/umap/login_popup_end.html b/umap/templates/umap/login_popup_end.html index 459d5547..9ea96dfc 100644 --- a/umap/templates/umap/login_popup_end.html +++ b/umap/templates/umap/login_popup_end.html @@ -9,7 +9,7 @@ window.opener.umap_proceed(); } else { // Trade off as Twitter does not allow us to access window.opener - window.location.href = '{% url "user_maps" request.user.username %}' + window.location.href = '{{ request.user.get_url }}' } } diff --git a/umap/templates/umap/map_list.html b/umap/templates/umap/map_list.html index 672aa6a8..280246de 100644 --- a/umap/templates/umap/map_list.html +++ b/umap/templates/umap/map_list.html @@ -4,7 +4,7 @@
{% map_fragment map_inst prefix=prefix page=request.GET.p %} -
{{ map_inst.name }}{% if map_inst.owner %} {% trans "by" %} {{ map_inst.owner }}{% endif %}
+
{{ map_inst.name }}{% if map_inst.owner %} {% trans "by" %} {{ map_inst.owner }}{% endif %}
{% endfor %} {% if maps.has_next %} diff --git a/umap/templates/umap/navigation.html b/umap/templates/umap/navigation.html index 58c4d8e6..139bf789 100644 --- a/umap/templates/umap/navigation.html +++ b/umap/templates/umap/navigation.html @@ -7,8 +7,8 @@
    {% if user.is_authenticated %} -
  • {% trans "My maps" %} ({{ user }})
  • -
  • {% trans "Starred maps" %}
  • +
  • {% trans "My maps" %} ({{ user }})
  • +
  • {% trans "Starred maps" %}
  • {% else %}
  • {% endif %} diff --git a/umap/urls.py b/umap/urls.py index e4d71291..f770425a 100644 --- a/umap/urls.py +++ b/umap/urls.py @@ -157,8 +157,8 @@ urlpatterns += i18n_patterns( ), re_path(r"^search/$", views.search, name="search"), re_path(r"^about/$", views.about, name="about"), - re_path(r"^user/(?P.+)/stars/$", views.user_stars, name="user_stars"), - re_path(r"^user/(?P.+)/$", views.user_maps, name="user_maps"), + re_path(r"^user/(?P.+)/stars/$", views.user_stars, name="user_stars"), + re_path(r"^user/(?P.+)/$", views.user_maps, name="user_maps"), re_path(r"", include(i18n_urls)), ) urlpatterns += (path("stats/", cache_page(60 * 60)(views.stats), name="stats"),) diff --git a/umap/views.py b/umap/views.py index 9485a2b9..35d350b3 100644 --- a/umap/views.py +++ b/umap/views.py @@ -154,8 +154,8 @@ about = About.as_view() class UserMaps(DetailView, PaginatorMixin): model = User - slug_url_kwarg = "username" - slug_field = "username" + slug_url_kwarg = "identifier" + slug_field = settings.USER_URL_FIELD list_template_name = "umap/map_list.html" context_object_name = "current_user" @@ -250,7 +250,7 @@ class MapsShowCase(View): description = "{description}\n{by} [[{url}|{name}]]".format( description=description, by=_("by"), - url=reverse("user_maps", kwargs={"username": m.owner.username}), + url=m.owner.get_url(), name=m.owner, ) description = "{}\n[[{}|{}]]".format( @@ -418,8 +418,8 @@ class MapDetailMixin: if not user.is_anonymous: properties["user"] = { "id": user.pk, - "name": user.get_username(), - "url": reverse(settings.USER_MAPS_URL, args=(user.get_username(),)), + "name": str(user), + "url": user.get_url(), } map_settings = self.get_geojson() if "properties" not in map_settings: @@ -465,16 +465,11 @@ class PermissionsMixin: if self.object.owner: permissions["owner"] = { "id": self.object.owner.pk, - "name": self.object.owner.get_username(), - "url": reverse( - settings.USER_MAPS_URL, args=(self.object.owner.get_username(),) - ), + "name": str(self.object.owner), + "url": self.object.owner.get_url(), } permissions["editors"] = [ - { - "id": editor.pk, - "name": editor.get_username(), - } + {"id": editor.pk, "name": str(editor)} for editor in self.object.editors.all() ] if not self.object.owner and self.object.is_anonymous_owner(self.request): From 02a14a32f47ca45379744854481da73e3f0ee0b1 Mon Sep 17 00:00:00 2001 From: Yohan Boniface Date: Sat, 17 Jun 2023 06:35:21 +0200 Subject: [PATCH 3/3] add tests for user display and slug customization --- umap/tests/test_views.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/umap/tests/test_views.py b/umap/tests/test_views.py index 2ceeee33..dc385917 100644 --- a/umap/tests/test_views.py +++ b/umap/tests/test_views.py @@ -207,3 +207,33 @@ def test_read_only_shows_create_buttons_if_disabled(client, settings): settings.UMAP_READONLY = False response = client.get(reverse("home")) assert "Create a map" in response.content.decode() + + +@pytest.mark.django_db +def test_change_user_display_name(client, user, settings): + username = "MyUserFooName" + first_name = "Ezekiel" + user.username = username + user.first_name = first_name + user.save() + client.login(username=username, password="123123") + response = client.get(reverse("home")) + assert username in response.content.decode() + assert first_name not in response.content.decode() + settings.USER_DISPLAY_NAME = "{first_name}" + response = client.get(reverse("home")) + assert first_name in response.content.decode() + # username will still be in the contant as it's in the "my maps" URL path. + + +@pytest.mark.django_db +def test_change_user_slug(client, user, settings): + username = "MyUserFooName" + user.username = username + user.save() + client.login(username=username, password="123123") + response = client.get(reverse("home")) + assert f"/en/user/{username}/" in response.content.decode() + settings.USER_URL_FIELD = "pk" + response = client.get(reverse("home")) + assert f"/en/user/{user.pk}/" in response.content.decode()