From d5a99f11f3a0d99258f73983dd2ad386b8a5ef54 Mon Sep 17 00:00:00 2001 From: ifarzana Date: Tue, 13 Dec 2022 16:53:08 +0000 Subject: [PATCH 01/16] Connector Reports --- .../XiboAudienceReportingConnector.php | 9 +- lib/Controller/Stats.php | 9 +- lib/Entity/ReportResult.php | 14 ++- lib/Event/ConnectorReportEvent.php | 42 +++++++++ lib/Listener/ConnectorReportListener.php | 88 +++++++++++++++++++ lib/Middleware/ListenersMiddleware.php | 9 ++ lib/Middleware/State.php | 4 +- lib/Report/CampaignProofOfPlay.php | 10 +-- lib/Service/ReportService.php | 27 ++++++ reports/campaign-proofofplay-report-form.twig | 4 + 10 files changed, 206 insertions(+), 10 deletions(-) create mode 100644 lib/Event/ConnectorReportEvent.php create mode 100644 lib/Listener/ConnectorReportListener.php diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 1c8223ba03..74441845ab 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -285,6 +285,9 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) } } + /** + * @throws GeneralException + */ public function onAudienceReport(ReportDataEvent $event) { $type = $event->getReportType(); @@ -310,6 +313,7 @@ public function onAudienceReport(ReportDataEvent $event) $json = json_decode($body, true); } catch (RequestException $requestException) { $this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage()); + $error = 'Failed to get campaign proofofplay result: '.$requestException->getMessage(); } break; @@ -317,7 +321,10 @@ public function onAudienceReport(ReportDataEvent $event) $this->getLogger()->error('Report type not found '); } - $event->setResults($json); + $event->setResults([ + 'json' => $json, + 'error' => $error ?? null + ]); } } diff --git a/lib/Controller/Stats.php b/lib/Controller/Stats.php index 57d75443b3..40f4f48618 100644 --- a/lib/Controller/Stats.php +++ b/lib/Controller/Stats.php @@ -24,6 +24,7 @@ use Carbon\Carbon; use Slim\Http\Response as Response; use Slim\Http\ServerRequest as Request; +use Xibo\Event\ConnectorReportEvent; use Xibo\Factory\DisplayFactory; use Xibo\Factory\DisplayGroupFactory; use Xibo\Factory\LayoutFactory; @@ -90,13 +91,19 @@ public function __construct($store, $timeSeriesStore, $reportService, $displayFa */ function displayReportPage(Request $request, Response $response) { + // ------------ + // Dispatch an event to get connector reports + $event = new ConnectorReportEvent(); + $this->getDispatcher()->dispatch($event, ConnectorReportEvent::$NAME); + $data = [ // List of Displays this user has permission for 'defaults' => [ 'fromDate' => Carbon::now()->subSeconds(86400 * 35)->format(DateFormatHelper::getSystemFormat()), 'fromDateOneDay' => Carbon::now()->subSeconds(86400)->format(DateFormatHelper::getSystemFormat()), 'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat()), - 'availableReports' => $this->reportService->listReports() + 'availableReports' => $this->reportService->listReports(), + 'connectorReports' => $event->getReports() ] ]; diff --git a/lib/Entity/ReportResult.php b/lib/Entity/ReportResult.php index 44987befd8..d37386cd48 100644 --- a/lib/Entity/ReportResult.php +++ b/lib/Entity/ReportResult.php @@ -41,6 +41,12 @@ class ReportResult implements \JsonSerializable */ public $chart; + /** + * Error message + * @var null|string + */ + public $error; + /** * Metadata that is used in the report preview or in the email template * @var array @@ -59,17 +65,20 @@ class ReportResult implements \JsonSerializable * @param array $table * @param int $recordsTotal * @param array $chart + * @param null|string $error */ public function __construct( array $metadata = [], array $table = [], int $recordsTotal = 0, - array $chart = [] + array $chart = [], + string $error = null ) { $this->metadata = $metadata; $this->table = $table; $this->recordsTotal = $recordsTotal; $this->chart = $chart; + $this->error = $error; return $this; } @@ -95,7 +104,8 @@ public function jsonSerialize() 'metadata' => $this->metadata, 'table' => $this->table, 'recordsTotal' => $this->recordsTotal, - 'chart' => $this->chart + 'chart' => $this->chart, + 'error' => $this->error ]; } } diff --git a/lib/Event/ConnectorReportEvent.php b/lib/Event/ConnectorReportEvent.php new file mode 100644 index 0000000000..ec676d0c77 --- /dev/null +++ b/lib/Event/ConnectorReportEvent.php @@ -0,0 +1,42 @@ +. + */ +namespace Xibo\Event; + +class ConnectorReportEvent extends Event +{ + public static $NAME = 'connector.report.event'; + + /** @var array */ + private $reports; + + public function getReports() + { + return $this->reports; + } + + public function setReportObject($reports) + { + $this->reports = $reports; + + return $this; + } +} diff --git a/lib/Listener/ConnectorReportListener.php b/lib/Listener/ConnectorReportListener.php new file mode 100644 index 0000000000..485adcd09b --- /dev/null +++ b/lib/Listener/ConnectorReportListener.php @@ -0,0 +1,88 @@ +. + */ + +namespace Xibo\Listener; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Xibo\Event\ConnectorReportEvent; +use Xibo\Storage\StorageServiceInterface; + +/** + * Connector report events + */ +class ConnectorReportListener +{ + use ListenerLoggerTrait; + + /** @var \Xibo\Storage\StorageServiceInterface */ + private $storageService; + + public function __construct( + StorageServiceInterface $storageService + ) { + $this->storageService = $storageService; + } + + public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorReportListener + { + $dispatcher->addListener(ConnectorReportEvent::$NAME, [$this, 'onRequestReport']); + return $this; + } + + /** + * Get reports + * @param ConnectorReportEvent $event + * @return void + */ + public function onRequestReport(ConnectorReportEvent $event) + { + $this->getLogger()->debug('onRequestReport'); + + $connectorReports = [ + [ + 'name'=> 'campaignProofOfPlayReport', + 'description'=> 'Campaign Proof of Play', + 'class'=> '\\Xibo\\Report\\CampaignProofOfPlay', + 'type'=> 'Report', + 'output_type'=> 'table', + 'color'=> 'gray', + 'fa_icon'=> 'fa-th', + 'category'=> 'Connector Reports', + 'feature'=> 'campaign-proof-of-play', + 'adminOnly'=> 0, + ], + ]; + + $reports = []; + foreach ($connectorReports as $connectorReport) { + + // Compatibility check + if (!isset($connectorReport['feature']) || !isset($connectorReport['category'])) { + continue; + } + + $reports[$connectorReport['category']][] = (object) $connectorReport; + } + + $event->setReportObject($reports); + } +} diff --git a/lib/Middleware/ListenersMiddleware.php b/lib/Middleware/ListenersMiddleware.php index 3daae92c45..153c73fbc5 100644 --- a/lib/Middleware/ListenersMiddleware.php +++ b/lib/Middleware/ListenersMiddleware.php @@ -38,6 +38,8 @@ use Xibo\Event\SystemUserChangedEvent; use Xibo\Event\UserDeleteEvent; use Xibo\Listener\CampaignListener; +use Xibo\Event\ConnectorReportEvent; +use Xibo\Listener\ConnectorReportListener; /** * This middleware is used to register listeners against the dispatcher @@ -88,6 +90,13 @@ public static function setListeners(App $app) ->useLogger($c->get('logger')) ->registerWithDispatcher($dispatcher); + // Listen for events that affect reports + (new ConnectorReportListener( + $c->get('store') + )) + ->useLogger($c->get('logger')) + ->registerWithDispatcher($dispatcher); + // Media Delete Events $dispatcher->addListener(MediaDeleteEvent::$NAME, (new \Xibo\Listener\OnMediaDelete\MenuBoardListener( $c->get('menuBoardCategoryFactory') diff --git a/lib/Middleware/State.php b/lib/Middleware/State.php index d92d32a3ad..334b906772 100644 --- a/lib/Middleware/State.php +++ b/lib/Middleware/State.php @@ -150,7 +150,7 @@ public static function setState(App $app, Request $request): Request // Register the report service $container->set('reportService', function (ContainerInterface $container) { - return new ReportService( + $reportService = new ReportService( $container, $container->get('store'), $container->get('timeSeriesStore'), @@ -159,6 +159,8 @@ public static function setState(App $app, Request $request): Request $container->get('sanitizerService'), $container->get('savedReportFactory') ); + $reportService->setDispatcher($container->get('dispatcher')); + return $reportService; }); // Set some public routes diff --git a/lib/Report/CampaignProofOfPlay.php b/lib/Report/CampaignProofOfPlay.php index 741e2ba236..a4932602b9 100644 --- a/lib/Report/CampaignProofOfPlay.php +++ b/lib/Report/CampaignProofOfPlay.php @@ -352,16 +352,14 @@ public function getResults(SanitizerInterface $sanitizedParams) // Dispatch the event - listened by Audience Report Connector $this->dispatcher->dispatch($event, ReportDataEvent::$NAME); - - // Get results from the event - $result['result'] = $event->getResults(); + $results = $event->getResults(); $result['periodStart'] = $params['fromDt']; $result['periodEnd'] = $params['toDt']; // Sanitize results?? $rows = []; - foreach ($result['result'] as $row) { + foreach ($results['json'] as $row) { $entry = []; $entry['labelDate'] = $row['labelDate']; @@ -387,7 +385,9 @@ public function getResults(SanitizerInterface $sanitizedParams) return new ReportResult( $metadata, $rows, - $recordsTotal + $recordsTotal, + [], + $results['error'] ?? null ); } } diff --git a/lib/Service/ReportService.php b/lib/Service/ReportService.php index 1384c36a2e..7701ef589e 100644 --- a/lib/Service/ReportService.php +++ b/lib/Service/ReportService.php @@ -25,7 +25,10 @@ use Illuminate\Support\Str; use Psr\Container\ContainerInterface; use Slim\Http\ServerRequest as Request; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Xibo\Entity\ReportResult; +use Xibo\Event\ConnectorReportEvent; use Xibo\Factory\SavedReportFactory; use Xibo\Helper\SanitizerService; use Xibo\Report\ReportInterface; @@ -75,6 +78,9 @@ class ReportService implements ReportServiceInterface */ private $savedReportFactory; + /** @var EventDispatcherInterface */ + private $dispatcher; + /** * @inheritdoc */ @@ -89,6 +95,21 @@ public function __construct($container, $store, $timeSeriesStore, $log, $config, $this->savedReportFactory = $savedReportFactory; } + /** @inheritDoc */ + public function setDispatcher(EventDispatcherInterface $dispatcher): ReportServiceInterface + { + $this->dispatcher = $dispatcher; + return $this; + } + + public function getDispatcher(): EventDispatcherInterface + { + if ($this->dispatcher === null) { + $this->dispatcher = new EventDispatcher(); + } + return $this->dispatcher; + } + /** * @inheritdoc */ @@ -124,6 +145,12 @@ public function listReports() $this->log->debug('Reports found in total: '.count($reports)); + // Get reports that are allowed by connectors + $event = new ConnectorReportEvent(); + $this->getDispatcher()->dispatch($event, ConnectorReportEvent::$NAME); + + // Merge built in reports and connector reports + $reports = array_merge($reports, $event->getReports()); foreach ($reports as $k => $report) { usort($report, function ($a, $b) { diff --git a/reports/campaign-proofofplay-report-form.twig b/reports/campaign-proofofplay-report-form.twig index 0aff2b937a..8839d3c49a 100644 --- a/reports/campaign-proofofplay-report-form.twig +++ b/reports/campaign-proofofplay-report-form.twig @@ -250,6 +250,10 @@ } else { setChartData(result.chart); } + + if (result.error) { + toastr.error(result.error); + } }, error: function error(xhr, textStatus, _error) { $applyBtn.removeClass('disabled'); From c44330344c4c46cc954a705c7a52e50c86511f34 Mon Sep 17 00:00:00 2001 From: ifarzana Date: Wed, 14 Dec 2022 09:10:16 +0000 Subject: [PATCH 02/16] Connector Reports: List reports of the connector https://github.com/xibosignage/xibo/issues/2959 --- .../XiboAudienceReportingConnector.php | 58 ++++++++++-- lib/Listener/ConnectorReportListener.php | 88 ------------------- lib/Middleware/ListenersMiddleware.php | 7 -- 3 files changed, 52 insertions(+), 101 deletions(-) delete mode 100644 lib/Listener/ConnectorReportListener.php diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 74441845ab..419d529679 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -25,17 +25,16 @@ use GuzzleHttp\Exception\RequestException; use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Xibo\Entity\User; +use Xibo\Event\ConnectorReportEvent; use Xibo\Event\ReportDataEvent; use Xibo\Event\MaintenanceRegularEvent; use Xibo\Factory\CampaignFactory; use Xibo\Factory\DisplayFactory; use Xibo\Helper\DateFormatHelper; use Xibo\Helper\SanitizerService; -use Xibo\Storage\StorageServiceInterface; use Xibo\Storage\TimeSeriesStoreInterface; -use Xibo\Support\Exception\DuplicateEntityException; use Xibo\Support\Exception\GeneralException; -use Xibo\Support\Exception\InvalidArgumentException; use Xibo\Support\Exception\NotFoundException; use Xibo\Support\Sanitizer\SanitizerInterface; @@ -43,8 +42,8 @@ class XiboAudienceReportingConnector implements ConnectorInterface { use ConnectorTrait; - /** @var StorageServiceInterface */ - private $store; + /** @var User */ + private $user; /** @var TimeSeriesStoreInterface */ private $timeSeriesStore; @@ -64,7 +63,7 @@ class XiboAudienceReportingConnector implements ConnectorInterface */ public function setFactories(ContainerInterface $container): ConnectorInterface { - $this->store = $container->get('store'); + $this->user = $container->get('user'); $this->timeSeriesStore = $container->get('timeSeriesStore'); $this->sanitizer = $container->get('sanitizerService'); $this->campaignFactory = $container->get('campaignFactory'); @@ -77,6 +76,7 @@ public function registerWithDispatcher(EventDispatcherInterface $dispatcher): Co { $dispatcher->addListener(MaintenanceRegularEvent::$NAME, [$this, 'onRegularMaintenance']); $dispatcher->addListener(ReportDataEvent::$NAME, [$this, 'onAudienceReport']); + $dispatcher->addListener(ConnectorReportEvent::$NAME, [$this, 'onRequestConnectorReport']); return $this; } @@ -328,6 +328,52 @@ public function onAudienceReport(ReportDataEvent $event) } } + /** + * Get this connector reports + * @param ConnectorReportEvent $event + * @return void + */ + public function onRequestConnectorReport(ConnectorReportEvent $event) + { + $this->getLogger()->debug('onRequestConnectorReport'); + + $connectorReports = [ + [ + 'name'=> 'campaignProofOfPlayReport', + 'description'=> 'Campaign Proof of Play', + 'class'=> '\\Xibo\\Report\\CampaignProofOfPlay', + 'type'=> 'Report', + 'output_type'=> 'table', + 'color'=> 'gray', + 'fa_icon'=> 'fa-th', + 'category'=> 'Connector Reports', + 'feature'=> 'campaign-proof-of-play', + 'adminOnly'=> 0, + 'sort_order' => 3 + ], + ]; + + $reports = []; + foreach ($connectorReports as $connectorReport) { + + // Compatibility check + if (!isset($connectorReport['feature']) || !isset($connectorReport['category'])) { + continue; + } + + // Check if only allowed for admin + if ($this->user->userTypeId != 1) { + if (isset($connectorReport['adminOnly']) && !empty($connectorReport['adminOnly'])) { + continue; + } + } + + $reports[$connectorReport['category']][] = (object) $connectorReport; + } + + $event->setReportObject($reports); + } + // } diff --git a/lib/Listener/ConnectorReportListener.php b/lib/Listener/ConnectorReportListener.php deleted file mode 100644 index 485adcd09b..0000000000 --- a/lib/Listener/ConnectorReportListener.php +++ /dev/null @@ -1,88 +0,0 @@ -. - */ - -namespace Xibo\Listener; - -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Xibo\Event\ConnectorReportEvent; -use Xibo\Storage\StorageServiceInterface; - -/** - * Connector report events - */ -class ConnectorReportListener -{ - use ListenerLoggerTrait; - - /** @var \Xibo\Storage\StorageServiceInterface */ - private $storageService; - - public function __construct( - StorageServiceInterface $storageService - ) { - $this->storageService = $storageService; - } - - public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorReportListener - { - $dispatcher->addListener(ConnectorReportEvent::$NAME, [$this, 'onRequestReport']); - return $this; - } - - /** - * Get reports - * @param ConnectorReportEvent $event - * @return void - */ - public function onRequestReport(ConnectorReportEvent $event) - { - $this->getLogger()->debug('onRequestReport'); - - $connectorReports = [ - [ - 'name'=> 'campaignProofOfPlayReport', - 'description'=> 'Campaign Proof of Play', - 'class'=> '\\Xibo\\Report\\CampaignProofOfPlay', - 'type'=> 'Report', - 'output_type'=> 'table', - 'color'=> 'gray', - 'fa_icon'=> 'fa-th', - 'category'=> 'Connector Reports', - 'feature'=> 'campaign-proof-of-play', - 'adminOnly'=> 0, - ], - ]; - - $reports = []; - foreach ($connectorReports as $connectorReport) { - - // Compatibility check - if (!isset($connectorReport['feature']) || !isset($connectorReport['category'])) { - continue; - } - - $reports[$connectorReport['category']][] = (object) $connectorReport; - } - - $event->setReportObject($reports); - } -} diff --git a/lib/Middleware/ListenersMiddleware.php b/lib/Middleware/ListenersMiddleware.php index 153c73fbc5..3a76a24455 100644 --- a/lib/Middleware/ListenersMiddleware.php +++ b/lib/Middleware/ListenersMiddleware.php @@ -90,13 +90,6 @@ public static function setListeners(App $app) ->useLogger($c->get('logger')) ->registerWithDispatcher($dispatcher); - // Listen for events that affect reports - (new ConnectorReportListener( - $c->get('store') - )) - ->useLogger($c->get('logger')) - ->registerWithDispatcher($dispatcher); - // Media Delete Events $dispatcher->addListener(MediaDeleteEvent::$NAME, (new \Xibo\Listener\OnMediaDelete\MenuBoardListener( $c->get('menuBoardCategoryFactory') From 71bb41b2741ef46873807fc0a765844f3707ce15 Mon Sep 17 00:00:00 2001 From: ifarzana Date: Wed, 14 Dec 2022 12:48:57 +0000 Subject: [PATCH 03/16] api key in audience connector form --- lib/Connector/XiboAudienceReportingConnector.php | 5 +++-- views/xibo-audience-connector-form-settings.twig | 10 ++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 419d529679..4cdab4426b 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -117,7 +117,9 @@ public function getSettingsFormTwig(): string public function processSettingsForm(SanitizerInterface $params, array $settings): array { - // TODO: Implement processSettingsForm() method. + if (!$this->isProviderSetting('apiKey')) { + $settings['apiKey'] = $params->getString('apiKey'); + } return $settings; } @@ -355,7 +357,6 @@ public function onRequestConnectorReport(ConnectorReportEvent $event) $reports = []; foreach ($connectorReports as $connectorReport) { - // Compatibility check if (!isset($connectorReport['feature']) || !isset($connectorReport['category'])) { continue; diff --git a/views/xibo-audience-connector-form-settings.twig b/views/xibo-audience-connector-form-settings.twig index 77b8c7d181..8e349ee182 100644 --- a/views/xibo-audience-connector-form-settings.twig +++ b/views/xibo-audience-connector-form-settings.twig @@ -27,7 +27,13 @@ {% block callBack %}audienceFormOpen{% endblock %} {% block connectorFormFields %} -

Coming soon

-

We're working on bringing you this exciting connector in the next update.

+ {% if not interface.isProviderSetting("apiKey") %} +

Settings

+

Your API key allows for secure communication between the CMS and the Xibo audience service. It is used + to register your credentials and retrieve reports. It is never possible to retrieve credentials.

+ {% set title %}{% trans "API Key" %}{% endset %} + {% set helpText %}{% trans "Enter your API Key from Xibo." %}{% endset %} + {{ forms.input("apiKey", title, interface.getSetting("apiKey"), helpText) }} +{% endif %} {% endblock %} From 290bb3813bba564c978b18c08283f2f68b2a4313 Mon Sep 17 00:00:00 2001 From: ifarzana Date: Wed, 14 Dec 2022 13:15:44 +0000 Subject: [PATCH 04/16] Review fix --- lib/Event/ConnectorReportEvent.php | 3 +++ lib/Event/ReportDataEvent.php | 3 +++ views/xibo-audience-connector-form-settings.twig | 14 +++++++------- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/Event/ConnectorReportEvent.php b/lib/Event/ConnectorReportEvent.php index ec676d0c77..8375d75d68 100644 --- a/lib/Event/ConnectorReportEvent.php +++ b/lib/Event/ConnectorReportEvent.php @@ -21,6 +21,9 @@ */ namespace Xibo\Event; +/** + * Event used to get list of connector reports + */ class ConnectorReportEvent extends Event { public static $NAME = 'connector.report.event'; diff --git a/lib/Event/ReportDataEvent.php b/lib/Event/ReportDataEvent.php index 8268d6579a..31d970739a 100644 --- a/lib/Event/ReportDataEvent.php +++ b/lib/Event/ReportDataEvent.php @@ -21,6 +21,9 @@ */ namespace Xibo\Event; +/** + * Event used to get report results + */ class ReportDataEvent extends Event { public static $NAME = 'audience.report.data.event'; diff --git a/views/xibo-audience-connector-form-settings.twig b/views/xibo-audience-connector-form-settings.twig index 8e349ee182..862ab555d4 100644 --- a/views/xibo-audience-connector-form-settings.twig +++ b/views/xibo-audience-connector-form-settings.twig @@ -28,12 +28,12 @@ {% block connectorFormFields %} {% if not interface.isProviderSetting("apiKey") %} -

Settings

-

Your API key allows for secure communication between the CMS and the Xibo audience service. It is used - to register your credentials and retrieve reports. It is never possible to retrieve credentials.

+

{% trans "Settings" %}

+

{% trans "Your API key allows for secure communication between the CMS and the Xibo audience service. It is used + to analyse your proof of play data for Ad Campaigns and retrieve reports. It is never possible to retrieve credentials." %}

- {% set title %}{% trans "API Key" %}{% endset %} - {% set helpText %}{% trans "Enter your API Key from Xibo." %}{% endset %} - {{ forms.input("apiKey", title, interface.getSetting("apiKey"), helpText) }} -{% endif %} + {% set title %}{% trans "API Key" %}{% endset %} + {% set helpText %}{% trans "Enter your API Key from Xibo." %}{% endset %} + {{ forms.input("apiKey", title, interface.getSetting("apiKey"), helpText) }} + {% endif %} {% endblock %} From 42ec0b6142df0b58dd581e062cd7442bad88bf5d Mon Sep 17 00:00:00 2001 From: ifarzana Date: Wed, 14 Dec 2022 16:18:28 +0000 Subject: [PATCH 05/16] Review fix --- lib/Event/ConnectorReportEvent.php | 2 +- lib/Middleware/ListenersMiddleware.php | 2 -- lib/Service/ReportService.php | 5 ++++- views/xibo-audience-connector-form-settings.twig | 3 +-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/Event/ConnectorReportEvent.php b/lib/Event/ConnectorReportEvent.php index 8375d75d68..0f5a20ed38 100644 --- a/lib/Event/ConnectorReportEvent.php +++ b/lib/Event/ConnectorReportEvent.php @@ -29,7 +29,7 @@ class ConnectorReportEvent extends Event public static $NAME = 'connector.report.event'; /** @var array */ - private $reports; + private $reports = []; public function getReports() { diff --git a/lib/Middleware/ListenersMiddleware.php b/lib/Middleware/ListenersMiddleware.php index 3a76a24455..3daae92c45 100644 --- a/lib/Middleware/ListenersMiddleware.php +++ b/lib/Middleware/ListenersMiddleware.php @@ -38,8 +38,6 @@ use Xibo\Event\SystemUserChangedEvent; use Xibo\Event\UserDeleteEvent; use Xibo\Listener\CampaignListener; -use Xibo\Event\ConnectorReportEvent; -use Xibo\Listener\ConnectorReportListener; /** * This middleware is used to register listeners against the dispatcher diff --git a/lib/Service/ReportService.php b/lib/Service/ReportService.php index 7701ef589e..c35b9efc1d 100644 --- a/lib/Service/ReportService.php +++ b/lib/Service/ReportService.php @@ -148,9 +148,12 @@ public function listReports() // Get reports that are allowed by connectors $event = new ConnectorReportEvent(); $this->getDispatcher()->dispatch($event, ConnectorReportEvent::$NAME); + $connectorReports = $event->getReports(); // Merge built in reports and connector reports - $reports = array_merge($reports, $event->getReports()); + if (count($connectorReports) > 0) { + $reports = array_merge($reports, $connectorReports); + } foreach ($reports as $k => $report) { usort($report, function ($a, $b) { diff --git a/views/xibo-audience-connector-form-settings.twig b/views/xibo-audience-connector-form-settings.twig index 862ab555d4..8d58eb12f5 100644 --- a/views/xibo-audience-connector-form-settings.twig +++ b/views/xibo-audience-connector-form-settings.twig @@ -29,8 +29,7 @@ {% if not interface.isProviderSetting("apiKey") %}

{% trans "Settings" %}

-

{% trans "Your API key allows for secure communication between the CMS and the Xibo audience service. It is used - to analyse your proof of play data for Ad Campaigns and retrieve reports. It is never possible to retrieve credentials." %}

+

{% trans "Your API key allows for secure communication between the CMS and the Xibo audience service. It is used to analyse your proof of play data for Ad Campaigns and retrieve reports. It is never possible to retrieve credentials." %}

{% set title %}{% trans "API Key" %}{% endset %} {% set helpText %}{% trans "Enter your API Key from Xibo." %}{% endset %} From c72adafb8bb595acdd56309955045c4445d6284d Mon Sep 17 00:00:00 2001 From: ifarzana Date: Wed, 14 Dec 2022 16:21:07 +0000 Subject: [PATCH 06/16] Tweak --- lib/Connector/XiboAudienceReportingConnector.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 4cdab4426b..5b90a73248 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -372,7 +372,9 @@ public function onRequestConnectorReport(ConnectorReportEvent $event) $reports[$connectorReport['category']][] = (object) $connectorReport; } - $event->setReportObject($reports); + if (count($reports) > 0) { + $event->setReportObject($reports); + } } From 1e820b7d0567f040554bba97229b0cdf0a85c6bc Mon Sep 17 00:00:00 2001 From: ifarzana Date: Fri, 16 Dec 2022 11:30:03 +0000 Subject: [PATCH 07/16] Add the Campaign column in the Saved Report https://github.com/xibosignage/xibo/issues/2965 --- reports/proofofplay-email-template.twig | 2 ++ reports/proofofplay-report-preview.twig | 2 ++ 2 files changed, 4 insertions(+) diff --git a/reports/proofofplay-email-template.twig b/reports/proofofplay-email-template.twig index 49abe9da41..8e3dd6e12e 100644 --- a/reports/proofofplay-email-template.twig +++ b/reports/proofofplay-email-template.twig @@ -33,6 +33,7 @@ {% trans "Type" %} {% trans "Display ID" %} {% trans "Display" %} + {% trans "Campaign" %} {% trans "Layout ID" %} {% trans "Layout" %} {% trans "Widget ID" %} @@ -48,6 +49,7 @@ {{ item.type }} {{ item.displayId }} {{ item.display }} + {{ item.parentCampaign }} {{ item.layoutId }} {{ item.layout }} {{ item.widgetId }} diff --git a/reports/proofofplay-report-preview.twig b/reports/proofofplay-report-preview.twig index 66b7c59769..0cfed44684 100644 --- a/reports/proofofplay-report-preview.twig +++ b/reports/proofofplay-report-preview.twig @@ -51,6 +51,7 @@ {% trans "Type" %} {% trans "Display ID" %} {% trans "Display" %} + {% trans "Campaign" %} {% trans "Layout ID" %} {% trans "Layout" %} {% trans "Widget ID" %} @@ -94,6 +95,7 @@ { "data": 'type' }, { "data": 'displayId' }, { "data": 'display' }, + {"data": "parentCampaign"}, { "data": 'layoutId' }, { "data": 'layout' }, { "data": 'widgetId' }, From 2346357bf026016f41713bf75b7605f5e4047ad1 Mon Sep 17 00:00:00 2001 From: ifarzana Date: Mon, 19 Dec 2022 11:41:24 +0000 Subject: [PATCH 08/16] Review reports and add reports for connector --- .../XiboAudienceReportingConnector.php | 116 ++++++++++++++++-- lib/Event/ConnectorReportEvent.php | 4 +- lib/Report/CampaignProofOfPlay.php | 23 +--- reports/campaign-proofofplay-report-form.twig | 3 +- reports/proofofplay-report-form.twig | 2 +- views/report-selector.twig | 4 +- 6 files changed, 116 insertions(+), 36 deletions(-) diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 5b90a73248..5f07408265 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -75,8 +75,8 @@ public function setFactories(ContainerInterface $container): ConnectorInterface public function registerWithDispatcher(EventDispatcherInterface $dispatcher): ConnectorInterface { $dispatcher->addListener(MaintenanceRegularEvent::$NAME, [$this, 'onRegularMaintenance']); - $dispatcher->addListener(ReportDataEvent::$NAME, [$this, 'onAudienceReport']); - $dispatcher->addListener(ConnectorReportEvent::$NAME, [$this, 'onRequestConnectorReport']); + $dispatcher->addListener(ReportDataEvent::$NAME, [$this, 'onRequestReportData']); + $dispatcher->addListener(ConnectorReportEvent::$NAME, [$this, 'onListReports']); return $this; } @@ -288,21 +288,44 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) } /** + * Request Report results from the audience report service * @throws GeneralException */ - public function onAudienceReport(ReportDataEvent $event) + public function onRequestReportData(ReportDataEvent $event) { + $this->getLogger()->debug('onRequestReportData'); + $type = $event->getReportType(); $typeUrl = [ - 'proofofplay' => $this->getServiceUrl() . '/campaign/proofofplay' + 'campaignProofofplay' => $this->getServiceUrl() . '/campaign/proofofplay', + 'mobileProofofplay' => $this->getServiceUrl() . '/campaign/proofofplay/mobile', + 'displayAdplays' => $this->getServiceUrl() . '/display/adplays' ]; if (array_key_exists($type, $typeUrl)) { $json = []; switch ($type) { - case 'proofofplay': - // Get audience proofofplay result + case 'campaignProofofplay': + // Get campaign proofofplay result + try { + $response = $this->getClient()->get($typeUrl[$type], [ + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey') + ], + 'query' => $event->getParams() + ]); + + $body = $response->getBody()->getContents(); + $json = json_decode($body, true); + } catch (RequestException $requestException) { + $this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage()); + $error = 'Failed to get campaign proofofplay result: '.$requestException->getMessage(); + } + break; + + case 'mobileProofofplay': + // Get mobile proofofplay result try { $response = $this->getClient()->get($typeUrl[$type], [ 'headers' => [ @@ -315,7 +338,25 @@ public function onAudienceReport(ReportDataEvent $event) $json = json_decode($body, true); } catch (RequestException $requestException) { $this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage()); - $error = 'Failed to get campaign proofofplay result: '.$requestException->getMessage(); + $error = 'Failed to get mobile proofofplay result: '.$requestException->getMessage(); + } + break; + + case 'displayAdplays': + // Get display adplays result + try { + $response = $this->getClient()->get($typeUrl[$type], [ + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey') + ], + 'query' => $event->getParams() + ]); + + $body = $response->getBody()->getContents(); + $json = json_decode($body, true); + } catch (RequestException $requestException) { + $this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage()); + $error = 'Failed to get display adplays result: '.$requestException->getMessage(); } break; @@ -335,9 +376,9 @@ public function onAudienceReport(ReportDataEvent $event) * @param ConnectorReportEvent $event * @return void */ - public function onRequestConnectorReport(ConnectorReportEvent $event) + public function onListReports(ConnectorReportEvent $event) { - $this->getLogger()->debug('onRequestConnectorReport'); + $this->getLogger()->debug('onListReports'); $connectorReports = [ [ @@ -351,8 +392,60 @@ public function onRequestConnectorReport(ConnectorReportEvent $event) 'category'=> 'Connector Reports', 'feature'=> 'campaign-proof-of-play', 'adminOnly'=> 0, - 'sort_order' => 3 + 'sort_order' => 1 ], +// [ +// 'name'=> 'mobileProofOfPlayReport', +// 'description'=> 'Mobile Proof of Play', +// 'class'=> '\\Xibo\\Report\\MobileProofOfPlay', +// 'type'=> 'Report', +// 'output_type'=> 'table', +// 'color'=> 'green', +// 'fa_icon'=> 'fa-th', +// 'category'=> 'Connector Reports', +// 'feature'=> 'mobile-proof-of-play', +// 'adminOnly'=> 0, +// 'sort_order' => 2 +// ], +// [ +// 'name'=> 'displayPlayedPercentageReport', +// 'description'=> 'Display played percentage', +// 'class'=> '\\Xibo\\Report\\DisplayPlayedPercentage', +// 'type'=> 'Report', +// 'output_type'=> 'table', +// 'color'=> 'green', +// 'fa_icon'=> 'fa-th', +// 'category'=> 'Connector Reports', +// 'feature'=> 'display-report', +// 'adminOnly'=> 0, +// 'sort_order' => 3 +// ], +// [ +// 'name'=> 'revenueByDisplayReport', +// 'description'=> 'Revenue by Display', +// 'class'=> '\\Xibo\\Report\\RevenueByDisplay', +// 'type'=> 'Report', +// 'output_type'=> 'table', +// 'color'=> 'green', +// 'fa_icon'=> 'fa-th', +// 'category'=> 'Connector Reports', +// 'feature'=> 'display-report', +// 'adminOnly'=> 0, +// 'sort_order' => 4 +// ], +// [ +// 'name'=> 'adPlaysReport', +// 'description'=> 'Ad Plays', +// 'class'=> '\\Xibo\\Report\\AdPlay', +// 'type'=> 'Report', +// 'output_type'=> 'table', +// 'color'=> 'green', +// 'fa_icon'=> 'fa-th', +// 'category'=> 'Connector Reports', +// 'feature'=> 'display-report', +// 'adminOnly'=> 0, +// 'sort_order' => 5 +// ], ]; $reports = []; @@ -373,10 +466,9 @@ public function onRequestConnectorReport(ConnectorReportEvent $event) } if (count($reports) > 0) { - $event->setReportObject($reports); + $event->addReports($reports); } } - // } diff --git a/lib/Event/ConnectorReportEvent.php b/lib/Event/ConnectorReportEvent.php index 0f5a20ed38..4e56ed1511 100644 --- a/lib/Event/ConnectorReportEvent.php +++ b/lib/Event/ConnectorReportEvent.php @@ -36,9 +36,9 @@ public function getReports() return $this->reports; } - public function setReportObject($reports) + public function addReports($reports) { - $this->reports = $reports; + $this->reports = array_merge_recursive($this->reports, $reports); return $this; } diff --git a/lib/Report/CampaignProofOfPlay.php b/lib/Report/CampaignProofOfPlay.php index a4932602b9..77e610b937 100644 --- a/lib/Report/CampaignProofOfPlay.php +++ b/lib/Report/CampaignProofOfPlay.php @@ -61,11 +61,6 @@ class CampaignProofOfPlay implements ReportInterface */ private $displayFactory; - /** - * @var MediaFactory - */ - private $mediaFactory; - /** * @var LayoutFactory */ @@ -91,21 +86,11 @@ class CampaignProofOfPlay implements ReportInterface */ private $state; - private $table = 'stat'; - - private $tagsType = [ - 'dg' => 'Display group', - 'media' => 'Media', - 'layout' => 'Layout' - ]; - /** @inheritdoc */ public function setFactories(ContainerInterface $container) { $this->campaignFactory = $container->get('campaignFactory'); $this->displayFactory = $container->get('displayFactory'); - $this->mediaFactory = $container->get('mediaFactory'); - $this->layoutFactory = $container->get('layoutFactory'); $this->reportScheduleFactory = $container->get('reportScheduleFactory'); $this->sanitizer = $container->get('sanitizerService'); $this->dispatcher = $container->get('dispatcher'); @@ -130,7 +115,7 @@ public function getReportForm() return new ReportForm( 'campaign-proofofplay-report-form', 'campaignProofOfPlay', - 'Campaign Proof of Play', + 'Connector Reports', [ 'fromDateOneDay' => Carbon::now()->subSeconds(86400)->format(DateFormatHelper::getSystemFormat()), 'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat()) @@ -220,6 +205,10 @@ public function restructureSavedReportOldJson($result) /** @inheritdoc */ public function getSavedReportResults($json, $savedReport) { + // Get filter criteria + $rs = $this->reportScheduleFactory->getById($savedReport->reportScheduleId, 1)->filterCriteria; + $filterCriteria = json_decode($rs, true); + // Show filter criteria $metadata = []; @@ -345,7 +334,7 @@ public function getResults(SanitizerInterface $sanitizedParams) // -------- // ReportDataEvent - $event = new ReportDataEvent('proofofplay'); + $event = new ReportDataEvent('campaignProofofplay'); // Set query params for audience proof of play report $event->setParams($params); diff --git a/reports/campaign-proofofplay-report-form.twig b/reports/campaign-proofofplay-report-form.twig index 8839d3c49a..2b8bdfcca8 100644 --- a/reports/campaign-proofofplay-report-form.twig +++ b/reports/campaign-proofofplay-report-form.twig @@ -40,7 +40,7 @@ {% include "report-selector.twig" %}
-
+
@@ -192,7 +192,6 @@ {% block javaScript %} +{% endblock %} \ No newline at end of file diff --git a/reports/mobile-proofofplay-report-preview.twig b/reports/mobile-proofofplay-report-preview.twig new file mode 100644 index 0000000000..90c01431e7 --- /dev/null +++ b/reports/mobile-proofofplay-report-preview.twig @@ -0,0 +1,117 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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 }}
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
StartEndDisplay IdDisplayLayout IdLayoutStart LatitudeStart LongitudeEnd LatitudeEnd LongitudeDuration
+
+
+
+
+ +{% endblock %} + +{% block javaScript %} + +{% endblock %} \ No newline at end of file diff --git a/reports/mobile-proofofplay-schedule-form-add.twig b/reports/mobile-proofofplay-schedule-form-add.twig new file mode 100644 index 0000000000..ec3db3fecf --- /dev/null +++ b/reports/mobile-proofofplay-schedule-form-add.twig @@ -0,0 +1,100 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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 %}reportScheduleCallback{% 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 "Filter" %}{% endset %} + {% set helpText %}{% trans "Select the report filter you would like 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-default-values", value: displayId }, + { 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 "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 From 040cf2e7e759e51a67ab42e13232f632ca38f6de Mon Sep 17 00:00:00 2001 From: ifarzana Date: Tue, 20 Dec 2022 14:52:24 +0000 Subject: [PATCH 10/16] Display Ad play --- .../XiboAudienceReportingConnector.php | 56 +- lib/Report/DisplayAdPlay.php | 449 ++++++++++++++++ lib/Report/MobileProofOfPlay.php | 5 +- .../campaign-proofofplay-email-template.twig | 48 ++ reports/campaign-proofofplay-report-form.twig | 27 +- reports/display-adplays-report-form.twig | 503 ++++++++++++++++++ reports/display-adplays-report-preview.twig | 98 ++++ .../display-adplays-schedule-form-add.twig | 100 ++++ .../mobile-proofofplay-email-template.twig | 62 +++ reports/mobile-proofofplay-report-form.twig | 41 +- .../mobile-proofofplay-report-preview.twig | 22 +- 11 files changed, 1341 insertions(+), 70 deletions(-) create mode 100644 lib/Report/DisplayAdPlay.php create mode 100644 reports/campaign-proofofplay-email-template.twig create mode 100644 reports/display-adplays-report-form.twig create mode 100644 reports/display-adplays-report-preview.twig create mode 100644 reports/display-adplays-schedule-form-add.twig create mode 100644 reports/mobile-proofofplay-email-template.twig diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 5f07408265..957ecd2e98 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -342,7 +342,7 @@ public function onRequestReportData(ReportDataEvent $event) } break; - case 'displayAdplays': + case 'displayAdPlay': // Get display adplays result try { $response = $this->getClient()->get($typeUrl[$type], [ @@ -382,7 +382,7 @@ public function onListReports(ConnectorReportEvent $event) $connectorReports = [ [ - 'name'=> 'campaignProofOfPlayReport', + 'name'=> 'campaignProofOfPlay', 'description'=> 'Campaign Proof of Play', 'class'=> '\\Xibo\\Report\\CampaignProofOfPlay', 'type'=> 'Report', @@ -394,19 +394,19 @@ public function onListReports(ConnectorReportEvent $event) 'adminOnly'=> 0, 'sort_order' => 1 ], -// [ -// 'name'=> 'mobileProofOfPlayReport', -// 'description'=> 'Mobile Proof of Play', -// 'class'=> '\\Xibo\\Report\\MobileProofOfPlay', -// 'type'=> 'Report', -// 'output_type'=> 'table', -// 'color'=> 'green', -// 'fa_icon'=> 'fa-th', -// 'category'=> 'Connector Reports', -// 'feature'=> 'mobile-proof-of-play', -// 'adminOnly'=> 0, -// 'sort_order' => 2 -// ], + [ + 'name'=> 'mobileProofOfPlay', + 'description'=> 'Mobile Proof of Play', + 'class'=> '\\Xibo\\Report\\MobileProofOfPlay', + 'type'=> 'Report', + 'output_type'=> 'table', + 'color'=> 'green', + 'fa_icon'=> 'fa-th', + 'category'=> 'Connector Reports', + 'feature'=> 'mobile-proof-of-play', + 'adminOnly'=> 0, + 'sort_order' => 2 + ], // [ // 'name'=> 'displayPlayedPercentageReport', // 'description'=> 'Display played percentage', @@ -433,19 +433,19 @@ public function onListReports(ConnectorReportEvent $event) // 'adminOnly'=> 0, // 'sort_order' => 4 // ], -// [ -// 'name'=> 'adPlaysReport', -// 'description'=> 'Ad Plays', -// 'class'=> '\\Xibo\\Report\\AdPlay', -// 'type'=> 'Report', -// 'output_type'=> 'table', -// 'color'=> 'green', -// 'fa_icon'=> 'fa-th', -// 'category'=> 'Connector Reports', -// 'feature'=> 'display-report', -// 'adminOnly'=> 0, -// 'sort_order' => 5 -// ], + [ + 'name'=> 'displayAdPlay', + 'description'=> 'Display Ad Plays', + 'class'=> '\\Xibo\\Report\\DisplayAdPlay', + 'type'=> 'Chart', + 'output_type'=> 'both', + 'color'=> 'red', + 'fa_icon'=> 'fa-bar-chart', + 'category'=> 'Connector Reports', + 'feature'=> 'display-report', + 'adminOnly'=> 0, + 'sort_order' => 5 + ], ]; $reports = []; diff --git a/lib/Report/DisplayAdPlay.php b/lib/Report/DisplayAdPlay.php new file mode 100644 index 0000000000..24290bf785 --- /dev/null +++ b/lib/Report/DisplayAdPlay.php @@ -0,0 +1,449 @@ +. + */ +namespace Xibo\Report; + +use Carbon\Carbon; +use MongoDB\BSON\UTCDateTime; +use Psr\Container\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Xibo\Controller\DataTablesDotNetTrait; +use Xibo\Entity\ReportForm; +use Xibo\Entity\ReportResult; +use Xibo\Entity\ReportSchedule; +use Xibo\Event\ReportDataEvent; +use Xibo\Factory\CampaignFactory; +use Xibo\Factory\DisplayFactory; +use Xibo\Factory\LayoutFactory; +use Xibo\Factory\ReportScheduleFactory; +use Xibo\Helper\ApplicationState; +use Xibo\Helper\DateFormatHelper; +use Xibo\Helper\SanitizerService; +use Xibo\Helper\Translate; +use Xibo\Support\Exception\GeneralException; +use Xibo\Support\Exception\NotFoundException; +use Xibo\Support\Sanitizer\SanitizerInterface; + +/** + * Class DisplayAdPlay + * @package Xibo\Report + */ +class DisplayAdPlay implements ReportInterface +{ + use ReportDefaultTrait, DataTablesDotNetTrait; + + /** + * @var CampaignFactory + */ + private $campaignFactory; + + /** + * @var DisplayFactory + */ + private $displayFactory; + + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * @var ReportScheduleFactory + */ + private $reportScheduleFactory; + + /** + * @var SanitizerService + */ + private $sanitizer; + + /** + * @var EventDispatcher + */ + private $dispatcher; + + /** + * @var ApplicationState + */ + private $state; + + /** @inheritdoc */ + public function setFactories(ContainerInterface $container) + { + $this->campaignFactory = $container->get('campaignFactory'); + $this->displayFactory = $container->get('displayFactory'); + $this->reportScheduleFactory = $container->get('reportScheduleFactory'); + $this->sanitizer = $container->get('sanitizerService'); + $this->dispatcher = $container->get('dispatcher'); + return $this; + } + + /** @inheritdoc */ + public function getReportChartScript($results) + { + return json_encode($results->chart); + } + + /** @inheritdoc */ + public function getReportEmailTemplate() + { + return 'display-adplays-email-template.twig'; + } + + /** @inheritdoc */ + public function getSavedReportTemplate() + { + return 'display-adplays-report-preview'; + } + + /** @inheritdoc */ + public function getReportForm() + { + return new ReportForm( + 'display-adplays-report-form', + 'displayAdPlay', + 'Connector Reports', + [ + 'fromDateOneDay' => Carbon::now()->subSeconds(86400)->format(DateFormatHelper::getSystemFormat()), + 'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat()), + ], + __('Select a display') + ); + } + + /** @inheritdoc */ + public function getReportScheduleFormData(SanitizerInterface $sanitizedParams) + { + $data = []; + + $data['hiddenFields'] = ''; + $data['reportName'] = 'displayAdPlay'; + + return [ + 'template' => 'display-adplays-schedule-form-add', + 'data' => $data + ]; + } + + /** @inheritdoc */ + public function setReportScheduleFormData(SanitizerInterface $sanitizedParams) + { + $filter = $sanitizedParams->getString('filter'); + $filterCriteria = [ + 'filter' => $filter, + 'displayId' => $sanitizedParams->getInt('displayId'), + 'displayIds' => $sanitizedParams->getIntArray('displayIds'), + ]; + + $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'; + $filterCriteria['groupByFilter'] = 'byweek'; + } elseif ($filter == 'yearly') { + $schedule = ReportSchedule::$SCHEDULE_YEARLY; + $filterCriteria['reportFilter'] = 'lastyear'; + $filterCriteria['groupByFilter'] = 'bymonth'; + } + + $filterCriteria['sendEmail'] = $sanitizedParams->getCheckbox('sendEmail'); + $filterCriteria['nonusers'] = $sanitizedParams->getString('nonusers'); + + // Return + return [ + 'filterCriteria' => json_encode($filterCriteria), + 'schedule' => $schedule + ]; + } + + /** @inheritdoc */ + public function generateSavedReportName(SanitizerInterface $sanitizedParams) + { + $saveAs = sprintf(__('%s report for ', ucfirst($sanitizedParams->getString('filter')))); + + $displayId = $sanitizedParams->getInt('displayId'); + if (!empty($displayId)) { + // Get display + try { + $displayName = $this->displayFactory->getById($displayId)->display; + $saveAs .= '(Display: '. $displayName . ')'; + } catch (NotFoundException $error) { + $saveAs .= '(DisplayId: Not Found )'; + } + } + + return $saveAs; + } + + /** @inheritdoc */ + public function restructureSavedReportOldJson($result) + { + return [ + 'periodStart' => $result['periodStart'], + 'periodEnd' => $result['periodEnd'], + 'table' => $result['result'], + ]; + } + + /** @inheritdoc */ + public function getSavedReportResults($json, $savedReport) + { + // Get filter criteria + $rs = $this->reportScheduleFactory->getById($savedReport->reportScheduleId, 1)->filterCriteria; + $filterCriteria = json_decode($rs, true); + + // Show filter criteria + $metadata = []; + + // Get Meta data + $metadata['periodStart'] = $json['metadata']['periodStart']; + $metadata['periodEnd'] = $json['metadata']['periodEnd']; + $metadata['generatedOn'] = Carbon::createFromTimestamp($savedReport->generatedOn) + ->format(DateFormatHelper::getSystemFormat()); + $metadata['title'] = $savedReport->saveAs; + + // Report result object + return new ReportResult( + $metadata, + $json['table'], + $json['recordsTotal'], + $json['chart'] + ); + } + + /** @inheritDoc */ + public function getResults(SanitizerInterface $sanitizedParams) + { + // Display filter. + try { + // Get an array of display id this user has access to. + $displayIds = $this->getDisplayIdFilter($sanitizedParams); + } catch (GeneralException $exception) { + // stop the query + return new ReportResult(); + } + + // + // 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) { + case 'today': + $fromDt = $now->copy()->startOfDay(); + $toDt = $fromDt->copy()->addDay(); + break; + + case 'yesterday': + $fromDt = $now->copy()->startOfDay()->subDay(); + $toDt = $now->copy()->startOfDay(); + break; + + case 'thisweek': + $fromDt = $now->copy()->locale(Translate::GetLocale())->startOfWeek(); + $toDt = $fromDt->copy()->addWeek(); + break; + + case 'thismonth': + $fromDt = $now->copy()->startOfMonth(); + $toDt = $fromDt->copy()->addMonth(); + break; + + case 'thisyear': + $fromDt = $now->copy()->startOfYear(); + $toDt = $fromDt->copy()->addYear(); + 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('statsFromDt', ['default' => Carbon::now()->subDay()]); + $fromDt->startOfDay(); + + $toDt = $sanitizedParams->getDate('statsToDt', ['default' => Carbon::now()]); + $toDt->endOfDay(); + + // What if the fromdt and todt are exactly the same? + // in this case assume an entire day from midnight on the fromdt to midnight on the todt (i.e. add a day to the todt) + if ($fromDt == $toDt) { + $toDt->addDay(); + } + + break; + } + + $params = [ + 'displayIds' => $displayIds, + 'groupBy' => $sanitizedParams->getString('groupBy') + ]; + $params['fromDt'] = $fromDt->format('Y-m-d H:i:s'); + $params['toDt'] = $toDt->format('Y-m-d H:i:s'); + + // -------- + // ReportDataEvent + $event = new ReportDataEvent('displayAdPlay'); + + // Set query params for report + $event->setParams($params); + + // Dispatch the event - listened by Audience Report Connector + $this->dispatcher->dispatch($event, ReportDataEvent::$NAME); + $results = $event->getResults(); + + $result['periodStart'] = $params['fromDt']; + $result['periodEnd'] = $params['toDt']; + + $rows = []; + $labels = []; + $adPlaysData = []; + $impressionsData = []; + $backgroundColor = []; + $borderColor = []; + + foreach ($results['json'] as $row) { + // ---- + // Build Chart data + $labels[] = $row['labelDate']; + + $backgroundColor[] = 'rgb(95, 186, 218, 0.6)'; + $borderColor[] = 'rgb(240,93,41, 0.8)'; + + $adPlays = $row['adPlays']; + $adPlaysData[] = ($adPlays == '') ? 0 : $adPlays; + + $impressions = $row['impressions']; + $impressionsData[] = ($impressions == '') ? 0 : $impressions; + + + // ---- + // Build Tabular data + $entry = []; + + $entry['labelDate'] = $row['labelDate']; + $entry['adPlays'] = $row['adPlays']; + $entry['adDuration'] = $row['adDuration']; + $entry['impressions'] = $row['impressions']; + + $rows[] = $entry; + } + + // Build Chart to pass in twig file chart.js + $chart = [ + 'type' => 'bar', + 'data' => [ + 'labels' => $labels, + 'datasets' => [ + [ + 'label' => __('Total ad plays'), + 'yAxisID' => 'AdPlay', + 'backgroundColor' => $backgroundColor, + 'data' => $adPlaysData + ], + [ + 'label' => __('Total impressions'), + 'yAxisID' => 'Impression', + 'borderColor' => $borderColor, + 'type' => 'line', + 'fill' => false, + 'data' => $impressionsData + ] + ] + ], + 'options' => [ + 'scales' => [ + 'yAxes' => [ + [ + 'id' => 'AdPlay', + 'type' => 'linear', + 'position' => 'left', + 'display' => true, + 'scaleLabel' => [ + 'display' => true, + 'labelString' => __('Ad Play(s)') + ], + 'ticks' => [ + 'beginAtZero' => true + ] + ], [ + 'id' => 'Impression', + 'type' => 'linear', + 'position' => 'right', + 'display' => true, + 'scaleLabel' => [ + 'display' => true, + 'labelString' => __('Impression(s)') + ], + 'ticks' => [ + 'beginAtZero' => true + ] + ] + ] + ] + ] + ]; + + // Set Meta data + $metadata = [ + 'periodStart' => $result['periodStart'], + 'periodEnd' => $result['periodEnd'], + ]; + + $recordsTotal = count($rows); + + // ---- + // Table Only + // Return data to build chart/table + // This will get saved to a json file when schedule runs + return new ReportResult( + $metadata, + $rows, + $recordsTotal, + $chart, + $results['error'] ?? null + ); + } +} diff --git a/lib/Report/MobileProofOfPlay.php b/lib/Report/MobileProofOfPlay.php index 28c1a7ab7a..3b6a718df8 100644 --- a/lib/Report/MobileProofOfPlay.php +++ b/lib/Report/MobileProofOfPlay.php @@ -230,6 +230,7 @@ public function getSavedReportResults($json, $savedReport) public function getResults(SanitizerInterface $sanitizedParams) { $parentCampaignId = $sanitizedParams->getInt('parentCampaignId'); + $layoutId = $sanitizedParams->getInt('layoutId'); // Get campaign if (!empty($parentCampaignId)) { @@ -316,6 +317,7 @@ public function getResults(SanitizerInterface $sanitizedParams) $params = [ 'campaignId' => $parentCampaignId, + 'layoutId' => $layoutId, 'displayIds' => $displayIds, ]; if (!empty($parentCampaignId)) { @@ -334,7 +336,7 @@ public function getResults(SanitizerInterface $sanitizedParams) // ReportDataEvent $event = new ReportDataEvent('mobileProofofplay'); - // Set query params for audience proof of play report + // Set query params for report $event->setParams($params); // Dispatch the event - listened by Audience Report Connector @@ -344,7 +346,6 @@ public function getResults(SanitizerInterface $sanitizedParams) $result['periodStart'] = $params['from']; $result['periodEnd'] = $params['to']; - // Sanitize results?? $rows = []; $displayCache = []; $layoutCache = []; diff --git a/reports/campaign-proofofplay-email-template.twig b/reports/campaign-proofofplay-email-template.twig new file mode 100644 index 0000000000..09e1ee7608 --- /dev/null +++ b/reports/campaign-proofofplay-email-template.twig @@ -0,0 +1,48 @@ +{# +/** + * Copyright (C) 2022 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 "base-report.twig" %} + +{% block content %} +
+ {% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }} +
+

+ + + + + + + + + {% for item in tableData %} + + + + + + + {% endfor %} +
{% trans "Period" %}{% trans "Ad Plays" %}{% trans "Ad Duration" %}{% trans "Audience Impressions" %}
{{ item.labelDate }}{{ item.adPlays }}{{ item.adDuration }}{{ item.impressions }}
+{% endblock %} + diff --git a/reports/campaign-proofofplay-report-form.twig b/reports/campaign-proofofplay-report-form.twig index 2b8bdfcca8..2b219cb119 100644 --- a/reports/campaign-proofofplay-report-form.twig +++ b/reports/campaign-proofofplay-report-form.twig @@ -68,6 +68,11 @@ ] %} {{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }} + {% set title %}{% trans "From Date" %}{% endset %} + {{ inline.date("statsFromDt", title, defaults.fromDateOneDay, "", "stats-from-dt", "", "") }} + + {% set title %}{% trans "To Date" %}{% endset %} + {{ inline.date("statsToDt", title, defaults.toDate, "", "stats-to-dt", "", "") }} {% set title %}{% trans "Group by" %}{% endset %} {% set hour %}{% trans "Hour" %}{% endset %} @@ -83,12 +88,6 @@ ] %} {{ inline.dropdown("groupBy", "single", title, "today", options, "name", "filter") }} - {% set title %}{% trans "From Date" %}{% endset %} - {{ inline.date("statsFromDt", title, defaults.fromDateOneDay, "", "stats-from-dt", "", "") }} - - {% set title %}{% trans "To Date" %}{% endset %} - {{ inline.date("statsToDt", title, defaults.toDate, "", "stats-to-dt", "", "") }} - {% set title %}{% trans "Display" %}{% endset %} {% set attributes = [ { name: "data-width", value: "200px" }, @@ -159,13 +158,13 @@ class="table xibo-table table-striped table-full-width" style="width: 100%" data-state-preference-name="proofOfPlayGrid" - data-url="/report/data/campaignProofOfPlayReport"> + data-url="/report/data/campaignProofOfPlay"> - Period - Ad Plays - Ad Duration - Audience Impressions + {% trans "Period" %} + {% trans "Ad Plays" %} + {% trans "Ad Duration" %} + {% trans "Audience Impressions" %} @@ -227,10 +226,6 @@ ], }); - // table.on('draw', dataTableDraw); - // table.on('processing.dt', dataTableProcessing); - // dataTableAddButtons(table, $('#stats_wrapper').find('.dataTables_buttons')); - // Get Data function getData(url) { @@ -382,7 +377,7 @@ let tags = $("#tags").val(); let exactTags = $('#exactTags').is(":checked"); - anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=campaignProofOfPlayReport" ); + anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=campaignProofOfPlay" ); }; diff --git a/reports/display-adplays-report-form.twig b/reports/display-adplays-report-form.twig new file mode 100644 index 0000000000..12dd5ff49f --- /dev/null +++ b/reports/display-adplays-report-form.twig @@ -0,0 +1,503 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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 Ad Plays" %} | {% endblock %} + +{% block actionMenu %} + {% include "report-schedule-buttons.twig" %} +{% endblock %} + +{% block pageContent %} + +
+
+ {% trans "Display Ad Plays" %} +
+ + {% include "report-selector.twig" %} + +
+
+
+
+ +
+ {% set title %}{% trans "Range" %}{% endset %} + {% set range %}{% trans "Select a range" %}{% endset %} + {% set today %}{% trans "Today" %}{% endset %} + {% set yesterday %}{% trans "Yesterday" %}{% endset %} + {% set thisweek %}{% trans "This Week" %}{% endset %} + {% set thismonth %}{% trans "This Month" %}{% endset %} + {% set thisyear %}{% trans "This Year" %}{% endset %} + {% set lastweek %}{% trans "Last Week" %}{% endset %} + {% set lastmonth %}{% trans "Last Month" %}{% endset %} + {% set lastyear %}{% trans "Last Year" %}{% endset %} + {% set options = [ + { filterName: "", reportFilter: range }, + { filterName: "today", reportFilter: today }, + { filterName: "yesterday", reportFilter: yesterday }, + { filterName: "thisweek", reportFilter: thisweek }, + { filterName: "thismonth", reportFilter: thismonth }, + { filterName: "thisyear", reportFilter: thisyear }, + { filterName: "lastweek", reportFilter: lastweek }, + { filterName: "lastmonth", reportFilter: lastmonth }, + { filterName: "lastyear", reportFilter: lastyear }, + ] %} + {{ inline.dropdown("reportFilter", "single", title, "today", options, "filterName", "reportFilter") }} + + {% set title %}{% trans "From Date" %}{% endset %} + {{ inline.date("fromDt", title, defaults.fromDateOneDay, "", "from-dt", "", "") }} + + {% set title %}{% trans "To Date" %}{% endset %} + {{ inline.date("toDt", title, defaults.toDate, "", "to-dt", "", "") }} + + {% set title %}{% trans "Group by" %}{% endset %} + {% set hour %}{% trans "Hour" %}{% endset %} + {% set day %}{% trans "Day" %}{% endset %} + {% set week %}{% trans "Week" %}{% endset %} + {% set month %}{% trans "Month" %}{% endset %} + + {% set options = [ + { name: "hour", filter: hour }, + { name: "day", filter: day }, + { name: "week", filter: week }, + { name: "month", filter: month }, + ] %} + {{ inline.dropdown("groupBy", "single", title, "today", options, "name", "filter") }} + + {% 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) }} + + {# Campaign list only. #} + {% set attributes = [ + { name: "data-search-url", value: url_for("campaign.search") }, + { name: "data-width", value: "200px" }, + { name: "data-allow-clear", value: "true" }, + { name: "data-placeholder--id", value: null }, + { name: "data-placeholder--value", value: "" }, + ] %} + + {% set title %}{% trans "Campaign" %}{% endset %} + {% set helpText %}{% trans "Please select a Campaign" %}{% endset %} + {{ inline.dropdown("parentCampaignId", "single", title, "", null, "campaignId", "campaign", "", "", "", "", "", attributes) }} + + {% set title %}{% trans "Layout" %}{% endset %} + {% set helpText %}{% trans "This field is required when the Type selected is Layout" %}{% 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("layout.search") }, + { name: "data-search-term", value: "layout" }, + { name: "data-search-term-tags", value: "tags" }, + { name: "data-id-property", value: "layoutId" }, + { name: "data-text-property", value: "layout" } + ] %} + {{ inline.dropdown("layoutId", "single", title, "", null, "layoutId", "layout", helpText, "pagedSelect layout-select", "", "l", "", attributes) }} + +
+ + {% trans "Apply" %} + + +
+
+
+
+ +
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + +
{% trans "Period" %}{% trans "Ad Plays" %}{% trans "Impressions" %}
+
+
+
+
+
+
+
+{% endblock %} + +{% block javaScript %} + +{% endblock %} \ No newline at end of file diff --git a/reports/display-adplays-report-preview.twig b/reports/display-adplays-report-preview.twig new file mode 100644 index 0000000000..49b881402b --- /dev/null +++ b/reports/display-adplays-report-preview.twig @@ -0,0 +1,98 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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 "Period" %}{% trans "Ad Plays" %}{% trans "Impressions" %}
+
+
+
+
+ +{% endblock %} + +{% block javaScript %} + +{% endblock %} \ No newline at end of file diff --git a/reports/display-adplays-schedule-form-add.twig b/reports/display-adplays-schedule-form-add.twig new file mode 100644 index 0000000000..ec3db3fecf --- /dev/null +++ b/reports/display-adplays-schedule-form-add.twig @@ -0,0 +1,100 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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 %}reportScheduleCallback{% 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 "Filter" %}{% endset %} + {% set helpText %}{% trans "Select the report filter you would like 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-default-values", value: displayId }, + { 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 "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/mobile-proofofplay-email-template.twig b/reports/mobile-proofofplay-email-template.twig new file mode 100644 index 0000000000..5276a5fbc1 --- /dev/null +++ b/reports/mobile-proofofplay-email-template.twig @@ -0,0 +1,62 @@ +{# +/** + * Copyright (C) 2022 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 "base-report.twig" %} + +{% block content %} +
+ {% trans "From" %} {{ metadata.periodStart }} {% trans "To" %} {{ metadata.periodEnd }} +
+

+ + + + + + + + + + + + + + + + {% for item in tableData %} + + + + + + + + + + + + + + {% endfor %} +
{% trans "Start" %}{% trans "End" %}{% trans "Display Id" %}{% trans "Display" %}{% trans "Layout Id" %}{% trans "Layout" %}{% trans "Start Latitude" %}{% trans "Start Longitude" %}{% trans "End Latitude" %}{% trans "End Longitude" %}{% trans "Duration" %}
{{ item.from }}{{ item.to }}{{ item.displayId }}{{ item.display }}{{ item.layoutId }}{{ item.layout }}{{ item.startLat }}{{ item.startLong }}{{ item.endLat }}{{ item.endLong }}{{ item.duration }}
+{% endblock %} + diff --git a/reports/mobile-proofofplay-report-form.twig b/reports/mobile-proofofplay-report-form.twig index 1282fd260b..d734c80d31 100644 --- a/reports/mobile-proofofplay-report-form.twig +++ b/reports/mobile-proofofplay-report-form.twig @@ -114,6 +114,21 @@ {% set helpText %}{% trans "Please select a Campaign" %}{% endset %} {{ inline.dropdown("parentCampaignId", "single", title, "", null, "campaignId", "campaign", "", "", "", "", "", attributes) }} + {% set title %}{% trans "Layout" %}{% endset %} + {% set helpText %}{% trans "This field is required when the Type selected is Layout" %}{% 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("layout.search") }, + { name: "data-search-term", value: "layout" }, + { name: "data-search-term-tags", value: "tags" }, + { name: "data-id-property", value: "layoutId" }, + { name: "data-text-property", value: "layout" } + ] %} + {{ inline.dropdown("layoutId", "single", title, "", null, "layoutId", "layout", helpText, "pagedSelect layout-select", "", "l", "", attributes) }} +
{% trans "Apply" %} @@ -144,20 +159,20 @@ class="table xibo-table table-striped table-full-width" style="width: 100%" data-state-preference-name="proofOfPlayGrid" - data-url="/report/data/mobileProofOfPlayReport"> + data-url="/report/data/mobileProofOfPlay"> - Start - End - Display Id - Display - Layout Id - Layout - Start Latitude - Start Longitude - End Latitude - End Longitude - Duration + {% trans "Start" %} + {% trans "End" %} + {% trans "Display Id" %} + {% trans "Display" %} + {% trans "Layout Id" %} + {% trans "Layout" %} + {% trans "Start Latitude" %} + {% trans "Start Longitude" %} + {% trans "End Latitude" %} + {% trans "End Longitude" %} + {% trans "Duration" %} @@ -386,7 +401,7 @@ let tags = $("#tags").val(); let exactTags = $('#exactTags').is(":checked"); - anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=mobileProofOfPlayReport" ); + anchorReportAddBtn.attr("href", "{{ url_for("reportschedule.add.form") }}?reportName=mobileProofOfPlay" ); }; diff --git a/reports/mobile-proofofplay-report-preview.twig b/reports/mobile-proofofplay-report-preview.twig index 90c01431e7..2ccbdf6b4e 100644 --- a/reports/mobile-proofofplay-report-preview.twig +++ b/reports/mobile-proofofplay-report-preview.twig @@ -48,17 +48,17 @@ - - - - - - - - - - - + + + + + + + + + + + From 7e4c53ef299cd2c62c62b77aafcc997bf8f1bfe6 Mon Sep 17 00:00:00 2001 From: ifarzana Date: Tue, 20 Dec 2022 15:29:21 +0000 Subject: [PATCH 11/16] Tweak --- lib/Connector/XiboAudienceReportingConnector.php | 4 ++-- lib/Report/DisplayAdPlay.php | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 957ecd2e98..9fa031047a 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -300,7 +300,7 @@ public function onRequestReportData(ReportDataEvent $event) $typeUrl = [ 'campaignProofofplay' => $this->getServiceUrl() . '/campaign/proofofplay', 'mobileProofofplay' => $this->getServiceUrl() . '/campaign/proofofplay/mobile', - 'displayAdplays' => $this->getServiceUrl() . '/display/adplays' + 'displayAdPlay' => $this->getServiceUrl() . '/display/adplays' ]; if (array_key_exists($type, $typeUrl)) { @@ -361,7 +361,7 @@ public function onRequestReportData(ReportDataEvent $event) break; default: - $this->getLogger()->error('Report type not found '); + $this->getLogger()->error('Connector Report not found '); } $event->setResults([ diff --git a/lib/Report/DisplayAdPlay.php b/lib/Report/DisplayAdPlay.php index 24290bf785..3ecf7b493e 100644 --- a/lib/Report/DisplayAdPlay.php +++ b/lib/Report/DisplayAdPlay.php @@ -358,7 +358,6 @@ public function getResults(SanitizerInterface $sanitizedParams) $impressions = $row['impressions']; $impressionsData[] = ($impressions == '') ? 0 : $impressions; - // ---- // Build Tabular data $entry = []; From d63637b3a040111adb01dfdd2685d909fef35055 Mon Sep 17 00:00:00 2001 From: Israt Jahan Farzana Date: Thu, 22 Dec 2022 17:06:04 +0000 Subject: [PATCH 12/16] Connector: Audience Reporting - fix for sending non ad campaign stats (#1524) Work towards: xibosignageltd/xibo-private#85 * Receive Stat takes only ad campaign stats * Workflows : update ubuntu version to 22.04 --- .github/workflows/build-container.yaml | 2 +- .github/workflows/build-cypress.yaml | 2 +- .github/workflows/build-tag.yaml | 2 +- .github/workflows/test-suite.yaml | 2 +- lib/Connector/XiboAudienceReportingConnector.php | 8 ++++++++ lib/Storage/MongoDbTimeSeriesStore.php | 6 ++++++ lib/Storage/MySqlTimeSeriesStore.php | 6 ++++++ 7 files changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-container.yaml b/.github/workflows/build-container.yaml index 41fba58f0c..6d86ed3439 100644 --- a/.github/workflows/build-container.yaml +++ b/.github/workflows/build-container.yaml @@ -12,7 +12,7 @@ jobs: build: name: Build Containers if: github.repository == 'xibosignage/xibo-cms' - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v1 diff --git a/.github/workflows/build-cypress.yaml b/.github/workflows/build-cypress.yaml index f39d1e0795..d2981dea5b 100644 --- a/.github/workflows/build-cypress.yaml +++ b/.github/workflows/build-cypress.yaml @@ -9,7 +9,7 @@ on: jobs: build: name: Build - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v1 diff --git a/.github/workflows/build-tag.yaml b/.github/workflows/build-tag.yaml index 7668168517..ff18c2c7a8 100644 --- a/.github/workflows/build-tag.yaml +++ b/.github/workflows/build-tag.yaml @@ -9,7 +9,7 @@ jobs: build: name: Build Containers if: github.repository == 'xibosignage/xibo-cms' - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v1 diff --git a/.github/workflows/test-suite.yaml b/.github/workflows/test-suite.yaml index 60bb6b8a4b..14566a6ee2 100644 --- a/.github/workflows/test-suite.yaml +++ b/.github/workflows/test-suite.yaml @@ -8,7 +8,7 @@ on: jobs: test-suite: name: Build Containers and Run Tests - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - name: Checkout uses: actions/checkout@v1 diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 9fa031047a..a5a951e4eb 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -154,6 +154,7 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) 'type' => 'layout', 'start' => 1, 'length' => 10000, + 'mustHaveParentCampaign' => true ]; if (!empty($watermark)) { @@ -192,12 +193,19 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) $entry['campaignEnd'] = $campaignCache[$parentCampaignId]['end']; } else { $parentCampaign = $this->campaignFactory->getById($parentCampaignId); + $campaignCache[$parentCampaignId]['type'] = $parentCampaign->type; + $campaignCache[$parentCampaignId]['start'] = $parentCampaign->getStartDt()->format(DateFormatHelper::getSystemFormat()); $campaignCache[$parentCampaignId]['end'] = $parentCampaign->getEndDt()->format(DateFormatHelper::getSystemFormat()); $entry['campaignStart'] = $campaignCache[$parentCampaignId]['start']; $entry['campaignEnd'] = $campaignCache[$parentCampaignId]['end']; } + // Skip list campaign stats, keep only ad campaign stats + if ($campaignCache[$parentCampaignId]['type'] !== 'ad') { + continue; + } + // -------- // Get Display // Cost per play and impressions per play diff --git a/lib/Storage/MongoDbTimeSeriesStore.php b/lib/Storage/MongoDbTimeSeriesStore.php index fae6f2e6b9..06a6d3e3b4 100644 --- a/lib/Storage/MongoDbTimeSeriesStore.php +++ b/lib/Storage/MongoDbTimeSeriesStore.php @@ -468,6 +468,7 @@ public function getStats($filterBy = []) $mediaIds = $filterBy['mediaIds'] ?? []; $campaignId = $filterBy['campaignId'] ?? null; $parentCampaignId = $filterBy['parentCampaignId'] ?? null; + $mustHaveParentCampaign = $filterBy['mustHaveParentCampaign'] ?? false; $eventTag = $filterBy['eventTag'] ?? null; // Limit @@ -569,6 +570,11 @@ public function getStats($filterBy = []) $match['$match']['parentCampaignId'] = $parentCampaignId; } + // Has Parent Campaign Filter + if ($mustHaveParentCampaign) { + $match['$match']['parentCampaignId'] = ['$exists' => true, '$ne' => 0]; + } + // Select collection $collection = $this->getClient()->selectCollection($this->config['database'], $this->table); diff --git a/lib/Storage/MySqlTimeSeriesStore.php b/lib/Storage/MySqlTimeSeriesStore.php index 7cfd866479..038d31ec5e 100644 --- a/lib/Storage/MySqlTimeSeriesStore.php +++ b/lib/Storage/MySqlTimeSeriesStore.php @@ -213,6 +213,7 @@ public function getStats($filterBy = []) $mediaIds = $filterBy['mediaIds'] ?? []; $campaignId = $filterBy['campaignId'] ?? null; $parentCampaignId = $filterBy['parentCampaignId'] ?? null; + $mustHaveParentCampaign = $filterBy['mustHaveParentCampaign'] ?? false; $eventTag = $filterBy['eventTag'] ?? null; // Tag embedding @@ -379,6 +380,11 @@ public function getStats($filterBy = []) $params['parentCampaignId'] = $parentCampaignId; } + // Has Parent Campaign Filter + if ($mustHaveParentCampaign) { + $body .= ' AND IFNULL(`stat`.parentCampaignId, 0) != 0 '; + } + // Campaign // -------- // Filter on Layouts linked to a Campaign From 023a37302c8af087db6e63b53e715cdb062a82ec Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 23 Dec 2022 13:03:46 +0000 Subject: [PATCH 13/16] Connector: Audience connector DMA forms (#1525) Build out the audience connector DMA feature with search/add/edit/delete functionality. Ready for testing. xibosignageltd/xibo-private#85 --- .../XiboAudienceReportingConnector.php | 291 ++++++++++- lib/Widget/SubPlaylist.php | 2 +- lib/routes-web.php | 7 +- ...bo-audience-connector-form-javascript.twig | 477 ++++++++++++++++++ ...xibo-audience-connector-form-settings.twig | 42 ++ 5 files changed, 808 insertions(+), 11 deletions(-) create mode 100644 views/xibo-audience-connector-form-javascript.twig diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index a5a951e4eb..03e78a1c02 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -22,19 +22,23 @@ namespace Xibo\Connector; use Carbon\Carbon; +use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Exception\ServerException; use Psr\Container\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Xibo\Entity\User; use Xibo\Event\ConnectorReportEvent; -use Xibo\Event\ReportDataEvent; use Xibo\Event\MaintenanceRegularEvent; +use Xibo\Event\ReportDataEvent; use Xibo\Factory\CampaignFactory; use Xibo\Factory\DisplayFactory; use Xibo\Helper\DateFormatHelper; use Xibo\Helper\SanitizerService; use Xibo\Storage\TimeSeriesStoreInterface; +use Xibo\Support\Exception\AccessDeniedException; use Xibo\Support\Exception\GeneralException; +use Xibo\Support\Exception\InvalidArgumentException; use Xibo\Support\Exception\NotFoundException; use Xibo\Support\Sanitizer\SanitizerInterface; @@ -97,7 +101,7 @@ public function getTitle(): string */ private function getServiceUrl(): string { - return $this->getSetting('serviceUrl', 'https://exchange.xibo-adspace.com/api/audience'); + return $this->getSetting('serviceUrl', 'https://exchange.xibo-adspace.com/api'); } public function getDescription(): string @@ -115,11 +119,20 @@ public function getSettingsFormTwig(): string return 'xibo-audience-connector-form-settings'; } + public function getSettingsFormJavaScript(): string + { + return 'xibo-audience-connector-form-javascript'; + } + public function processSettingsForm(SanitizerInterface $params, array $settings): array { if (!$this->isProviderSetting('apiKey')) { $settings['apiKey'] = $params->getString('apiKey'); } + + // Get this connector settings, etc. + $this->getOptionsFromAxe($settings['apiKey'], true); + return $settings; } @@ -138,9 +151,16 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) return; } + // Set displays on DMAs + foreach ($this->dmaSearch($this->sanitizer->getSanitizer([]))['data'] as $dma) { + if ($dma['displayGroupId'] !== null) { + $this->setDisplaysForDma($dma['_id'], $dma['displayGroupId']); + } + } + // Get Watermark try { - $response = $this->getClient()->get($this->getServiceUrl() . '/watermark', [ + $response = $this->getClient()->get($this->getServiceUrl() . '/audience/watermark', [ 'headers' => [ 'X-API-KEY' => $this->getSetting('apiKey') ], @@ -245,7 +265,7 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) $statusCode = 0; if (count($rows) > 0) { try { - $response = $this->getClient()->post($this->getServiceUrl() . '/receiveStats', [ + $response = $this->getClient()->post($this->getServiceUrl() . '/audience/receiveStats', [ 'headers' => [ 'X-API-KEY' => $this->getSetting('apiKey') ], @@ -261,7 +281,7 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) // Get Campaign Total if ($statusCode == 204) { try { - $response = $this->getClient()->get($this->getServiceUrl() . '/campaignTotal', [ + $response = $this->getClient()->get($this->getServiceUrl() . '/audience/campaignTotal', [ 'headers' => [ 'X-API-KEY' => $this->getSetting('apiKey') ], @@ -306,9 +326,9 @@ public function onRequestReportData(ReportDataEvent $event) $type = $event->getReportType(); $typeUrl = [ - 'campaignProofofplay' => $this->getServiceUrl() . '/campaign/proofofplay', - 'mobileProofofplay' => $this->getServiceUrl() . '/campaign/proofofplay/mobile', - 'displayAdPlay' => $this->getServiceUrl() . '/display/adplays' + 'campaignProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay', + 'mobileProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay/mobile', + 'displayAdplay' => $this->getServiceUrl() . '/audience/display/adplays' ]; if (array_key_exists($type, $typeUrl)) { @@ -479,4 +499,259 @@ public function onListReports(ConnectorReportEvent $event) } // + + // + + public function dmaSearch(SanitizerInterface $params): array + { + try { + $response = $this->getClient()->get($this->getServiceUrl() . '/dma', [ + 'timeout' => 120, + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey'), + ], + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if (!$body) { + throw new GeneralException(__('No response')); + } + + return [ + 'data' => $body, + 'recordsTotal' => count($body), + ]; + } catch (\Exception $e) { + $this->getLogger()->error('activity: e = ' . $e->getMessage()); + } + + return [ + 'data' => [], + 'recordsTotal' => 0, + ]; + } + + /** + * @throws \Xibo\Support\Exception\InvalidArgumentException + * @throws \Xibo\Support\Exception\GeneralException + */ + public function dmaAdd(SanitizerInterface $params): array + { + try { + $response = $this->getClient()->post($this->getServiceUrl() . '/dma', [ + 'timeout' => 120, + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey'), + ], + 'json' => [ + 'name' => $params->getString('name'), + 'costPerPlay' => $params->getDouble('costPerPlay'), + 'impressionSource' => $params->getString('impressionSource'), + 'impressionsPerPlay' => $params->getDouble('impressionsPerPlay'), + 'startDate' => $params->getString('startDate'), + 'endDate' => $params->getString('endDate'), + 'daysOfWeek' => $params->getIntArray('daysOfWeek'), + 'startTime' => $params->getString('startTime'), + 'endTime' => $params->getString('endTime'), + 'geoFence' => json_decode($params->getString('geoFence'), true), + 'priority' => $params->getInt('priority'), + 'displayGroupId' => $params->getInt('displayGroupId'), + ], + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if (!$body) { + throw new GeneralException(__('No response')); + } + + // Set the displays + $this->setDisplaysForDma($body['_id'], $params->getInt('displayGroupId')); + + return $body; + } catch (\Exception $e) { + $this->handleException($e); + } + } + + /** + * @throws \Xibo\Support\Exception\InvalidArgumentException + * @throws \Xibo\Support\Exception\GeneralException + */ + public function dmaEdit(SanitizerInterface $params): array + { + try { + $response = $this->getClient()->put($this->getServiceUrl() . '/dma/' . $params->getString('_id'), [ + 'timeout' => 120, + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey'), + ], + 'json' => [ + 'name' => $params->getString('name'), + 'costPerPlay' => $params->getDouble('costPerPlay'), + 'impressionSource' => $params->getString('impressionSource'), + 'impressionsPerPlay' => $params->getDouble('impressionsPerPlay'), + 'startDate' => $params->getString('startDate'), + 'endDate' => $params->getString('endDate'), + 'daysOfWeek' => $params->getIntArray('daysOfWeek'), + 'startTime' => $params->getString('startTime'), + 'endTime' => $params->getString('endTime'), + 'geoFence' => json_decode($params->getString('geoFence'), true), + 'priority' => $params->getInt('priority'), + 'displayGroupId' => $params->getInt('displayGroupId'), + ], + ]); + + $body = json_decode($response->getBody()->getContents(), true); + + if (!$body) { + throw new GeneralException(__('No response')); + } + + // Set the displays + $this->setDisplaysForDma($body['_id'], $params->getInt('displayGroupId')); + + return $body; + } catch (\Exception $e) { + $this->handleException($e); + } + } + + /** + * @throws \Xibo\Support\Exception\InvalidArgumentException + * @throws \Xibo\Support\Exception\GeneralException + */ + public function dmaDelete(SanitizerInterface $params) + { + try { + $this->getClient()->delete($this->getServiceUrl() . '/dma/' . $params->getString('_id'), [ + 'timeout' => 120, + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey'), + ], + ]); + + return null; + } catch (\Exception $e) { + $this->handleException($e); + } + } + + // + + /** + * @throws \Xibo\Support\Exception\InvalidArgumentException + * @throws \Xibo\Support\Exception\GeneralException + */ + public function getOptionsFromAxe($apiKey = null, $throw = false) + { + $apiKey = $apiKey ?? $this->getSetting('apiKey'); + if (empty($apiKey)) { + if ($throw) { + throw new InvalidArgumentException(__('Please provide an API key')); + } else { + return [ + 'error' => true, + 'message' => __('Please provide an API key'), + ]; + } + } + + try { + $response = $this->getClient()->get($this->getServiceUrl() . '/options', [ + 'timeout' => 120, + 'headers' => [ + 'X-API-KEY' => $apiKey, + ], + ]); + + return json_decode($response->getBody()->getContents(), true); + } catch (\Exception $e) { + try { + $this->handleException($e); + } catch (\Exception $exception) { + if ($throw) { + throw $exception; + } else { + return [ + 'error' => true, + 'message' => $exception->getMessage() ?: __('Unknown Error'), + ]; + } + } + } + } + + private function setDisplaysForDma($dmaId, $displayGroupId) + { + // Get displays + $displayIds = []; + foreach ($this->displayFactory->getByDisplayGroupId($displayGroupId) as $display) { + $displayIds[] = $display->displayId; + } + + // Make a blind call to update this DMA. + try { + $this->getClient()->post($this->getServiceUrl() . '/dma/' . $dmaId . '/displays', [ + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey') + ], + 'json' => [ + 'displays' => $displayIds, + ] + ]); + } catch (\Exception $e) { + $this->getLogger()->error('Exception updating Displays for dmaId: ' . $dmaId + . ', e: ' . $e->getMessage()); + } + } + + /** + * @param \Exception $exception + * @return void + * @throws \Xibo\Support\Exception\GeneralException + * @throws \Xibo\Support\Exception\InvalidArgumentException + */ + private function handleException($exception) + { + $this->getLogger()->debug('handleException: ' . $exception->getMessage()); + $this->getLogger()->debug('handleException: ' . $exception->getTraceAsString()); + + if ($exception instanceof ClientException) { + if ($exception->hasResponse()) { + $body = $exception->getResponse()->getBody() ?? null; + if (!empty($body)) { + $decodedBody = json_decode($body, true); + $message = $decodedBody['message'] ?? $body; + } else { + $message = __('An unknown error has occurred.'); + } + + switch ($exception->getResponse()->getStatusCode()) { + case 422: + throw new InvalidArgumentException($message); + + case 404: + throw new NotFoundException($message); + + case 401: + throw new AccessDeniedException(__('Access denied, please check your API key')); + + default: + throw new GeneralException(sprintf( + __('Unknown client exception processing your request, error code is %s'), + $exception->getResponse()->getStatusCode() + )); + } + } else { + throw new InvalidArgumentException(__('Invalid request')); + } + } else if ($exception instanceof ServerException) { + $this->getLogger()->error('handleException:' . $exception->getMessage()); + throw new GeneralException(__('There was a problem processing your request, please try again')); + } else { + throw new GeneralException(__('Unknown Error')); + } + } } diff --git a/lib/Widget/SubPlaylist.php b/lib/Widget/SubPlaylist.php index d496c127fb..92b939ed66 100644 --- a/lib/Widget/SubPlaylist.php +++ b/lib/Widget/SubPlaylist.php @@ -549,7 +549,7 @@ public function getSubPlaylistResolvedWidgets($parentWidgetId = 0): array continue; } else { // Not the first list, so we can swap over to fill mode and use the first list instead - $spotFill = 'fill'; + $playlistItem->spotFill = 'fill'; } } diff --git a/lib/routes-web.php b/lib/routes-web.php index 9419751c40..e44e619eca 100644 --- a/lib/routes-web.php +++ b/lib/routes-web.php @@ -545,8 +545,11 @@ $group->get('/connectors', ['\Xibo\Controller\Connector','grid'])->setName('connector.search'); $group->get('/connectors/form/edit/{id}', ['\Xibo\Controller\Connector','editForm']) ->setName('connector.edit.form'); - $group->get('/connectors/form/{id}/proxy/{method}', ['\Xibo\Controller\Connector', 'editFormProxy']) - ->setName('connector.edit.form.proxy'); + $group->map( + ['GET', 'POST'], + '/connectors/form/{id}/proxy/{method}', + ['\Xibo\Controller\Connector', 'editFormProxy'] + )->setName('connector.edit.form.proxy'); $group->put('/connectors/{id}', ['\Xibo\Controller\Connector','edit'])->setName('connector.edit'); })->addMiddleware(new SuperAdminAuth($app->getContainer())); diff --git a/views/xibo-audience-connector-form-javascript.twig b/views/xibo-audience-connector-form-javascript.twig new file mode 100644 index 0000000000..9350423417 --- /dev/null +++ b/views/xibo-audience-connector-form-javascript.twig @@ -0,0 +1,477 @@ +{# +/** + * Copyright (C) 2022 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 . + */ +#} +{% import "forms.twig" as forms %} + + diff --git a/views/xibo-audience-connector-form-settings.twig b/views/xibo-audience-connector-form-settings.twig index 8d58eb12f5..4453673d83 100644 --- a/views/xibo-audience-connector-form-settings.twig +++ b/views/xibo-audience-connector-form-settings.twig @@ -35,4 +35,46 @@ {% set helpText %}{% trans "Enter your API Key from Xibo." %}{% endset %} {{ forms.input("apiKey", title, interface.getSetting("apiKey"), helpText) }} {% endif %} + + {% set options = interface.getOptionsFromAxe() %} + {% if options.error %} +

{{ options.message }}

+ {% else %} + {% set numberOfAuthedDisplays = options.displays %} +

{% trans %}Your API key is authorised for {{ numberOfAuthedDisplays }} displays.{% endtrans %}

+ {% endif %} + +
+
+
+
StartEndDisplay IdDisplayLayout IdLayoutStart LatitudeStart LongitudeEnd LatitudeEnd LongitudeDuration{% trans "Start" %}{% trans "End" %}{% trans "Display Id" %}{% trans "Display" %}{% trans "Layout Id" %}{% trans "Layout" %}{% trans "Start Latitude" %}{% trans "Start Longitude" %}{% trans "End Latitude" %}{% trans "End Longitude" %}{% trans "Duration" %}
+ + + + + + + + + + + + + + + + + + + + +
{% trans "Name" %}{% trans "Cost per Play" %}{% trans "Impressions per Play" %}{% trans "Impression Source" %}{% trans "Start Date" %}{% trans "End Date" %}{% trans "Days of week" %}{% trans "Start Time" %}{% trans "End Time" %}{% trans "Is Geo?" %}{% trans "Priority" %}{% trans "Displays" %}
+
+
+
+ {% endblock %} From 22493f9b1ce5638a6458e627462c819c3df1a9fa Mon Sep 17 00:00:00 2001 From: Israt Jahan Farzana Date: Fri, 30 Dec 2022 10:22:54 +0000 Subject: [PATCH 14/16] Connector: Add Display Played Percentage Report (#1529) * Receive Stats and Display Percentage Connector Report --- .../XiboAudienceReportingConnector.php | 107 ++- lib/Report/DisplayPercentage.php | 316 ++++++++ package-lock.json | 81 +- package.json | 5 +- reports/display-percentage-report-form.twig | 700 ++++++++++++++++++ .../display-percentage-report-preview.twig | 101 +++ .../display-percentage-schedule-form-add.twig | 72 ++ ui/bundle_vendor.js | 1 + 8 files changed, 1306 insertions(+), 77 deletions(-) create mode 100644 lib/Report/DisplayPercentage.php create mode 100644 reports/display-percentage-report-form.twig create mode 100644 reports/display-percentage-report-preview.twig create mode 100644 reports/display-percentage-schedule-form-add.twig diff --git a/lib/Connector/XiboAudienceReportingConnector.php b/lib/Connector/XiboAudienceReportingConnector.php index 03e78a1c02..c5b2d8ae55 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -186,7 +186,8 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) // Array of campaigns for which we will update the total spend, impresssions, and plays $campaigns = []; - $campaignCache = []; + $adCampaignCache = []; + $listCampaignCache = []; $displayCache = []; $rows = []; @@ -200,30 +201,34 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) continue; } - $entry['parentCampaignId'] = $parentCampaignId; + if (array_key_exists($parentCampaignId, $listCampaignCache)) { + $this->getLogger()->debug('onRegularMaintenance: Campaign is a list campaign ' . $parentCampaignId); + continue; + } - // Campaign list in array - $campaigns[] = $parentCampaignId; + $entry['parentCampaignId'] = $parentCampaignId; // -------- // Get Campaign // Campaign start and end date - if (array_key_exists($parentCampaignId, $campaignCache)) { - $entry['campaignStart'] = $campaignCache[$parentCampaignId]['start']; - $entry['campaignEnd'] = $campaignCache[$parentCampaignId]['end']; + if (array_key_exists($parentCampaignId, $adCampaignCache)) { + $entry['campaignStart'] = $adCampaignCache[$parentCampaignId]['start']; + $entry['campaignEnd'] = $adCampaignCache[$parentCampaignId]['end']; } else { $parentCampaign = $this->campaignFactory->getById($parentCampaignId); - $campaignCache[$parentCampaignId]['type'] = $parentCampaign->type; - - $campaignCache[$parentCampaignId]['start'] = $parentCampaign->getStartDt()->format(DateFormatHelper::getSystemFormat()); - $campaignCache[$parentCampaignId]['end'] = $parentCampaign->getEndDt()->format(DateFormatHelper::getSystemFormat()); - $entry['campaignStart'] = $campaignCache[$parentCampaignId]['start']; - $entry['campaignEnd'] = $campaignCache[$parentCampaignId]['end']; - } + if ($parentCampaign->type == 'ad') { + $adCampaignCache[$parentCampaignId]['type'] = $parentCampaign->type; + } else { + $listCampaignCache[$parentCampaignId] = $parentCampaignId; + continue; + } - // Skip list campaign stats, keep only ad campaign stats - if ($campaignCache[$parentCampaignId]['type'] !== 'ad') { - continue; + if (!empty($parentCampaign->getStartDt()) && !empty($parentCampaign->getEndDt())) { + $adCampaignCache[$parentCampaignId]['start'] = $parentCampaign->getStartDt()->format(DateFormatHelper::getSystemFormat()); + $adCampaignCache[$parentCampaignId]['end'] = $parentCampaign->getEndDt()->format(DateFormatHelper::getSystemFormat()); + $entry['campaignStart'] = $adCampaignCache[$parentCampaignId]['start']; + $entry['campaignEnd'] = $adCampaignCache[$parentCampaignId]['end']; + } } // -------- @@ -241,12 +246,17 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) $entry['impressionsPerPlay'] = $displayCache[$displayId]['impressionsPerPlay']; } - if ($this->timeSeriesStore->getEngine() == 'mongodb') { - $start = Carbon::createFromTimestamp($row['start']->toDateTime()->format('U'))->format(DateFormatHelper::getSystemFormat()); - $end = Carbon::createFromTimestamp($row['end']->toDateTime()->format('U'))->format(DateFormatHelper::getSystemFormat()); - } else { - $start = Carbon::createFromTimestamp($row['start'])->format(DateFormatHelper::getSystemFormat()); - $end = Carbon::createFromTimestamp($row['end'])->format(DateFormatHelper::getSystemFormat()); + try { + if ($this->timeSeriesStore->getEngine() == 'mongodb') { + $start = Carbon::createFromTimestamp($row['start']->toDateTime()->format('U'))->format(DateFormatHelper::getSystemFormat()); + $end = Carbon::createFromTimestamp($row['end']->toDateTime()->format('U'))->format(DateFormatHelper::getSystemFormat()); + } else { + $start = Carbon::createFromTimestamp($row['start'])->format(DateFormatHelper::getSystemFormat()); + $end = Carbon::createFromTimestamp($row['end'])->format(DateFormatHelper::getSystemFormat()); + } + } catch (\Exception $exception) { + $this->getLogger()->debug('onRegularMaintenance: Date convert ' . $exception->getMessage()); + continue; } $entry['id'] = $resultSet->getIdFromRow($row); @@ -258,9 +268,13 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) $entry['engagements'] = $resultSet->getEngagementsFromRow($row); $rows[] = $entry; + + // Campaign list in array + $campaigns[] = $parentCampaignId; } $this->getLogger()->debug('onRegularMaintenance: Records sent: ' . count($rows) . ', Watermark: ' . $watermark); + $this->getLogger()->debug('onRegularMaintenance: Campaigns: ' . json_encode($campaigns)); $statusCode = 0; if (count($rows) > 0) { @@ -328,7 +342,8 @@ public function onRequestReportData(ReportDataEvent $event) $typeUrl = [ 'campaignProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay', 'mobileProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay/mobile', - 'displayAdplay' => $this->getServiceUrl() . '/audience/display/adplays' + 'displayAdPlay' => $this->getServiceUrl() . '/audience/display/adplays', + 'displayPercentage' => $this->getServiceUrl() . '/audience/display/percentage' ]; if (array_key_exists($type, $typeUrl)) { @@ -388,6 +403,24 @@ public function onRequestReportData(ReportDataEvent $event) } break; + case 'displayPercentage': + // Get display played percentage result + try { + $response = $this->getClient()->get($typeUrl[$type], [ + 'headers' => [ + 'X-API-KEY' => $this->getSetting('apiKey') + ], + 'query' => $event->getParams() + ]); + + $body = $response->getBody()->getContents(); + $json = json_decode($body, true); + } catch (RequestException $requestException) { + $this->getLogger()->error('Get '. $type.': failed. e = ' . $requestException->getMessage()); + $error = 'Failed to get display played percentage result: '.$requestException->getMessage(); + } + break; + default: $this->getLogger()->error('Connector Report not found '); } @@ -435,19 +468,19 @@ public function onListReports(ConnectorReportEvent $event) 'adminOnly'=> 0, 'sort_order' => 2 ], -// [ -// 'name'=> 'displayPlayedPercentageReport', -// 'description'=> 'Display played percentage', -// 'class'=> '\\Xibo\\Report\\DisplayPlayedPercentage', -// 'type'=> 'Report', -// 'output_type'=> 'table', -// 'color'=> 'green', -// 'fa_icon'=> 'fa-th', -// 'category'=> 'Connector Reports', -// 'feature'=> 'display-report', -// 'adminOnly'=> 0, -// 'sort_order' => 3 -// ], + [ + 'name'=> 'displayPercentage', + 'description'=> 'Display Played Percentage', + 'class'=> '\\Xibo\\Report\\DisplayPercentage', + 'type'=> 'Chart', + 'output_type'=> 'both', + 'color'=> 'blue', + 'fa_icon'=> 'fa-pie-chart', + 'category'=> 'Connector Reports', + 'feature'=> 'display-report', + 'adminOnly'=> 0, + 'sort_order' => 3 + ], // [ // 'name'=> 'revenueByDisplayReport', // 'description'=> 'Revenue by Display', diff --git a/lib/Report/DisplayPercentage.php b/lib/Report/DisplayPercentage.php new file mode 100644 index 0000000000..ed59dba865 --- /dev/null +++ b/lib/Report/DisplayPercentage.php @@ -0,0 +1,316 @@ +. + */ +namespace Xibo\Report; + +use Carbon\Carbon; +use MongoDB\BSON\UTCDateTime; +use Psr\Container\ContainerInterface; +use Symfony\Component\EventDispatcher\EventDispatcher; +use Xibo\Controller\DataTablesDotNetTrait; +use Xibo\Entity\ReportForm; +use Xibo\Entity\ReportResult; +use Xibo\Entity\ReportSchedule; +use Xibo\Event\ReportDataEvent; +use Xibo\Factory\CampaignFactory; +use Xibo\Factory\DisplayFactory; +use Xibo\Factory\LayoutFactory; +use Xibo\Factory\ReportScheduleFactory; +use Xibo\Helper\ApplicationState; +use Xibo\Helper\DateFormatHelper; +use Xibo\Helper\SanitizerService; +use Xibo\Helper\Translate; +use Xibo\Support\Exception\GeneralException; +use Xibo\Support\Exception\NotFoundException; +use Xibo\Support\Sanitizer\SanitizerInterface; + +/** + * Class DisplayPercentage + * @package Xibo\Report + */ +class DisplayPercentage implements ReportInterface +{ + use ReportDefaultTrait, DataTablesDotNetTrait; + + /** + * @var CampaignFactory + */ + private $campaignFactory; + + /** + * @var DisplayFactory + */ + private $displayFactory; + + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * @var ReportScheduleFactory + */ + private $reportScheduleFactory; + + /** + * @var SanitizerService + */ + private $sanitizer; + + /** + * @var EventDispatcher + */ + private $dispatcher; + + /** + * @var ApplicationState + */ + private $state; + + /** @inheritdoc */ + public function setFactories(ContainerInterface $container) + { + $this->campaignFactory = $container->get('campaignFactory'); + $this->displayFactory = $container->get('displayFactory'); + $this->reportScheduleFactory = $container->get('reportScheduleFactory'); + $this->sanitizer = $container->get('sanitizerService'); + $this->dispatcher = $container->get('dispatcher'); + return $this; + } + + /** @inheritdoc */ + public function getReportChartScript($results) + { + return json_encode($results->chart); + } + + /** @inheritdoc */ + public function getReportEmailTemplate() + { + return 'display-percentage-email-template.twig'; + } + + /** @inheritdoc */ + public function getSavedReportTemplate() + { + return 'display-percentage-report-preview'; + } + + /** @inheritdoc */ + public function getReportForm() + { + return new ReportForm( + 'display-percentage-report-form', + 'displayPercentage', + 'Connector Reports', + [ + 'fromDateOneDay' => Carbon::now()->subSeconds(86400)->format(DateFormatHelper::getSystemFormat()), + 'toDate' => Carbon::now()->format(DateFormatHelper::getSystemFormat()), + ], + __('Select a campaign') + ); + } + + /** @inheritdoc */ + public function getReportScheduleFormData(SanitizerInterface $sanitizedParams) + { + $data = []; + + $data['hiddenFields'] = json_encode([ + 'parentCampaignId' => $sanitizedParams->getInt('parentCampaignId') + ]); + $data['reportName'] = 'displayPercentage'; + + return [ + 'template' => 'display-percentage-schedule-form-add', + 'data' => $data + ]; + } + + /** @inheritdoc */ + public function setReportScheduleFormData(SanitizerInterface $sanitizedParams) + { + $filter = $sanitizedParams->getString('filter'); + $hiddenFields = json_decode($sanitizedParams->getString('hiddenFields'), true); + + $filterCriteria = [ + 'filter' => $filter, + 'parentCampaignId' => $hiddenFields['parentCampaignId'] + ]; + + $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'; + $filterCriteria['groupByFilter'] = 'byweek'; + } elseif ($filter == 'yearly') { + $schedule = ReportSchedule::$SCHEDULE_YEARLY; + $filterCriteria['reportFilter'] = 'lastyear'; + $filterCriteria['groupByFilter'] = 'bymonth'; + } + + $filterCriteria['sendEmail'] = $sanitizedParams->getCheckbox('sendEmail'); + $filterCriteria['nonusers'] = $sanitizedParams->getString('nonusers'); + + // Return + return [ + 'filterCriteria' => json_encode($filterCriteria), + 'schedule' => $schedule + ]; + } + + /** @inheritdoc */ + public function generateSavedReportName(SanitizerInterface $sanitizedParams) + { + $saveAs = sprintf(__('%s report for ', ucfirst($sanitizedParams->getString('filter')))); + + $parentCampaignId = $sanitizedParams->getInt('parentCampaignId'); + if (!empty($parentCampaignId)) { + // Get display + try { + $parentCampaignName = $this->campaignFactory->getById($parentCampaignId)->campaign; + $saveAs .= '(Campaign: '. $parentCampaignName . ')'; + } catch (NotFoundException $error) { + $saveAs .= '(Campaign: Not Found )'; + } + } + + return $saveAs; + } + + /** @inheritdoc */ + public function restructureSavedReportOldJson($result) + { + return [ + 'periodStart' => $result['periodStart'], + 'periodEnd' => $result['periodEnd'], + 'table' => $result['result'], + ]; + } + + /** @inheritdoc */ + public function getSavedReportResults($json, $savedReport) + { + // Get filter criteria + $rs = $this->reportScheduleFactory->getById($savedReport->reportScheduleId, 1)->filterCriteria; + $filterCriteria = json_decode($rs, true); + + // Show filter criteria + $metadata = []; + + // Get Meta data + $metadata['periodStart'] = $json['metadata']['periodStart']; + $metadata['periodEnd'] = $json['metadata']['periodEnd']; + $metadata['generatedOn'] = Carbon::createFromTimestamp($savedReport->generatedOn) + ->format(DateFormatHelper::getSystemFormat()); + $metadata['title'] = $savedReport->saveAs; + + // Report result object + return new ReportResult( + $metadata, + $json['table'], + $json['recordsTotal'], + $json['chart'] + ); + } + + /** @inheritDoc */ + public function getResults(SanitizerInterface $sanitizedParams) + { + $params = [ + 'parentCampaignId' => $sanitizedParams->getInt('parentCampaignId') + ]; + + // -------- + // ReportDataEvent + $event = new ReportDataEvent('displayPercentage'); + + // Set query params for report + $event->setParams($params); + + // Dispatch the event - listened by Audience Report Connector + $this->dispatcher->dispatch($event, ReportDataEvent::$NAME); + $results = $event->getResults(); + + // TODO + $result['periodStart'] = Carbon::now()->format('Y-m-d H:i:s'); + $result['periodEnd'] = Carbon::now()->format('Y-m-d H:i:s'); + + $rows = []; + $displayCache = []; + + foreach ($results['json'] as $row) { + // ---- + // Build Chart data + + // ---- + // Build Tabular data + $entry = []; + + // -------- + // Get Display + try { + if (!array_key_exists($row['displayId'], $displayCache)) { + $display = $this->displayFactory->getById($row['displayId']); + $displayCache[$row['displayId']] = $display->display; + } + $entry['label'] = $displayCache[$row['displayId']] ?? ''; + } catch (\Exception $e) { + $entry['label'] = 'Not found'; + } + + $entry['spendData'] = $row['spendData']; + $entry['playtimeDuration'] = $row['playtimeDuration']; + $entry['backgroundColor'] = '#'.substr(md5($row['displayId']), 0, 6); + + $rows[] = $entry; + } + + // Build Chart to pass in twig file chart.js + $chart = []; + + // Set Meta data + $metadata = [ + 'periodStart' => $result['periodStart'], + 'periodEnd' => $result['periodEnd'], + ]; + + $recordsTotal = count($rows); + + // ---- + // Table Only + // Return data to build chart/table + // This will get saved to a json file when schedule runs + return new ReportResult( + $metadata, + $rows, + $recordsTotal, + $chart, + $results['error'] ?? null + ); + } +} diff --git a/package-lock.json b/package-lock.json index 7f9c0cbe34..b463241746 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1164,7 +1164,7 @@ "@mapbox/leaflet-pip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@mapbox/leaflet-pip/-/leaflet-pip-1.1.0.tgz", - "integrity": "sha1-SnpL60WMjMJNhJNvylq6CV3m430=", + "integrity": "sha512-uySBUgl8Mxv4YTTf2SKcCA0XdGL9iBJOrgMUwDk6nHdKsg840X56OTJlMnlVj+8Yz8pIDRUc0BRFnRW9TD24VA==", "requires": { "geojson-utils": "~1.1.0", "uglify-js": "2.7.4" @@ -1173,12 +1173,12 @@ "async": { "version": "0.2.10", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" }, "uglify-js": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.7.4.tgz", - "integrity": "sha1-opWg3hK2plDAMcQN6w3ECxRWi9I=", + "integrity": "sha512-3fRfvQZzQlvfSO/w3VflUO9TQFd3iH8RoxyuWXI74W1Xo8SXOJB25xQKsY4VHHbdDAtjfBIUo7uH/nppXXP3/A==", "requires": { "async": "~0.2.6", "source-map": "~0.5.1", @@ -1515,7 +1515,7 @@ "align-text": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "integrity": "sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==", "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -1525,7 +1525,7 @@ "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", "requires": { "is-buffer": "^1.1.5" } @@ -1602,7 +1602,7 @@ "assert-plus": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, "astral-regex": { "version": "2.0.0", @@ -1619,7 +1619,7 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "at-least-node": { "version": "1.0.0", @@ -1685,7 +1685,7 @@ "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { "version": "1.8.0", @@ -2545,7 +2545,7 @@ "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "requires": { "tweetnacl": "^0.14.3" } @@ -2803,12 +2803,12 @@ "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "center-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "integrity": "sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==", "requires": { "align-text": "^0.1.3", "lazy-cache": "^1.0.3" @@ -2873,6 +2873,11 @@ "color-name": "^1.0.0" } }, + "chartjs-plugin-datalabels": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-1.0.0.tgz", + "integrity": "sha512-rqaAFrZT++7OoCJgRCPjugTR4kYpdmrTeX7qHTAZoNQaOaMdx4g6o0Eo9J7n3qLRQrhWjh9l731JsrbkJxL7Vg==" + }, "check-more-types": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", @@ -3806,7 +3811,7 @@ "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "requires": { "assert-plus": "^1.0.0" } @@ -3939,7 +3944,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" }, "deep-is": { "version": "0.1.3", @@ -3957,7 +3962,7 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" }, "desandro-matches-selector": { "version": "2.0.2", @@ -4018,7 +4023,7 @@ "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "requires": { "jsbn": "~0.1.0", "safer-buffer": "^2.1.0" @@ -4623,7 +4628,7 @@ "extsprintf": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "fast-deep-equal": { "version": "3.1.1", @@ -4805,7 +4810,7 @@ "forever-agent": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "form-data": { "version": "2.3.3", @@ -4891,7 +4896,7 @@ "geojson-utils": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/geojson-utils/-/geojson-utils-1.1.0.tgz", - "integrity": "sha1-6P+0yBwKdbPjBvUYcmXW8jBA9Qs=" + "integrity": "sha512-9irmjugYFrMvGjMw+qW5BQpghJO4ClFMDdPzVe2oDgtH2DhoKMaXqjIYPS5VpfrcnV0kOSrXcj8D9Izmr0AM9Q==" }, "get-intrinsic": { "version": "1.1.1", @@ -4925,7 +4930,7 @@ "getpass": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "requires": { "assert-plus": "^1.0.0" } @@ -5169,7 +5174,7 @@ "image-size": { "version": "0.5.5", "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", "optional": true }, "imagesloaded": { @@ -5519,7 +5524,7 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "is-unicode-supported": { "version": "0.1.0", @@ -5550,7 +5555,7 @@ "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "jalaali-js": { "version": "1.1.0", @@ -5622,7 +5627,7 @@ "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "jsesc": { "version": "2.5.2", @@ -5653,7 +5658,7 @@ "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "json5": { "version": "0.5.1", @@ -5712,7 +5717,7 @@ "lazy-cache": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==" }, "leaflet": { "version": "1.8.0", @@ -5903,7 +5908,7 @@ "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, "log-symbols": { @@ -6071,7 +6076,7 @@ "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + "integrity": "sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==" }, "loose-envify": { "version": "1.4.0", @@ -6342,7 +6347,7 @@ "normalize-range": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=" + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" }, "npm-run-path": { "version": "4.0.1", @@ -6355,7 +6360,7 @@ "num2fraction": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=" + "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==" }, "number-is-nan": { "version": "1.0.1", @@ -6543,7 +6548,7 @@ "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "persian-date": { "version": "0.3.1-b", @@ -7810,7 +7815,7 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==" }, "repeating": { "version": "2.0.1", @@ -7952,7 +7957,7 @@ "right-align": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "integrity": "sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==", "requires": { "align-text": "^0.1.1" } @@ -8641,7 +8646,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" }, "to-regex-range": { "version": "5.0.1", @@ -8687,7 +8692,7 @@ "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", "requires": { "safe-buffer": "^5.0.1" } @@ -8695,7 +8700,7 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "type-check": { "version": "0.4.0", @@ -8735,7 +8740,7 @@ "uglify-to-browserify": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" + "integrity": "sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==" }, "uglifyjs-webpack-plugin": { "version": "2.2.0", @@ -8878,7 +8883,7 @@ "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -9150,7 +9155,7 @@ "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + "integrity": "sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==" }, "word-wrap": { "version": "1.2.3", diff --git a/package.json b/package.json index d2bf342a58..d89076b848 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "bootstrap-switch": "3.3.4", "bootstrap-tagsinput": "~0.7.1", "chart.js": "^2.9.4", + "chartjs-plugin-datalabels": "1.0.0", "clean-webpack-plugin": "^0.1.17", "colors.js": "~1.2.4", "copy-webpack-plugin": "^6.4.0", @@ -83,10 +84,10 @@ "jstree": "^3.3.10", "leaflet": "^1.7.1", "leaflet-draw": "^1.0.4", - "leaflet-search": "^3.0.3", "leaflet-easyprint": "^2.1.9", - "leaflet.markercluster": "^1.4.1", "leaflet-fullscreen": "^1.0.2", + "leaflet-search": "^3.0.3", + "leaflet.markercluster": "^1.4.1", "less": "^3.11.1", "less-loader": "^4.1.0", "masonry-layout": "^4.2.2", diff --git a/reports/display-percentage-report-form.twig b/reports/display-percentage-report-form.twig new file mode 100644 index 0000000000..a7774db0a4 --- /dev/null +++ b/reports/display-percentage-report-form.twig @@ -0,0 +1,700 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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 Played Percentage" %} | {% endblock %} + +{% block actionMenu %} + {% include "report-schedule-buttons.twig" %} +{% endblock %} + +{% block pageContent %} + + +{% endblock %} + +{% block javaScript %} + + +{% endblock %} \ No newline at end of file diff --git a/reports/display-percentage-report-preview.twig b/reports/display-percentage-report-preview.twig new file mode 100644 index 0000000000..a889a444bf --- /dev/null +++ b/reports/display-percentage-report-preview.twig @@ -0,0 +1,101 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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" %}{% trans "Spend(%)" %}{% trans "Playtime(%)" %}
+
+
+
+
+ +{% endblock %} + +{% block javaScript %} + +{% endblock %} \ No newline at end of file diff --git a/reports/display-percentage-schedule-form-add.twig b/reports/display-percentage-schedule-form-add.twig new file mode 100644 index 0000000000..46e41454be --- /dev/null +++ b/reports/display-percentage-schedule-form-add.twig @@ -0,0 +1,72 @@ +{# +/* + * Xibo - Digital Signage - http://www.xibo.org.uk + * Copyright (C) 2022 Xibo Signage Ltd + * + * 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 %}reportScheduleCallback{% 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 "Filter" %}{% endset %} + {% set helpText %}{% trans "Select the report filter you would like 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 "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/ui/bundle_vendor.js b/ui/bundle_vendor.js index 47b7533d31..dd5eba6fad 100644 --- a/ui/bundle_vendor.js +++ b/ui/bundle_vendor.js @@ -99,6 +99,7 @@ require('colors.js'); // chart.js require('chart.js'); +window.ChartDataLabels = require('chartjs-plugin-datalabels'); // form-serializer require('form-serializer'); From 2794da68c9a1b26bbf0a6363d4b400a987c46323 Mon Sep 17 00:00:00 2001 From: Dan Garner Date: Fri, 30 Dec 2022 10:28:17 +0000 Subject: [PATCH 15/16] Ad Campaigns: adjust scheduler for xibosignage/xibo#2971 (#1528) * Ad Campaigns: adjust scheduler so that it: - stops scheduling when we reach 100% of target - stops scheduling when progress to target is 5% over - splits plays needed by the number of logged in/valid displays in the schedule - adjusts budget/impression targets based on logged in displays * Ad Campaign: only record plays on the campaign if the stat is for a layout. xibosignage/xibo#2971 --- lib/XTR/CampaignSchedulerTask.php | 89 +++++++++++++++++++------------ lib/Xmds/Soap.php | 4 +- 2 files changed, 59 insertions(+), 34 deletions(-) diff --git a/lib/XTR/CampaignSchedulerTask.php b/lib/XTR/CampaignSchedulerTask.php index 52c112fde4..a0e55b4e17 100644 --- a/lib/XTR/CampaignSchedulerTask.php +++ b/lib/XTR/CampaignSchedulerTask.php @@ -49,6 +49,9 @@ class CampaignSchedulerTask implements TaskInterface /** @var \Xibo\Factory\DisplayGroupFactory */ private $displayGroupFactory; + /** @var \Xibo\Factory\DisplayFactory */ + private $displayFactory; + /** @var \Xibo\Service\DisplayNotifyServiceInterface */ private $displayNotifyService; @@ -62,6 +65,7 @@ public function setFactories($container) $this->scheduleFactory = $container->get('scheduleFactory'); $this->dayPartFactory = $container->get('dayPartFactory'); $this->displayGroupFactory = $container->get('displayGroupFactory'); + $this->displayFactory = $container->get('displayFactory'); $this->displayNotifyService = $container->get('displayNotifyService'); return $this; } @@ -78,30 +82,35 @@ public function run() 'endDt' => $nextHour->unix(), ]); - // Do not schedule more than an hours worth of schedules. - $totalSovAvailable = 3600; - $totalSovPerDisplay = []; + // We will need to notify some displays at the end. + $notifyDisplayGroupIds = []; // See what we can schedule for each one. - $notifyDisplayGroupIds = []; foreach ($activeCampaigns as $campaign) { try { $this->log->debug('campaignSchedulerTask: active campaign found, id: ' . $campaign->campaignId); // Display groups $displayGroups = []; + $countDisplays = 0; + $costPerPlay = 0; + $impressionsPerPlay = 0; + foreach ($campaign->loadDisplayGroupIds() as $displayGroupId) { $displayGroups[] = $this->displayGroupFactory->getById($displayGroupId); - - // SoV per Display, each display starts with 3600 ($totalSovAvailable) - if (!array_key_exists($displayGroupId, $totalSovPerDisplay)) { - $totalSovPerDisplay[$displayGroupId] = $totalSovAvailable; - } - // Add to our list of displays to notify once finished. + // Record ids to notify if (!in_array($displayGroupId, $notifyDisplayGroupIds)) { $notifyDisplayGroupIds[] = $displayGroupId; } + + foreach ($this->displayFactory->getByDisplayGroupId($displayGroupId) as $display) { + if ($display->licensed === 1 && $display->loggedIn === 1) { + $countDisplays++; + $costPerPlay += $display->costPerPlay; + $impressionsPerPlay += $display->impressionsPerPlay; + } + } } $this->log->debug('campaignSchedulerTask: campaign has ' . count($displayGroups) . ' displays'); @@ -160,38 +169,58 @@ public function run() // We work out how much we should have played vs how much we have played $progress = $campaign->getProgress($nextHour->copy()); - // A simple assessment of how many plays we need to check in this hour period (we assume the campaign + // A simple assessment of how much of the target we need in this hour period (we assume the campaign // will play for 24 hours a day and that adjustments to later scheduling will solve any underplay) - $playsNeeded = $progress->targetPerDay / 24; + $targetNeededPerDay = $progress->targetPerDay / 24; + + // If we are more than 5% ahead of where we should be, or we are at 100% already, then don't + // schedule anything else + if ($progress->progressTarget >= 100) { + $this->log->debug('campaignSchedulerTask: campaign has completed, skipping'); + continue; + } else if ($progress->progressTime > 0 + && ($progress->progressTime - $progress->progressTarget + 5) <= 0 + ) { + $this->log->debug('campaignSchedulerTask: campaign is 5% or more ahead of schedule, skipping'); + continue; + } if ($progress->progressTime > 0 && $progress->progressTarget > 0) { // If we're behind, then increase our play rate accordingly - // again, this is a simple calculation $ratio = $progress->progressTime / $progress->progressTarget; - $playsNeeded = $playsNeeded * $ratio; + $targetNeededPerDay = $targetNeededPerDay * $ratio; - $this->log->debug('campaignSchedulerTask: playsNeeded is ' . $playsNeeded + $this->log->debug('campaignSchedulerTask: targetNeededPerDay is ' . $targetNeededPerDay . ', adjusted by ' . $ratio); } // Spread across the layouts - $playsNeededPerLayout = intval(ceil($playsNeeded / $countActiveLayouts)); + $targetNeededPerLayout = $targetNeededPerDay / $countActiveLayouts; + + // Modify the target depending on what units it is expressed in + // This also caters for spreading the target across the active displays because the + // cost/impressions/displays are sums. + if ($campaign->targetType === 'budget') { + $playsNeededPerLayout = $targetNeededPerLayout / $costPerPlay; + } else if ($campaign->targetType === 'impressions') { + $playsNeededPerLayout = $targetNeededPerLayout / $impressionsPerPlay; + } else { + $playsNeededPerLayout = $targetNeededPerLayout / $countDisplays; + } - $this->log->debug('campaignSchedulerTask: playsNeededPerLayout is ' . $playsNeededPerLayout); + // Take the ceiling because we can't do part plays + $playsNeededPerLayout = intval(ceil($playsNeededPerLayout)); + + $this->log->debug('campaignSchedulerTask: targetNeededPerLayout is ' . $targetNeededPerLayout + . ', targetType: ' . $campaign->targetType + . ', playsNeededPerLayout: ' . $playsNeededPerLayout + . ', there are ' . $countDisplays . ' displays.'); foreach ($activeLayouts as $layout) { // We are on an active day of the week and within an active day part // create a scheduled event for all displays assigned. // and for each geo fence - // how much time do we need to schedule? - foreach ($displayGroups as $displayGroup) { - // if any of the Displays assigned to this ad campaign went over the SoV - // we log an error, but we do allow the schedule to be created - if ($totalSovPerDisplay[$displayGroup->displayGroupId] <= 0) { - $this->log->error('campaignSchedulerTask: total SOV available has been consumed for Display ' . $displayGroup->displayGroup); - } - } - + // Create our schedule $schedule = $this->scheduleFactory->createEmpty(); $schedule->setCampaignFactory($this->campaignFactory); @@ -217,7 +246,7 @@ public function run() $schedule->syncEvent = 0; // We cap SOV at 3600 - $schedule->shareOfVoice = min($playsNeededPerLayout * $layout->duration, $totalSovAvailable); + $schedule->shareOfVoice = min($playsNeededPerLayout * $layout->duration, 3600); $schedule->maxPlaysPerHour = $playsNeededPerLayout; // Do we have a geofence? (geo schedules do not count against totalSovAvailable) @@ -241,12 +270,6 @@ public function run() $schedule->save(['notify' => false]); } } else { - // Reduce the total available on a per Displays basis - // (geo schedules do not count against totalSovAvailable) - foreach ($displayGroups as $displayGroup) { - $totalSovPerDisplay[$displayGroup->displayGroupId] -= $schedule->shareOfVoice; - } - $schedule->save(['notify' => false]); } } diff --git a/lib/Xmds/Soap.php b/lib/Xmds/Soap.php index e1e72cf708..73ea4eea67 100644 --- a/lib/Xmds/Soap.php +++ b/lib/Xmds/Soap.php @@ -1745,6 +1745,7 @@ protected function doSubmitStats($serverKey, $hardwareKey, $statXml) // Cache a count for this scheduleId $parentCampaignId = 0; $parentCampaign = null; + if ($scheduleId > 0) { // Lookup this schedule if (!array_key_exists($scheduleId, $schedules)) { @@ -1761,7 +1762,8 @@ protected function doSubmitStats($serverKey, $hardwareKey, $statXml) $campaigns[$parentCampaignId] = $this->campaignFactory->getById($parentCampaignId); } - if ($campaigns[$parentCampaignId]->type === 'ad') { + // For a layout stat we should increment the number of plays on the Campaign + if ($type === 'layout' && $campaigns[$parentCampaignId]->type === 'ad') { $parentCampaign = $campaigns[$parentCampaignId]; // spend/impressions multiplier for this display From 0ca5024af57777e1ab409eb64c023360702e92a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Gon=C3=A7alves?= Date: Mon, 2 Jan 2023 13:34:50 +0100 Subject: [PATCH 16/16] Add translation strings for the dropdown list items --- views/user-form-edit-profile.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/views/user-form-edit-profile.twig b/views/user-form-edit-profile.twig index c5538747b8..1ebeb8151c 100644 --- a/views/user-form-edit-profile.twig +++ b/views/user-form-edit-profile.twig @@ -61,7 +61,7 @@ {% set title %}{% trans "Two Factor Authentication" %}{% endset %} {% set helpText %}{% trans "Enable an option to provide a two factor authentication code to log into the CMS for added security." %}{% endset %} - {% set values = [{id: 0, value: "Off"}, {id: 1, value: "Email"}, {id: 2, value: "Google Authenticator"}] %} + {% set values = [{id: 0, value: {% trans "Off" %}}, {id: 1, value: {% trans "Email" %}}, {id: 2, value: {% trans "Google Authenticator" %}}] %} {{ forms.dropdown("twoFactorTypeId", "single", title, currentUser.twoFactorTypeId, values, "id", "value", helpText) }} {{ forms.hidden("twoFactorRecoveryCodes", currentUser.twoFactorRecoveryCodes) }} @@ -94,4 +94,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %}