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 1c8223ba03..c5b2d8ae55 100644 --- a/lib/Connector/XiboAudienceReportingConnector.php +++ b/lib/Connector/XiboAudienceReportingConnector.php @@ -22,18 +22,21 @@ 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\Event\ReportDataEvent; +use Xibo\Entity\User; +use Xibo\Event\ConnectorReportEvent; 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\StorageServiceInterface; use Xibo\Storage\TimeSeriesStoreInterface; -use Xibo\Support\Exception\DuplicateEntityException; +use Xibo\Support\Exception\AccessDeniedException; use Xibo\Support\Exception\GeneralException; use Xibo\Support\Exception\InvalidArgumentException; use Xibo\Support\Exception\NotFoundException; @@ -43,8 +46,8 @@ class XiboAudienceReportingConnector implements ConnectorInterface { use ConnectorTrait; - /** @var StorageServiceInterface */ - private $store; + /** @var User */ + private $user; /** @var TimeSeriesStoreInterface */ private $timeSeriesStore; @@ -64,7 +67,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'); @@ -76,7 +79,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(ReportDataEvent::$NAME, [$this, 'onRequestReportData']); + $dispatcher->addListener(ConnectorReportEvent::$NAME, [$this, 'onListReports']); return $this; } @@ -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,9 +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 { - // TODO: Implement processSettingsForm() method. + if (!$this->isProviderSetting('apiKey')) { + $settings['apiKey'] = $params->getString('apiKey'); + } + + // Get this connector settings, etc. + $this->getOptionsFromAxe($settings['apiKey'], true); + return $settings; } @@ -136,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') ], @@ -152,6 +174,7 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) 'type' => 'layout', 'start' => 1, 'length' => 10000, + 'mustHaveParentCampaign' => true ]; if (!empty($watermark)) { @@ -163,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 = []; @@ -177,23 +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]['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; + } + + 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']; + } } // -------- @@ -211,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); @@ -228,14 +268,18 @@ 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) { try { - $response = $this->getClient()->post($this->getServiceUrl() . '/receiveStats', [ + $response = $this->getClient()->post($this->getServiceUrl() . '/audience/receiveStats', [ 'headers' => [ 'X-API-KEY' => $this->getSetting('apiKey') ], @@ -251,7 +295,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') ], @@ -285,19 +329,64 @@ public function onRegularMaintenance(MaintenanceRegularEvent $event) } } - public function onAudienceReport(ReportDataEvent $event) + /** + * Request Report results from the audience report service + * @throws GeneralException + */ + public function onRequestReportData(ReportDataEvent $event) { + $this->getLogger()->debug('onRequestReportData'); + $type = $event->getReportType(); $typeUrl = [ - 'proofofplay' => $this->getServiceUrl() . '/campaign/proofofplay' + 'campaignProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay', + 'mobileProofofplay' => $this->getServiceUrl() . '/audience/campaign/proofofplay/mobile', + 'displayAdPlay' => $this->getServiceUrl() . '/audience/display/adplays', + 'displayPercentage' => $this->getServiceUrl() . '/audience/display/percentage' ]; 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' => [ + '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 mobile proofofplay result: '.$requestException->getMessage(); + } + break; + + case 'displayAdPlay': + // Get display adplays result try { $response = $this->getClient()->get($typeUrl[$type], [ 'headers' => [ @@ -310,17 +399,392 @@ 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 display adplays result: '.$requestException->getMessage(); + } + 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('Report type not found '); + $this->getLogger()->error('Connector Report not found '); + } + + $event->setResults([ + 'json' => $json, + 'error' => $error ?? null + ]); + } + } + + /** + * Get this connector reports + * @param ConnectorReportEvent $event + * @return void + */ + public function onListReports(ConnectorReportEvent $event) + { + $this->getLogger()->debug('onListReports'); + + $connectorReports = [ + [ + 'name'=> 'campaignProofOfPlay', + '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' => 1 + ], + [ + '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'=> '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', +// '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'=> '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 = []; + 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; + } + + if (count($reports) > 0) { + $event->addReports($reports); + } + } + + // + + // + + 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')); } - $event->setResults($json); + 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/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..4e56ed1511 --- /dev/null +++ b/lib/Event/ConnectorReportEvent.php @@ -0,0 +1,45 @@ +. + */ +namespace Xibo\Event; + +/** + * Event used to get list of connector reports + */ +class ConnectorReportEvent extends Event +{ + public static $NAME = 'connector.report.event'; + + /** @var array */ + private $reports = []; + + public function getReports() + { + return $this->reports; + } + + public function addReports($reports) + { + $this->reports = array_merge_recursive($this->reports, $reports); + + return $this; + } +} 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/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..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,23 +334,21 @@ 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); // 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 +374,9 @@ public function getResults(SanitizerInterface $sanitizedParams) return new ReportResult( $metadata, $rows, - $recordsTotal + $recordsTotal, + [], + $results['error'] ?? null ); } } diff --git a/lib/Report/DisplayAdPlay.php b/lib/Report/DisplayAdPlay.php new file mode 100644 index 0000000000..3ecf7b493e --- /dev/null +++ b/lib/Report/DisplayAdPlay.php @@ -0,0 +1,448 @@ +. + */ +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/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/lib/Report/MobileProofOfPlay.php b/lib/Report/MobileProofOfPlay.php new file mode 100644 index 0000000000..3b6a718df8 --- /dev/null +++ b/lib/Report/MobileProofOfPlay.php @@ -0,0 +1,409 @@ +. + */ +namespace Xibo\Report; + +use Carbon\Carbon; +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 MobileProofOfPlay + * @package Xibo\Report + */ +class MobileProofOfPlay 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->layoutFactory = $container->get('layoutFactory'); + $this->reportScheduleFactory = $container->get('reportScheduleFactory'); + $this->sanitizer = $container->get('sanitizerService'); + $this->dispatcher = $container->get('dispatcher'); + return $this; + } + + /** @inheritdoc */ + public function getReportEmailTemplate() + { + return 'mobile-proofofplay-email-template.twig'; + } + + /** @inheritdoc */ + public function getSavedReportTemplate() + { + return 'mobile-proofofplay-report-preview'; + } + + /** @inheritdoc */ + public function getReportForm() + { + return new ReportForm( + 'mobile-proofofplay-report-form', + 'mobileProofOfPlay', + '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'] = 'mobileProofOfPlay'; + + return [ + 'template' => 'mobile-proofofplay-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'; + } elseif ($filter == 'yearly') { + $schedule = ReportSchedule::$SCHEDULE_YEARLY; + $filterCriteria['reportFilter'] = 'lastyear'; + } + + $filterCriteria['sendEmail'] = $sanitizedParams->getCheckbox('sendEmail'); + $filterCriteria['nonusers'] = $sanitizedParams->getString('nonusers'); + + // Return + return [ + 'filterCriteria' => json_encode($filterCriteria), + 'schedule' => $schedule + ]; + } + + /** @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'] + ); + } + + /** @inheritdoc */ + public function getResults(SanitizerInterface $sanitizedParams) + { + $parentCampaignId = $sanitizedParams->getInt('parentCampaignId'); + $layoutId = $sanitizedParams->getInt('layoutId'); + + // Get campaign + if (!empty($parentCampaignId)) { + $campaign = $this->campaignFactory->getById($parentCampaignId); + } + + // 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 = [ + 'campaignId' => $parentCampaignId, + 'layoutId' => $layoutId, + 'displayIds' => $displayIds, + ]; + if (!empty($parentCampaignId)) { + $params['from'] = !empty($campaign->getStartDt()) ? $campaign->getStartDt()->format('Y-m-d H:i:s') : null; + $params['to'] = !empty($campaign->getEndDt()) ? $campaign->getEndDt()->format('Y-m-d H:i:s') : null; + + if (empty($campaign->getStartDt()) || empty($campaign->getEndDt())) { + return new ReportResult(); + } + } else { + $params['from'] = $fromDt->format('Y-m-d H:i:s'); + $params['to'] = $toDt->format('Y-m-d H:i:s'); + } + + // -------- + // ReportDataEvent + $event = new ReportDataEvent('mobileProofofplay'); + + // 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['from']; + $result['periodEnd'] = $params['to']; + + $rows = []; + $displayCache = []; + $layoutCache = []; + foreach ($results['json'] as $row) { + $entry = []; + + $entry['from'] = $row['from']; + $entry['to'] = $row['to']; + + // -------- + // Get Display + $entry['displayId'] = $row['displayId']; + if (!empty($entry['displayId'])) { + if (!array_key_exists($row['displayId'], $displayCache)) { + $display = $this->displayFactory->getById($row['displayId']); + $displayCache[$row['displayId']] = $display->display; + } + } + $entry['display'] = $displayCache[$row['displayId']] ?? ''; + + // -------- + // Get layout + $entry['layoutId'] = $row['layoutId']; + if (!empty($entry['layoutId'])) { + if (!array_key_exists($row['layoutId'], $layoutCache)) { + $layout = $this->layoutFactory->getById($row['layoutId']); + $layoutCache[$row['layoutId']] = $layout->layout; + } + } + $entry['layout'] = $layoutCache[$row['layoutId']] ?? ''; + + $entry['startLat'] = $row['startLat']; + $entry['startLong'] = $row['startLong']; + $entry['endLat'] = $row['endLat']; + $entry['endLong'] = $row['endLong']; + $entry['duration'] = $row['duration']; + + $rows[] = $entry; + } + + // 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, + [], + $results['error'] ?? null + ); + } +} diff --git a/lib/Service/ReportService.php b/lib/Service/ReportService.php index 1384c36a2e..c35b9efc1d 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,15 @@ 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); + $connectorReports = $event->getReports(); + + // Merge built in reports and connector reports + if (count($connectorReports) > 0) { + $reports = array_merge($reports, $connectorReports); + } foreach ($reports as $k => $report) { usort($report, function ($a, $b) { 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 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/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 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/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/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 0aff2b937a..2b219cb119 100644 --- a/reports/campaign-proofofplay-report-form.twig +++ b/reports/campaign-proofofplay-report-form.twig @@ -40,7 +40,7 @@ {% include "report-selector.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" %} @@ -192,7 +191,6 @@ {% 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/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 %} + +
+
+ {% trans "Display Played Percentage" %} +
+ + {% include "report-selector.twig" %} + +
+
+
+
+ +
+ {# 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) }} + + +
+
+
+ + +
+ +
+ + +
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + +
{% trans "Display" %}{% trans "Spend(%)" %}{% trans "Playtime(%)" %}
+
+
+ + +
+ +
+
+ +
+
+
+ +
+
+ + +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+{% 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/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 new file mode 100644 index 0000000000..d734c80d31 --- /dev/null +++ b/reports/mobile-proofofplay-report-form.twig @@ -0,0 +1,511 @@ +{# +/* + * 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: Mobile Proof of Play" %} | {% endblock %} + +{% block actionMenu %} + {% include "report-schedule-buttons.twig" %} +{% endblock %} + +{% block pageContent %} + +
+
+ {% trans "Mobile Proof of Play" %} +
+ + {% 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 "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 "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" %}
+
+
+
+
+
+
+
+{% endblock %} + +{% 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..2ccbdf6b4e --- /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 }}
+
+
+ +
+
+ +
+ + + + + + + + + + + + + + + + + + + +
{% 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" %}
+
+
+
+
+ +{% 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 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-form.twig b/reports/proofofplay-report-form.twig index 0913ec327b..c1da869829 100644 --- a/reports/proofofplay-report-form.twig +++ b/reports/proofofplay-report-form.twig @@ -40,7 +40,7 @@ {% include "report-selector.twig" %}
-
+
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' }, 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'); diff --git a/views/report-selector.twig b/views/report-selector.twig index bfb545baa7..424b9c3816 100644 --- a/views/report-selector.twig +++ b/views/report-selector.twig @@ -6,8 +6,8 @@
-{% endblock %} \ No newline at end of file +{% endblock %} 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 77b8c7d181..4453673d83 100644 --- a/views/xibo-audience-connector-form-settings.twig +++ b/views/xibo-audience-connector-form-settings.twig @@ -27,7 +27,54 @@ {% 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") %} +

{% 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 options = interface.getOptionsFromAxe() %} + {% if options.error %} +

{{ options.message }}

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

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

+ {% endif %} + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
{% 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 %}