From 069160c75da2fac56d029b5347b8277f42fffe13 Mon Sep 17 00:00:00 2001 From: Daniel Grams Date: Tue, 14 Nov 2023 16:24:03 +0100 Subject: [PATCH] User locale #557 --- doc/development.md | 4 +- messages.pot | 305 ++++++++--------- migrations/versions/182a83a49c1f_.py | 26 ++ project/__init__.py | 6 +- project/api/organization/resources.py | 6 +- project/forms/user.py | 21 +- project/i10n.py | 29 +- project/models/user.py | 1 + project/templates/profile.html | 4 + project/templates/user/general.html | 21 ++ project/templates/user/notifications.html | 7 +- .../translations/de/LC_MESSAGES/messages.mo | Bin 47062 -> 47162 bytes .../translations/de/LC_MESSAGES/messages.po | 311 +++++++++--------- .../translations/en/LC_MESSAGES/messages.mo | Bin 4214 -> 4214 bytes .../translations/en/LC_MESSAGES/messages.po | 308 ++++++++--------- project/utils.py | 4 + project/views/admin.py | 37 ++- project/views/admin_unit.py | 23 +- project/views/admin_unit_member_invitation.py | 5 +- project/views/event.py | 26 +- project/views/event_suggestion.py | 5 +- project/views/reference_request.py | 21 +- project/views/reference_request_review.py | 19 +- project/views/user.py | 27 +- project/views/utils.py | 102 +++++- project/views/verification_request.py | 24 +- project/views/verification_request_review.py | 19 +- project/views/widget.py | 16 +- tests/seeder.py | 2 + tests/test_i10n.py | 2 +- tests/utils.py | 11 +- tests/views/test_admin.py | 3 +- tests/views/test_user.py | 33 +- tests/views/test_utils.py | 7 +- 34 files changed, 828 insertions(+), 607 deletions(-) create mode 100644 migrations/versions/182a83a49c1f_.py create mode 100644 project/templates/user/general.html diff --git a/doc/development.md b/doc/development.md index 0e32335..fe9531b 100644 --- a/doc/development.md +++ b/doc/development.md @@ -62,7 +62,7 @@ flask db upgrade ### Init ```sh -pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel init -i messages.pot -d project/translations -l de +pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -k dummy_gettext -o messages.pot . && pybabel init -i messages.pot -d project/translations -l de ``` ### Add locale @@ -74,7 +74,7 @@ pybabel init -i messages.pot -d project/translations -l en ### Extract new msgid's and merge into \*.po files ```sh -pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot . && pybabel update -N -i messages.pot -d project/translations +pybabel extract -F babel.cfg -o messages.pot . && pybabel extract -F babel.cfg -k lazy_gettext -k dummy_gettext -o messages.pot . && pybabel update -N -i messages.pot -d project/translations ``` #### Compile after translation is done diff --git a/messages.pot b/messages.pot index 2d9ec47..cdef2cc 100644 --- a/messages.pot +++ b/messages.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-09-30 11:43+0200\n" +"POT-Creation-Date: 2023-11-13 17:25+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,183 +17,183 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.12.1\n" -#: project/i10n.py:15 +#: project/i10n.py:32 msgid "Event_Art" msgstr "" -#: project/i10n.py:16 +#: project/i10n.py:33 msgid "Event_Book" msgstr "" -#: project/i10n.py:17 +#: project/i10n.py:34 msgid "Event_Movie" msgstr "" -#: project/i10n.py:18 +#: project/i10n.py:35 msgid "Event_Family" msgstr "" -#: project/i10n.py:19 +#: project/i10n.py:36 msgid "Event_Festival" msgstr "" -#: project/i10n.py:20 +#: project/i10n.py:37 msgid "Event_Religious" msgstr "" -#: project/i10n.py:21 +#: project/i10n.py:38 msgid "Event_Shopping" msgstr "" -#: project/i10n.py:22 +#: project/i10n.py:39 msgid "Event_Comedy" msgstr "" -#: project/i10n.py:23 +#: project/i10n.py:40 msgid "Event_Music" msgstr "" -#: project/i10n.py:24 +#: project/i10n.py:41 msgid "Event_Dance" msgstr "" -#: project/i10n.py:25 +#: project/i10n.py:42 msgid "Event_Nightlife" msgstr "" -#: project/i10n.py:26 +#: project/i10n.py:43 msgid "Event_Theater" msgstr "" -#: project/i10n.py:27 +#: project/i10n.py:44 msgid "Event_Dining" msgstr "" -#: project/i10n.py:28 +#: project/i10n.py:45 msgid "Event_Conference" msgstr "" -#: project/i10n.py:29 +#: project/i10n.py:46 msgid "Event_Meetup" msgstr "" -#: project/i10n.py:30 +#: project/i10n.py:47 msgid "Event_Fitness" msgstr "" -#: project/i10n.py:31 +#: project/i10n.py:48 msgid "Event_Sports" msgstr "" -#: project/i10n.py:32 +#: project/i10n.py:49 msgid "Event_Other" msgstr "" -#: project/i10n.py:33 +#: project/i10n.py:50 msgid "Event_Exhibition" msgstr "" -#: project/i10n.py:34 +#: project/i10n.py:51 msgid "Event_Culture" msgstr "" -#: project/i10n.py:35 +#: project/i10n.py:52 msgid "Event_Tour" msgstr "" -#: project/i10n.py:36 +#: project/i10n.py:53 msgid "Event_OpenAir" msgstr "" -#: project/i10n.py:37 +#: project/i10n.py:54 msgid "Event_Stage" msgstr "" -#: project/i10n.py:38 +#: project/i10n.py:55 msgid "Event_Lecture" msgstr "" -#: project/i10n.py:39 +#: project/i10n.py:56 msgid "Typical Age range" msgstr "" -#: project/i10n.py:40 +#: project/i10n.py:57 msgid "Administrator" msgstr "" -#: project/i10n.py:41 +#: project/i10n.py:58 msgid "Event expert" msgstr "" -#: project/i10n.py:42 +#: project/i10n.py:59 msgid "EventReviewStatus.inbox" msgstr "" -#: project/i10n.py:43 +#: project/i10n.py:60 msgid "EventReviewStatus.verified" msgstr "" -#: project/i10n.py:44 +#: project/i10n.py:61 msgid "EventReviewStatus.rejected" msgstr "" -#: project/i10n.py:45 +#: project/i10n.py:62 msgid "Scope_openid" msgstr "" -#: project/i10n.py:46 +#: project/i10n.py:63 msgid "Scope_profile" msgstr "" -#: project/i10n.py:47 +#: project/i10n.py:64 msgid "Scope_user:read" msgstr "" -#: project/i10n.py:48 +#: project/i10n.py:65 msgid "Scope_user:write" msgstr "" -#: project/i10n.py:49 +#: project/i10n.py:66 msgid "Scope_organizer:write" msgstr "" -#: project/i10n.py:50 +#: project/i10n.py:67 msgid "Scope_place:write" msgstr "" -#: project/i10n.py:51 +#: project/i10n.py:68 msgid "Scope_event:write" msgstr "" -#: project/i10n.py:52 +#: project/i10n.py:69 msgid "Scope_eventlist:write" msgstr "" -#: project/i10n.py:53 +#: project/i10n.py:70 msgid "Scope_eventreference:write" msgstr "" -#: project/i10n.py:54 +#: project/i10n.py:71 msgid "Scope_organization:read" msgstr "" -#: project/i10n.py:55 +#: project/i10n.py:72 msgid "Scope_organization:write" msgstr "" -#: project/i10n.py:56 +#: project/i10n.py:73 msgid "Scope_customwidget:write" msgstr "" -#: project/i10n.py:57 +#: project/i10n.py:74 msgid "There must be no self-reference." msgstr "" -#: project/utils.py:15 +#: project/utils.py:19 msgid "Event_" msgstr "" -#: project/utils.py:19 +#: project/utils.py:23 msgid "." msgstr "" @@ -201,11 +201,6 @@ msgstr "" msgid "message" msgstr "" -#: project/api/organization/resources.py:647 -#: project/views/admin_unit_member_invitation.py:89 -msgid "You have received an invitation" -msgstr "" - #: project/forms/admin.py:11 project/templates/layout.html:344 #: project/views/root.py:55 msgid "Terms of service" @@ -237,7 +232,7 @@ msgid "Announcement" msgstr "" #: project/forms/admin.py:18 project/forms/oauth2_client.py:24 -#: project/forms/user.py:13 +#: project/forms/user.py:18 project/forms/user.py:32 msgid "Save" msgstr "" @@ -267,7 +262,7 @@ msgstr "" #: project/forms/admin_unit_member.py:12 project/forms/admin_unit_member.py:25 #: project/forms/admin_unit_member.py:30 project/forms/event.py:112 #: project/forms/event_suggestion.py:38 project/forms/organizer.py:33 -#: project/forms/user.py:18 project/forms/user.py:23 +#: project/forms/user.py:37 project/forms/user.py:42 #: project/templates/_macros.html:246 project/templates/_macros.html:1586 #: project/templates/admin/admin.html:27 project/templates/admin/email.html:4 #: project/templates/admin/email.html:66 project/templates/admin/users.html:19 @@ -475,7 +470,7 @@ msgid "Verification requests postal codes" msgstr "" #: project/forms/admin_unit.py:93 -msgid "Accept verification requests to organizations with these postal codes." +msgid "Limit verification requests to organizations with these postal codes." msgstr "" #: project/forms/admin_unit.py:109 @@ -523,11 +518,11 @@ msgstr "" msgid "Link Color" msgstr "" -#: project/forms/admin_unit.py:165 project/forms/user.py:17 +#: project/forms/admin_unit.py:165 project/forms/user.py:36 msgid "Request deletion" msgstr "" -#: project/forms/admin_unit.py:170 project/forms/user.py:22 +#: project/forms/admin_unit.py:170 project/forms/user.py:41 #: project/templates/admin_unit/cancel_deletion.html:6 #: project/templates/admin_unit/update.html:44 #: project/templates/manage/events.html:49 project/templates/profile.html:13 @@ -1356,13 +1351,21 @@ msgstr "" msgid "Confirm" msgstr "" -#: project/forms/user.py:9 project/templates/admin/admin.html:31 +#: project/forms/user.py:9 +msgid "Language" +msgstr "" + +#: project/forms/user.py:11 +msgid "Default" +msgstr "" + +#: project/forms/user.py:28 project/templates/admin/admin.html:31 #: project/templates/admin/newsletter.html:4 #: project/templates/admin/newsletter.html:93 msgid "Newsletter" msgstr "" -#: project/forms/user.py:10 +#: project/forms/user.py:29 msgid "Information about new features and improvements." msgstr "" @@ -1850,7 +1853,7 @@ msgstr "" #: project/templates/developer/read.html:4 #: project/templates/developer/read.html:8 project/templates/layout.html:361 -#: project/templates/profile.html:45 +#: project/templates/profile.html:49 msgid "Developer" msgstr "" @@ -1864,20 +1867,25 @@ msgstr "" msgid "Delete account" msgstr "" -#: project/templates/profile.html:36 +#: project/templates/profile.html:36 project/templates/user/general.html:4 +#: project/templates/user/general.html:8 +msgid "General" +msgstr "" + +#: project/templates/profile.html:40 #: project/templates/user/notifications.html:4 #: project/templates/user/notifications.html:8 msgid "Notifications" msgstr "" -#: project/templates/profile.html:40 +#: project/templates/profile.html:44 msgid "Applications" msgstr "" #: project/templates/oauth2_client/list.html:4 #: project/templates/oauth2_client/list.html:11 #: project/templates/oauth2_client/read.html:11 -#: project/templates/profile.html:49 +#: project/templates/profile.html:53 msgid "OAuth2 clients" msgstr "" @@ -1912,7 +1920,7 @@ msgstr "" msgid "User" msgstr "" -#: project/templates/admin/email.html:47 project/views/admin.py:165 +#: project/templates/admin/email.html:47 project/views/admin.py:163 msgid "Mail sent successfully" msgstr "" @@ -2297,7 +2305,7 @@ msgstr "" #: project/templates/manage/reference_requests_incoming.html:20 #: project/templates/reference/read.html:22 #: project/templates/reference_request/review_status.html:17 -#: project/views/reference_request_review.py:42 +#: project/views/reference_request_review.py:38 msgid "View event" msgstr "" @@ -2408,7 +2416,7 @@ msgid "Reviews" msgstr "" #: project/templates/manage/verification_requests_incoming.html:19 -#: project/views/verification_request_review.py:36 +#: project/views/verification_request_review.py:32 msgid "View organization" msgstr "" @@ -2537,7 +2545,7 @@ msgstr "" msgid "You do not have an account yet? Not a problem!" msgstr "" -#: project/templates/security/login_user.html:44 project/views/widget.py:122 +#: project/templates/security/login_user.html:44 project/views/widget.py:119 msgid "Register for free" msgstr "" @@ -2614,29 +2622,19 @@ msgid "Organization successfully deleted" msgstr "" #: project/views/admin.py:133 project/views/manage.py:485 -#: project/views/user.py:63 +#: project/views/user.py:64 project/views/user.py:83 msgid "Settings successfully updated" msgstr "" -#: project/views/admin.py:153 -#, python-format -msgid "Test mail from %(site_name)s" -msgstr "" - -#: project/views/admin.py:183 -#, python-format -msgid "Newsletter from %(site_name)s" -msgstr "" - -#: project/views/admin.py:232 +#: project/views/admin.py:233 msgid "User successfully updated" msgstr "" -#: project/views/admin.py:252 +#: project/views/admin.py:253 msgid "Entered email does not match user email" msgstr "" -#: project/views/admin.py:256 +#: project/views/admin.py:257 msgid "User successfully deleted" msgstr "" @@ -2654,14 +2652,6 @@ msgstr "" msgid "AdminUnit successfully updated" msgstr "" -#: project/views/admin_unit.py:271 -msgid "Organization invitation accepted" -msgstr "" - -#: project/views/admin_unit.py:285 -msgid "Organization deletion requested" -msgstr "" - #: project/views/admin_unit_member.py:46 msgid "Member successfully updated" msgstr "" @@ -2682,46 +2672,38 @@ msgstr "" msgid "Invitation successfully declined" msgstr "" -#: project/views/admin_unit_member_invitation.py:94 +#: project/views/admin_unit_member_invitation.py:93 msgid "Invitation successfully sent" msgstr "" -#: project/views/admin_unit_member_invitation.py:117 +#: project/views/admin_unit_member_invitation.py:116 msgid "Entered email does not match invitation email" msgstr "" -#: project/views/admin_unit_member_invitation.py:122 +#: project/views/admin_unit_member_invitation.py:121 msgid "Invitation successfully deleted" msgstr "" -#: project/views/event.py:208 +#: project/views/event.py:209 msgid "Event successfully published" msgstr "" -#: project/views/event.py:210 +#: project/views/event.py:211 msgid "Draft successfully saved" msgstr "" -#: project/views/event.py:212 +#: project/views/event.py:213 msgid "Event successfully planned" msgstr "" -#: project/views/event.py:272 +#: project/views/event.py:273 msgid "Event successfully updated" msgstr "" -#: project/views/event.py:298 +#: project/views/event.py:299 msgid "Event successfully deleted" msgstr "" -#: project/views/event.py:463 -msgid "Referenced event changed" -msgstr "" - -#: project/views/event.py:486 -msgid "New event report" -msgstr "" - #: project/views/event_place.py:34 msgid "Place successfully created" msgstr "" @@ -2742,11 +2724,6 @@ msgstr "" msgid "Event suggestion successfully rejected" msgstr "" -#: project/views/event_suggestion.py:87 -#: project/views/reference_request_review.py:139 -msgid "Event review status updated" -msgstr "" - #: project/views/js.py:33 msgid "Short name is already taken" msgstr "" @@ -2847,68 +2824,114 @@ msgid "" "notified after the other organization reviews the event." msgstr "" -#: project/views/reference_request.py:172 -msgid "New reference request" -msgstr "" - -#: project/views/reference_request.py:181 -msgid "New reference automatically verified" -msgstr "" - -#: project/views/reference_request_review.py:40 +#: project/views/reference_request_review.py:36 msgid "Request already verified" msgstr "" -#: project/views/reference_request_review.py:61 +#: project/views/reference_request_review.py:57 msgid "Reference successfully created" msgstr "" -#: project/views/reference_request_review.py:69 +#: project/views/reference_request_review.py:65 msgid "Request successfully updated" msgstr "" -#: project/views/reference_request_review.py:92 -#: project/views/verification_request_review.py:83 +#: project/views/reference_request_review.py:88 +#: project/views/verification_request_review.py:79 #, python-format msgid "" "If all upcoming reference requests of %(admin_unit_name)s should be " "verified automatically." msgstr "" -#: project/views/user.py:107 +#: project/views/user.py:127 msgid "" "You are administrator of at least one organization. Cancel your " "membership to delete your account." msgstr "" -#: project/views/user.py:114 project/views/user.py:141 +#: project/views/user.py:134 project/views/user.py:161 msgid "Entered email does not match your email" msgstr "" -#: project/views/user.py:160 +#: project/views/utils.py:27 +msgid "New event report" +msgstr "" + +#: project/views/utils.py:28 project/views/utils.py:36 +msgid "You have received an invitation" +msgstr "" + +#: project/views/utils.py:29 +#, python-format +msgid "Newsletter from %(site_name)s" +msgstr "" + +#: project/views/utils.py:30 +msgid "Organization deletion requested" +msgstr "" + +#: project/views/utils.py:33 +msgid "Organization invitation accepted" +msgstr "" + +#: project/views/utils.py:37 +msgid "New reference automatically verified" +msgstr "" + +#: project/views/utils.py:40 +msgid "New reference request" +msgstr "" + +#: project/views/utils.py:41 project/views/utils.py:46 +msgid "Event review status updated" +msgstr "" + +#: project/views/utils.py:44 +msgid "Referenced event changed" +msgstr "" + +#: project/views/utils.py:45 +msgid "New event review" +msgstr "" + +#: project/views/utils.py:47 msgid "User deletion requested" msgstr "" -#: project/views/utils.py:91 +#: project/views/utils.py:48 +#, python-format +msgid "Test mail from %(site_name)s" +msgstr "" + +#: project/views/utils.py:49 +msgid "New verification request" +msgstr "" + +#: project/views/utils.py:50 +msgid "Verification request review status updated" +msgstr "" + +#: project/views/utils.py:122 msgid "" "An entry with the entered values ​​already exists. Duplicate entries are " "not allowed." msgstr "" -#: project/views/utils.py:142 +#: project/views/utils.py:173 #, python-format msgid "Error in the %s field - %s" msgstr "" -#: project/views/utils.py:149 +#: project/views/utils.py:180 msgid "Show" msgstr "" -#: project/views/utils.py:157 +#: project/views/utils.py:188 msgid "You do not have permission for this action" msgstr "" -#: project/views/utils.py:306 +#: project/views/utils.py:382 msgid "" "The invitation was issued to another user. Sign in with the email address" " the invitation was sent to." @@ -2924,37 +2947,25 @@ msgstr "" msgid "Verification request successfully deleted" msgstr "" -#: project/views/verification_request.py:207 -msgid "New verification request" -msgstr "" - -#: project/views/verification_request_review.py:34 +#: project/views/verification_request_review.py:30 msgid "Verification request already verified" msgstr "" -#: project/views/verification_request_review.py:64 +#: project/views/verification_request_review.py:60 msgid "Organization successfully verified" msgstr "" -#: project/views/verification_request_review.py:66 +#: project/views/verification_request_review.py:62 msgid "Verification request successfully updated" msgstr "" -#: project/views/verification_request_review.py:119 -msgid "Verification request review status updated" -msgstr "" - -#: project/views/widget.py:114 +#: project/views/widget.py:111 msgid "Thank you so much! The event is being verified." msgstr "" -#: project/views/widget.py:118 +#: project/views/widget.py:115 msgid "" "For more options and your own calendar of events, you can register for " "free." msgstr "" -#: project/views/widget.py:184 -msgid "New event review" -msgstr "" - diff --git a/migrations/versions/182a83a49c1f_.py b/migrations/versions/182a83a49c1f_.py new file mode 100644 index 0000000..295a04d --- /dev/null +++ b/migrations/versions/182a83a49c1f_.py @@ -0,0 +1,26 @@ +"""empty message + +Revision ID: 182a83a49c1f +Revises: c20d6d7575be +Create Date: 2023-11-13 15:50:05.343979 + +""" +import sqlalchemy as sa +import sqlalchemy_utils +from alembic import op + +from project import dbtypes + +# revision identifiers, used by Alembic. +revision = "182a83a49c1f" +down_revision = "c20d6d7575be" +branch_labels = None +depends_on = None + + +def upgrade(): + op.add_column("user", sa.Column("locale", sa.String(length=255), nullable=True)) + + +def downgrade(): + op.drop_column("user", "locale") diff --git a/project/__init__.py b/project/__init__.py index 90d3d64..fdedffd 100644 --- a/project/__init__.py +++ b/project/__init__.py @@ -142,7 +142,7 @@ sitemap_path = os.path.join(cache_path, sitemap_file) robots_txt_path = os.path.join(cache_path, robots_txt_file) # i18n -from project.i10n import get_locale +from project.i10n import get_locale, get_locale_from_request app.config["BABEL_DEFAULT_LOCALE"] = "de" app.config["BABEL_DEFAULT_TIMEZONE"] = "Europe/Berlin" @@ -238,6 +238,10 @@ def user_registered_sighandler(app, user, confirm_token, confirmation_token, for set_user_accepted_tos(user) db.session.commit() + if not user.locale: + user.locale = get_locale_from_request() + db.session.commit() + # OAuth2 from project.oauth2 import config_oauth diff --git a/project/api/organization/resources.py b/project/api/organization/resources.py index 68ebfd6..174d927 100644 --- a/project/api/organization/resources.py +++ b/project/api/organization/resources.py @@ -1,6 +1,5 @@ from flask import abort, request from flask_apispec import doc, marshal_with, use_kwargs -from flask_babel import gettext from sqlalchemy import and_ from project import db @@ -131,7 +130,7 @@ from project.views.reference_request import ( handle_request_according_to_relation, send_reference_request_mails, ) -from project.views.utils import get_current_admin_unit_for_api, send_mail_async +from project.views.utils import get_current_admin_unit_for_api, send_template_mail_async from project.views.verification_request import send_verification_request_inbox_mails @@ -642,9 +641,8 @@ class OrganizationOrganizationInvitationListResource(BaseResource): db.session.add(invitation) db.session.commit() - send_mail_async( + send_template_mail_async( invitation.email, - gettext("You have received an invitation"), "organization_invitation_notice", invitation=invitation, ) diff --git a/project/forms/user.py b/project/forms/user.py index 5e85688..9707f1b 100644 --- a/project/forms/user.py +++ b/project/forms/user.py @@ -1,9 +1,28 @@ from flask_babel import lazy_gettext from flask_wtf import FlaskForm -from wtforms import BooleanField, EmailField, SubmitField +from wtforms import BooleanField, EmailField, SelectField, SubmitField from wtforms.validators import DataRequired, Optional +class GeneralForm(FlaskForm): + locale = SelectField( + lazy_gettext("Language"), + choices=[ + ("None", lazy_gettext("Default")), + ("de", "Deutsch"), + ("en", "English"), + ], + default="None", + validators=[Optional()], + ) + submit = SubmitField(lazy_gettext("Save")) + + def populate_obj(self, obj): + super().populate_obj(obj) + if obj.locale == "None": + obj.locale = None + + class NotificationForm(FlaskForm): newsletter_enabled = BooleanField( lazy_gettext("Newsletter"), diff --git a/project/i10n.py b/project/i10n.py index 8fe7b1d..9a81f13 100644 --- a/project/i10n.py +++ b/project/i10n.py @@ -1,14 +1,35 @@ from flask import request -from flask_babel import gettext +from flask_babel import Locale, gettext +from flask_security import current_user from project import app def get_locale(): - if not request: - return app.config["LANGUAGES"][0] + try: + if ( + current_user + and current_user.is_authenticated + and current_user.locale + and Locale.parse(current_user.locale) + ): + return current_user.locale + except Exception: + pass - return request.accept_languages.best_match(app.config["LANGUAGES"]) + if not request: + return app.config["BABEL_DEFAULT_LOCALE"] + + return get_locale_from_request() + + +def get_locale_from_request(): + if not request: # pragma: no cover + return None + + return request.accept_languages.best_match( + app.config["LANGUAGES"], app.config["BABEL_DEFAULT_LOCALE"] + ) def print_dynamic_texts(): diff --git a/project/models/user.py b/project/models/user.py index 0da92ed..ab46b67 100644 --- a/project/models/user.py +++ b/project/models/user.py @@ -65,6 +65,7 @@ class User(db.Model, UserMixin): ) created_at = deferred(Column(DateTime, default=datetime.datetime.utcnow)) deletion_requested_at = deferred(Column(DateTime, nullable=True)) + locale = Column(String(255), nullable=True) @property def is_member_of_verified_admin_unit(self): diff --git a/project/templates/profile.html b/project/templates/profile.html index 06fe6d1..2d5866a 100644 --- a/project/templates/profile.html +++ b/project/templates/profile.html @@ -32,6 +32,10 @@

{{ _('Settings') }}

+ + {{ _('General') }} + + {{ _('Notifications') }} diff --git a/project/templates/user/general.html b/project/templates/user/general.html new file mode 100644 index 0000000..8d33f50 --- /dev/null +++ b/project/templates/user/general.html @@ -0,0 +1,21 @@ +{% extends "layout.html" %} +{% from "_macros.html" import render_field_with_errors, render_field %} +{%- block title -%} +{{ _('General') }} +{%- endblock -%} +{% block content %} + + + +
+ {{ form.hidden_tag() }} + {{ render_field_with_errors(form.locale) }} + {{ render_field(form.submit) }} +
+ +{% endblock %} diff --git a/project/templates/user/notifications.html b/project/templates/user/notifications.html index 04fdc24..0e5c828 100644 --- a/project/templates/user/notifications.html +++ b/project/templates/user/notifications.html @@ -5,7 +5,12 @@ {%- endblock -%} {% block content %} -

{{ _('Notifications') }}

+
{{ form.hidden_tag() }} diff --git a/project/translations/de/LC_MESSAGES/messages.mo b/project/translations/de/LC_MESSAGES/messages.mo index 25b886b6ca87019c455c040578fb11d905c20e8f..69d59830a0f3c0241cd374a7b0b45fba1a9ad1d4 100644 GIT binary patch delta 10069 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 delta 9989 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\n" "Language: de\n" @@ -18,183 +18,183 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.12.1\n" -#: project/i10n.py:15 +#: project/i10n.py:32 msgid "Event_Art" msgstr "Kunst" -#: project/i10n.py:16 +#: project/i10n.py:33 msgid "Event_Book" msgstr "Literatur" -#: project/i10n.py:17 +#: project/i10n.py:34 msgid "Event_Movie" msgstr "Film" -#: project/i10n.py:18 +#: project/i10n.py:35 msgid "Event_Family" msgstr "Familie" -#: project/i10n.py:19 +#: project/i10n.py:36 msgid "Event_Festival" msgstr "Festival" -#: project/i10n.py:20 +#: project/i10n.py:37 msgid "Event_Religious" msgstr "Religion" -#: project/i10n.py:21 +#: project/i10n.py:38 msgid "Event_Shopping" msgstr "Shopping" -#: project/i10n.py:22 +#: project/i10n.py:39 msgid "Event_Comedy" msgstr "Comedy" -#: project/i10n.py:23 +#: project/i10n.py:40 msgid "Event_Music" msgstr "Musik" -#: project/i10n.py:24 +#: project/i10n.py:41 msgid "Event_Dance" msgstr "Tanz" -#: project/i10n.py:25 +#: project/i10n.py:42 msgid "Event_Nightlife" msgstr "Party" -#: project/i10n.py:26 +#: project/i10n.py:43 msgid "Event_Theater" msgstr "Theater" -#: project/i10n.py:27 +#: project/i10n.py:44 msgid "Event_Dining" msgstr "Essen" -#: project/i10n.py:28 +#: project/i10n.py:45 msgid "Event_Conference" msgstr "Konferenz" -#: project/i10n.py:29 +#: project/i10n.py:46 msgid "Event_Meetup" msgstr "Networking" -#: project/i10n.py:30 +#: project/i10n.py:47 msgid "Event_Fitness" msgstr "Fitness" -#: project/i10n.py:31 +#: project/i10n.py:48 msgid "Event_Sports" msgstr "Sport" -#: project/i10n.py:32 +#: project/i10n.py:49 msgid "Event_Other" msgstr "Sonstiges" -#: project/i10n.py:33 +#: project/i10n.py:50 msgid "Event_Exhibition" msgstr "Ausstellung" -#: project/i10n.py:34 +#: project/i10n.py:51 msgid "Event_Culture" msgstr "Kultur" -#: project/i10n.py:35 +#: project/i10n.py:52 msgid "Event_Tour" msgstr "Führung" -#: project/i10n.py:36 +#: project/i10n.py:53 msgid "Event_OpenAir" msgstr "Open Air" -#: project/i10n.py:37 +#: project/i10n.py:54 msgid "Event_Stage" msgstr "Bühne" -#: project/i10n.py:38 +#: project/i10n.py:55 msgid "Event_Lecture" msgstr "Vortrag" -#: project/i10n.py:39 +#: project/i10n.py:56 msgid "Typical Age range" msgstr "Typische Altersspanne" -#: project/i10n.py:40 +#: project/i10n.py:57 msgid "Administrator" msgstr "Administrator:in" -#: project/i10n.py:41 +#: project/i10n.py:58 msgid "Event expert" msgstr "Veranstaltungsexpert:in" -#: project/i10n.py:42 +#: project/i10n.py:59 msgid "EventReviewStatus.inbox" msgstr "Ungeprüft" -#: project/i10n.py:43 +#: project/i10n.py:60 msgid "EventReviewStatus.verified" msgstr "Verifiziert" -#: project/i10n.py:44 +#: project/i10n.py:61 msgid "EventReviewStatus.rejected" msgstr "Abgelehnt" -#: project/i10n.py:45 +#: project/i10n.py:62 msgid "Scope_openid" msgstr "Identität verifizieren" -#: project/i10n.py:46 +#: project/i10n.py:63 msgid "Scope_profile" msgstr "Profil-Informationen" -#: project/i10n.py:47 +#: project/i10n.py:64 msgid "Scope_user:read" msgstr "Nutzer-Einstellungen lesen" -#: project/i10n.py:48 +#: project/i10n.py:65 msgid "Scope_user:write" msgstr "Nutzer-Einstellungen anlegen, ändern und löschen" -#: project/i10n.py:49 +#: project/i10n.py:66 msgid "Scope_organizer:write" msgstr "Veranstalter anlegen, ändern und löschen" -#: project/i10n.py:50 +#: project/i10n.py:67 msgid "Scope_place:write" msgstr "Orte anlegen, ändern und löschen" -#: project/i10n.py:51 +#: project/i10n.py:68 msgid "Scope_event:write" msgstr "Veranstaltungen anlegen, ändern und löschen" -#: project/i10n.py:52 +#: project/i10n.py:69 msgid "Scope_eventlist:write" msgstr "Veranstaltungslisten anlegen, ändern und löschen" -#: project/i10n.py:53 +#: project/i10n.py:70 msgid "Scope_eventreference:write" msgstr "Empfehlungen anlegen, ändern und löschen" -#: project/i10n.py:54 +#: project/i10n.py:71 msgid "Scope_organization:read" msgstr "Organisationen lesen" -#: project/i10n.py:55 +#: project/i10n.py:72 msgid "Scope_organization:write" msgstr "Organisationen anlegen, ändern und löschen" -#: project/i10n.py:56 +#: project/i10n.py:73 msgid "Scope_customwidget:write" msgstr "Widgets anlegen, ändern und löschen" -#: project/i10n.py:57 +#: project/i10n.py:74 msgid "There must be no self-reference." msgstr "Es darf keine Selbstreferenz geben." -#: project/utils.py:15 +#: project/utils.py:19 msgid "Event_" msgstr "Event_" -#: project/utils.py:19 +#: project/utils.py:23 msgid "." msgstr "." @@ -202,11 +202,6 @@ msgstr "." msgid "message" msgstr "message" -#: project/api/organization/resources.py:647 -#: project/views/admin_unit_member_invitation.py:89 -msgid "You have received an invitation" -msgstr "Du hast eine Einladung erhalten" - #: project/forms/admin.py:11 project/templates/layout.html:344 #: project/views/root.py:55 msgid "Terms of service" @@ -238,7 +233,7 @@ msgid "Announcement" msgstr "Ankündigung" #: project/forms/admin.py:18 project/forms/oauth2_client.py:24 -#: project/forms/user.py:13 +#: project/forms/user.py:18 project/forms/user.py:32 msgid "Save" msgstr "Speichern" @@ -268,7 +263,7 @@ msgstr "Nutzer löschen" #: project/forms/admin_unit_member.py:12 project/forms/admin_unit_member.py:25 #: project/forms/admin_unit_member.py:30 project/forms/event.py:112 #: project/forms/event_suggestion.py:38 project/forms/organizer.py:33 -#: project/forms/user.py:18 project/forms/user.py:23 +#: project/forms/user.py:37 project/forms/user.py:42 #: project/templates/_macros.html:246 project/templates/_macros.html:1586 #: project/templates/admin/admin.html:27 project/templates/admin/email.html:4 #: project/templates/admin/email.html:66 project/templates/admin/users.html:19 @@ -492,7 +487,9 @@ msgstr "Postleitzahlen für Verifizierungsanfragen" #: project/forms/admin_unit.py:93 msgid "Limit verification requests to organizations with these postal codes." -msgstr "Limitiere Verifizierungsanfragen auf Oranisationen mit diesen Postleitzahlen" +msgstr "" +"Limitiere Verifizierungsanfragen auf Oranisationen mit diesen " +"Postleitzahlen" #: project/forms/admin_unit.py:109 msgid "Verify new organization" @@ -543,11 +540,11 @@ msgstr "Hauptfarbe" msgid "Link Color" msgstr "Linkfarbe" -#: project/forms/admin_unit.py:165 project/forms/user.py:17 +#: project/forms/admin_unit.py:165 project/forms/user.py:36 msgid "Request deletion" msgstr "Löschung beantragen" -#: project/forms/admin_unit.py:170 project/forms/user.py:22 +#: project/forms/admin_unit.py:170 project/forms/user.py:41 #: project/templates/admin_unit/cancel_deletion.html:6 #: project/templates/admin_unit/update.html:44 #: project/templates/manage/events.html:49 project/templates/profile.html:13 @@ -1415,13 +1412,21 @@ msgstr "Ablehnen" msgid "Confirm" msgstr "Bestätigen" -#: project/forms/user.py:9 project/templates/admin/admin.html:31 +#: project/forms/user.py:9 +msgid "Language" +msgstr "Sprache" + +#: project/forms/user.py:11 +msgid "Default" +msgstr "Standard" + +#: project/forms/user.py:28 project/templates/admin/admin.html:31 #: project/templates/admin/newsletter.html:4 #: project/templates/admin/newsletter.html:93 msgid "Newsletter" msgstr "Newsletter" -#: project/forms/user.py:10 +#: project/forms/user.py:29 msgid "Information about new features and improvements." msgstr "Informationen über neue Features und Verbesserungen." @@ -1911,7 +1916,7 @@ msgstr "Organisation wechseln" #: project/templates/developer/read.html:4 #: project/templates/developer/read.html:8 project/templates/layout.html:361 -#: project/templates/profile.html:45 +#: project/templates/profile.html:49 msgid "Developer" msgstr "Entwickler" @@ -1925,20 +1930,25 @@ msgstr "Dein Account ist zur Löschung vorgesehen." msgid "Delete account" msgstr "Account löschen" -#: project/templates/profile.html:36 +#: project/templates/profile.html:36 project/templates/user/general.html:4 +#: project/templates/user/general.html:8 +msgid "General" +msgstr "Allgemein" + +#: project/templates/profile.html:40 #: project/templates/user/notifications.html:4 #: project/templates/user/notifications.html:8 msgid "Notifications" msgstr "Benachrichtigungen" -#: project/templates/profile.html:40 +#: project/templates/profile.html:44 msgid "Applications" msgstr "Apps" #: project/templates/oauth2_client/list.html:4 #: project/templates/oauth2_client/list.html:11 #: project/templates/oauth2_client/read.html:11 -#: project/templates/profile.html:49 +#: project/templates/profile.html:53 msgid "OAuth2 clients" msgstr "OAuth2 Clients" @@ -1973,7 +1983,7 @@ msgstr "Löschen" msgid "User" msgstr "Nutzer" -#: project/templates/admin/email.html:47 project/views/admin.py:165 +#: project/templates/admin/email.html:47 project/views/admin.py:163 msgid "Mail sent successfully" msgstr "Mail erfolgreich gesendet" @@ -2369,7 +2379,7 @@ msgstr "Veranstaltungsvorschlag prüfen" #: project/templates/manage/reference_requests_incoming.html:20 #: project/templates/reference/read.html:22 #: project/templates/reference_request/review_status.html:17 -#: project/views/reference_request_review.py:42 +#: project/views/reference_request_review.py:38 msgid "View event" msgstr "Veranstaltung anzeigen" @@ -2482,7 +2492,7 @@ msgid "Reviews" msgstr "Prüfungen" #: project/templates/manage/verification_requests_incoming.html:19 -#: project/views/verification_request_review.py:36 +#: project/views/verification_request_review.py:32 msgid "View organization" msgstr "Organisation anzeigen" @@ -2508,7 +2518,9 @@ msgstr "" msgid "" "No organizations were found that can verify your organization with postal" " code %(postal_code)s." -msgstr "Es wurden keine Organisationen gefunden, die deine Organisation mit der Postleitzahl %(postal_code)s verifizieren können." +msgstr "" +"Es wurden keine Organisationen gefunden, die deine Organisation mit der " +"Postleitzahl %(postal_code)s verifizieren können." #: project/templates/manage/widgets.html:87 msgid "Link, um Veranstaltungen vorzuschlagen" @@ -2613,7 +2625,7 @@ msgstr "Angemeldet bleiben" msgid "You do not have an account yet? Not a problem!" msgstr "Du hast noch keinen Account? Kein Problem!" -#: project/templates/security/login_user.html:44 project/views/widget.py:122 +#: project/templates/security/login_user.html:44 project/views/widget.py:119 msgid "Register for free" msgstr "Kostenlos registrieren" @@ -2692,29 +2704,19 @@ msgid "Organization successfully deleted" msgstr "Organisation erfolgreich gelöscht" #: project/views/admin.py:133 project/views/manage.py:485 -#: project/views/user.py:63 +#: project/views/user.py:64 project/views/user.py:83 msgid "Settings successfully updated" msgstr "Einstellungen erfolgreich aktualisiert" -#: project/views/admin.py:153 -#, python-format -msgid "Test mail from %(site_name)s" -msgstr "Test-Mail von %(site_name)s" - -#: project/views/admin.py:183 -#, python-format -msgid "Newsletter from %(site_name)s" -msgstr "Newsletter von %(site_name)s" - -#: project/views/admin.py:232 +#: project/views/admin.py:233 msgid "User successfully updated" msgstr "Nutzer erfolgreich aktualisiert" -#: project/views/admin.py:252 +#: project/views/admin.py:253 msgid "Entered email does not match user email" msgstr "Die eingegebene Email passt nicht zur Email des Nutzers" -#: project/views/admin.py:256 +#: project/views/admin.py:257 msgid "User successfully deleted" msgstr "Nutzer erfolgreich gelöscht" @@ -2735,14 +2737,6 @@ msgstr "Organisation erfolgreich erstellt" msgid "AdminUnit successfully updated" msgstr "Organisation erfolgreich aktualisiert" -#: project/views/admin_unit.py:271 -msgid "Organization invitation accepted" -msgstr "Organisationseinladung akzeptiert" - -#: project/views/admin_unit.py:285 -msgid "Organization deletion requested" -msgstr "Löschung der Organisation beantragt" - #: project/views/admin_unit_member.py:46 msgid "Member successfully updated" msgstr "Mitglied erfolgreich aktualisiert" @@ -2763,46 +2757,38 @@ msgstr "Einladung erfolgreich akzeptiert" msgid "Invitation successfully declined" msgstr "Einladung erfolgreich abgelehnt" -#: project/views/admin_unit_member_invitation.py:94 +#: project/views/admin_unit_member_invitation.py:93 msgid "Invitation successfully sent" msgstr "Einladung erfolgreich gesendet" -#: project/views/admin_unit_member_invitation.py:117 +#: project/views/admin_unit_member_invitation.py:116 msgid "Entered email does not match invitation email" msgstr "Die eingegebene Email passt nicht zur Email der Einladung" -#: project/views/admin_unit_member_invitation.py:122 +#: project/views/admin_unit_member_invitation.py:121 msgid "Invitation successfully deleted" msgstr "Einladung erfolgreich gelöscht" -#: project/views/event.py:208 +#: project/views/event.py:209 msgid "Event successfully published" msgstr "Veranstaltung erfolgreich veröffentlicht" -#: project/views/event.py:210 +#: project/views/event.py:211 msgid "Draft successfully saved" msgstr "Entwurf erfolgreich gespeichert" -#: project/views/event.py:212 +#: project/views/event.py:213 msgid "Event successfully planned" msgstr "Veranstaltung erfolgreich geplant" -#: project/views/event.py:272 +#: project/views/event.py:273 msgid "Event successfully updated" msgstr "Veranstaltung erfolgreich aktualisiert" -#: project/views/event.py:298 +#: project/views/event.py:299 msgid "Event successfully deleted" msgstr "Veranstaltung erfolgreich gelöscht" -#: project/views/event.py:463 -msgid "Referenced event changed" -msgstr "Empfohlene Veranstaltung wurde geändert" - -#: project/views/event.py:486 -msgid "New event report" -msgstr "Neue Meldung zu einer Veranstaltung" - #: project/views/event_place.py:34 msgid "Place successfully created" msgstr "Ort erfolgreich erstellt" @@ -2823,11 +2809,6 @@ msgstr "Ort erfolgreich gelöscht" msgid "Event suggestion successfully rejected" msgstr "Veranstaltungsvorschlag erfolgreich abgelehnt" -#: project/views/event_suggestion.py:87 -#: project/views/reference_request_review.py:139 -msgid "Event review status updated" -msgstr "Prüfungsstatus aktualisiert" - #: project/views/js.py:33 msgid "Short name is already taken" msgstr "Der Kurzname ist bereits vergeben" @@ -2933,28 +2914,20 @@ msgstr "" "benachrichtigt, nachdem die andere Organisation die Veranstaltung geprüft" " hat." -#: project/views/reference_request.py:172 -msgid "New reference request" -msgstr "Neue Empfehlungsanfrage" - -#: project/views/reference_request.py:181 -msgid "New reference automatically verified" -msgstr "Neue automatisch verifizierte Empfehlung" - -#: project/views/reference_request_review.py:40 +#: project/views/reference_request_review.py:36 msgid "Request already verified" msgstr "Empfehlungsanfrage ist bereits verifiziert" -#: project/views/reference_request_review.py:61 +#: project/views/reference_request_review.py:57 msgid "Reference successfully created" msgstr "Empfehlung erfolgreich erstellt" -#: project/views/reference_request_review.py:69 +#: project/views/reference_request_review.py:65 msgid "Request successfully updated" msgstr "Empfehlungsanfrage erfolgreich aktualisiert" -#: project/views/reference_request_review.py:92 -#: project/views/verification_request_review.py:83 +#: project/views/reference_request_review.py:88 +#: project/views/verification_request_review.py:79 #, python-format msgid "" "If all upcoming reference requests of %(admin_unit_name)s should be " @@ -2963,7 +2936,7 @@ msgstr "" "Ob alle zukünftigen Empfehlungsanfragen von %(admin_unit_name)s " "automatisch verifiziert werden sollen." -#: project/views/user.py:107 +#: project/views/user.py:127 msgid "" "You are administrator of at least one organization. Cancel your " "membership to delete your account." @@ -2971,15 +2944,69 @@ msgstr "" "Du bist Administrator von mindestens einer Organisation. Beende deine " "Mitgliedschaft, um deinen Account zu löschen." -#: project/views/user.py:114 project/views/user.py:141 +#: project/views/user.py:134 project/views/user.py:161 msgid "Entered email does not match your email" msgstr "Die eingegebene Email entspricht nicht deiner Email" -#: project/views/user.py:160 +#: project/views/utils.py:27 +msgid "New event report" +msgstr "Neue Meldung zu einer Veranstaltung" + +#: project/views/utils.py:28 project/views/utils.py:36 +msgid "You have received an invitation" +msgstr "Du hast eine Einladung erhalten" + +#: project/views/utils.py:29 +#, python-format +msgid "Newsletter from %(site_name)s" +msgstr "Newsletter von %(site_name)s" + +#: project/views/utils.py:30 +msgid "Organization deletion requested" +msgstr "Löschung der Organisation beantragt" + +#: project/views/utils.py:33 +msgid "Organization invitation accepted" +msgstr "Organisationseinladung akzeptiert" + +#: project/views/utils.py:37 +msgid "New reference automatically verified" +msgstr "Neue automatisch verifizierte Empfehlung" + +#: project/views/utils.py:40 +msgid "New reference request" +msgstr "Neue Empfehlungsanfrage" + +#: project/views/utils.py:41 project/views/utils.py:46 +msgid "Event review status updated" +msgstr "Prüfungsstatus aktualisiert" + +#: project/views/utils.py:44 +msgid "Referenced event changed" +msgstr "Empfohlene Veranstaltung wurde geändert" + +#: project/views/utils.py:45 +msgid "New event review" +msgstr "Neue Veranstaltung zu prüfen" + +#: project/views/utils.py:47 msgid "User deletion requested" msgstr "Löschung des Nutzers beantragt" -#: project/views/utils.py:91 +#: project/views/utils.py:48 +#, python-format +msgid "Test mail from %(site_name)s" +msgstr "Test-Mail von %(site_name)s" + +#: project/views/utils.py:49 +msgid "New verification request" +msgstr "Neue Verifizierungsanfrage" + +#: project/views/utils.py:50 +msgid "Verification request review status updated" +msgstr "Prüfungsstatus der Verifizierungsanfrage aktualisiert" + +#: project/views/utils.py:122 msgid "" "An entry with the entered values ​​already exists. Duplicate entries are " "not allowed." @@ -2987,20 +3014,20 @@ msgstr "" "Ein Eintrag mit den eingegebenen Werten existiert bereits. Doppelte " "Einträge sind nicht erlaubt." -#: project/views/utils.py:142 +#: project/views/utils.py:173 #, python-format msgid "Error in the %s field - %s" msgstr "Fehler im Feld %s: %s" -#: project/views/utils.py:149 +#: project/views/utils.py:180 msgid "Show" msgstr "Anzeigen" -#: project/views/utils.py:157 +#: project/views/utils.py:188 msgid "You do not have permission for this action" msgstr "Du hast keine Berechtigung für diese Aktion" -#: project/views/utils.py:306 +#: project/views/utils.py:382 msgid "" "The invitation was issued to another user. Sign in with the email address" " the invitation was sent to." @@ -3020,31 +3047,23 @@ msgstr "" msgid "Verification request successfully deleted" msgstr "Verifizierungsanfrage erfolgreich gelöscht" -#: project/views/verification_request.py:207 -msgid "New verification request" -msgstr "Neue Verifizierungsanfrage" - -#: project/views/verification_request_review.py:34 +#: project/views/verification_request_review.py:30 msgid "Verification request already verified" msgstr "Verifizierungsanfrage ist bereits verifiziert" -#: project/views/verification_request_review.py:64 +#: project/views/verification_request_review.py:60 msgid "Organization successfully verified" msgstr "Organisation erfolgreich verifiziert" -#: project/views/verification_request_review.py:66 +#: project/views/verification_request_review.py:62 msgid "Verification request successfully updated" msgstr "Verifizierungsanfrage erfolgreich aktualisiert" -#: project/views/verification_request_review.py:119 -msgid "Verification request review status updated" -msgstr "Prüfungsstatus der Verifizierungsanfrage aktualisiert" - -#: project/views/widget.py:114 +#: project/views/widget.py:111 msgid "Thank you so much! The event is being verified." msgstr "Vielen Dank! Die Veranstaltung wird geprüft." -#: project/views/widget.py:118 +#: project/views/widget.py:115 msgid "" "For more options and your own calendar of events, you can register for " "free." @@ -3052,10 +3071,6 @@ msgstr "" "Für mehr Optionen und einen eigenen Veranstaltungskalender, kannst du " "dich kostenlos registrieren." -#: project/views/widget.py:184 -msgid "New event review" -msgstr "Neue Veranstaltung zu prüfen" - #~ msgid "" #~ "Indicate when the event will end. " #~ "An event can last a maximum of " diff --git a/project/translations/en/LC_MESSAGES/messages.mo b/project/translations/en/LC_MESSAGES/messages.mo index 9a79561ea0ef12cfb6b63dbc0279a5e2eaa187bc..289dac7a8a951d5f441f582dadaa1be0aa88ed77 100644 GIT binary patch delta 27 icmeyS@J(Su7#E+Rp{}8^f}y#Uk*T(U;pP-BAvOSa)dws9 delta 27 icmeyS@J(Su7#E*`rLM7of}x?6iLthU(dHB`AvOSb1qUqv diff --git a/project/translations/en/LC_MESSAGES/messages.po b/project/translations/en/LC_MESSAGES/messages.po index bba3a7b..869cf65 100644 --- a/project/translations/en/LC_MESSAGES/messages.po +++ b/project/translations/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-09-30 11:43+0200\n" +"POT-Creation-Date: 2023-11-13 17:25+0100\n" "PO-Revision-Date: 2021-04-30 15:04+0200\n" "Last-Translator: FULL NAME \n" "Language: en\n" @@ -18,183 +18,183 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 2.12.1\n" -#: project/i10n.py:15 +#: project/i10n.py:32 msgid "Event_Art" msgstr "Art" -#: project/i10n.py:16 +#: project/i10n.py:33 msgid "Event_Book" msgstr "Book" -#: project/i10n.py:17 +#: project/i10n.py:34 msgid "Event_Movie" msgstr "Movie" -#: project/i10n.py:18 +#: project/i10n.py:35 msgid "Event_Family" msgstr "Family" -#: project/i10n.py:19 +#: project/i10n.py:36 msgid "Event_Festival" msgstr "Festival" -#: project/i10n.py:20 +#: project/i10n.py:37 msgid "Event_Religious" msgstr "Religious" -#: project/i10n.py:21 +#: project/i10n.py:38 msgid "Event_Shopping" msgstr "Shopping" -#: project/i10n.py:22 +#: project/i10n.py:39 msgid "Event_Comedy" msgstr "Comedy" -#: project/i10n.py:23 +#: project/i10n.py:40 msgid "Event_Music" msgstr "Music" -#: project/i10n.py:24 +#: project/i10n.py:41 msgid "Event_Dance" msgstr "Dance" -#: project/i10n.py:25 +#: project/i10n.py:42 msgid "Event_Nightlife" msgstr "Nightlife" -#: project/i10n.py:26 +#: project/i10n.py:43 msgid "Event_Theater" msgstr "Theater" -#: project/i10n.py:27 +#: project/i10n.py:44 msgid "Event_Dining" msgstr "Dining" -#: project/i10n.py:28 +#: project/i10n.py:45 msgid "Event_Conference" msgstr "Conference" -#: project/i10n.py:29 +#: project/i10n.py:46 msgid "Event_Meetup" msgstr "Meetup" -#: project/i10n.py:30 +#: project/i10n.py:47 msgid "Event_Fitness" msgstr "Fitness" -#: project/i10n.py:31 +#: project/i10n.py:48 msgid "Event_Sports" msgstr "Sports" -#: project/i10n.py:32 +#: project/i10n.py:49 msgid "Event_Other" msgstr "Other" -#: project/i10n.py:33 +#: project/i10n.py:50 msgid "Event_Exhibition" msgstr "Exhibition" -#: project/i10n.py:34 +#: project/i10n.py:51 msgid "Event_Culture" msgstr "Culture" -#: project/i10n.py:35 +#: project/i10n.py:52 msgid "Event_Tour" msgstr "Tour" -#: project/i10n.py:36 +#: project/i10n.py:53 msgid "Event_OpenAir" msgstr "Open Air" -#: project/i10n.py:37 +#: project/i10n.py:54 msgid "Event_Stage" msgstr "Stage" -#: project/i10n.py:38 +#: project/i10n.py:55 msgid "Event_Lecture" msgstr "Lecture" -#: project/i10n.py:39 +#: project/i10n.py:56 msgid "Typical Age range" msgstr "" -#: project/i10n.py:40 +#: project/i10n.py:57 msgid "Administrator" msgstr "" -#: project/i10n.py:41 +#: project/i10n.py:58 msgid "Event expert" msgstr "" -#: project/i10n.py:42 +#: project/i10n.py:59 msgid "EventReviewStatus.inbox" msgstr "Inbox" -#: project/i10n.py:43 +#: project/i10n.py:60 msgid "EventReviewStatus.verified" msgstr "Verified" -#: project/i10n.py:44 +#: project/i10n.py:61 msgid "EventReviewStatus.rejected" msgstr "Rejected" -#: project/i10n.py:45 +#: project/i10n.py:62 msgid "Scope_openid" msgstr "Verify identity" -#: project/i10n.py:46 +#: project/i10n.py:63 msgid "Scope_profile" msgstr "Profile information" -#: project/i10n.py:47 +#: project/i10n.py:64 msgid "Scope_user:read" msgstr "Read user settings" -#: project/i10n.py:48 +#: project/i10n.py:65 msgid "Scope_user:write" msgstr "Create, update and delete user settings" -#: project/i10n.py:49 +#: project/i10n.py:66 msgid "Scope_organizer:write" msgstr "Create, update and delete organizers" -#: project/i10n.py:50 +#: project/i10n.py:67 msgid "Scope_place:write" msgstr "Create, update and delete places" -#: project/i10n.py:51 +#: project/i10n.py:68 msgid "Scope_event:write" msgstr "Create, update and delete events" -#: project/i10n.py:52 +#: project/i10n.py:69 msgid "Scope_eventlist:write" msgstr "Create, update and delete event lists" -#: project/i10n.py:53 +#: project/i10n.py:70 msgid "Scope_eventreference:write" msgstr "" -#: project/i10n.py:54 +#: project/i10n.py:71 msgid "Scope_organization:read" msgstr "Read organizations" -#: project/i10n.py:55 +#: project/i10n.py:72 msgid "Scope_organization:write" msgstr "Create, update and delete organizations" -#: project/i10n.py:56 +#: project/i10n.py:73 msgid "Scope_customwidget:write" msgstr "" -#: project/i10n.py:57 +#: project/i10n.py:74 msgid "There must be no self-reference." msgstr "" -#: project/utils.py:15 +#: project/utils.py:19 msgid "Event_" msgstr "" -#: project/utils.py:19 +#: project/utils.py:23 msgid "." msgstr "" @@ -202,11 +202,6 @@ msgstr "" msgid "message" msgstr "" -#: project/api/organization/resources.py:647 -#: project/views/admin_unit_member_invitation.py:89 -msgid "You have received an invitation" -msgstr "" - #: project/forms/admin.py:11 project/templates/layout.html:344 #: project/views/root.py:55 msgid "Terms of service" @@ -238,7 +233,7 @@ msgid "Announcement" msgstr "" #: project/forms/admin.py:18 project/forms/oauth2_client.py:24 -#: project/forms/user.py:13 +#: project/forms/user.py:18 project/forms/user.py:32 msgid "Save" msgstr "" @@ -268,7 +263,7 @@ msgstr "" #: project/forms/admin_unit_member.py:12 project/forms/admin_unit_member.py:25 #: project/forms/admin_unit_member.py:30 project/forms/event.py:112 #: project/forms/event_suggestion.py:38 project/forms/organizer.py:33 -#: project/forms/user.py:18 project/forms/user.py:23 +#: project/forms/user.py:37 project/forms/user.py:42 #: project/templates/_macros.html:246 project/templates/_macros.html:1586 #: project/templates/admin/admin.html:27 project/templates/admin/email.html:4 #: project/templates/admin/email.html:66 project/templates/admin/users.html:19 @@ -476,7 +471,7 @@ msgid "Verification requests postal codes" msgstr "" #: project/forms/admin_unit.py:93 -msgid "Accept verification requests to organizations with these postal codes." +msgid "Limit verification requests to organizations with these postal codes." msgstr "" #: project/forms/admin_unit.py:109 @@ -524,11 +519,11 @@ msgstr "" msgid "Link Color" msgstr "" -#: project/forms/admin_unit.py:165 project/forms/user.py:17 +#: project/forms/admin_unit.py:165 project/forms/user.py:36 msgid "Request deletion" msgstr "" -#: project/forms/admin_unit.py:170 project/forms/user.py:22 +#: project/forms/admin_unit.py:170 project/forms/user.py:41 #: project/templates/admin_unit/cancel_deletion.html:6 #: project/templates/admin_unit/update.html:44 #: project/templates/manage/events.html:49 project/templates/profile.html:13 @@ -1364,13 +1359,21 @@ msgstr "" msgid "Confirm" msgstr "" -#: project/forms/user.py:9 project/templates/admin/admin.html:31 +#: project/forms/user.py:9 +msgid "Language" +msgstr "" + +#: project/forms/user.py:11 +msgid "Default" +msgstr "" + +#: project/forms/user.py:28 project/templates/admin/admin.html:31 #: project/templates/admin/newsletter.html:4 #: project/templates/admin/newsletter.html:93 msgid "Newsletter" msgstr "" -#: project/forms/user.py:10 +#: project/forms/user.py:29 msgid "Information about new features and improvements." msgstr "" @@ -1858,7 +1861,7 @@ msgstr "" #: project/templates/developer/read.html:4 #: project/templates/developer/read.html:8 project/templates/layout.html:361 -#: project/templates/profile.html:45 +#: project/templates/profile.html:49 msgid "Developer" msgstr "" @@ -1872,20 +1875,25 @@ msgstr "" msgid "Delete account" msgstr "" -#: project/templates/profile.html:36 +#: project/templates/profile.html:36 project/templates/user/general.html:4 +#: project/templates/user/general.html:8 +msgid "General" +msgstr "" + +#: project/templates/profile.html:40 #: project/templates/user/notifications.html:4 #: project/templates/user/notifications.html:8 msgid "Notifications" msgstr "" -#: project/templates/profile.html:40 +#: project/templates/profile.html:44 msgid "Applications" msgstr "" #: project/templates/oauth2_client/list.html:4 #: project/templates/oauth2_client/list.html:11 #: project/templates/oauth2_client/read.html:11 -#: project/templates/profile.html:49 +#: project/templates/profile.html:53 msgid "OAuth2 clients" msgstr "" @@ -1920,7 +1928,7 @@ msgstr "" msgid "User" msgstr "" -#: project/templates/admin/email.html:47 project/views/admin.py:165 +#: project/templates/admin/email.html:47 project/views/admin.py:163 msgid "Mail sent successfully" msgstr "" @@ -2305,7 +2313,7 @@ msgstr "" #: project/templates/manage/reference_requests_incoming.html:20 #: project/templates/reference/read.html:22 #: project/templates/reference_request/review_status.html:17 -#: project/views/reference_request_review.py:42 +#: project/views/reference_request_review.py:38 msgid "View event" msgstr "" @@ -2416,7 +2424,7 @@ msgid "Reviews" msgstr "" #: project/templates/manage/verification_requests_incoming.html:19 -#: project/views/verification_request_review.py:36 +#: project/views/verification_request_review.py:32 msgid "View organization" msgstr "" @@ -2545,7 +2553,7 @@ msgstr "" msgid "You do not have an account yet? Not a problem!" msgstr "" -#: project/templates/security/login_user.html:44 project/views/widget.py:122 +#: project/templates/security/login_user.html:44 project/views/widget.py:119 msgid "Register for free" msgstr "" @@ -2622,29 +2630,19 @@ msgid "Organization successfully deleted" msgstr "" #: project/views/admin.py:133 project/views/manage.py:485 -#: project/views/user.py:63 +#: project/views/user.py:64 project/views/user.py:83 msgid "Settings successfully updated" msgstr "" -#: project/views/admin.py:153 -#, python-format -msgid "Test mail from %(site_name)s" -msgstr "" - -#: project/views/admin.py:183 -#, python-format -msgid "Newsletter from %(site_name)s" -msgstr "" - -#: project/views/admin.py:232 +#: project/views/admin.py:233 msgid "User successfully updated" msgstr "" -#: project/views/admin.py:252 +#: project/views/admin.py:253 msgid "Entered email does not match user email" msgstr "" -#: project/views/admin.py:256 +#: project/views/admin.py:257 msgid "User successfully deleted" msgstr "" @@ -2662,14 +2660,6 @@ msgstr "" msgid "AdminUnit successfully updated" msgstr "" -#: project/views/admin_unit.py:271 -msgid "Organization invitation accepted" -msgstr "" - -#: project/views/admin_unit.py:285 -msgid "Organization deletion requested" -msgstr "" - #: project/views/admin_unit_member.py:46 msgid "Member successfully updated" msgstr "" @@ -2690,46 +2680,38 @@ msgstr "" msgid "Invitation successfully declined" msgstr "" -#: project/views/admin_unit_member_invitation.py:94 +#: project/views/admin_unit_member_invitation.py:93 msgid "Invitation successfully sent" msgstr "" -#: project/views/admin_unit_member_invitation.py:117 +#: project/views/admin_unit_member_invitation.py:116 msgid "Entered email does not match invitation email" msgstr "" -#: project/views/admin_unit_member_invitation.py:122 +#: project/views/admin_unit_member_invitation.py:121 msgid "Invitation successfully deleted" msgstr "" -#: project/views/event.py:208 +#: project/views/event.py:209 msgid "Event successfully published" msgstr "" -#: project/views/event.py:210 +#: project/views/event.py:211 msgid "Draft successfully saved" msgstr "" -#: project/views/event.py:212 +#: project/views/event.py:213 msgid "Event successfully planned" msgstr "" -#: project/views/event.py:272 +#: project/views/event.py:273 msgid "Event successfully updated" msgstr "" -#: project/views/event.py:298 +#: project/views/event.py:299 msgid "Event successfully deleted" msgstr "" -#: project/views/event.py:463 -msgid "Referenced event changed" -msgstr "" - -#: project/views/event.py:486 -msgid "New event report" -msgstr "" - #: project/views/event_place.py:34 msgid "Place successfully created" msgstr "" @@ -2750,11 +2732,6 @@ msgstr "" msgid "Event suggestion successfully rejected" msgstr "" -#: project/views/event_suggestion.py:87 -#: project/views/reference_request_review.py:139 -msgid "Event review status updated" -msgstr "" - #: project/views/js.py:33 msgid "Short name is already taken" msgstr "" @@ -2855,68 +2832,114 @@ msgid "" "notified after the other organization reviews the event." msgstr "" -#: project/views/reference_request.py:172 -msgid "New reference request" -msgstr "" - -#: project/views/reference_request.py:181 -msgid "New reference automatically verified" -msgstr "" - -#: project/views/reference_request_review.py:40 +#: project/views/reference_request_review.py:36 msgid "Request already verified" msgstr "" -#: project/views/reference_request_review.py:61 +#: project/views/reference_request_review.py:57 msgid "Reference successfully created" msgstr "" -#: project/views/reference_request_review.py:69 +#: project/views/reference_request_review.py:65 msgid "Request successfully updated" msgstr "" -#: project/views/reference_request_review.py:92 -#: project/views/verification_request_review.py:83 +#: project/views/reference_request_review.py:88 +#: project/views/verification_request_review.py:79 #, python-format msgid "" "If all upcoming reference requests of %(admin_unit_name)s should be " "verified automatically." msgstr "" -#: project/views/user.py:107 +#: project/views/user.py:127 msgid "" "You are administrator of at least one organization. Cancel your " "membership to delete your account." msgstr "" -#: project/views/user.py:114 project/views/user.py:141 +#: project/views/user.py:134 project/views/user.py:161 msgid "Entered email does not match your email" msgstr "" -#: project/views/user.py:160 +#: project/views/utils.py:27 +msgid "New event report" +msgstr "" + +#: project/views/utils.py:28 project/views/utils.py:36 +msgid "You have received an invitation" +msgstr "" + +#: project/views/utils.py:29 +#, python-format +msgid "Newsletter from %(site_name)s" +msgstr "" + +#: project/views/utils.py:30 +msgid "Organization deletion requested" +msgstr "" + +#: project/views/utils.py:33 +msgid "Organization invitation accepted" +msgstr "" + +#: project/views/utils.py:37 +msgid "New reference automatically verified" +msgstr "" + +#: project/views/utils.py:40 +msgid "New reference request" +msgstr "" + +#: project/views/utils.py:41 project/views/utils.py:46 +msgid "Event review status updated" +msgstr "" + +#: project/views/utils.py:44 +msgid "Referenced event changed" +msgstr "" + +#: project/views/utils.py:45 +msgid "New event review" +msgstr "" + +#: project/views/utils.py:47 msgid "User deletion requested" msgstr "" -#: project/views/utils.py:91 +#: project/views/utils.py:48 +#, python-format +msgid "Test mail from %(site_name)s" +msgstr "" + +#: project/views/utils.py:49 +msgid "New verification request" +msgstr "" + +#: project/views/utils.py:50 +msgid "Verification request review status updated" +msgstr "" + +#: project/views/utils.py:122 msgid "" "An entry with the entered values ​​already exists. Duplicate entries are " "not allowed." msgstr "" -#: project/views/utils.py:142 +#: project/views/utils.py:173 #, python-format msgid "Error in the %s field - %s" msgstr "" -#: project/views/utils.py:149 +#: project/views/utils.py:180 msgid "Show" msgstr "" -#: project/views/utils.py:157 +#: project/views/utils.py:188 msgid "You do not have permission for this action" msgstr "" -#: project/views/utils.py:306 +#: project/views/utils.py:382 msgid "" "The invitation was issued to another user. Sign in with the email address" " the invitation was sent to." @@ -2932,40 +2955,28 @@ msgstr "" msgid "Verification request successfully deleted" msgstr "" -#: project/views/verification_request.py:207 -msgid "New verification request" -msgstr "" - -#: project/views/verification_request_review.py:34 +#: project/views/verification_request_review.py:30 msgid "Verification request already verified" msgstr "" -#: project/views/verification_request_review.py:64 +#: project/views/verification_request_review.py:60 msgid "Organization successfully verified" msgstr "" -#: project/views/verification_request_review.py:66 +#: project/views/verification_request_review.py:62 msgid "Verification request successfully updated" msgstr "" -#: project/views/verification_request_review.py:119 -msgid "Verification request review status updated" -msgstr "" - -#: project/views/widget.py:114 +#: project/views/widget.py:111 msgid "Thank you so much! The event is being verified." msgstr "" -#: project/views/widget.py:118 +#: project/views/widget.py:115 msgid "" "For more options and your own calendar of events, you can register for " "free." msgstr "" -#: project/views/widget.py:184 -msgid "New event review" -msgstr "" - #~ msgid "" #~ "Indicate when the event will end. " #~ "An event can last a maximum of " @@ -3052,3 +3063,6 @@ msgstr "" #~ msgid "Switch to place search" #~ msgstr "" +#~ msgid "Accept verification requests to organizations with these postal codes." +#~ msgstr "" + diff --git a/project/utils.py b/project/utils.py index 2f22e46..251b1ef 100644 --- a/project/utils.py +++ b/project/utils.py @@ -7,6 +7,10 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.base import NO_CHANGE, object_state +def dummy_gettext(message: str): + return message + + def strings_are_equal_ignoring_case(string1: str, string2: str): return string1 and string2 and string1.casefold() == string2.casefold() diff --git a/project/views/admin.py b/project/views/admin.py index ef742a4..d62d473 100644 --- a/project/views/admin.py +++ b/project/views/admin.py @@ -25,9 +25,9 @@ from project.views.utils import ( get_pagination_urls, handleSqlError, non_match_for_deletion, - send_mail, - send_mail_async, - send_mails_async, + send_template_mail, + send_template_mail_async, + send_template_mails_to_users_async, ) @@ -150,18 +150,16 @@ def admin_email(): return get_celery_poll_group_result() if form.validate_on_submit(): - subject = gettext( - "Test mail from %(site_name)s", - site_name=app.config["SITE_NAME"], - ) + template = "test_email" + context = {"site_name": app.config["SITE_NAME"]} if "async" in request.args: # pragma: no cover - result = send_mail_async(form.recipient.data, subject, "test_email") + result = send_template_mail_async(form.recipient.data, template, **context) result.save() return {"result_id": result.id} try: - send_mail(form.recipient.data, subject, "test_email") + send_template_mail(form.recipient.data, template, **context) flash(gettext("Mail sent successfully"), "success") except Exception as e: # pragma: no cover flash(str(e), "danger") @@ -180,13 +178,15 @@ def admin_newsletter(): return get_celery_poll_group_result() if form.validate_on_submit(): - subject = gettext( - "Newsletter from %(site_name)s", - site_name=app.config["SITE_NAME"], - ) + template = "newsletter" + context = {"site_name": app.config["SITE_NAME"], "message": form.message.data} if form.recipient_choice.data == 1: # pragma: no cover - recipients = [form.test_recipient.data] + result = send_template_mail_async( + form.test_recipient.data, + template, + **context, + ) else: users = ( User.query.filter(User.email != None) @@ -194,11 +194,12 @@ def admin_newsletter(): .filter(User.newsletter_enabled) .all() ) - recipients = [u.email for u in users] + result = send_template_mails_to_users_async( + users, + template, + **context, + ) - result = send_mails_async( - recipients, subject, "newsletter", message=form.message.data - ) result.save() return {"result_id": result.id} diff --git a/project/views/admin_unit.py b/project/views/admin_unit.py index b044dd4..d91f2c7 100644 --- a/project/views/admin_unit.py +++ b/project/views/admin_unit.py @@ -9,7 +9,6 @@ from project import app, db from project.access import ( can_create_admin_unit, get_admin_unit_for_manage_or_404, - get_admin_unit_members_with_permission, has_access, ) from project.forms.admin_unit import ( @@ -32,7 +31,7 @@ from project.views.utils import ( manage_required, non_match_for_deletion, permission_missing, - send_mails_async, + send_template_mails_to_admin_unit_members_async, ) @@ -261,14 +260,9 @@ def send_admin_unit_invitation_accepted_mails( invitation: AdminUnitInvitation, relation: AdminUnitRelation, admin_unit: AdminUnit ): # Benachrichtige alle Mitglieder der AdminUnit, die diese Einladung erstellt hatte - members = get_admin_unit_members_with_permission( - invitation.admin_unit_id, "admin_unit:update" - ) - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async( - emails, - gettext("Organization invitation accepted"), + send_template_mails_to_admin_unit_members_async( + invitation.admin_unit_id, + "admin_unit:update", "organization_invitation_accepted_notice", invitation=invitation, relation=relation, @@ -277,12 +271,9 @@ def send_admin_unit_invitation_accepted_mails( def send_admin_unit_deletion_requested_mails(admin_unit: AdminUnit): - members = get_admin_unit_members_with_permission(admin_unit.id, "admin_unit:update") - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async( - emails, - gettext("Organization deletion requested"), + send_template_mails_to_admin_unit_members_async( + admin_unit.id, + "admin_unit:update", "organization_deletion_requested_notice", admin_unit=admin_unit, ) diff --git a/project/views/admin_unit_member_invitation.py b/project/views/admin_unit_member_invitation.py index 71a0839..3d74a8d 100644 --- a/project/views/admin_unit_member_invitation.py +++ b/project/views/admin_unit_member_invitation.py @@ -18,7 +18,7 @@ from project.views.utils import ( handleSqlError, non_match_for_deletion, permission_missing, - send_mail_async, + send_template_mail_async, ) @@ -84,9 +84,8 @@ def manage_admin_unit_member_invite(id): db.session.add(invitation) db.session.commit() - send_mail_async( + send_template_mail_async( invitation.email, - gettext("You have received an invitation"), "invitation_notice", invitation=invitation, ) diff --git a/project/views/event.py b/project/views/event.py index 80ae70d..a84a936 100644 --- a/project/views/event.py +++ b/project/views/event.py @@ -55,7 +55,8 @@ from project.views.utils import ( get_calendar_links_for_event, get_share_links, handleSqlError, - send_mails_async, + send_template_mails_to_admin_unit_members_async, + send_template_mails_to_users_async, set_current_admin_unit, ) @@ -453,14 +454,9 @@ def send_referenced_event_changed_mails(event): references = EventReference.query.filter(EventReference.event_id == event.id).all() for reference in references: # Alle Mitglieder der AdminUnit, die das Recht haben, Requests zu verifizieren - members = get_admin_unit_members_with_permission( - reference.admin_unit_id, "reference_request:verify" - ) - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async( - emails, - gettext("Referenced event changed"), + send_template_mails_to_admin_unit_members_async( + reference.admin_unit_id, + "reference_request:verify", "referenced_event_changed_notice", event=event, reference=reference, @@ -474,16 +470,16 @@ def send_event_report_mails(event: Event, report: dict): members = get_admin_unit_members_with_permission( event.admin_unit_id, "event:update" ) - emails = list(map(lambda member: member.user.email, members)) + users = [member.user for member in members] # Alle globalen Admins admins = find_all_users_with_role("admin") - admin_emails = list(map(lambda admin: admin.email, admins)) - emails.extend(x for x in admin_emails if x not in emails) + users.extend( + admin for admin in admins if all(user.id != admin.id for user in users) + ) - send_mails_async( - emails, - gettext("New event report"), + send_template_mails_to_users_async( + users, "event_report_notice", event=event, report=report, diff --git a/project/views/event_suggestion.py b/project/views/event_suggestion.py index b5a3557..393b1cb 100644 --- a/project/views/event_suggestion.py +++ b/project/views/event_suggestion.py @@ -7,7 +7,7 @@ from project import app, db from project.access import access_or_401 from project.forms.event_suggestion import RejectEventSuggestionForm from project.models import EventReviewStatus, EventSuggestion -from project.views.utils import flash_errors, handleSqlError, send_mail_async +from project.views.utils import flash_errors, handleSqlError, send_template_mail_async @app.route("/event_suggestion//review") @@ -82,9 +82,8 @@ def event_suggestion_review_status(event_suggestion_id): def send_event_suggestion_review_status_mail(event_suggestion): if event_suggestion.contact_email and event_suggestion.contact_email_notice: - send_mail_async( + send_template_mail_async( event_suggestion.contact_email, - gettext("Event review status updated"), "review_status_notice", event_suggestion=event_suggestion, ) diff --git a/project/views/reference_request.py b/project/views/reference_request.py index 4eb236a..42cbf2f 100644 --- a/project/views/reference_request.py +++ b/project/views/reference_request.py @@ -4,11 +4,7 @@ from flask_security import auth_required from sqlalchemy.exc import SQLAlchemyError from project import app, db -from project.access import ( - can_request_event_reference, - get_admin_unit_for_manage_or_404, - get_admin_unit_members_with_permission, -) +from project.access import can_request_event_reference, get_admin_unit_for_manage_or_404 from project.forms.reference_request import CreateEventReferenceRequestForm from project.models import ( Event, @@ -31,7 +27,7 @@ from project.views.utils import ( flash_errors, get_pagination_urls, handleSqlError, - send_mails_async, + send_template_mails_to_admin_unit_members_async, ) @@ -169,7 +165,6 @@ def send_reference_request_mails( def _send_reference_request_inbox_mails(request): _send_member_reference_request_verify_mails( request.admin_unit_id, - gettext("New reference request"), "reference_request_notice", request=request, ) @@ -178,19 +173,13 @@ def _send_reference_request_inbox_mails(request): def _send_auto_reference_mails(reference): _send_member_reference_request_verify_mails( reference.admin_unit_id, - gettext("New reference automatically verified"), "reference_auto_verified_notice", reference=reference, ) -def _send_member_reference_request_verify_mails( - admin_unit_id, subject, template, **context -): +def _send_member_reference_request_verify_mails(admin_unit_id, template, **context): # Benachrichtige alle Mitglieder der AdminUnit, die Requests verifizieren können - members = get_admin_unit_members_with_permission( - admin_unit_id, "reference_request:verify" + send_template_mails_to_admin_unit_members_async( + admin_unit_id, "reference_request:verify", template, **context ) - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async(emails, subject, template, **context) diff --git a/project/views/reference_request_review.py b/project/views/reference_request_review.py index 7c295a1..2da1511 100644 --- a/project/views/reference_request_review.py +++ b/project/views/reference_request_review.py @@ -4,11 +4,7 @@ from flask_security import auth_required from sqlalchemy.exc import SQLAlchemyError from project import app, db -from project.access import ( - access_or_401, - get_admin_unit_members_with_permission, - has_access, -) +from project.access import access_or_401, has_access from project.dateutils import get_today from project.forms.reference_request import ReferenceRequestReviewForm from project.models import ( @@ -25,7 +21,7 @@ from project.views.utils import ( flash_errors, flash_message, handleSqlError, - send_mails_async, + send_template_mails_to_admin_unit_members_async, ) @@ -129,14 +125,9 @@ def event_reference_request_review_status(id): def send_reference_request_review_status_mails(request): # Benachrichtige alle Mitglieder der AdminUnit, die diesen Request erstellt hatte - members = get_admin_unit_members_with_permission( - request.event.admin_unit_id, "reference_request:create" - ) - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async( - emails, - gettext("Event review status updated"), + send_template_mails_to_admin_unit_members_async( + request.event.admin_unit_id, + "reference_request:create", "reference_request_review_status_notice", request=request, ) diff --git a/project/views/user.py b/project/views/user.py index 68b73b6..e8aa09a 100644 --- a/project/views/user.py +++ b/project/views/user.py @@ -10,6 +10,7 @@ from project import app, db from project.forms.security import AcceptTosForm from project.forms.user import ( CancelUserDeletionForm, + GeneralForm, NotificationForm, RequestUserDeletionForm, ) @@ -20,7 +21,7 @@ from project.views.utils import ( get_invitation_access_result, handleSqlError, non_match_for_deletion, - send_mail_async, + send_template_mails_to_users_async, ) @@ -50,6 +51,25 @@ def user_accept_tos(): return render_template("user/accept_tos.html", form=form) +@app.route("/user/general", methods=("GET", "POST")) +@auth_required() +def user_general(): + user = User.query.get_or_404(current_user.id) + form = GeneralForm(obj=user) + + if form.validate_on_submit(): + try: + form.populate_obj(user) + db.session.commit() + flash(gettext("Settings successfully updated"), "success") + return redirect(url_for("profile")) + except SQLAlchemyError as e: # pragma: no cover + db.session.rollback() + flash(handleSqlError(e), "danger") + + return render_template("user/general.html", form=form) + + @app.route("/user/notifications", methods=("GET", "POST")) @auth_required() def user_notifications(): @@ -155,9 +175,8 @@ def user_cancel_deletion(): def send_user_deletion_requested_mail(user): - send_mail_async( - user.email, - gettext("User deletion requested"), + send_template_mails_to_users_async( + [user], "user_deletion_requested_notice", user=user, ) diff --git a/project/views/utils.py b/project/views/utils.py index 21f1e51..38d1c1b 100644 --- a/project/views/utils.py +++ b/project/views/utils.py @@ -1,8 +1,9 @@ from functools import wraps +from itertools import groupby from urllib.parse import quote_plus from flask import Markup, flash, g, redirect, render_template, request, url_for -from flask_babel import gettext +from flask_babel import force_locale, gettext from flask_login.utils import decode_cookie from flask_mail import Message from flask_security import current_user @@ -14,12 +15,42 @@ from project import app, celery, mail from project.access import ( get_admin_unit_for_manage, get_admin_unit_for_manage_or_404, + get_admin_unit_members_with_permission, get_admin_units_for_manage, has_access, ) from project.dateutils import berlin_tz, round_to_next_day from project.models import Event, EventAttendanceMode, EventDate -from project.utils import get_place_str, strings_are_equal_ignoring_case +from project.utils import dummy_gettext, get_place_str, strings_are_equal_ignoring_case + +mail_template_subject_mapping = { + "event_report_notice": dummy_gettext("New event report"), + "invitation_notice": dummy_gettext("You have received an invitation"), + "newsletter": dummy_gettext("Newsletter from %(site_name)s"), + "organization_deletion_requested_notice": dummy_gettext( + "Organization deletion requested" + ), + "organization_invitation_accepted_notice": dummy_gettext( + "Organization invitation accepted" + ), + "organization_invitation_notice": dummy_gettext("You have received an invitation"), + "reference_auto_verified_notice": dummy_gettext( + "New reference automatically verified" + ), + "reference_request_notice": dummy_gettext("New reference request"), + "reference_request_review_status_notice": dummy_gettext( + "Event review status updated" + ), + "referenced_event_changed_notice": dummy_gettext("Referenced event changed"), + "review_notice": dummy_gettext("New event review"), + "review_status_notice": dummy_gettext("Event review status updated"), + "user_deletion_requested_notice": dummy_gettext("User deletion requested"), + "test_email": dummy_gettext("Test mail from %(site_name)s"), + "verification_request_notice": dummy_gettext("New verification request"), + "verification_request_review_status_notice": dummy_gettext( + "Verification request review status updated" + ), +} def set_current_admin_unit(admin_unit): @@ -160,37 +191,86 @@ def permission_missing(redirect_location, message=None): return redirect(redirect_location) -def send_mail(recipient, subject, template, **context): - send_mails([recipient], subject, template, **context) +def send_template_mail(recipient, template, **context): + send_template_mails([recipient], template, **context) -def send_mails(recipients, subject, template, **context): +def send_template_mails(recipients, template, **context): if len(recipients) == 0: # pragma: no cover return - body, html = render_mail_body(template, **context) + subject, body, html = render_mail_body_with_subject(template, **context) send_mails_with_body(recipients, subject, body, html) -def send_mail_async(recipient, subject, template, **context): - return send_mails_async([recipient], subject, template, **context) +def send_template_mail_async(recipient, template, **context): + return send_template_mails_async([recipient], template, **context) -def send_mails_async(recipients, subject, template, **context): +def send_template_mails_async(recipients, template, **context): if len(recipients) == 0: # pragma: no cover return - body, html = render_mail_body(template, **context) + subject, body, html = render_mail_body_with_subject(template, **context) return send_mails_with_body_async(recipients, subject, body, html) +def render_mail_body_with_subject(template, **context): + subject_key = mail_template_subject_mapping.get(template) + locale = context.get("locale", None) or app.config["BABEL_DEFAULT_LOCALE"] + + with force_locale(locale): + subject = gettext(subject_key, **context) + body, html = render_mail_body(template, **context) + + return subject, body, html + + +def send_template_mails_to_admin_unit_members_async( + admin_unit_id, permissions, template, **context +): + members = get_admin_unit_members_with_permission(admin_unit_id, permissions) + users = [member.user for member in members] + + return send_template_mails_to_users_async(users, template, **context) + + +def send_template_mails_to_users_async(users, template, **context): + if len(users) == 0: # pragma: no cover + return + + # Group by locale + def locale_func(user): + return user.locale if user.locale else "" + + sorted_users = sorted(users, key=locale_func) + grouped_users = groupby(sorted_users, locale_func) + + signatures = list() + + for locale, locale_users in grouped_users: + context["locale"] = locale + subject, body, html = render_mail_body_with_subject(template, **context) + signatures.extend([(user.email, subject, body, html) for user in locale_users]) + + return send_mails_with_signatures_async(signatures) + + def send_mails_with_body_async(recipients, subject, body, html): + signatures = [(recipient, subject, body, html) for recipient in recipients] + return send_mails_with_signatures_async(signatures) + + +def send_mails_with_signatures_async(signatures): from celery import group from project.base_tasks import send_mail_with_body_task + if len(signatures) == 0: # pragma: no cover + return + result = group( - send_mail_with_body_task.s(r, subject, body, html) for r in recipients + send_mail_with_body_task.s(*signature) for signature in signatures ).delay() return result diff --git a/project/views/verification_request.py b/project/views/verification_request.py index 9301e61..6b331ca 100644 --- a/project/views/verification_request.py +++ b/project/views/verification_request.py @@ -4,7 +4,7 @@ from flask_security import auth_required from sqlalchemy.exc import SQLAlchemyError from project import app, db -from project.access import access_or_401, get_admin_unit_members_with_permission +from project.access import access_or_401 from project.forms.verification_request import ( CreateAdminUnitVerificationRequestForm, DeleteVerificationRequestForm, @@ -30,7 +30,7 @@ from project.views.utils import ( handleSqlError, manage_required, non_match_for_deletion, - send_mails_async, + send_template_mails_to_admin_unit_members_async, ) @@ -189,22 +189,12 @@ def admin_unit_verification_request_delete(id): ) -def send_member_verification_request_verify_mails( - admin_unit_id, subject, template, **context -): - # Benachrichtige alle Mitglieder der AdminUnit, die Requests verifizieren können - members = get_admin_unit_members_with_permission( - admin_unit_id, "verification_request:verify" - ) - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async(emails, subject, template, **context) - - def send_verification_request_inbox_mails(request): - send_member_verification_request_verify_mails( - request.target_admin_unit_id or request.target_admin_unit.id, - gettext("New verification request"), + # Benachrichtige alle Mitglieder der AdminUnit, die Requests verifizieren können + admin_unit_id = request.target_admin_unit_id or request.target_admin_unit.id + send_template_mails_to_admin_unit_members_async( + admin_unit_id, + "verification_request:verify", "verification_request_notice", request=request, ) diff --git a/project/views/verification_request_review.py b/project/views/verification_request_review.py index cc7f82e..6232dc5 100644 --- a/project/views/verification_request_review.py +++ b/project/views/verification_request_review.py @@ -4,11 +4,7 @@ from flask_security import auth_required from sqlalchemy.exc import SQLAlchemyError from project import app, db -from project.access import ( - access_or_401, - get_admin_unit_members_with_permission, - has_access, -) +from project.access import access_or_401, has_access from project.forms.verification_request import VerificationRequestReviewForm from project.models import ( AdminUnitVerificationRequest, @@ -19,7 +15,7 @@ from project.views.utils import ( flash_errors, flash_message, handleSqlError, - send_mails_async, + send_template_mails_to_admin_unit_members_async, ) @@ -109,14 +105,9 @@ def admin_unit_verification_request_review_status(id): def send_verification_request_review_status_mails(request): # Benachrichtige alle Mitglieder der AdminUnit, die diesen Request erstellt hatte - members = get_admin_unit_members_with_permission( - request.source_admin_unit_id, "verification_request:create" - ) - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async( - emails, - gettext("Verification request review status updated"), + send_template_mails_to_admin_unit_members_async( + request.source_admin_unit_id, + "verification_request:create", "verification_request_review_status_notice", request=request, ) diff --git a/project/views/widget.py b/project/views/widget.py index 363e811..2f82d01 100644 --- a/project/views/widget.py +++ b/project/views/widget.py @@ -5,10 +5,7 @@ from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.sql import func from project import app, db -from project.access import ( - admin_unit_suggestions_enabled_or_404, - get_admin_unit_members_with_permission, -) +from project.access import admin_unit_suggestions_enabled_or_404 from project.dateutils import get_next_full_hour from project.forms.event_date import FindEventDateWidgetForm from project.forms.event_suggestion import CreateEventSuggestionForm @@ -23,7 +20,7 @@ from project.views.utils import ( flash_message, get_pagination_urls, handleSqlError, - send_mails_async, + send_template_mails_to_admin_unit_members_async, ) @@ -176,12 +173,9 @@ def get_styles(admin_unit): def send_event_inbox_mails(admin_unit, event_suggestion): - members = get_admin_unit_members_with_permission(admin_unit.id, "event:verify") - emails = list(map(lambda member: member.user.email, members)) - - send_mails_async( - emails, - gettext("New event review"), + send_template_mails_to_admin_unit_members_async( + admin_unit.id, + "event:verify", "review_notice", event_suggestion=event_suggestion, ) diff --git a/tests/seeder.py b/tests/seeder.py index 8afb212..936aec8 100644 --- a/tests/seeder.py +++ b/tests/seeder.py @@ -38,6 +38,7 @@ class Seeder(object): admin=False, confirm=True, tos_accepted=True, + locale=None, ): from flask_security.confirmable import confirm_user @@ -63,6 +64,7 @@ class Seeder(object): if admin: add_admin_roles_to_user(email) + user.locale = locale self._db.session.commit() user_id = user.id diff --git a/tests/test_i10n.py b/tests/test_i10n.py index 8dd73be..a6e992b 100644 --- a/tests/test_i10n.py +++ b/tests/test_i10n.py @@ -1,4 +1,4 @@ -def test_send_mails(client, app, utils): +def test_dynamic_texts(client, app, utils): from project.i10n import print_dynamic_texts print_dynamic_texts() diff --git a/tests/utils.py b/tests/utils.py index b014c77..2c29900 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -206,14 +206,21 @@ class UtilActions(object): return mocker.patch("project.views.utils.send_mails_with_body") def mock_send_mails_async(self, mocker): - return mocker.patch("project.views.utils.send_mails_with_body_async") + return mocker.patch("project.views.utils.send_mails_with_signatures_async") def assert_send_mail_called( self, mock, expected_recipients, expected_contents=None ): mock.assert_called_once() args, kwargs = mock.call_args - send_recipients, subject, body, html = args + + if mock._extract_mock_name() == "send_mails_with_signatures_async": + signatures = args[0] + send_recipients = [s[0] for s in signatures] + first_signature = signatures[0] + _, subject, body, html = first_signature + else: + send_recipients, subject, body, html = args if not isinstance(expected_recipients, list): expected_recipients = [expected_recipients] diff --git a/tests/views/test_admin.py b/tests/views/test_admin.py index 528e3e0..3324ab5 100644 --- a/tests/views/test_admin.py +++ b/tests/views/test_admin.py @@ -86,7 +86,8 @@ def test_newsletter(app, utils, seeder): user_id, admin_unit_id = seeder.setup_base(True) for i in range(10): - seeder.create_user(f"test{i}@test.de") + locale = "de" if (i % 3) == 0 else "en" if (i % 3) == 1 else None + seeder.create_user(f"test{i}@test.de", locale=locale) url = utils.get_url("admin_newsletter") response = utils.get_ok(url) diff --git a/tests/views/test_user.py b/tests/views/test_user.py index f95d7a5..09173d6 100644 --- a/tests/views/test_user.py +++ b/tests/views/test_user.py @@ -69,6 +69,35 @@ def test_user_favorite_events(client, seeder, utils): utils.get_ok(url) +@pytest.mark.parametrize("locale", [None, "de"]) +def test_user_general(client, seeder, utils, app, db, locale): + user_id, admin_unit_id = seeder.setup_base() + + url = utils.get_url("user_general") + response = utils.get_ok(url) + + if locale is None: + values = dict() + else: + values = { + "locale": locale, + } + + response = utils.post_form( + url, + response, + values, + ) + + utils.assert_response_redirect(response, "profile") + + with app.app_context(): + from project.models import User + + user = db.session.get(User, user_id) + assert user.locale == locale + + def test_user_notifications(client, seeder, utils, app, db): user_id, admin_unit_id = seeder.setup_base() @@ -88,8 +117,8 @@ def test_user_notifications(client, seeder, utils, app, db): with app.app_context(): from project.models import User - place = db.session.get(User, user_id) - assert not place.newsletter_enabled + user = db.session.get(User, user_id) + assert not user.newsletter_enabled def test_login_flash(client, seeder, utils): diff --git a/tests/views/test_utils.py b/tests/views/test_utils.py index 785ce57..335b8fc 100644 --- a/tests/views/test_utils.py +++ b/tests/views/test_utils.py @@ -1,6 +1,6 @@ -def test_send_mails(client, seeder, app, db, utils): +def test_send_template_mails(client, seeder, app, db, utils): from project.models import AdminUnitMemberInvitation - from project.views.utils import send_mail + from project.views.utils import send_template_mail user_id, admin_unit_id = seeder.setup_base() email = "new@member.de" @@ -12,9 +12,8 @@ def test_send_mails(client, seeder, app, db, utils): mail.default_sender = None invitation = db.session.get(AdminUnitMemberInvitation, invitation_id) - send_mail( + send_template_mail( email, - "You have received an invitation", "invitation_notice", invitation=invitation, )