diff --git a/db/migrations/20240408121908_display_alerts_migration.php b/db/migrations/20240408121908_display_alerts_migration.php new file mode 100644 index 0000000000..29071a4208 --- /dev/null +++ b/db/migrations/20240408121908_display_alerts_migration.php @@ -0,0 +1,50 @@ +. + */ + +use Phinx\Migration\AbstractMigration; + +/** + * Migration for adding more columns to displayevent table. + * Add a new column on Command table for createAlertOn. + * Add a new column on lkcommanddisplayprofile for createAlertOn. + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ +class DisplayAlertsMigration extends AbstractMigration +{ + public function change(): void + { + $this->table('displayevent') + ->changeColumn('start', 'integer', ['null' => true]) + ->addColumn('eventTypeId', 'integer', ['null' => false, 'default' => 1]) + ->addColumn('refId', 'integer', ['null' => true, 'default' => null]) + ->addColumn('detail', 'text', ['null' => true, 'default' => null]) + ->save(); + + $this->table('command') + ->addColumn('createAlertOn', 'string', ['null' => false, 'default' => 'never']) + ->save(); + + $this->table('lkcommanddisplayprofile') + ->addColumn('createAlertOn', 'string', ['null' => true, 'default' => null]) + ->save(); + } +} diff --git a/lib/Controller/Command.php b/lib/Controller/Command.php index b2d94d4e5d..a6847ed506 100644 --- a/lib/Controller/Command.php +++ b/lib/Controller/Command.php @@ -369,6 +369,14 @@ public function deleteForm(Request $request, Response $response, $id) * type="string", * required=false * ), + * @SWG\Parameter( + * name="createAlertOn", + * in="formData", + * description="On command execution, when should a Display alert be created? + * success, failure, always or never", + * type="string", + * required=false + * ), * @SWG\Response( * response=201, * description="successful operation", @@ -398,6 +406,7 @@ public function add(Request $request, Response $response) $command->userId = $this->getUser()->userId; $command->commandString = $sanitizedParams->getString('commandString'); $command->validationString = $sanitizedParams->getString('validationString'); + $command->createAlertOn = $sanitizedParams->getString('createAlertOn', ['default' => 'never']); $availableOn = $sanitizedParams->getArray('availableOn'); if (empty($availableOn)) { $command->availableOn = null; @@ -476,6 +485,14 @@ public function add(Request $request, Response $response) * type="string", * required=false * ), + * @SWG\Parameter( + * name="createAlertOn", + * in="formData", + * description="On command execution, when should a Display alert be created? + * success, failure, always or never", + * type="string", + * required=false + * ), * @SWG\Response( * response=200, * description="successful operation", @@ -496,6 +513,7 @@ public function edit(Request $request, Response $response, $id) $command->description = $sanitizedParams->getString('description'); $command->commandString = $sanitizedParams->getString('commandString'); $command->validationString = $sanitizedParams->getString('validationString'); + $command->createAlertOn = $sanitizedParams->getString('createAlertOn', ['default' => 'never']); $availableOn = $sanitizedParams->getArray('availableOn'); if (empty($availableOn)) { $command->availableOn = null; @@ -552,7 +570,7 @@ public function delete(Request $request, Response $response, $id) throw new AccessDeniedException(); } - $this->getDispatcher()->dispatch(CommandDeleteEvent::$NAME, new CommandDeleteEvent($command)); + $this->getDispatcher()->dispatch(new CommandDeleteEvent($command), CommandDeleteEvent::$NAME); $command->delete(); diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index 2a942c037a..4928791744 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -2474,6 +2474,8 @@ public function validateDisplays($displays) $event = $this->displayEventFactory->createEmpty(); $event->displayId = $display->displayId; $event->start = $display->lastAccessed; + // eventTypeId 1 is for Display up/down events. + $event->eventTypeId = 1; $event->save(); } diff --git a/lib/Controller/DisplayProfile.php b/lib/Controller/DisplayProfile.php index 3c548c01b4..3d27d9a93a 100644 --- a/lib/Controller/DisplayProfile.php +++ b/lib/Controller/DisplayProfile.php @@ -466,6 +466,7 @@ public function edit(Request $request, Response $response, $id) // Set and assign the command $command->commandString = $parsedParams->getString('commandString_' . $command->commandId); $command->validationString = $parsedParams->getString('validationString_' . $command->commandId); + $command->createAlertOn = $parsedParams->getString('createAlertOn_' . $command->commandId); $displayProfile->assignCommand($command); } else { diff --git a/lib/Entity/Command.php b/lib/Entity/Command.php index 5eefbb2e9c..90fd0d04ec 100644 --- a/lib/Entity/Command.php +++ b/lib/Entity/Command.php @@ -1,8 +1,8 @@ validationStringDisplayProfile) ? $this->validationString : $this->validationStringDisplayProfile; + return empty($this->validationStringDisplayProfile) + ? $this->validationString + : $this->validationStringDisplayProfile; + } + + /** + * @return string + */ + public function getCreateAlertOn(): string + { + return empty($this->createAlertOnDisplayProfile) + ? $this->createAlertOn + : $this->createAlertOnDisplayProfile; } /** @@ -210,18 +237,24 @@ public function isReady() public function validate() { if (!v::stringType()->notEmpty()->length(1, 254)->validate($this->command)) { - throw new InvalidArgumentException(__('Please enter a command name between 1 and 254 characters'), - 'command'); + throw new InvalidArgumentException( + __('Please enter a command name between 1 and 254 characters'), + 'command' + ); } if (!v::alpha('_')->NoWhitespace()->notEmpty()->length(1, 50)->validate($this->code)) { - throw new InvalidArgumentException(__('Please enter a code between 1 and 50 characters containing only alpha characters and no spaces'), - 'code'); + throw new InvalidArgumentException( + __('Please enter a code between 1 and 50 characters containing only alpha characters and no spaces'), + 'code' + ); } if (!v::stringType()->length(0, 1000)->validate($this->description)) { - throw new InvalidArgumentException(__('Please enter a description between 1 and 1000 characters'), - 'description'); + throw new InvalidArgumentException( + __('Please enter a description between 1 and 1000 characters'), + 'description' + ); } } @@ -251,14 +284,35 @@ public function save($options = []) */ public function delete() { - $this->getStore()->update('DELETE FROM `command` WHERE `commandId` = :commandId', ['commandId' => $this->commandId]); + $this->getStore()->update( + 'DELETE FROM `command` WHERE `commandId` = :commandId', + ['commandId' => $this->commandId] + ); } private function add() { $this->commandId = $this->getStore()->insert(' - INSERT INTO `command` (`command`, `code`, `description`, `userId`, `commandString`, `validationString`, `availableOn`) - VALUES (:command, :code, :description, :userId, :commandString, :validationString, :availableOn) + INSERT INTO `command` ( + `command`, + `code`, + `description`, + `userId`, + `commandString`, + `validationString`, + `availableOn`, + `createAlertOn` + ) + VALUES ( + :command, + :code, + :description, + :userId, + :commandString, + :validationString, + :availableOn, + :createAlertOn + ) ', [ 'command' => $this->command, 'code' => $this->code, @@ -266,7 +320,8 @@ private function add() 'userId' => $this->userId, 'commandString' => $this->commandString, 'validationString' => $this->validationString, - 'availableOn' => $this->availableOn + 'availableOn' => $this->availableOn, + 'createAlertOn' => $this->createAlertOn ]); } @@ -280,7 +335,8 @@ private function edit() `userId` = :userId, `commandString` = :commandString, `validationString` = :validationString, - `availableOn` = :availableOn + `availableOn` = :availableOn, + `createAlertOn` = :createAlertOn WHERE `commandId` = :commandId ', [ 'command' => $this->command, @@ -290,7 +346,8 @@ private function edit() 'commandId' => $this->commandId, 'commandString' => $this->commandString, 'validationString' => $this->validationString, - 'availableOn' => $this->availableOn + 'availableOn' => $this->availableOn, + 'createAlertOn' => $this->createAlertOn ]); } } \ No newline at end of file diff --git a/lib/Entity/DisplayEvent.php b/lib/Entity/DisplayEvent.php index 4d6f814461..094bfc60dd 100644 --- a/lib/Entity/DisplayEvent.php +++ b/lib/Entity/DisplayEvent.php @@ -1,8 +1,8 @@ . */ - namespace Xibo\Entity; + use Carbon\Carbon; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Xibo\Service\LogServiceInterface; use Xibo\Storage\StorageServiceInterface; @@ -39,55 +40,181 @@ class DisplayEvent implements \JsonSerializable public $eventDate; public $start; public $end; + public $eventTypeId; + public $refId; + public $detail; /** * Entity constructor. * @param StorageServiceInterface $store * @param LogServiceInterface $log - * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher + * @param EventDispatcherInterface $dispatcher */ - public function __construct($store, $log, $dispatcher) - { + public function __construct( + StorageServiceInterface $store, + LogServiceInterface $log, + EventDispatcherInterface $dispatcher + ) { $this->setCommonDependencies($store, $log, $dispatcher); } - public function save() + /** + * Save displayevent + * @return void + */ + public function save(): void { - if ($this->displayEventId == null) + if ($this->displayEventId == null) { $this->add(); - else + } else { $this->edit(); + } } - private function add() + /** + * Add a new displayevent + * @return void + */ + private function add(): void { $this->displayEventId = $this->getStore()->insert(' - INSERT INTO `displayevent` (eventDate, start, end, displayID) - VALUES (:eventDate, :start, :end, :displayId) + INSERT INTO `displayevent` (eventDate, start, end, displayID, eventTypeId, refId, detail) + VALUES (:eventDate, :start, :end, :displayId, :eventTypeId, :refId, :detail) ', [ 'eventDate' => Carbon::now()->format('U'), 'start' => $this->start, 'end' => $this->end, - 'displayId' => $this->displayId + 'displayId' => $this->displayId, + 'eventTypeId' => $this->eventTypeId, + 'refId' => $this->refId, + 'detail' => $this->detail, ]); } - private function edit() + /** + * Edit displayevent + * @return void + */ + private function edit(): void { - $this->getStore()->update('UPDATE `displayevent` SET `end` = :end WHERE statId = :statId', [ - 'displayevent' => $this->displayEventId, 'end' => $this->end + $this->getStore()->update(' + UPDATE displayevent + SET end = :end, + displayId = :displayId, + eventTypeId = :eventTypeId, + refId = :refId, + detail = :detail + WHERE displayEventId = :displayEventId + ', [ + 'displayEventId' => $this->displayEventId, + 'end' => $this->end, + 'displayId' => $this->displayId, + 'eventTypeId' => $this->eventTypeId, + 'refId' => $this->refId, + 'detail' => $this->detail, ]); } + /** - * Record the display coming online - * @param $displayId + * Record end date for specified display and event type. + * @param int $displayId + * @param int|null $date + * @param int $eventTypeId + * @return void */ - public function displayUp($displayId) + public function eventEnd(int $displayId, int $eventTypeId = 1, ?int $date = null): void { - $this->getStore()->update('UPDATE `displayevent` SET `end` = :toDt WHERE displayId = :displayId AND `end` IS NULL', [ - 'toDt' => Carbon::now()->format('U'), - 'displayId' => $displayId - ]); + $this->getLog()->debug( + sprintf( + 'displayEvent : end display alert for eventType %s and displayId %d', + $this->getEventNameFromId($eventTypeId), + $displayId + ) + ); + + $this->getStore()->update( + 'UPDATE `displayevent` SET `end` = :toDt + WHERE displayId = :displayId + AND `end` IS NULL + AND eventTypeId = :eventTypeId', + [ + 'toDt' => $date ?? Carbon::now()->format('U'), + 'displayId' => $displayId, + 'eventTypeId' => $eventTypeId, + ] + ); + } + + /** + * Record end date for specified display, event type and refId + * @param int $displayId + * @param int $eventTypeId + * @param int $refId + * @param int|null $date + * @return void + */ + public function eventEndByReference(int $displayId, int $eventTypeId, int $refId, ?int $date = null): void + { + $this->getLog()->debug( + sprintf( + 'displayEvent : end display alert for refId %d, displayId %d and eventType %s', + $refId, + $displayId, + $this->getEventNameFromId($eventTypeId), + ) + ); + + $this->getStore()->update( + 'UPDATE `displayevent` SET `end` = :toDt + WHERE displayId = :displayId + AND `end` IS NULL + AND eventTypeId = :eventTypeId + AND refId = :refId', + [ + 'toDt' => $date ?? Carbon::now()->format('U'), + 'displayId' => $displayId, + 'eventTypeId' => $eventTypeId, + 'refId' => $refId, + ] + ); + } + + /** + * Match event type string from log to eventTypeId in database. + * @param string $eventType + * @return int + */ + public function getEventIdFromString(string $eventType): int + { + return match ($eventType) { + 'Display Up/down' => 1, + 'App Start' => 2, + 'Power Cycle' => 3, + 'Network Cycle' => 4, + 'TV Monitoring' => 5, + 'Player Fault' => 6, + 'Command' => 7, + default => 8 + }; + } + + /** + * Match eventTypeId from database to string event name. + * @param int $eventTypeId + * @return string + */ + public function getEventNameFromId(int $eventTypeId): string + { + return match ($eventTypeId) { + 1 => __('Display Up/down'), + 2 => __('App Start'), + 3 => __('Power Cycle'), + 4 => __('Network Cycle'), + 5 => __('TV Monitoring'), + 6 => __('Player Fault'), + 7 => __('Command'), + default => __('Other') + }; } -} \ No newline at end of file +} diff --git a/lib/Entity/DisplayProfile.php b/lib/Entity/DisplayProfile.php index 9755331002..034baa7d54 100644 --- a/lib/Entity/DisplayProfile.php +++ b/lib/Entity/DisplayProfile.php @@ -1,6 +1,6 @@ getId() == $command->getId()) { $alreadyAssigned->commandString = $command->commandString; $alreadyAssigned->validationString = $command->validationString; + $alreadyAssigned->createAlertOn = $command->createAlertOn; $assigned = true; break; } } - if (!$assigned) + if (!$assigned) { $this->commands[] = $command; + } } /** @@ -481,22 +483,41 @@ private function manageAssignments() foreach ($this->commands as $command) { /* @var Command $command */ $this->getStore()->update(' - INSERT INTO `lkcommanddisplayprofile` (`commandId`, `displayProfileId`, `commandString`, `validationString`) VALUES - (:commandId, :displayProfileId, :commandString, :validationString) ON DUPLICATE KEY UPDATE commandString = :commandString2, validationString = :validationString2 + INSERT INTO `lkcommanddisplayprofile` ( + `commandId`, + `displayProfileId`, + `commandString`, + `validationString`, + `createAlertOn` + ) + VALUES ( + :commandId, + :displayProfileId, + :commandString, + :validationString, + :createAlertOn + ) + ON DUPLICATE KEY UPDATE + commandString = :commandString2, + validationString = :validationString2, + createAlertOn = :createAlertOn2 ', [ 'commandId' => $command->commandId, 'displayProfileId' => $this->displayProfileId, 'commandString' => $command->commandString, 'validationString' => $command->validationString, + 'createAlertOn' => $command->createAlertOn, 'commandString2' => $command->commandString, - 'validationString2' => $command->validationString + 'validationString2' => $command->validationString, + 'createAlertOn2' => $command->createAlertOn ]); } // Unlink $params = ['displayProfileId' => $this->displayProfileId]; - $sql = 'DELETE FROM `lkcommanddisplayprofile` WHERE `displayProfileId` = :displayProfileId AND `commandId` NOT IN (0'; + $sql = 'DELETE FROM `lkcommanddisplayprofile` + WHERE `displayProfileId` = :displayProfileId AND `commandId` NOT IN (0'; $i = 0; foreach ($this->commands as $command) { @@ -554,7 +575,10 @@ public function getCustomEditTemplate() if ($this->isCustom()) { return $this->displayProfileFactory->getCustomEditTemplate($this->getClientType()); } else { - $this->getLog()->error('Attempting to get Custom Edit template for Display Profile ' . $this->getClientType() . ' that is not custom'); + $this->getLog()->error( + 'Attempting to get Custom Edit template for Display Profile ' . + $this->getClientType() . ' that is not custom' + ); return null; } } diff --git a/lib/Factory/CommandFactory.php b/lib/Factory/CommandFactory.php index 19c6e3b558..5991e94c61 100644 --- a/lib/Factory/CommandFactory.php +++ b/lib/Factory/CommandFactory.php @@ -117,16 +117,19 @@ public function query($sortOrder = null, $filterBy = []) `command`.userId, `command`.availableOn, `command`.commandString, - `command`.validationString '; + `command`.validationString, + `command`.createAlertOn + '; if ($sanitizedFilter->getInt('displayProfileId') !== null) { $select .= ', :displayProfileId AS displayProfileId, `lkcommanddisplayprofile`.commandString AS commandStringDisplayProfile, - `lkcommanddisplayprofile`.validationString AS validationStringDisplayProfile '; + `lkcommanddisplayprofile`.validationString AS validationStringDisplayProfile, + `lkcommanddisplayprofile`.createAlertOn AS createAlertOnDisplayProfile '; } - $select .= " , (SELECT GROUP_CONCAT(DISTINCT `group`.group) + $select .= ' , (SELECT GROUP_CONCAT(DISTINCT `group`.group) FROM `permission` INNER JOIN `permissionentity` ON `permissionentity`.entityId = permission.entityId @@ -135,7 +138,7 @@ public function query($sortOrder = null, $filterBy = []) WHERE entity = :permissionEntityForGroup AND objectId = command.commandId AND view = 1 - ) AS groupsWithPermissions "; + ) AS groupsWithPermissions '; $params['permissionEntityForGroup'] = 'Xibo\\Entity\\Command'; $body = ' FROM `command` '; @@ -195,7 +198,14 @@ public function query($sortOrder = null, $filterBy = []) $params['userId'] = $sanitizedFilter->getInt('userId'); } - $this->viewPermissionSql('Xibo\Entity\Command', $body, $params, 'command.commandId', 'command.userId', $filterBy); + $this->viewPermissionSql( + 'Xibo\Entity\Command', + $body, + $params, + 'command.commandId', + 'command.userId', + $filterBy + ); // Sorting? $order = ''; diff --git a/lib/Report/DisplayAlerts.php b/lib/Report/DisplayAlerts.php new file mode 100644 index 0000000000..37abf820e3 --- /dev/null +++ b/lib/Report/DisplayAlerts.php @@ -0,0 +1,353 @@ +. + */ + +namespace Xibo\Report; + +use Carbon\Carbon; +use Psr\Container\ContainerInterface; +use Xibo\Controller\DataTablesDotNetTrait; +use Xibo\Entity\ReportForm; +use Xibo\Entity\ReportResult; +use Xibo\Entity\ReportSchedule; +use Xibo\Factory\DisplayEventFactory; +use Xibo\Factory\DisplayFactory; +use Xibo\Factory\DisplayGroupFactory; +use Xibo\Helper\DateFormatHelper; +use Xibo\Helper\Translate; +use Xibo\Support\Sanitizer\SanitizerInterface; + +/** + * Class DisplayAlerts + * @package Xibo\Report + */ +class DisplayAlerts implements ReportInterface +{ + use ReportDefaultTrait, DataTablesDotNetTrait; + + /** @var DisplayFactory */ + private $displayFactory; + /** @var DisplayGroupFactory */ + private $displayGroupFactory; + /** @var DisplayEventFactory */ + private $displayEventFactory; + + public function setFactories(ContainerInterface $container) + { + $this->displayFactory = $container->get('displayFactory'); + $this->displayGroupFactory = $container->get('displayGroupFactory'); + $this->displayEventFactory = $container->get('displayEventFactory'); + + return $this; + } + + public function getReportEmailTemplate() + { + return 'displayalerts-email-template.twig'; + } + + public function getSavedReportTemplate() + { + return 'displayalerts-report-preview'; + } + + public function getReportForm() + { + return new ReportForm( + 'displayalerts-report-form', + 'displayalerts', + 'Display', + [ + 'fromDate' => Carbon::now()->startOfMonth()->format(DateFormatHelper::getSystemFormat()), + 'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat()), + ] + ); + } + + public function getReportScheduleFormData(SanitizerInterface $sanitizedParams) + { + $data = []; + $data['reportName'] = 'displayalerts'; + + return [ + 'template' => 'displayalerts-schedule-form-add', + 'data' => $data + ]; + } + + public function setReportScheduleFormData(SanitizerInterface $sanitizedParams) + { + $filter = $sanitizedParams->getString('filter'); + $displayId = $sanitizedParams->getInt('displayId'); + $displayGroupIds = $sanitizedParams->getIntArray('displayGroupId', ['default' => []]); + $filterCriteria['displayId'] = $displayId; + + if (empty($displayId) && count($displayGroupIds) > 0) { + $filterCriteria['displayGroupId'] = $displayGroupIds; + } + + $filterCriteria['filter'] = $filter; + + $schedule = ''; + if ($filter == 'daily') { + $schedule = ReportSchedule::$SCHEDULE_DAILY; + $filterCriteria['reportFilter'] = 'yesterday'; + } elseif ($filter == 'weekly') { + $schedule = ReportSchedule::$SCHEDULE_WEEKLY; + $filterCriteria['reportFilter'] = 'lastweek'; + } elseif ($filter == 'monthly') { + $schedule = ReportSchedule::$SCHEDULE_MONTHLY; + $filterCriteria['reportFilter'] = 'lastmonth'; + } elseif ($filter == 'yearly') { + $schedule = ReportSchedule::$SCHEDULE_YEARLY; + $filterCriteria['reportFilter'] = 'lastyear'; + } + + $filterCriteria['sendEmail'] = $sanitizedParams->getCheckbox('sendEmail'); + $filterCriteria['nonusers'] = $sanitizedParams->getString('nonusers'); + + // Return + return [ + 'filterCriteria' => json_encode($filterCriteria), + 'schedule' => $schedule + ]; + } + + public function generateSavedReportName(SanitizerInterface $sanitizedParams) + { + return sprintf(__('%s report for Display'), ucfirst($sanitizedParams->getString('filter'))); + } + + public function restructureSavedReportOldJson($json) + { + return $json; + } + + public function getSavedReportResults($json, $savedReport) + { + $metadata = [ + 'periodStart' => $json['metadata']['periodStart'], + 'periodEnd' => $json['metadata']['periodEnd'], + 'generatedOn' => Carbon::createFromTimestamp($savedReport->generatedOn) + ->format(DateFormatHelper::getSystemFormat()), + 'title' => $savedReport->saveAs, + ]; + + // Report result object + return new ReportResult( + $metadata, + $json['table'], + $json['recordsTotal'], + ); + } + + public function getResults(SanitizerInterface $sanitizedParams) + { + $displayIds = $this->getDisplayIdFilter($sanitizedParams); + $onlyLoggedIn = $sanitizedParams->getCheckbox('onlyLoggedIn') == 1; + + $currentDate = Carbon::now()->startOfDay(); + + // + // From and To Date Selection + // -------------------------- + // Our report has a range filter which determines whether the user has to enter their own from / to dates + // check the range filter first and set from/to dates accordingly. + $reportFilter = $sanitizedParams->getString('reportFilter'); + + // Use the current date as a helper + $now = Carbon::now(); + + switch ($reportFilter) { + // the monthly data starts from yesterday + case 'yesterday': + $fromDt = $now->copy()->startOfDay()->subDay(); + $toDt = $now->copy()->startOfDay(); + break; + + case 'lastweek': + $fromDt = $now->copy()->locale(Translate::GetLocale())->startOfWeek()->subWeek(); + $toDt = $fromDt->copy()->addWeek(); + break; + + case 'lastmonth': + $fromDt = $now->copy()->startOfMonth()->subMonth(); + $toDt = $fromDt->copy()->addMonth(); + break; + + case 'lastyear': + $fromDt = $now->copy()->startOfYear()->subYear(); + $toDt = $fromDt->copy()->addYear(); + break; + + case '': + default: + // Expect dates to be provided. + $fromDt = $sanitizedParams->getDate('fromDt'); + $toDt = $sanitizedParams->getDate('toDt'); + + $fromDt = $fromDt->startOfDay(); + + // If toDt is current date then make it current datetime + if ($toDt->format('Y-m-d') == $currentDate->format('Y-m-d')) { + $toDt = Carbon::now(); + } else { + $toDt = $toDt->addDay()->startOfDay(); + } + + break; + } + + $metadata = [ + 'periodStart' => Carbon::createFromTimestamp($fromDt->toDateTime()->format('U')) + ->format(DateFormatHelper::getSystemFormat()), + 'periodEnd' => Carbon::createFromTimestamp($toDt->toDateTime()->format('U')) + ->format(DateFormatHelper::getSystemFormat()), + ]; + + $params = [ + 'start' => $fromDt->format('U'), + 'end' => $toDt->format('U') + ]; + + $sql = 'SELECT + `displayevent`.displayId, + `display`.display, + `displayevent`.start, + `displayevent`.end, + `displayevent`.eventTypeId, + `displayevent`.refId, + `displayevent`.detail + FROM `displayevent` + INNER JOIN `display` ON `display`.displayId = `displayevent`.displayId + INNER JOIN `lkdisplaydg` ON `display`.displayId = `lkdisplaydg`.displayId + INNER JOIN `displaygroup` ON `displaygroup`.displayGroupId = `lkdisplaydg`.displayGroupId + AND `displaygroup`.isDisplaySpecific = 1 + WHERE `displayevent`.eventDate BETWEEN :start AND :end '; + + if (count($displayIds) > 0) { + $sql .= 'AND `displayevent`.displayId IN (' . implode(',', $displayIds) . ')'; + } + + if ($onlyLoggedIn) { + $sql .= ' AND `display`.loggedIn = 1 '; + } + + // Tags + if (!empty($sanitizedParams->getString('tags'))) { + $tagFilter = $sanitizedParams->getString('tags'); + + if (trim($tagFilter) === '--no-tag') { + $sql .= ' AND `displaygroup`.displaygroupId NOT IN ( + SELECT `lktagdisplaygroup`.displaygroupId + FROM tag + INNER JOIN `lktagdisplaygroup` + ON `lktagdisplaygroup`.tagId = tag.tagId + ) + '; + } else { + $operator = $sanitizedParams->getCheckbox('exactTags') == 1 ? '=' : 'LIKE'; + $logicalOperator = $sanitizedParams->getString('logicalOperator', ['default' => 'OR']); + $allTags = explode(',', $tagFilter); + $notTags = []; + $tags = []; + + foreach ($allTags as $tag) { + if (str_starts_with($tag, '-')) { + $notTags[] = ltrim(($tag), '-'); + } else { + $tags[] = $tag; + } + } + + if (!empty($notTags)) { + $sql .= ' AND `displaygroup`.displaygroupId NOT IN ( + SELECT `lktagdisplaygroup`.displaygroupId + FROM tag + INNER JOIN `lktagdisplaygroup` + ON `lktagdisplaygroup`.tagId = tag.tagId + '; + + $this->displayFactory->tagFilter( + $notTags, + 'lktagdisplaygroup', + 'lkTagDisplayGroupId', + 'displayGroupId', + $logicalOperator, + $operator, + true, + $sql, + $params + ); + } + + if (!empty($tags)) { + $sql .= ' AND `displaygroup`.displaygroupId IN ( + SELECT `lktagdisplaygroup`.displaygroupId + FROM tag + INNER JOIN `lktagdisplaygroup` + ON `lktagdisplaygroup`.tagId = tag.tagId + '; + + $this->displayFactory->tagFilter( + $tags, + 'lktagdisplaygroup', + 'lkTagDisplayGroupId', + 'displayGroupId', + $logicalOperator, + $operator, + false, + $sql, + $params + ); + } + } + } + + // Sorting? + $sortOrder = $this->gridRenderSort($sanitizedParams); + + if (is_array($sortOrder)) { + $sql .= 'ORDER BY ' . implode(',', $sortOrder); + } + + $rows = []; + foreach ($this->store->select($sql, $params) as $row) { + $displayEvent = $this->displayEventFactory->createEmpty()->hydrate($row); + $displayEvent->setUnmatchedProperty( + 'eventType', + $displayEvent->getEventNameFromId($displayEvent->eventTypeId) + ); + $displayEvent->setUnmatchedProperty( + 'display', + $row['display'] + ); + + $rows[] = $displayEvent; + } + + return new ReportResult( + $metadata, + $rows, + count($rows), + ); + } +} diff --git a/lib/Report/TimeConnected.php b/lib/Report/TimeConnected.php index 64080a8d9f..da3eb0e07a 100644 --- a/lib/Report/TimeConnected.php +++ b/lib/Report/TimeConnected.php @@ -1,8 +1,8 @@ 0) { - $query .= ' WHERE display.displayID IN (' . implode(',', $displayIds) . ') '; + $query .= ' AND display.displayID IN (' . implode(',', $displayIds) . ') '; } $query .= ' diff --git a/lib/Report/TimeDisconnectedSummary.php b/lib/Report/TimeDisconnectedSummary.php index ce261f2962..ef3fe494a5 100644 --- a/lib/Report/TimeDisconnectedSummary.php +++ b/lib/Report/TimeDisconnectedSummary.php @@ -1,6 +1,6 @@ = :start - AND :end <= UNIX_TIMESTAMP(NOW()) '; + AND :end <= UNIX_TIMESTAMP(NOW()) + AND `displayevent`.eventTypeId = 1 '; if (count($displayIds) > 0) { $body .= 'AND display.displayId IN (' . implode(',', $displayIds) . ') '; diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php index 6aafba9fa0..b255b9e2e0 100644 --- a/lib/Xmds/Soap.php +++ b/lib/Xmds/Soap.php @@ -43,6 +43,7 @@ use Xibo\Factory\MediaFactory; use Xibo\Factory\ModuleFactory; use Xibo\Factory\NotificationFactory; +use Xibo\Factory\PlayerFaultFactory; use Xibo\Factory\PlayerVersionFactory; use Xibo\Factory\RegionFactory; use Xibo\Factory\RequiredFileFactory; @@ -167,6 +168,9 @@ class Soap /** @var \Xibo\Factory\CampaignFactory */ private $campaignFactory; + /** @var PlayerFaultFactory */ + protected $playerFaultFactory; + /** * Soap constructor. * @param LogProcessor $logProcessor @@ -220,7 +224,8 @@ public function __construct( $playerVersionFactory, $dispatcher, $campaignFactory, - $syncGroupFactory + $syncGroupFactory, + $playerFaultFactory ) { $this->logProcessor = $logProcessor; $this->pool = $pool; @@ -247,6 +252,7 @@ public function __construct( $this->dispatcher = $dispatcher; $this->campaignFactory = $campaignFactory; $this->syncGroupFactory = $syncGroupFactory; + $this->playerFaultFactory = $playerFaultFactory; } /** @@ -1737,6 +1743,14 @@ protected function doSubmitLog($serverKey, $hardwareKey, $logXml) continue; } + // special handling for event + // this will create record in displayevent table + // and is not added to the logs. + if ($cat == 'event') { + $this->createDisplayAlert($node); + continue; + } + // Does this meet the current log level? if ($cat == 'error') { $recordLogLevel = Logger::ERROR; @@ -1758,25 +1772,7 @@ protected function doSubmitLog($serverKey, $hardwareKey, $logXml) } // Adjust the date according to the display timezone - try { - $date = ($this->display->timeZone != null) - ? Carbon::createFromFormat( - DateFormatHelper::getSystemFormat(), - $date, - $this->display->timeZone - )->tz($defaultTimeZone) - : Carbon::createFromFormat( - DateFormatHelper::getSystemFormat(), - $date - ); - $date = $date->format(DateFormatHelper::getSystemFormat()); - } catch (\Exception $e) { - // Protect against the date format being unreadable - $this->getLog()->debug('Date format unreadable on log message: ' . $date); - - // Use now instead - $date = Carbon::now()->format(DateFormatHelper::getSystemFormat()); - } + $date = $this->adjustDisplayLogDate($date, DateFormatHelper::getSystemFormat()); // Get the date and the message (all log types have these) foreach ($node->childNodes as $nodeElements) { @@ -2657,12 +2653,11 @@ protected function alertDisplayUp() { $maintenanceEnabled = $this->getConfig()->getSetting('MAINTENANCE_ENABLED'); - if ($this->display->loggedIn == 0) { - + if ($this->display->loggedIn == 0 && !empty($this->display->displayId)) { $this->getLog()->info(sprintf('Display %s was down, now its up.', $this->display->display)); // Log display up - $this->displayEventFactory->createEmpty()->displayUp($this->display->displayId); + $this->displayEventFactory->createEmpty()->eventEnd($this->display->displayId); $dayPartId = $this->display->getSetting('dayPartId', null,['displayOverride' => true]); @@ -3041,4 +3036,86 @@ protected function setDateFilters() $this->getLog()->debug(sprintf('FromDT = %s [%d]. ToDt = %s [%d]', $fromFilter->toRssString(), $fromFilter->format('U'), $toFilter->toRssString(), $toFilter->format('U'))); } + + /** + * Adjust the log date according to the Display timezone. + * Return current date if we fail. + * @param string $date + * @param string $format + * @return string + */ + protected function adjustDisplayLogDate(string $date, string $format): string + { + // Get the display timezone to use when adjusting log dates. + $defaultTimeZone = $this->getConfig()->getSetting('defaultTimezone'); + + // Adjust the date according to the display timezone + try { + $date = ($this->display->timeZone != null) + ? Carbon::createFromFormat( + DateFormatHelper::getSystemFormat(), + $date, + $this->display->timeZone + )->tz($defaultTimeZone) + : Carbon::createFromFormat( + DateFormatHelper::getSystemFormat(), + $date + ); + $date = $date->format($format); + } catch (\Exception $e) { + // Protect against the date format being unreadable + $this->getLog()->debug('Date format unreadable on log message: ' . $date); + + // Use now instead + $date = Carbon::now()->format($format); + } + + return $date; + } + + private function createDisplayAlert(\DomElement $alertNode) + { + $date = $this->adjustDisplayLogDate($alertNode->getAttribute('date'), 'U'); + $eventType = ''; + $refId = ''; + $detail = ''; + $alertType = ''; + + // Get the nodes we are expecting + foreach ($alertNode->childNodes as $nodeElements) { + if ($nodeElements->nodeName == 'eventType') { + $eventType = $nodeElements->textContent; + } else if ($nodeElements->nodeName == 'refId') { + $refId = $nodeElements->textContent; + } else if ($nodeElements->nodeName == 'message') { + $detail = $nodeElements->textContent; + } else if ($nodeElements->nodeName == 'alertType') { + $alertType = $nodeElements->textContent; + } + } + + // if alerts should provide both start and end or just start + if ($alertType == 'both' || $alertType == 'start') { + $displayEvent = $this->displayEventFactory->createEmpty(); + + // new record populated from the submitLog xml. + $displayEvent->displayId = $this->display->displayId; + $displayEvent->eventTypeId = $displayEvent->getEventIdFromString($eventType); + $displayEvent->eventDate = $date; + $displayEvent->start = $date; + $displayEvent->end = $alertType == 'both' ? $date : null; + $displayEvent->refId = empty($refId) ? null : $refId; + $displayEvent->detail = $detail; + + $displayEvent->save(); + } else if ($alertType == 'end') { + // if this event pertain only to end date for an existing event record, + // then set the end date for this display and the specified eventType + $displayEvent = $this->displayEventFactory->createEmpty(); + $eventTypeId = $displayEvent->getEventIdFromString($eventType); + empty($refId) + ? $displayEvent->eventEnd($this->display->displayId, $eventTypeId, $date) + : $displayEvent->eventEndByReference($this->display->displayId, $eventTypeId, $refId); + } + } } diff --git a/lib/Xmds/Soap5.php b/lib/Xmds/Soap5.php index 7d898c3ffa..fdbba867a6 100644 --- a/lib/Xmds/Soap5.php +++ b/lib/Xmds/Soap5.php @@ -329,6 +329,7 @@ public function RegisterDisplay( } $node = $return->createElement($command->code); + $node->setAttribute('createAlertOn', $command->getCreateAlertOn()); $commandString = $return->createElement('commandString'); $commandStringCData = $return->createCDATASection($command->getCommandString()); $commandString->appendChild($commandStringCData); diff --git a/lib/Xmds/Soap6.php b/lib/Xmds/Soap6.php index 0dfc7acdbb..e053332a97 100644 --- a/lib/Xmds/Soap6.php +++ b/lib/Xmds/Soap6.php @@ -25,6 +25,7 @@ use Carbon\Carbon; use Xibo\Entity\Bandwidth; use Xibo\Helper\DateFormatHelper; +use Xibo\Support\Sanitizer\SanitizerInterface; /** * Class Soap6 @@ -57,7 +58,10 @@ public function reportFaults(string $serverKey, string $hardwareKey, string $fau // Check the serverKey matches if ($serverKey != $this->getConfig()->getSetting('SERVER_KEY')) { - throw new \SoapFault('Sender', 'The Server key you entered does not match with the server key at this address'); + throw new \SoapFault( + 'Sender', + 'The Server key you entered does not match with the server key at this address' + ); } // Auth this request... @@ -70,17 +74,23 @@ public function reportFaults(string $serverKey, string $hardwareKey, string $fau throw new \SoapFault('Receiver', 'Bandwidth Limit exceeded'); } + $faultDecoded = json_decode($fault, true); + + // check if we should record or update any display events. + $this->manageDisplayAlerts($faultDecoded); + // clear existing records from player_faults table $this->getStore()->update('DELETE FROM `player_faults` WHERE displayId = :displayId', [ 'displayId' => $this->display->displayId ]); - $faultDecoded = json_decode($fault, true); - foreach ($faultDecoded as $faultAlert) { $sanitizedFaultAlert = $this->getSanitizer($faultAlert); - $incidentDt = $sanitizedFaultAlert->getDate('date', ['default' => Carbon::now()])->format(DateFormatHelper::getSystemFormat()); + $incidentDt = $sanitizedFaultAlert->getDate( + 'date', + ['default' => Carbon::now()] + )->format(DateFormatHelper::getSystemFormat()); $expires = $sanitizedFaultAlert->getDate('expires', ['default' => null]); $code = $sanitizedFaultAlert->getInt('code'); $reason = $sanitizedFaultAlert->getString('reason'); @@ -125,4 +135,99 @@ public function reportFaults(string $serverKey, string $hardwareKey, string $fau return true; } + + private function manageDisplayAlerts(array $newPlayerFaults) + { + // check current faults for player + $currentFaults = $this->playerFaultFactory->getByDisplayId($this->display->displayId); + + // if we had faults and now we no longer have any to add + // set end date for any existing fault events + // add display event for cleared all faults + if (!empty($currentFaults) && empty($newPlayerFaults)) { + $displayEvent = $this->displayEventFactory->createEmpty(); + $displayEvent->eventTypeId = $displayEvent->getEventIdFromString('Player Fault'); + $displayEvent->displayId = $this->display->displayId; + // clear any open player fault events for this display + $displayEvent->eventEnd($displayEvent->displayId, $displayEvent->eventTypeId); + + // log new event for all faults cleared. + $displayEvent->start = Carbon::now()->format('U'); + $displayEvent->end = Carbon::now()->format('U'); + $displayEvent->detail = __('All Player faults cleared'); + $displayEvent->save(); + } else if (empty($currentFaults) && !empty($newPlayerFaults)) { + $codesAdded = []; + // we do not have any faults currently, but new ones will be added + foreach ($newPlayerFaults as $newPlayerFault) { + $sanitizedFaultAlert = $this->getSanitizer($newPlayerFault); + // if we do not have an alert for the specific code yet, add it + if (!in_array($sanitizedFaultAlert->getInt('code'), $codesAdded)) { + $this->addDisplayEvent($sanitizedFaultAlert); + // keep track of added codes, we want a single alert per code + $codesAdded[] = $sanitizedFaultAlert->getInt('code'); + } + } + } else if (!empty($newPlayerFaults) && !empty($currentFaults)) { + // we have both existing faults and new faults + $existingFaultsCodes = []; + $newFaultCodes = []; + $codesAdded = []; + + // keep track of existing fault codes. + foreach ($currentFaults as $currentFault) { + $existingFaultsCodes[] = $currentFault->code; + } + + // go through new faults + foreach ($newPlayerFaults as $newPlayerFault) { + $sanitizedFaultAlert = $this->getSanitizer($newPlayerFault); + $newFaultCodes[] = $sanitizedFaultAlert->getInt('code'); + // if it already exists, we do not do anything with alerts + // if it is a new code and was not added already + // add it now + if (!in_array($sanitizedFaultAlert->getInt('code'), $existingFaultsCodes) + && !in_array($sanitizedFaultAlert->getInt('code'), $codesAdded) + ) { + $this->addDisplayEvent($sanitizedFaultAlert); + // keep track of added codes, we want a single alert per code + $codesAdded[] = $sanitizedFaultAlert->getInt('code'); + } + } + + // go through any existing codes that are no longer reported + // update the end date on all of them. + foreach (array_diff($existingFaultsCodes, $newFaultCodes) as $code) { + $displayEvent = $this->displayEventFactory->createEmpty(); + $displayEvent->eventEndByReference( + $this->display->displayId, + $displayEvent->getEventIdFromString('Player Fault'), + $code + ); + } + } + } + + private function addDisplayEvent(SanitizerInterface $sanitizedFaultAlert) + { + $this->getLog()->debug( + sprintf( + 'displayEvent : add new display alert for player fault code %d and displayId %d', + $sanitizedFaultAlert->getInt('code'), + $this->display->displayId + ) + ); + + $displayEvent = $this->displayEventFactory->createEmpty(); + $displayEvent->eventTypeId = $displayEvent->getEventIdFromString('Player Fault'); + $displayEvent->displayId = $this->display->displayId; + $displayEvent->start = $sanitizedFaultAlert->getDate( + 'date', + ['default' => Carbon::now()] + )->format('U'); + $displayEvent->end = null; + $displayEvent->detail = $sanitizedFaultAlert->getString('reason'); + $displayEvent->refId = $sanitizedFaultAlert->getInt('code'); + $displayEvent->save(); + } } diff --git a/reports/displayalerts-email-template.twig b/reports/displayalerts-email-template.twig new file mode 100644 index 0000000000..6f03d91f37 --- /dev/null +++ b/reports/displayalerts-email-template.twig @@ -0,0 +1,56 @@ +{# +/** + * Copyright (C) 2024 Xibo Signage Ltd + * + * Xibo - Digital Signage - https://xibosignage.com + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +#} +{% extends "base-report.twig" %} + +{% block content %} +
+ {% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }} +
+

+ + + + + + + + + + + + {% for item in tableData %} + + + + + + + + + + {% endfor %} +
{% trans "Display ID" %}{% trans "Display" %}{% trans "Event Type" %}{% trans "Start" %}{% trans "End" %}{% trans "Reference ID" %}{% trans "Detail" %}
{{ item.displayId }}{{ item.display }}{{ item.eventType }}{{ item.start }}{{ item.end }}{{ item.refId }}{{ item.detail }}
+
+ {{ placeholder }} + +{% endblock %} \ No newline at end of file diff --git a/reports/displayalerts-report-form.twig b/reports/displayalerts-report-form.twig new file mode 100644 index 0000000000..1b22fb6cc8 --- /dev/null +++ b/reports/displayalerts-report-form.twig @@ -0,0 +1,267 @@ +{# +/** + * Copyright (C) 2024 Xibo Signage Ltd + * + * Xibo - Digital Signage - https://xibosignage.com + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +#} + +{% extends "authed.twig" %} +{% import "inline.twig" as inline %} + +{% block title %}{% trans "Report: Display Alerts" %} | {% endblock %} + +{% block actionMenu %} + {% include "report-schedule-buttons.twig" %} +{% endblock %} + +{% block pageContent %} + +
+
+ {% trans "Display Alerts" %} +
+ + {% include "report-selector.twig" %} + +
+
+
+
+
+ {% set title %}{% trans "From Date" %}{% endset %} + {{ inline.date("fromDt", title, defaults.fromDate, "", "", "", "") }} + + {% set title %}{% trans "To Date" %}{% endset %} + {{ inline.date("toDt", title, defaults.toDate, "", "", "", "") }} + + {% set title %}{% trans "Display" %}{% endset %} + {% set attributes = [ + { name: "data-width", value: "200px" }, + { name: "data-allow-clear", value: "true" }, + { name: "data-placeholder--id", value: null }, + { name: "data-placeholder--value", value: "" }, + { name: "data-search-url", value: url_for("display.search") }, + { name: "data-search-term", value: "display" }, + { name: "data-search-term-tags", value: "tags" }, + { name: "data-id-property", value: "displayId" }, + { name: "data-text-property", value: "display" } + ] %} + {{ inline.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }} + + {% set title %}{% trans "Display Group" %}{% endset %} + {% set attributes = [ + { name: "data-width", value: "200px" }, + { name: "data-allow-clear", value: "true" }, + { name: "data-placeholder--id", value: null }, + { name: "data-placeholder--value", value: "" }, + { name: "data-search-url", value: url_for("displayGroup.search") }, + { name: "data-search-term", value: "displayGroup" }, + { name: "data-id-property", value: "displayGroupId" }, + { name: "data-text-property", value: "displayGroup" } + ] %} + {{ inline.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }} + + {% if currentUser.featureEnabled("tag.tagging") %} + {% set title %}{% trans "Tags" %}{% endset %} + {% set exactTagTitle %}{% trans "Exact match?" %}{% endset %} + {% set logicalOperatorTitle %}{% trans "When filtering by multiple Tags, which logical operator should be used?" %}{% endset %} + {% set helpText %}{% trans "A comma separated list of tags to filter by. Enter --no-tag to see items without tags." %}{% endset %} + {{ inline.inputWithTags("tags", title, null, helpText, null, null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }} + {% endif %} + + {% set title %}{% trans "Only show currently logged in?" %}{% endset %} + {{ inline.checkbox("onlyLoggedIn", title) }} + +
+ + {% trans "Apply" %} + + + +
+
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Display ID" %}{% trans "Display" %}{% trans "Event Type" %}{% trans "Start" %}{% trans "End" %}{% trans "Reference" %}{% trans "Detail" %}
+
+
+
+
+
+ +{% endblock %} + +{% block javaScript %} + +{% endblock %} \ No newline at end of file diff --git a/reports/displayalerts-report-preview.twig b/reports/displayalerts-report-preview.twig new file mode 100644 index 0000000000..9a3289725e --- /dev/null +++ b/reports/displayalerts-report-preview.twig @@ -0,0 +1,122 @@ +{# +/** + * Copyright (C) 2020 Xibo Signage Ltd + * + * Xibo - Digital Signage - http://www.xibo.org.uk + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +#} + +{% extends "authed.twig" %} +{% import "inline.twig" as inline %} + +{% block actionMenu %} +
+ +
+{% endblock %} + +{% block pageContent %} + +
+
+ + {{ metadata.title }} + ({% trans "Generated on: " %}{{ metadata.generatedOn }}) +
{% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }}
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + +
{% trans "Display ID" %}{% trans "Display" %}{% trans "Event Type" %}{% trans "Start" %}{% trans "End" %}{% trans "Reference" %}{% trans "Detail" %}
+
+
+
+
+ +{% endblock %} + +{% block javaScript %} + +{% endblock %} \ No newline at end of file diff --git a/reports/displayalerts-schedule-form-add.twig b/reports/displayalerts-schedule-form-add.twig new file mode 100644 index 0000000000..5a60f409a8 --- /dev/null +++ b/reports/displayalerts-schedule-form-add.twig @@ -0,0 +1,106 @@ +{# +/** + * Copyright (C) 2024 Xibo Signage Ltd + * + * Xibo - Digital Signage - https://xibosignage.com + * + * This file is part of Xibo. + * + * Xibo is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * any later version. + * + * Xibo is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Xibo. If not, see . + */ +#} +{% extends "form-base.twig" %} +{% import "forms.twig" as forms %} + +{% block formTitle %} + {% trans "Add Report Schedule" %} +{% endblock %} + +{% block formButtons %} + {% trans "Cancel" %}, XiboDialogClose() + {% trans "Save" %}, $("#reportScheduleAddForm").submit() +{% endblock %} + +{% block callBack %}displayAlertsReportScheduleFormOpen{% endblock %} + +{% block formHtml %} +
+
+
+ {{ forms.hidden("hiddenFields", hiddenFields) }} + {{ forms.hidden("reportName", reportName) }} + + {% set title %}{% trans "Name" %}{% endset %} + {% set helpText %}{% trans "The name for this report schedule" %}{% endset %} + {{ forms.input("name", title, "", helpText, "", "required") }} + + {% set title %}{% trans "Frequency" %}{% endset %} + {% set helpText %}{% trans "Select how frequently you would like this report to run" %}{% endset %} + {% set daily %}{% trans "Daily" %}{% endset %} + {% set weekly %}{% trans "Weekly" %}{% endset %} + {% set monthly %}{% trans "Monthly" %}{% endset %} + {% set yearly %}{% trans "Yearly" %}{% endset %} + {% set options = [ + { name: "daily", filter: daily }, + { name: "weekly", filter: weekly }, + { name: "monthly", filter: monthly }, + { name: "yearly", filter: yearly }, + ] %} + {{ forms.dropdown("filter", "single", title, "", options, "name", "filter", helpText) }} + + {% set title %}{% trans "Display" %}{% endset %} + {% set attributes = [ + { name: "data-width", value: "100%" }, + { name: "data-allow-clear", value: "true" }, + { name: "data-placeholder--id", value: null }, + { name: "data-placeholder--value", value: "" }, + { name: "data-search-url", value: url_for("display.search") }, + { name: "data-search-term", value: "display" }, + { name: "data-search-term-tags", value: "tags" }, + { name: "data-id-property", value: "displayId" }, + { name: "data-text-property", value: "display" } + ] %} + {{ forms.dropdown("displayId", "single", title, "", null, "displayId", "display", "", "pagedSelect", "", "d", "", attributes) }} + + {% set title %}{% trans "Display Group" %}{% endset %} + {% set attributes = [ + { name: "data-width", value: "100%" }, + { name: "data-allow-clear", value: "true" }, + { name: "data-placeholder--id", value: null }, + { name: "data-placeholder--value", value: "" }, + { name: "data-search-url", value: url_for("displayGroup.search") }, + { name: "data-search-term", value: "displayGroup" }, + { name: "data-id-property", value: "displayGroupId" }, + { name: "data-text-property", value: "displayGroup" } + ] %} + {{ forms.dropdown("displayGroupId[]", "dropdownmulti", title, "", null, "displayGroupId", "displayGroup", "", "pagedSelect", "", "d", "", attributes) }} + + {% set title %}{% trans "Start Time" %}{% endset %} + {% set helpText %}{% trans "Set a future date and time to run this report. Leave blank to run from the next collection point." %}{% endset %} + {{ forms.dateTime("fromDt", title, "", helpText, "starttime-control") }} + + {% set title %}{% trans "End Time" %}{% endset %} + {% set helpText %}{% trans "Set a future date and time to end the schedule. Leave blank to run indefinitely." %}{% endset %} + {{ forms.dateTime("toDt", title, "", helpText, "endtime-control") }} + + {% set title %}{% trans "Should an email be sent?" %}{% endset %} + {{ forms.checkbox("sendEmail", title, sendEmail) }} + + {% set title %}{% trans "Email addresses" %}{% endset %} + {% set helpText %}{% trans "Additional emails separated by a comma." %}{% endset %} + {{ forms.inputWithTags("nonusers", title, nonusers, helpText) }} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/reports/displayalerts.report b/reports/displayalerts.report new file mode 100644 index 0000000000..d5616ccd4f --- /dev/null +++ b/reports/displayalerts.report @@ -0,0 +1,14 @@ +{ + "name": "displayalerts", + "description": "Display Alerts", + "class": "\\Xibo\\Report\\DisplayAlerts", + "type": "Report", + "output_type": "table", + "color":"orange", + "fa_icon": "fa-bell", + "sort_order": 5, + "hidden": 0, + "category": "Display", + "feature": "displays.reporting", + "adminOnly": 0 +} \ No newline at end of file diff --git a/tests/Xmds/RegisterDisplayTest.php b/tests/Xmds/RegisterDisplayTest.php index 6757684dcd..d39bc1ed8e 100644 --- a/tests/Xmds/RegisterDisplayTest.php +++ b/tests/Xmds/RegisterDisplayTest.php @@ -1,6 +1,6 @@ loadXML($result); $this->assertSame('READY', $innerDocument->documentElement->getAttribute('code')); - $this->assertSame('Display is active and ready to start.', $innerDocument->documentElement->getAttribute('message')); + $this->assertSame( + 'Display is active and ready to start.', + $innerDocument->documentElement->getAttribute('message') + ); } public function testRegisterDisplayNoAuth() @@ -99,4 +102,33 @@ public function testRegisterDisplayNoAuth() } } } + + public function testRegisterNewDisplay() + { + $request = $this->sendRequest( + 'POST', + $this->register( + 'PHPUnitAddedTest' . mt_rand(1, 10), + 'phpunitaddedtest', + 'android' + ), + 7 + ); + + $response = $request->getBody()->getContents(); + + $document = new DOMDocument(); + $document->loadXML($response); + + $xpath = new DOMXpath($document); + $result = $xpath->evaluate('string(//ActivationMessage)'); + $innerDocument = new DOMDocument(); + $innerDocument->loadXML($result); + + $this->assertSame('ADDED', $innerDocument->documentElement->getAttribute('code')); + $this->assertSame( + 'Display is now Registered and awaiting Authorisation from an Administrator in the CMS', + $innerDocument->documentElement->getAttribute('message') + ); + } } diff --git a/tests/Xmds/Xibo/Tests/Xmds/SubmitLogTest.php b/tests/Xmds/Xibo/Tests/Xmds/SubmitLogTest.php new file mode 100644 index 0000000000..dc0543bcb8 --- /dev/null +++ b/tests/Xmds/Xibo/Tests/Xmds/SubmitLogTest.php @@ -0,0 +1,56 @@ +. + */ + +namespace Xibo\Tests\Xmds; + +use GuzzleHttp\Exception\GuzzleException; +use Xibo\Tests\xmdsTestCase; + +class SubmitLogTest extends XmdsTestCase +{ + use XmdsHelperTrait; + + public function setUp(): void + { + parent::setUp(); + } + + /** + * Submit log with category event + * @return void + * @throws GuzzleException + */ + public function testSubmitEventLog() + { + $request = $this->sendRequest( + 'POST', + $this->submitEventLog('7'), + 7 + ); + + $this->assertStringContainsString( + 'true', + $request->getBody()->getContents(), + 'Submit Log received incorrect response' + ); + } +} diff --git a/tests/Xmds/XmdsHelperTrait.php b/tests/Xmds/XmdsHelperTrait.php index 41ef1a0b38..b1113256da 100644 --- a/tests/Xmds/XmdsHelperTrait.php +++ b/tests/Xmds/XmdsHelperTrait.php @@ -1,6 +1,6 @@ 6v4RduQhaw5Q PHPUnit'.$version.' - [{date:"2023-04-20 17:03:52",expires:"2023-04-21 17:03:52",code:00001,reason:"Test",scheduleId:0,layoutId:0,regionId:0,mediaId:0,widgetId:0}] + [{"date":"2023-04-20 17:03:52","expires":"2023-04-21 17:03:52","code":"10001","reason":"Test","scheduleId":"0","layoutId":0,"regionId":"0","mediaId":"0","widgetId":"0"}] '; @@ -116,4 +116,21 @@ public function getWidgetData($version, $widgetId) '; } + + public function submitEventLog($version): string + { + return ' + + + 6v4RduQhaw5Q + PHPUnit'. $version .' + <log><event date="2024-04-10 12:45:55" category="event"><eventType>App Start</eventType><message>Detailed message about this event</message><alertType>both</alertType><refId></refId></event></log> + + + '; + } } diff --git a/views/command-form-add.twig b/views/command-form-add.twig index d0807db010..c52996bb73 100644 --- a/views/command-form-add.twig +++ b/views/command-form-add.twig @@ -72,6 +72,17 @@ {% set helpText %}{% trans "Leave empty if this command should be available on all types of Display." %}{% endset %} {{ forms.dropdown("availableOn[]", "dropdownmulti", title, "", options, "optionid", "option", helpText, "selectPicker") }} + + {% set options = [ + { optionid: "never", option: "Never" }, + { optionid: "success", option: "Success" }, + { optionid: "failure", option: "Failure" }, + { optionid: "always", option: "Always" }, + ] %} + {% set title %}{% trans "Create Alert On" %}{% endset %} + {% set helpText %}{% trans "On command execution, when should a Display alert be created?" %}{% endset %} + + {{ forms.dropdown("createAlertOn", "single", title, "never", options, "optionid", "option", helpText) }}
diff --git a/views/command-form-edit.twig b/views/command-form-edit.twig index dcf0efe7d9..3d1fd87067 100644 --- a/views/command-form-edit.twig +++ b/views/command-form-edit.twig @@ -73,6 +73,17 @@ {% set helpText %}{% trans "Leave empty if this command should be available on all types of Display." %}{% endset %} {{ forms.dropdown("availableOn[]", "dropdownmulti", title, command.getAvailableOn(), options, "optionid", "option", helpText, "selectPicker") }} + + {% set options = [ + { optionid: "never", option: "Never" }, + { optionid: "success", option: "Success" }, + { optionid: "failure", option: "Failure" }, + { optionid: "always", option: "Always" }, + ] %} + {% set title %}{% trans "Create Alert On" %}{% endset %} + {% set helpText %}{% trans "On command execution, when should a Display alert be created?" %}{% endset %} + + {{ forms.dropdown("createAlertOn", "single", title, command.createAlertOn, options, "optionid", "option", helpText) }}
diff --git a/views/displayprofile-form-edit-command-fields.twig b/views/displayprofile-form-edit-command-fields.twig index 8339772ffc..f04b13bad3 100644 --- a/views/displayprofile-form-edit-command-fields.twig +++ b/views/displayprofile-form-edit-command-fields.twig @@ -56,6 +56,23 @@ {% set title %}{% trans "Validation" %}{% endset %} {% set helpText %}{% trans "The Validation String for this Command on this display" %}{% endset %} {{ forms.input(fieldId, title, field.validationStringDisplayProfile, helpText) }} + + {% if field.createAlertOn != "" %} + {{ forms.disabled("", "", "This Command has a default setting for creating alerts."|trans, field.createAlertOn) }} + {% endif %} + + {% set fieldId = "createAlertOn_" ~ field.commandId %} + {% set options = [ + { optionid: "", option: "" }, + { optionid: "never", option: "Never" }, + { optionid: "success", option: "Success" }, + { optionid: "failure", option: "Failure" }, + { optionid: "always", option: "Always" }, + ] %} + {% set title %}{% trans "Create Alert On" %}{% endset %} + {% set helpText %}{% trans "On command execution, when should a Display alert be created?" %}{% endset %} + + {{ forms.dropdown(fieldId, "single", title, field.createAlertOnDisplayProfile, options, "optionid", "option", helpText) }}
diff --git a/web/xmds.php b/web/xmds.php index f3b9a75d83..541d551081 100755 --- a/web/xmds.php +++ b/web/xmds.php @@ -1,6 +1,6 @@ get('playerVersionFactory'), $container->get('dispatcher'), $container->get('campaignFactory'), - $container->get('syncGroupFactory') + $container->get('syncGroupFactory'), + $container->get('playerFaultFactory') ); // Add manual raw post data parsing, as HTTP_RAW_POST_DATA is deprecated.