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 "