From b2c872c36f7ea07f26c9da0b46ef76abae193959 Mon Sep 17 00:00:00 2001 From: theresantonie Date: Wed, 7 Aug 2024 22:52:17 +0200 Subject: [PATCH 01/23] add new menu item --- integreat_cms/cms/templates/_base.html | 8 ++++++++ integreat_cms/locale/de/LC_MESSAGES/django.po | 13 +++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/integreat_cms/cms/templates/_base.html b/integreat_cms/cms/templates/_base.html index 97e3b81d18..6f94b4400b 100644 --- a/integreat_cms/cms/templates/_base.html +++ b/integreat_cms/cms/templates/_base.html @@ -187,6 +187,14 @@ {% endif %} + {% if perms.cms.manage_translations %} +
+ + + {% translate "Contacts" %} + +
+ {% endif %} {% if perms.cms.view_poi %}
diff --git a/integreat_cms/locale/de/LC_MESSAGES/django.po b/integreat_cms/locale/de/LC_MESSAGES/django.po index 9ae3d79603..066a568d77 100644 --- a/integreat_cms/locale/de/LC_MESSAGES/django.po +++ b/integreat_cms/locale/de/LC_MESSAGES/django.po @@ -4359,6 +4359,10 @@ msgstr "Inhalt" msgid "Media Library" msgstr "Medienbibliothek" +#: cms/templates/_base.html +msgid "Contacts" +msgstr "Kontakte" + #: cms/templates/_base.html #: cms/templates/push_notifications/push_notification_list.html #: core/settings.py @@ -7247,7 +7251,8 @@ msgstr "Ort löschen" #: cms/templates/pois/poi_form_sidebar/action_box.html msgid "You cannot delete a location which is referenced by an event." msgstr "" -"Der Ort kann nicht gelöscht werden, da er von einer Veranstaltung verwendet wird." +"Der Ort kann nicht gelöscht werden, da er von einer Veranstaltung verwendet " +"wird." #: cms/templates/pois/poi_form_sidebar/action_box.html msgid "To delete this location, you have to delete this event first:" @@ -10279,6 +10284,9 @@ msgstr "" "Diese Seite konnte nicht importiert werden, da sie zu einer anderen Region " "gehört ({})." +#~ msgid "Contents" +#~ msgstr "Inhalte" + #~ msgid "View location" #~ msgstr "Ort ansehen" @@ -10967,9 +10975,6 @@ msgstr "" #~ msgid "Language tree node for \"{}\" was successfully created" #~ msgstr "Sprach-Knoten für \"{}\" wurde erfolgreich erstellt" -#~ msgid "Contents" -#~ msgstr "Inhalte" - #~ msgid "Organization was successfully deleted" #~ msgstr "Organisation wurde erfolgreich gespeichert" From 486c6a79eaf0b56fb683067dd11d4ede27cb752a Mon Sep 17 00:00:00 2001 From: JoeyStk Date: Tue, 6 Aug 2024 15:28:08 +0200 Subject: [PATCH 02/23] Add contact model --- integreat_cms/cms/constants/roles.py | 3 + integreat_cms/cms/fixtures/test_data.json | 30 +++++ .../cms/migrations/0098_add_contact.py | 112 ++++++++++++++++++ integreat_cms/cms/models/__init__.py | 1 + integreat_cms/cms/models/contact/contact.py | 72 +++++++++++ .../cms/views/regions/region_actions.py | 4 +- integreat_cms/locale/de/LC_MESSAGES/django.po | 87 ++++++++------ 7 files changed, 271 insertions(+), 38 deletions(-) create mode 100644 integreat_cms/cms/migrations/0098_add_contact.py create mode 100644 integreat_cms/cms/models/contact/contact.py diff --git a/integreat_cms/cms/constants/roles.py b/integreat_cms/cms/constants/roles.py index 761ff20ffb..102d95a90c 100644 --- a/integreat_cms/cms/constants/roles.py +++ b/integreat_cms/cms/constants/roles.py @@ -187,6 +187,9 @@ "delete_user", "grant_page_permissions", "manage_translations", + "change_contact", + "delete_contact", + "view_contact", ] #: The permissions of the cms team diff --git a/integreat_cms/cms/fixtures/test_data.json b/integreat_cms/cms/fixtures/test_data.json index f0a67e97c4..b93fe9d0a1 100644 --- a/integreat_cms/cms/fixtures/test_data.json +++ b/integreat_cms/cms/fixtures/test_data.json @@ -5779,5 +5779,35 @@ "poi": 4, "meta_description": "Meta description of the test location" } + }, + { + "model": "cms.contact", + "pk": 1, + "fields": { + "title": "Integrationsbeauftragte", + "name": "Martina Musterfrau", + "poi": 6, + "email": "martina-musterfrau@example.com", + "phone_number": "0123456789", + "website": "", + "archived": false, + "created_date": "2024-08-06T13:23:45.256Z", + "last_updated": "2024-08-06T13:23:45.256Z" + } + }, + { + "model": "cms.contact", + "pk": 2, + "fields": { + "title": "Integrationsberaterin", + "name": "Martina Musterfrau", + "poi": 6, + "email": "martina-musterfrau@example.com", + "phone_number": "0987654321", + "website": "www.random-page.com", + "archived": true, + "created_date": "2024-08-06T13:23:45.256Z", + "last_updated": "2024-08-06T13:23:45.256Z" + } } ] diff --git a/integreat_cms/cms/migrations/0098_add_contact.py b/integreat_cms/cms/migrations/0098_add_contact.py new file mode 100644 index 0000000000..3a35308056 --- /dev/null +++ b/integreat_cms/cms/migrations/0098_add_contact.py @@ -0,0 +1,112 @@ +import django.db.models.deletion +import django.utils.timezone +from django.apps.registry import Apps +from django.core.management.sql import emit_post_migrate_signal +from django.db import migrations, models +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + +from integreat_cms.cms.constants import roles + + +# pylint: disable=unused-argument +def update_roles(apps: Apps, schema_editor: BaseDatabaseSchemaEditor) -> None: + """ + Add permissions for managing external calendars + :param apps: The configuration of installed applications + :param schema_editor: The database abstraction layer that creates actual SQL code + """ + Group = apps.get_model("auth", "Group") + Permission = apps.get_model("auth", "Permission") + + # Emit post-migrate signal to make sure the Permission objects are created before they can be assigned + emit_post_migrate_signal(2, False, "default") + + # Clear and update permissions according to new constants + for role_name in dict(roles.CHOICES): + group, _ = Group.objects.get_or_create(name=role_name) + # Clear permissions + group.permissions.clear() + # Set permissions + group.permissions.add( + *Permission.objects.filter(codename__in=roles.PERMISSIONS[role_name]) + ) + + +class Migration(migrations.Migration): + """ + Initial migration file for contact + """ + + dependencies = [ + ("cms", "0097_alter_pushnotificationtranslation_text"), + ] + + operations = [ + migrations.CreateModel( + name="Contact", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(max_length=200, verbose_name="title")), + ("name", models.CharField(max_length=200, verbose_name="name")), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "phone_number", + models.CharField( + blank=True, max_length=250, verbose_name="phone number" + ), + ), + ( + "website", + models.URLField(blank=True, max_length=250, verbose_name="website"), + ), + ( + "archived", + models.BooleanField( + default=False, + help_text="Whether or not the location is read-only and hidden in the API.", + verbose_name="archived", + ), + ), + ( + "last_updated", + models.DateTimeField( + auto_now=True, verbose_name="modification date" + ), + ), + ( + "created_date", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="creation date" + ), + ), + ( + "poi", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="cms.poi", + verbose_name="POI", + ), + ), + ], + options={ + "verbose_name": "contact", + "verbose_name_plural": "contacts", + "ordering": ["name"], + "default_permissions": ("change", "delete", "view"), + "default_related_name": "contact", + }, + ), + ] diff --git a/integreat_cms/cms/models/__init__.py b/integreat_cms/cms/models/__init__.py index 504ce46254..dcba9fb615 100644 --- a/integreat_cms/cms/models/__init__.py +++ b/integreat_cms/cms/models/__init__.py @@ -8,6 +8,7 @@ from .chat.attachment_map import AttachmentMap from .chat.chat_message import ChatMessage from .chat.user_chat import ABTester, UserChat +from .contact.contact import Contact from .events.event import Event from .events.event_translation import EventTranslation from .events.recurrence_rule import RecurrenceRule diff --git a/integreat_cms/cms/models/contact/contact.py b/integreat_cms/cms/models/contact/contact.py new file mode 100644 index 0000000000..afcdaa637f --- /dev/null +++ b/integreat_cms/cms/models/contact/contact.py @@ -0,0 +1,72 @@ +from django.db import models +from django.utils import timezone +from django.utils.functional import cached_property +from django.utils.translation import gettext_lazy as _ + +from ..abstract_base_model import AbstractBaseModel +from ..pois.poi import POI +from ..regions.region import Region + + +class Contact(AbstractBaseModel): + """ + Data model representing a contact + """ + + title = models.CharField(max_length=200, verbose_name=_("title")) + name = models.CharField(max_length=200, verbose_name=_("name")) + poi = models.ForeignKey(POI, on_delete=models.PROTECT, verbose_name=_("POI")) + email = models.EmailField( + blank=True, + verbose_name=_("email address"), + ) + phone_number = models.CharField( + max_length=250, blank=True, verbose_name=_("phone number") + ) + website = models.URLField(blank=True, max_length=250, verbose_name=_("website")) + archived = models.BooleanField( + default=False, + verbose_name=_("archived"), + help_text=_("Whether or not the location is read-only and hidden in the API."), + ) + last_updated = models.DateTimeField( + auto_now=True, + verbose_name=_("modification date"), + ) + created_date = models.DateTimeField( + default=timezone.now, verbose_name=_("creation date") + ) + + @cached_property + def region(self) -> Region: + """ + Returns the region this contact belongs to + + :return: Region this contact belongs to + """ + return self.poi.region + + def __str__(self) -> str: + """ + This overwrites the default Django :meth:`~django.db.models.Model.__str__` method which would return ``Contact object (id)``. + It is used in the Django admin backend and as label for ModelChoiceFields. + + :return: A readable string representation of the contact + """ + return f"{self.title} {self.name}" + + def get_repr(self) -> str: + """ + This overwrites the default Django ``__repr__()`` method which would return ````. + It is used for logging. + + :return: The canonical string representation of the contact + """ + return f"" + + class Meta: + verbose_name = _("contact") + default_related_name = "contact" + verbose_name_plural = _("contacts") + default_permissions = ("change", "delete", "view") + ordering = ["name"] diff --git a/integreat_cms/cms/views/regions/region_actions.py b/integreat_cms/cms/views/regions/region_actions.py index 344a1fd48a..e98ca0da59 100644 --- a/integreat_cms/cms/views/regions/region_actions.py +++ b/integreat_cms/cms/views/regions/region_actions.py @@ -16,7 +16,7 @@ from linkcheck.listeners import disable_listeners from ...decorators import permission_required -from ...models import Page, PushNotification, Region +from ...models import Contact, Page, PushNotification, Region if TYPE_CHECKING: from typing import Any @@ -96,6 +96,8 @@ def delete_region( region.events.update(location=None) # Prevent ProtectedError when media files get deleted before their usages as organization logo region.organizations.all().delete() + # Prevent ProtectedError when location gets deleted before their contacts + Contact.objects.filter(poi__region=region).delete() # Prevent IntegrityError when multiple feedback objects exist region.feedback.all().delete() # Disable linkchecking while deleting this region diff --git a/integreat_cms/locale/de/LC_MESSAGES/django.po b/integreat_cms/locale/de/LC_MESSAGES/django.po index 9ae3d79603..8fa1098a2f 100644 --- a/integreat_cms/locale/de/LC_MESSAGES/django.po +++ b/integreat_cms/locale/de/LC_MESSAGES/django.po @@ -2529,16 +2529,17 @@ msgstr "Die Passwörter stimmen nicht überein." msgid "region" msgstr "Region" -#: cms/models/abstract_content_model.py cms/models/feedback/feedback.py -#: cms/models/languages/language.py cms/models/languages/language_tree_node.py -#: cms/models/media/directory.py cms/models/offers/offer_template.py +#: cms/models/abstract_content_model.py cms/models/contact/contact.py +#: cms/models/feedback/feedback.py cms/models/languages/language.py +#: cms/models/languages/language_tree_node.py cms/models/media/directory.py +#: cms/models/offers/offer_template.py #: cms/models/push_notifications/push_notification.py #: cms/models/regions/region.py cms/models/users/organization.py #: cms/models/users/user_fido_key.py msgid "creation date" msgstr "Erstellungsdatum" -#: cms/models/abstract_content_translation.py +#: cms/models/abstract_content_translation.py cms/models/contact/contact.py #: cms/models/push_notifications/push_notification_translation.py msgid "title" msgstr "Titel" @@ -2612,8 +2613,8 @@ msgstr "" "Kreuzen Sie an, wenn diese Änderung keine Aktualisierung der Übersetzungen " "in anderen Sprachen erfordert." -#: cms/models/abstract_content_translation.py cms/models/languages/language.py -#: cms/models/languages/language_tree_node.py +#: cms/models/abstract_content_translation.py cms/models/contact/contact.py +#: cms/models/languages/language.py cms/models/languages/language_tree_node.py #: cms/models/offers/offer_template.py #: cms/models/push_notifications/push_notification_translation.py #: cms/models/regions/region.py cms/models/users/organization.py @@ -2690,6 +2691,47 @@ msgstr "Nutzer-Chat" msgid "user chats" msgstr "Nutzer-Chats" +#: cms/models/contact/contact.py cms/models/media/directory.py +#: cms/models/media/media_file.py cms/models/offers/offer_template.py +#: cms/models/regions/region.py cms/models/users/organization.py +#: cms/models/users/role.py +msgid "name" +msgstr "Name" + +#: cms/models/contact/contact.py +msgid "POI" +msgstr "Ort" + +#: cms/models/contact/contact.py cms/models/pois/poi.py +msgid "email address" +msgstr "E-Mail-Adresse" + +#: cms/models/contact/contact.py cms/models/pois/poi.py +msgid "phone number" +msgstr "Telefonnummer" + +#: cms/models/contact/contact.py cms/models/pois/poi.py +#: cms/models/users/organization.py +msgid "website" +msgstr "Webseite" + +#: cms/models/contact/contact.py cms/models/events/event.py +#: cms/models/feedback/feedback.py cms/models/pois/poi.py +msgid "archived" +msgstr "archiviert" + +#: cms/models/contact/contact.py cms/models/pois/poi.py +msgid "Whether or not the location is read-only and hidden in the API." +msgstr "Ob der Ort schreibgeschützt und in der API verborgen ist oder nicht." + +#: cms/models/contact/contact.py +msgid "contact" +msgstr "Kontakt" + +#: cms/models/contact/contact.py +msgid "contacts" +msgstr "Kontakte" + #: cms/models/events/event.py cms/models/pois/poi.py #: cms/models/pois/poi_translation.py msgid "location" @@ -2712,11 +2754,6 @@ msgstr "Wiederholungs-Regel" msgid "icon" msgstr "Icon" -#: cms/models/events/event.py cms/models/feedback/feedback.py -#: cms/models/pois/poi.py -msgid "archived" -msgstr "archiviert" - #: cms/models/events/event.py msgid "copy" msgstr "Kopie" @@ -3136,12 +3173,6 @@ msgstr "Sprach-Knoten" msgid "language tree nodes" msgstr "Sprach-Knoten" -#: cms/models/media/directory.py cms/models/media/media_file.py -#: cms/models/offers/offer_template.py cms/models/regions/region.py -#: cms/models/users/organization.py cms/models/users/role.py -msgid "name" -msgstr "Name" - #: cms/models/media/directory.py cms/models/media/media_file.py msgid "parent directory" msgstr "Elternverzeichnis" @@ -3564,22 +3595,6 @@ msgid "The best results are achieved with images in 16:9 aspect ratio." msgstr "" "Die besten Ergebnisse erzielen Sie mit Bildern im Seitenverhältnis 16:9." -#: cms/models/pois/poi.py -msgid "Whether or not the location is read-only and hidden in the API." -msgstr "Ob der Ort schreibgeschützt und in der API verborgen ist oder nicht." - -#: cms/models/pois/poi.py cms/models/users/organization.py -msgid "website" -msgstr "Webseite" - -#: cms/models/pois/poi.py -msgid "email address" -msgstr "E-Mail-Adresse" - -#: cms/models/pois/poi.py -msgid "phone number" -msgstr "Telefonnummer" - #: cms/models/pois/poi.py msgid "temporarily closed" msgstr "Temporär geschlossen" @@ -7247,7 +7262,8 @@ msgstr "Ort löschen" #: cms/templates/pois/poi_form_sidebar/action_box.html msgid "You cannot delete a location which is referenced by an event." msgstr "" -"Der Ort kann nicht gelöscht werden, da er von einer Veranstaltung verwendet wird." +"Der Ort kann nicht gelöscht werden, da er von einer Veranstaltung verwendet " +"wird." #: cms/templates/pois/poi_form_sidebar/action_box.html msgid "To delete this location, you have to delete this event first:" @@ -10773,9 +10789,6 @@ msgstr "" #~ msgid "Duplicate page translation in XLIFF, skipping" #~ msgstr "Doppelte Übersetzung in XLIFF, überspringe" -#~ msgid "POIs" -#~ msgstr "POIs" - #~ msgid "" #~ "To further protect your account you can add additional authentication " #~ "methods (2-factor authentication). The system currently supports the " From 4c3472031b9f5a3daee31e9851c615573dd4a5aa Mon Sep 17 00:00:00 2001 From: lunars97 Date: Tue, 9 Jul 2024 20:07:13 +0200 Subject: [PATCH 03/23] Fix automatic slug update on sbs form --- .../cms/templates/pages/page_sbs.html | 5 ++-- .../release_notes/current/unreleased/2797.yml | 2 ++ .../static/src/js/forms/update-permalink.ts | 30 ++++++++++--------- 3 files changed, 21 insertions(+), 16 deletions(-) create mode 100644 integreat_cms/release_notes/current/unreleased/2797.yml diff --git a/integreat_cms/cms/templates/pages/page_sbs.html b/integreat_cms/cms/templates/pages/page_sbs.html index 80111d04d7..8ceba41922 100644 --- a/integreat_cms/cms/templates/pages/page_sbs.html +++ b/integreat_cms/cms/templates/pages/page_sbs.html @@ -170,7 +170,8 @@

{% translate "Not saved yet" %} {% endif %} -
@@ -183,7 +184,7 @@

- {% render_field page_translation_form.title|add_error_class:"border-red-500" id="target_translation_title" %} + {% render_field page_translation_form.title|add_error_class:"border-red-500" %} diff --git a/integreat_cms/release_notes/current/unreleased/2797.yml b/integreat_cms/release_notes/current/unreleased/2797.yml new file mode 100644 index 0000000000..df3148eb96 --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2797.yml @@ -0,0 +1,2 @@ +en: Fix automatic url update after changing a title of the page on side by side view +de: Behebe einen Fehler bei der automatischen Aktualisierung der Seiten-URL nach Änderung des Seitentitels in der Seitenansicht diff --git a/integreat_cms/static/src/js/forms/update-permalink.ts b/integreat_cms/static/src/js/forms/update-permalink.ts index 08a1fe6f07..f5ccc33946 100644 --- a/integreat_cms/static/src/js/forms/update-permalink.ts +++ b/integreat_cms/static/src/js/forms/update-permalink.ts @@ -40,23 +40,25 @@ window.addEventListener("load", () => { .setAttribute("data-copy-to-clipboard", encodeURI(updatedLink.concat(currentSlug))); }; - if ( - document.getElementById("id_title") && - (document.querySelector('[for="id_title"]') as HTMLElement)?.dataset?.slugifyUrl - ) { - document.getElementById("id_title").addEventListener("focusout", ({ target }) => { + document.querySelectorAll("#id_title").forEach((item) => { + item.addEventListener("focusout", ({ target }) => { const submissionLock = new SubmissionPrevention(".no-premature-submission"); const currentTitle = (target as HTMLInputElement).value; - const dataset = (document.querySelector('[for="id_title"]') as HTMLElement).dataset; - slugify(dataset.slugifyUrl, { title: currentTitle, model_id: dataset.modelId }) - .then((response) => { - /* on success write response to both slug field and permalink */ - slugField.value = response.unique_slug; - updatePermalink(response.unique_slug); - }) - .finally(() => submissionLock.release()); + const nodeList: NodeListOf = document.querySelectorAll( + '[for="id_title"],[for="id_slug"]' + ); + for (const node of nodeList) { + const datasetItem = node.dataset; + slugify(datasetItem.slugifyUrl, { title: currentTitle, model_id: datasetItem.modelId }) + .then((response) => { + /* on success write response to both slug field and permalink */ + slugField.value = response.unique_slug; + updatePermalink(response.unique_slug); + }) + .finally(() => submissionLock.release()); + } }); - } + }); const toggleSlugMode = () => { // Toggle all permalink buttons (and the rendered link) From 7a239e1b30d4355df0b187161829e1b67d08be22 Mon Sep 17 00:00:00 2001 From: Sven Seeberg Date: Wed, 12 Jun 2024 14:15:16 +0200 Subject: [PATCH 04/23] Do not return internal Zammad messages to customers, fix #2843 --- integreat_cms/api/v3/chat/zammad_api.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/integreat_cms/api/v3/chat/zammad_api.py b/integreat_cms/api/v3/chat/zammad_api.py index a0f59192f3..01956712ce 100644 --- a/integreat_cms/api/v3/chat/zammad_api.py +++ b/integreat_cms/api/v3/chat/zammad_api.py @@ -144,14 +144,17 @@ def _transform_attachment( # pylint: disable=method-hidden def get_messages(self, chat: UserChat) -> dict[str, dict | list[dict]]: """ - Get all messages for a given ticket + Get all non-internal messages for a given ticket :param chat: UserChat instance for the relevant Zammad ticket """ + raw_response = self._attempt_call(self.client.ticket.articles, chat.zammad_id) + if not isinstance(raw_response, list): + return self._parse_response(raw_response) # type: ignore[return-value] + response = self._parse_response( - self._attempt_call(self.client.ticket.articles, chat.zammad_id) + [article for article in raw_response if not article.get("internal")] ) - for message in response: if "attachments" in message: message["attachments"] = [ From 45cc6af7a0966085549cf444b832498c991b4065 Mon Sep 17 00:00:00 2001 From: theresantonie Date: Sat, 10 Aug 2024 20:12:52 +0200 Subject: [PATCH 05/23] change order of menu items --- integreat_cms/cms/templates/_base.html | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/integreat_cms/cms/templates/_base.html b/integreat_cms/cms/templates/_base.html index 6f94b4400b..7156cd05a3 100644 --- a/integreat_cms/cms/templates/_base.html +++ b/integreat_cms/cms/templates/_base.html @@ -187,14 +187,6 @@

{% endif %} - {% if perms.cms.manage_translations %} - - {% endif %} {% if perms.cms.view_poi %} {% endif %} + {% if perms.cms.manage_translations %} + + {% endif %} {% if FCM_ENABLED and request.region.push_notifications_enabled and perms.cms.view_pushnotification %} {% endif %} - {% if perms.cms.manage_translations %} + {% if perms.cms.view_contact %}
From 8e18bed204e6005e39a2b87145e01dc108946615 Mon Sep 17 00:00:00 2001 From: MizukiTemma Date: Fri, 9 Aug 2024 12:34:49 +0200 Subject: [PATCH 07/23] Include links of imprints into link replacement Co-authored-by: David Venhoff --- integreat_cms/cms/utils/linkcheck_utils.py | 2 +- integreat_cms/release_notes/current/unreleased/2982.yml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 integreat_cms/release_notes/current/unreleased/2982.yml diff --git a/integreat_cms/cms/utils/linkcheck_utils.py b/integreat_cms/cms/utils/linkcheck_utils.py index ac33be4af6..9d85f1ccaa 100644 --- a/integreat_cms/cms/utils/linkcheck_utils.py +++ b/integreat_cms/cms/utils/linkcheck_utils.py @@ -252,7 +252,7 @@ def replace_links( region_msg, user_msg, ) - models = [PageTranslation, EventTranslation, POITranslation] + models = [PageTranslation, EventTranslation, POITranslation, ImprintPageTranslation] with update_lock: for model in models: filters = {} diff --git a/integreat_cms/release_notes/current/unreleased/2982.yml b/integreat_cms/release_notes/current/unreleased/2982.yml new file mode 100644 index 0000000000..ed01654ab8 --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2982.yml @@ -0,0 +1,2 @@ +en: Include links in imprints for link replacement +de: Beziehe Links in Impressen in der Link-Ersetzung ein From bff00d65f921f0f2e8e93eb15943a224dc21368e Mon Sep 17 00:00:00 2001 From: Dennis Dierkes Date: Mon, 12 Aug 2024 09:28:54 +0200 Subject: [PATCH 08/23] Retry sending failed push notifications that were intended to be sent immediately (#2874) * Implement overdue filter for push notifications Settings can be overridden via environment variables, following the guidelines outlined in https://12factor.net/config Fix pylint and mypi errors * Add hint for overdue messages Fix code style issue * Update firebase_api_client.py * Add fcm prefix to setting * Move setup methods to the top of the class * Retry sending failed push notifications that were intended to be sent immediately. * Update integreat_cms/core/management/commands/send_push_notifications.py Co-authored-by: David Venhoff * Add release note * Fix invalid syntax error --------- Co-authored-by: Dennis Dierkes Co-authored-by: David Venhoff --- .../commands/send_push_notifications.py | 20 +++- .../release_notes/current/unreleased/2874.yml | 2 + .../commands/test_send_push_notifications.py | 104 +++++++++++++++--- 3 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 integreat_cms/release_notes/current/unreleased/2874.yml diff --git a/integreat_cms/core/management/commands/send_push_notifications.py b/integreat_cms/core/management/commands/send_push_notifications.py index 6e32c6e51e..ceeb15fc14 100644 --- a/integreat_cms/core/management/commands/send_push_notifications.py +++ b/integreat_cms/core/management/commands/send_push_notifications.py @@ -51,14 +51,28 @@ def handle(self, *args: Any, **options: Any) -> None: f"The system runs with DEBUG=True but the region with TEST_REGION_SLUG={settings.TEST_REGION_SLUG} does not exist." ) from e - pending_push_notifications = PushNotification.objects.filter( + retain_time = settings.FCM_NOTIFICATION_RETAIN_TIME_IN_HOURS + + scheduled_push_notifications = PushNotification.objects.filter( scheduled_send_date__isnull=False, sent_date__isnull=True, draft=False, scheduled_send_date__lte=timezone.now(), - scheduled_send_date__gte=timezone.now() - - timedelta(hours=settings.FCM_NOTIFICATION_RETAIN_TIME_IN_HOURS), + scheduled_send_date__gte=timezone.now() - timedelta(hours=retain_time), + ) + + failed_push_notifications = PushNotification.objects.filter( + draft=False, + is_template=False, + sent_date__isnull=True, + scheduled_send_date__isnull=True, + created_date__gte=timezone.now() - timedelta(hours=retain_time), + ) + + pending_push_notifications = list(failed_push_notifications) + list( + scheduled_push_notifications ) + if total := len(pending_push_notifications): for counter, push_notification in enumerate(pending_push_notifications): self.send_push_notification(counter, total, push_notification) diff --git a/integreat_cms/release_notes/current/unreleased/2874.yml b/integreat_cms/release_notes/current/unreleased/2874.yml new file mode 100644 index 0000000000..bbcd39b673 --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2874.yml @@ -0,0 +1,2 @@ +en: Retry mechanism for push notifications that were intended to be sent immediately but failed due to an error +de: Mechanismus zum Wiederholen von Push-Benachrichtigungen, die aufgrund eines Fehlers beim ersten Versuch nicht sofort gesendet werden konnten diff --git a/tests/core/management/commands/test_send_push_notifications.py b/tests/core/management/commands/test_send_push_notifications.py index c6c09d04cc..2207d8eea2 100644 --- a/tests/core/management/commands/test_send_push_notifications.py +++ b/tests/core/management/commands/test_send_push_notifications.py @@ -34,6 +34,9 @@ class TestSendPushNotification: patch: Any = None + called_success = 0 + called_error = 0 + def setup_method(self) -> None: self.patch = patch.object( FirebaseApiClient, "_get_access_token", return_value="secret access token" @@ -44,6 +47,31 @@ def teardown_method(self) -> None: self.patch.stop() self.patch = None + @pytest.fixture(autouse=True) + def reset_state(self) -> None: + self.called_error = 0 + self.called_success = 0 + + def success_json_function( + self, _request: requests.PreparedRequest, _context: Any + ) -> dict[str, str | int]: + self.called_success += 1 + + return { + "name": "projects/integreat-2020/messages/1", + "status_code": 200, + } + + def error_json_function( + self, _request: requests.PreparedRequest, _context: Any + ) -> dict[str, str | int]: + self.called_error += 1 + + return { + "name": "projects/integreat-2020/messages/1", + "status_code": 500, + } + @pytest.mark.django_db def test_push_notifications_disabled(self, settings: SettingsWrapper) -> None: """ @@ -75,22 +103,9 @@ def test_push_notifications_nonexisting_testregion( def test_ignore_overdue_notification( self, settings: SettingsWrapper, requests_mock: Mocker ) -> None: - called = 0 - - def json_func( - _request: requests.PreparedRequest, _context: Any - ) -> dict[str, str | int]: - nonlocal called - called += 1 - - return { - "name": "projects/integreat-2020/messages/1", - "status_code": 200, - } - requests_mock.post( "https://fcm.googleapis.com/v1/projects/integreat-2020/messages:send", - json=json_func, + json=self.success_json_function, status_code=200, ) @@ -150,4 +165,63 @@ def json_func( call_command("send_push_notifications") - assert called == 1 + assert self.called_success == 1 + + @pytest.mark.django_db + def test_retry_failed_notification( + self, settings: SettingsWrapper, requests_mock: Mocker + ) -> None: + requests_mock.post( + "https://fcm.googleapis.com/v1/projects/integreat-2020/messages:send", + json=self.error_json_function, + status_code=500, + ) + + german_language = Language.objects.create( + slug="de-test", + bcp47_tag="de", + native_name="Deutsch", + english_name="German", + text_direction="ltr", + primary_country_code="DE", + table_of_contents="Inhaltsverzeichnis", + ) + + region = Region.objects.create(name="unit-test-region") + + LanguageTreeNode.objects.create( + language=german_language, lft=1, rgt=2, tree_id=1, depth=1, region=region + ) + + push_notification = PushNotification.objects.create( + channel="default", + draft=False, + sent_date=None, + mode="ONLY_AVAILABLE", + is_template=False, + template_name=None, + ) + + PushNotificationTranslation.objects.create( + title="Test Push Notification", + text="Test Push Notification", + push_notification=push_notification, + language=german_language, + ) + + push_notification.regions.add(region) + push_notification.save() + + call_command("send_push_notifications") + + assert self.called_error == 1 + + requests_mock.post( + "https://fcm.googleapis.com/v1/projects/integreat-2020/messages:send", + json=self.success_json_function, + status_code=200, + ) + + call_command("send_push_notifications") + + assert self.called_success == 1 From dc5cddd6c06417c401743c20b8d70ac0c64b31b7 Mon Sep 17 00:00:00 2001 From: lunars97 Date: Thu, 8 Aug 2024 14:36:54 +0200 Subject: [PATCH 09/23] Hide links to images with empty alt attributes Add release note Initialize function --- integreat_cms/cms/utils/content_utils.py | 45 +++++++++++++++++++ .../release_notes/current/unreleased/2801.yml | 2 + 2 files changed, 47 insertions(+) create mode 100644 integreat_cms/release_notes/current/unreleased/2801.yml diff --git a/integreat_cms/cms/utils/content_utils.py b/integreat_cms/cms/utils/content_utils.py index 1050ae9427..967384475c 100644 --- a/integreat_cms/cms/utils/content_utils.py +++ b/integreat_cms/cms/utils/content_utils.py @@ -31,6 +31,7 @@ def clean_content(content: str, language_slug: str) -> str: convert_monospaced_tags(content) update_links(content, language_slug) fix_alt_texts(content) + hide_anchor_tag_around_image(content) content_str = tostring(content, encoding="unicode", with_tail=False) return fix_content_link_encoding(content_str) @@ -162,3 +163,47 @@ def fix_alt_texts(content: HtmlElement) -> None: image.attrib["alt"] = media_file.alt_text else: logger.warning("Empty img tag was found.") + + +def hide_anchor_tag_around_image(content: HtmlElement) -> None: + """ + This function checks whether an image tag wrapped by an anchor tag has an empty alt tag, if so it hides anchor tag from screen-reader and tab-key + + :param content: the content which has an anchor tag wrapped around an img tag + """ + + for anchor in content.iter("a"): + children = list(anchor.iterchildren()) + + # Check if the anchor tag has only img children and no other text content + if ( + len(children) == 1 + and (img := children[0]).tag == "img" + and not anchor.text_content().strip() + ): + if img.attrib.get("alt", ""): + if "aria-hidden" in anchor.attrib: + del anchor.attrib["aria-hidden"] + logger.debug( + "Removed 'aria-hidden' from anchor: %r", + tostring(anchor, encoding="unicode"), + ) + if "tabindex" in anchor.attrib: + del anchor.attrib["tabindex"] + logger.debug( + "Removed 'tabindex' from anchor: %r", + tostring(anchor, encoding="unicode"), + ) + else: + # Hide the anchor tag by setting aria-hidden attribute if the image alt text is empty + anchor.set("aria-hidden", "true") + logger.debug( + "Set 'aria-hidden' to true for anchor: %r", + tostring(anchor, encoding="unicode"), + ) + # Unfocus the anchor tag from tab key + anchor.set("tabindex", "-1") + logger.debug( + "Set 'tabindex' to -1 for anchor: %r", + tostring(anchor, encoding="unicode"), + ) diff --git a/integreat_cms/release_notes/current/unreleased/2801.yml b/integreat_cms/release_notes/current/unreleased/2801.yml new file mode 100644 index 0000000000..9f27e29b19 --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2801.yml @@ -0,0 +1,2 @@ +en: Hide links to images with empty alt attributes +de: Blende Links zu Bildern mit leeren Alt-Attributen aus From 41d4d8e9346a929fc3ce3c9194102899008b2ff5 Mon Sep 17 00:00:00 2001 From: charludo Date: Mon, 12 Aug 2024 08:13:00 +0200 Subject: [PATCH 10/23] Fix machine translations being considered as not up-to-date --- integreat_cms/cms/models/abstract_content_translation.py | 7 +++++-- integreat_cms/release_notes/current/unreleased/2807.yml | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 integreat_cms/release_notes/current/unreleased/2807.yml diff --git a/integreat_cms/cms/models/abstract_content_translation.py b/integreat_cms/cms/models/abstract_content_translation.py index 6404ec3fc9..df1c24b011 100644 --- a/integreat_cms/cms/models/abstract_content_translation.py +++ b/integreat_cms/cms/models/abstract_content_translation.py @@ -441,11 +441,14 @@ def is_outdated(self) -> bool: def is_up_to_date(self) -> bool: """ This property checks whether a translation is up to date. - A translation is considered up to date when it is not outdated and not being translated at the moment. + A translation is considered up to date when it is either explicitly set to up-to-date, or has been machine-translated. :return: Flag which indicates whether a translation is up to date """ - return self.translation_state == translation_status.UP_TO_DATE + return self.translation_state in [ + translation_status.UP_TO_DATE, + translation_status.MACHINE_TRANSLATED, + ] @cached_property def translation_state(self) -> str: diff --git a/integreat_cms/release_notes/current/unreleased/2807.yml b/integreat_cms/release_notes/current/unreleased/2807.yml new file mode 100644 index 0000000000..2521baca5c --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2807.yml @@ -0,0 +1,2 @@ +en: Fix a bug where machine-translated pages where considered out-of-date +de: Behebe einen Fehler bei dem maschinell übersetzte Seiten als nicht aktuell markiert wurden From 267ff7537cc09b9f35c19b1300c1559b3544c6d4 Mon Sep 17 00:00:00 2001 From: David Venhoff Date: Wed, 14 Aug 2024 10:17:05 +0200 Subject: [PATCH 11/23] Fix some typos and wrong type hints (#2986) --- integreat_cms/cms/models/abstract_tree_node.py | 14 +++++--------- integreat_cms/cms/models/events/event.py | 7 ++++--- integreat_cms/cms/models/pages/page.py | 6 ++++-- integreat_cms/cms/models/pages/page_translation.py | 14 +++++++------- integreat_cms/cms/models/regions/region.py | 3 +-- 5 files changed, 21 insertions(+), 23 deletions(-) diff --git a/integreat_cms/cms/models/abstract_tree_node.py b/integreat_cms/cms/models/abstract_tree_node.py index 0126f5d9b4..f25d3780e4 100644 --- a/integreat_cms/cms/models/abstract_tree_node.py +++ b/integreat_cms/cms/models/abstract_tree_node.py @@ -13,7 +13,7 @@ from treebeard.ns_tree import NS_Node, NS_NodeManager if TYPE_CHECKING: - from typing import Any + from typing import Any, Self from treebeard.ns_tree import NS_NodeQuerySet @@ -193,9 +193,7 @@ def next_region_sibling(self) -> AbstractTreeNode | None: return siblings[idx + 1] return None - def get_cached_ancestors( - self, include_self: bool = False - ) -> list[AbstractTreeNode]: + def get_cached_ancestors(self, include_self: bool = False) -> list[Self]: """ Get the cached ancestors of a specific node @@ -210,7 +208,7 @@ def get_cached_ancestors( return self._cached_ancestors @cached_property - def cached_parent(self) -> AbstractTreeNode | None: + def cached_parent(self) -> Self | None: """ Get the parent node of the current node object. Caches the result in the object itself to help in loops. @@ -221,9 +219,7 @@ def cached_parent(self) -> AbstractTreeNode | None: return None return self.get_cached_ancestors()[-1] - def get_cached_descendants( - self, include_self: bool = False - ) -> list[AbstractTreeNode]: + def get_cached_descendants(self, include_self: bool = False) -> list[Self]: """ Get the cached descendants of a specific node @@ -238,7 +234,7 @@ def get_cached_descendants( return self._cached_descendants @cached_property - def cached_children(self) -> list[AbstractTreeNode]: + def cached_children(self) -> list[Self]: """ Get all cached children diff --git a/integreat_cms/cms/models/events/event.py b/integreat_cms/cms/models/events/event.py index 8f506cbe93..a694941871 100644 --- a/integreat_cms/cms/models/events/event.py +++ b/integreat_cms/cms/models/events/event.py @@ -20,6 +20,7 @@ if TYPE_CHECKING: from datetime import date + from typing import Self from django.db.models.base import ModelBase @@ -32,7 +33,7 @@ class EventQuerySet(ContentQuerySet): Custom QuerySet to facilitate the filtering by date while taking recurring events into account. """ - def filter_upcoming(self, from_date: datetime | None = None) -> EventQuerySet: + def filter_upcoming(self, from_date: datetime | None = None) -> Self: """ Filter all events that take place after the given date. This is, per definition, if at least one of the following conditions is true: @@ -57,7 +58,7 @@ def filter_upcoming(self, from_date: datetime | None = None) -> EventQuerySet: ) ) - def filter_completed(self, to_date: date | None = None) -> EventQuerySet: + def filter_completed(self, to_date: date | None = None) -> Self: """ Filter all events that are not ongoing and don't have any occurrences in the future. This is, per definition, if at least one of the following conditions is true: @@ -213,7 +214,7 @@ def get_occurrences(self, start: datetime, end: datetime) -> list[datetime]: Get occurrences of the event that overlap with ``[start, end]``. Expects ``start < end``. - :param start: the begin of the requested interval. + :param start: the start of the requested interval. :param end: the end of the requested interval. :return: start datetimes of occurrences of the event that are in the given timeframe """ diff --git a/integreat_cms/cms/models/pages/page.py b/integreat_cms/cms/models/pages/page.py index c4cac53eae..67ffa9ccfe 100644 --- a/integreat_cms/cms/models/pages/page.py +++ b/integreat_cms/cms/models/pages/page.py @@ -7,6 +7,7 @@ from django.conf import settings from django.db import models from django.utils.functional import cached_property +from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from linkcheck.models import Link from treebeard.ns_tree import NS_NodeQuerySet @@ -21,6 +22,7 @@ if TYPE_CHECKING: from typing import Any, Iterator + from django.db.models import QuerySet from django.db.models.base import ModelBase from django.utils.safestring import SafeString @@ -276,7 +278,7 @@ def get_non_archived_children(self) -> Iterator[Page]: yield child_page @classmethod - def get_root_pages(cls, region_slug: str) -> PageQuerySet: + def get_root_pages(cls, region_slug: str) -> QuerySet: """ Gets all root pages @@ -362,7 +364,7 @@ def __str__(self) -> SafeString: :return: A readable string representation of the page """ - return self.best_translation.path() + return mark_safe(self.best_translation.path()) def get_repr(self) -> str: """ diff --git a/integreat_cms/cms/models/pages/page_translation.py b/integreat_cms/cms/models/pages/page_translation.py index 522a748a57..e62f66839e 100644 --- a/integreat_cms/cms/models/pages/page_translation.py +++ b/integreat_cms/cms/models/pages/page_translation.py @@ -18,7 +18,7 @@ from django.utils.functional import Promise from django.utils.safestring import SafeString - from ..language.language import Language + from ..languages.language import Language from ..regions.region import Region from django.utils.safestring import mark_safe @@ -147,7 +147,7 @@ def mirrored_translation_text_or_fallback_message(self) -> str: :return: The text, as specified """ - # If there is no page embetted + # If there is no page embedded if not self.page.mirrored_page: return "" @@ -172,7 +172,7 @@ def mirrored_translation_text_or_fallback_message(self) -> str: if not translations: return "" - # If a fall back exits + # If a fallback exists error_message = ( self.language.message_partial_live_content_not_available if self.content and not self.content.isspace() @@ -295,7 +295,7 @@ def get_translations( cls, region: Region, language: Language ) -> QuerySet[PageTranslation]: """ - This function retrieves the most recent versions of a all :class:`~integreat_cms.cms.models.pages.page_translation.PageTranslation` + This function retrieves the most recent versions of all :class:`~integreat_cms.cms.models.pages.page_translation.PageTranslation` objects of a :class:`~integreat_cms.cms.models.regions.region.Region` in a specific :class:`~integreat_cms.cms.models.languages.language.Language` :param region: The requested :class:`~integreat_cms.cms.models.regions.region.Region` @@ -309,7 +309,7 @@ def get_translations( @classmethod def get_up_to_date_translations( cls, region: Region, language: Language - ) -> QuerySet[PageTranslation]: + ) -> list[PageTranslation]: """ This function is similar to :func:`~integreat_cms.cms.models.pages.page_translation.PageTranslation.get_translations` but returns only page translations which are up to date @@ -329,7 +329,7 @@ def get_up_to_date_translations( @classmethod def get_current_translations( cls, region: Region, language: Language - ) -> QuerySet[PageTranslation]: + ) -> list[PageTranslation]: """ This function is similar to :func:`~integreat_cms.cms.models.pages.page_translation.PageTranslation.get_translations` but returns only page translations which are currently being translated by an external translator @@ -349,7 +349,7 @@ def get_current_translations( @classmethod def get_outdated_translations( cls, region: Region, language: Language - ) -> QuerySet[PageTranslation]: + ) -> list[PageTranslation]: """ This function is similar to :func:`~integreat_cms.cms.models.pages.page_translation.PageTranslation.get_translations` but returns only page translations which are outdated diff --git a/integreat_cms/cms/models/regions/region.py b/integreat_cms/cms/models/regions/region.py index 46575cdf9b..aa1bb13f78 100644 --- a/integreat_cms/cms/models/regions/region.py +++ b/integreat_cms/cms/models/regions/region.py @@ -24,7 +24,6 @@ from django.utils.functional import Promise from django.utils.safestring import SafeString - from ...nominatim_api.utils import BoundingBox from ..languages.language import Language from ..languages.language_tree_node import LanguageTreeNode from ..pages.imprint_page import ImprintPage @@ -575,7 +574,7 @@ def visible_languages(self) -> list[Language]: @cached_property def language_tree_root(self) -> LanguageTreeNode | None: """ - This property returns a the root node of the region's language tree + This property returns the root node of the region's language tree :return: The region's language root node """ From 735dfd0f5809c39b0d0cd858695cc9000c719ed4 Mon Sep 17 00:00:00 2001 From: Dennis Dierkes Date: Wed, 14 Aug 2024 12:23:09 +0200 Subject: [PATCH 12/23] Disable auto-skip on y-axis ticks to ensure all translation report labels are displayed (#2977) * Disable auto-skip on y-axis ticks to ensure all labels are displayed * Add release note about translation chart a11y improvements --- integreat_cms/release_notes/current/unreleased/2977.yml | 2 ++ integreat_cms/static/src/js/analytics/translation_coverage.ts | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 integreat_cms/release_notes/current/unreleased/2977.yml diff --git a/integreat_cms/release_notes/current/unreleased/2977.yml b/integreat_cms/release_notes/current/unreleased/2977.yml new file mode 100644 index 0000000000..9f9c63255a --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2977.yml @@ -0,0 +1,2 @@ +en: The translation report shows all language labels on the y-axis +de: Im Übersetzungsbericht werden nun immer alle Sprachen angezeigt diff --git a/integreat_cms/static/src/js/analytics/translation_coverage.ts b/integreat_cms/static/src/js/analytics/translation_coverage.ts index 28ac90552f..fd5a57bfe7 100644 --- a/integreat_cms/static/src/js/analytics/translation_coverage.ts +++ b/integreat_cms/static/src/js/analytics/translation_coverage.ts @@ -34,6 +34,9 @@ window.addEventListener("load", async () => { display: true, text: chart.getAttribute("data-chart-languages"), }, + ticks: { + autoSkip: false, + }, }, }, }, From 5ba5e46a82aa47c14d4c4d7cf202dd28f5c26c8a Mon Sep 17 00:00:00 2001 From: Peter Nerlich Date: Wed, 14 Aug 2024 14:54:59 +0200 Subject: [PATCH 13/23] ignore soft hyphens when detecting links --- integreat_cms/release_notes/current/unreleased/2998.yml | 2 ++ .../static/src/js/tinymce-plugins/autolink_tel/plugin.js | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 integreat_cms/release_notes/current/unreleased/2998.yml diff --git a/integreat_cms/release_notes/current/unreleased/2998.yml b/integreat_cms/release_notes/current/unreleased/2998.yml new file mode 100644 index 0000000000..94bff6ca74 --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2998.yml @@ -0,0 +1,2 @@ +en: Ignore soft hyphens when detecting links +de: Ignoriere bedingte Trennstriche bei der Erkennung von Links diff --git a/integreat_cms/static/src/js/tinymce-plugins/autolink_tel/plugin.js b/integreat_cms/static/src/js/tinymce-plugins/autolink_tel/plugin.js index b021ed392b..52ae3778f5 100644 --- a/integreat_cms/static/src/js/tinymce-plugins/autolink_tel/plugin.js +++ b/integreat_cms/static/src/js/tinymce-plugins/autolink_tel/plugin.js @@ -28,6 +28,7 @@ // constant values and magic numbers used below const CHAR_CODE_SPACE = 160; + const CHAR_CODE_SOFT_HYPHEN = 173; const NODE_TYPE_ELEMENT = 1; const NODE_TYPE_TEXT = 3; const MIN_RANGE_THRESHOLD = 5; @@ -165,7 +166,7 @@ if (text.charAt(text.length - 1) === ".") { setEnd(rng, endContainer, start - 1); } - text = rng.toString().trim(); + text = rng.toString().trim().replace(String.fromCharCode(CHAR_CODE_SOFT_HYPHEN), ""); const matches = text.match(autoLinkPattern); const phoneMatches = text.match(/(0|\+)[0-9\-/]{6,20}/); const protocol = getDefaultLinkProtocol(editor); From ed09e2e7cd8cbc204fc7bada91e92e7a3d9dd0b2 Mon Sep 17 00:00:00 2001 From: David Venhoff Date: Fri, 16 Aug 2024 16:33:32 +0200 Subject: [PATCH 14/23] Correctly represent the last week in rrules (#2992) * Correctly specify last month for rrule Co-authored-by: Charlotte <47758554+charludo@users.noreply.github.com> --- integreat_cms/cms/constants/weeks.py | 9 +++++++ .../cms/models/events/recurrence_rule.py | 3 ++- .../release_notes/current/unreleased/2989.yml | 2 ++ .../cms/models/events/test_recurrence_rule.py | 24 +++++++++++++++---- 4 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 integreat_cms/release_notes/current/unreleased/2989.yml diff --git a/integreat_cms/cms/constants/weeks.py b/integreat_cms/cms/constants/weeks.py index 0b117a88cb..84bb56b1b3 100644 --- a/integreat_cms/cms/constants/weeks.py +++ b/integreat_cms/cms/constants/weeks.py @@ -33,3 +33,12 @@ (FOURTH, _("Fourth week")), (LAST, _("Last week")), ] + +#: A mapping from our week constants to the expected rrule values +WEEK_TO_RRULE_WEEK = { + FIRST: 1, + SECOND: 2, + THIRD: 3, + FOURTH: 4, + LAST: -1, +} diff --git a/integreat_cms/cms/models/events/recurrence_rule.py b/integreat_cms/cms/models/events/recurrence_rule.py index 0d3be1068f..ed6c39e132 100644 --- a/integreat_cms/cms/models/events/recurrence_rule.py +++ b/integreat_cms/cms/models/events/recurrence_rule.py @@ -206,7 +206,8 @@ def to_ical_rrule(self) -> rrule.rrule: kwargs["byweekday"] = self.weekdays_for_weekly elif self.frequency == frequency.MONTHLY: kwargs["byweekday"] = rrule.weekday( - self.weekday_for_monthly, self.week_for_monthly + self.weekday_for_monthly, + weeks.WEEK_TO_RRULE_WEEK[self.week_for_monthly], ) if self.recurrence_end_date: kwargs["until"] = make_aware( diff --git a/integreat_cms/release_notes/current/unreleased/2989.yml b/integreat_cms/release_notes/current/unreleased/2989.yml new file mode 100644 index 0000000000..280071a18c --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/2989.yml @@ -0,0 +1,2 @@ +en: Fix bug where adding an event on the last weekday of every month only adds it in months with 5 weeks +de: Behebe einen Fehler wobei ein Event, das sich monatlich in der letzten Woche des Monats wiederholen soll, nur in Monaten mit 5 Wochen wiederholt wird diff --git a/tests/cms/models/events/test_recurrence_rule.py b/tests/cms/models/events/test_recurrence_rule.py index 045cbc5975..18d682f0a4 100644 --- a/tests/cms/models/events/test_recurrence_rule.py +++ b/tests/cms/models/events/test_recurrence_rule.py @@ -8,6 +8,7 @@ from typing import TYPE_CHECKING from zoneinfo import ZoneInfo +from integreat_cms.cms.constants import weekdays, weeks from integreat_cms.cms.models import Event, RecurrenceRule if TYPE_CHECKING: @@ -63,7 +64,7 @@ def test_api_rrule_weekly(self) -> None: recurrence_rule = RecurrenceRule( frequency="WEEKLY", interval=1, - weekdays_for_weekly=[0, 1], + weekdays_for_weekly=[weekdays.MONDAY, weekdays.TUESDAY], weekday_for_monthly=None, week_for_monthly=None, recurrence_end_date=None, @@ -77,21 +78,34 @@ def test_api_rrule_monthly(self) -> None: frequency="MONTHLY", interval=1, weekdays_for_weekly=None, - weekday_for_monthly=4, - week_for_monthly=1, + weekday_for_monthly=weekdays.FRIDAY, + week_for_monthly=weeks.FIRST, recurrence_end_date=None, ) self.check_rrule( recurrence_rule, "DTSTART:20300101T113000\nRRULE:FREQ=MONTHLY;BYDAY=+1FR" ) + def test_api_rrule_last_week_in_month(self) -> None: + recurrence_rule = RecurrenceRule( + frequency="MONTHLY", + interval=1, + weekdays_for_weekly=None, + weekday_for_monthly=weekdays.WEDNESDAY, + week_for_monthly=weeks.LAST, + recurrence_end_date=None, + ) + self.check_rrule( + recurrence_rule, "DTSTART:20300101T113000\nRRULE:FREQ=MONTHLY;BYDAY=-1WE" + ) + def test_api_rrule_bimonthly_until(self) -> None: recurrence_rule = RecurrenceRule( frequency="MONTHLY", interval=2, weekdays_for_weekly=None, - weekday_for_monthly=6, - week_for_monthly=1, + weekday_for_monthly=weekdays.SUNDAY, + week_for_monthly=weeks.FIRST, recurrence_end_date=datetime.date(2030, 10, 19), ) self.check_rrule( From b8fbdbfb89932d763cf097684612e38d5a90de70 Mon Sep 17 00:00:00 2001 From: Peter Nerlich Date: Thu, 15 Aug 2024 18:19:42 +0200 Subject: [PATCH 15/23] fix link auto update checkbox always checked --- .../cms/templates/_tinymce_config.html | 2 +- integreat_cms/locale/de/LC_MESSAGES/django.po | 4 +-- .../release_notes/current/unreleased/3001.yml | 2 ++ .../custom_link_input/plugin.js | 32 +++++++++++++++---- 4 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 integreat_cms/release_notes/current/unreleased/3001.yml diff --git a/integreat_cms/cms/templates/_tinymce_config.html b/integreat_cms/cms/templates/_tinymce_config.html index 4682850b4f..8ab3df522a 100644 --- a/integreat_cms/cms/templates/_tinymce_config.html +++ b/integreat_cms/cms/templates/_tinymce_config.html @@ -63,7 +63,7 @@ data-link-dialog-url-text='{% translate "URL" %}' data-link-dialog-text-text='{% translate "Text to display" %}' data-link-dialog-internal_link-text='{% translate "Or link to existing content" %}' - data-link-dialog-autoupdate-text='{% translate "Automatically update the link text when the linked content changes" %}' + data-link-dialog-autoupdate-text='{% translate "Automatically use the title of the linked content for the link" %}' data-custom-plugins="{% get_base_url %}{{ editor_content_js_files.0.url }}" data-content-css="{% get_base_url %}{{ editor_content_css_files.0.url }}" data-content-style="{{ content_style }}" diff --git a/integreat_cms/locale/de/LC_MESSAGES/django.po b/integreat_cms/locale/de/LC_MESSAGES/django.po index b4c9ac4a1a..9fc7f71c38 100644 --- a/integreat_cms/locale/de/LC_MESSAGES/django.po +++ b/integreat_cms/locale/de/LC_MESSAGES/django.po @@ -4707,9 +4707,9 @@ msgid "Or link to existing content" msgstr "Oder auf bestehende Inhalte verlinken" #: cms/templates/_tinymce_config.html -msgid "Automatically update the link text when the linked content changes" +msgid "Automatically use the title of the linked content for the link" msgstr "" -"Den Linktext automatisch aktualisieren, wenn sich der verlinkte Inhalt ändert" +"Automatisch den Titel des verlinkten Inhalts als Linktext verwenden" #: cms/templates/_tinymce_config.html msgid "Media Library..." diff --git a/integreat_cms/release_notes/current/unreleased/3001.yml b/integreat_cms/release_notes/current/unreleased/3001.yml new file mode 100644 index 0000000000..b9b3e4615a --- /dev/null +++ b/integreat_cms/release_notes/current/unreleased/3001.yml @@ -0,0 +1,2 @@ +en: Fix link auto update checkbox always checked +de: Behebe, dass die Link Auto-Update Checkbox ständig aktiviert ist diff --git a/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js b/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js index ffe5d1cde2..7cf2660c04 100644 --- a/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js +++ b/integreat_cms/static/src/js/tinymce-plugins/custom_link_input/plugin.js @@ -62,7 +62,10 @@ import { getCsrfToken } from "../../utils/csrf-token"; const anchor = getAnchor(); const initialText = anchor ? anchor.textContent : editor.selection.getContent({ format: "text" }); const initialUrl = anchor ? anchor.getAttribute("href") : ""; - const initialAutoUpdateValue = anchor ? anchor.getAttribute("data-integreat-auto-update") === "true" : true; + const initialAutoUpdateValue = anchor + ? anchor.getAttribute("data-integreat-auto-update") === "true" + : initialText === ""; + let prevAutoupdateValue = initialAutoUpdateValue; const textDisabled = anchor ? anchor.children.length > 0 : false; let prevSearchText = ""; @@ -85,6 +88,19 @@ import { getCsrfToken } from "../../utils/csrf-token"; const updateDialog = (api) => { let data = api.getData(); + if (data.autoupdate) { + api.disable("text"); + if (!prevAutoupdateValue) { + api.setData({ + url: "", + text: "", + }); + } + } else { + api.enable("text"); + } + prevAutoupdateValue = data.autoupdate; + let urlChangedBySearch = false; // Check if the selected completion changed if (prevSelectedCompletion !== data.completions) { @@ -107,11 +123,12 @@ import { getCsrfToken } from "../../utils/csrf-token"; if (data.completions !== "") { urlChangedBySearch = true; api.setData({ url: data.completions }); - // if the text is not defined by the user, set it to the current completion item - if (!data.text || (userData.text !== data.text && !textDisabled)) { + // if the link should be automatically updated orthe text is not defined by the user, + // set it to the current completion item + if (data.autoupdate || !data.text || (userData.text !== data.text && !textDisabled)) { api.setData({ text: currentCompletionText }); } - } else { + } else if (!data.autoupdate) { // restore the original user data api.setData({ url: userData.url, @@ -268,11 +285,14 @@ import { getCsrfToken } from "../../utils/csrf-token"; // Either insert a new link or update the existing one const anchor = getAnchor(); if (!anchor) { - editor.insertContent(`${text}`); + editor.insertContent( + `${text}` + ); } else { updateLink(editor, anchor, text, { "href": realUrl, - "data-integreat-auto-update": autoupdate, + // If false, remove the attribute rather than writing it out to equal false + "data-integreat-auto-update": autoupdate ? true : null, }); } }, From 24113e91fb35b1f8f02fd82c4dcad503d904fcc6 Mon Sep 17 00:00:00 2001 From: Dennis Dierkes Date: Mon, 19 Aug 2024 09:09:08 +0200 Subject: [PATCH 16/23] Mask api token in MatomoException (#3000) --- integreat_cms/matomo_api/matomo_api_client.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/integreat_cms/matomo_api/matomo_api_client.py b/integreat_cms/matomo_api/matomo_api_client.py index 7d743bc5d4..b6b240a1ca 100644 --- a/integreat_cms/matomo_api/matomo_api_client.py +++ b/integreat_cms/matomo_api/matomo_api_client.py @@ -84,12 +84,15 @@ async def fetch( # Update with the custom params for this request query_params.update(kwargs) + def mask_token_auth(req_url: str) -> str: + return re.sub("&token_auth=[^&]+", "&token_auth=********", req_url) + url = f"{settings.MATOMO_URL}/?{urlencode(query_params)}" logger.debug( "Requesting %r: %s", query_params.get("method"), # Mask auth token in log - re.sub(r"&token_auth=[^&]+", "&token_auth=********", url), + mask_token_auth(url), ) try: async with session.get(url) as response: @@ -101,7 +104,9 @@ async def fetch( raise MatomoException(response_data["message"]) return response_data except aiohttp.ClientError as e: - raise MatomoException(str(e)) from e + raise MatomoException( + f"An error occurred {mask_token_auth(str(e))}" + ) from None async def get_matomo_id_async(self, **query_params: Any) -> list[int]: r""" From 30b5fc5b49bea0cd6315aa9800bfb494aa5e464e Mon Sep 17 00:00:00 2001 From: jarlhengstmengel Date: Thu, 2 May 2024 14:13:34 +0200 Subject: [PATCH 17/23] Add translation status reset bulk action --- .../cms/templates/pages/page_tree.html | 5 ++ integreat_cms/cms/urls/protected.py | 7 ++ integreat_cms/cms/views/pages/__init__.py | 1 + integreat_cms/cms/views/pages/page_actions.py | 8 +- .../cms/views/pages/page_bulk_actions.py | 85 +++++++++++++++++++ .../cms/views/pages/page_tree_view.py | 7 ++ integreat_cms/locale/de/LC_MESSAGES/django.po | 72 ++++++++++++++-- .../release_notes/current/unreleased/2865.yml | 2 + 8 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 integreat_cms/release_notes/current/unreleased/2865.yml diff --git a/integreat_cms/cms/templates/pages/page_tree.html b/integreat_cms/cms/templates/pages/page_tree.html index 2c2d7804dd..e4a5922c80 100644 --- a/integreat_cms/cms/templates/pages/page_tree.html +++ b/integreat_cms/cms/templates/pages/page_tree.html @@ -252,6 +252,11 @@

{% endblocktranslate %} {% endif %} + {% if has_pages_in_translation %} + + {% endif %}