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