diff --git a/.circleci/config.yml b/.circleci/config.yml
index f2935e841d..c940e212e0 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -19,6 +19,7 @@ jobs:
else
python3 -m venv .venv
source .venv/bin/activate
+ pip install --upgrade pip
pip install -e .[dev-pinned,pinned]
fi
- save_cache:
diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md
index 8148eb5a0d..b79f7c5d61 100644
--- a/.github/ISSUE_TEMPLATE/bug-report.md
+++ b/.github/ISSUE_TEMPLATE/bug-report.md
@@ -34,3 +34,7 @@ labels: 'bug'
```
+
+### Related Issues
+
+
diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md
index 00b8789785..e227c53c3c 100644
--- a/.github/ISSUE_TEMPLATE/feature-request.md
+++ b/.github/ISSUE_TEMPLATE/feature-request.md
@@ -28,3 +28,8 @@ As a [type of user] I want [goals or objectives] so that [values or benefits]. -
### Design Requirements
+
+### Related Issues
+
+
+
diff --git a/.gitignore b/.gitignore
index a61ca98cfe..8dc9526001 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,3 +68,6 @@ integreat_cms/xliff/download
# Postgres folder
.postgres
+
+# Celery
+celerybeat-schedule.db
diff --git a/docs/src/debugging.rst b/docs/src/debugging.rst
index 0e02062648..82772399da 100644
--- a/docs/src/debugging.rst
+++ b/docs/src/debugging.rst
@@ -45,6 +45,40 @@ VSCodium
^^^^^^^^
Coming soon(TM)?
+PyCharm (Professional)
+^^^^^^^^^^^^^^^^^^^^^^
+
+PyCharm Professional has built-in support for django projects, but it needs some configuration to work with the integreat CMS.
+This is unfortunately not the case for the free edition of PyCharm. Students can get a free license for the professional version, however.
+
+Enable Django Support
+---------------------
+
+#. Go to settings → Languages & Frameworks → Django
+#. Click the ``Enable Django Support`` Checkbox
+#. Set the root ``integreat_cms`` directory as the project root
+#. For ``Settings`` use ``integreat_cms/core/docker_settings.py``
+#. Check ``Do not use Django test runner``
+#. For ``Manage script``, use ``integreat_cms/integreat-cms-cli``. If PyCharm does not let you select this script, because it does not end in .py, you can manually specify it in ``.idea/integreat-cms.iml``.
+
+Your configuration should now look similar to this:
+
+ .. image:: images/debugging/debug-pycharm-01-django-config.png
+ :alt: Django configuration
+
+Create a Run Configuration
+--------------------------
+
+#. Create a new ``Django Server`` run configuration
+#. Use a different port (For example 8001) to avoid conflicts with the non-debug server at port 8000
+#. At `Environment Variables`, add these: ``PYTHONUNBUFFERED=1;DJANGO_SETTINGS_MODULE=integreat_cms.core.docker_settings;INTEGREAT_CMS_DEBUG=True``
+
+Start Debugging
+---------------
+
+#. First execute the ``./tools/run.sh`` to make sure that the database is available and all assets are compiled.
+#. Once the server has started, you can start debugging by launching the run configuration
+
Neovim
^^^^^^
diff --git a/docs/src/images/debugging/debug-pycharm-01-django-config.png b/docs/src/images/debugging/debug-pycharm-01-django-config.png
new file mode 100644
index 0000000000..4264d2b032
Binary files /dev/null and b/docs/src/images/debugging/debug-pycharm-01-django-config.png differ
diff --git a/integreat_cms/api/v3/chat/chat_bot.py b/integreat_cms/api/v3/chat/chat_bot.py
deleted file mode 100644
index da5023279f..0000000000
--- a/integreat_cms/api/v3/chat/chat_bot.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""
-Wrapper for the Chat Bot / LLM API
-"""
-
-import requests
-from django.conf import settings
-
-from ....cms.models import Region
-from ....cms.utils.content_translation_utils import get_public_translation_for_link
-
-
-class ChatBot:
- """
- API Wrapper for the LLM / Chat Bot
- """
-
- def __init__(self, hostname: str = "igchat-inference.tuerantuer.org"):
- self.hostname = hostname
-
- def automatic_answer(self, message: str, region: Region, language_slug: str) -> str:
- """
- Get automatic answer to question
- """
- url = f"https://{self.hostname}/chatanswers/extract_answer/"
- body = {"message": message, "language": language_slug, "region": region.slug}
- r = requests.post(url, json=body, timeout=30)
- return self.format_message(r.json())
-
- def format_message(self, response: dict) -> str:
- """
- Transform JSON into readable message
- """
- if "answer" not in response or not response["answer"]:
- return ""
- if "sources" not in response or not response["sources"]:
- return response["answer"]
- sources = "".join(
- [
- f"
{title}"
- for path in response["sources"]
- if (
- title := get_public_translation_for_link(settings.WEBAPP_URL + path)
- )
- ]
- )
- return f"{response['answer']}\n"
-
- def automatic_translation(
- self, message: str, source_lang_slug: str, target_lang_slug: str
- ) -> str:
- """
- Use LLM to translate message
- """
- url = f"https://{self.hostname}/chatanswers/translate_message/"
- body = {
- "message": message,
- "source_language": source_lang_slug,
- "target_language": target_lang_slug,
- }
- response = requests.post(url, json=body, timeout=30).json()
- if "status" in response and response["status"] == "success":
- return response["translation"]
- return ""
diff --git a/integreat_cms/api/v3/chat/user_chat.py b/integreat_cms/api/v3/chat/user_chat.py
index 49743186db..31c0f27b83 100644
--- a/integreat_cms/api/v3/chat/user_chat.py
+++ b/integreat_cms/api/v3/chat/user_chat.py
@@ -7,7 +7,6 @@
import json
import logging
import random
-import socket
from typing import TYPE_CHECKING
from django.http import HttpResponse, JsonResponse
@@ -17,8 +16,8 @@
from ....cms.models import ABTester, AttachmentMap, Language, Region, UserChat
from ...decorators import json_response
-from .chat_bot import ChatBot
-from .zammad_api import ZammadChatAPI
+from .utils.chat_bot import process_answer, process_user_message
+from .utils.zammad_api import ZammadChatAPI
if TYPE_CHECKING:
from django.http import HttpRequest
@@ -227,15 +226,13 @@ def zammad_webhook(request: HttpRequest) -> JsonResponse:
"""
Receive webhooks from Zammad to update the latest article translation
"""
- zammad_url = (
- f"https://{socket.getnameinfo((request.META.get('REMOTE_ADDR'), 0), 0)[0]}"
+ region = get_object_or_404(
+ Region, zammad_webhook_token=request.GET.get("token", None)
)
- region = get_object_or_404(Region, zammad_url=zammad_url)
- client = ZammadChatAPI(region)
+ if not region.integreat_chat_enabled:
+ return JsonResponse({"status": "Integreat Chat disabled"})
webhook_message = json.loads(request.body)
message_text = webhook_message["article"]["body"]
- zammad_chat = UserChat.objects.get(zammad_id=webhook_message["ticket"]["id"])
- chat_bot = ChatBot()
actions = []
if webhook_message["article"]["internal"]:
@@ -249,34 +246,14 @@ def zammad_webhook(request: HttpRequest) -> JsonResponse:
webhook_message["article"]["created_by"]["login"]
== "tech+integreat-cms@tuerantuer.org"
):
- actions.append("question translation")
- client.send_message(
- zammad_chat.zammad_id,
- chat_bot.automatic_translation(
- message_text, zammad_chat.language.slug, region.default_language.slug
- ),
- True,
- True,
+ actions.append("question translation queued")
+ process_user_message.apply_async(
+ args=[message_text, region.slug, webhook_message["ticket"]["id"]]
)
- if answer := chat_bot.automatic_answer(
- message_text, region, zammad_chat.language.slug
- ):
- actions.append("automatic answer")
- client.send_message(
- zammad_chat.zammad_id,
- answer,
- False,
- True,
- )
else:
- actions.append("answer translation")
- client.send_message(
- zammad_chat.zammad_id,
- chat_bot.automatic_translation(
- message_text, region.default_language.slug, zammad_chat.language.slug
- ),
- False,
- True,
+ actions.append("answer translation queued")
+ process_answer.apply_async(
+ args=[message_text, region.slug, webhook_message["ticket"]["id"]]
)
return JsonResponse(
{
diff --git a/integreat_cms/api/v3/chat/utils/__init__.py b/integreat_cms/api/v3/chat/utils/__init__.py
new file mode 100644
index 0000000000..15126b51d8
--- /dev/null
+++ b/integreat_cms/api/v3/chat/utils/__init__.py
@@ -0,0 +1,3 @@
+"""
+Utils for the Integreat Chat
+"""
diff --git a/integreat_cms/api/v3/chat/utils/chat_bot.py b/integreat_cms/api/v3/chat/utils/chat_bot.py
new file mode 100644
index 0000000000..146bb5a1f4
--- /dev/null
+++ b/integreat_cms/api/v3/chat/utils/chat_bot.py
@@ -0,0 +1,111 @@
+"""
+Wrapper for the Chat Bot / LLM API
+"""
+
+from __future__ import annotations
+
+import requests
+from celery import shared_task
+from django.conf import settings
+
+from integreat_cms.cms.models import Region, UserChat
+from integreat_cms.cms.utils.content_translation_utils import (
+ get_public_translation_for_link,
+)
+
+from .zammad_api import ZammadChatAPI
+
+
+def format_message(response: dict) -> str:
+ """
+ Transform JSON into readable message
+ """
+ if "answer" not in response or not response["answer"]:
+ raise ValueError("Could not format message, no answer attribute in response")
+ if "sources" not in response or not response["sources"]:
+ return response["answer"]
+ sources = "".join(
+ [
+ f"{title}"
+ for path in response["sources"]
+ if (title := get_public_translation_for_link(settings.WEBAPP_URL + path))
+ ]
+ )
+ return f"{response['answer']}\n"
+
+
+def automatic_answer(message: str, region: Region, language_slug: str) -> str | None:
+ """
+ Get automatic answer to question
+ """
+ url = (
+ f"https://{settings.INTEGREAT_CHAT_BACK_END_DOMAIN}/chatanswers/extract_answer/"
+ )
+ body = {"message": message, "language": language_slug, "region": region.slug}
+ r = requests.post(url, json=body, timeout=120)
+ return format_message(r.json())
+
+
+def automatic_translation(
+ message: str, source_language_slug: str, target_language_slug: str
+) -> str:
+ """
+ Use LLM to translate message
+ """
+ url = f"https://{settings.INTEGREAT_CHAT_BACK_END_DOMAIN}/chatanswers/translate_message/"
+ body = {
+ "message": message,
+ "source_language": source_language_slug,
+ "target_language": target_language_slug,
+ }
+ response = requests.post(url, json=body, timeout=120).json()
+ if "status" in response and response["status"] == "success":
+ return response["translation"]
+ raise ValueError("Did not receive success response for translation request.")
+
+
+@shared_task
+def process_user_message(
+ message_text: str, region_slug: str, zammad_ticket_id: int
+) -> None:
+ """
+ Process the message from an Integreat App user
+ """
+ zammad_chat = UserChat.objects.get(zammad_id=zammad_ticket_id)
+ region = Region.objects.get(slug=region_slug)
+ client = ZammadChatAPI(region)
+ if translation := automatic_translation(
+ message_text, zammad_chat.language.slug, region.default_language.slug
+ ):
+ client.send_message(
+ zammad_chat.zammad_id,
+ translation,
+ True,
+ True,
+ )
+ if answer := automatic_answer(message_text, region, zammad_chat.language.slug):
+ client.send_message(
+ zammad_chat.zammad_id,
+ answer,
+ False,
+ True,
+ )
+
+
+@shared_task
+def process_answer(message_text: str, region_slug: str, zammad_ticket_id: int) -> None:
+ """
+ Process automatic or counselor answers
+ """
+ zammad_chat = UserChat.objects.get(zammad_id=zammad_ticket_id)
+ region = Region.objects.get(slug=region_slug)
+ client = ZammadChatAPI(region)
+ if translation := automatic_translation(
+ message_text, region.default_language.slug, zammad_chat.language.slug
+ ):
+ client.send_message(
+ zammad_chat.zammad_id,
+ translation,
+ False,
+ True,
+ )
diff --git a/integreat_cms/api/v3/chat/zammad_api.py b/integreat_cms/api/v3/chat/utils/zammad_api.py
similarity index 90%
rename from integreat_cms/api/v3/chat/zammad_api.py
rename to integreat_cms/api/v3/chat/utils/zammad_api.py
index 808f37dce9..5f49cd3c83 100644
--- a/integreat_cms/api/v3/chat/zammad_api.py
+++ b/integreat_cms/api/v3/chat/utils/zammad_api.py
@@ -13,7 +13,7 @@
from requests.exceptions import HTTPError
from zammad_py import ZammadAPI
-from ....cms.models import AttachmentMap, Region, UserChat
+from integreat_cms.cms.models import AttachmentMap, Region, UserChat
logger = logging.getLogger(__name__)
@@ -166,10 +166,20 @@ def get_messages(self, chat: UserChat) -> dict[str, dict | list[dict]]:
# pylint: disable=method-hidden
def send_message(
- self, chat_id: int, message: str, internal: bool = False, auto: bool = False
+ self,
+ chat_id: int,
+ message: str,
+ internal: bool = False,
+ automatic_message: bool = False,
) -> dict:
"""
Post a new message to the given ticket
+
+ param chat_id: Zammad ID of the chat
+ param message: The message body
+ param internal: keep the message internal in Zammad (do not show to user)
+ param automatic_message: sets title to "automatically generated message"
+ return: dict with Zammad article data
"""
params = {
"ticket_id": chat_id,
@@ -178,9 +188,11 @@ def send_message(
"content_type": "text/html",
"internal": internal,
"subject": (
- "automatically generated message" if auto else "app user message"
+ "automatically generated message"
+ if automatic_message
+ else "app user message"
),
- "sender": "Customer" if not auto else "Agent",
+ "sender": "Customer" if not automatic_message else "Agent",
}
return self._parse_response( # type: ignore[return-value]
self._attempt_call(self.client.ticket_article.create, params=params)
diff --git a/integreat_cms/cms/constants/frequency.py b/integreat_cms/cms/constants/frequency.py
index 3326003aad..0c5deafb16 100644
--- a/integreat_cms/cms/constants/frequency.py
+++ b/integreat_cms/cms/constants/frequency.py
@@ -14,6 +14,7 @@
from django.utils.functional import Promise
+# The frequencies must adhere to the icalendar naming: https://icalendar.org/iCalendar-RFC-5545/3-3-10-recurrence-rule.html
#: Daily
DAILY: Final = "DAILY"
diff --git a/integreat_cms/cms/constants/language_color.py b/integreat_cms/cms/constants/language_color.py
index 40725f6ef3..8013358ea1 100644
--- a/integreat_cms/cms/constants/language_color.py
+++ b/integreat_cms/cms/constants/language_color.py
@@ -48,6 +48,16 @@
PINE_GREEN: Final = "#20B2AA"
ALMOND: Final = "#FFDAB9"
CHERRY: Final = "#D62728"
+DUSTY_ORANGE: Final = "#DB6D4B"
+DUST_STORM: Final = "#E0D3C0"
+SILVER: Final = "#C9C7C1"
+LIGHT_GRAY: Final = "#CED6DA"
+DARK_GRAY: Final = "#91B1C0"
+DEEP_PINK: Final = "#F72681"
+DARK_YELLOWISH_GREEN: Final = "#26662C"
+MODERATE_REDDISH_PURPLE: Final = "#9C5B87"
+STRONG_YELLOW: Final = "#C8B03C"
+BRILLIANT_PURPLISH_BLUE = "#6F79AB"
TOTAL_ACCESS: Final = "#000000"
WEB_APP_ACCESS: Final = "#FF00A8"
@@ -87,4 +97,14 @@
(PINE_GREEN, _("Pine green")),
(ALMOND, _("Almond")),
(CHERRY, _("Cherry")),
+ (DUSTY_ORANGE, _("Dusty orange")),
+ (DUST_STORM, _("Dust storm")),
+ (SILVER, _("Silver")),
+ (LIGHT_GRAY, _("Light gray")),
+ (DARK_GRAY, _("Dark gray")),
+ (DEEP_PINK, _("Deep pink")),
+ (DARK_YELLOWISH_GREEN, _("Dark yellowish green")),
+ (MODERATE_REDDISH_PURPLE, _("Moderate reddish purple")),
+ (STRONG_YELLOW, _("Strong yellow")),
+ (BRILLIANT_PURPLISH_BLUE, _("Brilliant purplish blue")),
]
diff --git a/integreat_cms/cms/constants/weekdays.py b/integreat_cms/cms/constants/weekdays.py
index c1bc2c1f72..fc15963a9d 100644
--- a/integreat_cms/cms/constants/weekdays.py
+++ b/integreat_cms/cms/constants/weekdays.py
@@ -45,3 +45,14 @@
#: Weekend: Saturday and Sunday
WEEKEND: Final[list[int]] = [SATURDAY, SUNDAY]
+
+#: A mapping from an ical recurrence rule weekday string to a cms weekday constant
+RRULE_WEEKDAY_TO_WEEKDAY = {
+ "MO": MONDAY,
+ "TU": TUESDAY,
+ "WE": WEDNESDAY,
+ "TH": THURSDAY,
+ "FR": FRIDAY,
+ "SA": SATURDAY,
+ "SU": SUNDAY,
+}
diff --git a/integreat_cms/cms/constants/weeks.py b/integreat_cms/cms/constants/weeks.py
index 84bb56b1b3..e3e8c9efe8 100644
--- a/integreat_cms/cms/constants/weeks.py
+++ b/integreat_cms/cms/constants/weeks.py
@@ -42,3 +42,6 @@
FOURTH: 4,
LAST: -1,
}
+
+#: A mapping from the expected rrule values to our week constants
+RRULE_WEEK_TO_WEEK = {1: FIRST, 2: SECOND, 3: THIRD, 4: FOURTH, -1: LAST}
diff --git a/integreat_cms/cms/fixtures/test_data.json b/integreat_cms/cms/fixtures/test_data.json
index 0c25c7d85d..f87b91b954 100644
--- a/integreat_cms/cms/fixtures/test_data.json
+++ b/integreat_cms/cms/fixtures/test_data.json
@@ -143,6 +143,7 @@
"integreat_chat_enabled": true,
"zammad_url": "https://zammad.example.com",
"zammad_access_token": "dummytoken",
+ "zammad_webhook_token": "07cce1d7-ec79-4ae3-98cf-04d340ae6655",
"zammad_chat_handlers": "handler@example.com",
"chat_beta_tester_percentage": 50,
"offers": [3, 2, 1, 5]
@@ -194,8 +195,9 @@
"machine_translate_events": 1,
"machine_translate_pois": 1,
"integreat_chat_enabled": false,
- "zammad_url": null,
+ "zammad_url": "",
"zammad_access_token": "",
+ "zammad_webhook_token": "eaea8b88-5f17-4d55-b04c-face1264ca45",
"zammad_chat_handlers": "",
"chat_beta_tester_percentage": 0,
"offers": [3, 4, 2]
@@ -247,8 +249,9 @@
"machine_translate_events": 1,
"machine_translate_pois": 1,
"integreat_chat_enabled": false,
- "zammad_url": null,
+ "zammad_url": "",
"zammad_access_token": "",
+ "zammad_webhook_token": "bf21d6b8-0dcf-4ac9-ba00-1befac1ee25a",
"zammad_chat_handlers": "",
"chat_beta_tester_percentage": 0,
"offers": []
@@ -300,8 +303,9 @@
"machine_translate_events": 1,
"machine_translate_pois": 1,
"integreat_chat_enabled": false,
- "zammad_url": null,
+ "zammad_url": "",
"zammad_access_token": "",
+ "zammad_webhook_token": "4f130b2e-b85e-4a0e-8a31-63591adb6b3c",
"zammad_chat_handlers": "",
"chat_beta_tester_percentage": 0,
"offers": []
@@ -353,8 +357,9 @@
"machine_translate_events": 1,
"machine_translate_pois": 1,
"integreat_chat_enabled": false,
- "zammad_url": null,
+ "zammad_url": "",
"zammad_access_token": "",
+ "zammad_webhook_token": "9de4a0be-c394-44a3-8e35-434c5325d8af",
"zammad_chat_handlers": "",
"chat_beta_tester_percentage": 0,
"offers": [1]
@@ -406,8 +411,9 @@
"machine_translate_events": 1,
"machine_translate_pois": 1,
"integreat_chat_enabled": false,
- "zammad_url": null,
+ "zammad_url": "",
"zammad_access_token": "",
+ "zammad_webhook_token": "249bdf60-511d-45a4-a4df-d3b6feca26a1",
"zammad_chat_handlers": "",
"chat_beta_tester_percentage": 0,
"offers": []
@@ -459,8 +465,9 @@
"machine_translate_events": 1,
"machine_translate_pois": 1,
"integreat_chat_enabled": false,
- "zammad_url": null,
+ "zammad_url": "",
"zammad_access_token": "",
+ "zammad_webhook_token": "ef0a7824-1516-45e2-b878-e1ad5a34b70b",
"zammad_chat_handlers": "",
"chat_beta_tester_percentage": 0,
"offers": []
@@ -512,8 +519,9 @@
"machine_translate_events": 1,
"machine_translate_pois": 1,
"integreat_chat_enabled": false,
- "zammad_url": null,
+ "zammad_url": "",
"zammad_access_token": "",
+ "zammad_webhook_token": "1a6940d4-9114-470a-9077-42db05037511",
"zammad_chat_handlers": "",
"chat_beta_tester_percentage": 0,
"offers": []
@@ -821,6 +829,20 @@
"archived": true
}
},
+ {
+ "model": "cms.organization",
+ "pk": 3,
+ "fields": {
+ "name": "Not Referenced Organisation",
+ "slug": "not-referenced-organisation",
+ "icon": 1,
+ "last_updated": "2024-08-08T07:57:42.456Z",
+ "region": 1,
+ "created_date": "2024-08-08T07:57:42.456Z",
+ "website": "https://integreat-app.de/",
+ "archived": false
+ }
+ },
{
"model": "cms.poi",
"pk": 4,
diff --git a/integreat_cms/cms/forms/regions/region_form.py b/integreat_cms/cms/forms/regions/region_form.py
index 144befc6c5..1c839827d9 100644
--- a/integreat_cms/cms/forms/regions/region_form.py
+++ b/integreat_cms/cms/forms/regions/region_form.py
@@ -210,6 +210,7 @@ class Meta:
"integreat_chat_enabled",
"zammad_url",
"zammad_access_token",
+ "zammad_webhook_token",
"zammad_chat_handlers",
"chat_beta_tester_percentage",
]
@@ -397,12 +398,14 @@ def clean(self) -> dict[str, Any]:
# Integreat Chat can only be enabled if Zammad URL and access key are set
if cleaned_data["integreat_chat_enabled"] and (
- not cleaned_data["zammad_url"] or not cleaned_data["zammad_access_token"]
+ not cleaned_data["zammad_url"]
+ or not cleaned_data["zammad_access_token"]
+ or not cleaned_data["zammad_webhook_token"]
):
self.add_error(
"integreat_chat_enabled",
_(
- "A Zammad URL and Access Token are required in order to enable the Integreat Chat."
+ "A Zammad URL, Zammad Webhook Token and Access Token are required in order to enable the Integreat Chat."
),
)
@@ -607,14 +610,14 @@ def clean_hix_enabled(self) -> bool:
)
return cleaned_hix_enabled
- def clean_zammad_url(self) -> str | None:
+ def clean_zammad_url(self) -> str:
"""
Validate the zammad_url field (see :ref:`overriding-modelform-clean-method`).
:return: The validated field
"""
if not (cleaned_zammad_url := self.cleaned_data["zammad_url"]):
- return None
+ return ""
# Remove superfluous path parts
cleaned_zammad_url = cleaned_zammad_url.split("/api/v1")[0]
cleaned_zammad_url = cleaned_zammad_url.rstrip("/")
diff --git a/integreat_cms/cms/migrations/0110_region_zammad_webhook_token_alter_region_zammad_url.py b/integreat_cms/cms/migrations/0110_region_zammad_webhook_token_alter_region_zammad_url.py
new file mode 100644
index 0000000000..60016f4994
--- /dev/null
+++ b/integreat_cms/cms/migrations/0110_region_zammad_webhook_token_alter_region_zammad_url.py
@@ -0,0 +1,67 @@
+# Generated by Django 4.2.13 on 2024-11-08 12:41
+
+import uuid
+from typing import Callable
+
+from django.apps.registry import Apps
+from django.db import migrations, models
+
+
+def set_zammad_urls(
+ apps: Apps, schema_editor: Callable # pylint: disable=unused-argument
+) -> None:
+ """
+ Update empty Zammad URLs to Null
+ """
+ Region = apps.get_model("cms", "Region")
+ Region.objects.filter(zammad_url=None).update(zammad_url="")
+ for region in Region.objects.all():
+ region.zammad_webhook_token = uuid.uuid4()
+ region.save()
+
+
+class Migration(migrations.Migration):
+ """
+ Remove zammad_url unique constraint, replace Null values with empty strings, remove null=true
+ """
+
+ dependencies = [
+ ("cms", "0109_custom_truncating_char_field"),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name="region",
+ name="zammad_webhook_token",
+ field=models.UUIDField(
+ blank=True,
+ default=uuid.uuid4,
+ help_text="Token used by Zammad webhooks to inform the Integreat CMS about changed tickets. The token has to be appended with a token= GET parameter to the webhook path.",
+ verbose_name="Token used by Zammad webhook",
+ ),
+ ),
+ migrations.AlterField(
+ model_name="region",
+ name="zammad_url",
+ field=models.URLField(
+ blank=True,
+ default=None,
+ help_text="URL pointing to this region's Zammad instance. Setting this enables Zammad form offers.",
+ max_length=256,
+ null=True,
+ verbose_name="Zammad-URL",
+ ),
+ ),
+ migrations.RunPython(set_zammad_urls),
+ migrations.AlterField(
+ model_name="region",
+ name="zammad_url",
+ field=models.URLField(
+ blank=True,
+ default="",
+ help_text="URL pointing to this region's Zammad instance. Setting this enables Zammad form offers.",
+ max_length=256,
+ verbose_name="Zammad-URL",
+ ),
+ ),
+ ]
diff --git a/integreat_cms/cms/migrations/0111_alter_language_language_color.py b/integreat_cms/cms/migrations/0111_alter_language_language_color.py
new file mode 100644
index 0000000000..6a3d99b821
--- /dev/null
+++ b/integreat_cms/cms/migrations/0111_alter_language_language_color.py
@@ -0,0 +1,71 @@
+# Generated by Django 4.2.16 on 2024-11-26 07:19
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+ """
+ Add additional language colors
+ """
+
+ dependencies = [
+ ("cms", "0110_region_zammad_webhook_token_alter_region_zammad_url"),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name="language",
+ name="language_color",
+ field=models.CharField(
+ choices=[
+ ("#FFBB78", "Mellow apricot"),
+ ("#2CA02C", "Forest green"),
+ ("#FF9896", "Rose"),
+ ("#C5B0D5", "Tropical violet"),
+ ("#FF4500", "Red"),
+ ("#FFA500", "Orange"),
+ ("#17157D", "Dark blue"),
+ ("#1F77B4", "Green blue"),
+ ("#FFD700", "Yellow"),
+ ("#008080", "Teal"),
+ ("#9EDAE5", "Arctic"),
+ ("#5894E3", "Azure"),
+ ("#17BECF", "Pacific blue"),
+ ("#FF6347", "Orange red"),
+ ("#98DF8A", "Light green"),
+ ("#9467BD", "Violet"),
+ ("#ADFF2F", "Lime"),
+ ("#E377C2", "Lavender"),
+ ("#8C564B", "Brown"),
+ ("#FFA07A", "Pink orange"),
+ ("#FFE4F0", "Pastel pink"),
+ ("#F0E68C", "Khaki"),
+ ("#BCBD22", "Yellow green"),
+ ("#800080", "Mauve"),
+ ("#BA55D3", "Purple"),
+ ("#DBDB8D", "Primrose"),
+ ("#4B5563", "Fiord"),
+ ("#C49C94", "Quicksand"),
+ ("#7F7F7F", "Grey"),
+ ("#26FCFF", "Aqua"),
+ ("#20B2AA", "Pine green"),
+ ("#FFDAB9", "Almond"),
+ ("#D62728", "Cherry"),
+ ("#DB6D4B", "Dusty orange"),
+ ("#E0D3C0", "Dust storm"),
+ ("#C9C7C1", "Silver"),
+ ("#CED6DA", "Light gray"),
+ ("#91B1C0", "Dark gray"),
+ ("#F72681", "Deep pink"),
+ ("#26662C", "Dark yellowish green"),
+ ("#9C5B87", "Moderate reddish purple"),
+ ("#C8B03C", "Strong yellow"),
+ ("#6F79AB", "Brilliant purplish blue"),
+ ],
+ default="#000000",
+ help_text="This color is used to represent the color label of the chosen language",
+ max_length=7,
+ verbose_name="language color",
+ ),
+ ),
+ ]
diff --git a/integreat_cms/cms/models/contact/contact.py b/integreat_cms/cms/models/contact/contact.py
index 510cb93ab6..c6f63ad9f0 100644
--- a/integreat_cms/cms/models/contact/contact.py
+++ b/integreat_cms/cms/models/contact/contact.py
@@ -3,6 +3,7 @@
from django.db.utils import DataError
from django.utils import timezone
from django.utils.functional import cached_property
+from django.utils.translation import gettext
from django.utils.translation import gettext_lazy as _
from ..abstract_base_model import AbstractBaseModel
@@ -91,6 +92,21 @@ def get_additional_attribute(self) -> str:
return ""
+ def label_in_reference_list(self) -> str:
+ """
+ This function returns a display name of this contact for the poi contacts list
+ """
+
+ label = (
+ _("General contact information")
+ if not self.point_of_contact_for
+ else f"{self.point_of_contact_for} {self.name}"
+ )
+ if self.archived:
+ label += " (⚠ " + gettext("Archived") + ")"
+
+ return label
+
def get_repr(self) -> str:
"""
This overwrites the default Django ``__repr__()`` method which would return ````.
diff --git a/integreat_cms/cms/models/regions/region.py b/integreat_cms/cms/models/regions/region.py
index 24d7e40444..51b83104e5 100644
--- a/integreat_cms/cms/models/regions/region.py
+++ b/integreat_cms/cms/models/regions/region.py
@@ -1,6 +1,7 @@
from __future__ import annotations
import logging
+import uuid
from datetime import datetime
from html import escape
from typing import TYPE_CHECKING
@@ -418,9 +419,7 @@ class Region(AbstractBaseModel):
zammad_url = models.URLField(
max_length=256,
blank=True,
- null=True,
- default=None,
- unique=True,
+ default="",
verbose_name=_("Zammad-URL"),
help_text=_(
"URL pointing to this region's Zammad instance. Setting this enables Zammad form offers."
@@ -435,6 +434,15 @@ class Region(AbstractBaseModel):
'Access token for a Zammad user account. In Zammad, the account must be part of the "Agent" role and have full group permissions for the group:'
),
)
+ zammad_webhook_token = models.UUIDField(
+ max_length=64,
+ blank=True,
+ default=uuid.uuid4,
+ verbose_name=_("Token used by Zammad webhook"),
+ help_text=_(
+ "Token used by Zammad webhooks to inform the Integreat CMS about changed tickets. The token has to be appended with a token= GET parameter to the webhook path."
+ ),
+ )
zammad_chat_handlers = models.CharField(
max_length=1024,
blank=True,
diff --git a/integreat_cms/cms/templates/_tinymce_config.html b/integreat_cms/cms/templates/_tinymce_config.html
index 8ab3df522a..5856b9ace8 100644
--- a/integreat_cms/cms/templates/_tinymce_config.html
+++ b/integreat_cms/cms/templates/_tinymce_config.html
@@ -51,6 +51,8 @@
data-speech-icon-text='{% translate "Spoken Languages" %}'
data-speech-icon-src="{% get_base_url %}{% static 'svg/speech.svg' %}"
data-speech-icon-alt="{% translate "Spoken Languages" %}"
+ data-fax-icon-text='{% translate "Fax Number" %}'
+ data-fax-icon-src="{% get_base_url %}{% static 'svg/fax.svg' %}"
data-update-text='{% translate "Update" %}'
data-dialog-submit-text='{% translate "Submit" %}'
data-dialog-cancel-text='{% translate "Cancel" %}'
diff --git a/integreat_cms/cms/templates/events/event_list.html b/integreat_cms/cms/templates/events/event_list.html
index 2e3c723d82..94ca91e258 100644
--- a/integreat_cms/cms/templates/events/event_list.html
+++ b/integreat_cms/cms/templates/events/event_list.html
@@ -7,16 +7,30 @@