Notify board members about moved cards according to their Deck setting
Replaces the yes/no notify_email flag with notify_mode:
- off: no mail
- owner: only the board owner, as before (migrated from notify_email=1)
- board: everyone with access to the board, filtered by their own Deck
"Notifications" setting for it (board:<id>:notify-due: off /
assigned / all), mirroring Deck's NotificationHelper rules
Recipients are resolved via PermissionService::findUsers() (owner, ACL,
groups and circles expanded), AssignmentMapper::isUserAssigned() and
IConfig, none of which reads Deck's frozen $userId. If they cannot be
resolved, no mail is sent rather than one to the wrong people.
Moved cards are collected over the whole run and each recipient gets a
single digest mail afterwards, in their own language.
Bumps to 35.2.0 so the migrations run.
This commit is contained in:
1 parent
27717205cd
commit
578b235f08
14 files changed
+579
-100
No files matched your search
@@ -25,7 +25,7 @@ Alle Lesezugriffe (Boards, Stacks, Karten) und das Verschieben laufen über Deck
|
||||
|
||||
**Regel:** Bevor ein weiterer `OCA\Deck\*`-Dienst im Runner genutzt wird, prüfen, ob er eine eingefrorene `$userId` trägt – der Fehler ist still und nutzerübergreifend. Bevorzugt Mapper (QBMapper ohne Nutzerzustand) oder Methoden mit explizitem uid-Parameter verwenden. Deshalb wird `BoardService::getUserBoards()` nicht verwendet.
|
||||
|
||||
Die Fälligkeitslogik `WorkflowRunner::isOverdue()` ist bewusst `static` und seiteneffektfrei → unit-testbar (`tests/Unit/Service/WorkflowRunnerTest.php`), der einzige getestete Teil der App. Neue reine Logik in dieser Form halten.
|
||||
Die Fälligkeitslogik `WorkflowRunner::isOverdue()` und die Empfängerregel `NotificationPolicy::shouldNotify()` sind bewusst `static` und seiteneffektfrei, also ohne Deck unit-testbar (`tests/Unit/Service/`). Das sind die einzigen getesteten Teile der App. Neue reine Logik in dieser Form halten.
|
||||
|
||||
## Workflows werden deaktiviert, wenn Board/Stack verschwinden
|
||||
|
||||
@@ -34,7 +34,7 @@ Die Fälligkeitslogik `WorkflowRunner::isOverdue()` ist bewusst `static` und sei
|
||||
- **Nur positives Wissen deaktiviert.** `findStackIds()` liefert `null` nur bei „Board-Zeile weg“ (`DoesNotExistException`, Decks `NotFoundException`), „Board im Papierkorb“ (`deleted_at > 0`) oder „uid hat keine Leserechte“ – alles andere *wirft*, und der Runner überspringt den Workflow. Degradiert die Methode auf `[]`/`null`, deaktiviert ein Deck-Ausfall alle Workflows der Instanz und mailt alle Nutzer an.
|
||||
- **`findStackIds()` fragt nie einen Deck-Dienst, *wer* fragt.** Sie bekommt die uid als Argument und nutzt nur `BoardMapper`/`StackMapper` plus `PermissionService::getPermissions($boardId, $userId)` mit expliziter uid. Nicht zu `StackService::findAll($boardId)` „vereinfachen“ – genau das prüfte gegen die eingefrorene `null`-uid, warf `NoPermissionException`, wurde als „Board weg“ gelesen und verschickte Deaktivierungsmails für intakte Boards.
|
||||
- **`NoPermissionException` beweist nichts** und steht deshalb bewusst nicht in `isMissing()`.
|
||||
- **Die Deaktivierungsmail ignoriert `notifyEmail`** (das betrifft verschobene Karten). Sie bleibt einmalig, weil `findAllEnabled()` deaktivierte Workflows nicht mehr lädt.
|
||||
- **Die Deaktivierungsmail ignoriert `notifyMode`** (das betrifft verschobene Karten) und geht immer nur an den Owner. Sie bleibt einmalig, weil `findAllEnabled()` deaktivierte Workflows nicht mehr lädt.
|
||||
|
||||
Archivierte Boards lösen das nicht aus (sie bleiben lesbar), werden aber in den Dropdowns ausgeblendet.
|
||||
|
||||
@@ -49,6 +49,19 @@ Wird ein Board in Deck übertragen, wandern seine Workflows mit.
|
||||
|
||||
`user_id` heißt also nur „als wer läuft der Job / wer bekommt Mail“.
|
||||
|
||||
## Mails über verschobene Karten folgen Decks Benachrichtigungseinstellung
|
||||
|
||||
`notify_mode` (`NotificationPolicy::MODES`) ersetzt seit `Version1002…`/`Version1003…` das frühere `notify_email` (`true` → `owner`):
|
||||
|
||||
- `off`: keine Mail. `owner`: nur der Board-Owner, ohne Rücksicht auf seine Deck-Einstellung (altes Verhalten).
|
||||
- `board`: alle Board-Berechtigten, gefiltert nach deren Deck-Nutzereinstellung `deck` / `board:<id>:notify-due` (`off`/`assigned`/`all`, Default `assigned`). `NotificationPolicy::shouldNotify()` bildet Decks Regel aus `NotificationHelper::sendCardDuedate()` nach und ist reine, getestete Logik. Dazu gehört: Der Owner eines ungeteilten Boards wird außer bei `off` immer benachrichtigt.
|
||||
- `DeckIntegrationService::findNotificationRecipients()` nutzt nur uid-explizite Bausteine: `PermissionService::findUsers($boardId, true)` (Owner + ACL, löst Gruppen/Circles auf, `true` umgeht den Prozess-Cache), `AssignmentMapper::isUserAssigned()` (auch Gruppen-/Circle-Zuweisungen) und `IConfig`. `FederatedUser` wird übersprungen. Bei jedem Fehler wirft die Methode, und der Runner verschickt dann **keine** Mail, statt an eine falsch aufgelöste Empfängerliste zu schicken.
|
||||
- Der Runner sammelt verschobene Karten in `$pendingMails` und verschickt **nach** der Nutzer-Schleife (also ohne Impersonation) eine Sammelmail pro Empfänger und Lauf. Die Sprache kommt pro Empfänger aus `IFactory::getUserLanguage()`, weil ein injiziertes `IL10N` im Cron nur die Instanz-Standardsprache kennt.
|
||||
|
||||
## Fälligkeit wird als UTC-Zeitpunkt verglichen
|
||||
|
||||
Decks `duedate` ist eine `DATETIME`-Spalte ohne Zeitzone mit UTC-Uhrzeit (Frontend sendet `toISOString()`). `Entity` baut daraus `new \DateTime($string)` in PHPs Standardzone, die Nextcloud auf UTC setzt. `WorkflowRunner::asStoredUtc()` legt die Uhrzeit trotzdem ausdrücklich auf UTC fest, damit das Ergebnis nicht von dieser Standardzone abhängt. Decks eigene Pfade `Card::getDaysUntilDue()` (tagesgenau) und `CardMapper::findOverdue()` (vergleicht mit dem `NOW()` der Datenbank, also Ortszeit) sind dafür ungeeignet und werden nicht verwendet.
|
||||
|
||||
## Kartenfilter wurden entfernt
|
||||
|
||||
Die früheren Filter (`filter_user_ids`/`filter_label_ids`) wurden auf Wunsch entfernt. `Version1001Date20260930120000` löscht die Spalten, und gefilterte Workflows laufen seitdem *ungefiltert* weiter (bewusste Entscheidung). Die Spalten müssen gelöscht bleiben: `Entity::fromRow()` ruft pro Spalte einen Setter auf, den `Workflow` nicht mehr hat. Mit entfernt wurden: Labels-/Participants-Endpunkte, `listLabels()`/`listParticipants()`/`findFilterOptions()` und `CardService::enrichCards()` in `getActiveCardsInStack()` (lief über `BoardService::find()` mit eingefrorener uid). Die Karten dort sind also **nicht angereichert** (keine `assignedUsers`/Labels).
|
||||
@@ -78,7 +91,7 @@ Hintergrundjob und persönliche Einstellungen stehen in `appinfo/info.xml` (`<ba
|
||||
|
||||
- **UI:** `src/PersonalSettings.vue` (Vue 3), gemountet aus `templates/settings/personal.php` (Sektion: `lib/Settings/PersonalSection.php` + `Personal.php`) → `src/api.js` → `lib/Controller/WorkflowController.php` (`OCSController`, Routen in `appinfo/routes.php`, `/ocs/v2.php/apps/workflow_deck_automation/api/v1/...`).
|
||||
- **Speicher:** `lib/Db/Workflow.php` / `WorkflowMapper.php`, Tabelle `wfda_workflows` (Migrationen in `lib/Migration/`).
|
||||
- **Automation:** `RunWorkflowsJob` → `WorkflowRunner` → `DeckIntegrationService` (alle Deck-Aufrufe) und `NotificationMailer` (`IMailer`, an die Account-Adresse, nie an eine eingegebene Adresse).
|
||||
- **Automation:** `RunWorkflowsJob` → `WorkflowRunner` → `DeckIntegrationService` (alle Deck-Aufrufe) und `NotificationMailer` (`IMailer`, an die Account-Adressen der Empfänger, nie an eine eingegebene Adresse).
|
||||
- Controller (Dropdowns) und Runner (Karten lesen/verschieben) nutzen denselben `DeckIntegrationService`.
|
||||
|
||||
## Befehle und Verifikation
|
||||
|
||||
Reference in new issue
Block a user