From fbd6a7d729c7f74237e598176a1b20e2f4d8559d Mon Sep 17 00:00:00 2001 From: Leon Date: Mon, 27 Apr 2026 16:07:24 +0200 Subject: [PATCH] add missing attributes --- src/divera247/models/event.py | 64 +++++++++++++- src/divera247/models/message_channel.py | 112 +++++++++++++++++++++--- src/divera247/models/news.py | 61 +++++++++++++ src/divera247/models/pull.py | 50 ++++++++++- src/divera247/models/reporttype.py | 2 + uv.lock | 4 +- 6 files changed, 279 insertions(+), 14 deletions(-) diff --git a/src/divera247/models/event.py b/src/divera247/models/event.py index d28292c..51cb904 100644 --- a/src/divera247/models/event.py +++ b/src/divera247/models/event.py @@ -14,15 +14,32 @@ class EventResult(BaseModel): """Event result schema (event-result).""" + class EventReminder(BaseModel): + """Reminder details in event result.""" + + notification_filter: int | None = Field(default=None, description='Benachrichtigungsfilter') + ts: datetime.datetime | None = Field(default=None, description='UNIX-Timestamp Erinnerung') + send_push: bool | None = Field(default=None, description='Push-Erinnerung aktiv') + send_mail: bool | None = Field(default=None, description='Mail-Erinnerung aktiv') + executed: bool | None = Field(default=None, description='Erinnerung bereits ausgeführt') + id: int | None = Field(default=None, description='ID/Primärschlüssel') foreign_id: str | None = Field(default=None, description='Fremdschlüssel') author_id: int | None = Field(default=None, description='ID des Nutzers') - date: datetime.datetime | None = Field(default=None, description='Terminszeit als UNIX-Timestamp') + cluster_id: int | None = Field(default=None, description='ID der Einheit') + message_channel_id: int | None = Field(default=None, description='ID des Nachrichtenkanals') + date: datetime.datetime | None = Field(default=None, description='Datum als UNIX-Timestamp') start: datetime.datetime | None = Field(default=None, description='Beginn als UNIX-Timestamp') end: datetime.datetime | None = Field(default=None, description='Ende als UNIX-Timestamp') title: str | None = Field(default=None, description='Titel') text: str | None = Field(default=None, description='Meldung') address: str | None = Field(default=None, description='Ort') + lat: float | int | None = Field(default=None, description='Breitengrad') + lng: float | int | None = Field(default=None, description='Längengrad') + fullday: bool | None = Field(default=None, description='Ganztägiger Termin') + days: int | None = Field(default=None, description='Dauer in Tagen') + archive: bool | None = Field(default=None, description='Archiviert') + ts_archive: datetime.datetime | None = Field(default=None, description='Archiv-Zeitpunkt') cluster: Sequence[int] | None = Field(default=None, description='IDs der Standorte') group: Sequence[int] | None = Field(default=None, description='IDs der Gruppen') user_cluster_relation: Sequence[int] | None = Field( @@ -37,8 +54,53 @@ class EventResult(BaseModel): new: bool | None = Field(default=None, description='Neu/Ungelesen') editable: bool | None = Field(default=None, description='Bearbeitbar') answerable: bool | None = Field(default=None, description='Beantwortbar') + custom_answers: bool | None = Field(default=None, description='Eigene Antwortoptionen aktiv') + participation: int | None = Field( + default=None, + description='Eigene Rückmeldung (1=Ja, 2=Unsicher, 3=Nein)', + ) + note: str | None = Field(default=None, description='Eigene Freitext-Rückmeldung') + show_result_count: int | None = Field(default=None, description='Ergebnisanzeige Anzahl') + show_result_names: int | None = Field(default=None, description='Ergebnisanzeige Namen') + count_recipients: int | None = Field(default=None, description='Anzahl Empfänger') + count_read: int | None = Field(default=None, description='Anzahl Gelesen') + ucr_addressed: Sequence[int] | None = Field(default=None, description='Adressierte Benutzer') + ucr_self_addressed: bool | None = Field(default=None, description='Selbst adressiert') + ucr_read: Sequence[int] | None = Field(default=None, description='Lesende Benutzer') hidden: bool | None = Field(default=None, description='Entwurf') deleted: bool | None = Field(default=None, description='Im Archiv') + message_channel: bool | None = Field(default=None, description='Ist Nachrichtenkanal') + attachment_count: int | None = Field(default=None, description='Anzahl Anhänge') + response_type: int | None = Field(default=None, description='Rückmeldetyp') + response_until: bool | None = Field(default=None, description='Rückmeldung zeitlich begrenzt') + ts_response: datetime.datetime | None = Field(default=None, description='Rückmeldung bis') + send_reminder: bool | None = Field(default=None, description='Erinnerung senden') + access_names: bool | None = Field(default=None, description='Namen sichtbar') + access_count: bool | None = Field(default=None, description='Anzahl sichtbar') + participationlist: Mapping[str, Sequence[int]] | None = Field( + default=None, + description='Rückmeldungen nach Antwortoption', + ) + participationcount: Mapping[str, int] | None = Field( + default=None, + description='Anzahl Rückmeldungen nach Antwortoption', + ) + participationnotes: Sequence[JsonPayload] | None = Field( + default=None, + description='Freitext-Rückmeldungen', + ) + multiple_answers: bool | None = Field(default=None, description='Mehrfachauswahl möglich') + send_push: bool | None = Field(default=None, description='Push senden') + send_sms: bool | None = Field(default=None, description='SMS senden') + send_call: bool | None = Field(default=None, description='Sprachanruf senden') + send_mail: bool | None = Field(default=None, description='E-Mail senden') + send_pager: bool | None = Field(default=None, description='Pager senden') + ucr_answered: Sequence[int] | Mapping[str, JsonPayload] | None = Field( + default=None, + description='Rückmeldende Benutzer', + ) + reminder: EventReminder | None = Field(default=None, description='Erinnerung') + ts_publish: datetime.datetime | None = Field(default=None, description='Veröffentlichungszeitpunkt') ts_create: datetime.datetime | None = Field(default=None, description='UNIX-Timestamp Erstelldatum') ts_update: datetime.datetime | None = Field( default=None, diff --git a/src/divera247/models/message_channel.py b/src/divera247/models/message_channel.py index 662b27a..2e39c5b 100644 --- a/src/divera247/models/message_channel.py +++ b/src/divera247/models/message_channel.py @@ -3,6 +3,7 @@ These models map to the schemas defined in ``api_v2_message-channel.yaml``. """ +import datetime from collections.abc import Mapping, Sequence from pydantic import BaseModel, Field @@ -10,6 +11,67 @@ from divera247.models.alarm import JsonPayload +class MessageChannelConfigDefault(BaseModel): + """Default channel access config.""" + + editable: bool | None = Field(default=None, description='Bearbeitbar') + notification: bool | None = Field(default=None, description='Benachrichtigung aktiv') + writable: bool | None = Field(default=None, description='Schreibbar') + attachable: bool | None = Field(default=None, description='Anhänge erlaubt') + access_earlier_messages: bool | None = Field(default=None, description='Zugriff auf ältere Nachrichten') + access_start: bool | int | None = Field(default=None, description='Zugriff ab') + access_expire: bool | int | None = Field(default=None, description='Zugriff bis') + ts_access_start: datetime.datetime | None = Field( + default=None, + description='Zugriff ab als Timestamp', + ) + ts_access_expire: datetime.datetime | None = Field( + default=None, + description='Zugriff bis als Timestamp', + ) + + +class MessageChannelConfig(BaseModel): + """Message channel config container.""" + + default: MessageChannelConfigDefault | None = Field(default=None, description='Default-Konfiguration') + + +class MessageChannelAccessEntry(BaseModel): + """Single ucr_access entry.""" + + user_cluster_relation_id: int | None = Field(default=None, description='UCR ID') + access: int | bool | None = Field(default=None, description='Zugriff') + access_start: bool | int | None = Field(default=None, description='Teilnahme ab') + access_expire: bool | int | None = Field(default=None, description='Teilnahme bis') + access_earlier_messages: int | bool | None = Field(default=None, description='Ältere Nachrichten') + attachable: bool | None = Field(default=None, description='Anhänge erlaubt') + confirmed: int | None = Field(default=None, description='Bestätigt') + editable: bool | None = Field(default=None, description='Bearbeitbar') + notification: bool | None = Field(default=None, description='Benachrichtigung') + silent: bool | None = Field(default=None, description='Stumm') + ts_access_expire: datetime.datetime | None = Field(default=None, description='Zugriff bis') + ts_access_start: datetime.datetime | None = Field(default=None, description='Zugriff ab') + writable: bool | None = Field(default=None, description='Schreibbar') + ts_create: datetime.datetime | None = Field(default=None, description='Erstellt') + + +class MessageChannelSelfAccess(BaseModel): + """Self access information.""" + + editable: bool | None = Field(default=None, description='Bearbeitbar') + writable: bool | None = Field(default=None, description='Schreibbar') + attachable: bool | None = Field(default=None, description='Anhänge erlaubt') + notification: bool | None = Field(default=None, description='Benachrichtigung') + access: bool | None = Field(default=None, description='Zugriff') + access_start: bool | None = Field(default=None, description='Zugriff ab gesetzt') + access_expire: bool | None = Field(default=None, description='Zugriff bis gesetzt') + access_earlier_messages: bool | None = Field(default=None, description='Ältere Nachrichten') + ts_access_start: datetime.datetime | None = Field(default=None, description='Zugriff ab') + ts_access_expire: datetime.datetime | None = Field(default=None, description='Zugriff bis') + ts_create: datetime.datetime | None = Field(default=None, description='Erstellt') + + class MessageChannelResult(BaseModel): """Message channel result schema (message-channel-result).""" @@ -35,11 +97,32 @@ class MessageChannelResult(BaseModel): default=None, description='Für alle Mitglieder zugänglich', ) - ts_create: int | None = Field(default=None, description='UNIX-Timestamp Erstelldatum') - ts_update: int | None = Field( + entities: Mapping[str, Sequence[int]] | None = Field( + default=None, + description='Adressierung nach Typ', + ) + config: MessageChannelConfig | None = Field(default=None, description='Kanal-Konfiguration') + ucr_access: Mapping[str, MessageChannelAccessEntry] | None = Field( + default=None, + description='Zugriffseinträge je UCR', + ) + self_access: MessageChannelSelfAccess | None = Field( + default=None, + description='Eigener Zugriff', + ) + private_mode: bool | None = Field(default=None, description='Sichtbarkeit privat') + editable: bool | None = Field(default=None, description='Bearbeitbar') + ts_publish: datetime.datetime | None = Field(default=None, description='Veröffentlichung') + archive: bool | None = Field(default=None, description='Archiviert') + ts_archive: datetime.datetime | None = Field(default=None, description='Archivierung') + delete: bool | None = Field(default=None, description='Zum Löschen markiert') + ts_delete: datetime.datetime | None = Field(default=None, description='Löschen am') + ts_create: datetime.datetime | None = Field(default=None, description='UNIX-Timestamp Erstelldatum') + ts_update: datetime.datetime | None = Field( default=None, description='UNIX-Timestamp zuletzt bearbeitet', ) + ts_last_message: datetime.datetime | None = Field(default=None, description='Zeitpunkt letzte Nachricht') class MessageChannelsData(BaseModel): @@ -96,16 +179,24 @@ class MessageChannelInputMessageChannel(BaseModel): default=None, description='IDs der Nutzer, Gruppen, Standorte', ) + config: MessageChannelConfig | None = Field( + default=None, + description='Standard-Berechtigungen', + ) + ucr_access: Mapping[str, MessageChannelAccessEntry] | None = Field( + default=None, + description='Berechtigungen je UserClusterRelation', + ) private_mode: bool | None = Field(default=None, description='Sichtbarkeit privat') - ts_publish: int | None = Field( + ts_publish: datetime.datetime | None = Field( default=None, description='Zeitgesteuerte Veröffentlichung', ) - ts_archive: int | None = Field( + ts_archive: datetime.datetime | None = Field( default=None, description='Zeitgesteuerte Archivierung', ) - ts_delete: int | None = Field( + ts_delete: datetime.datetime | None = Field( default=None, description='Zeitgesteuertes Löschen', ) @@ -142,11 +233,11 @@ class MessageChannelNotificationSelfAccess(BaseModel): default=None, description='Stumm schalten ab sofort', ) - ts_silent_expire: int | None = Field( + ts_silent_expire: datetime.datetime | None = Field( default=None, description='Stumm ab als Timestamp', ) - ts_silent_start: int | None = Field( + ts_silent_start: datetime.datetime | None = Field( default=None, description='Stumm bis als Timestamp', ) @@ -179,7 +270,7 @@ class MessagesItemsData(BaseModel): message_count: int | None = Field(default=None, description='Anzahl Nachrichten') first_message_id: int | None = Field(default=None, description='ID erste Nachricht') last_message_id: int | None = Field(default=None, description='ID letzte Nachricht') - ts_last_message: int | None = Field( + ts_last_message: datetime.datetime | None = Field( default=None, description='UNIX-Timestamp letzte Nachricht', ) @@ -199,14 +290,15 @@ class MessagesResponse(BaseModel): class MessageSortingData(BaseModel): """Data for GET /api/v2/message-channels/message-sorting/{id}.""" - sorting: Mapping[str, int] = Field( + ucr: int | None = Field(default=None, description='UCR ID im Response-Objekt') + sorting: Mapping[str, datetime.datetime] = Field( default_factory=dict, description='ID -> UNIX-Timestamp', ) message_count: int | None = Field(default=None, description='Anzahl Nachrichten') first_message_id: int | None = Field(default=None, description='ID erste Nachricht') last_message_id: int | None = Field(default=None, description='ID letzte Nachricht') - ts_last_message: int | None = Field( + ts_last_message: datetime.datetime | None = Field( default=None, description='UNIX-Timestamp letzte Nachricht', ) diff --git a/src/divera247/models/news.py b/src/divera247/models/news.py index eb94904..150d559 100644 --- a/src/divera247/models/news.py +++ b/src/divera247/models/news.py @@ -14,6 +14,49 @@ class NewsResult(BaseModel): """News result schema (news-result).""" + class NewsSurveyCustomAnswer(BaseModel): + """Custom free-text answer in a survey option.""" + + id: int | None = Field(default=None, description='ID') + ts: datetime.datetime | None = Field(default=None, description='UNIX-Timestamp') + note: str | None = Field(default=None, description='Notiz') + + class NewsSurveyAnswer(BaseModel): + """One selectable answer option within a survey.""" + + id: int | None = Field(default=None, description='ID') + title: str | None = Field(default=None, description='Antwort') + note: str | None = Field(default=None, description='Notiz') + answeredcount: int | None = Field(default=None, description='Anzahl Antworten') + answeredlist: Sequence[int] | None = Field(default=None, description='Antwortende Benutzer') + custom_answers: Sequence['NewsResult.NewsSurveyCustomAnswer'] | None = Field( + default=None, + description='Freitext-Antworten', + ) + + class NewsSurvey(BaseModel): + """Survey definition attached to a news item.""" + + id: int | None = Field(default=None, description='ID') + title: str | None = Field(default=None, description='Frage') + show_result_count: int | None = Field(default=None, description='Ergebnisanzeige Anzahl') + show_result_names: int | None = Field(default=None, description='Ergebnisanzeige Namen') + access_count: bool | None = Field(default=None, description='Anzahl sichtbar') + access_names: bool | None = Field(default=None, description='Namen sichtbar') + multiple_answers: bool | None = Field(default=None, description='Mehrfachauswahl möglich') + custom_answers: bool | None = Field(default=None, description='Freitext möglich') + response_until: bool | None = Field(default=None, description='Rückmeldung zeitlich begrenzt') + ts_response: datetime.datetime | None = Field(default=None, description='Rückmeldung bis') + answers: Sequence['NewsResult.NewsSurveyAnswer'] | None = Field( + default=None, + description='Antwortoptionen', + ) + answer_sorting: Sequence[str | int] | None = Field( + default=None, + alias='answerSorting', + description='Antwort-Reihenfolge', + ) + id: int | None = Field(default=None, description='ID/Primärschlüssel') foreign_id: str | None = Field(default=None, description='Fremdschlüssel') author_id: int | None = Field(default=None, description='ID des Nutzers') @@ -35,11 +78,29 @@ class NewsResult(BaseModel): default=None, description='Empfänger-Auswahl (1-4)', ) + send_push: bool | None = Field(default=None, description='Push senden') + send_sms: bool | None = Field(default=None, description='SMS senden') + send_call: bool | None = Field(default=None, description='Sprachanruf senden') + send_mail: bool | None = Field(default=None, description='E-Mail senden') + send_pager: bool | None = Field(default=None, description='Pager senden') + survey: bool | None = Field(default=None, description='Umfrage aktiv') + surveys: Sequence[NewsSurvey] | None = Field(default=None, description='Umfragen') new: bool | None = Field(default=None, description='Neu/Ungelesen') editable: bool | None = Field(default=None, description='Bearbeitbar') answerable: bool | None = Field(default=None, description='Beantwortbar') hidden: bool | None = Field(default=None, description='Entwurf') deleted: bool | None = Field(default=None, description='Im Archiv') + ucr_addressed: Sequence[int] | None = Field(default=None, description='Adressierte Benutzer') + ucr_answered: Sequence[int] | Mapping[str, JsonPayload] | None = Field( + default=None, + description='Rückmeldende Benutzer', + ) + ucr_self_addressed: bool | None = Field(default=None, description='Selbst adressiert') + count_recipients: int | None = Field(default=None, description='Anzahl Empfänger') + count_read: int | None = Field(default=None, description='Anzahl Gelesen') + ts_publish: datetime.datetime | None = Field(default=None, description='Veröffentlichungszeitpunkt') + archive: bool | None = Field(default=None, description='Archiviert') + ts_archive: datetime.datetime | None = Field(default=None, description='Archivierungszeitpunkt') ts_create: datetime.datetime | None = Field(default=None, description='UNIX-Timestamp Erstelldatum') ts_update: datetime.datetime | None = Field( default=None, diff --git a/src/divera247/models/pull.py b/src/divera247/models/pull.py index 1e69a9b..27b233f 100644 --- a/src/divera247/models/pull.py +++ b/src/divera247/models/pull.py @@ -366,12 +366,60 @@ class PullLocalmonitorData(BaseModel): class PullMonitorData(BaseModel): """Personnel availability (monitor) in pull data.""" + class AnonymousStatusEntry(BaseModel): + """Aggregated anonymous availability for one status.""" + + all: int | None = Field(default=None, description='Anzahl Nutzer im Status') + qualification: Mapping[str, int] = Field( + default_factory=dict, + description='Anzahl nach Qualifikation (qualification_id -> count)', + ) + + class UserRef(BaseModel): + """Minimal user reference in monitor qualification lists.""" + + id: int | None = Field(default=None, description='ID des Benutzers') + + class DetailedStatusEntry(BaseModel): + """Detailed availability for one status.""" + + all: Sequence[StatusChangeEntry] = Field( + default_factory=tuple, + description='Nutzer mit Statusdetails', + ) + qualification: Mapping[str, Sequence['PullMonitorData.UserRef']] = Field( + default_factory=dict, + description='Nutzer nach Qualifikation (qualification_id -> user refs)', + ) + + anonymous_by_status: Mapping[str, AnonymousStatusEntry] = Field( + default_factory=dict, + alias='1', + description='Anonyme Aggregation nach Status', + ) + detailed_by_status: Mapping[str, DetailedStatusEntry] = Field( + default_factory=dict, + alias='2', + description='Detaillierte Nutzerlisten nach Status', + ) + users: Mapping[str, StatusChangeEntry] = Field( + default_factory=dict, + alias='3', + description='Aktueller Status je Nutzer (user_id -> entry)', + ) ts: datetime.datetime | None = Field( default=None, description='Letzte Änderung als UNIX-Timestamp', ) cached: bool | None = Field(default=None, description='Aus Cache') - # Keys "1", "2", "3" etc. are group/category IDs, values are status_id -> count mappings + + @field_validator('anonymous_by_status', 'detailed_by_status', 'users', mode='before') + @classmethod + def _coerce_monitor_mappings(cls, v: object) -> Mapping[str, object]: + """Coerce legacy scalar fixtures into empty mapping.""" + if isinstance(v, dict): + return cast('Mapping[str, object]', v) + return cast('Mapping[str, object]', {}) class PullConsumerData(BaseModel): diff --git a/src/divera247/models/reporttype.py b/src/divera247/models/reporttype.py index 60cebc4..4280fca 100644 --- a/src/divera247/models/reporttype.py +++ b/src/divera247/models/reporttype.py @@ -41,6 +41,8 @@ class ReporttypeResult(BaseModel): uploads: bool | None = Field(default=None, description='Dateianhänge möglich') anonym: bool | None = Field(default=None, description='Anonyme Datenerfassung') location: bool | None = Field(default=None, description='Adresse erfassen') + skip_empty: bool | None = Field(default=None, description='Leere Felder überspringen') + pdf_layout: str | None = Field(default=None, description='PDF-Layout') fields: Sequence[ReporttypeField] | None = Field( default=None, description='Eingabefelder', diff --git a/uv.lock b/uv.lock index 0e93aaa..60d6687 100644 --- a/uv.lock +++ b/uv.lock @@ -103,10 +103,10 @@ test = [ [package.metadata] requires-dist = [ - { name = "anyio", specifier = ">=4.13.0" }, + { name = "anyio", specifier = ">=4.10,<5" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "httpx-ws", marker = "extra == 'ws'", specifier = ">=0.9.0" }, - { name = "pydantic", specifier = ">=2.12.5" }, + { name = "pydantic", specifier = ">=2.12.2" }, ] provides-extras = ["ws"]