├── .gitignore ├── .upgrade.yml ├── LICENSE ├── README.md ├── _config.php ├── composer.json ├── images ├── notifications-icon.png └── notifications-icon.svg ├── src ├── Controller │ └── NotificationAdmin.php ├── Extension │ ├── MemberExtension.php │ └── ReadNotificationExtension.php ├── Helper │ └── NotificationHelper.php ├── Job │ └── SendNotificationJob.php ├── Model │ ├── BroadcastNotification.php │ ├── InternalNotification.php │ ├── NotificationSender.php │ ├── NotifiedOn.php │ └── SystemNotification.php ├── Report │ └── NotificationReport.php └── Service │ ├── EmailNotificationSender.php │ ├── InternalNotificationSender.php │ ├── NotificationService.php │ └── NotifyService.php └── tests ├── DummyNotificationSender.php ├── NotificationsTest.php ├── NotificationsTest.yml └── NotifyOnThis.php /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.upgrade.yml: -------------------------------------------------------------------------------- 1 | mappings: 2 | NotificationAdmin: Symbiote\Notifications\Controller\NotificationAdmin 3 | NotificationHelper: Symbiote\Notifications\Helper\NotificationHelper 4 | SendNotificationJob: Symbiote\Notifications\Job\SendNotificationJob 5 | NotificationSender: Symbiote\Notifications\Model\NotificationSender 6 | NotifiedOn: Symbiote\Notifications\Model\NotifiedOn 7 | SystemNotification: Symbiote\Notifications\Model\SystemNotification 8 | EmailNotificationSender: Symbiote\Notifications\Service\EmailNotificationSender 9 | NotificationService: Symbiote\Notifications\Service\NotificationService 10 | CheckEmailNotifications: Symbiote\Notifications\Task\CheckEmailNotifications -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Symbiote. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | Neither the name of the organisation nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SilverStripe Notifications Module 2 | 3 | Send CMS managed system email notifications from code. 4 | 5 | ## Maintainer Contacts 6 | * Marcus Nyeholt () 7 | * Shea Dawson () 8 | 9 | ## Requirements 10 | * SilverStripe 4.0 + 11 | 12 | ## Installation Instructions 13 | 14 | ``` 15 | composer require symbiote/silverstripe-notifications 16 | ``` 17 | 18 | ## Sending a notification 19 | 20 | The module comes with a default BroadcastNotification object that can be used to send a notification to multiple 21 | people at once. First, create the SystemNotification (which defines how the notification will be sent) 22 | 23 | > Notifications => Add System Notification 24 | 25 | * Identifier: BROADCAST 26 | * Title: (your own) 27 | * Relevant For: BroadcastNotification 28 | * Send via channels: Internal 29 | * Text: (Your own; use $Context.Content to output the broadcast content) 30 | 31 | > Notifications => Add Broadcast Notification 32 | 33 | * Title: (your own) 34 | * Content: (your own) 35 | * Click Create 36 | * Groups: choose which groups to receive the notification 37 | * Send Now: Click when ready for the notification to send. 38 | 39 | ## Creating System Notifications 40 | 41 | Creating custom notifications requires a few pieces of code to put things together. Use the 42 | BroadcastNotification as an example, with the key points identified below 43 | 44 | ### 1) 45 | In your _config yml file, add an identifier for each notification you require. This allows you to lookup Notification objects in the database from your code. 46 | 47 | ``` 48 | Symbiote\Notifications\Model\SystemNotification: 49 | identifiers: 50 | - 'NAME_OF_NOTIFICATION1' 51 | - 'NAME_OF_NOTIFICATION2' 52 | ``` 53 | 54 | ### 2) 55 | Add the NotifiedOn interface to any dataobjects that are relevant to the notifications you will be sending. This is required so the Notifications module can look up the below methods on your object to send the notification. 56 | 57 | ```php 58 | use Symbiote\Notifications\Model\NotifiedOn; 59 | 60 | class MyDataObject extends DataObject implements NotifiedOn { 61 | ... 62 | ``` 63 | 64 | Define the following interface methods on the Object being notified on. 65 | 66 | ```php 67 | /** 68 | * Return a list of available keywords in the format 69 | * array('keyword' => 'A description') to help users format notification fields 70 | * @return array 71 | */ 72 | public function getAvailableKeywords(); 73 | ``` 74 | ```php 75 | /** 76 | * Gets an associative array of data that can be accessed in 77 | * notification fields and templates 78 | * @return array 79 | */ 80 | public function getNotificationTemplateData(); 81 | ``` 82 | 83 | Note: the follow template data is automatically included: 84 | 85 | * $ThemeDirs (an ArrayList object of themes, if you only have one theme using `$ThemeDirs.First` should be the same as the old `$ThemeDir` ) 86 | * $SiteConfig 87 | * $MyDataObject (whatever the ClassName of your NotifiedOn DataObject is) 88 | * $Member (The Member object this message is being sent to) 89 | 90 | ```php 91 | /** 92 | * Gets the list of recipients for a given notification event, based on this object's 93 | * state. 94 | * $event The identifier of the event that triggered this notification 95 | * @return array An array of Member objects 96 | */ 97 | public function getRecipients($event); 98 | ``` 99 | 100 | Note: getRecipients() can return an array of any objects, as long as they have an Email property or method 101 | 102 | ### 3) 103 | 104 | Create a notification in the Notifications model admin, in the CMS. 105 | 106 | ### 4) 107 | Send the notification from your code, where $contextObject is an instance of the DataObject being NotifiedOn 108 | ```php 109 | use Symbiote\Notifications\Service\NotificationService; 110 | 111 | singleton(NotificationService::class)->notify('NOTIFICATION_IDENTIFIER', $contextObject); 112 | 113 | ``` 114 | 115 | 116 | 117 | ## Templates 118 | 119 | Notifications can be rendered with .ss templates. This is useful if you want to have a header/footer in your email notifications. You can either specify a template on a per/notification basis in the CMS, and/or set a default template for all notifications to be rendered with: 120 | 121 | ``` 122 | Symbiote\Notifications\Model\SystemNotification: 123 | default_template: EmailNotification 124 | ``` 125 | 126 | In your templates, you render the notification text with the $Body variable. 127 | 128 | ## Configuration 129 | 130 | You will probably want to configure a send_from email address - 131 | ``` 132 | Symbiote\Notifications\Service\EmailNotificationSender: 133 | send_notifications_from: 'notifications@example.com' 134 | ``` 135 | 136 | ## TODO 137 | 138 | * Test with QueuedJobs module for handling large amounts of notifications in configurable batches/queues 139 | -------------------------------------------------------------------------------- /_config.php: -------------------------------------------------------------------------------- 1 | =5.6.0", 26 | "silverstripe/framework": "^4.0", 27 | "symbiote/silverstripe-multivaluefield": "^5.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Symbiote\\Notifications\\": "src/", 32 | "Symbiote\\Notifications\\Tests\\": "tests/" 33 | } 34 | }, 35 | "prefer-stable": true, 36 | "minimum-stability": "dev", 37 | "extra": { 38 | "installer-name": "notifications", 39 | "expose": [ 40 | "images" 41 | ], 42 | "branch-alias": { 43 | "dev-master": "4.4.x-dev" 44 | } 45 | }, 46 | "suggest": { 47 | "symbiote/silverstripe-queuedjobs": "Use the SendNotificationJob to queue and send notifications." 48 | }, 49 | "replace": { 50 | "silverstripe-australia/notifications": "self.version" 51 | }, 52 | "require-dev": { 53 | "phpunit/phpunit": "^5" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /images/notifications-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/symbiote/silverstripe-notifications/4cdffac7e95b3baf6e49274c6d899941237a1474/images/notifications-icon.png -------------------------------------------------------------------------------- /images/notifications-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 17 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Controller/NotificationAdmin.php: -------------------------------------------------------------------------------- 1 | $this->owner->ID] 20 | ); 21 | 22 | $notifications = ArrayList::create(); 23 | 24 | foreach (InternalNotification::get()->filter($filter)->limit($limit, $offset) as $intNote) { 25 | $notification = ArrayData::create($intNote->toMap()); 26 | $notification->setField('FromUsername', $intNote->From()->getNotificationUsername()); 27 | $notifications->push($notification); 28 | } 29 | 30 | return $notifications; 31 | } 32 | 33 | public function getNotificationUsername() 34 | { 35 | if ($this->owner->Username) { 36 | return $this->owner->Username; 37 | } 38 | 39 | return $this->owner->getTitle(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Extension/ReadNotificationExtension.php: -------------------------------------------------------------------------------- 1 | owner->getRequest()->getVar('notification')) { 15 | $id = $this->owner->getRequest()->getVar('notification'); 16 | $note = InternalNotification::get()->byID($id); 17 | if ($note && $note->ToID == $member->ID) { 18 | $note->IsRead = 1; 19 | $note->write(); 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Helper/NotificationHelper.php: -------------------------------------------------------------------------------- 1 | owner = $owner; 31 | } 32 | 33 | /** 34 | * Return a list of all available keywords in the format 35 | * eg. array( 36 | * 'keyword' => 'A description' 37 | * ) 38 | * 39 | * @return array 40 | */ 41 | public function getAvailableKeywords() 42 | { 43 | if (!$this->availableKeywords) { 44 | $objectFields = DataObject::getSchema()->databaseFields(get_class($this->owner)); 45 | 46 | $objectFields['Created'] = 'Created'; 47 | $objectFields['LastEdited'] = 'LastEdited'; 48 | $objectFields['Link'] = 'Link'; 49 | 50 | $this->availableKeywords = []; 51 | 52 | foreach ($objectFields as $key => $value) { 53 | $this->availableKeywords[$key] = $key; 54 | } 55 | } 56 | 57 | return $this->availableKeywords; 58 | } 59 | 60 | /** 61 | * Gets a replacement for a keyword 62 | * 63 | * @param $keyword 64 | * @return string 65 | */ 66 | public function getKeyword($keyword) 67 | { 68 | $k = $this->getAvailableKeywords(); 69 | 70 | if ($keyword == 'Link') { 71 | $link = Director::makeRelative($this->owner->Link()); 72 | 73 | return Controller::join_links(Director::absoluteBaseURL(), $link); 74 | } 75 | 76 | if (isset($k[$keyword])) { 77 | return $this->owner->$keyword; 78 | } 79 | 80 | return; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Job/SendNotificationJob.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class SendNotificationJob extends AbstractQueuedJob implements QueuedJob 24 | { 25 | /** 26 | * SendNotificationJob constructor. 27 | * @param \Symbiote\Notifications\Model\SystemNotification|null $notification 28 | * @param \SilverStripe\ORM\DataObject|null $context 29 | * @param array $data 30 | */ 31 | public function __construct( 32 | SystemNotification $notification = null, 33 | DataObject $context = null, 34 | $data = [] 35 | ) { 36 | if ($notification) { 37 | $this->notificationID = $notification->ID; 38 | $this->contextID = $context->ID; 39 | $this->contextClass = get_class($context); 40 | $this->extraData = $data; 41 | } 42 | } 43 | 44 | /** 45 | * @return \SilverStripe\ORM\DataObject 46 | */ 47 | public function getNotification() 48 | { 49 | return SystemNotification::get()->byID($this->notificationID); 50 | } 51 | 52 | /** 53 | * @return \SilverStripe\ORM\DataObject|null 54 | */ 55 | public function getContext() 56 | { 57 | if ($this->contextID) { 58 | return DataObject::get_by_id($this->contextClass, $this->contextID); 59 | } 60 | 61 | return; 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getTitle() 68 | { 69 | $context = $this->getContext(); 70 | $notification = $this->getNotification(); 71 | 72 | if ($context) { 73 | $title = ''; 74 | if ($context->hasField('Title')) { 75 | $title = $context->Title; 76 | } else { 77 | if ($context->hasField('Name')) { 78 | $title = $context->Name; 79 | } else { 80 | if ($context->hasField('Description')) { 81 | $title = $context->Description; 82 | } else { 83 | $title = '#'.$context->ID; 84 | } 85 | } 86 | } 87 | } else { 88 | $title = $notification->Title; 89 | } 90 | 91 | return 'Sending notification "'.$notification->Description.'" for '.$title; 92 | } 93 | 94 | /** 95 | * @return string 96 | */ 97 | public function getJobType() 98 | { 99 | $notification = $this->getNotification(); 100 | $recipients = $notification->getRecipients($this->getContext()); 101 | $sendTo = []; 102 | if ($recipients) { 103 | if (is_array($recipients) || $recipients instanceof DataList || $recipients instanceof ArrayList) { 104 | foreach ($recipients as $r) { 105 | $sendTo[$r->ID] = $r->ClassName; 106 | } 107 | } else { 108 | if ($recipients instanceof MultiValueField) { 109 | $recipients = $recipients->getValues(); 110 | foreach ($recipients as $id) { 111 | $sendTo[$id] = Member::class; 112 | } 113 | } 114 | } 115 | 116 | $this->totalSteps = count($recipients); 117 | $this->sendTo = $sendTo; 118 | } 119 | 120 | $this->totalSteps = count($this->sendTo); 121 | 122 | return $this->totalSteps > 5 ? QueuedJob::QUEUED : QueuedJob::IMMEDIATE; 123 | } 124 | 125 | public function process() 126 | { 127 | $remaining = $this->sendTo; 128 | 129 | // if there's no more, we're done! 130 | if (!count($remaining)) { 131 | $this->isComplete = true; 132 | 133 | return; 134 | } 135 | 136 | $this->currentStep++; 137 | 138 | $keys = array_keys($remaining); 139 | $toID = array_shift($keys); 140 | $toClass = $remaining[$toID]; 141 | unset($remaining[$toID]); 142 | 143 | $notification = $this->getNotification(); 144 | $context = $this->getContext(); 145 | 146 | $service = singleton(NotificationService::class); 147 | 148 | $user = DataObject::get_by_id($toClass, (int)$toID); 149 | 150 | $data = []; 151 | 152 | // extra data is an array - need to deserialise it!! 153 | foreach ($this->extraData as $k => $v) { 154 | $data[$k] = $v; 155 | } 156 | 157 | // now send to the single user 158 | $service->sendToUser($notification, $context, $user, $data); 159 | 160 | // save new data 161 | $this->sendTo = $remaining; 162 | 163 | if (count($remaining) <= 0) { 164 | $this->isComplete = true; 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/Model/BroadcastNotification.php: -------------------------------------------------------------------------------- 1 | 'Varchar(255)', 21 | 'Content' => 'Text', 22 | 'SendNow' => 'Boolean', 23 | 'IsPublic' => 'Boolean', 24 | 'Context' => 'MultiValueField' 25 | ]; 26 | 27 | private static $many_many = [ 28 | 'Groups' => Group::class 29 | ]; 30 | 31 | public function onBeforeWrite() 32 | { 33 | if ($this->SendNow) { 34 | $this->SendNow = false; 35 | Injector::inst()->get(NotificationService::class)->notify( 36 | 'BROADCAST', 37 | $this 38 | ); 39 | } 40 | parent::onBeforeWrite(); 41 | } 42 | 43 | public function getCMSFields() 44 | { 45 | $fields = parent::getCMSFields(); 46 | 47 | $fields->dataFieldByName('IsPublic')->setRightTitle('Indicate whether this can be displayed to public users'); 48 | 49 | if ($this->ID) { 50 | $fields->dataFieldByName('SendNow')->setRightTitle('If selected, this notification will be broadcast to all users in groups selected below'); 51 | 52 | $fields->removeByName('Groups'); 53 | 54 | $fields->addFieldToTab('Root.Main', ListboxField::create('Groups', 'Groups', Group::get())); 55 | } else { 56 | $fields->removeByName('SendNow'); 57 | } 58 | 59 | $context = KeyValueField::create('Context')->setRightTitle('Add a Link and Title field here to provide context for this message'); 60 | 61 | $fields->replaceField('Context', $context); 62 | 63 | return $fields; 64 | } 65 | 66 | public function getAvailableKeywords() 67 | { 68 | $fields = $this->getNotificationTemplateData(); 69 | $names = array_keys($fields); 70 | return array_combine($names, $names); 71 | } 72 | 73 | /** 74 | * Gets an associative array of data that can be accessed in 75 | * notification fields and templates 76 | * @return array 77 | */ 78 | public function getNotificationTemplateData() 79 | { 80 | $fields = $this->Context->getValues(); 81 | if (!is_array($fields)) { 82 | $fields = []; 83 | } 84 | $fields['Content'] = $this->Content; 85 | return $fields; 86 | } 87 | 88 | /** 89 | * Gets the list of recipients for a given notification event, based on this object's 90 | * state. 91 | * @param string $event The Identifier of the notification being sent 92 | * @return array An array of Member objects 93 | */ 94 | public function getRecipients($event) 95 | { 96 | $groupIds = $this->Groups()->column('ID'); 97 | if (count($groupIds)) { 98 | $members = Member::get()->filter('Groups.ID', $groupIds); 99 | return $members; 100 | } 101 | return []; 102 | } 103 | 104 | public function Link() 105 | { 106 | $context = $this->Context->getValues(); 107 | return isset($context['Link']) ? $context['Link'] : null; 108 | } 109 | 110 | public function canCreate($member = null, $context = array()) 111 | { 112 | return Permission::check('CMS_ACCESS_' . NotificationAdmin::class) || parent::canCreate($member, $context); 113 | } 114 | 115 | public function canDelete($member = null) 116 | { 117 | return Permission::check('CMS_ACCESS_' . NotificationAdmin::class) || parent::canDelete($member); 118 | } 119 | 120 | public function canView($member = null) 121 | { 122 | return $this->IsPublic || (Permission::check('CMS_ACCESS_' . NotificationAdmin::class) || parent::canView($member)); 123 | } 124 | 125 | public function canEdit($member = null) 126 | { 127 | return Permission::check('CMS_ACCESS_' . NotificationAdmin::class) || parent::canEdit($member); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Model/InternalNotification.php: -------------------------------------------------------------------------------- 1 | 'Varchar(255)', 18 | 'Message' => 'Text', 19 | 'SentOn' => 'Datetime', 20 | 'IsRead' => 'Boolean', 21 | 'IsSeen' => 'Boolean', 22 | 'Context' => MultiValueField::class, 23 | ]; 24 | 25 | private static $has_one = [ 26 | 'To' => Member::class, 27 | 'From' => Member::class, 28 | 'SourceObject' => DataObject::class, 29 | 'SourceNotification' => SystemNotification::class, 30 | ]; 31 | 32 | private static $summary_fields = [ 33 | 'Title' => 'Title', 34 | 'To.Name' => 'To', 35 | 'SentOn' => 'Sent on', 36 | 'IsSeen.Nice' => 'Seen?', 37 | 'IsRead.Nice' => 'Read?' 38 | ]; 39 | 40 | private static $default_sort = 'ID DESC'; 41 | 42 | public function onBeforeWrite() 43 | { 44 | parent::onBeforeWrite(); 45 | 46 | if ($this->IsRead) { 47 | $this->IsSeen = true; 48 | } 49 | } 50 | 51 | public function getCMSFields() 52 | { 53 | $fields = parent::getCMSFields(); 54 | 55 | $fields->replaceField('Context', KeyValueField::create('Context')); 56 | return $fields; 57 | } 58 | 59 | public function canView($member = null) 60 | { 61 | $member = $member ?: Security::getCurrentUser(); 62 | if (!$member) { 63 | return false; 64 | } 65 | if (Permission::check('ADMIN')) { 66 | return true; 67 | } 68 | return $member && (!$this->ID || $this->ToID == $member->ID || $this->FromID == $member->ID); 69 | } 70 | 71 | public function canEdit($member = null) 72 | { 73 | $member = $member ?: Security::getCurrentUser(); 74 | if (!$member) { 75 | return false; 76 | } 77 | if (Permission::check('ADMIN')) { 78 | return true; 79 | } 80 | return $member && (!$this->ID || $this->ToID == $member->ID); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Model/NotificationSender.php: -------------------------------------------------------------------------------- 1 | 'A description') to help users format notification fields 15 | * @return array 16 | */ 17 | public function getAvailableKeywords(); 18 | 19 | /** 20 | * Gets an associative array of data that can be accessed in 21 | * notification fields and templates 22 | * @return array 23 | */ 24 | public function getNotificationTemplateData(); 25 | 26 | /** 27 | * Gets the list of recipients for a given notification event, based on this object's 28 | * state. 29 | * @param string $event The Identifier of the notification being sent 30 | * @return array An array of Member objects 31 | */ 32 | public function getRecipients($event); 33 | } 34 | -------------------------------------------------------------------------------- /src/Model/SystemNotification.php: -------------------------------------------------------------------------------- 1 | 'The item associated with the notification', 66 | 'Member' => 'The user who triggered the notification', 67 | ]; 68 | 69 | /** 70 | * If true, notification text can contain html and a wysiwyg editor will be 71 | * used to create the notification text rather than textarea 72 | * @var boolean 73 | */ 74 | private static $html_notifications = false; 75 | 76 | /** 77 | * Name of a template file to render all notifications with 78 | * Note: it's up to the NotificationSender to decide whether or not to use it 79 | * @var string 80 | */ 81 | private static $default_template; 82 | 83 | private static $db = [ 84 | 'Identifier' => 'Varchar', // used to reference this notification from code 85 | 'Title' => 'Varchar(255)', 86 | 'Description' => 'Text', 87 | 'NotificationText' => 'Text', 88 | 'NotificationHTML' => 'HTMLText', 89 | 'NotifyOnClass' => 'Varchar(128)', 90 | 'Channels' => 'Varchar(64)', 91 | 'CustomTemplate' => 'Varchar', 92 | ]; 93 | 94 | /** 95 | * @return FieldList 96 | */ 97 | public function getCMSFields() 98 | { 99 | // Get NotifiedOn implementors 100 | $types = ClassInfo::implementorsOf(NotifiedOn::class); 101 | $configTypes = self::config()->notify_on; 102 | 103 | $types = array_merge($types, $configTypes); 104 | 105 | $types = array_combine($types, $types); 106 | unset($types['NotifyOnThis']); 107 | if (!$types) { 108 | $types = []; 109 | } 110 | 111 | // Available keywords 112 | $keywords = $this->getKeywords(); 113 | if (count($keywords)) { 114 | $availableKeywords = '
'. 115 | '
'. 116 | '

Available Keywords:

'. 117 | '
    '. 118 | '
  • $'.implode('
  • $', $keywords).'
  • '. 119 | '
'. 120 | '
'; 121 | } else { 122 | $availableKeywords = "Available keywords will be shown if you select a NotifyOnClass"; 123 | } 124 | 125 | // Identifiers 126 | $identifiers = $this->config()->get('identifiers'); 127 | if (count($identifiers)) { 128 | $identifiers = array_combine($identifiers, $identifiers); 129 | } 130 | 131 | $fields = FieldList::create(); 132 | 133 | $relevantMsg = 'Relevant for (note: this notification will only be sent if the '. 134 | 'context of raising the notification is of this type)'; 135 | $fields->push( 136 | TabSet::create( 137 | 'Root', 138 | Tab::create( 139 | 'Main', 140 | DropdownField::create( 141 | 'Identifier', 142 | _t('SystemNotification.IDENTIFIER', 'Identifier'), 143 | $identifiers 144 | ), 145 | TextField::create('Title', _t('SystemNotification.TITLE', 'Title')), 146 | TextField::create( 147 | 'Description', 148 | _t('SystemNotification.DESCRIPTION', 'Description') 149 | ), 150 | DropdownField::create( 151 | 'NotifyOnClass', 152 | _t('SystemNotification.NOTIFY_ON_CLASS', $relevantMsg), 153 | $types 154 | )->setEmptyString(''), 155 | TextField::create( 156 | 'CustomTemplate', 157 | _t( 158 | 'SystemNotification.TEMPLATE', 159 | 'Template (Optional)' 160 | ) 161 | )->setAttribute( 162 | 'placeholder', 163 | $this->config()->get('default_template') 164 | ), 165 | LiteralField::create('AvailableKeywords', $availableKeywords) 166 | ) 167 | ) 168 | ); 169 | 170 | $channels = Injector::inst()->get(NotificationService::class)->getChannels(); 171 | if ($channels && count($channels)) { 172 | $sendChannels = array_combine($channels, array_map('ucfirst', $channels)); 173 | $list = ListboxField::create('Channels', 'Send via channels', $sendChannels); 174 | $fields->insertBefore('AvailableKeywords', $list); 175 | $list->setRightTitle('Leave empty to send to all channels'); 176 | } 177 | 178 | if ($this->config()->html_notifications) { 179 | $fields->insertBefore( 180 | 'AvailableKeywords', 181 | HTMLEditorField::create( 182 | 'NotificationHTML', 183 | _t('SystemNotification.TEXT', 'Text') 184 | ) 185 | ); 186 | } else { 187 | $fields->insertBefore( 188 | 'AvailableKeywords', 189 | TextareaField::create( 190 | 'NotificationText', 191 | _t('SystemNotification.TEXT', 'Text') 192 | ) 193 | ); 194 | } 195 | 196 | $this->extend('updateCMSFields', $fields); 197 | 198 | return $fields; 199 | } 200 | 201 | /** 202 | * Get a list of available keywords to help the cms user know what's available 203 | * @return array 204 | **/ 205 | public function getKeywords() 206 | { 207 | $keywords = []; 208 | 209 | foreach ($this->config()->get('global_keywords') as $k => $v) { 210 | $keywords[] = ''.$k.' ' . $v; 211 | } 212 | 213 | if ($this->NotifyOnClass && class_exists($this->NotifyOnClass)) { 214 | $dummy = singleton($this->NotifyOnClass); 215 | if ($dummy instanceof NotifiedOn || $dummy->hasMethod('getAvailableKeywords')) { 216 | $myKeywords = $dummy->getAvailableKeywords(); 217 | 218 | if (is_array($myKeywords)) { 219 | foreach ($myKeywords as $keyword => $desc) { 220 | $keywords[] = ''.$keyword.' - '.$desc; 221 | } 222 | } 223 | } 224 | } 225 | 226 | return $keywords; 227 | } 228 | 229 | /** 230 | * Get a list of recipients from the notification with the given context 231 | * @param DataObject $context 232 | * The context object this notification is attached to. 233 | * @return ArrayList 234 | */ 235 | public function getRecipients($context = null) 236 | { 237 | $recipients = ArrayList::create(); 238 | 239 | // if we have a context, use that for returning the recipients 240 | if ($context && ($context instanceof NotifiedOn || $context->hasMethod('getRecipients'))) 241 | { 242 | $contextRecipients = $context->getRecipients($this->Identifier); 243 | if ($contextRecipients) { 244 | $recipients->merge($contextRecipients); 245 | } 246 | } 247 | 248 | if ($context instanceof Member) { 249 | $recipients->push($context); 250 | } else { 251 | if ($context instanceof Group) { 252 | $recipients = $context->Members(); 253 | } 254 | } 255 | 256 | // otherwise load with a preconfigured list of recipients 257 | return $recipients; 258 | } 259 | 260 | /** 261 | * Format text with given keywords etc 262 | * @param string $text 263 | * @param DataObject $context 264 | * @param Member $user 265 | * @param array $extraData 266 | * @return string 267 | */ 268 | public function format($text, $context, $user = null, $extraData = []) 269 | { 270 | $data = $this->getTemplateData($context, $user, $extraData); 271 | 272 | // render 273 | $viewer = new SSViewer_FromString($text); 274 | try { 275 | $string = $viewer->process($data); 276 | } catch (Exception $e) { 277 | $string = $text; 278 | } 279 | 280 | return $string; 281 | } 282 | 283 | /** 284 | * Get compiled template data to render a string with 285 | * @param NotifiedOn $context 286 | * @param Member $user 287 | * @param array $extraData 288 | * @return ArrayData 289 | */ 290 | public function getTemplateData($context, $user = null, $extraData = []) 291 | { 292 | // useful global data 293 | $data = [ 294 | 'ThemeDirs' => new ArrayList(SSViewer::get_themes()), 295 | 'SiteConfig' => SiteConfig::current_site_config(), 296 | ]; 297 | 298 | // the context object, keyed by it's class name 299 | $clsPath = explode('\\', get_class($context)); 300 | $data[end($clsPath)] = $context; 301 | $data['Context'] = $context; 302 | 303 | // data as defined by the context object 304 | $contextData = method_exists($context, 'getNotificationTemplateData') ? $context->getNotificationTemplateData() : null; 305 | if (is_array($contextData)) { 306 | $data = array_merge($data, $contextData); 307 | } 308 | 309 | // the member the notification is being sent to 310 | $data['Member'] = $user; 311 | 312 | // extra data 313 | $data = array_merge($data, $extraData); 314 | 315 | return ArrayData::create($data); 316 | } 317 | 318 | /** 319 | * Get the custom or default template to render this notification with 320 | * @return string 321 | */ 322 | public function getTemplate() 323 | { 324 | return $this->CustomTemplate ? $this->CustomTemplate : $this->config()->get('default_template'); 325 | } 326 | 327 | /** 328 | * Get the notification content, whether that's html or plain text 329 | * @return string 330 | */ 331 | public function NotificationContent() 332 | { 333 | return $this->config()->html_notifications ? $this->NotificationHTML : $this->NotificationText; 334 | } 335 | 336 | public function canView($member = null) 337 | { 338 | return Permission::check('ADMIN') || Permission::check('SYSTEMNOTIFICATION_VIEW'); 339 | } 340 | 341 | public function canEdit($member = null) 342 | { 343 | return Permission::check('ADMIN') || Permission::check('SYSTEMNOTIFICATION_EDIT'); 344 | } 345 | 346 | public function canDelete($member = null) 347 | { 348 | return Permission::check('ADMIN') || Permission::check('SYSTEMNOTIFICATION_DELETE'); 349 | } 350 | 351 | public function canCreate($member = null, $context = array()) 352 | { 353 | return Permission::check('ADMIN') || Permission::check('SYSTEMNOTIFICATION_CREATE'); 354 | } 355 | 356 | public function providePermissions() 357 | { 358 | return [ 359 | 'SYSTEMNOTIFICATION_VIEW' => [ 360 | 'name' => 'View System Notifications', 361 | 'category' => 'Notifications', 362 | ], 363 | 'SYSTEMNOTIFICATION_EDIT' => [ 364 | 'name' => 'Edit a System Notification', 365 | 'category' => 'Notifications', 366 | ], 367 | 'SYSTEMNOTIFICATION_DELETE' => [ 368 | 'name' => 'Delete a System Notification', 369 | 'category' => 'Notifications', 370 | ], 371 | 'SYSTEMNOTIFICATION_CREATE' => [ 372 | 'name' => 'Create a System Notification', 373 | 'category' => 'Notifications', 374 | ], 375 | ]; 376 | } 377 | } 378 | -------------------------------------------------------------------------------- /src/Report/NotificationReport.php: -------------------------------------------------------------------------------- 1 | 'Internal message', 21 | ]; 22 | 23 | public function title() 24 | { 25 | return _t(__CLASS__ . '.NOTIFICATION_REPORT', 'Notifications'); 26 | } 27 | 28 | public function group() 29 | { 30 | return _t(__CLASS__ . '.NOTIFICATION_REPORT_TITLE', "Notification reports"); 31 | } 32 | 33 | public function sourceRecords($params, $sort, $limit) 34 | { 35 | $type = $this->getReportType($params); 36 | 37 | $list = DataList::create($type); 38 | 39 | $fromDate = $params['From'] ?? date('Y-m-d', strtotime('-1 month')); 40 | $toDate = $params['To'] ?? date('Y-m-d'); 41 | 42 | $list = $list->filter([ 43 | 'Created:GreaterThan' => $fromDate, 44 | 'Created:LessThanOrEqual' => $toDate 45 | ]); 46 | return $list; 47 | } 48 | 49 | public function columns() 50 | { 51 | $ctrl = Controller::curr(); 52 | $params = $ctrl ? $ctrl->getRequest()->getVar('filter') : []; 53 | 54 | $type = $this->getReportType($params); 55 | return $type::config()->summary_fields; 56 | } 57 | 58 | protected function getReportType($params) 59 | { 60 | $type = $params['Type'] ?? InternalNotification::class; 61 | if (!isset(self::config()->notification_types[$type])) { 62 | throw new Exception("Invalid type"); 63 | } 64 | return $type; 65 | } 66 | 67 | public function parameterFields() 68 | { 69 | $ctrl = Controller::curr(); 70 | $params = $ctrl ? $ctrl->getRequest()->getVar('filter') : []; 71 | 72 | $fields = FieldList::create( 73 | DropdownField::create('Type', 'Notification type', self::config()->notification_types), 74 | $from = DateField::create('From'), 75 | $to = DateField::create('To') 76 | ); 77 | 78 | if (!isset($params['From'])) { 79 | $from->setValue(date('Y-m-d', strtotime('-1 month'))); 80 | } 81 | 82 | return $fields; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Service/EmailNotificationSender.php: -------------------------------------------------------------------------------- 1 | '%$Psr\Log\LoggerInterface', 35 | ]; 36 | 37 | /** 38 | * @var LoggerInterface 39 | */ 40 | public $logger; 41 | 42 | /** 43 | * Send a notification via email to the selected users 44 | * 45 | * @param SystemNotification $notification 46 | * @param \SilverStripe\ORM\DataObject $context 47 | * @param array $data 48 | */ 49 | public function sendNotification($notification, $context, $data) 50 | { 51 | $users = $notification->getRecipients($context); 52 | foreach ($users as $user) { 53 | $this->sendToUser($notification, $context, $user, $data); 54 | } 55 | } 56 | 57 | /** 58 | * Send a notification directly to a single user 59 | * 60 | * @param SystemNotification $notification 61 | * @param $context 62 | * @param $user 63 | * @param array $data 64 | */ 65 | public function sendToUser($notification, $context, $user, $data) 66 | { 67 | $subject = $notification->format($notification->Title, $context, $user, $data); 68 | 69 | if (Config::inst()->get(SystemNotification::class, 'html_notifications')) { 70 | $message = $notification->format( 71 | $notification->NotificationContent(), 72 | $context, 73 | $user, 74 | $data 75 | ); 76 | } else { 77 | $message = $notification->format( 78 | nl2br($notification->NotificationContent()), 79 | $context, 80 | $user, 81 | $data 82 | ); 83 | } 84 | 85 | if ($template = $notification->getTemplate()) { 86 | $templateData = $notification->getTemplateData($context, $user, $data); 87 | $templateData->setField('Body', $message); 88 | try { 89 | $body = $templateData->renderWith($template); 90 | } catch (Exception $e) { 91 | $body = $message; 92 | } 93 | } else { 94 | $body = $message; 95 | } 96 | 97 | $from = $this->config()->get('send_notifications_from'); 98 | $to = $user->Email; 99 | if (!$to && method_exists($user, 'getEmailAddress')) { 100 | $to = $user->getEmailAddress(); 101 | } 102 | 103 | if (!$from || !$to) { 104 | return; 105 | } 106 | // send 107 | try { 108 | $email = new Email($from, $to, $subject); 109 | $email->setBody($body); 110 | $this->extend('onBeforeSendToUser', $email); 111 | $email->send(); 112 | } catch (\Swift_SwiftException $e) { 113 | if ($this->logger) { 114 | if ($to !== 'admin') { 115 | $this->logger->warning("Failed sending email to $to"); 116 | } 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * @param LoggerInterface $logger 123 | * @return $this 124 | */ 125 | public function setLogger($logger) 126 | { 127 | $this->logger = $logger; 128 | 129 | return $this; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Service/InternalNotificationSender.php: -------------------------------------------------------------------------------- 1 | getRecipients($context); 30 | foreach ($users as $user) { 31 | $this->sendToUser($notification, $context, $user, $data); 32 | } 33 | } 34 | 35 | /** 36 | * Send a notification directly to a single user 37 | * 38 | * @param SystemNotification $notification 39 | * @param $context 40 | * @param $user 41 | * @param array $data 42 | */ 43 | public function sendToUser($notification, $context, $user, $data) 44 | { 45 | if (!($user instanceof Member)) { 46 | // don't send to non-member user object types 47 | return; 48 | } 49 | $subject = $notification->format($notification->Title, $context, $user, $data); 50 | 51 | $content = $notification->NotificationContent(); 52 | 53 | if (!Config::inst()->get(SystemNotification::class, 'html_notifications')) { 54 | $content = strip_tags($content); 55 | } 56 | 57 | $message = $notification->format( 58 | $content, 59 | $context, 60 | $user, 61 | $data 62 | ); 63 | 64 | if ($template = $notification->getTemplate()) { 65 | $templateData = $notification->getTemplateData($context, $user, $data); 66 | $templateData->setField('Body', $message); 67 | try { 68 | $body = $templateData->renderWith($template); 69 | } catch (Exception $e) { 70 | $body = $message; 71 | } 72 | } else { 73 | $body = $message; 74 | } 75 | 76 | $contextData = array_merge([ 77 | 'ClassName' => get_class($context), 78 | 'ID' => $context->ID, 79 | 'Link' => $context->hasMethod('Link') ? $context->Link() : '' 80 | ], $data); 81 | 82 | $notice = InternalNotification::create([ 83 | 'Title' => $subject, 84 | 'Message' => $body, 85 | 'ToID' => $user->ID, 86 | 'FromID' => Member::currentUserID(), 87 | 'SentOn' => date('Y-m-d H:i:s'), 88 | 'SourceObjectID' => $context->ID, 89 | 'SourceNotificationID' => $notification->ID, 90 | 'Context' => $contextData 91 | ]); 92 | 93 | $notice->write(); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/Service/NotificationService.php: -------------------------------------------------------------------------------- 1 | EmailNotificationSender::class, 28 | 'internal' => InternalNotificationSender::class, 29 | ]; 30 | 31 | /** 32 | * The list of channels to send to by default 33 | * @var array 34 | */ 35 | private static $default_channels = [ 36 | 'email', 37 | 'internal', 38 | ]; 39 | 40 | /** 41 | * Should we use the queued jobs approach to sending notifications? 42 | * @var Boolean 43 | */ 44 | private static $use_queues = true; 45 | 46 | /** 47 | * The objects to use for actually sending a notification, indexed 48 | * by their channel ID 49 | * @var array 50 | */ 51 | protected $senders; 52 | 53 | /** 54 | * The list of channels to send to 55 | * @var array 56 | */ 57 | protected $channels; 58 | 59 | public function __construct() 60 | { 61 | if (!ClassInfo::exists(QueuedJobService::class)) { 62 | $this->config()->use_queues = false; 63 | } 64 | 65 | $this->setSenders($this->config()->get('default_senders')); 66 | $this->setChannels($this->config()->get('default_channels')); 67 | } 68 | 69 | /** 70 | * Add a channel that this notification service should use when sending notifications 71 | * @param string $channel The channel to add 72 | * @return \Symbiote\Notifications\Service\NotificationService 73 | */ 74 | public function addChannel($channel) 75 | { 76 | $this->channels[] = $channel; 77 | 78 | return $this; 79 | } 80 | 81 | public function getChannels() 82 | { 83 | return $this->channels; 84 | } 85 | 86 | /** 87 | * Set the list of channels this notification service should use when sending notifications 88 | * @param array $channels The channels to send to 89 | * @return \Symbiote\Notifications\Service\NotificationService 90 | */ 91 | public function setChannels($channels) 92 | { 93 | $this->channels = $channels; 94 | 95 | return $this; 96 | } 97 | 98 | /** 99 | * Add a notification sender 100 | * @param string $channel The channel to send through 101 | * @param NotificationSender|string $sender The notification channel 102 | * @return \Symbiote\Notifications\Service\NotificationService 103 | */ 104 | public function addSender($channel, $sender) 105 | { 106 | $sender = is_string($sender) ? singleton($sender) : $sender; 107 | $this->senders[$channel] = $sender; 108 | 109 | return $this; 110 | } 111 | 112 | /** 113 | * Add a notification sender to a channel 114 | * @param array $senders 115 | * @return \Symbiote\Notifications\Service\NotificationService 116 | */ 117 | public function setSenders($senders) 118 | { 119 | $this->senders = []; 120 | if (count($senders)) { 121 | foreach ($senders as $channel => $sender) { 122 | $this->addSender($channel, $sender); 123 | } 124 | } 125 | 126 | return $this; 127 | } 128 | 129 | /** 130 | * Get a sender for a particular channel 131 | * @param string $channel 132 | * @return mixed|null 133 | */ 134 | public function getSender($channel) 135 | { 136 | return isset($this->senders[$channel]) ? $this->senders[$channel] : null; 137 | } 138 | 139 | /** 140 | * Trigger a notification event 141 | * @param string $identifier The Identifier of the notification event 142 | * @param DataObject $context The context (if relevant) of the object to notify on 143 | * @param array $data Extra data to be sent along with the notification 144 | * @param string|null $channel 145 | */ 146 | public function notify($identifier, $context, $data = [], $channel = null) 147 | { 148 | // okay, lets find any notification set up with this identifier 149 | if ($notifications = SystemNotification::get()->filter('Identifier', $identifier)) { 150 | foreach ($notifications as $notification) { 151 | if ($notification->NotifyOnClass) { 152 | $subclasses = ClassInfo::subclassesFor($notification->NotifyOnClass); 153 | if (!isset($subclasses[strtolower(get_class($context))])) { 154 | continue; 155 | } 156 | } 157 | 158 | // figure out the channels to send the notification on 159 | $channels = $channel ? [$channel] : []; 160 | if ($notification->Channels) { 161 | $channels = json_decode($notification->Channels); 162 | } 163 | 164 | $this->sendNotification($notification, $context, $data, $channels); 165 | } 166 | } 167 | } 168 | 169 | /** 170 | * Send out a notification 171 | * @param SystemNotification $notification The configured notification object 172 | * @param DataObject $context The context of the notification to send 173 | * @param array $extraData Any extra data to add into the notification text 174 | * @param string $channels The specific channels to send through. If not set, just 175 | * sends to the default configured 176 | */ 177 | public function sendNotification( 178 | SystemNotification $notification, 179 | DataObject $context, 180 | $extraData = [], 181 | $channels = null 182 | ) { 183 | // check to make sure that there are users to send it to. If not, we don't bother with it at all 184 | $recipients = $notification->getRecipients($context); 185 | if (!count($recipients)) { 186 | return; 187 | } 188 | 189 | // if we've got queues and a large number of recipients, lets send via a queued job instead 190 | if ($this->config()->get('use_queues') && count($recipients) > 5) { 191 | $extraData['SEND_CHANNELS'] = $channels; 192 | singleton(QueuedJobService::class)->queueJob( 193 | new SendNotificationJob( 194 | $notification, 195 | $context, 196 | $extraData 197 | ) 198 | ); 199 | } else { 200 | if (!is_array($channels)) { 201 | $channels = [$channels]; 202 | } 203 | $channels = count($channels) ? $channels : $this->channels; 204 | foreach ($channels as $channel) { 205 | if ($sender = $this->getSender($channel)) { 206 | $sender->sendNotification($notification, $context, $extraData); 207 | } 208 | } 209 | } 210 | } 211 | 212 | /** 213 | * Sends a notification directly to a user 214 | * @param SystemNotification $notification 215 | * @param DataObject $context 216 | * @param DataObject $user 217 | * @param array $extraData 218 | */ 219 | public function sendToUser( 220 | SystemNotification $notification, 221 | DataObject $context, 222 | $user, 223 | $extraData = [] 224 | ) { 225 | $channels = $this->channels; 226 | if ($extraData && isset($extraData['SEND_CHANNELS'])) { 227 | $channels = $extraData['SEND_CHANNELS']; 228 | unset($extraData['SEND_CHANNELS']); 229 | } 230 | 231 | if (!is_array($channels)) { 232 | $channels = [$channels]; 233 | } 234 | 235 | foreach ($channels as $channel) { 236 | if ($sender = $this->getSender($channel)) { 237 | $sender->sendToUser($notification, $context, $user, $extraData); 238 | } 239 | } 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /src/Service/NotifyService.php: -------------------------------------------------------------------------------- 1 | 'GET', 14 | 'read' => 'POST', 15 | 'see' => 'POST' 16 | ); 17 | } 18 | 19 | /** 20 | * List all the notifications a user has, on a particular item, 21 | * and/or of a particular type 22 | * 23 | * @return DataList|null 24 | */ 25 | public function list() 26 | { 27 | $member = Member::currentUser(); 28 | if (!$member) { 29 | return false; 30 | } 31 | 32 | return $member->getNotifications(); 33 | } 34 | 35 | /** 36 | * Mark a Notification as read, accepts a notification ID and returns a 37 | * boolean for success or failure. 38 | * 39 | * @param string|int $ID The ID of an InternalNotification for the current 40 | * logged in Member 41 | * @return boolean true when marked read otherwise false 42 | */ 43 | public function read($ID) 44 | { 45 | $member = Member::currentUser(); 46 | if (!$member) { 47 | return false; 48 | } 49 | 50 | if ($ID) { 51 | $notification = InternalNotification::get() 52 | ->filter([ 53 | 'ID' => $ID, 54 | 'ToID' => $member->ID, 55 | 'IsRead' => false 56 | ])->first(); 57 | if ($notification) { 58 | $notification->IsRead = true; 59 | $notification->write(); 60 | return true; 61 | } 62 | } 63 | return false; 64 | } 65 | 66 | /** 67 | * Mark a Notification as seen, accepts a notification ID and returns a 68 | * boolean for success or failure. 69 | * 70 | * @param string|int $ID The ID of an InternalNotification for the current 71 | * logged in Member 72 | * @return boolean true when marked seen otherwise false 73 | */ 74 | public function see($ID) 75 | { 76 | $member = Member::currentUser(); 77 | if (!$member) { 78 | return false; 79 | } 80 | 81 | if ($ID) { 82 | $notification = InternalNotification::get() 83 | ->filter([ 84 | 'ID' => $ID, 85 | 'ToID' => $member->ID 86 | ])->first(); 87 | if ($notification) { 88 | if (!$notification->IsSeen) { 89 | $notification->IsSeen = true; 90 | $notification->write(); 91 | } 92 | return true; 93 | } 94 | } 95 | return false; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /tests/DummyNotificationSender.php: -------------------------------------------------------------------------------- 1 | getRecipients($context); 20 | 21 | foreach ($users as $user) { 22 | $this->sendToUser($notification, $context, $user, $data); 23 | } 24 | } 25 | 26 | /** 27 | * Send a notification to a single user at a time 28 | * @param UserNotification $notification 29 | * @param $context 30 | * @param $user 31 | * @param array $data 32 | */ 33 | public function sendToUser($notification, $context, $user, $data) 34 | { 35 | $cls = new \stdClass(); 36 | $cls->notification = $notification; 37 | $cls->text = $notification->format($notification->NotificationText, $context, $user, $data); 38 | $cls->context = $context; 39 | $cls->user = $user; 40 | $cls->data = $data; 41 | 42 | $this->notifications[] = $cls; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/NotificationsTest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class NotificationsTest extends SapphireTest 17 | { 18 | protected static $fixture_file = __DIR__.'/NotificationsTest.yml'; 19 | 20 | protected static $extra_dataobjects = [ 21 | NotifyOnThis::class, 22 | ]; 23 | 24 | public function testNotificationTrigger() 25 | { 26 | // create a new notification and add it to the object 27 | $notification = new SystemNotification(); 28 | $notification->Title = "Notify on event"; 29 | $notification->Description = 'Notifies on an event occurring'; 30 | $notification->NotificationText = 'This is a notfication to $Member.Email about $NotifyOnThis.Title'; 31 | $notification->Identifier = 'NOTIFY_ON_EVENT'; 32 | $notification->NotifyOnClass = NotifyOnThis::class; 33 | $notification->write(); 34 | 35 | // okay, add it to our page 36 | $page = $this->objFromFixture(NotifyOnThis::class, 'not1'); 37 | 38 | $ns = new NotificationService(); 39 | $ds = new DummyNotificationSender(); 40 | 41 | Config::inst()->update(NotificationService::class, 'use_queues', false); 42 | 43 | $ns->addSender('dummy', $ds); 44 | $ns->setChannels(['dummy']); 45 | $ns->notify('NOTIFY_ON_EVENT', $page); 46 | 47 | // check that there was an actual notification added into our DS 48 | $this->assertEquals(1, count($ds->notifications)); 49 | 50 | // check that the message was formatted appropriately 51 | $msg = $ds->notifications[0]; 52 | $this->assertEquals( 53 | "This is a notfication to dummy@nowhere.com about Some Data Object", 54 | $msg->text 55 | ); 56 | } 57 | 58 | public function testSpecificChannels() 59 | { 60 | $notification = new SystemNotification(); 61 | $notification->Title = "Notify on event"; 62 | $notification->Description = 'Notifies on an event occurring'; 63 | $notification->NotificationText = 'This is a notfication to $Member.Email about $NotifyOnThis.Title'; 64 | $notification->Identifier = 'NOTIFY_ON_EVENT'; 65 | $notification->NotifyOnClass = NotifyOnThis::class; 66 | $notification->write(); 67 | 68 | // okay, add it to our page 69 | $page = $this->objFromFixture(NotifyOnThis::class, 'not1'); 70 | 71 | $ns = new NotificationService(); 72 | $ds = new DummyNotificationSender(); 73 | 74 | Config::inst()->update(NotificationService::class, 'use_queues', false); 75 | 76 | $ns->addSender('dummy', $ds); 77 | $ns->notify('NOTIFY_ON_EVENT', $page, [], 'dummy'); 78 | 79 | // now check that there was an actual notification added into our DS 80 | $this->assertEquals(1, count($ds->notifications)); 81 | 82 | $msg = $ds->notifications[0]; 83 | 84 | $this->assertEquals( 85 | "This is a notfication to dummy@nowhere.com about Some Data Object", 86 | $msg->text 87 | ); 88 | } 89 | 90 | public function testSendEmailNotification() 91 | { 92 | $notification = new SystemNotification(); 93 | $notification->Title = "Notify on event"; 94 | $notification->Description = 'Notifies on an event occurring'; 95 | $notification->NotificationText = 'This is a notfication to $Member.Email about $NotifyOnThis.Title'; 96 | $notification->Identifier = 'NOTIFY_ON_EVENT'; 97 | $notification->NotifyOnClass = NotifyOnThis::class; 98 | $notification->write(); 99 | 100 | // okay, add it to our page 101 | $page = $this->objFromFixture(NotifyOnThis::class, 'not1'); 102 | 103 | $ns = new NotificationService(); 104 | 105 | Config::inst()->update(NotificationService::class, 'use_queues', false); 106 | Config::inst()->update( 107 | EmailNotificationSender::class, 108 | 'send_notifications_from', 109 | 'test@test.com' 110 | ); 111 | Config::inst()->update(SystemNotification::class, 'default_template', false); 112 | 113 | $ns->setSenders(['email' => EmailNotificationSender::class]); 114 | $ns->setChannels(['email']); 115 | $ns->notify('NOTIFY_ON_EVENT', $page); 116 | 117 | // now check that there was an email sent 118 | $users = $page->getRecipients($notification->Identifier); 119 | $expectedTo = $users[0]->Email; 120 | $expectedFrom = 'test@test.com'; 121 | $expectedSubject = $notification->Title; 122 | $expectedBody = "This is a notfication to $expectedTo about $page->Title"; 123 | $expectedBody = $notification->format(nl2br($expectedBody), $page); // TODO 124 | 125 | $this->assertEmailSent($expectedTo, $expectedFrom, $expectedSubject); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/NotificationsTest.yml: -------------------------------------------------------------------------------- 1 | Symbiote\Notifications\Tests\NotifyOnThis: 2 | not1: 3 | Title: Some Data Object 4 | Status: Available -------------------------------------------------------------------------------- /tests/NotifyOnThis.php: -------------------------------------------------------------------------------- 1 | 'Varchar', 22 | 'NotifyBy' => 'Datetime', 23 | 'Status' => 'Varchar', 24 | ]; 25 | 26 | protected $availableKeywords; 27 | 28 | /** 29 | * Return a list of all available keywords in the format 30 | * array( 31 | * 'keyword' => 'A description' 32 | * ) 33 | * @return array 34 | */ 35 | public function getAvailableKeywords() 36 | { 37 | if (!$this->availableKeywords) { 38 | $objectFields = $this->config()->get('db'); 39 | 40 | $objectFields['Created'] = 'Created'; 41 | $objectFields['LastEdited'] = 'LastEdited'; 42 | 43 | $this->availableKeywords = []; 44 | 45 | foreach ($objectFields as $key => $value) { 46 | $this->availableKeywords[$key] = ['short' => $key, 'long' => $key]; 47 | } 48 | } 49 | 50 | return $this->availableKeywords; 51 | } 52 | 53 | /** 54 | * Gets an associative array of data that can be accessed in 55 | * notification fields and templates 56 | * @return array 57 | */ 58 | public function getNotificationTemplateData() 59 | { 60 | return []; 61 | } 62 | 63 | /** 64 | * Gets the list of recipients for a given notification event, based on this object's 65 | * state. 66 | * @param string $event The Identifier of the notification being sent 67 | * @return array 68 | */ 69 | public function getRecipients($event) 70 | { 71 | $member = new Member(); 72 | $member->Email = 'dummy@nowhere.com'; 73 | $member->FirstName = "First"; 74 | $member->Surname = "Last"; 75 | 76 | return [$member]; 77 | } 78 | } 79 | --------------------------------------------------------------------------------