Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions geonode/base/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
# Geonode functionality

from django.shortcuts import render
from django.utils import translation
from django.utils.deprecation import MiddlewareMixin

from geonode.base.utils import configuration_session_cache


Expand Down Expand Up @@ -95,3 +98,19 @@ def process_view(self, request, view_func, view_args, view_kwargs):
# check if the request is not against whitelisted views (check by URL names)
if request.resolver_match.url_name not in self.WHITELISTED_URL_NAMES:
return render(request, "base/maintenance.html", status=503)


SESSION_LANG_KEY = "language_override"


class ProfileLanguageMiddleware(MiddlewareMixin):
def process_request(self, request):
if not request.user.is_authenticated:
return None

lang = getattr(request.user, "language", None)
if lang:
translation.activate(lang)
request.LANGUAGE_CODE = lang

return None
1 change: 1 addition & 0 deletions geonode/people/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def update_profile(sociallogin):
"profile",
"voice",
"zipcode",
"language",
)
for field in profile_fields:
try:
Expand Down
35 changes: 23 additions & 12 deletions geonode/people/forms/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from django.contrib.auth import get_user_model
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.utils.translation import gettext_lazy as _
from django.conf import settings

# Ported in from django-registration
attrs_dict = {"class": "required"}
Expand All @@ -49,19 +50,29 @@ class ProfileForm(forms.ModelForm):
label=_("Keywords"), required=False, help_text=_("A space or comma-separated list of keywords")
)

language = forms.ChoiceField(
label=_("Language"),
choices=settings.PROFILE_LANGUAGE_CHOICES,
)

class Meta:
model = get_user_model()
exclude = (
"user",
"password",
"last_login",
"groups",
"user_permissions",
"username",
"is_staff",
"is_superuser",
"is_active",
"date_joined",
# Explicitly listing fields instead of exclude them
fields = (
"first_name",
"last_name",
"email",
"organization",
"profile",
"position",
"voice",
"fax",
"delivery",
"city",
"area",
"zipcode",
"country",
"language",
"extra_data",
"keywords",
"timezone",
)
13 changes: 12 additions & 1 deletion geonode/people/templates/people/profile_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,18 @@ <h2 class="page-title">{% trans "Edit Profile for" %} {{ profile.username }}</h2
{% for field in form %}
{% if field.name not in PROFILE_EDIT_EXCLUDE_FIELD %}
<div id="div_{{ field.auto_id }}" class="form-group">
<label for="{{ field.auto_id }}" class="control-label">{{ field.label }}</label>
<label for="{{ field.auto_id }}" class="control-label">
{{ field.label }}
{% if field.name == 'language' %}
<span class="text-info"
data-toggle="tooltip"
data-placement="top"
title="{% trans 'Your preferred interface language. Changing this here or via the top-bar menu will permanently update your profile settings across all devices.' %}">
<i class="glyphicon glyphicon-info-sign"></i>
</span>
{% endif %}

</label>
<div class="">
{% render_field field class="form-control" %}
</div>
Expand Down
11 changes: 11 additions & 0 deletions geonode/people/test_adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def setUp(self):
self.fake_profile = "phony_profile"
self.fake_voice = "phony_voice"
self.fake_zipcode = "phony_zipcode"
self.fake_language = "phony_language"
mock_social_login_class = mock.MagicMock(spec="allauth.socialaccount.models.SocialLogin")
self.mock_social_login = mock_social_login_class.return_value
self.mock_social_login.user = self.fake_user
Expand All @@ -69,6 +70,7 @@ def setUp(self):
self.mock_extractor.extract_profile.return_value = self.fake_profile
self.mock_extractor.extract_voice.return_value = self.fake_voice
self.mock_extractor.extract_zipcode.return_value = self.fake_zipcode
self.mock_extractor.extract_language.return_value = self.fake_language

@mock.patch.object(adapters, "get_data_extractor")
@mock.patch("geonode.people.adapters.user_field", autospec=True)
Expand Down Expand Up @@ -190,6 +192,15 @@ def test_update_profile_covers_zipcode_field(self, mock_user_field, mock_get_ext
expected_call = mock.call(self.fake_user, "zipcode", self.fake_zipcode)
self.assertIn(expected_call, args_list)

@mock.patch.object(adapters, "get_data_extractor")
@mock.patch("geonode.people.adapters.user_field", autospec=True)
def test_update_profile_covers_language_field(self, mock_user_field, mock_get_extractor):
mock_get_extractor.return_value = self.mock_extractor
mock_user_field.return_value = None
adapters.update_profile(self.mock_social_login)
self.mock_extractor.extract_language.assert_called_once_with(self.mock_social_login.account.extra_data)
mock_user_field.assert_any_call(self.fake_user, "language", self.fake_language)


class SiteAllowsSignupTestCase(TestCase):
def setUp(self):
Expand Down
48 changes: 48 additions & 0 deletions geonode/people/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -1322,3 +1322,51 @@ def test_migrate_sha1_passwords(self):
self.assertTrue(user.check_password("password"))
# after checking it should be migrated to default hash
self.assertTrue(user.password.startswith("pbkdf2_sha1"))

def test_language_switcher_sets_session_override(self):
response = self.client.post(
"/i18n/setlang/",
data={"language": "it", "next": "/"},
follow=False,
)
self.assertEqual(response.status_code, 302)
session = self.client.session
self.assertEqual(session["language_override"], "it")

def test_authenticated_user_language_switch_updates_session_and_db(self):
user = get_user_model().objects.get(username="bobby")
user.language = "en"
user.save(update_fields=["language"])

self.client.login(username="bobby", password="bob")

response = self.client.post(
"/i18n/setlang/",
data={"language": "it", "next": "/"},
follow=False,
)

self.assertEqual(response.status_code, 302)
self.assertEqual(self.client.session["language_override"], "it")

user.refresh_from_db()
self.assertEqual(user.language, "it")

def test_logout_and_next_login_keep_updated_db_language(self):
user = get_user_model().objects.get(username="bobby")
user.language = "en"
user.save(update_fields=["language"])

self.client.login(username="bobby", password="bob")

self.client.post("/i18n/setlang/", data={"language": "fr", "next": "/"})

user.refresh_from_db()
self.assertEqual(user.language, "fr")
self.assertEqual(self.client.session.get("language_override"), "fr")

self.client.logout()
self.client.login(username="bobby", password="bob")

user.refresh_from_db()
self.assertEqual(user.language, "fr")
21 changes: 20 additions & 1 deletion geonode/people/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,22 @@
from django.contrib import messages
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.utils.translation import check_for_language, gettext_lazy as _
from django.contrib.sites.models import Site
from django.conf import settings
from django.http import HttpResponseForbidden
from django.db.models import Q
from django.views import View
from django.views.decorators.http import require_POST
from django.views.i18n import set_language as django_set_language

from geonode.tasks.tasks import send_email
from geonode.people.forms import ProfileForm
from geonode.people.utils import get_available_users
from geonode.base.auth import get_or_create_token
from geonode.people.forms import ForgotUsernameForm
from geonode.base.views import user_and_group_permission
from geonode.base.middleware import SESSION_LANG_KEY
from dal import autocomplete


Expand Down Expand Up @@ -167,3 +170,19 @@ def get_queryset(self):
)

return qs


@require_POST
def set_session_language(request):
raw_lang = request.POST.get("language")
lang_code = raw_lang.split("-")[0].lower() if raw_lang else None

if lang_code and check_for_language(lang_code):
request.session[SESSION_LANG_KEY] = lang_code
Comment on lines +180 to +181
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To fully synchronize the UI language selection with the user profile as intended, the selected language should be persisted to the user's profile when they are authenticated. This ensures the preference is preserved across different sessions and devices.

Suggested change
if lang_code and check_for_language(lang_code):
request.session[SESSION_LANG_KEY] = lang_code
if lang_code and check_for_language(lang_code):
request.session[SESSION_LANG_KEY] = lang_code
if request.user.is_authenticated:
request.user.language = lang_code
request.user.save(update_fields=["language"])

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would change the semantics of the topbar switcher from a session-only override to a persistent profile update. So I will keep the current approach.


if request.user.is_authenticated and hasattr(request.user, "language"):
if request.user.language != lang_code:
request.user.language = lang_code
request.user.save(update_fields=["language"])

return django_set_language(request)
4 changes: 4 additions & 0 deletions geonode/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,7 @@
"django_user_agents.middleware.UserAgentMiddleware",
"geonode.base.middleware.MaintenanceMiddleware",
"geonode.base.middleware.ReadOnlyMiddleware", # a Middleware enabling Read Only mode of Geonode
"geonode.base.middleware.ProfileLanguageMiddleware",
)

MESSAGE_STORAGE = "django.contrib.messages.storage.cookie.CookieStorage"
Expand Down Expand Up @@ -1486,6 +1487,9 @@ def get_geonode_catalogue_service():
("it-it", "Italiano"),
)

# This setting includes supported Maptstore language choices in a DB-based format
PROFILE_LANGUAGE_CHOICES = tuple((code.split("-")[0].lower(), label) for code, label in MAPSTORE_DEFAULT_LANGUAGES)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't we use the get_2letters_languages utils with some changes?

Copy link
Copy Markdown
Member Author

@Gpetrak Gpetrak Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattiagiupponi thanks for the comment. Ini my opinion, the idea in this setting is to completely be synced with the Mapstore supported languages. I'm afraid that if we use / modify this method, we will add a bit of complexity and it will not such clear that the source of truth here is the supported Mapstore language.


if os.getenv("LANGUAGES"):
# Map given languages to mapstore supported languages.
LANGUAGES = tuple(
Expand Down
3 changes: 2 additions & 1 deletion geonode/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from geonode import geoserver
from geonode.utils import check_ogc_backend
from geonode.base import register_url_event
from .people.views import CustomSignupView, CustomLoginView
from .people.views import CustomSignupView, CustomLoginView, set_session_language
from oauth2_provider.urls import app_name as oauth2_app_name, base_urlpatterns, oidc_urlpatterns

admin.autodiscover()
Expand Down Expand Up @@ -141,6 +141,7 @@
ImporterViewSet.as_view({"post": "create"}),
name="importer_upload",
),
path("i18n/setlang/", set_session_language, name="set_language"),
]

# django-select2 Widgets
Expand Down
Loading