Allow to customize user display name and URL slug

This commit is contained in:
Yohan Boniface 2023-06-16 14:59:59 +02:00
parent 453a7b5616
commit 81fcc080d9
9 changed files with 64 additions and 24 deletions

View file

@ -82,4 +82,22 @@ And so on!
See also See also
[https://github.com/etalab/cartes.data.gouv.fr](https://github.com/etalab/cartes.data.gouv.fr) [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"

View file

@ -1,6 +1,5 @@
from django.conf import settings from django.conf import settings
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.urls import reverse
from agnocomplete.register import register from agnocomplete.register import register
@ -10,10 +9,9 @@ from agnocomplete.core import AgnocompleteModel
@register @register
class AutocompleteUser(AgnocompleteModel): class AutocompleteUser(AgnocompleteModel):
model = get_user_model() model = get_user_model()
fields = ['^username'] fields = settings.USER_AUTOCOMPLETE_FIELDS
def item(self, current_item): def item(self, current_item):
data = super().item(current_item) data = super().item(current_item)
data['url'] = reverse(settings.USER_MAPS_URL, data['url'] = current_item.get_url()
args=(current_item.get_username(), ))
return data return data

View file

@ -1,6 +1,7 @@
import os import os
import time import time
from django.contrib.auth.models import User
from django.contrib.gis.db import models from django.contrib.gis.db import models
from django.conf import settings from django.conf import settings
from django.urls import reverse from django.urls import reverse
@ -12,6 +13,29 @@ from django.core.files.base import File
from .managers import PublicManager 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): class NamedModel(models.Model):
name = models.CharField(max_length=200, verbose_name=_("name")) name = models.CharField(max_length=200, verbose_name=_("name"))

View file

@ -211,6 +211,11 @@ MIDDLEWARE = (
# Set to True if login into django account should be possible. Default is to # Set to True if login into django account should be possible. Default is to
# only use OAuth flow. # only use OAuth flow.
ENABLE_ACCOUNT_LOGIN = env.bool("ENABLE_ACCOUNT_LOGIN", default=False) 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 # Miscellaneous project settings

View file

@ -9,7 +9,7 @@
window.opener.umap_proceed(); window.opener.umap_proceed();
} else { } else {
// Trade off as Twitter does not allow us to access window.opener // 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 }}'
} }
} }

View file

@ -4,7 +4,7 @@
<hr /> <hr />
<div class="col wide"> <div class="col wide">
{% map_fragment map_inst prefix=prefix page=request.GET.p %} {% map_fragment map_inst prefix=prefix page=request.GET.p %}
<div class="legend"><a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>{% if map_inst.owner %} <em>{% trans "by" %} <a href="{% url 'user_maps' map_inst.owner.username %}">{{ map_inst.owner }}</a></em>{% endif %}</div> <div class="legend"><a href="{{ map_inst.get_absolute_url }}">{{ map_inst.name }}</a>{% if map_inst.owner %} <em>{% trans "by" %} <a href="{{ map_inst.owner.get_url }}">{{ map_inst.owner }}</a></em>{% endif %}</div>
</div> </div>
{% endfor %} {% endfor %}
{% if maps.has_next %} {% if maps.has_next %}

View file

@ -7,8 +7,8 @@
<section> <section>
<ul> <ul>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<li><a href="{% url 'user_maps' user.username %}">{% trans "My maps" %} ({{ user }})</a></li> <li><a href="{{ user.get_url }}">{% trans "My maps" %} ({{ user }})</a></li>
<li><a href="{% url 'user_stars' user.username %}">{% trans "Starred maps" %}</a></li> <li><a href="{{ user.get_stars_url %}">{% trans "Starred maps" %}</a></li>
{% else %} {% else %}
<li><a href="{% url 'login' %}" class="login">{% trans "Log in" %} / {% trans "Sign in" %}</a></li> <li><a href="{% url 'login' %}" class="login">{% trans "Log in" %} / {% trans "Sign in" %}</a></li>
{% endif %} {% endif %}

View file

@ -157,8 +157,8 @@ urlpatterns += i18n_patterns(
), ),
re_path(r"^search/$", views.search, name="search"), re_path(r"^search/$", views.search, name="search"),
re_path(r"^about/$", views.about, name="about"), re_path(r"^about/$", views.about, name="about"),
re_path(r"^user/(?P<username>.+)/stars/$", views.user_stars, name="user_stars"), re_path(r"^user/(?P<identifier>.+)/stars/$", views.user_stars, name="user_stars"),
re_path(r"^user/(?P<username>.+)/$", views.user_maps, name="user_maps"), re_path(r"^user/(?P<identifier>.+)/$", views.user_maps, name="user_maps"),
re_path(r"", include(i18n_urls)), re_path(r"", include(i18n_urls)),
) )
urlpatterns += (path("stats/", cache_page(60 * 60)(views.stats), name="stats"),) urlpatterns += (path("stats/", cache_page(60 * 60)(views.stats), name="stats"),)

View file

@ -154,8 +154,8 @@ about = About.as_view()
class UserMaps(DetailView, PaginatorMixin): class UserMaps(DetailView, PaginatorMixin):
model = User model = User
slug_url_kwarg = "username" slug_url_kwarg = "identifier"
slug_field = "username" slug_field = settings.USER_URL_FIELD
list_template_name = "umap/map_list.html" list_template_name = "umap/map_list.html"
context_object_name = "current_user" context_object_name = "current_user"
@ -250,7 +250,7 @@ class MapsShowCase(View):
description = "{description}\n{by} [[{url}|{name}]]".format( description = "{description}\n{by} [[{url}|{name}]]".format(
description=description, description=description,
by=_("by"), by=_("by"),
url=reverse("user_maps", kwargs={"username": m.owner.username}), url=m.owner.get_url(),
name=m.owner, name=m.owner,
) )
description = "{}\n[[{}|{}]]".format( description = "{}\n[[{}|{}]]".format(
@ -418,8 +418,8 @@ class MapDetailMixin:
if not user.is_anonymous: if not user.is_anonymous:
properties["user"] = { properties["user"] = {
"id": user.pk, "id": user.pk,
"name": user.get_username(), "name": str(user),
"url": reverse(settings.USER_MAPS_URL, args=(user.get_username(),)), "url": user.get_url(),
} }
map_settings = self.get_geojson() map_settings = self.get_geojson()
if "properties" not in map_settings: if "properties" not in map_settings:
@ -465,16 +465,11 @@ class PermissionsMixin:
if self.object.owner: if self.object.owner:
permissions["owner"] = { permissions["owner"] = {
"id": self.object.owner.pk, "id": self.object.owner.pk,
"name": self.object.owner.get_username(), "name": str(self.object.owner),
"url": reverse( "url": self.object.owner.get_url(),
settings.USER_MAPS_URL, args=(self.object.owner.get_username(),)
),
} }
permissions["editors"] = [ permissions["editors"] = [
{ {"id": editor.pk, "name": str(editor)}
"id": editor.pk,
"name": editor.get_username(),
}
for editor in self.object.editors.all() for editor in self.object.editors.all()
] ]
if not self.object.owner and self.object.is_anonymous_owner(self.request): if not self.object.owner and self.object.is_anonymous_owner(self.request):