From 8984f88de35623d9bf2fa8a1dfbf0fac6cf085d3 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 30 Jul 2020 16:30:19 +0100 Subject: [PATCH 01/10] Move where we load schedule exclusions to improve performance and reduce SQL SELECTs xibosignage/xibo#2302 --- lib/Entity/Schedule.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/Entity/Schedule.php b/lib/Entity/Schedule.php index 89fde35b62..ae46af9357 100644 --- a/lib/Entity/Schedule.php +++ b/lib/Entity/Schedule.php @@ -808,6 +808,9 @@ public function getEvents($fromDt, $toDt) $this->getLog()->debug('The main event has a start and end date within the month, no need to pull it in from the prior month. [eventId:' . $this->eventId . ']'); } + // Keep a cache of schedule exclusions, so we look them up by eventId only one time per event + $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]);; + // Request month cache while ($fromDt < $toDt) { @@ -823,8 +826,6 @@ public function getEvents($fromDt, $toDt) foreach ($this->scheduleEvents as $scheduleEvent) { // Find the excluded recurring events - $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]); - $exclude = false; foreach ($scheduleExclusions as $exclusion) { if ($scheduleEvent->fromDt == $exclusion->fromDt && @@ -857,6 +858,9 @@ public function getEvents($fromDt, $toDt) $fromDt->addMonth(); } + // Clear our cache of schedule exclusions + $scheduleExclusions = null; + $this->getLog()->debug('Filtered ' . count($this->scheduleEvents) . ' to ' . count($events) . ', events: ' . json_encode($events, JSON_PRETTY_PRINT)); return $events; From 18b4d8b50a9c71d45987393860994794873df735 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 30 Jul 2020 16:30:48 +0100 Subject: [PATCH 02/10] Tidy trailing ; xibosignage/xibo#2302 --- lib/Entity/Schedule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Entity/Schedule.php b/lib/Entity/Schedule.php index ae46af9357..cf35c0fd9c 100644 --- a/lib/Entity/Schedule.php +++ b/lib/Entity/Schedule.php @@ -809,7 +809,7 @@ public function getEvents($fromDt, $toDt) } // Keep a cache of schedule exclusions, so we look them up by eventId only one time per event - $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]);; + $scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $this->eventId]); // Request month cache while ($fromDt < $toDt) { From 6375d709d1ae3019cbfc236a6948cadfc55cd569 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 10 Aug 2020 13:16:38 +0100 Subject: [PATCH 03/10] Add a welcome page and refactor "home" route to pass through the welcome page if user not seen it. --- cypress/integration/dashboard_spec.js | 5 +-- lib/Controller/User.php | 50 +++++++++++++++++++++++++++ lib/routes-web.php | 32 ++--------------- views/authed-user-menu.twig | 2 +- views/user-welcome.twig | 8 ----- views/welcome-page.twig | 33 ++++++++++++++++++ 6 files changed, 89 insertions(+), 41 deletions(-) create mode 100644 views/welcome-page.twig diff --git a/cypress/integration/dashboard_spec.js b/cypress/integration/dashboard_spec.js index 8637dbb071..e4e6d983bf 100644 --- a/cypress/integration/dashboard_spec.js +++ b/cypress/integration/dashboard_spec.js @@ -18,7 +18,8 @@ describe('Dashboard', function() { cy.contains('Latest News'); }); - it('should show the welcome tutorial, and then disable it', function() { + // TODO: replace + /*it('should show the welcome tutorial, and then disable it', function() { cy.server(); cy.route('POST', '/user/welcome').as('showTour'); cy.route('PUT', '/user/welcome').as('disableTour'); @@ -43,5 +44,5 @@ describe('Dashboard', function() { cy.wait(500); cy.get('.popover.tour').should('not.be.visible'); }); - }); + });*/ }); \ No newline at end of file diff --git a/lib/Controller/User.php b/lib/Controller/User.php index 7fd7416b39..d15dcc4b2e 100644 --- a/lib/Controller/User.php +++ b/lib/Controller/User.php @@ -195,6 +195,56 @@ public function __construct($log, $sanitizerService, $state, $user, $help, $conf $this->dataSetFactory = $dataSetFactory; } + /** + * Home Page + * this redirects to the appropriate page for this user. + * @param Request $request + * @param Response $response + * @return \Psr\Http\Message\ResponseInterface|Response + * @throws \Xibo\Support\Exception\GeneralException + */ + public function home(Request $request, Response $response) + { + // Should we show this user the welcome page? + if ($this->getUser()->newUserWizard == 0) { + return $response->withRedirect($this->urlFor($request, 'welcome.view')); + } + + // User wizard seen, go to home page + $this->getLog()->debug('Showing the homepage: ' . $this->getUser()->homePageId); + + $page = $this->pageFactory->getById($this->getUser()->homePageId); + + // Check to see if this user has permission for this page, and if not show a meaningful error telling them what + // has happened. + if (!$this->getUser()->checkViewable($page)) { + throw new \Xibo\Support\Exception\AccessDeniedException(__('You do not have permission for your homepage, please contact your administrator')); + } + + return $response->withRedirect($this->urlFor($request, $page->getName() . '.view')); + } + + /** + * Welcome Page + * @param Request $request + * @param Response $response + * @return \Psr\Http\Message\ResponseInterface|Response + * @throws GeneralException + * @throws \Xibo\Support\Exception\ControllerNotImplemented + */ + public function welcome(Request $request, Response $response) + { + $this->getState()->template = 'welcome-page'; + + // Mark the page as seen + if ($this->getUser()->newUserWizard == 0) { + $this->getUser()->newUserWizard = 1; + $this->getUser()->save(['validate' => false]); + } + + return $this->render($request, $response); + } + /** * Controls which pages are to be displayed * @param Request $request diff --git a/lib/routes-web.php b/lib/routes-web.php index 741f800216..56ea23b57f 100644 --- a/lib/routes-web.php +++ b/lib/routes-web.php @@ -20,37 +20,9 @@ * along with Xibo. If not, see . */ -use Slim\Http\Response as Response; -use Slim\Http\ServerRequest as Request; - // Special "root" route -$app->get('/', function (Request $request, Response $response) use ($app) { - - // Different controller depending on the homepage of the user. - $app->getContainer()->get('configService')->setDependencies( - $app->getContainer()->get('store'), - $app->getContainer()->get('rootUri') - ); - - $routeParser = $app->getRouteCollector()->getRouteParser(); - - /* @var \Xibo\Entity\User $user */ - $user = $app->getContainer()->get('user'); - - $app->getContainer()->get('logger')->debug('Showing the homepage: ' . $user->homePageId); - - /** @var \Xibo\Entity\Page $page */ - $page = $app->getContainer()->get('pageFactory')->getById($user->homePageId); - - // Check to see if this user has permission for this page, and if not show a meaningful error telling them what - // has happened. - if (!$user->checkViewable($page)) { - throw new \Xibo\Support\Exception\AccessDeniedException(__('You do not have permission for your homepage, please contact your administrator')); - } - - return $response->withRedirect($routeParser->urlFor($page->getName() . '.view')); - -})->setName('home'); +$app->get('/', ['\Xibo\Controller\User', 'home'])->setName('home'); +$app->get('/welcome', ['\Xibo\Controller\User', 'welcome'])->setName('welcome.view'); // Dashboards $app->get('/statusdashboard', ['\Xibo\Controller\StatusDashboard','displayPage'])->setName('statusdashboard.view'); diff --git a/views/authed-user-menu.twig b/views/authed-user-menu.twig index f9ac798723..db79b71eaf 100644 --- a/views/authed-user-menu.twig +++ b/views/authed-user-menu.twig @@ -9,7 +9,7 @@
  • {% trans "Preferences" %}
  • {% trans "Edit Profile" %}
  • -
  • {% trans "Reshow welcome" %}
  • +
  • {% trans "Reshow welcome" %}
  • {% if horizontalNav %}
  • {% trans "About" %}
  • {% endif %} diff --git a/views/user-welcome.twig b/views/user-welcome.twig index 65467f6bf2..157927d9aa 100644 --- a/views/user-welcome.twig +++ b/views/user-welcome.twig @@ -616,14 +616,6 @@ } ] }); - - if (hasSeenNewUserWizard === 0) { - restartTour(tour); - } - - $("#reshowWelcomeMenuItem").on("click", function() { - restartTour(tour, true, true); - }); }); function tourStepBroken(tour, stepNumber) { diff --git a/views/welcome-page.twig b/views/welcome-page.twig new file mode 100644 index 0000000000..7323a42463 --- /dev/null +++ b/views/welcome-page.twig @@ -0,0 +1,33 @@ +{# +/** + * 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 title %}{{ "Welcome"|trans }} | {% endblock %} + +{% block pageContent %} +

    WIP: Welcome

    +{% endblock %} + +{% block javaScript %} +{% endblock %} \ No newline at end of file From e41db8f30cc3c97eb3e1bbb25c321f8227e79f7e Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Mon, 10 Aug 2020 13:19:58 +0100 Subject: [PATCH 04/10] Make the welcome page always accessible. --- lib/Entity/User.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/Entity/User.php b/lib/Entity/User.php index 2e306af7e1..f0ab153cc7 100644 --- a/lib/Entity/User.php +++ b/lib/Entity/User.php @@ -1202,9 +1202,10 @@ public function routeViewable($route) if ($this->isSuperAdmin()) return true; - // All users have access to the logout page - if ($route === '/logout') + // All users have access to the logout page and welcome page + if ($route === '/logout' || $route === '/welcome') { return true; + } try { if ($this->pagePermissionCache == null) { From 6dcd229593d7694a5c8c982792dd83863d8fc947 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Tue, 18 Aug 2020 16:16:03 +0100 Subject: [PATCH 05/10] Video previewing issue, potential fix. --- lib/Controller/Module.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Controller/Module.php b/lib/Controller/Module.php index 6168dbb46c..7a000b7a10 100644 --- a/lib/Controller/Module.php +++ b/lib/Controller/Module.php @@ -1327,18 +1327,18 @@ public function getResource(Request $request, Response $response, $regionId, $id if ($module->getModule()->regionSpecific == 0) { // Non region specific module - no caching required as this is only ever called via preview. - $resource = $module->download($request, $response); + $response = $module->download($request, $response); } else { // Region-specific module, need to handle caching and locking. $resource = $module->getResourceOrCache(); + + if (!empty($resource)) { + $response->getBody()->write($resource); + } } $this->setNoOutput(true); - if (!empty($resource)) { - $response->getBody()->write($resource); - } - return $this->render($request, $response); } From 84a762caf967fd9072f55828e28914386027ed72 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 2 Sep 2020 17:08:32 +0100 Subject: [PATCH 06/10] Add display status window as transient data (to cache). Add an API to get it out again. xibosignage/xibo#2211 --- lib/Controller/Display.php | 43 +++ lib/Entity/Display.php | 34 ++ lib/Xmds/Soap4.php | 8 + lib/Xmds/Soap5.php | 4 + lib/routes.php | 1 + web/swagger.json | 673 ++++++++++++++++++++++++++++++++++++- 6 files changed, 747 insertions(+), 16 deletions(-) diff --git a/lib/Controller/Display.php b/lib/Controller/Display.php index 9b4b3de90b..368cb1d48d 100644 --- a/lib/Controller/Display.php +++ b/lib/Controller/Display.php @@ -2225,4 +2225,47 @@ public function checkLicence(Request $request, Response $response, $id) return $this->render($request, $response); } + + /** + * @SWG\Get( + * path="/display/status/{id}", + * operationId="displayStatus", + * tags={"display"}, + * summary="Display Status", + * description="Get the display status window for this Display.", + * @SWG\Parameter( + * name="id", + * in="path", + * description="Display Id", + * type="integer", + * required=true + * ), + * @SWG\Response( + * response=200, + * description="successful operation", + * @SWG\Schema( + * type="array", + * @SWG\Items(type="string") + * ) + * ) + * ) + * + * @param Request $request + * @param Response $response + * @param int $id displayId + * @return \Psr\Http\Message\ResponseInterface|Response + * @throws \Xibo\Support\Exception\AccessDeniedException + * @throws \Xibo\Support\Exception\InvalidArgumentException + * @throws \Xibo\Support\Exception\NotFoundException + */ + public function statusWindow(Request $request, Response $response, $id) + { + $display = $this->displayFactory->getById($id); + + if (!$this->getUser()->checkViewable($display)) { + throw new AccessDeniedException(); + } + + return $response->withJson($display->getStatusWindow($this->pool)); + } } diff --git a/lib/Entity/Display.php b/lib/Entity/Display.php index 84a6d6dffe..de73d75c4d 100644 --- a/lib/Entity/Display.php +++ b/lib/Entity/Display.php @@ -1109,4 +1109,38 @@ public function setCurrentScreenShotTime($pool, $date) return $this; } + + /** + * @param PoolInterface $pool + * @return array + */ + public function getStatusWindow($pool) + { + $item = $pool->getItem('/statusWindow/' . $this->displayId); + + if ($item->isMiss()) { + return []; + } else { + return $item->get(); + } + } + + /** + * @param PoolInterface $pool + * @param array $status + * @return $this + */ + public function setStatusWindow($pool, $status) + { + // Cache it + $this->getLog()->debug('Caching statusWindow with Pool'); + + $item = $pool->getItem('/statusWindow/' . $this->displayId); + $item->set($status); + $item->expiresAfter(new \DateInterval('P1D')); + + $pool->saveDeferred($item); + + return $this; + } } diff --git a/lib/Xmds/Soap4.php b/lib/Xmds/Soap4.php index 56a8cf9727..9d7f5d2a8a 100644 --- a/lib/Xmds/Soap4.php +++ b/lib/Xmds/Soap4.php @@ -591,7 +591,15 @@ public function NotifyStatus($serverKey, $hardwareKey, $status) $statusDialog = $sanitizedStatus->getString('statusDialog', ['default' => null]); if ($statusDialog !== null) { + // Log in as an alert $this->getLog()->alert($statusDialog); + + // Cache on the display as transient data + try { + $this->display->setStatusWindow($this->getPool(), json_decode($statusDialog, true)); + } catch (\Exception $exception) { + $this->getLog()->error('Unable to cache display status. e = ' . $exception->getMessage()); + } } // Resolution diff --git a/lib/Xmds/Soap5.php b/lib/Xmds/Soap5.php index 11870a619e..295534ae2a 100644 --- a/lib/Xmds/Soap5.php +++ b/lib/Xmds/Soap5.php @@ -34,6 +34,10 @@ use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\NotFoundException; +/** + * Class Soap5 + * @package Xibo\Xmds + */ class Soap5 extends Soap4 { /** diff --git a/lib/routes.php b/lib/routes.php index f5de753b7f..b0ef4de81c 100644 --- a/lib/routes.php +++ b/lib/routes.php @@ -268,6 +268,7 @@ $app->put('/display/requestscreenshot/{id}', ['\Xibo\Controller\Display','requestScreenShot'])->setName('display.requestscreenshot'); $app->put('/display/licenceCheck/{id}', ['\Xibo\Controller\Display','checkLicence'])->setName('display.licencecheck'); $app->get('/display/screenshot/{id}', ['\Xibo\Controller\Display','screenShot'])->setName('display.screenShot'); +$app->get('/display/status/{id}', ['\Xibo\Controller\Display','statusWindow'])->setName('display.statusWindow'); $app->post('/display/{id}/displaygroup/assign', ['\Xibo\Controller\Display','assignDisplayGroup'])->setName('display.assign.displayGroup'); $app->put('/display/{id}/moveCms', ['\Xibo\Controller\Display','moveCms'])->setName('display.moveCms'); $app->post('/display/addViaCode', ['\Xibo\Controller\Display','addViaCode'])->setName('display.addViaCode'); diff --git a/web/swagger.json b/web/swagger.json index 15b27647c1..c6e4bbdff2 100644 --- a/web/swagger.json +++ b/web/swagger.json @@ -21,6 +21,205 @@ "application/json" ], "paths": { + "/action": { + "get": { + "tags": [ + "action" + ], + "summary": "Search Actions", + "description": "Search all Actions this user has access to", + "operationId": "actionSearch", + "parameters": [ + { + "name": "actionId", + "in": "query", + "description": "Filter by Action Id", + "required": false, + "type": "integer" + }, + { + "name": "ownerId", + "in": "query", + "description": "Filter by Owner Id", + "required": false, + "type": "integer" + }, + { + "name": "triggerType", + "in": "query", + "description": "Filter by Action trigger type", + "required": false, + "type": "string" + }, + { + "name": "triggerCode", + "in": "query", + "description": "Filter by Action trigger code", + "required": false, + "type": "string" + }, + { + "name": "actionType", + "in": "query", + "description": "Filter by Action type", + "required": false, + "type": "string" + }, + { + "name": "source", + "in": "query", + "description": "Filter by Action source", + "required": false, + "type": "string" + }, + { + "name": "sourceId", + "in": "query", + "description": "Filter by Action source Id", + "required": false, + "type": "integer" + }, + { + "name": "target", + "in": "query", + "description": "Filter by Action target", + "required": false, + "type": "string" + }, + { + "name": "targetId", + "in": "query", + "description": "Filter by Action target Id", + "required": false, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Action" + } + } + } + } + } + }, + "/action/{source}/{sourceId}": { + "post": { + "tags": [ + "action" + ], + "summary": "Add Action", + "description": "Add a new Action", + "operationId": "actionAdd", + "parameters": [ + { + "name": "source", + "in": "path", + "description": "Source for this action layout, region or widget", + "required": true, + "type": "string" + }, + { + "name": "sourceId", + "in": "path", + "description": "The id of the source object, layoutId, regionId or widgetId", + "required": true, + "type": "integer" + }, + { + "name": "triggerType", + "in": "formData", + "description": "Action trigger type, touch or webhook", + "required": true, + "type": "string" + }, + { + "name": "triggerCode", + "in": "formData", + "description": "Action trigger code", + "required": true, + "type": "string" + }, + { + "name": "actionType", + "in": "formData", + "description": "Action type, next, previous, navLayout, navWidget", + "required": true, + "type": "string" + }, + { + "name": "target", + "in": "formData", + "description": "Target for this action, screen or region", + "required": true, + "type": "string" + }, + { + "name": "targetId", + "in": "formData", + "description": "The id of the target for this action - regionId if the target is set to region", + "required": false, + "type": "string" + }, + { + "name": "widgetId", + "in": "formData", + "description": "For navWidget actionType, the WidgetId to navigate to", + "required": false, + "type": "integer" + }, + { + "name": "layoutCode", + "in": "formData", + "description": "For navLayout, the Layout Code identifier to navigate to", + "required": false, + "type": "string" + } + ], + "responses": { + "201": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Action" + }, + "headers": { + "Location": { + "description": "Location of the new record", + "type": "string" + } + } + } + } + } + }, + "/action/{actionId}": { + "delete": { + "tags": [ + "action" + ], + "summary": "Delete Action", + "description": "Delete an existing Action", + "operationId": "actionDelete", + "parameters": [ + { + "name": "actionId", + "in": "path", + "description": "The Action ID to Delete", + "required": true, + "type": "integer" + } + ], + "responses": { + "204": { + "description": "successful operation" + } + } + } + }, "/campaign": { "get": { "tags": [ @@ -324,6 +523,27 @@ "description": "A unique code for this command", "required": true, "type": "string" + }, + { + "name": "commandString", + "in": "formData", + "description": "The Command String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "validationString", + "in": "formData", + "description": "The Validation String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "availableOn", + "in": "formData", + "description": "An array of Player types this Command is available on, empty for all.", + "required": false, + "type": "string" } ], "responses": { @@ -371,6 +591,27 @@ "description": "A description for the command", "required": false, "type": "string" + }, + { + "name": "commandString", + "in": "formData", + "description": "The Command String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "validationString", + "in": "formData", + "description": "The Validation String for this Command. Can be overridden on Display Settings.", + "required": false, + "type": "string" + }, + { + "name": "availableOn", + "in": "formData", + "description": "An array of Player types this Command is available on, empty for all.", + "required": false, + "type": "string" } ], "responses": { @@ -581,6 +822,34 @@ "description": "Which field should be used to summarize", "required": false, "type": "string" + }, + { + "name": "sourceId", + "in": "formData", + "description": "For remote DataSet, what type data is it? 1 - json, 2 - csv", + "required": false, + "type": "integer" + }, + { + "name": "ignoreFirstRow", + "in": "formData", + "description": "For remote DataSet with sourceId 2 (CSV), should we ignore first row?", + "required": false, + "type": "integer" + }, + { + "name": "rowLimit", + "in": "formData", + "description": "For remote DataSet, maximum number of rows this DataSet can hold, if left empty the CMS Setting for DataSet row limit will be used.", + "required": false, + "type": "integer" + }, + { + "name": "limitPolicy", + "in": "formData", + "description": "For remote DataSet, what should happen when the DataSet row limit is reached? stop, fifo or truncate", + "required": false, + "type": "string" } ], "responses": { @@ -733,6 +1002,34 @@ "description": "Which field should be used to summarize", "required": false, "type": "string" + }, + { + "name": "sourceId", + "in": "formData", + "description": "For remote DataSet, what type data is it? 1 - json, 2 - csv", + "required": false, + "type": "integer" + }, + { + "name": "ignoreFirstRow", + "in": "formData", + "description": "For remote DataSet with sourceId 2 (CSV), should we ignore first row?", + "required": false, + "type": "integer" + }, + { + "name": "rowLimit", + "in": "formData", + "description": "For remote DataSet, maximum number of rows this DataSet can hold, if left empty the CMS Setting for DataSet row limit will be used.", + "required": false, + "type": "integer" + }, + { + "name": "limitPolicy", + "in": "formData", + "description": "For remote DataSet, what should happen when the DataSet row limit is reached? stop, fifo or truncate", + "required": false, + "type": "string" } ], "responses": { @@ -2011,6 +2308,20 @@ "description": "Clear the cached XMR configuration and send a rekey", "required": false, "type": "integer" + }, + { + "name": "teamViewerSerial", + "in": "formData", + "description": "The TeamViewer serial number for this Display, if applicable", + "required": false, + "type": "string" + }, + { + "name": "webkeySerial", + "in": "formData", + "description": "The Webkey serial number for this Display, if applicable", + "required": false, + "type": "string" } ], "responses": { @@ -2175,6 +2486,36 @@ } } }, + "/display/status/{id}": { + "get": { + "tags": [ + "display" + ], + "summary": "Display Status", + "description": "Get the display status window for this Display.", + "operationId": "displayStatus", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "Display Id", + "required": true, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, "/displaygroup": { "get": { "tags": [ @@ -3333,6 +3674,20 @@ "description": "If a Template is not provided, provide the resolutionId for this Layout.", "required": false, "type": "integer" + }, + { + "name": "returnDraft", + "in": "formData", + "description": "Should we return the Draft Layout or the Published Layout on Success?", + "required": false, + "type": "boolean" + }, + { + "name": "code", + "in": "formData", + "description": "Code identifier for this Layout", + "required": false, + "type": "string" } ], "responses": { @@ -3400,6 +3755,13 @@ "description": "Flag indicating whether the Layout stat is enabled", "required": false, "type": "integer" + }, + { + "name": "code", + "in": "formData", + "description": "Code identifier for this Layout", + "required": false, + "type": "string" } ], "responses": { @@ -5671,6 +6033,39 @@ } } }, + "/region/drawer/{id}": { + "post": { + "tags": [ + "layout" + ], + "summary": "Add drawer Region", + "description": "Add a drawer Region to a Layout", + "operationId": "regionDrawerAdd", + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The Layout ID to add the Region to", + "required": true, + "type": "integer" + } + ], + "responses": { + "201": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Region" + }, + "headers": { + "Location": { + "description": "Location of the new record", + "type": "string" + } + } + } + } + } + }, "/resolution": { "get": { "tags": [ @@ -6302,7 +6697,7 @@ { "name": "type", "in": "query", - "description": "The type of stat to return. Layout|Media|Widget or All", + "description": "The type of stat to return. Layout|Media|Widget", "required": false, "type": "string" }, @@ -6341,6 +6736,16 @@ "required": false, "type": "integer" }, + { + "name": "displayIds", + "in": "query", + "description": "An optional array of display Id to filter", + "required": false, + "type": "array", + "items": { + "type": "integer" + } + }, { "name": "layoutId", "in": "query", @@ -6367,6 +6772,27 @@ "description": "An optional Campaign Id to filter", "required": false, "type": "integer" + }, + { + "name": "returnDisplayLocalTime", + "in": "query", + "description": "true/1/On if the results should be in display local time, otherwise CMS time", + "required": false, + "type": "boolean" + }, + { + "name": "returnDateFormat", + "in": "query", + "description": "A PHP formatted date format for how the dates in this call should be returned.", + "required": false, + "type": "string" + }, + { + "name": "embed", + "in": "query", + "description": "Should the return embed additional data, options are layoutTags,displayTags and mediaTags", + "required": false, + "type": "string" } ], "responses": { @@ -6382,6 +6808,43 @@ } } }, + "/stats/getExportStatsCount": { + "get": { + "tags": [ + "statistics" + ], + "summary": "Total count of stats", + "operationId": "getExportStatsCount", + "parameters": [ + { + "name": "fromDt", + "in": "query", + "description": "The start date for the filter. Default = 24 hours ago", + "required": false, + "type": "string" + }, + { + "name": "toDt", + "in": "query", + "description": "The end date for the filter. Default = now.", + "required": false, + "type": "string" + }, + { + "name": "displayId", + "in": "query", + "description": "An optional display Id to filter", + "required": false, + "type": "integer" + } + ], + "responses": { + "200": { + "description": "successful operation" + } + } + } + }, "/stats/timeDisconnected": { "get": { "tags": [ @@ -9354,7 +9817,7 @@ { "name": "widgetId", "in": "path", - "description": "The Widget ID", + "description": "The WidgetId ID", "required": true, "type": "integer" }, @@ -9694,6 +10157,20 @@ "required": false, "type": "string" }, + { + "name": "globalCommand", + "in": "formData", + "description": "Enter a global command line compatible with multiple devices", + "required": false, + "type": "string" + }, + { + "name": "androidCommand", + "in": "formData", + "description": "Enter a Android command line compatible command", + "required": false, + "type": "string" + }, { "name": "windowsCommand", "in": "formData", @@ -9704,7 +10181,7 @@ { "name": "linuxCommand", "in": "formData", - "description": "Enter a Android / Linux command line compatible command", + "description": "Enter a Linux command line compatible command", "required": false, "type": "string" }, @@ -11172,18 +11649,15 @@ "widgetId": { "type": "integer" }, + "scheduleId": { + "type": "integer" + }, "numberPlays": { "type": "integer" }, "duration": { "type": "integer" }, - "minStart": { - "type": "string" - }, - "maxEnd": { - "type": "string" - }, "start": { "type": "string" }, @@ -11220,6 +11694,54 @@ } } }, + "Action": { + "properties": { + "actionId": { + "description": "The Action Id", + "type": "integer" + }, + "ownerId": { + "description": "The Owner Id", + "type": "integer" + }, + "triggerType": { + "description": "The Action trigger type", + "type": "string" + }, + "triggerCode": { + "description": "The Action trigger code", + "type": "string" + }, + "actionType": { + "description": "The Action type", + "type": "string" + }, + "source": { + "description": "The Action source (layout, region or widget)", + "type": "string" + }, + "sourceId": { + "description": "The Action source Id (layoutId, regionId or widgetId)", + "type": "integer" + }, + "target": { + "description": "The Action target (region)", + "type": "string" + }, + "targetId": { + "description": "The Action target Id (regionId)", + "type": "integer" + }, + "widgetId": { + "description": "Widget ID that will be loaded as a result of navigate to Widget Action type", + "type": "integer" + }, + "layoutCode": { + "description": "Layout Code identifier", + "type": "string" + } + } + }, "Application": { "properties": { "key": { @@ -11253,6 +11775,10 @@ "clientCredentials": { "description": "Flag indicating whether to allow the clientCredentials Grant Type", "type": "integer" + }, + "isConfidential": { + "description": "Flag indicating whether this Application will be confidential or not (can it keep a secret?)", + "type": "integer" } } }, @@ -11343,11 +11869,27 @@ "type": "integer" }, "commandString": { - "description": "Command String - when child of a Display Profile", + "description": "Command String", "type": "string" }, "validationString": { - "description": "Validation String - when child of a Display Profile", + "description": "Validation String", + "type": "string" + }, + "displayProfileId": { + "description": "DisplayProfileId if specific to a Display Profile", + "type": "integer" + }, + "commandStringDisplayProfile": { + "description": "Command String specific to the provided DisplayProfile", + "type": "string" + }, + "validationStringDisplayProfile": { + "description": "Validation String specific to the provided DisplayProfile", + "type": "string" + }, + "availableOn": { + "description": "A comma separated list of player types this command is available on", "type": "string" }, "groupsWithPermissions": { @@ -11465,6 +12007,14 @@ "ignoreFirstRow": { "description": "A flag whether to ignore the first row, for CSV source remote dataSet", "type": "integer" + }, + "rowLimit": { + "description": "Soft limit on number of rows per DataSet, if left empty the global DataSet row limit will be used.", + "type": "integer" + }, + "limitPolicy": { + "description": "Type of action that should be taken on next remote DataSet sync - stop, fifo or truncate", + "type": "string" } } }, @@ -11773,6 +12323,14 @@ "commercialLicence": { "description": "Status of the commercial licence for this Display. 0 - Not licensed, 1 - licensed, 2 - trial licence, 3 - not applicable", "type": "integer" + }, + "teamViewerSerial": { + "description": "The TeamViewer serial number for this Display", + "type": "string" + }, + "webkeySerial": { + "description": "The Webkey serial number for this Display", + "type": "string" } } }, @@ -11981,6 +12539,10 @@ "description": "Flag indicating whether the default transitions should be applied to this Layout", "type": "integer" }, + "code": { + "description": "Code identifier for this Layout", + "type": "string" + }, "regions": { "description": "An array of Regions belonging to this Layout", "type": "array", @@ -12188,10 +12750,7 @@ }, "settings": { "description": "An array of additional module specific settings", - "type": "array", - "items": { - "type": "string" - } + "type": "array" }, "schemaVersion": { "description": "The schema version of the module", @@ -12522,6 +13081,10 @@ "description": "A read-only estimate of this Regions's total duration in seconds. This is valid when the parent layout status is 1 or 2.", "type": "integer" }, + "isDrawer": { + "description": "Flag, whether this region is used as an interactive drawer attached to a layout.", + "type": "integer" + }, "regionPlaylist": { "description": "This Regions Playlist - null if getPlaylist() has not been called.", "$ref": "#/definitions/Playlist" @@ -12645,6 +13208,10 @@ "storedAs": { "description": "Stored As", "type": "string" + }, + "schemaVersion": { + "description": "Schema Version", + "type": "integer" } } }, @@ -12760,7 +13327,7 @@ "type": "integer" }, "shareOfVoice": { - "description": "Percentage (0-100) of each full hour that is scheduled that this Layout should occupy", + "description": "Seconds (0-3600) of each full hour that is scheduled that this Layout should occupy", "type": "integer" }, "isGeoAware": { @@ -13333,7 +13900,77 @@ } } }, + "parameters": { + "actionId": { + "name": "actionId", + "in": "path", + "description": "Action ID to edit", + "required": true, + "type": "integer" + }, + "triggerType": { + "name": "triggerType", + "in": "formData", + "description": "Action trigger type, touch, webhook", + "required": true, + "type": "string" + }, + "triggerCode": { + "name": "triggerCode", + "in": "formData", + "description": "Action trigger code", + "required": false, + "type": "string" + }, + "actionType": { + "name": "actionType", + "in": "formData", + "description": "Action type, next, previous, navLayout, navWidget", + "required": true, + "type": "string" + }, + "target": { + "name": "target", + "in": "formData", + "description": "Target for this action, screen or region", + "required": true, + "type": "string" + }, + "targetId": { + "name": "targetId", + "in": "formData", + "description": "The id of the target for this action, regionId if target set to region", + "required": false, + "type": "integer" + }, + "widgetId": { + "name": "widgetId", + "in": "formData", + "description": "For navWidget actionType, the WidgetId to navigate to", + "required": false, + "type": "integer" + }, + "layoutCode": { + "name": "layoutCode", + "in": "formData", + "description": "For navLayout, the Layout Code identifier to navigate to", + "required": false, + "type": "string" + } + }, "responses": { + "201": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Action" + }, + "headers": { + "Location": { + "description": "Location of the new record", + "type": "string" + } + } + }, "200": { "description": "successful operation" } @@ -13457,6 +14094,10 @@ { "name": "tags", "description": "Tags" + }, + { + "name": "actions", + "description": "Actions" } ], "externalDocs": { From e8ba9b67a3b5f8e4e76647969bc8624cd2ad082d Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 2 Sep 2020 17:16:47 +0100 Subject: [PATCH 07/10] Fix for some swagger doc parsing errors. --- lib/Entity/Module.php | 2 +- lib/Widget/Weather.php | 8 +-- web/swagger.json | 158 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 162 insertions(+), 6 deletions(-) diff --git a/lib/Entity/Module.php b/lib/Entity/Module.php index 7d75e39b82..e10759da8f 100644 --- a/lib/Entity/Module.php +++ b/lib/Entity/Module.php @@ -105,7 +105,7 @@ class Module implements \JsonSerializable public $defaultDuration; /** - * @SWG\Property(description="An array of additional module specific settings") + * @SWG\Property(description="An array of additional module specific settings", type="array", @SWG\Items(type="string")) * @var array */ public $settings = []; diff --git a/lib/Widget/Weather.php b/lib/Widget/Weather.php index 6f4d52e4c5..7966aca3d7 100644 --- a/lib/Widget/Weather.php +++ b/lib/Widget/Weather.php @@ -244,11 +244,11 @@ public function settings(Request $request, Response $response): Response * Edit Widget * * @SWG\Put( - * path="/playlist/widget/{widgetId}?weather", - * operationId="WidgetWeatherEdit", + * path="/playlist/widget/{widgetId}?weatherTiles", + * operationId="WidgetWeatherTilesEdit", * tags={"widget"}, - * summary="Edit Weather Widget", - * description="Edit Weather Widget. This call will replace existing Widget object, all not supplied parameters will be set to default.", + * summary="Edit Weather Tiles Widget", + * description="Edit Weather Tiles Widget. This call will replace existing Widget object, all not supplied parameters will be set to default.", * @SWG\Parameter( * name="widgetId", * in="path", diff --git a/web/swagger.json b/web/swagger.json index c6e4bbdff2..c8193a1ba8 100644 --- a/web/swagger.json +++ b/web/swagger.json @@ -11403,6 +11403,159 @@ } } }, + "/playlist/widget/{widgetId}?weatherTiles": { + "put": { + "tags": [ + "widget" + ], + "summary": "Edit Weather Tiles Widget", + "description": "Edit Weather Tiles Widget. This call will replace existing Widget object, all not supplied parameters will be set to default.", + "operationId": "WidgetWeatherTilesEdit", + "parameters": [ + { + "name": "widgetId", + "in": "path", + "description": "The WidgetId to Edit", + "required": true, + "type": "integer" + }, + { + "name": "name", + "in": "formData", + "description": "Optional Widget Name", + "required": false, + "type": "string" + }, + { + "name": "duration", + "in": "formData", + "description": "Widget Duration", + "required": false, + "type": "integer" + }, + { + "name": "useDuration", + "in": "formData", + "description": "(0, 1) Select 1 only if you will provide duration parameter as well", + "required": false, + "type": "integer" + }, + { + "name": "enableStat", + "in": "formData", + "description": "The option (On, Off, Inherit) to enable the collection of Widget Proof of Play statistics", + "required": false, + "type": "string" + }, + { + "name": "useDisplayLocation", + "in": "formData", + "description": "Flag (0, 1) Use the location configured on display", + "required": true, + "type": "integer" + }, + { + "name": "longitude", + "in": "formData", + "description": "The longitude for this weather widget, only pass if useDisplayLocation set to 0", + "required": false, + "type": "number" + }, + { + "name": "latitude", + "in": "formData", + "description": "The latitude for this weather widget, only pass if useDisplayLocation set to 0", + "required": false, + "type": "number" + }, + { + "name": "templateId", + "in": "formData", + "description": "Use pre-configured templates, available options: weather-module0-5day, weather-module0-singleday, weather-module0-singleday2, weather-module1l, weather-module1p, weather-module2l, weather-module2p, weather-module3l, weather-module3p, weather-module4l, weather-module4p, weather-module5l, weather-module6v, weather-module6h", + "required": false, + "type": "string" + }, + { + "name": "units", + "in": "formData", + "description": "Units you would like to use, available options: auto, ca, si, uk2, us", + "required": false, + "type": "string" + }, + { + "name": "updateInterval", + "in": "formData", + "description": "Update interval in minutes, should be kept as high as possible, if data change once per hour, this should be set to 60", + "required": false, + "type": "integer" + }, + { + "name": "lang", + "in": "formData", + "description": "Language you'd like to use, supported languages ar, az, be, bs, cs, de, en, el, es, fr, hr, hu, id, it, is, kw, nb, nl, pl, pt, ru, sk, sr, sv, tet, tr, uk, x-pig-latin, zh, zh-tw", + "required": false, + "type": "string" + }, + { + "name": "dayConditionsOnly", + "in": "formData", + "description": "Flag (0, 1) Would you like to only show the Daytime weather conditions", + "required": false, + "type": "integer" + }, + { + "name": "overrideTemplate", + "in": "formData", + "description": "flag (0, 1) set to 0 and use templateId or set to 1 and provide whole template in the next parameters", + "required": false, + "type": "integer" + }, + { + "name": "widgetOriginalWidth", + "in": "formData", + "description": "This is the intended Width of the template and is used to scale the Widget within it's region when the template is applied, Pass only with overrideTemplate set to 1", + "required": false, + "type": "integer" + }, + { + "name": "widgetOriginalHeight", + "in": "formData", + "description": "This is the intended Height of the template and is used to scale the Widget within it's region when the template is applied, Pass only with overrideTemplate set to 1", + "required": false, + "type": "integer" + }, + { + "name": "template", + "in": "formData", + "description": "Current template, Pass only with overrideTemplate set to 1 ", + "required": false, + "type": "string" + }, + { + "name": "styleSheet", + "in": "formData", + "description": "Optional StyleSheet, Pass only with overrideTemplate set to 1 ", + "required": false, + "type": "string" + }, + { + "name": "styleSheet", + "in": "formData", + "description": "Optional JavaScript, Pass only with overrideTemplate set to 1 ", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { + "$ref": "#/definitions/Widget" + } + } + } + } + }, "/playlist/widget/{widgetId}?webpage": { "put": { "tags": [ @@ -12750,7 +12903,10 @@ }, "settings": { "description": "An array of additional module specific settings", - "type": "array" + "type": "array", + "items": { + "type": "string" + } }, "schemaVersion": { "description": "The schema version of the module", From 6d6d505ca3d1d1c2523e635108a5dc92d51267e5 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 2 Sep 2020 17:45:53 +0100 Subject: [PATCH 08/10] Removed null parameters from TimeSeriesStore setDependencies() - the interface should be concrete. Add missing displayGroupFactory parameter. Delayed instantiation of MongoDB client to allow for "setClient()" replacement if needed. This might also speed up XMDS because it won't create a Mongo client with each instantiation. Removed redundant error messages (which output the exact same content) --- lib/Factory/ContainerFactory.php | 14 ++--- lib/Storage/MongoDbTimeSeriesStore.php | 79 ++++++++++++++---------- lib/Storage/MySqlTimeSeriesStore.php | 6 +- lib/Storage/TimeSeriesStoreInterface.php | 15 ++++- 4 files changed, 66 insertions(+), 48 deletions(-) diff --git a/lib/Factory/ContainerFactory.php b/lib/Factory/ContainerFactory.php index 7d29a4fcbe..b711177621 100644 --- a/lib/Factory/ContainerFactory.php +++ b/lib/Factory/ContainerFactory.php @@ -122,16 +122,14 @@ public static function create() }, 'timeSeriesStore' => function(ContainerInterface $c) { if ($c->get('configService')->timeSeriesStore == null) { - return (new MySqlTimeSeriesStore()) - ->setDependencies($c->get('logService'), - $c->get('layoutFactory'), - $c->get('campaignFactory')) - ->setStore($c->get('store')); + $timeSeriesStore = new MySqlTimeSeriesStore(); } else { $timeSeriesStore = $c->get('configService')->timeSeriesStore; $timeSeriesStore = $timeSeriesStore(); + } - return $timeSeriesStore->setDependencies( + return $timeSeriesStore + ->setDependencies( $c->get('logService'), $c->get('layoutFactory'), $c->get('campaignFactory'), @@ -139,8 +137,8 @@ public static function create() $c->get('widgetFactory'), $c->get('displayFactory'), $c->get('displayGroupFactory') - ); - } + ) + ->setStore($c->get('store')); }, 'state' => function() { return new ApplicationState(); diff --git a/lib/Storage/MongoDbTimeSeriesStore.php b/lib/Storage/MongoDbTimeSeriesStore.php index 58f252f520..c9666473d3 100644 --- a/lib/Storage/MongoDbTimeSeriesStore.php +++ b/lib/Storage/MongoDbTimeSeriesStore.php @@ -88,40 +88,60 @@ class MongoDbTimeSeriesStore implements TimeSeriesStoreInterface */ public function __construct($config = null) { - $this->config = $config; } /** * @inheritdoc */ - public function setDependencies($log, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null, $displayGroupFactory = null) + public function setDependencies($log, $layoutFactory, $campaignFactory, $mediaFactory, $widgetFactory, $displayFactory, $displayGroupFactory) { $this->log = $log; + $this->layoutFactory = $layoutFactory; + $this->campaignFactory = $campaignFactory; $this->mediaFactory = $mediaFactory; $this->widgetFactory = $widgetFactory; - $this->layoutFactory = $layoutFactory; $this->displayFactory = $displayFactory; $this->displayGroupFactory = $displayGroupFactory; - $this->campaignFactory = $campaignFactory; + return $this; + } - try { - $uri = isset($this->config['uri']) ? $this->config['uri'] : 'mongodb://' . $this->config['host'] . ':' . $this->config['port']; - $this->client = new Client($uri, [ - 'username' => $this->config['username'], - 'password' => $this->config['password'] - ], (array_key_exists('driverOptions', $this->config) ? $this->config['driverOptions'] : [])); - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->critical($e->getMessage()); + /** + * Set Client in the event you want to completely replace the configuration options and roll your own client. + * @param \MongoDB\Client $client + */ + public function setClient($client) + { + $this->client = $client; + } + + /** + * Get a MongoDB client to use. + * @return \MongoDB\Client + * @throws \Xibo\Support\Exception\GeneralException + */ + private function getClient() + { + if ($this->client === null) { + try { + $uri = isset($this->config['uri']) ? $this->config['uri'] : 'mongodb://' . $this->config['host'] . ':' . $this->config['port']; + $this->client = new Client($uri, [ + 'username' => $this->config['username'], + 'password' => $this->config['password'] + ], (array_key_exists('driverOptions', $this->config) ? $this->config['driverOptions'] : [])); + } catch (\MongoDB\Exception\RuntimeException $e) { + $this->log->error('Unable to connect to MongoDB: ' . $e->getMessage()); + $this->log->debug($e->getTraceAsString()); + throw new GeneralException('Connection to Time Series Database failed, please try again.'); + } } - return $this; + return $this->client; } /** @inheritdoc */ public function addStat($statData) { - // We need to transform string date to UTC date $statData['statDate'] = new UTCDateTime($statData['statDate']->format('U') * 1000); @@ -358,12 +378,14 @@ public function addStat($statData) } - /** @inheritdoc */ + /** @inheritdoc + * @throws \Xibo\Support\Exception\GeneralException + */ public function addStatFinalize() { // Insert statistics if (count($this->stats) > 0) { - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); try { $collection->insertMany($this->stats); @@ -375,7 +397,7 @@ public function addStatFinalize() } // Create a period collection if it doesnot exist - $collectionPeriod = $this->client->selectCollection($this->config['database'], $this->periodTable); + $collectionPeriod = $this->getClient()->selectCollection($this->config['database'], $this->periodTable); try { $cursor = $collectionPeriod->findOne(['name' => 'period']); @@ -397,7 +419,7 @@ public function addStatFinalize() /** @inheritdoc */ public function getEarliestDate() { - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); try { $earliestDate = $collection->aggregate([ [ @@ -521,7 +543,7 @@ public function getStats($filterBy = []) $campaignIds[] = $this->layoutFactory->getCampaignIdFromLayoutHistory($layoutId); } catch (NotFoundException $notFoundException) { // Ignore the missing one - $this->getLog()->debug('Filter for Layout without Layout History Record, layoutId is ' . $layoutId); + $this->log->debug('Filter for Layout without Layout History Record, layoutId is ' . $layoutId); } } $match['$match']['campaignId'] = [ '$in' => $campaignIds ]; @@ -532,7 +554,7 @@ public function getStats($filterBy = []) $match['$match']['mediaId'] = [ '$in' => $mediaIds ]; } - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); $group = [ '$group' => [ @@ -559,9 +581,6 @@ public function getStats($filterBy = []) $totalCount = $totalCursor->toArray(); $total = (count($totalCount) > 0) ? $totalCount[0]['count'] : 0; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error('Error: Total Count. '. $e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error('Error: Total Count. '. $e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); @@ -619,9 +638,6 @@ public function getStats($filterBy = []) // Total $result->totalCount = $total; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error('Error: Get total. '. $e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error('Error: Get total. '. $e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); @@ -655,7 +671,7 @@ public function getExportStatsCount($filterBy = []) $match['$match']['displayId'] = [ '$in' => $displayIds ]; } - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); // Get total try { @@ -673,9 +689,6 @@ public function getExportStatsCount($filterBy = []) $totalCount = $totalCursor->toArray(); $total = (count($totalCount) > 0) ? $totalCount[0]['count'] : 0; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error($e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting total number of Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error($e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting total number of Proof of Play data, please consult your administrator')); @@ -699,7 +712,7 @@ public function deleteStats($maxage, $fromDt = null, $options = []) $toDt = new UTCDateTime($maxage->format('U')*1000); - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); $rows = 1; $count = 0; @@ -764,7 +777,7 @@ public function executeQuery($options = []) $aggregateConfig['maxTimeMS']= $options['maxTimeMS']; } - $collection = $this->client->selectCollection($this->config['database'], $options['collection']); + $collection = $this->getClient()->selectCollection($this->config['database'], $options['collection']); try { $cursor = $collection->aggregate($options['query'], $aggregateConfig); @@ -775,10 +788,10 @@ public function executeQuery($options = []) } catch (\MongoDB\Driver\Exception\RuntimeException $e) { $this->log->error($e->getMessage()); + $this->log->debug($e->getTraceAsString()); throw new GeneralException($e->getMessage()); } return $results; - } } \ No newline at end of file diff --git a/lib/Storage/MySqlTimeSeriesStore.php b/lib/Storage/MySqlTimeSeriesStore.php index 93b827fbc9..720c51aea2 100644 --- a/lib/Storage/MySqlTimeSeriesStore.php +++ b/lib/Storage/MySqlTimeSeriesStore.php @@ -62,7 +62,7 @@ public function __construct($config = null) /** * @inheritdoc */ - public function setDependencies($log, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null) + public function setDependencies($log, $layoutFactory, $campaignFactory, $mediaFactory, $widgetFactory, $displayFactory, $displayGroupFactory) { $this->log = $log; $this->layoutFactory = $layoutFactory; @@ -424,9 +424,7 @@ public function getExportStatsCount($filterBy = []) $resTotal = $this->store->select($sql, $params); // Total - $totalCount = isset($resTotal[0]['total']) ? $resTotal[0]['total'] : 0; - - return $totalCount; + return isset($resTotal[0]['total']) ? $resTotal[0]['total'] : 0; } /** @inheritdoc */ diff --git a/lib/Storage/TimeSeriesStoreInterface.php b/lib/Storage/TimeSeriesStoreInterface.php index 79e171b92e..997ca1303b 100644 --- a/lib/Storage/TimeSeriesStoreInterface.php +++ b/lib/Storage/TimeSeriesStoreInterface.php @@ -46,13 +46,22 @@ public function __construct($config = null); /** * Set Time series Dependencies * @param LogServiceInterface $logger + * @param LayoutFactory $layoutFactory + * @param CampaignFactory $campaignFactory * @param MediaFactory $mediaFactory * @param WidgetFactory $widgetFactory - * @param LayoutFactory $layoutFactory * @param DisplayFactory $displayFactory - * @param CampaignFactory $campaignFactory + * @param \Xibo\Entity\DisplayGroup $displayGroupFactory */ - public function setDependencies($logger, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null); + public function setDependencies( + $logger, + $layoutFactory, + $campaignFactory, + $mediaFactory, + $widgetFactory, + $displayFactory, + $displayGroupFactory + ); /** * Process and add a single statdata to array From fc3b569d53cafa8753579eb6263ed2924148e2da Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Wed, 2 Sep 2020 17:45:53 +0100 Subject: [PATCH 09/10] Removed null parameters from TimeSeriesStore setDependencies() - the interface should be concrete. Add missing displayGroupFactory parameter. Delayed instantiation of MongoDB client to allow for "setClient()" replacement if needed. This might also speed up XMDS because it won't create a Mongo client with each instantiation. Removed redundant error messages (which output the exact same content) xibosignage/xibo#2315 --- lib/Factory/ContainerFactory.php | 14 ++--- lib/Storage/MongoDbTimeSeriesStore.php | 79 ++++++++++++++---------- lib/Storage/MySqlTimeSeriesStore.php | 6 +- lib/Storage/TimeSeriesStoreInterface.php | 15 ++++- 4 files changed, 66 insertions(+), 48 deletions(-) diff --git a/lib/Factory/ContainerFactory.php b/lib/Factory/ContainerFactory.php index 7d29a4fcbe..b711177621 100644 --- a/lib/Factory/ContainerFactory.php +++ b/lib/Factory/ContainerFactory.php @@ -122,16 +122,14 @@ public static function create() }, 'timeSeriesStore' => function(ContainerInterface $c) { if ($c->get('configService')->timeSeriesStore == null) { - return (new MySqlTimeSeriesStore()) - ->setDependencies($c->get('logService'), - $c->get('layoutFactory'), - $c->get('campaignFactory')) - ->setStore($c->get('store')); + $timeSeriesStore = new MySqlTimeSeriesStore(); } else { $timeSeriesStore = $c->get('configService')->timeSeriesStore; $timeSeriesStore = $timeSeriesStore(); + } - return $timeSeriesStore->setDependencies( + return $timeSeriesStore + ->setDependencies( $c->get('logService'), $c->get('layoutFactory'), $c->get('campaignFactory'), @@ -139,8 +137,8 @@ public static function create() $c->get('widgetFactory'), $c->get('displayFactory'), $c->get('displayGroupFactory') - ); - } + ) + ->setStore($c->get('store')); }, 'state' => function() { return new ApplicationState(); diff --git a/lib/Storage/MongoDbTimeSeriesStore.php b/lib/Storage/MongoDbTimeSeriesStore.php index 58f252f520..c9666473d3 100644 --- a/lib/Storage/MongoDbTimeSeriesStore.php +++ b/lib/Storage/MongoDbTimeSeriesStore.php @@ -88,40 +88,60 @@ class MongoDbTimeSeriesStore implements TimeSeriesStoreInterface */ public function __construct($config = null) { - $this->config = $config; } /** * @inheritdoc */ - public function setDependencies($log, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null, $displayGroupFactory = null) + public function setDependencies($log, $layoutFactory, $campaignFactory, $mediaFactory, $widgetFactory, $displayFactory, $displayGroupFactory) { $this->log = $log; + $this->layoutFactory = $layoutFactory; + $this->campaignFactory = $campaignFactory; $this->mediaFactory = $mediaFactory; $this->widgetFactory = $widgetFactory; - $this->layoutFactory = $layoutFactory; $this->displayFactory = $displayFactory; $this->displayGroupFactory = $displayGroupFactory; - $this->campaignFactory = $campaignFactory; + return $this; + } - try { - $uri = isset($this->config['uri']) ? $this->config['uri'] : 'mongodb://' . $this->config['host'] . ':' . $this->config['port']; - $this->client = new Client($uri, [ - 'username' => $this->config['username'], - 'password' => $this->config['password'] - ], (array_key_exists('driverOptions', $this->config) ? $this->config['driverOptions'] : [])); - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->critical($e->getMessage()); + /** + * Set Client in the event you want to completely replace the configuration options and roll your own client. + * @param \MongoDB\Client $client + */ + public function setClient($client) + { + $this->client = $client; + } + + /** + * Get a MongoDB client to use. + * @return \MongoDB\Client + * @throws \Xibo\Support\Exception\GeneralException + */ + private function getClient() + { + if ($this->client === null) { + try { + $uri = isset($this->config['uri']) ? $this->config['uri'] : 'mongodb://' . $this->config['host'] . ':' . $this->config['port']; + $this->client = new Client($uri, [ + 'username' => $this->config['username'], + 'password' => $this->config['password'] + ], (array_key_exists('driverOptions', $this->config) ? $this->config['driverOptions'] : [])); + } catch (\MongoDB\Exception\RuntimeException $e) { + $this->log->error('Unable to connect to MongoDB: ' . $e->getMessage()); + $this->log->debug($e->getTraceAsString()); + throw new GeneralException('Connection to Time Series Database failed, please try again.'); + } } - return $this; + return $this->client; } /** @inheritdoc */ public function addStat($statData) { - // We need to transform string date to UTC date $statData['statDate'] = new UTCDateTime($statData['statDate']->format('U') * 1000); @@ -358,12 +378,14 @@ public function addStat($statData) } - /** @inheritdoc */ + /** @inheritdoc + * @throws \Xibo\Support\Exception\GeneralException + */ public function addStatFinalize() { // Insert statistics if (count($this->stats) > 0) { - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); try { $collection->insertMany($this->stats); @@ -375,7 +397,7 @@ public function addStatFinalize() } // Create a period collection if it doesnot exist - $collectionPeriod = $this->client->selectCollection($this->config['database'], $this->periodTable); + $collectionPeriod = $this->getClient()->selectCollection($this->config['database'], $this->periodTable); try { $cursor = $collectionPeriod->findOne(['name' => 'period']); @@ -397,7 +419,7 @@ public function addStatFinalize() /** @inheritdoc */ public function getEarliestDate() { - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); try { $earliestDate = $collection->aggregate([ [ @@ -521,7 +543,7 @@ public function getStats($filterBy = []) $campaignIds[] = $this->layoutFactory->getCampaignIdFromLayoutHistory($layoutId); } catch (NotFoundException $notFoundException) { // Ignore the missing one - $this->getLog()->debug('Filter for Layout without Layout History Record, layoutId is ' . $layoutId); + $this->log->debug('Filter for Layout without Layout History Record, layoutId is ' . $layoutId); } } $match['$match']['campaignId'] = [ '$in' => $campaignIds ]; @@ -532,7 +554,7 @@ public function getStats($filterBy = []) $match['$match']['mediaId'] = [ '$in' => $mediaIds ]; } - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); $group = [ '$group' => [ @@ -559,9 +581,6 @@ public function getStats($filterBy = []) $totalCount = $totalCursor->toArray(); $total = (count($totalCount) > 0) ? $totalCount[0]['count'] : 0; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error('Error: Total Count. '. $e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error('Error: Total Count. '. $e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); @@ -619,9 +638,6 @@ public function getStats($filterBy = []) // Total $result->totalCount = $total; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error('Error: Get total. '. $e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error('Error: Get total. '. $e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting Proof of Play data, please consult your administrator')); @@ -655,7 +671,7 @@ public function getExportStatsCount($filterBy = []) $match['$match']['displayId'] = [ '$in' => $displayIds ]; } - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); // Get total try { @@ -673,9 +689,6 @@ public function getExportStatsCount($filterBy = []) $totalCount = $totalCursor->toArray(); $total = (count($totalCount) > 0) ? $totalCount[0]['count'] : 0; - } catch (\MongoDB\Exception\RuntimeException $e) { - $this->log->error($e->getMessage()); - throw new GeneralException(__('Sorry we encountered an error getting total number of Proof of Play data, please consult your administrator')); } catch (\Exception $e) { $this->log->error($e->getMessage()); throw new GeneralException(__('Sorry we encountered an error getting total number of Proof of Play data, please consult your administrator')); @@ -699,7 +712,7 @@ public function deleteStats($maxage, $fromDt = null, $options = []) $toDt = new UTCDateTime($maxage->format('U')*1000); - $collection = $this->client->selectCollection($this->config['database'], $this->table); + $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); $rows = 1; $count = 0; @@ -764,7 +777,7 @@ public function executeQuery($options = []) $aggregateConfig['maxTimeMS']= $options['maxTimeMS']; } - $collection = $this->client->selectCollection($this->config['database'], $options['collection']); + $collection = $this->getClient()->selectCollection($this->config['database'], $options['collection']); try { $cursor = $collection->aggregate($options['query'], $aggregateConfig); @@ -775,10 +788,10 @@ public function executeQuery($options = []) } catch (\MongoDB\Driver\Exception\RuntimeException $e) { $this->log->error($e->getMessage()); + $this->log->debug($e->getTraceAsString()); throw new GeneralException($e->getMessage()); } return $results; - } } \ No newline at end of file diff --git a/lib/Storage/MySqlTimeSeriesStore.php b/lib/Storage/MySqlTimeSeriesStore.php index 93b827fbc9..720c51aea2 100644 --- a/lib/Storage/MySqlTimeSeriesStore.php +++ b/lib/Storage/MySqlTimeSeriesStore.php @@ -62,7 +62,7 @@ public function __construct($config = null) /** * @inheritdoc */ - public function setDependencies($log, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null) + public function setDependencies($log, $layoutFactory, $campaignFactory, $mediaFactory, $widgetFactory, $displayFactory, $displayGroupFactory) { $this->log = $log; $this->layoutFactory = $layoutFactory; @@ -424,9 +424,7 @@ public function getExportStatsCount($filterBy = []) $resTotal = $this->store->select($sql, $params); // Total - $totalCount = isset($resTotal[0]['total']) ? $resTotal[0]['total'] : 0; - - return $totalCount; + return isset($resTotal[0]['total']) ? $resTotal[0]['total'] : 0; } /** @inheritdoc */ diff --git a/lib/Storage/TimeSeriesStoreInterface.php b/lib/Storage/TimeSeriesStoreInterface.php index 79e171b92e..997ca1303b 100644 --- a/lib/Storage/TimeSeriesStoreInterface.php +++ b/lib/Storage/TimeSeriesStoreInterface.php @@ -46,13 +46,22 @@ public function __construct($config = null); /** * Set Time series Dependencies * @param LogServiceInterface $logger + * @param LayoutFactory $layoutFactory + * @param CampaignFactory $campaignFactory * @param MediaFactory $mediaFactory * @param WidgetFactory $widgetFactory - * @param LayoutFactory $layoutFactory * @param DisplayFactory $displayFactory - * @param CampaignFactory $campaignFactory + * @param \Xibo\Entity\DisplayGroup $displayGroupFactory */ - public function setDependencies($logger, $layoutFactory = null, $campaignFactory = null, $mediaFactory = null, $widgetFactory = null, $displayFactory = null); + public function setDependencies( + $logger, + $layoutFactory, + $campaignFactory, + $mediaFactory, + $widgetFactory, + $displayFactory, + $displayGroupFactory + ); /** * Process and add a single statdata to array From 9597ce1100ed933dabcfd21b856811772befc597 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Thu, 3 Sep 2020 08:06:12 +0100 Subject: [PATCH 10/10] Fix for date formatter twig extension. --- bin/locale.php | 1 - lib/Controller/Campaign.php | 22 ++++++------ lib/Factory/ContainerFactory.php | 1 - lib/Twig/ByteFormatterTwigExtension.php | 31 ++++++++++++---- lib/Twig/DateFormatTwigExtension.php | 31 ++++++++++++---- lib/Twig/UrlDecodeTwigExtension.php | 47 ------------------------- views/campaign-preview.twig | 2 +- 7 files changed, 61 insertions(+), 74 deletions(-) delete mode 100644 lib/Twig/UrlDecodeTwigExtension.php diff --git a/bin/locale.php b/bin/locale.php index c35dadda4b..fb49be7732 100644 --- a/bin/locale.php +++ b/bin/locale.php @@ -36,7 +36,6 @@ $twig->addExtension(new \Slim\Views\TwigExtension()); $twig->addExtension(new \Xibo\Twig\TransExtension()); $twig->addExtension(new \Xibo\Twig\ByteFormatterTwigExtension()); -$twig->addExtension(new \Xibo\Twig\UrlDecodeTwigExtension()); $twig->addExtension(new \Xibo\Twig\DateFormatTwigExtension()); diff --git a/lib/Controller/Campaign.php b/lib/Controller/Campaign.php index 01032d246e..f68293e590 100644 --- a/lib/Controller/Campaign.php +++ b/lib/Controller/Campaign.php @@ -888,18 +888,20 @@ public function preview(Request $request, Response $response, $id) $duration = 0 ; $extendedLayouts = []; - foreach($layouts as $layout) + foreach ($layouts as $layout) { $duration += $layout->duration; - $extendedLayouts[] = ['layout' => $layout, - 'duration' => $layout->duration, - 'previewOptions' => [ - 'getXlfUrl' => $this->urlFor($request,'layout.getXlf', ['id' => $layout->layoutId]), - 'getResourceUrl' => $this->urlFor($request,'module.getResource', ['regionId' => ':regionId', 'id' => ':id']), - 'libraryDownloadUrl' => $this->urlFor($request,'library.download'), - 'layoutBackgroundDownloadUrl' => $this->urlFor($request,'layout.download.background', ['id' => ':id']), - 'loaderUrl' => $this->getConfig()->uri('img/loader.gif')] - ]; + $extendedLayouts[] = [ + 'layout' => $layout, + 'duration' => $layout->duration, + 'previewOptions' => [ + 'getXlfUrl' => $this->urlFor($request,'layout.getXlf', ['id' => $layout->layoutId]), + 'getResourceUrl' => $this->urlFor($request,'module.getResource', ['regionId' => ':regionId', 'id' => ':id']), + 'libraryDownloadUrl' => $this->urlFor($request,'library.download'), + 'layoutBackgroundDownloadUrl' => $this->urlFor($request,'layout.download.background', ['id' => ':id']), + 'loaderUrl' => $this->getConfig()->uri('img/loader.gif') + ] + ]; } $this->getState()->template = 'campaign-preview'; $this->getState()->setData([ diff --git a/lib/Factory/ContainerFactory.php b/lib/Factory/ContainerFactory.php index b711177621..105343c695 100644 --- a/lib/Factory/ContainerFactory.php +++ b/lib/Factory/ContainerFactory.php @@ -109,7 +109,6 @@ public static function create() ]); $view->addExtension(new TransExtension()); $view->addExtension(new ByteFormatterTwigExtension()); - $view->addExtension(new UrlDecodeTwigExtension()); $view->addExtension(new DateFormatTwigExtension()); return $view; diff --git a/lib/Twig/ByteFormatterTwigExtension.php b/lib/Twig/ByteFormatterTwigExtension.php index 04b8dc5d16..4d828c42cb 100644 --- a/lib/Twig/ByteFormatterTwigExtension.php +++ b/lib/Twig/ByteFormatterTwigExtension.php @@ -1,16 +1,33 @@ . */ - - namespace Xibo\Twig; - -use Xibo\Helper\ByteFormatter; use Twig\Extension\AbstractExtension; +use Xibo\Helper\ByteFormatter; + +/** + * Class ByteFormatterTwigExtension + * @package Xibo\Twig + */ class ByteFormatterTwigExtension extends AbstractExtension { public function getName() diff --git a/lib/Twig/DateFormatTwigExtension.php b/lib/Twig/DateFormatTwigExtension.php index fbf1e15799..a9faeb3eeb 100644 --- a/lib/Twig/DateFormatTwigExtension.php +++ b/lib/Twig/DateFormatTwigExtension.php @@ -1,16 +1,33 @@ . */ - namespace Xibo\Twig; - use Twig\Extension\AbstractExtension; +/** + * Class DateFormatTwigExtension + * @package Xibo\Twig + */ class DateFormatTwigExtension extends AbstractExtension { /** @@ -19,7 +36,7 @@ class DateFormatTwigExtension extends AbstractExtension public function getFilters() { return array( - 'datehms' => new \Twig\TwigFilter('dateFormat', $this) + new \Twig\TwigFilter('datehms', [$this, 'dateFormat']) ); } @@ -30,7 +47,7 @@ public function getFilters() * * @return string formated as HH:mm:ss */ - public function dateFormat( $date ) + public function dateFormat($date) { return gmdate('H:i:s', $date); } diff --git a/lib/Twig/UrlDecodeTwigExtension.php b/lib/Twig/UrlDecodeTwigExtension.php deleted file mode 100644 index a003eb937b..0000000000 --- a/lib/Twig/UrlDecodeTwigExtension.php +++ /dev/null @@ -1,47 +0,0 @@ - new \Twig\TwigFilter('urlDecode', $this) - ); - } - - /** - * URL Decode a string - * - * @param string $url - * - * @return string The decoded URL - */ - public function urlDecode( $url ) - { - return urldecode( $url ); - } - - /** - * Returns the name of the extension. - * - * @return string The extension name - */ - public function getName() - { - return 'url_decode'; - } -} \ No newline at end of file diff --git a/views/campaign-preview.twig b/views/campaign-preview.twig index 5690b69738..e555cc7ba2 100644 --- a/views/campaign-preview.twig +++ b/views/campaign-preview.twig @@ -78,7 +78,7 @@ {% autoescape "js" %} previewTranslations.actionControllerTitle = "{{ "Action Controller"|trans }}"; previewTranslations.navigateToLayout = "{{ "Navigate to layout with code [layoutTag]?"|trans }}"; - previewTranslations.emptyRegionMessage = "{{% "Empty Region"|trans }}"; + previewTranslations.emptyRegionMessage = "{{ "Empty Region"|trans }}"; {% endautoescape %} (function($){