diff --git a/geonode/base/middleware.py b/geonode/base/middleware.py
index e1ac3fdaee8..496d971067a 100644
--- a/geonode/base/middleware.py
+++ b/geonode/base/middleware.py
@@ -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
@@ -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
diff --git a/geonode/people/adapters.py b/geonode/people/adapters.py
index 31ae2557f67..60dbee3a076 100644
--- a/geonode/people/adapters.py
+++ b/geonode/people/adapters.py
@@ -100,6 +100,7 @@ def update_profile(sociallogin):
"profile",
"voice",
"zipcode",
+ "language",
)
for field in profile_fields:
try:
diff --git a/geonode/people/forms/__init__.py b/geonode/people/forms/__init__.py
index cdb51f67823..9e7fefb9e14 100644
--- a/geonode/people/forms/__init__.py
+++ b/geonode/people/forms/__init__.py
@@ -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"}
@@ -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",
)
diff --git a/geonode/people/templates/people/profile_edit.html b/geonode/people/templates/people/profile_edit.html
index 84b772f8027..f02e55e397e 100644
--- a/geonode/people/templates/people/profile_edit.html
+++ b/geonode/people/templates/people/profile_edit.html
@@ -29,7 +29,18 @@
{% trans "Edit Profile for" %} {{ profile.username }}
-
+
{% render_field field class="form-control" %}
diff --git a/geonode/people/test_adapters.py b/geonode/people/test_adapters.py
index c70fea439f6..1f70c4d1f69 100644
--- a/geonode/people/test_adapters.py
+++ b/geonode/people/test_adapters.py
@@ -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
@@ -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)
@@ -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):
diff --git a/geonode/people/tests.py b/geonode/people/tests.py
index 8bea76afbfc..b1aaab1b015 100644
--- a/geonode/people/tests.py
+++ b/geonode/people/tests.py
@@ -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")
diff --git a/geonode/people/views.py b/geonode/people/views.py
index bf9e8f4cf8f..ad01bae0743 100644
--- a/geonode/people/views.py
+++ b/geonode/people/views.py
@@ -22,12 +22,14 @@
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
@@ -35,6 +37,7 @@
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
@@ -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
+
+ 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)
diff --git a/geonode/settings.py b/geonode/settings.py
index 3ba59905ff2..36968b77272 100644
--- a/geonode/settings.py
+++ b/geonode/settings.py
@@ -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"
@@ -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)
+
if os.getenv("LANGUAGES"):
# Map given languages to mapstore supported languages.
LANGUAGES = tuple(
diff --git a/geonode/urls.py b/geonode/urls.py
index a9a73f15605..34a78c5f1d1 100644
--- a/geonode/urls.py
+++ b/geonode/urls.py
@@ -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()
@@ -141,6 +141,7 @@
ImporterViewSet.as_view({"post": "create"}),
name="importer_upload",
),
+ path("i18n/setlang/", set_session_language, name="set_language"),
]
# django-select2 Widgets