From a346e215cd619319a8322a10ae8ceadf11d9418c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 18 Sep 2017 19:26:09 +0200 Subject: [PATCH] Send notifications when a card is overdue MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- appinfo/database.xml | 5 ++ appinfo/info.xml | 1 + lib/Cron/ScheduledNotifications.php | 126 ++++++++++++++++++++++++++++ lib/Db/Card.php | 3 + lib/Db/CardMapper.php | 27 +++++- lib/Notification/Notifier.php | 26 +++++- 6 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 lib/Cron/ScheduledNotifications.php diff --git a/appinfo/database.xml b/appinfo/database.xml index 895a265c7..74f81ba3e 100644 --- a/appinfo/database.xml +++ b/appinfo/database.xml @@ -162,6 +162,11 @@ timestamp 0 + + notified + boolean + false + deck_cards_stack_id_index diff --git a/appinfo/info.xml b/appinfo/info.xml index c98f28507..07ad719c8 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -31,6 +31,7 @@ OCA\Deck\Cron\DeleteCron + OCA\Deck\Cron\ScheduledNotifications diff --git a/lib/Cron/ScheduledNotifications.php b/lib/Cron/ScheduledNotifications.php new file mode 100644 index 000000000..048cb3baf --- /dev/null +++ b/lib/Cron/ScheduledNotifications.php @@ -0,0 +1,126 @@ + + * + * @author Julius Härtl + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +/** + * Created by PhpStorm. + * User: jus + * Date: 16.05.17 + * Time: 12:34 + */ + +namespace OCA\Deck\Cron; + +use DateTime; +use OC\BackgroundJob\Job; +use OCA\Deck\Db\Acl; +use OCA\Deck\Db\BoardMapper; +use OCA\Deck\Db\Card; +use OCA\Deck\Db\CardMapper; +use OCP\IUser; +use OCP\Notification\IManager; + +class ScheduledNotifications extends Job { + + /** @var CardMapper */ + protected $cardMapper; + /** @var BoardMapper */ + protected $boardMapper; + /** @var IManager */ + protected $notificationManager; + /** @var array */ + private $users = []; + /** @var array */ + private $boards = []; + + public function __construct( + CardMapper $cardMapper, + BoardMapper $boardMapper, + IManager $notificationManager + ) { + $this->cardMapper = $cardMapper; + $this->boardMapper = $boardMapper; + $this->notificationManager = $notificationManager; + } + + public function run($argument) { + // Notify board owner and card creator about overdue cards + // TODO: Once assigning users is possible, those should be notified instead of all users of the board + $cards = $this->cardMapper->findOverdue(); + /** @var Card $card */ + foreach ($cards as $card) { + // check if notification has already been sent + // ideally notifications should not be deleted once seen by the user so we can + // also deliver due date notifications for users who have been added later to a board + // this should maybe be addressed in nextcloud/server + if ($card->getNotified()) { + continue; + } + $boardId = $this->cardMapper->findBoardId($card->getId()); + /** @var IUser $user */ + foreach ($this->getUsers($boardId) as $user) { + $this->sendNotification($user, $card, $boardId); + } + $this->cardMapper->markNotified($card); + } + } + + private function getUsers($boardId) { + // cache users of a board so we don't query them for every cards + if (array_key_exists((string)$boardId, $this->users)) { + return $this->users[(string)$boardId]; + } + $this->boards[(string)$boardId] = $this->boardMapper->find($boardId, false, true); + $users = [$this->boards[(string)$boardId]->getOwner()]; + /** @var Acl $acl */ + foreach ($this->boards[(string)$boardId]->getAcl() as $acl) { + if ($acl->getType() === Acl::PERMISSION_TYPE_USER) { + $users[] = $acl->getParticipant(); + } + if ($acl->getType() === Acl::PERMISSION_TYPE_GROUP) { + $group = \OC::$server->getGroupManager()->get($acl->getParticipant()); + /** @var IUser $user */ + foreach ($group->getUsers() as $user) { + $users[] = $user->getUID(); + } + } + } + $this->users[(string)$boardId] = array_unique($users); + return $this->users[(string)$boardId]; + } + + private function sendNotification($user, $card, $boardId) { + $notification = $this->notificationManager->createNotification(); + $notification + ->setApp('deck') + ->setUser($user) + ->setObject('card', $card->getId()) + ->setSubject('card-overdue', [$card->getTitle(), $this->boards[(string)$boardId]->getTitle()]); + // this is only needed, if a notification exists for a user and the notified attribute is not set on the card + // if ($this->notificationManager->getCount($notification) > 0) + // return; + $notification + ->setDateTime(new DateTime($card->getDuedate())); + $this->notificationManager->notify($notification); + } + +} \ No newline at end of file diff --git a/lib/Db/Card.php b/lib/Db/Card.php index 2c2ba7a39..5680986d1 100644 --- a/lib/Db/Card.php +++ b/lib/Db/Card.php @@ -41,6 +41,7 @@ class Card extends RelationalEntity implements JsonSerializable { protected $order; protected $archived = false; protected $duedate = null; + protected $notified = false; const DUEDATE_FUTURE = 0; const DUEDATE_NEXT = 1; @@ -54,6 +55,7 @@ class Card extends RelationalEntity implements JsonSerializable { $this->addType('lastModified', 'integer'); $this->addType('createdAt', 'integer'); $this->addType('archived', 'boolean'); + $this->addType('notified', 'boolean'); $this->addRelation('labels'); $this->addResolvable('owner'); } @@ -92,6 +94,7 @@ class Card extends RelationalEntity implements JsonSerializable { } } $json['duedate'] = $this->getDuedate(); + unset($json['notified']); return $json; } diff --git a/lib/Db/CardMapper.php b/lib/Db/CardMapper.php index e5f4fa284..8e9fdcc34 100644 --- a/lib/Db/CardMapper.php +++ b/lib/Db/CardMapper.php @@ -45,11 +45,28 @@ class CardMapper extends DeckMapper implements IPermissionMapper { return parent::insert($entity); } - public function update(Entity $entity) { - $entity->setLastModified(time()); + public function update(Entity $entity, $updateModified = true) { + if ($updateModified) + $entity->setLastModified(time()); + + // make sure we only reset the notification flag if the duedate changes + if (in_array('duedate', $entity->getUpdatedFields())) { + $existing = $this->find($entity->getId()); + if ($existing->getDuedate() !== $entity->getDuedate()) + $entity->setNotified(false); + } + + // TODO: also remove pending notifications + return parent::update($entity); } + public function markNotified(Card $card) { + $cardUpdate = new Card(); + $cardUpdate->setId($card->getId()); + $cardUpdate->setNotified(true); + return parent::update($cardUpdate); + } /** * @param $id * @return RelationalEntity if not found @@ -84,6 +101,12 @@ class CardMapper extends DeckMapper implements IPermissionMapper { return $entities; } + public function findOverdue() { + $sql = 'SELECT id,title,duedate,notified from `*PREFIX*deck_cards` WHERE duedate < NOW()'; + $entities = $this->findEntities($sql); + return $entities; + } + public function delete(Entity $entity) { // delete assigned labels $this->labelMapper->deleteLabelAssignmentsForCard($entity->getId()); diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index d1aa3c271..f8d313f1a 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -85,11 +85,35 @@ class Notifier implements INotifier { } $l = $this->l10nFactory->get('deck', $languageCode); $notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('deck', 'deck-dark.svg'))); + $params = $notification->getSubjectParameters(); switch($notification->getSubject()) { + case 'card-overdue': + $cardId = $notification->getObjectId(); + $boardId = $this->cardMapper->findBoardId($cardId); + $notification->setParsedSubject( + (string) $l->t('The card "%s" on "%s" has reached its due date.', $notification->getSubjectParameters()) + ); + // FIXME: Use type that is provided by NC / if custom type is supported + $notification->setRichSubject( + (string) $l->t('The card {card} on {board} has reached its due date.'), + [ + 'card' => [ + 'id' => null, + 'type' => 'announcement', + 'name' => $params[0], + ], + 'board' => [ + 'id' => null, + 'type' => 'announcement', + 'name' => $params[1], + ], + ] + ); + $notification->setLink($this->url->linkToRouteAbsolute('deck.page.index') . '#!/board/'.$boardId.'//card/'.$cardId.''); + break; case 'board-shared': $boardId = $notification->getObjectId(); - $params = $notification->getSubjectParameters(); $initiator = \OC::$server->getUserManager()->get($params[1]); if($initiator !== null) { $dn = $initiator->getDisplayName();