diff --git a/ephios/core/forms/users.py b/ephios/core/forms/users.py index 6ef4cddb5..ebef19105 100644 --- a/ephios/core/forms/users.py +++ b/ephios/core/forms/users.py @@ -464,3 +464,9 @@ def __init__(self, *args, **kwargs): def update_preferences(self): self.user.preferences["notifications__notifications"] = self.cleaned_data + + +class UserOwnDataForm(ModelForm): + class Meta: + model = UserProfile + fields = ["preferred_language"] diff --git a/ephios/core/migrations/0021_userprofile_preferred_language_and_more.py b/ephios/core/migrations/0021_userprofile_preferred_language_and_more.py new file mode 100644 index 000000000..94ae2979d --- /dev/null +++ b/ephios/core/migrations/0021_userprofile_preferred_language_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.4 on 2023-09-30 13:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0020_qualificationcategory_show_with_user_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="preferred_language", + field=models.CharField( + choices=[("de", "German"), ("en", "English")], + default="de", + max_length=10, + verbose_name="preferred language", + ), + ), + migrations.AlterField( + model_name="qualificationcategory", + name="show_with_user", + field=models.BooleanField( + default=True, + verbose_name="Show qualifications of this category everywhere a user is presented", + ), + ), + ] diff --git a/ephios/core/models/users.py b/ephios/core/models/users.py index 156d4e203..61ba3f8bf 100644 --- a/ephios/core/models/users.py +++ b/ephios/core/models/users.py @@ -7,6 +7,7 @@ from typing import Optional import guardian.mixins +from django.conf import settings from django.contrib.auth import get_user_model from django.contrib.auth.base_user import AbstractBaseUser, BaseUserManager from django.contrib.auth.models import Group, PermissionsMixin @@ -110,6 +111,12 @@ class UserProfile(guardian.mixins.GuardianUserMixin, PermissionsMixin, AbstractB date_of_birth = DateField(_("date of birth"), null=True, blank=False) phone = CharField(_("phone number"), max_length=254, blank=True, null=True) calendar_token = CharField(_("calendar token"), max_length=254, default=secrets.token_urlsafe) + preferred_language = CharField( + _("preferred language"), + max_length=10, + default=settings.LANGUAGE_CODE, + choices=settings.LANGUAGES, + ) USERNAME_FIELD = "email" REQUIRED_FIELDS = [ diff --git a/ephios/core/services/notifications/backends.py b/ephios/core/services/notifications/backends.py index 21a69be46..114eeeec3 100644 --- a/ephios/core/services/notifications/backends.py +++ b/ephios/core/services/notifications/backends.py @@ -9,6 +9,7 @@ from ephios.core.models.users import Notification from ephios.core.services.mail.send import send_mail +from ephios.extra.i18n import language logger = logging.getLogger(__name__) @@ -31,23 +32,24 @@ def send_all_notifications(): for backend in installed_notification_backends(): for notification in Notification.objects.filter(failed=False): if backend.can_send(notification) and backend.user_prefers_sending(notification): - try: - backend.send(notification) - except Exception as e: # pylint: disable=broad-except - if settings.DEBUG: - raise e - notification.failed = True - notification.save() + with language((notification.user and notification.user.preferred_language) or None): try: - mail_admins( - "Notification sending failed", - f"Notification: {notification}\nException: {e}\n{traceback.format_exc()}", + backend.send(notification) + except Exception as e: # pylint: disable=broad-except + if settings.DEBUG: + raise e + notification.failed = True + notification.save() + try: + mail_admins( + "Notification sending failed", + f"Notification: {notification}\nException: {e}\n{traceback.format_exc()}", + ) + except smtplib.SMTPConnectError: + pass # if the mail backend threw this, mail admin will probably throw this as well + logger.warning( + f"Notification sending failed for notification object #{notification.pk} ({notification}) for backend {backend} with {e}" ) - except smtplib.SMTPConnectError: - pass # if the mail backend threw this, mail admin will probably throw this as well - logger.warning( - f"Notification sending failed for notification object #{notification.pk} ({notification}) for backend {backend} with {e}" - ) Notification.objects.filter(failed=False).delete() diff --git a/ephios/core/templates/core/settings/settings_personal_data.html b/ephios/core/templates/core/settings/settings_personal_data.html index 45e1b4c5d..cee7c78f5 100644 --- a/ephios/core/templates/core/settings/settings_personal_data.html +++ b/ephios/core/templates/core/settings/settings_personal_data.html @@ -29,4 +29,10 @@

{% translate "Qualifications" %}

{% translate "You have not been assigned any qualificiations." %} {% endfor %} +

{% translate "Language" %}

+
+ {% csrf_token %} + {{ form|crispy }} + +
{% endblock %} \ No newline at end of file diff --git a/ephios/core/views/settings.py b/ephios/core/views/settings.py index 92bc336be..effeae988 100644 --- a/ephios/core/views/settings.py +++ b/ephios/core/views/settings.py @@ -1,12 +1,14 @@ +from django.conf import settings from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.views import PasswordChangeView from django.contrib.messages.views import SuccessMessageMixin from django.urls import reverse, reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import FormView, TemplateView +from django.views.generic.edit import UpdateView from dynamic_preferences.forms import global_preference_form_builder -from ephios.core.forms.users import UserNotificationPreferenceForm +from ephios.core.forms.users import UserNotificationPreferenceForm, UserOwnDataForm from ephios.core.services.health.healthchecks import run_healthchecks from ephios.core.signals import management_settings_sections from ephios.extra.mixins import StaffRequiredMixin @@ -62,12 +64,18 @@ def get_context_data(self, **kwargs): return super().get_context_data(**kwargs) -class PersonalDataSettingsView(LoginRequiredMixin, TemplateView): +class PersonalDataSettingsView(LoginRequiredMixin, UpdateView): template_name = "core/settings/settings_personal_data.html" + form_class = UserOwnDataForm + success_url = reverse_lazy("core:settings_personal_data") - def get_context_data(self, **kwargs): - kwargs["userprofile"] = self.request.user - return super().get_context_data(**kwargs) + def get_object(self, queryset=None): + return self.request.user + + def form_valid(self, form): + response = super().form_valid(form) + response.set_cookie(settings.LANGUAGE_COOKIE_NAME, form.cleaned_data["preferred_language"]) + return response class CalendarSettingsView(LoginRequiredMixin, TemplateView): diff --git a/ephios/extra/i18n.py b/ephios/extra/i18n.py new file mode 100644 index 000000000..649881d9f --- /dev/null +++ b/ephios/extra/i18n.py @@ -0,0 +1,15 @@ +from contextlib import contextmanager + +from django.conf import settings +from django.utils import translation + + +@contextmanager +def language(lang): + previous_language = translation.get_language() + lang = lang or settings.LANGUAGE_CODE + translation.activate(lang) + try: + yield + finally: + translation.activate(previous_language) diff --git a/ephios/extra/middleware.py b/ephios/extra/middleware.py new file mode 100644 index 000000000..e2a4d8cf3 --- /dev/null +++ b/ephios/extra/middleware.py @@ -0,0 +1,15 @@ +from django.conf import settings + + +class EphiosLocaleMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + response = self.get_response(request) + if settings.LANGUAGE_COOKIE_NAME not in request.COOKIES: + try: + response.set_cookie(settings.LANGUAGE_COOKIE_NAME, request.user.preferred_language) + except (KeyError, AttributeError): + pass + return response diff --git a/ephios/modellogging/json.py b/ephios/modellogging/json.py index 3abe84081..cbdca4a51 100644 --- a/ephios/modellogging/json.py +++ b/ephios/modellogging/json.py @@ -66,7 +66,7 @@ def custom_hook(self, d): return ContentType.objects.get_for_id(d["contenttype_id"]).get_object_for_this_type( pk=d["pk"] ) - except ObjectDoesNotExist: + except (ObjectDoesNotExist, AttributeError): return d["str"] for k, v in d.items(): if isinstance(v, str): diff --git a/ephios/settings.py b/ephios/settings.py index 218f2f901..0bf7e6d23 100644 --- a/ephios/settings.py +++ b/ephios/settings.py @@ -122,6 +122,8 @@ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.locale.LocaleMiddleware", + "ephios.extra.middleware.EphiosLocaleMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", @@ -206,6 +208,10 @@ # https://docs.djangoproject.com/en/3.0/topics/i18n/ LANGUAGE_CODE = "de" +LANGUAGES = [ + ("de", gettext_lazy("German")), + ("en", gettext_lazy("English")), +] TIME_ZONE = "Europe/Berlin"