Let workflows move cards by start date instead of due date
Build package / xml-lint (push) Successful in 3m32s
Build package / unit-tests (push) Successful in 4m49s
Build package / php-lint (8.5) (push) Successful in 4m52s
Build package / php-lint (8.4) (push) Successful in 4m56s
Build package / package (push) Successful in 2m28s
Build package / xml-lint (push) Successful in 3m32s
Build package / unit-tests (push) Successful in 4m49s
Build package / php-lint (8.5) (push) Successful in 4m52s
Build package / php-lint (8.4) (push) Successful in 4m56s
Build package / package (push) Successful in 2m28s
Each workflow now has a trigger date (due or start): the runner compares either the card's due date or its start date against now, read straight from Deck's Card entity. Mails and Nextcloud notifications say whether the card was overdue or reached its start date. Cards without the chosen date stay put. Deck only has a start date since 1.18 and Entity getters are magic, so DeckIntegrationService::supportsStartdate() checks the property; without it the form rejects start-date workflows and the runner skips them without disabling them. WorkflowRunner::isOverdue() becomes hasPassed(), as it now covers both dates. Existing workflows keep the due date (migration Version1005). Bumps to 35.4.0 so the migration runs.
This commit is contained in:
1 parent
5bd3181f10
commit
8268f1068d
18 files changed
+286
-71
No files matched your search
@@ -19,7 +19,8 @@ use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Evaluates all enabled workflows and moves overdue cards.
|
||||
* Evaluates all enabled workflows and moves every card whose due date or
|
||||
* start date — whichever the workflow uses, see TriggerDate — has passed.
|
||||
*
|
||||
* This runner has to evaluate rules for many different users within a
|
||||
* single background-job process, so workflows are grouped by owner and
|
||||
@@ -224,11 +225,24 @@ class WorkflowRunner {
|
||||
return;
|
||||
}
|
||||
|
||||
$trigger = (string)$workflow->getTriggerDate();
|
||||
if ($trigger === TriggerDate::START && !$this->deckService->supportsStartdate()) {
|
||||
// Not disabled: the workflow is intact, Deck is just too old for
|
||||
// it, and comes back to life with a Deck update.
|
||||
$this->logger->warning('Skipping workflow {id}: this Deck version has no start date', [
|
||||
'app' => 'workflow_deck_automation',
|
||||
'id' => $workflow->getId(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$moved = [];
|
||||
$now = $this->timeFactory->getDateTime();
|
||||
|
||||
foreach ($cards as $card) {
|
||||
if (!self::isOverdue($card->getDuedate(), $now)) {
|
||||
// Anything but `start` counts as `due`, the column default.
|
||||
$date = $trigger === TriggerDate::START ? $card->getStartdate() : $card->getDuedate();
|
||||
if (!self::hasPassed($date, $now)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -424,7 +438,10 @@ class WorkflowRunner {
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the card's actual due timestamp against $now instead of Deck's own
|
||||
* Whether the card's trigger date (due or start date, see TriggerDate)
|
||||
* lies before $now.
|
||||
*
|
||||
* Compares the card's actual timestamp against $now instead of Deck's own
|
||||
* Card::getDaysUntilDue(), which truncates both sides to midnight before diffing
|
||||
* (DateInterval's %a is a whole-day count) and therefore reports 0 - not overdue -
|
||||
* for any card whose due time has already passed today. That silently held back
|
||||
@@ -432,30 +449,31 @@ class WorkflowRunner {
|
||||
* instead matches what Deck's own UI shows (e.g. "vor 2 Stunden") and has no
|
||||
* day-rounding step to get wrong.
|
||||
*
|
||||
* Both sides are compared as absolute instants, and the due date is first
|
||||
* pinned to UTC via asStoredUtc() — see there for why.
|
||||
* Both sides are compared as absolute instants, and the card's date is
|
||||
* first pinned to UTC via asStoredUtc() — see there for why.
|
||||
*/
|
||||
public static function isOverdue(?DateTimeInterface $duedate, DateTimeInterface $now): bool {
|
||||
return $duedate !== null && self::asStoredUtc($duedate) < $now;
|
||||
public static function hasPassed(?DateTimeInterface $date, DateTimeInterface $now): bool {
|
||||
return $date !== null && self::asStoredUtc($date) < $now;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deck's `duedate` is a DATETIME column without a zone that holds UTC
|
||||
* wall-clock time: Deck's frontend sends `toISOString()`, and the value is
|
||||
* written as plain `Y-m-d H:i:s`. Entity hydrates it with
|
||||
* `new \DateTime($string)`, i.e. in PHP's *default* timezone — correct only
|
||||
* because Nextcloud sets that default to UTC during bootstrap. Reading the
|
||||
* wall clock and pinning it to UTC explicitly makes the comparison
|
||||
* independent of that default, so a server or CLI running with a local
|
||||
* default zone cannot shift every due date by its UTC offset.
|
||||
* Deck's `duedate` and `startdate` are DATETIME columns without a zone
|
||||
* that hold UTC wall-clock time: Deck's frontend sends `toISOString()`,
|
||||
* and the value is written as plain `Y-m-d H:i:s`. Entity hydrates it
|
||||
* with `new \DateTime($string)`, i.e. in PHP's *default* timezone —
|
||||
* correct only because Nextcloud sets that default to UTC during
|
||||
* bootstrap. Reading the wall clock and pinning it to UTC explicitly
|
||||
* makes the comparison independent of that default, so a server or CLI
|
||||
* running with a local default zone cannot shift every date by its UTC
|
||||
* offset.
|
||||
*
|
||||
* Deck itself has two timezone-sensitive paths this deliberately avoids:
|
||||
* Card::getDaysUntilDue() (day-truncated, see isOverdue()) and
|
||||
* Card::getDaysUntilDue() (day-truncated, see hasPassed()) and
|
||||
* CardMapper::findOverdue(), which compares the UTC column against the
|
||||
* database's `NOW()` — the DB session's local time.
|
||||
*/
|
||||
private static function asStoredUtc(DateTimeInterface $duedate): DateTimeImmutable {
|
||||
return new DateTimeImmutable($duedate->format('Y-m-d H:i:s.u'), new DateTimeZone('UTC'));
|
||||
private static function asStoredUtc(DateTimeInterface $date): DateTimeImmutable {
|
||||
return new DateTimeImmutable($date->format('Y-m-d H:i:s.u'), new DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user