From 73a5b9bc75d40a30e7c9674b292957657ac01f63 Mon Sep 17 00:00:00 2001 From: Frederic Guillot Date: Sat, 17 Oct 2015 09:51:15 -0400 Subject: Make user notifications pluggable --- app/Controller/App.php | 2 +- app/Controller/User.php | 10 +- app/Controller/WebNotification.php | 50 ++++++++ app/Controller/Webnotification.php | 39 ------ app/Core/Base.php | 8 +- app/Core/NotificationInterface.php | 22 ---- app/Helper/User.php | 2 +- app/Model/EmailNotification.php | 123 ------------------ app/Model/Notification.php | 187 --------------------------- app/Model/NotificationFilter.php | 199 ----------------------------- app/Model/NotificationType.php | 76 ++++++----- app/Model/OverdueNotification.php | 6 +- app/Model/UserNotification.php | 185 +++++++++++++++++++++++++++ app/Model/UserNotificationFilter.php | 199 +++++++++++++++++++++++++++++ app/Model/UserNotificationType.php | 51 ++++++++ app/Model/UserUnreadNotification.php | 154 ++++++++++++++++++++++ app/Model/WebNotification.php | 156 ---------------------- app/Notification/Mail.php | 140 ++++++++++++++++++++ app/Notification/NotificationInterface.php | 32 +++++ app/Notification/Web.php | 39 ++++++ app/ServiceProvider/ClassProvider.php | 20 +-- app/Subscriber/NotificationSubscriber.php | 2 +- app/Template/app/notifications.php | 4 +- 23 files changed, 922 insertions(+), 784 deletions(-) create mode 100644 app/Controller/WebNotification.php delete mode 100644 app/Controller/Webnotification.php delete mode 100644 app/Core/NotificationInterface.php delete mode 100644 app/Model/EmailNotification.php delete mode 100644 app/Model/Notification.php delete mode 100644 app/Model/NotificationFilter.php create mode 100644 app/Model/UserNotification.php create mode 100644 app/Model/UserNotificationFilter.php create mode 100644 app/Model/UserNotificationType.php create mode 100644 app/Model/UserUnreadNotification.php delete mode 100644 app/Model/WebNotification.php create mode 100644 app/Notification/Mail.php create mode 100644 app/Notification/NotificationInterface.php create mode 100644 app/Notification/Web.php (limited to 'app') diff --git a/app/Controller/App.php b/app/Controller/App.php index 50ac1d32..3f3f0176 100644 --- a/app/Controller/App.php +++ b/app/Controller/App.php @@ -198,7 +198,7 @@ class App extends Base $this->response->html($this->layout('app/notifications', array( 'title' => t('My notifications'), - 'notifications' => $this->webNotification->getAll($user['id']), + 'notifications' => $this->userUnreadNotification->getAll($user['id']), 'user' => $user, ))); } diff --git a/app/Controller/User.php b/app/Controller/User.php index 15e9a0ee..7444ed82 100644 --- a/app/Controller/User.php +++ b/app/Controller/User.php @@ -96,7 +96,7 @@ class User extends Base $this->projectPermission->addMember($project_id, $user_id); if (! empty($values['notifications_enabled'])) { - $this->notificationType->saveUserSelectedTypes($user_id, array(NotificationType::TYPE_EMAIL)); + $this->userNotificationType->saveSelectedTypes($user_id, array(NotificationType::TYPE_EMAIL)); } $this->session->flash(t('User created successfully.')); @@ -201,16 +201,16 @@ class User extends Base if ($this->request->isPost()) { $values = $this->request->getValues(); - $this->notification->saveSettings($user['id'], $values); + $this->userNotification->saveSettings($user['id'], $values); $this->session->flash(t('User updated successfully.')); $this->response->redirect($this->helper->url->to('user', 'notifications', array('user_id' => $user['id']))); } $this->response->html($this->layout('user/notifications', array( 'projects' => $this->projectPermission->getMemberProjects($user['id']), - 'notifications' => $this->notification->readSettings($user['id']), - 'types' => $this->notificationType->getTypes(), - 'filters' => $this->notificationFilter->getFilters(), + 'notifications' => $this->userNotification->readSettings($user['id']), + 'types' => $this->userNotificationType->getTypes(), + 'filters' => $this->userNotificationFilter->getFilters(), 'user' => $user, ))); } diff --git a/app/Controller/WebNotification.php b/app/Controller/WebNotification.php new file mode 100644 index 00000000..dca5cb46 --- /dev/null +++ b/app/Controller/WebNotification.php @@ -0,0 +1,50 @@ +getUserId(); + + $this->userUnreadNotification->markAllAsRead($user_id); + $this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id))); + } + + /** + * Mark a notification as read + * + * @access public + */ + public function remove() + { + $user_id = $this->getUserId(); + $notification_id = $this->request->getIntegerParam('notification_id'); + + $this->userUnreadNotification->markAsRead($user_id, $notification_id); + $this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id))); + } + + private function getUserId() + { + $user_id = $this->request->getIntegerParam('user_id'); + + if (! $this->userSession->isAdmin() && $user_id != $this->userSession->getId()) { + $user_id = $this->userSession->getId(); + } + + return $user_id; + } +} diff --git a/app/Controller/Webnotification.php b/app/Controller/Webnotification.php deleted file mode 100644 index 52e3f266..00000000 --- a/app/Controller/Webnotification.php +++ /dev/null @@ -1,39 +0,0 @@ -userSession->getId(); - - $this->webNotification->markAllAsRead($user_id); - $this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id))); - } - - /** - * Mark a notification as read - * - * @access public - */ - public function remove() - { - $user_id = $this->userSession->getId(); - $notification_id = $this->request->getIntegerParam('notification_id'); - - $this->webNotification->markAsRead($user_id, $notification_id); - $this->response->redirect($this->helper->url->to('app', 'notifications', array('user_id' => $user_id))); - } -} diff --git a/app/Core/Base.php b/app/Core/Base.php index 76723e8f..c542cd47 100644 --- a/app/Core/Base.php +++ b/app/Core/Base.php @@ -49,11 +49,11 @@ use Pimple\Container; * @property \Kanboard\Model\File $file * @property \Kanboard\Model\LastLogin $lastLogin * @property \Kanboard\Model\Link $link - * @property \Kanboard\Model\Notification $notification - * @property \Kanboard\Model\NotificationType $notificationType - * @property \Kanboard\Model\NotificationFilter $notificationFilter + * @property \Kanboard\Model\UserNotification $userNotification + * @property \Kanboard\Model\UserNotificationType $userNotificationType + * @property \Kanboard\Model\UserNotificationFilter $userNotificationFilter + * @property \Kanboard\Model\UserUnreadNotification $userUnreadNotification * @property \Kanboard\Model\OverdueNotification $overdueNotification - * @property \Kanboard\Model\WebNotification $webNotification * @property \Kanboard\Model\Project $project * @property \Kanboard\Model\ProjectActivity $projectActivity * @property \Kanboard\Model\ProjectAnalytic $projectAnalytic diff --git a/app/Core/NotificationInterface.php b/app/Core/NotificationInterface.php deleted file mode 100644 index b6cd1781..00000000 --- a/app/Core/NotificationInterface.php +++ /dev/null @@ -1,22 +0,0 @@ -webNotification->hasNotifications($this->userSession->getId()); + return $this->userUnreadNotification->hasNotifications($this->userSession->getId()); } /** diff --git a/app/Model/EmailNotification.php b/app/Model/EmailNotification.php deleted file mode 100644 index 312828e4..00000000 --- a/app/Model/EmailNotification.php +++ /dev/null @@ -1,123 +0,0 @@ -emailClient->send( - $user['email'], - $user['name'] ?: $user['username'], - $this->getMailSubject($event_name, $event_data), - $this->getMailContent($event_name, $event_data) - ); - } - } - - /** - * Get the mail content for a given template name - * - * @access public - * @param string $event_name Event name - * @param array $event_data Event data - * @return string - */ - public function getMailContent($event_name, array $event_data) - { - return $this->template->render( - 'notification/'.str_replace('.', '_', $event_name), - $event_data + array('application_url' => $this->config->get('application_url')) - ); - } - - /** - * Get the mail subject for a given template name - * - * @access public - * @param string $event_name Event name - * @param array $event_data Event data - * @return string - */ - public function getMailSubject($event_name, array $event_data) - { - switch ($event_name) { - case File::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New attachment'), $event_data); - break; - case Comment::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New comment'), $event_data); - break; - case Comment::EVENT_UPDATE: - $subject = $this->getStandardMailSubject(e('Comment updated'), $event_data); - break; - case Subtask::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New subtask'), $event_data); - break; - case Subtask::EVENT_UPDATE: - $subject = $this->getStandardMailSubject(e('Subtask updated'), $event_data); - break; - case Task::EVENT_CREATE: - $subject = $this->getStandardMailSubject(e('New task'), $event_data); - break; - case Task::EVENT_UPDATE: - $subject = $this->getStandardMailSubject(e('Task updated'), $event_data); - break; - case Task::EVENT_CLOSE: - $subject = $this->getStandardMailSubject(e('Task closed'), $event_data); - break; - case Task::EVENT_OPEN: - $subject = $this->getStandardMailSubject(e('Task opened'), $event_data); - break; - case Task::EVENT_MOVE_COLUMN: - $subject = $this->getStandardMailSubject(e('Column change'), $event_data); - break; - case Task::EVENT_MOVE_POSITION: - $subject = $this->getStandardMailSubject(e('Position change'), $event_data); - break; - case Task::EVENT_MOVE_SWIMLANE: - $subject = $this->getStandardMailSubject(e('Swimlane change'), $event_data); - break; - case Task::EVENT_ASSIGNEE_CHANGE: - $subject = $this->getStandardMailSubject(e('Assignee change'), $event_data); - break; - case Task::EVENT_OVERDUE: - $subject = e('[%s] Overdue tasks', $event_data['project_name']); - break; - default: - $subject = e('Notification'); - } - - return $subject; - } - - /** - * Get the mail subject for a given label - * - * @access private - * @param string $label Label - * @param array $data Template data - * @return string - */ - private function getStandardMailSubject($label, array $data) - { - return sprintf('[%s][%s] %s (#%d)', $data['task']['project_name'], $label, $data['task']['title'], $data['task']['id']); - } -} diff --git a/app/Model/Notification.php b/app/Model/Notification.php deleted file mode 100644 index 692f37a7..00000000 --- a/app/Model/Notification.php +++ /dev/null @@ -1,187 +0,0 @@ -userSession->isLogged() ? $this->userSession->getId() : 0; - $users = $this->notification->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id); - - if (! empty($users)) { - - foreach ($users as $user) { - if ($this->notificationFilter->shouldReceiveNotification($user, $event_data)) { - $this->sendUserNotification($user, $event_name, $event_data); - } - } - - // Restore locales - $this->config->setupTranslations(); - } - } - - /** - * Send notification to someone - * - * @access public - * @param array $user User - * @param string $event_name - * @param array $event_data - */ - public function sendUserNotification(array $user, $event_name, array $event_data) - { - Translator::unload(); - - // Use the user language otherwise use the application language (do not use the session language) - if (! empty($user['language'])) { - Translator::load($user['language']); - } - else { - Translator::load($this->config->get('application_language', 'en_US')); - } - - foreach ($this->notificationType->getUserSelectedTypes($user['id']) as $type) { - $className = strtolower($type).'Notification'; - $this->$className->send($user, $event_name, $event_data); - } - } - - /** - * Get a list of people with notifications enabled - * - * @access public - * @param integer $project_id Project id - * @param integer $exclude_user_id User id to exclude - * @return array - */ - public function getUsersWithNotificationEnabled($project_id, $exclude_user_id = 0) - { - if ($this->projectPermission->isEverybodyAllowed($project_id)) { - return $this->getEverybodyWithNotificationEnabled($exclude_user_id); - } - - return $this->getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id); - } - - /** - * Enable notification for someone - * - * @access public - * @param integer $user_id - * @return boolean - */ - public function enableNotification($user_id) - { - return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 1)); - } - - /** - * Disable notification for someone - * - * @access public - * @param integer $user_id - * @return boolean - */ - public function disableNotification($user_id) - { - return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 0)); - } - - /** - * Save settings for the given user - * - * @access public - * @param integer $user_id User id - * @param array $values Form values - */ - public function saveSettings($user_id, array $values) - { - $this->db->startTransaction(); - - if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) { - $this->enableNotification($user_id); - - $filter = empty($values['notifications_filter']) ? NotificationFilter::FILTER_BOTH : $values['notifications_filter']; - $projects = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']); - $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']); - - $this->notificationFilter->saveUserFilter($user_id, $filter); - $this->notificationFilter->saveUserSelectedProjects($user_id, $projects); - $this->notificationType->saveUserSelectedTypes($user_id, $types); - } - else { - $this->disableNotification($user_id); - } - - $this->db->closeTransaction(); - } - - /** - * Read user settings to display the form - * - * @access public - * @param integer $user_id User id - * @return array - */ - public function readSettings($user_id) - { - $values = $this->db->table(User::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne(); - $values['notification_types'] = $this->notificationType->getUserSelectedTypes($user_id); - $values['notification_projects'] = $this->notificationFilter->getUserSelectedProjects($user_id); - return $values; - } - - /** - * Get a list of project members with notification enabled - * - * @access private - * @param integer $project_id Project id - * @param integer $exclude_user_id User id to exclude - * @return array - */ - private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id) - { - return $this->db - ->table(ProjectPermission::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') - ->join(User::TABLE, 'id', 'user_id') - ->eq('project_id', $project_id) - ->eq('notifications_enabled', '1') - ->neq(User::TABLE.'.id', $exclude_user_id) - ->findAll(); - } - - /** - * Get a list of project members with notification enabled - * - * @access private - * @param integer $exclude_user_id User id to exclude - * @return array - */ - private function getEverybodyWithNotificationEnabled($exclude_user_id) - { - return $this->db - ->table(User::TABLE) - ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') - ->eq('notifications_enabled', '1') - ->neq(User::TABLE.'.id', $exclude_user_id) - ->findAll(); - } -} diff --git a/app/Model/NotificationFilter.php b/app/Model/NotificationFilter.php deleted file mode 100644 index f1ad367b..00000000 --- a/app/Model/NotificationFilter.php +++ /dev/null @@ -1,199 +0,0 @@ - t('All tasks'), - self::FILTER_ASSIGNEE => t('Only for tasks assigned to me'), - self::FILTER_CREATOR => t('Only for tasks created by me'), - self::FILTER_BOTH => t('Only for tasks created by me and assigned to me'), - ); - } - - /** - * Get user selected filter - * - * @access public - * @param integer $user_id - * @return integer - */ - public function getUserSelectedFilter($user_id) - { - return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter'); - } - - /** - * Save selected filter for a user - * - * @access public - * @param integer $user_id - * @param string $filter - */ - public function saveUserFilter($user_id, $filter) - { - $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( - 'notifications_filter' => $filter, - )); - } - - /** - * Get user selected projects - * - * @access public - * @param integer $user_id - * @return array - */ - public function getUserSelectedProjects($user_id) - { - return $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id'); - } - - /** - * Save selected projects for a user - * - * @access public - * @param integer $user_id - * @param integer[] $project_ids - */ - public function saveUserSelectedProjects($user_id, array $project_ids) - { - $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove(); - - foreach ($project_ids as $project_id) { - $this->db->table(self::PROJECT_TABLE)->insert(array( - 'user_id' => $user_id, - 'project_id' => $project_id, - )); - } - } - - /** - * Return true if the user should receive notification - * - * @access public - * @param array $user - * @param array $event_data - * @return boolean - */ - public function shouldReceiveNotification(array $user, array $event_data) - { - $filters = array( - 'filterNone', - 'filterAssignee', - 'filterCreator', - 'filterBoth', - ); - - foreach ($filters as $filter) { - if ($this->$filter($user, $event_data)) { - return $this->filterProject($user, $event_data); - } - } - - return false; - } - - /** - * Return true if the user will receive all notifications - * - * @access public - * @param array $user - * @return boolean - */ - public function filterNone(array $user) - { - return $user['notifications_filter'] == self::FILTER_NONE; - } - - /** - * Return true if the user is the assignee and selected the filter "assignee" - * - * @access public - * @param array $user - * @param array $event_data - * @return boolean - */ - public function filterAssignee(array $user, array $event_data) - { - return $user['notifications_filter'] == self::FILTER_ASSIGNEE && $event_data['task']['owner_id'] == $user['id']; - } - - /** - * Return true if the user is the creator and enabled the filter "creator" - * - * @access public - * @param array $user - * @param array $event_data - * @return boolean - */ - public function filterCreator(array $user, array $event_data) - { - return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id']; - } - - /** - * Return true if the user is the assignee or the creator and selected the filter "both" - * - * @access public - * @param array $user - * @param array $event_data - * @return boolean - */ - public function filterBoth(array $user, array $event_data) - { - return $user['notifications_filter'] == self::FILTER_BOTH && - ($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id']); - } - - /** - * Return true if the user want to receive notification for the selected project - * - * @access public - * @param array $user - * @param array $event_data - * @return boolean - */ - public function filterProject(array $user, array $event_data) - { - $projects = $this->getUserSelectedProjects($user['id']); - - if (! empty($projects)) { - return in_array($event_data['task']['project_id'], $projects); - } - - return true; - } -} diff --git a/app/Model/NotificationType.php b/app/Model/NotificationType.php index b0514b19..9ec250ab 100644 --- a/app/Model/NotificationType.php +++ b/app/Model/NotificationType.php @@ -2,72 +2,82 @@ namespace Kanboard\Model; +use Pimple\Container; + /** - * Notification Type model + * Notification Type * * @package model * @author Frederic Guillot */ -class NotificationType extends Base +abstract class NotificationType extends Base { /** - * SQL table name + * Mail transport instances * - * @var string + * @access private + * @var \Pimple\Container */ - const TABLE = 'user_has_notification_types'; + private $classes; /** - * Types + * Mail transport instances * - * @var string + * @access private + * @var array */ - const TYPE_WEB = 'web'; - const TYPE_EMAIL = 'email'; + private $labels = array(); /** - * Get all notification types + * Constructor * * @access public - * @return array + * @param \Pimple\Container $container */ - public function getTypes() + public function __construct(Container $container) { - return array( - self::TYPE_EMAIL => t('Email'), - self::TYPE_WEB => t('Web'), - ); + parent::__construct($container); + $this->classes = new Container; } /** - * Get selected notification types for a given user + * Add a new notification type * * @access public - * @param integer $user_id - * @return array + * @param string $type + * @param string $label + * @param string $class */ - public function getUserSelectedTypes($user_id) + public function setType($type, $label, $class) { - return $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('notification_type')->findAllByColumn('notification_type'); + $container = $this->container; + $this->labels[$type] = $label; + + $this->classes[$type] = function() use ($class, $container) { + return new $class($container); + }; } /** - * Save notification types for a given user + * Get mail notification type instance * * @access public - * @param integer $user_id - * @param string[] $types - * @return boolean + * @param string $type + * @return NotificationInterface */ - public function saveUserSelectedTypes($user_id, array $types) + public function getType($type) { - $results = array(); - $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove(); - - foreach ($types as $type) { - $results[] = $this->db->table(self::TABLE)->insert(array('user_id' => $user_id, 'notification_type' => $type)); - } + return $this->classes[$type]; + } - return ! in_array(false, $results); + /** + * Get all notification types with labels + * + * @access public + * @return array + */ + public function getTypes() + { + return $this->labels; } } diff --git a/app/Model/OverdueNotification.php b/app/Model/OverdueNotification.php index f44c8dd7..d8c2fef4 100644 --- a/app/Model/OverdueNotification.php +++ b/app/Model/OverdueNotification.php @@ -22,7 +22,7 @@ class OverdueNotification extends Base foreach ($this->groupByColumn($tasks, 'project_id') as $project_id => $project_tasks) { // Get the list of users that should receive notifications for each projects - $users = $this->notification->getUsersWithNotificationEnabled($project_id); + $users = $this->userNotification->getUsersWithNotificationEnabled($project_id); foreach ($users as $user) { $this->sendUserOverdueTaskNotifications($user, $project_tasks); @@ -44,13 +44,13 @@ class OverdueNotification extends Base $user_tasks = array(); foreach ($tasks as $task) { - if ($this->notificationFilter->shouldReceiveNotification($user, array('task' => $task))) { + if ($this->userNotificationFilter->shouldReceiveNotification($user, array('task' => $task))) { $user_tasks[] = $task; } } if (! empty($user_tasks)) { - $this->notification->sendUserNotification( + $this->userNotification->sendUserNotification( $user, Task::EVENT_OVERDUE, array('tasks' => $user_tasks, 'project_name' => $tasks[0]['project_name']) diff --git a/app/Model/UserNotification.php b/app/Model/UserNotification.php new file mode 100644 index 00000000..b4ee77a9 --- /dev/null +++ b/app/Model/UserNotification.php @@ -0,0 +1,185 @@ +userSession->isLogged() ? $this->userSession->getId() : 0; + $users = $this->getUsersWithNotificationEnabled($event_data['task']['project_id'], $logged_user_id); + + if (! empty($users)) { + foreach ($users as $user) { + if ($this->userNotificationFilter->shouldReceiveNotification($user, $event_data)) { + $this->sendUserNotification($user, $event_name, $event_data); + } + } + + // Restore locales + $this->config->setupTranslations(); + } + } + + /** + * Send notification to someone + * + * @access public + * @param array $user User + * @param string $event_name + * @param array $event_data + */ + public function sendUserNotification(array $user, $event_name, array $event_data) + { + Translator::unload(); + + // Use the user language otherwise use the application language (do not use the session language) + if (! empty($user['language'])) { + Translator::load($user['language']); + } + else { + Translator::load($this->config->get('application_language', 'en_US')); + } + + foreach ($this->userNotificationType->getSelectedTypes($user['id']) as $type) { + $this->userNotificationType->getType($type)->notifyUser($user, $event_name, $event_data); + } + } + + /** + * Get a list of people with notifications enabled + * + * @access public + * @param integer $project_id Project id + * @param integer $exclude_user_id User id to exclude + * @return array + */ + public function getUsersWithNotificationEnabled($project_id, $exclude_user_id = 0) + { + if ($this->projectPermission->isEverybodyAllowed($project_id)) { + return $this->getEverybodyWithNotificationEnabled($exclude_user_id); + } + + return $this->getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id); + } + + /** + * Enable notification for someone + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function enableNotification($user_id) + { + return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 1)); + } + + /** + * Disable notification for someone + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function disableNotification($user_id) + { + return $this->db->table(User::TABLE)->eq('id', $user_id)->update(array('notifications_enabled' => 0)); + } + + /** + * Save settings for the given user + * + * @access public + * @param integer $user_id User id + * @param array $values Form values + */ + public function saveSettings($user_id, array $values) + { + $this->db->startTransaction(); + + if (isset($values['notifications_enabled']) && $values['notifications_enabled'] == 1) { + $this->enableNotification($user_id); + + $filter = empty($values['notifications_filter']) ? UserNotificationFilter::FILTER_BOTH : $values['notifications_filter']; + $projects = empty($values['notification_projects']) ? array() : array_keys($values['notification_projects']); + $types = empty($values['notification_types']) ? array() : array_keys($values['notification_types']); + + $this->userNotificationFilter->saveFilter($user_id, $filter); + $this->userNotificationFilter->saveSelectedProjects($user_id, $projects); + $this->userNotificationType->saveSelectedTypes($user_id, $types); + } + else { + $this->disableNotification($user_id); + } + + $this->db->closeTransaction(); + } + + /** + * Read user settings to display the form + * + * @access public + * @param integer $user_id User id + * @return array + */ + public function readSettings($user_id) + { + $values = $this->db->table(User::TABLE)->eq('id', $user_id)->columns('notifications_enabled', 'notifications_filter')->findOne(); + $values['notification_types'] = $this->userNotificationType->getSelectedTypes($user_id); + $values['notification_projects'] = $this->userNotificationFilter->getSelectedProjects($user_id); + return $values; + } + + /** + * Get a list of project members with notification enabled + * + * @access private + * @param integer $project_id Project id + * @param integer $exclude_user_id User id to exclude + * @return array + */ + private function getProjectMembersWithNotificationEnabled($project_id, $exclude_user_id) + { + return $this->db + ->table(ProjectPermission::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') + ->join(User::TABLE, 'id', 'user_id') + ->eq('project_id', $project_id) + ->eq('notifications_enabled', '1') + ->neq(User::TABLE.'.id', $exclude_user_id) + ->findAll(); + } + + /** + * Get a list of project members with notification enabled + * + * @access private + * @param integer $exclude_user_id User id to exclude + * @return array + */ + private function getEverybodyWithNotificationEnabled($exclude_user_id) + { + return $this->db + ->table(User::TABLE) + ->columns(User::TABLE.'.id', User::TABLE.'.username', User::TABLE.'.name', User::TABLE.'.email', User::TABLE.'.language', User::TABLE.'.notifications_filter') + ->eq('notifications_enabled', '1') + ->neq(User::TABLE.'.id', $exclude_user_id) + ->findAll(); + } +} diff --git a/app/Model/UserNotificationFilter.php b/app/Model/UserNotificationFilter.php new file mode 100644 index 00000000..d4afd278 --- /dev/null +++ b/app/Model/UserNotificationFilter.php @@ -0,0 +1,199 @@ + t('All tasks'), + self::FILTER_ASSIGNEE => t('Only for tasks assigned to me'), + self::FILTER_CREATOR => t('Only for tasks created by me'), + self::FILTER_BOTH => t('Only for tasks created by me and assigned to me'), + ); + } + + /** + * Get user selected filter + * + * @access public + * @param integer $user_id + * @return integer + */ + public function getSelectedFilter($user_id) + { + return $this->db->table(User::TABLE)->eq('id', $user_id)->findOneColumn('notifications_filter'); + } + + /** + * Save selected filter for a user + * + * @access public + * @param integer $user_id + * @param string $filter + */ + public function saveFilter($user_id, $filter) + { + $this->db->table(User::TABLE)->eq('id', $user_id)->update(array( + 'notifications_filter' => $filter, + )); + } + + /** + * Get user selected projects + * + * @access public + * @param integer $user_id + * @return array + */ + public function getSelectedProjects($user_id) + { + return $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->findAllByColumn('project_id'); + } + + /** + * Save selected projects for a user + * + * @access public + * @param integer $user_id + * @param integer[] $project_ids + */ + public function saveSelectedProjects($user_id, array $project_ids) + { + $this->db->table(self::PROJECT_TABLE)->eq('user_id', $user_id)->remove(); + + foreach ($project_ids as $project_id) { + $this->db->table(self::PROJECT_TABLE)->insert(array( + 'user_id' => $user_id, + 'project_id' => $project_id, + )); + } + } + + /** + * Return true if the user should receive notification + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function shouldReceiveNotification(array $user, array $event_data) + { + $filters = array( + 'filterNone', + 'filterAssignee', + 'filterCreator', + 'filterBoth', + ); + + foreach ($filters as $filter) { + if ($this->$filter($user, $event_data)) { + return $this->filterProject($user, $event_data); + } + } + + return false; + } + + /** + * Return true if the user will receive all notifications + * + * @access public + * @param array $user + * @return boolean + */ + public function filterNone(array $user) + { + return $user['notifications_filter'] == self::FILTER_NONE; + } + + /** + * Return true if the user is the assignee and selected the filter "assignee" + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterAssignee(array $user, array $event_data) + { + return $user['notifications_filter'] == self::FILTER_ASSIGNEE && $event_data['task']['owner_id'] == $user['id']; + } + + /** + * Return true if the user is the creator and enabled the filter "creator" + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterCreator(array $user, array $event_data) + { + return $user['notifications_filter'] == self::FILTER_CREATOR && $event_data['task']['creator_id'] == $user['id']; + } + + /** + * Return true if the user is the assignee or the creator and selected the filter "both" + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterBoth(array $user, array $event_data) + { + return $user['notifications_filter'] == self::FILTER_BOTH && + ($event_data['task']['creator_id'] == $user['id'] || $event_data['task']['owner_id'] == $user['id']); + } + + /** + * Return true if the user want to receive notification for the selected project + * + * @access public + * @param array $user + * @param array $event_data + * @return boolean + */ + public function filterProject(array $user, array $event_data) + { + $projects = $this->getSelectedProjects($user['id']); + + if (! empty($projects)) { + return in_array($event_data['task']['project_id'], $projects); + } + + return true; + } +} diff --git a/app/Model/UserNotificationType.php b/app/Model/UserNotificationType.php new file mode 100644 index 00000000..d2613440 --- /dev/null +++ b/app/Model/UserNotificationType.php @@ -0,0 +1,51 @@ +db->table(self::TABLE)->eq('user_id', $user_id)->asc('notification_type')->findAllByColumn('notification_type'); + } + + /** + * Save notification types for a given user + * + * @access public + * @param integer $user_id + * @param string[] $types + * @return boolean + */ + public function saveSelectedTypes($user_id, array $types) + { + $results = array(); + $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove(); + + foreach ($types as $type) { + $results[] = $this->db->table(self::TABLE)->insert(array('user_id' => $user_id, 'notification_type' => $type)); + } + + return ! in_array(false, $results); + } +} diff --git a/app/Model/UserUnreadNotification.php b/app/Model/UserUnreadNotification.php new file mode 100644 index 00000000..98a337a2 --- /dev/null +++ b/app/Model/UserUnreadNotification.php @@ -0,0 +1,154 @@ +db->table(self::TABLE)->insert(array( + 'user_id' => $user_id, + 'date_creation' => time(), + 'event_name' => $event_name, + 'event_data' => json_encode($event_data), + )); + } + + /** + * Get all notifications for a user + * + * @access public + * @param integer $user_id + * @return array + */ + public function getAll($user_id) + { + $events = $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('date_creation')->findAll(); + + foreach ($events as &$event) { + $event['event_data'] = json_decode($event['event_data'], true); + $event['title'] = $this->getTitleFromEvent($event['event_name'], $event['event_data']); + } + + return $events; + } + + /** + * Mark a notification as read + * + * @access public + * @param integer $user_id + * @param integer $notification_id + * @return boolean + */ + public function markAsRead($user_id, $notification_id) + { + return $this->db->table(self::TABLE)->eq('id', $notification_id)->eq('user_id', $user_id)->remove(); + } + + /** + * Mark all notifications as read for a user + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function markAllAsRead($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove(); + } + + /** + * Return true if the user as unread notifications + * + * @access public + * @param integer $user_id + * @return boolean + */ + public function hasNotifications($user_id) + { + return $this->db->table(self::TABLE)->eq('user_id', $user_id)->exists(); + } + + /** + * Get title from event + * + * @access public + * @param string $event_name + * @param array $event_data + * @return string + */ + public function getTitleFromEvent($event_name, array $event_data) + { + switch ($event_name) { + case File::EVENT_CREATE: + $title = t('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); + break; + case Comment::EVENT_CREATE: + $title = t('New comment on task #%d', $event_data['comment']['task_id']); + break; + case Comment::EVENT_UPDATE: + $title = t('Comment updated on task #%d', $event_data['comment']['task_id']); + break; + case Subtask::EVENT_CREATE: + $title = t('New subtask on task #%d', $event_data['subtask']['task_id']); + break; + case Subtask::EVENT_UPDATE: + $title = t('Subtask updated on task #%d', $event_data['subtask']['task_id']); + break; + case Task::EVENT_CREATE: + $title = t('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); + break; + case Task::EVENT_UPDATE: + $title = t('Task updated #%d', $event_data['task']['id']); + break; + case Task::EVENT_CLOSE: + $title = t('Task #%d closed', $event_data['task']['id']); + break; + case Task::EVENT_OPEN: + $title = t('Task #%d opened', $event_data['task']['id']); + break; + case Task::EVENT_MOVE_COLUMN: + $title = t('Column changed for task #%d', $event_data['task']['id']); + break; + case Task::EVENT_MOVE_POSITION: + $title = t('New position for task #%d', $event_data['task']['id']); + break; + case Task::EVENT_MOVE_SWIMLANE: + $title = t('Swimlane changed for task #%d', $event_data['task']['id']); + break; + case Task::EVENT_ASSIGNEE_CHANGE: + $title = t('Assignee changed on task #%d', $event_data['task']['id']); + break; + case Task::EVENT_OVERDUE: + $nb = count($event_data['tasks']); + $title = $nb > 1 ? t('%d overdue tasks', $nb) : t('Task #%d is overdue', $event_data['tasks'][0]['id']); + break; + default: + $title = e('Notification'); + } + + return $title; + } +} diff --git a/app/Model/WebNotification.php b/app/Model/WebNotification.php deleted file mode 100644 index 02bcc956..00000000 --- a/app/Model/WebNotification.php +++ /dev/null @@ -1,156 +0,0 @@ -db->table(self::TABLE)->insert(array( - 'user_id' => $user['id'], - 'date_creation' => time(), - 'event_name' => $event_name, - 'event_data' => json_encode($event_data), - )); - } - - /** - * Get all notifications for a user - * - * @access public - * @param integer $user_id - * @return array - */ - public function getAll($user_id) - { - $events = $this->db->table(self::TABLE)->eq('user_id', $user_id)->asc('date_creation')->findAll(); - - foreach ($events as &$event) { - $event['event_data'] = json_decode($event['event_data'], true); - $event['title'] = $this->getTitleFromEvent($event['event_name'], $event['event_data']); - } - - return $events; - } - - /** - * Mark a notification as read - * - * @access public - * @param integer $user_id - * @param integer $notification_id - * @return boolean - */ - public function markAsRead($user_id, $notification_id) - { - return $this->db->table(self::TABLE)->eq('id', $notification_id)->eq('user_id', $user_id)->remove(); - } - - /** - * Mark all notifications as read for a user - * - * @access public - * @param integer $user_id - * @return boolean - */ - public function markAllAsRead($user_id) - { - return $this->db->table(self::TABLE)->eq('user_id', $user_id)->remove(); - } - - /** - * Return true if the user as unread notifications - * - * @access public - * @param integer $user_id - * @return boolean - */ - public function hasNotifications($user_id) - { - return $this->db->table(self::TABLE)->eq('user_id', $user_id)->exists(); - } - - /** - * Get title from event - * - * @access public - * @param string $event_name - * @param array $event_data - * @return string - */ - public function getTitleFromEvent($event_name, array $event_data) - { - switch ($event_name) { - case File::EVENT_CREATE: - $title = t('New attachment on task #%d: %s', $event_data['file']['task_id'], $event_data['file']['name']); - break; - case Comment::EVENT_CREATE: - $title = t('New comment on task #%d', $event_data['comment']['task_id']); - break; - case Comment::EVENT_UPDATE: - $title = t('Comment updated on task #%d', $event_data['comment']['task_id']); - break; - case Subtask::EVENT_CREATE: - $title = t('New subtask on task #%d', $event_data['subtask']['task_id']); - break; - case Subtask::EVENT_UPDATE: - $title = t('Subtask updated on task #%d', $event_data['subtask']['task_id']); - break; - case Task::EVENT_CREATE: - $title = t('New task #%d: %s', $event_data['task']['id'], $event_data['task']['title']); - break; - case Task::EVENT_UPDATE: - $title = t('Task updated #%d', $event_data['task']['id']); - break; - case Task::EVENT_CLOSE: - $title = t('Task #%d closed', $event_data['task']['id']); - break; - case Task::EVENT_OPEN: - $title = t('Task #%d opened', $event_data['task']['id']); - break; - case Task::EVENT_MOVE_COLUMN: - $title = t('Column changed for task #%d', $event_data['task']['id']); - break; - case Task::EVENT_MOVE_POSITION: - $title = t('New position for task #%d', $event_data['task']['id']); - break; - case Task::EVENT_MOVE_SWIMLANE: - $title = t('Swimlane changed for task #%d', $event_data['task']['id']); - break; - case Task::EVENT_ASSIGNEE_CHANGE: - $title = t('Assignee changed on task #%d', $event_data['task']['id']); - break; - case Task::EVENT_OVERDUE: - $nb = count($event_data['tasks']); - $title = $nb > 1 ? t('%d overdue tasks', $nb) : t('Task #%d is overdue', $event_data['tasks'][0]['id']); - break; - default: - $title = e('Notification'); - } - - return $title; - } -} diff --git a/app/Notification/Mail.php b/app/Notification/Mail.php new file mode 100644 index 00000000..69b8888d --- /dev/null +++ b/app/Notification/Mail.php @@ -0,0 +1,140 @@ +emailClient->send( + $user['email'], + $user['name'] ?: $user['username'], + $this->getMailSubject($event_name, $event_data), + $this->getMailContent($event_name, $event_data) + ); + } + } + + /** + * Send notification to a project + * + * @access public + * @param array $project + * @param string $event_name + * @param array $event_data + */ + public function notifyProject(array $project, $event_name, array $event_data) + { + + } + + /** + * Get the mail content for a given template name + * + * @access public + * @param string $event_name Event name + * @param array $event_data Event data + * @return string + */ + public function getMailContent($event_name, array $event_data) + { + return $this->template->render( + 'notification/'.str_replace('.', '_', $event_name), + $event_data + array('application_url' => $this->config->get('application_url')) + ); + } + + /** + * Get the mail subject for a given template name + * + * @access public + * @param string $event_name Event name + * @param array $event_data Event data + * @return string + */ + public function getMailSubject($event_name, array $event_data) + { + switch ($event_name) { + case File::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New attachment'), $event_data); + break; + case Comment::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New comment'), $event_data); + break; + case Comment::EVENT_UPDATE: + $subject = $this->getStandardMailSubject(e('Comment updated'), $event_data); + break; + case Subtask::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New subtask'), $event_data); + break; + case Subtask::EVENT_UPDATE: + $subject = $this->getStandardMailSubject(e('Subtask updated'), $event_data); + break; + case Task::EVENT_CREATE: + $subject = $this->getStandardMailSubject(e('New task'), $event_data); + break; + case Task::EVENT_UPDATE: + $subject = $this->getStandardMailSubject(e('Task updated'), $event_data); + break; + case Task::EVENT_CLOSE: + $subject = $this->getStandardMailSubject(e('Task closed'), $event_data); + break; + case Task::EVENT_OPEN: + $subject = $this->getStandardMailSubject(e('Task opened'), $event_data); + break; + case Task::EVENT_MOVE_COLUMN: + $subject = $this->getStandardMailSubject(e('Column change'), $event_data); + break; + case Task::EVENT_MOVE_POSITION: + $subject = $this->getStandardMailSubject(e('Position change'), $event_data); + break; + case Task::EVENT_MOVE_SWIMLANE: + $subject = $this->getStandardMailSubject(e('Swimlane change'), $event_data); + break; + case Task::EVENT_ASSIGNEE_CHANGE: + $subject = $this->getStandardMailSubject(e('Assignee change'), $event_data); + break; + case Task::EVENT_OVERDUE: + $subject = e('[%s] Overdue tasks', $event_data['project_name']); + break; + default: + $subject = e('Notification'); + } + + return $subject; + } + + /** + * Get the mail subject for a given label + * + * @access private + * @param string $label Label + * @param array $data Template data + * @return string + */ + private function getStandardMailSubject($label, array $data) + { + return sprintf('[%s][%s] %s (#%d)', $data['task']['project_name'], $label, $data['task']['title'], $data['task']['id']); + } +} diff --git a/app/Notification/NotificationInterface.php b/app/Notification/NotificationInterface.php new file mode 100644 index 00000000..8431a77c --- /dev/null +++ b/app/Notification/NotificationInterface.php @@ -0,0 +1,32 @@ +userUnreadNotification->create($user['id'], $event_name, $event_data); + } + + /** + * Send notification to a project + * + * @access public + * @param array $project + * @param string $event_name + * @param array $event_data + */ + public function notifyProject(array $project, $event_name, array $event_data) + { + } +} diff --git a/app/ServiceProvider/ClassProvider.php b/app/ServiceProvider/ClassProvider.php index 9df2dede..322c1a9b 100644 --- a/app/ServiceProvider/ClassProvider.php +++ b/app/ServiceProvider/ClassProvider.php @@ -8,9 +8,7 @@ use Kanboard\Core\ObjectStorage\FileStorage; use Kanboard\Core\Paginator; use Kanboard\Core\OAuth2; use Kanboard\Core\Tool; -use Kanboard\Model\Config; -use Kanboard\Model\Project; -use Kanboard\Model\Webhook; +use Kanboard\Model\UserNotificationType; use Pimple\Container; use Pimple\ServiceProviderInterface; use League\HTMLToMarkdown\HtmlConverter; @@ -32,12 +30,7 @@ class ClassProvider implements ServiceProviderInterface 'File', 'LastLogin', 'Link', - 'Notification', - 'NotificationType', - 'NotificationFilter', 'OverdueNotification', - 'WebNotification', - 'EmailNotification', 'Project', 'ProjectActivity', 'ProjectAnalytic', @@ -68,6 +61,10 @@ class ClassProvider implements ServiceProviderInterface 'User', 'UserImport', 'UserSession', + 'UserNotification', + 'UserNotificationType', + 'UserNotificationFilter', + 'UserUnreadNotification', 'Webhook', ), 'Formatter' => array( @@ -131,6 +128,13 @@ class ClassProvider implements ServiceProviderInterface return $mailer; }; + $container['userNotificationType'] = function($container) { + $type = new UserNotificationType($container); + $type->setType('email', t('Email'), '\Kanboard\Notification\Mail'); + $type->setType('web', t('Web'), '\Kanboard\Notification\Web'); + return $type; + }; + $container['pluginLoader'] = new Loader($container); $container['cspRules'] = array('style-src' => "'self' 'unsafe-inline'", 'img-src' => '* data:'); diff --git a/app/Subscriber/NotificationSubscriber.php b/app/Subscriber/NotificationSubscriber.php index 610fbadb..07ce00a1 100644 --- a/app/Subscriber/NotificationSubscriber.php +++ b/app/Subscriber/NotificationSubscriber.php @@ -32,7 +32,7 @@ class NotificationSubscriber extends \Kanboard\Core\Base implements EventSubscri public function execute(GenericEvent $event, $event_name) { - $this->notification->sendNotifications($event_name, $this->getEventData($event)); + $this->userNotification->sendNotifications($event_name, $this->getEventData($event)); } public function getEventData(GenericEvent $event) diff --git a/app/Template/app/notifications.php b/app/Template/app/notifications.php index 4f7dd353..511f377b 100644 --- a/app/Template/app/notifications.php +++ b/app/Template/app/notifications.php @@ -8,7 +8,7 @@ @@ -53,7 +53,7 @@ - url->link(t('Mark as read'), 'webnotification', 'remove', array('user_id' => $user['id'], 'notification_id' => $notification['id'])) ?> + url->link(t('Mark as read'), 'webNotification', 'remove', array('user_id' => $user['id'], 'notification_id' => $notification['id'])) ?> -- cgit v1.2.3