2788 lines
110 KiB
PHP
2788 lines
110 KiB
PHP
|
|
<?php
|
||
|
|
/*
|
||
|
|
* Copyright (C) 2024 Xibo Signage Ltd
|
||
|
|
*
|
||
|
|
* Xibo - Digital Signage - https://xibosignage.com
|
||
|
|
*
|
||
|
|
* This file is part of Xibo.
|
||
|
|
*
|
||
|
|
* Xibo is free software: you can redistribute it and/or modify
|
||
|
|
* it under the terms of the GNU Affero General Public License as published by
|
||
|
|
* the Free Software Foundation, either version 3 of the License, or
|
||
|
|
* any later version.
|
||
|
|
*
|
||
|
|
* Xibo is distributed in the hope that it will be useful,
|
||
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
|
* GNU Affero General Public License for more details.
|
||
|
|
*
|
||
|
|
* You should have received a copy of the GNU Affero General Public License
|
||
|
|
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
|
||
|
|
*/
|
||
|
|
namespace Xibo\Controller;
|
||
|
|
|
||
|
|
use Carbon\Carbon;
|
||
|
|
use Illuminate\Support\Str;
|
||
|
|
use Psr\Http\Message\ResponseInterface;
|
||
|
|
use Slim\Http\Response as Response;
|
||
|
|
use Slim\Http\ServerRequest as Request;
|
||
|
|
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||
|
|
use Xibo\Entity\ScheduleReminder;
|
||
|
|
use Xibo\Event\ScheduleCriteriaRequestEvent;
|
||
|
|
use Xibo\Factory\CampaignFactory;
|
||
|
|
use Xibo\Factory\CommandFactory;
|
||
|
|
use Xibo\Factory\DayPartFactory;
|
||
|
|
use Xibo\Factory\DisplayFactory;
|
||
|
|
use Xibo\Factory\DisplayGroupFactory;
|
||
|
|
use Xibo\Factory\LayoutFactory;
|
||
|
|
use Xibo\Factory\ScheduleCriteriaFactory;
|
||
|
|
use Xibo\Factory\ScheduleExclusionFactory;
|
||
|
|
use Xibo\Factory\ScheduleFactory;
|
||
|
|
use Xibo\Factory\ScheduleReminderFactory;
|
||
|
|
use Xibo\Factory\SyncGroupFactory;
|
||
|
|
use Xibo\Helper\DateFormatHelper;
|
||
|
|
use Xibo\Helper\Session;
|
||
|
|
use Xibo\Support\Exception\AccessDeniedException;
|
||
|
|
use Xibo\Support\Exception\ControllerNotImplemented;
|
||
|
|
use Xibo\Support\Exception\GeneralException;
|
||
|
|
use Xibo\Support\Exception\InvalidArgumentException;
|
||
|
|
use Xibo\Support\Exception\NotFoundException;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Class Schedule
|
||
|
|
* @package Xibo\Controller
|
||
|
|
*/
|
||
|
|
class Schedule extends Base
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* @var Session
|
||
|
|
*/
|
||
|
|
private $session;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var ScheduleFactory
|
||
|
|
*/
|
||
|
|
private $scheduleFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var ScheduleReminderFactory
|
||
|
|
*/
|
||
|
|
private $scheduleReminderFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var ScheduleExclusionFactory
|
||
|
|
*/
|
||
|
|
private $scheduleExclusionFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var DisplayGroupFactory
|
||
|
|
*/
|
||
|
|
private $displayGroupFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var CampaignFactory
|
||
|
|
*/
|
||
|
|
private $campaignFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @var CommandFactory
|
||
|
|
*/
|
||
|
|
private $commandFactory;
|
||
|
|
|
||
|
|
/** @var DisplayFactory */
|
||
|
|
private $displayFactory;
|
||
|
|
|
||
|
|
/** @var LayoutFactory */
|
||
|
|
private $layoutFactory;
|
||
|
|
|
||
|
|
/** @var DayPartFactory */
|
||
|
|
private $dayPartFactory;
|
||
|
|
|
||
|
|
private SyncGroupFactory $syncGroupFactory;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Set common dependencies.
|
||
|
|
* @param Session $session
|
||
|
|
* @param ScheduleFactory $scheduleFactory
|
||
|
|
* @param DisplayGroupFactory $displayGroupFactory
|
||
|
|
* @param CampaignFactory $campaignFactory
|
||
|
|
* @param CommandFactory $commandFactory
|
||
|
|
* @param DisplayFactory $displayFactory
|
||
|
|
* @param LayoutFactory $layoutFactory
|
||
|
|
* @param DayPartFactory $dayPartFactory
|
||
|
|
* @param ScheduleReminderFactory $scheduleReminderFactory
|
||
|
|
* @param ScheduleExclusionFactory $scheduleExclusionFactory
|
||
|
|
*/
|
||
|
|
|
||
|
|
public function __construct(
|
||
|
|
$session,
|
||
|
|
$scheduleFactory,
|
||
|
|
$displayGroupFactory,
|
||
|
|
$campaignFactory,
|
||
|
|
$commandFactory,
|
||
|
|
$displayFactory,
|
||
|
|
$layoutFactory,
|
||
|
|
$dayPartFactory,
|
||
|
|
$scheduleReminderFactory,
|
||
|
|
$scheduleExclusionFactory,
|
||
|
|
SyncGroupFactory $syncGroupFactory,
|
||
|
|
private readonly ScheduleCriteriaFactory $scheduleCriteriaFactory
|
||
|
|
) {
|
||
|
|
$this->session = $session;
|
||
|
|
$this->scheduleFactory = $scheduleFactory;
|
||
|
|
$this->displayGroupFactory = $displayGroupFactory;
|
||
|
|
$this->campaignFactory = $campaignFactory;
|
||
|
|
$this->commandFactory = $commandFactory;
|
||
|
|
$this->displayFactory = $displayFactory;
|
||
|
|
$this->layoutFactory = $layoutFactory;
|
||
|
|
$this->dayPartFactory = $dayPartFactory;
|
||
|
|
$this->scheduleReminderFactory = $scheduleReminderFactory;
|
||
|
|
$this->scheduleExclusionFactory = $scheduleExclusionFactory;
|
||
|
|
$this->syncGroupFactory = $syncGroupFactory;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
*/
|
||
|
|
function displayPage(Request $request, Response $response)
|
||
|
|
{
|
||
|
|
// get the default longitude and latitude from CMS options
|
||
|
|
$defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT');
|
||
|
|
$defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG');
|
||
|
|
|
||
|
|
$data = [
|
||
|
|
'defaultLat' => $defaultLat,
|
||
|
|
'defaultLong' => $defaultLong,
|
||
|
|
'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(),
|
||
|
|
];
|
||
|
|
|
||
|
|
// Render the Theme and output
|
||
|
|
$this->getState()->template = 'schedule-page';
|
||
|
|
$this->getState()->setData($data);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generates the calendar that we draw events on
|
||
|
|
* @deprecated - Deprecated API: This endpoint will be removed in v5.0
|
||
|
|
* @SWG\Get(
|
||
|
|
* path="/schedule/data/events",
|
||
|
|
* operationId="scheduleCalendarData",
|
||
|
|
* description="⚠️ This endpoint is deprecated and will be removed in v5.0.",
|
||
|
|
* tags={"schedule"},
|
||
|
|
* deprecated=true,
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="displayGroupIds",
|
||
|
|
* description="The DisplayGroupIds to return the schedule for. [-1] for All.",
|
||
|
|
* in="query",
|
||
|
|
* type="array",
|
||
|
|
* required=true,
|
||
|
|
* @SWG\Items(
|
||
|
|
* type="integer"
|
||
|
|
* )
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="from",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="string",
|
||
|
|
* description="From Date in Y-m-d H:i:s format, if not provided defaults to start of the current month"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="to",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="string",
|
||
|
|
* description="To Date in Y-m-d H:i:s format, if not provided defaults to start of the next month"
|
||
|
|
* ),
|
||
|
|
* @SWG\Response(
|
||
|
|
* response=200,
|
||
|
|
* description="successful response",
|
||
|
|
* @SWG\Schema(
|
||
|
|
* type="array",
|
||
|
|
* @SWG\Items(ref="#/definitions/ScheduleCalendarData")
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws InvalidArgumentException
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
public function eventData(Request $request, Response $response)
|
||
|
|
{
|
||
|
|
$response = $response
|
||
|
|
->withHeader(
|
||
|
|
'Warning',
|
||
|
|
'299 - "Deprecated API: /schedule/data/events will be removed in v5.0"'
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->getLog()->error('Deprecated API called: /schedule/data/events');
|
||
|
|
|
||
|
|
$this->setNoOutput();
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
|
||
|
|
$displayGroupIds = $sanitizedParams->getIntArray('displayGroupIds', ['default' => []]);
|
||
|
|
$displaySpecificDisplayGroupIds = $sanitizedParams->getIntArray('displaySpecificGroupIds', ['default' => []]);
|
||
|
|
$originalDisplayGroupIds = array_merge($displayGroupIds, $displaySpecificDisplayGroupIds);
|
||
|
|
$campaignId = $sanitizedParams->getInt('campaignId');
|
||
|
|
|
||
|
|
$start = $sanitizedParams->getDate('from', ['default' => Carbon::now()->startOfMonth()]);
|
||
|
|
$end = $sanitizedParams->getDate('to', ['default' => Carbon::now()->addMonth()->startOfMonth()]);
|
||
|
|
|
||
|
|
if (count($originalDisplayGroupIds) <= 0) {
|
||
|
|
return $response->withJson(['success' => 1, 'result' => []]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Setting for whether we show Layouts without permissions
|
||
|
|
$showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1);
|
||
|
|
|
||
|
|
// Permissions check the list of display groups with the user accessible list of display groups
|
||
|
|
$resolvedDisplayGroupIds = array_diff($originalDisplayGroupIds, [-1]);
|
||
|
|
|
||
|
|
if (!$this->getUser()->isSuperAdmin()) {
|
||
|
|
$userDisplayGroupIds = array_map(function ($element) {
|
||
|
|
/** @var \Xibo\Entity\DisplayGroup $element */
|
||
|
|
return $element->displayGroupId;
|
||
|
|
}, $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1]));
|
||
|
|
|
||
|
|
// Reset the list to only those display groups that intersect and if 0 have been provided, only those from
|
||
|
|
// the user list
|
||
|
|
$resolvedDisplayGroupIds = (count($originalDisplayGroupIds) > 0) ? array_intersect($originalDisplayGroupIds, $userDisplayGroupIds) : $userDisplayGroupIds;
|
||
|
|
|
||
|
|
$this->getLog()->debug('Resolved list of display groups ['
|
||
|
|
. json_encode($resolvedDisplayGroupIds) . '] from provided list ['
|
||
|
|
. json_encode($originalDisplayGroupIds) . '] and user list ['
|
||
|
|
. json_encode($userDisplayGroupIds) . ']');
|
||
|
|
|
||
|
|
// If we have none, then we do not return any events.
|
||
|
|
if (count($resolvedDisplayGroupIds) <= 0) {
|
||
|
|
return $response->withJson(['success' => 1, 'result' => []]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$events = [];
|
||
|
|
$filter = [
|
||
|
|
'futureSchedulesFrom' => $start->format('U'),
|
||
|
|
'futureSchedulesTo' => $end->format('U'),
|
||
|
|
'displayGroupIds' => $resolvedDisplayGroupIds,
|
||
|
|
'geoAware' => $sanitizedParams->getInt('geoAware'),
|
||
|
|
'recurring' => $sanitizedParams->getInt('recurring'),
|
||
|
|
'eventTypeId' => $sanitizedParams->getInt('eventTypeId'),
|
||
|
|
'name' => $sanitizedParams->getString('name'),
|
||
|
|
'useRegexForName' => $sanitizedParams->getCheckbox('useRegexForName'),
|
||
|
|
'logicalOperatorName' => $sanitizedParams->getString('logicalOperatorName'),
|
||
|
|
];
|
||
|
|
|
||
|
|
if ($campaignId != null) {
|
||
|
|
// Is this an ad campaign?
|
||
|
|
$campaign = $this->campaignFactory->getById($campaignId);
|
||
|
|
if ($campaign->type === 'ad') {
|
||
|
|
$filter['parentCampaignId'] = $campaignId;
|
||
|
|
} else {
|
||
|
|
$filter['campaignId'] = $campaignId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($this->scheduleFactory->query(['FromDT'], $filter) as $row) {
|
||
|
|
/* @var \Xibo\Entity\Schedule $row */
|
||
|
|
|
||
|
|
// Generate this event
|
||
|
|
try {
|
||
|
|
$scheduleEvents = $row->getEvents($start, $end);
|
||
|
|
} catch (GeneralException $e) {
|
||
|
|
$this->getLog()->error('Unable to getEvents for ' . $row->eventId);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (count($scheduleEvents) <= 0) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug('EventId ' . $row->eventId . ' as events: ' . json_encode($scheduleEvents));
|
||
|
|
|
||
|
|
// Load the display groups
|
||
|
|
$row->load();
|
||
|
|
|
||
|
|
$displayGroupList = '';
|
||
|
|
|
||
|
|
if (count($row->displayGroups) >= 0) {
|
||
|
|
$array = array_map(function ($object) {
|
||
|
|
return $object->displayGroup;
|
||
|
|
}, $row->displayGroups);
|
||
|
|
$displayGroupList = implode(', ', $array);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Event Permissions
|
||
|
|
$editable = $this->getUser()->featureEnabled('schedule.modify')
|
||
|
|
&& $this->isEventEditable($row);
|
||
|
|
|
||
|
|
// Event Title
|
||
|
|
if ($this->isSyncEvent($row->eventTypeId)) {
|
||
|
|
$title = sprintf(
|
||
|
|
__('%s scheduled on sync group %s'),
|
||
|
|
$row->getSyncTypeForEvent(),
|
||
|
|
$row->getUnmatchedProperty('syncGroupName'),
|
||
|
|
);
|
||
|
|
} else if ($row->campaignId == 0) {
|
||
|
|
// Command
|
||
|
|
$title = __('%s scheduled on %s', $row->command, $displayGroupList);
|
||
|
|
} else {
|
||
|
|
// Should we show the Layout name, or not (depending on permission)
|
||
|
|
// Make sure we only run the below code if we have to, it's quite expensive
|
||
|
|
if (!$showLayoutName && !$this->getUser()->isSuperAdmin()) {
|
||
|
|
// Campaign
|
||
|
|
$campaign = $this->campaignFactory->getById($row->campaignId);
|
||
|
|
|
||
|
|
if (!$this->getUser()->checkViewable($campaign)) {
|
||
|
|
$row->campaign = __('Private Item');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$title = sprintf(
|
||
|
|
__('%s scheduled on %s'),
|
||
|
|
$row->getUnmatchedProperty('parentCampaignName', $row->campaign),
|
||
|
|
$displayGroupList
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($row->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
|
||
|
|
$title .= __(' with Share of Voice %d seconds per hour', $row->shareOfVoice);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Day diff from start date to end date
|
||
|
|
$diff = $end->diff($start)->days;
|
||
|
|
|
||
|
|
// Show all Hourly repeats on the day view
|
||
|
|
if ($row->recurrenceType == 'Minute' || ($diff > 1 && $row->recurrenceType == 'Hour')) {
|
||
|
|
$title .= __(', Repeats every %s %s', $row->recurrenceDetail, $row->recurrenceType);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Event URL
|
||
|
|
$editUrlWeb = 'schedule.edit.form';
|
||
|
|
$editUrl = ($this->isApi($request)) ? 'schedule.edit' : $editUrlWeb;
|
||
|
|
$url = ($editable) ? $this->urlFor($request, $editUrl, ['id' => $row->eventId]) : '#';
|
||
|
|
|
||
|
|
$days = [];
|
||
|
|
|
||
|
|
// Event scheduled events
|
||
|
|
foreach ($scheduleEvents as $scheduleEvent) {
|
||
|
|
$this->getLog()->debug(sprintf('Parsing event dates from %s and %s', $scheduleEvent->fromDt, $scheduleEvent->toDt));
|
||
|
|
|
||
|
|
// Get the day of schedule start
|
||
|
|
$fromDtDay = Carbon::createFromTimestamp($scheduleEvent->fromDt)->format('Y-m-d');
|
||
|
|
|
||
|
|
// Handle command events which do not have a toDt
|
||
|
|
if ($row->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) {
|
||
|
|
$scheduleEvent->toDt = $scheduleEvent->fromDt;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Parse our dates into a Date object, so that we convert to local time correctly.
|
||
|
|
$fromDt = Carbon::createFromTimestamp($scheduleEvent->fromDt);
|
||
|
|
$toDt = Carbon::createFromTimestamp($scheduleEvent->toDt);
|
||
|
|
|
||
|
|
// Set the row from/to date to be an ISO date for display
|
||
|
|
$scheduleEvent->fromDt =
|
||
|
|
Carbon::createFromTimestamp($scheduleEvent->fromDt)
|
||
|
|
->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$scheduleEvent->toDt =
|
||
|
|
Carbon::createFromTimestamp($scheduleEvent->toDt)
|
||
|
|
->format(DateFormatHelper::getSystemFormat());
|
||
|
|
|
||
|
|
$this->getLog()->debug(sprintf('Start date is ' . $fromDt->toRssString() . ' ' . $scheduleEvent->fromDt));
|
||
|
|
$this->getLog()->debug(sprintf('End date is ' . $toDt->toRssString() . ' ' . $scheduleEvent->toDt));
|
||
|
|
|
||
|
|
// For a minute/hourly repeating events show only 1 event per day
|
||
|
|
if ($row->recurrenceType == 'Minute' || ($diff > 1 && $row->recurrenceType == 'Hour')) {
|
||
|
|
if (array_key_exists($fromDtDay, $days)) {
|
||
|
|
continue;
|
||
|
|
} else {
|
||
|
|
$days[$fromDtDay] = $scheduleEvent->fromDt;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @SWG\Definition(
|
||
|
|
* definition="ScheduleCalendarData",
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="id",
|
||
|
|
* type="integer",
|
||
|
|
* description="Event ID"
|
||
|
|
* ),
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="title",
|
||
|
|
* type="string",
|
||
|
|
* description="Event Title"
|
||
|
|
* ),
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="sameDay",
|
||
|
|
* type="integer",
|
||
|
|
* description="Does this event happen only on 1 day"
|
||
|
|
* ),
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="event",
|
||
|
|
* ref="#/definitions/Schedule"
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
$events[] = [
|
||
|
|
'id' => $row->eventId,
|
||
|
|
'title' => $title,
|
||
|
|
'url' => ($editable) ? $url : null,
|
||
|
|
'start' => $fromDt->format('U') * 1000,
|
||
|
|
'end' => $toDt->format('U') * 1000,
|
||
|
|
'sameDay' => ($fromDt->day == $toDt->day && $fromDt->month == $toDt->month && $fromDt->year == $toDt->year),
|
||
|
|
'editable' => $editable,
|
||
|
|
'event' => $row,
|
||
|
|
'scheduleEvent' => $scheduleEvent,
|
||
|
|
'recurringEvent' => $row->recurrenceType != ''
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return $response->withJson(['success' => 1, 'result' => $events]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Event List
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param $id
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws AccessDeniedException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
* @SWG\Get(
|
||
|
|
* path="/schedule/{displayGroupId}/events",
|
||
|
|
* operationId="scheduleCalendarDataDisplayGroup",
|
||
|
|
* tags={"schedule"},
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="displayGroupId",
|
||
|
|
* description="The DisplayGroupId to return the event list for.",
|
||
|
|
* in="path",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="singlePointInTime",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="integer",
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="date",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="string",
|
||
|
|
* description="Date in Y-m-d H:i:s"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="startDate",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="string",
|
||
|
|
* description="Date in Y-m-d H:i:s"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="endDate",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="string",
|
||
|
|
* description="Date in Y-m-d H:i:s"
|
||
|
|
* ),
|
||
|
|
* @SWG\Response(
|
||
|
|
* response=200,
|
||
|
|
* description="successful response"
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public function eventList(Request $request, Response $response, $id)
|
||
|
|
{
|
||
|
|
$displayGroup = $this->displayGroupFactory->getById($id);
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
|
||
|
|
if (!$this->getUser()->checkViewable($displayGroup)) {
|
||
|
|
throw new AccessDeniedException();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Setting for whether we show Layouts with out permissions
|
||
|
|
$showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1);
|
||
|
|
|
||
|
|
$singlePointInTime = $sanitizedParams->getInt('singlePointInTime');
|
||
|
|
if ($singlePointInTime == 1) {
|
||
|
|
$startDate = $sanitizedParams->getDate('date');
|
||
|
|
$endDate = $sanitizedParams->getDate('date');
|
||
|
|
} else {
|
||
|
|
$startDate = $sanitizedParams->getDate('startDate');
|
||
|
|
$endDate = $sanitizedParams->getDate('endDate');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reset the seconds
|
||
|
|
$startDate->second(0);
|
||
|
|
$endDate->second(0);
|
||
|
|
|
||
|
|
$this->getLog()->debug(
|
||
|
|
sprintf(
|
||
|
|
'Generating eventList for DisplayGroupId ' . $id . ' from date '
|
||
|
|
. $startDate->format(DateFormatHelper::getSystemFormat()) . ' to '
|
||
|
|
. $endDate->format(DateFormatHelper::getSystemFormat())
|
||
|
|
)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Get a list of scheduled events
|
||
|
|
$events = [];
|
||
|
|
$displayGroups = [];
|
||
|
|
$layouts = [];
|
||
|
|
$campaigns = [];
|
||
|
|
|
||
|
|
// Add the displayGroupId I am filtering for to the displayGroup object
|
||
|
|
$displayGroups[$displayGroup->displayGroupId] = $displayGroup;
|
||
|
|
|
||
|
|
// Is this group a display specific group, or a standalone?
|
||
|
|
$options = [];
|
||
|
|
/** @var \Xibo\Entity\Display $display */
|
||
|
|
$display = null;
|
||
|
|
if ($displayGroup->isDisplaySpecific == 1) {
|
||
|
|
// We should lookup the displayId for this group.
|
||
|
|
$display = $this->displayFactory->getByDisplayGroupId($id)[0];
|
||
|
|
} else {
|
||
|
|
$options['useGroupId'] = true;
|
||
|
|
$options['displayGroupId'] = $id;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get list of events
|
||
|
|
$scheduleForXmds = $this->scheduleFactory->getForXmds(
|
||
|
|
($display === null) ? null : $display->displayId,
|
||
|
|
$startDate,
|
||
|
|
$endDate,
|
||
|
|
$options
|
||
|
|
);
|
||
|
|
|
||
|
|
$this->getLog()->debug(count($scheduleForXmds) . ' events returned for displaygroup and date');
|
||
|
|
|
||
|
|
foreach ($scheduleForXmds as $event) {
|
||
|
|
|
||
|
|
// Ignore command events
|
||
|
|
if ($event['eventTypeId'] == \Xibo\Entity\Schedule::$COMMAND_EVENT)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
// Ignore events that have a campaignId, but no layoutId (empty Campaigns)
|
||
|
|
if ($event['layoutId'] == 0 && $event['campaignId'] != 0)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
// Assess schedules
|
||
|
|
$schedule = $this->scheduleFactory->createEmpty()->hydrate($event, ['intProperties' => ['isPriority', 'syncTimezone', 'displayOrder', 'fromDt', 'toDt']]);
|
||
|
|
$schedule->load();
|
||
|
|
|
||
|
|
$this->getLog()->debug('EventId ' . $schedule->eventId . ' exists in the schedule window, checking its instances for activity');
|
||
|
|
|
||
|
|
// Get scheduled events based on recurrence
|
||
|
|
try {
|
||
|
|
$scheduleEvents = $schedule->getEvents($startDate, $endDate);
|
||
|
|
} catch (GeneralException $e) {
|
||
|
|
$this->getLog()->error('Unable to getEvents for ' . $schedule->eventId);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If this event is active, collect extra information and add to the events list
|
||
|
|
if (count($scheduleEvents) > 0) {
|
||
|
|
// Add the link to the schedule
|
||
|
|
if (!$this->isApi($request)) {
|
||
|
|
$route = 'schedule.edit.form';
|
||
|
|
$schedule->setUnmatchedProperty(
|
||
|
|
'link',
|
||
|
|
$this->urlFor($request, $route, ['id' => $schedule->eventId])
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add the Layout
|
||
|
|
if ($event['eventTypeId'] == \Xibo\Entity\Schedule::$SYNC_EVENT) {
|
||
|
|
$layoutId = $event['syncLayoutId'];
|
||
|
|
} else {
|
||
|
|
$layoutId = $event['layoutId'];
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug('Adding this events layoutId [' . $layoutId . '] to list');
|
||
|
|
|
||
|
|
if ($layoutId != 0 && !array_key_exists($layoutId, $layouts)) {
|
||
|
|
// Look up the layout details
|
||
|
|
$layout = $this->layoutFactory->getById($layoutId);
|
||
|
|
|
||
|
|
// Add the link to the layout
|
||
|
|
if (!$this->isApi($request)) {
|
||
|
|
// do not link to Layout Designer for Full screen Media/Playlist Layout.
|
||
|
|
$link = (in_array($event['eventTypeId'], [7, 8]))
|
||
|
|
? ''
|
||
|
|
: $this->urlFor($request, 'layout.designer', ['id' => $layout->layoutId]);
|
||
|
|
|
||
|
|
$layout->setUnmatchedProperty(
|
||
|
|
'link',
|
||
|
|
$link
|
||
|
|
);
|
||
|
|
}
|
||
|
|
if ($showLayoutName || $this->getUser()->checkViewable($layout)) {
|
||
|
|
$layouts[$layoutId] = $layout;
|
||
|
|
} else {
|
||
|
|
$layouts[$layoutId] = [
|
||
|
|
'layout' => __('Private Item')
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
// Add the Campaign
|
||
|
|
$layout->campaigns = $this->campaignFactory->getByLayoutId($layout->layoutId);
|
||
|
|
|
||
|
|
if (count($layout->campaigns) > 0) {
|
||
|
|
// Add to the campaigns array
|
||
|
|
foreach ($layout->campaigns as $campaign) {
|
||
|
|
if (!array_key_exists($campaign->campaignId, $campaigns)) {
|
||
|
|
$campaigns[$campaign->campaignId] = $campaign;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$event['campaign'] = is_object($layouts[$layoutId]) ? $layouts[$layoutId]->layout : $layouts[$layoutId];
|
||
|
|
|
||
|
|
// Display Group details
|
||
|
|
$this->getLog()->debug('Adding this events displayGroupIds to list');
|
||
|
|
$schedule->excludeProperty('displayGroups');
|
||
|
|
|
||
|
|
foreach ($schedule->displayGroups as $scheduleDisplayGroup) {
|
||
|
|
if (!array_key_exists($scheduleDisplayGroup->displayGroupId, $displayGroups)) {
|
||
|
|
$displayGroups[$scheduleDisplayGroup->displayGroupId] = $scheduleDisplayGroup;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Determine the intermediate display groups
|
||
|
|
$this->getLog()->debug('Adding this events intermediateDisplayGroupIds to list');
|
||
|
|
$schedule->setUnmatchedProperty(
|
||
|
|
'intermediateDisplayGroupIds',
|
||
|
|
$this->calculateIntermediates($display, $displayGroup, $event['displayGroupId'])
|
||
|
|
);
|
||
|
|
|
||
|
|
foreach ($schedule->getUnmatchedProperty('intermediateDisplayGroupIds') as $intermediate) {
|
||
|
|
if (!array_key_exists($intermediate, $displayGroups)) {
|
||
|
|
$displayGroups[$intermediate] = $this->displayGroupFactory->getById($intermediate);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug(sprintf('Adding scheduled events: ' . json_encode($scheduleEvents)));
|
||
|
|
|
||
|
|
// We will never save this and we need the eventId on the agenda view
|
||
|
|
$eventId = $schedule->eventId;
|
||
|
|
|
||
|
|
foreach ($scheduleEvents as $scheduleEvent) {
|
||
|
|
$schedule = clone $schedule;
|
||
|
|
$schedule->eventId = $eventId;
|
||
|
|
$schedule->fromDt = $scheduleEvent->fromDt;
|
||
|
|
$schedule->toDt = $scheduleEvent->toDt;
|
||
|
|
$schedule->setUnmatchedProperty('layoutId', intval($layoutId));
|
||
|
|
$schedule->setUnmatchedProperty('displayGroupId', intval($event['displayGroupId']));
|
||
|
|
|
||
|
|
$events[] = $schedule;
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$this->getLog()->debug('No activity inside window');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getState()->hydrate([
|
||
|
|
'data' => [
|
||
|
|
'events' => $events,
|
||
|
|
'displayGroups' => $displayGroups,
|
||
|
|
'layouts' => $layouts,
|
||
|
|
'campaigns' => $campaigns
|
||
|
|
]
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param \Xibo\Entity\Display $display
|
||
|
|
* @param \Xibo\Entity\DisplayGroup $displayGroup
|
||
|
|
* @param int $eventDisplayGroupId
|
||
|
|
* @return array
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
private function calculateIntermediates($display, $displayGroup, $eventDisplayGroupId)
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('Calculating intermediates for events displayGroupId ' . $eventDisplayGroupId . ' viewing displayGroupId ' . $displayGroup->displayGroupId);
|
||
|
|
|
||
|
|
$intermediates = [];
|
||
|
|
$eventDisplayGroup = $this->displayGroupFactory->getById($eventDisplayGroupId);
|
||
|
|
|
||
|
|
// Is the event scheduled directly on the displayGroup in question?
|
||
|
|
if ($displayGroup->displayGroupId == $eventDisplayGroupId)
|
||
|
|
return $intermediates;
|
||
|
|
|
||
|
|
// Is the event scheduled directly on the display in question?
|
||
|
|
if ($eventDisplayGroup->isDisplaySpecific == 1)
|
||
|
|
return $intermediates;
|
||
|
|
|
||
|
|
$this->getLog()->debug('Event isnt directly scheduled to a display or to the current displaygroup ');
|
||
|
|
|
||
|
|
// There are nested groups involved, so we need to trace the relationship tree.
|
||
|
|
if ($display === null) {
|
||
|
|
$this->getLog()->debug('We are looking at a DisplayGroup');
|
||
|
|
// We are on a group.
|
||
|
|
|
||
|
|
// Get the relationship tree for this display group
|
||
|
|
$tree = $this->displayGroupFactory->getRelationShipTree($displayGroup->displayGroupId);
|
||
|
|
|
||
|
|
foreach ($tree as $branch) {
|
||
|
|
$this->getLog()->debug(
|
||
|
|
'Branch found: ' . $branch->displayGroup .
|
||
|
|
' [' . $branch->displayGroupId . '], ' .
|
||
|
|
$branch->getUnmatchedProperty('depth') . '-' .
|
||
|
|
$branch->getUnmatchedProperty('level')
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($branch->getUnmatchedProperty('depth') < 0 &&
|
||
|
|
$branch->displayGroupId != $eventDisplayGroup->displayGroupId
|
||
|
|
) {
|
||
|
|
$intermediates[] = $branch->displayGroupId;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// We are on a display.
|
||
|
|
$this->getLog()->debug('We are looking at a Display');
|
||
|
|
|
||
|
|
// We will need to get all of this displays groups and then add only those ones that give us an eventual
|
||
|
|
// match on the events display group (complicated or what!)
|
||
|
|
$display->load();
|
||
|
|
|
||
|
|
foreach ($display->displayGroups as $displayDisplayGroup) {
|
||
|
|
|
||
|
|
// Ignore the display specific group
|
||
|
|
if ($displayDisplayGroup->isDisplaySpecific == 1)
|
||
|
|
continue;
|
||
|
|
|
||
|
|
// Get the relationship tree for this display group
|
||
|
|
$tree = $this->displayGroupFactory->getRelationShipTree($displayDisplayGroup->displayGroupId);
|
||
|
|
|
||
|
|
$found = false;
|
||
|
|
$possibleIntermediates = [];
|
||
|
|
|
||
|
|
foreach ($tree as $branch) {
|
||
|
|
$this->getLog()->debug(
|
||
|
|
'Branch found: ' . $branch->displayGroup .
|
||
|
|
' [' . $branch->displayGroupId . '], ' .
|
||
|
|
$branch->getUnmatchedProperty('depth') . '-' .
|
||
|
|
$branch->getUnmatchedProperty('level')
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($branch->displayGroupId != $eventDisplayGroup->displayGroupId) {
|
||
|
|
$possibleIntermediates[] = $branch->displayGroupId;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($branch->displayGroupId != $eventDisplayGroup->displayGroupId && count($possibleIntermediates) > 0)
|
||
|
|
$found = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($found) {
|
||
|
|
$this->getLog()->debug('We have found intermediates ' . json_encode($possibleIntermediates) . ' for display when looking at displayGroupId ' . $displayDisplayGroup->displayGroupId);
|
||
|
|
$intermediates = array_merge($intermediates, $possibleIntermediates);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug('Returning intermediates: ' . json_encode($intermediates));
|
||
|
|
|
||
|
|
return $intermediates;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Shows a form to add an event
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param string|null $from
|
||
|
|
* @param int|null $id
|
||
|
|
* @return ResponseInterface|Response
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws InvalidArgumentException
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
public function addForm(Request $request, Response $response, ?string $from, ?int $id): Response|ResponseInterface
|
||
|
|
{
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
|
||
|
|
// get the default longitude and latitude from CMS options
|
||
|
|
$defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT');
|
||
|
|
$defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG');
|
||
|
|
|
||
|
|
// Dispatch an event to initialize schedule criteria
|
||
|
|
$event = new ScheduleCriteriaRequestEvent();
|
||
|
|
$this->getDispatcher()->dispatch($event, ScheduleCriteriaRequestEvent::$NAME);
|
||
|
|
|
||
|
|
// Retrieve the criteria data from the event
|
||
|
|
$criteria = $event->getCriteria();
|
||
|
|
$criteriaDefaultCondition = $event->getCriteriaDefaultCondition();
|
||
|
|
|
||
|
|
$addFormData = [
|
||
|
|
'dayParts' => $this->dayPartFactory->allWithSystem(['isRetired' => 0]),
|
||
|
|
'reminders' => [],
|
||
|
|
'defaultLat' => $defaultLat,
|
||
|
|
'defaultLong' => $defaultLong,
|
||
|
|
'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(),
|
||
|
|
'addForm' => true,
|
||
|
|
'relativeTime' => 0,
|
||
|
|
'setDisplaysFromFilter' => true,
|
||
|
|
'scheduleCriteria' => $criteria,
|
||
|
|
'criteriaDefaultCondition' => $criteriaDefaultCondition
|
||
|
|
];
|
||
|
|
$formNowData = [];
|
||
|
|
|
||
|
|
if (!empty($from) && !empty($id)) {
|
||
|
|
$formNowData = [
|
||
|
|
'event' => [
|
||
|
|
'eventTypeId' => $this->getEventTypeId($from),
|
||
|
|
],
|
||
|
|
'campaign' => (
|
||
|
|
($from == 'Campaign' || $from == 'Layout')
|
||
|
|
? $this->campaignFactory->getById($id)
|
||
|
|
: null
|
||
|
|
),
|
||
|
|
'displayGroups' => (($from == 'DisplayGroup') ? [$this->displayGroupFactory->getById($id)] : null),
|
||
|
|
'displayGroupIds' => (($from == 'DisplayGroup') ? [$id] : [0]),
|
||
|
|
'mediaId' => (($from === 'Library') ? $id : null),
|
||
|
|
'playlistId' => (($from === 'Playlist') ? $id : null),
|
||
|
|
// Lock for layout editor only
|
||
|
|
'readonlySelect' => ($from == 'Layout' && $sanitizedParams->getString('fromLayoutEditor') === '1'),
|
||
|
|
// Hide event type, except for Display Groups
|
||
|
|
'hideEventType' => !($from == 'DisplayGroup'),
|
||
|
|
// Skip first step, except for Display Groups
|
||
|
|
'skipFirstStep' => !($from == 'DisplayGroup'),
|
||
|
|
// If coming from display page, don't show syncEvent type
|
||
|
|
'eventTypes' => \Xibo\Entity\Schedule::getEventTypes((($from === 'DisplayGroup') ? [9] : [])),
|
||
|
|
'addForm' => true,
|
||
|
|
'fromCampaign' => ($from == 'Campaign'),
|
||
|
|
'relativeTime' => 1,
|
||
|
|
'setDisplaysFromFilter' => false,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
$formData = array_merge($addFormData, $formNowData);
|
||
|
|
|
||
|
|
$this->getState()->template = 'schedule-form-edit';
|
||
|
|
$this->getState()->setData($formData);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Model to use for supplying key/value pairs to arrays
|
||
|
|
* @SWG\Definition(
|
||
|
|
* definition="ScheduleReminderArray",
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="reminder_value",
|
||
|
|
* type="integer"
|
||
|
|
* ),
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="reminder_type",
|
||
|
|
* type="integer"
|
||
|
|
* ),
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="reminder_option",
|
||
|
|
* type="integer"
|
||
|
|
* ),
|
||
|
|
* @SWG\Property(
|
||
|
|
* property="reminder_isEmailHidden",
|
||
|
|
* type="integer"
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Add Event
|
||
|
|
* @SWG\Post(
|
||
|
|
* path="/schedule",
|
||
|
|
* operationId="scheduleAdd",
|
||
|
|
* tags={"schedule"},
|
||
|
|
* summary="Add Schedule Event",
|
||
|
|
* description="Add a new scheduled event for a Campaign/Layout to be shown on a Display Group/Display.",
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="eventTypeId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Event Type Id to use for this Event.
|
||
|
|
* 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action, 7=Media Library, 8=Playlist",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="campaignId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Campaign ID to use for this Event.
|
||
|
|
* If a Layout is needed then the Campaign specific ID for that Layout should be used.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="fullScreenCampaignId",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Media or Playlist event Type. The Layout specific Campaign ID to use for this Event.
|
||
|
|
* This needs to be the Layout created with layout/fullscreen function",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="commandId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Command ID to use for this Event.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="mediaId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Media ID to use for this Event.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="displayOrder",
|
||
|
|
* in="formData",
|
||
|
|
* description="The display order for this event. ",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="isPriority",
|
||
|
|
* in="formData",
|
||
|
|
* description="An integer indicating the priority of this event. Normal events have a priority of 0.",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="displayGroupIds",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Display Group IDs for this event. Display specific Group IDs should be used to schedule on single displays.",
|
||
|
|
* type="array",
|
||
|
|
* required=true,
|
||
|
|
* @SWG\Items(type="integer")
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="dayPartId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Day Part for this event. Overrides supported are 0(custom) and 1(always). Defaulted to 0.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="syncTimezone",
|
||
|
|
* in="formData",
|
||
|
|
* description="Should this schedule be synced to the resulting Display timezone?",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="fromDt",
|
||
|
|
* in="formData",
|
||
|
|
* description="The from date for this event.",
|
||
|
|
* type="string",
|
||
|
|
* format="date-time",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="toDt",
|
||
|
|
* in="formData",
|
||
|
|
* description="The to date for this event.",
|
||
|
|
* type="string",
|
||
|
|
* format="date-time",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceType",
|
||
|
|
* in="formData",
|
||
|
|
* description="The type of recurrence to apply to this event.",
|
||
|
|
* type="string",
|
||
|
|
* required=false,
|
||
|
|
* enum={"", "Minute", "Hour", "Day", "Week", "Month", "Year"}
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceDetail",
|
||
|
|
* in="formData",
|
||
|
|
* description="The interval for the recurrence.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceRange",
|
||
|
|
* in="formData",
|
||
|
|
* description="The end date for this events recurrence.",
|
||
|
|
* type="string",
|
||
|
|
* format="date-time",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceRepeatsOn",
|
||
|
|
* in="formData",
|
||
|
|
* description="The days of the week that this event repeats - weekly only",
|
||
|
|
* type="string",
|
||
|
|
* format="array",
|
||
|
|
* required=false,
|
||
|
|
* @SWG\Items(type="integer")
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="scheduleReminders",
|
||
|
|
* in="formData",
|
||
|
|
* description="Array of Reminders for this event",
|
||
|
|
* type="array",
|
||
|
|
* required=false,
|
||
|
|
* @SWG\Items(
|
||
|
|
* ref="#/definitions/ScheduleReminderArray"
|
||
|
|
* )
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="isGeoAware",
|
||
|
|
* in="formData",
|
||
|
|
* description="Flag (0-1), whether this event is using Geo Location",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="geoLocation",
|
||
|
|
* in="formData",
|
||
|
|
* description="Array of comma separated strings each with comma separated pair of coordinates",
|
||
|
|
* type="array",
|
||
|
|
* required=false,
|
||
|
|
* @SWG\Items(type="string")
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="geoLocationJson",
|
||
|
|
* in="formData",
|
||
|
|
* description="Valid GeoJSON string, use as an alternative to geoLocation parameter",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="actionType",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Action eventTypeId, the type of the action - command or navLayout",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="actionTriggerCode",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Action eventTypeId, the webhook trigger code for the Action",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="actionLayoutCode",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Action eventTypeId and navLayout actionType, the Layout Code identifier",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="dataSetId",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Data Connector eventTypeId, the DataSet ID",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="dataSetParams",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Data Connector eventTypeId, the DataSet params",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Response(
|
||
|
|
* response=201,
|
||
|
|
* description="successful operation",
|
||
|
|
* @SWG\Schema(ref="#/definitions/Schedule"),
|
||
|
|
* @SWG\Header(
|
||
|
|
* header="Location",
|
||
|
|
* description="Location of the new record",
|
||
|
|
* type="string"
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
*
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
public function add(Request $request, Response $response)
|
||
|
|
{
|
||
|
|
$this->getLog()->debug('Add Schedule');
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
|
||
|
|
$embed = ($sanitizedParams->getString('embed') != null)
|
||
|
|
? explode(',', $sanitizedParams->getString('embed'))
|
||
|
|
: [];
|
||
|
|
|
||
|
|
// Get the custom day part to use as a default day part
|
||
|
|
$customDayPart = $this->dayPartFactory->getCustomDayPart();
|
||
|
|
|
||
|
|
$schedule = $this->scheduleFactory->createEmpty();
|
||
|
|
$schedule->userId = $this->getUser()->userId;
|
||
|
|
$schedule->eventTypeId = $sanitizedParams->getInt('eventTypeId');
|
||
|
|
$schedule->campaignId = $this->isFullScreenSchedule($schedule->eventTypeId)
|
||
|
|
? $sanitizedParams->getInt('fullScreenCampaignId')
|
||
|
|
: $sanitizedParams->getInt('campaignId');
|
||
|
|
$schedule->commandId = $sanitizedParams->getInt('commandId');
|
||
|
|
$schedule->displayOrder = $sanitizedParams->getInt('displayOrder', ['default' => 0]);
|
||
|
|
$schedule->isPriority = $sanitizedParams->getInt('isPriority', ['default' => 0]);
|
||
|
|
$schedule->dayPartId = $sanitizedParams->getInt('dayPartId', ['default' => $customDayPart->dayPartId]);
|
||
|
|
$schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware');
|
||
|
|
$schedule->actionType = $sanitizedParams->getString('actionType');
|
||
|
|
$schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode');
|
||
|
|
$schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode');
|
||
|
|
$schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]);
|
||
|
|
$schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId');
|
||
|
|
$schedule->name = $sanitizedParams->getString('name');
|
||
|
|
|
||
|
|
// Set the parentCampaignId for campaign events
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) {
|
||
|
|
$schedule->parentCampaignId = $schedule->campaignId;
|
||
|
|
|
||
|
|
// Make sure we're not directly scheduling an ad campaign
|
||
|
|
$campaign = $this->campaignFactory->getById($schedule->campaignId);
|
||
|
|
if ($campaign->type === 'ad') {
|
||
|
|
throw new InvalidArgumentException(
|
||
|
|
__('Direct scheduling of an Ad Campaign is not allowed'),
|
||
|
|
'campaignId'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fields only collected for interrupt events
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
|
||
|
|
$schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [
|
||
|
|
'throw' => function () {
|
||
|
|
new InvalidArgumentException(
|
||
|
|
__('Share of Voice must be a whole number between 0 and 3600'),
|
||
|
|
'shareOfVoice'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
]);
|
||
|
|
} else {
|
||
|
|
$schedule->shareOfVoice = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fields only collected for data connector events
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$DATA_CONNECTOR_EVENT) {
|
||
|
|
$schedule->dataSetId = $sanitizedParams->getInt('dataSetId', [
|
||
|
|
'throw' => function () {
|
||
|
|
new InvalidArgumentException(
|
||
|
|
__('Please select a DataSet'),
|
||
|
|
'dataSetId'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
]);
|
||
|
|
$schedule->dataSetParams = $sanitizedParams->getString('dataSetParams');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create fullscreen layout for media/playlist events
|
||
|
|
if ($this->isFullScreenSchedule($schedule->eventTypeId)) {
|
||
|
|
$type = $schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT ? 'media' : 'playlist';
|
||
|
|
$id = ($type === 'media') ? $sanitizedParams->getInt('mediaId') : $sanitizedParams->getInt('playlistId');
|
||
|
|
|
||
|
|
if (!$id) {
|
||
|
|
throw new InvalidArgumentException(
|
||
|
|
sprintf('%sId is required when scheduling %s events.', ucfirst($type), $type)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
$fsLayout = $this->layoutFactory->createFullScreenLayout(
|
||
|
|
$type,
|
||
|
|
$id,
|
||
|
|
$sanitizedParams->getInt('resolutionId'),
|
||
|
|
$sanitizedParams->getString('backgroundColor'),
|
||
|
|
$sanitizedParams->getInt('layoutDuration'),
|
||
|
|
);
|
||
|
|
|
||
|
|
$schedule->campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($fsLayout->layoutId);
|
||
|
|
$schedule->parentCampaignId = $schedule->campaignId;
|
||
|
|
}
|
||
|
|
|
||
|
|
// API request can provide an array of coordinates or valid GeoJSON, handle both cases here.
|
||
|
|
if ($this->isApi($request) && $schedule->isGeoAware === 1) {
|
||
|
|
if ($sanitizedParams->getArray('geoLocation') != null) {
|
||
|
|
// get string array from API
|
||
|
|
$coordinates = $sanitizedParams->getArray('geoLocation');
|
||
|
|
// generate GeoJSON and assign to Schedule
|
||
|
|
$schedule->geoLocation = $this->createGeoJson($coordinates);
|
||
|
|
} else {
|
||
|
|
// we were provided with GeoJSON
|
||
|
|
$schedule->geoLocation = $sanitizedParams->getString('geoLocationJson');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// if we are not using API, then valid GeoJSON is created in the front end.
|
||
|
|
$schedule->geoLocation = $sanitizedParams->getString('geoLocation');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Workaround for cases where we're supplied 0 as the dayPartId (legacy custom dayPart)
|
||
|
|
if ($schedule->dayPartId === 0) {
|
||
|
|
$schedule->dayPartId = $customDayPart->dayPartId;
|
||
|
|
}
|
||
|
|
|
||
|
|
$schedule->syncTimezone = $sanitizedParams->getCheckbox('syncTimezone');
|
||
|
|
$schedule->syncEvent = $this->isSyncEvent($schedule->eventTypeId);
|
||
|
|
$schedule->recurrenceType = $sanitizedParams->getString('recurrenceType');
|
||
|
|
$schedule->recurrenceDetail = $sanitizedParams->getInt('recurrenceDetail');
|
||
|
|
$recurrenceRepeatsOn = $sanitizedParams->getIntArray('recurrenceRepeatsOn');
|
||
|
|
$schedule->recurrenceRepeatsOn = (empty($recurrenceRepeatsOn)) ? null : implode(',', $recurrenceRepeatsOn);
|
||
|
|
$schedule->recurrenceMonthlyRepeatsOn = $sanitizedParams->getInt(
|
||
|
|
'recurrenceMonthlyRepeatsOn',
|
||
|
|
['default' => 0]
|
||
|
|
);
|
||
|
|
|
||
|
|
foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => []]) as $displayGroupId) {
|
||
|
|
$schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!$schedule->isAlwaysDayPart()) {
|
||
|
|
// Handle the dates
|
||
|
|
$fromDt = $sanitizedParams->getDate('fromDt');
|
||
|
|
$toDt = $sanitizedParams->getDate('toDt');
|
||
|
|
$recurrenceRange = $sanitizedParams->getDate('recurrenceRange');
|
||
|
|
|
||
|
|
if ($fromDt === null) {
|
||
|
|
throw new InvalidArgumentException(__('Please enter a from date'), 'fromDt');
|
||
|
|
}
|
||
|
|
|
||
|
|
$logToDt = $toDt?->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$this->getLog()->debug(
|
||
|
|
'Times received are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat())
|
||
|
|
. '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) {
|
||
|
|
// Daypart selected
|
||
|
|
// expect only a start date (no time)
|
||
|
|
$schedule->fromDt = $fromDt->startOfDay()->format('U');
|
||
|
|
$schedule->toDt = null;
|
||
|
|
|
||
|
|
if ($recurrenceRange != null) {
|
||
|
|
$schedule->recurrenceRange = $recurrenceRange->format('U');
|
||
|
|
}
|
||
|
|
|
||
|
|
} else if (!($this->isApi($request) || Str::contains($this->getConfig()->getSetting('DATE_FORMAT'), 's'))) {
|
||
|
|
// In some circumstances we want to trim the seconds from the provided dates.
|
||
|
|
// this happens when the date format provided does not include seconds and when the add
|
||
|
|
// event comes from the UI.
|
||
|
|
$this->getLog()->debug('Date format does not include seconds, removing them');
|
||
|
|
$schedule->fromDt = $fromDt->setTime($fromDt->hour, $fromDt->minute, 0)->format('U');
|
||
|
|
|
||
|
|
if ($toDt !== null) {
|
||
|
|
$schedule->toDt = $toDt->setTime($toDt->hour, $toDt->minute, 0)->format('U');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($recurrenceRange != null) {
|
||
|
|
$schedule->recurrenceRange =
|
||
|
|
$recurrenceRange->setTime(
|
||
|
|
$recurrenceRange->hour,
|
||
|
|
$recurrenceRange->minute,
|
||
|
|
0
|
||
|
|
)->format('U');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$schedule->fromDt = $fromDt->format('U');
|
||
|
|
|
||
|
|
if ($toDt !== null) {
|
||
|
|
$schedule->toDt = $toDt->format('U');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($recurrenceRange != null) {
|
||
|
|
$schedule->recurrenceRange = $recurrenceRange->format('U');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$logToDt = $toDt?->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$this->getLog()->debug(
|
||
|
|
'Processed times are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat())
|
||
|
|
. '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Schedule Criteria
|
||
|
|
$criteria = $sanitizedParams->getArray('criteria');
|
||
|
|
if (is_array($criteria) && count($criteria) > 0) {
|
||
|
|
foreach ($criteria as $item) {
|
||
|
|
$itemParams = $this->getSanitizer($item);
|
||
|
|
$criterion = $this->scheduleCriteriaFactory->createEmpty();
|
||
|
|
$criterion->metric = $itemParams->getString('metric');
|
||
|
|
$criterion->type = $itemParams->getString('type');
|
||
|
|
$criterion->condition = $itemParams->getString('condition');
|
||
|
|
$criterion->value = $itemParams->getString('value');
|
||
|
|
$schedule->addOrUpdateCriteria($criterion);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ready to do the add
|
||
|
|
$schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService());
|
||
|
|
if ($schedule->campaignId != null) {
|
||
|
|
$schedule->setCampaignFactory($this->campaignFactory);
|
||
|
|
}
|
||
|
|
$schedule->save();
|
||
|
|
|
||
|
|
$this->getLog()->debug('Add Schedule Reminder');
|
||
|
|
|
||
|
|
// API Request
|
||
|
|
$rows = [];
|
||
|
|
if ($this->isApi($request)) {
|
||
|
|
$reminders = $sanitizedParams->getArray('scheduleReminders', ['default' => []]);
|
||
|
|
foreach ($reminders as $i => $reminder) {
|
||
|
|
$rows[$i]['reminder_value'] = (int) $reminder['reminder_value'];
|
||
|
|
$rows[$i]['reminder_type'] = (int) $reminder['reminder_type'];
|
||
|
|
$rows[$i]['reminder_option'] = (int) $reminder['reminder_option'];
|
||
|
|
$rows[$i]['reminder_isEmailHidden'] = (int) $reminder['reminder_isEmailHidden'];
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value', ['default' => []])); $i++) {
|
||
|
|
$rows[$i]['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i];
|
||
|
|
$rows[$i]['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i];
|
||
|
|
$rows[$i]['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i];
|
||
|
|
$rows[$i]['reminder_isEmailHidden'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save new reminders
|
||
|
|
foreach ($rows as $reminder) {
|
||
|
|
// Do not add reminder if empty value provided for number of minute/hour
|
||
|
|
if ($reminder['reminder_value'] == 0) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$scheduleReminder = $this->scheduleReminderFactory->createEmpty();
|
||
|
|
$scheduleReminder->scheduleReminderId = null;
|
||
|
|
$scheduleReminder->eventId = $schedule->eventId;
|
||
|
|
$scheduleReminder->value = $reminder['reminder_value'];
|
||
|
|
$scheduleReminder->type = $reminder['reminder_type'];
|
||
|
|
$scheduleReminder->option = $reminder['reminder_option'];
|
||
|
|
$scheduleReminder->isEmail = $reminder['reminder_isEmailHidden'];
|
||
|
|
|
||
|
|
$this->saveReminder($schedule, $scheduleReminder);
|
||
|
|
}
|
||
|
|
|
||
|
|
// We can get schedule reminders in an array
|
||
|
|
if ($this->isApi($request)) {
|
||
|
|
$schedule = $this->scheduleFactory->getById($schedule->eventId);
|
||
|
|
$schedule->load([
|
||
|
|
'loadScheduleReminders' => in_array('scheduleReminders', $embed),
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($this->isSyncEvent($schedule->eventTypeId)) {
|
||
|
|
$syncGroup = $this->syncGroupFactory->getById($schedule->syncGroupId);
|
||
|
|
$syncGroup->validateForSchedule($sanitizedParams);
|
||
|
|
$schedule->updateSyncLinks($syncGroup, $sanitizedParams);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return
|
||
|
|
$this->getState()->hydrate([
|
||
|
|
'httpStatus' => 201,
|
||
|
|
'message' => __('Added Event'),
|
||
|
|
'id' => $schedule->eventId,
|
||
|
|
'data' => $schedule
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Shows a form to edit an event
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param $id
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws AccessDeniedException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
*/
|
||
|
|
function editForm(Request $request, Response $response, $id)
|
||
|
|
{
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
// Recurring event start/end
|
||
|
|
$eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]) / 1000;
|
||
|
|
$eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]) / 1000;
|
||
|
|
|
||
|
|
$schedule = $this->scheduleFactory->getById($id);
|
||
|
|
$schedule->load();
|
||
|
|
|
||
|
|
// Dispatch an event to retrieve criteria for scheduling from the OpenWeatherMap connector.
|
||
|
|
$event = new ScheduleCriteriaRequestEvent();
|
||
|
|
$this->getDispatcher()->dispatch($event, ScheduleCriteriaRequestEvent::$NAME);
|
||
|
|
|
||
|
|
// Retrieve the data from the event
|
||
|
|
$criteria = $event->getCriteria();
|
||
|
|
$criteriaDefaultCondition = $event->getCriteriaDefaultCondition();
|
||
|
|
|
||
|
|
if (!$this->isEventEditable($schedule)) {
|
||
|
|
throw new AccessDeniedException();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fix the event dates for display
|
||
|
|
if ($schedule->isAlwaysDayPart()) {
|
||
|
|
$schedule->fromDt = '';
|
||
|
|
$schedule->toDt = '';
|
||
|
|
} else {
|
||
|
|
$schedule->fromDt =
|
||
|
|
Carbon::createFromTimestamp($schedule->fromDt)
|
||
|
|
->format(DateFormatHelper::getSystemFormat());
|
||
|
|
if ($schedule->toDt != null) {
|
||
|
|
$schedule->toDt =
|
||
|
|
Carbon::createFromTimestamp($schedule->toDt)
|
||
|
|
->format(DateFormatHelper::getSystemFormat());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($schedule->recurrenceRange != null) {
|
||
|
|
$schedule->recurrenceRange =
|
||
|
|
Carbon::createFromTimestamp($schedule->recurrenceRange)
|
||
|
|
->format(DateFormatHelper::getSystemFormat());
|
||
|
|
}
|
||
|
|
// Get all reminders
|
||
|
|
$scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $id]);
|
||
|
|
|
||
|
|
// get the default longitude and latitude from CMS options
|
||
|
|
$defaultLat = (float)$this->getConfig()->getSetting('DEFAULT_LAT');
|
||
|
|
$defaultLong = (float)$this->getConfig()->getSetting('DEFAULT_LONG');
|
||
|
|
|
||
|
|
if ($this->isFullScreenSchedule($schedule->eventTypeId)) {
|
||
|
|
$schedule->setUnmatchedProperty('fullScreenCampaignId', $schedule->campaignId);
|
||
|
|
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT) {
|
||
|
|
$schedule->setUnmatchedProperty(
|
||
|
|
'mediaId',
|
||
|
|
$this->layoutFactory->getLinkedFullScreenMediaId($schedule->campaignId)
|
||
|
|
);
|
||
|
|
} else if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$PLAYLIST_EVENT) {
|
||
|
|
$schedule->setUnmatchedProperty(
|
||
|
|
'playlistId',
|
||
|
|
$this->layoutFactory->getLinkedFullScreenPlaylistId($schedule->campaignId)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the associated fullscreen layout
|
||
|
|
$fsLayout = $this->layoutFactory->getById(
|
||
|
|
$this->campaignFactory->getLinkedLayouts($schedule->campaignId)[0]->layoutId
|
||
|
|
);
|
||
|
|
|
||
|
|
// Set the layout properties
|
||
|
|
$schedule->backgroundColor = $fsLayout->backgroundColor;
|
||
|
|
$schedule->layoutDuration = $fsLayout->duration;
|
||
|
|
$schedule->resolutionId = $this->layoutFactory->getLayoutResolutionId($fsLayout)->resolutionId;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getState()->template = 'schedule-form-edit';
|
||
|
|
$this->getState()->setData([
|
||
|
|
'event' => $schedule,
|
||
|
|
'dayParts' => $this->dayPartFactory->allWithSystem(['isRetired' => 0]),
|
||
|
|
'displayGroups' => $schedule->displayGroups,
|
||
|
|
'campaign' => !empty($schedule->campaignId) ? $this->campaignFactory->getById($schedule->campaignId) : null,
|
||
|
|
'displayGroupIds' => array_map(function ($element) {
|
||
|
|
return $element->displayGroupId;
|
||
|
|
}, $schedule->displayGroups),
|
||
|
|
'addForm' => false,
|
||
|
|
'reminders' => $scheduleReminders,
|
||
|
|
'defaultLat' => $defaultLat,
|
||
|
|
'defaultLong' => $defaultLong,
|
||
|
|
'recurringEvent' => $schedule->recurrenceType != '',
|
||
|
|
'eventStart' => $eventStart,
|
||
|
|
'eventEnd' => $eventEnd,
|
||
|
|
'eventTypes' => \Xibo\Entity\Schedule::getEventTypes(),
|
||
|
|
'scheduleCriteria' => $criteria,
|
||
|
|
'criteriaDefaultCondition' => $criteriaDefaultCondition
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Shows the Delete a Recurring Event form
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param $id
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws AccessDeniedException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
*/
|
||
|
|
public function deleteRecurrenceForm(Request $request, Response $response, $id)
|
||
|
|
{
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
// Recurring event start/end
|
||
|
|
$eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]);
|
||
|
|
$eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]);
|
||
|
|
|
||
|
|
$schedule = $this->scheduleFactory->getById($id);
|
||
|
|
$schedule->load();
|
||
|
|
|
||
|
|
if (!$this->isEventEditable($schedule)) {
|
||
|
|
throw new AccessDeniedException();
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getState()->template = 'schedule-recurrence-form-delete';
|
||
|
|
$this->getState()->setData([
|
||
|
|
'event' => $schedule,
|
||
|
|
'eventStart' => $eventStart,
|
||
|
|
'eventEnd' => $eventEnd,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Deletes a recurring Event from all displays
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param $id
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws AccessDeniedException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
* @SWG\Delete(
|
||
|
|
* path="/schedulerecurrence/{eventId}",
|
||
|
|
* operationId="schedulerecurrenceDelete",
|
||
|
|
* tags={"schedule"},
|
||
|
|
* summary="Delete a Recurring Event",
|
||
|
|
* description="Delete a Recurring Event of a Scheduled Event",
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="eventId",
|
||
|
|
* in="path",
|
||
|
|
* description="The Scheduled Event ID",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Response(
|
||
|
|
* response=204,
|
||
|
|
* description="successful operation"
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public function deleteRecurrence(Request $request, Response $response, $id)
|
||
|
|
{
|
||
|
|
$schedule = $this->scheduleFactory->getById($id);
|
||
|
|
$schedule->load();
|
||
|
|
|
||
|
|
if (!$this->isEventEditable($schedule)) {
|
||
|
|
throw new AccessDeniedException();
|
||
|
|
}
|
||
|
|
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
// Recurring event start/end
|
||
|
|
$eventStart = $sanitizedParams->getInt('eventStart', ['default' => 1000]);
|
||
|
|
$eventEnd = $sanitizedParams->getInt('eventEnd', ['default' => 1000]);
|
||
|
|
$scheduleExclusion = $this->scheduleExclusionFactory->create($schedule->eventId, $eventStart, $eventEnd);
|
||
|
|
|
||
|
|
$this->getLog()->debug('Create a schedule exclusion record');
|
||
|
|
$scheduleExclusion->save();
|
||
|
|
|
||
|
|
// Return
|
||
|
|
$this->getState()->hydrate([
|
||
|
|
'httpStatus' => 204,
|
||
|
|
'message' => __('Deleted Event')
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Edits an event
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param $id
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws AccessDeniedException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
* @SWG\Put(
|
||
|
|
* path="/schedule/{eventId}",
|
||
|
|
* operationId="scheduleEdit",
|
||
|
|
* tags={"schedule"},
|
||
|
|
* summary="Edit Schedule Event",
|
||
|
|
* description="Edit a scheduled event for a Campaign/Layout to be shown on a Display Group/Display.",
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="eventId",
|
||
|
|
* in="path",
|
||
|
|
* description="The Scheduled Event ID",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="eventTypeId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Event Type Id to use for this Event.
|
||
|
|
* 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="campaignId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Campaign ID to use for this Event.
|
||
|
|
* If a Layout is needed then the Campaign specific ID for that Layout should be used.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="commandId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Command ID to use for this Event.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="mediaId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Media ID to use for this Event.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="displayOrder",
|
||
|
|
* in="formData",
|
||
|
|
* description="The display order for this event. ",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="isPriority",
|
||
|
|
* in="formData",
|
||
|
|
* description="An integer indicating the priority of this event. Normal events have a priority of 0.",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="displayGroupIds",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Display Group IDs for this event.
|
||
|
|
* Display specific Group IDs should be used to schedule on single displays.",
|
||
|
|
* type="array",
|
||
|
|
* required=true,
|
||
|
|
* @SWG\Items(type="integer")
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="dayPartId",
|
||
|
|
* in="formData",
|
||
|
|
* description="The Day Part for this event. Overrides supported are 0(custom) and 1(always). Defaulted to 0.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="syncTimezone",
|
||
|
|
* in="formData",
|
||
|
|
* description="Should this schedule be synced to the resulting Display timezone?",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="fromDt",
|
||
|
|
* in="formData",
|
||
|
|
* description="The from date for this event.",
|
||
|
|
* type="string",
|
||
|
|
* format="date-time",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="toDt",
|
||
|
|
* in="formData",
|
||
|
|
* description="The to date for this event.",
|
||
|
|
* type="string",
|
||
|
|
* format="date-time",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceType",
|
||
|
|
* in="formData",
|
||
|
|
* description="The type of recurrence to apply to this event.",
|
||
|
|
* type="string",
|
||
|
|
* required=false,
|
||
|
|
* enum={"", "Minute", "Hour", "Day", "Week", "Month", "Year"}
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceDetail",
|
||
|
|
* in="formData",
|
||
|
|
* description="The interval for the recurrence.",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceRange",
|
||
|
|
* in="formData",
|
||
|
|
* description="The end date for this events recurrence.",
|
||
|
|
* type="string",
|
||
|
|
* format="date-time",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurrenceRepeatsOn",
|
||
|
|
* in="formData",
|
||
|
|
* description="The days of the week that this event repeats - weekly only",
|
||
|
|
* type="string",
|
||
|
|
* format="array",
|
||
|
|
* required=false,
|
||
|
|
* @SWG\Items(type="integer")
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="scheduleReminders",
|
||
|
|
* in="formData",
|
||
|
|
* description="Array of Reminders for this event",
|
||
|
|
* type="array",
|
||
|
|
* required=false,
|
||
|
|
* @SWG\Items(
|
||
|
|
* ref="#/definitions/ScheduleReminderArray"
|
||
|
|
* )
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="isGeoAware",
|
||
|
|
* in="formData",
|
||
|
|
* description="Flag (0-1), whether this event is using Geo Location",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="geoLocation",
|
||
|
|
* in="formData",
|
||
|
|
* description="Array of comma separated strings each with comma separated pair of coordinates",
|
||
|
|
* type="array",
|
||
|
|
* required=false,
|
||
|
|
* @SWG\Items(type="string")
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="geoLocationJson",
|
||
|
|
* in="formData",
|
||
|
|
* description="Valid GeoJSON string, use as an alternative to geoLocation parameter",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="actionType",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Action eventTypeId, the type of the action - command or navLayout",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="actionTriggerCode",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Action eventTypeId, the webhook trigger code for the Action",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="actionLayoutCode",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Action eventTypeId and navLayout actionType, the Layout Code identifier",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="dataSetId",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Data Connector eventTypeId, the DataSet ID",
|
||
|
|
* type="integer",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="dataSetParams",
|
||
|
|
* in="formData",
|
||
|
|
* description="For Data Connector eventTypeId, the DataSet params",
|
||
|
|
* type="string",
|
||
|
|
* required=false
|
||
|
|
* ),
|
||
|
|
* @SWG\Response(
|
||
|
|
* response=200,
|
||
|
|
* description="successful operation",
|
||
|
|
* @SWG\Schema(ref="#/definitions/Schedule")
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public function edit(Request $request, Response $response, $id)
|
||
|
|
{
|
||
|
|
$sanitizedParams = $this->getSanitizer($request->getParams());
|
||
|
|
$embed = ($sanitizedParams->getString('embed') != null) ? explode(',', $sanitizedParams->getString('embed')) : [];
|
||
|
|
|
||
|
|
$schedule = $this->scheduleFactory->getById($id);
|
||
|
|
$oldSchedule = clone $schedule;
|
||
|
|
|
||
|
|
$schedule->load([
|
||
|
|
'loadScheduleReminders' => in_array('scheduleReminders', $embed),
|
||
|
|
]);
|
||
|
|
|
||
|
|
if (!$this->isEventEditable($schedule)) {
|
||
|
|
throw new AccessDeniedException();
|
||
|
|
}
|
||
|
|
|
||
|
|
$schedule->eventTypeId = $sanitizedParams->getInt('eventTypeId');
|
||
|
|
$schedule->campaignId = $this->isFullScreenSchedule($schedule->eventTypeId)
|
||
|
|
? $sanitizedParams->getInt('fullScreenCampaignId')
|
||
|
|
: $sanitizedParams->getInt('campaignId');
|
||
|
|
// displayOrder and isPriority: if present but empty (""): set to 0
|
||
|
|
// if missing from form: keep existing value (fallback to 0 if unset)
|
||
|
|
$schedule->displayOrder = $sanitizedParams->hasParam('displayOrder')
|
||
|
|
? $sanitizedParams->getInt('displayOrder', ['default' => 0])
|
||
|
|
: ($schedule->displayOrder ?? 0);
|
||
|
|
|
||
|
|
$schedule->isPriority = $sanitizedParams->hasParam('isPriority')
|
||
|
|
? $sanitizedParams->getInt('isPriority', ['default' => 0])
|
||
|
|
: ($schedule->isPriority ?? 0);
|
||
|
|
|
||
|
|
$schedule->dayPartId = $sanitizedParams->getInt('dayPartId', ['default' => $schedule->dayPartId]);
|
||
|
|
$schedule->syncTimezone = $sanitizedParams->getCheckbox('syncTimezone');
|
||
|
|
$schedule->syncEvent = $this->isSyncEvent($schedule->eventTypeId);
|
||
|
|
$schedule->recurrenceType = $sanitizedParams->getString('recurrenceType');
|
||
|
|
$schedule->recurrenceDetail = $sanitizedParams->getInt('recurrenceDetail');
|
||
|
|
$recurrenceRepeatsOn = $sanitizedParams->getIntArray('recurrenceRepeatsOn');
|
||
|
|
$schedule->recurrenceRepeatsOn = (empty($recurrenceRepeatsOn)) ? null : implode(',', $recurrenceRepeatsOn);
|
||
|
|
$schedule->recurrenceMonthlyRepeatsOn = $sanitizedParams->getInt(
|
||
|
|
'recurrenceMonthlyRepeatsOn',
|
||
|
|
['default' => 0]
|
||
|
|
);
|
||
|
|
$schedule->displayGroups = [];
|
||
|
|
$schedule->isGeoAware = $sanitizedParams->getCheckbox('isGeoAware');
|
||
|
|
$schedule->maxPlaysPerHour = $sanitizedParams->getInt('maxPlaysPerHour', ['default' => 0]);
|
||
|
|
$schedule->syncGroupId = $sanitizedParams->getInt('syncGroupId');
|
||
|
|
$schedule->name = $sanitizedParams->getString('name');
|
||
|
|
$schedule->modifiedBy = $this->getUser()->getId();
|
||
|
|
|
||
|
|
// collect action event relevant properties only on action event
|
||
|
|
// null these properties otherwise
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$ACTION_EVENT) {
|
||
|
|
$schedule->actionType = $sanitizedParams->getString('actionType');
|
||
|
|
$schedule->actionTriggerCode = $sanitizedParams->getString('actionTriggerCode');
|
||
|
|
$schedule->commandId = $sanitizedParams->getInt('commandId');
|
||
|
|
$schedule->actionLayoutCode = $sanitizedParams->getString('actionLayoutCode');
|
||
|
|
$schedule->campaignId = null;
|
||
|
|
} else {
|
||
|
|
$schedule->actionType = null;
|
||
|
|
$schedule->actionTriggerCode = null;
|
||
|
|
$schedule->commandId = null;
|
||
|
|
$schedule->actionLayoutCode = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// collect commandId on Command event
|
||
|
|
// Retain existing commandId value otherwise
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$COMMAND_EVENT) {
|
||
|
|
$schedule->commandId = $sanitizedParams->getInt('commandId');
|
||
|
|
$schedule->campaignId = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set the parentCampaignId for campaign events
|
||
|
|
// null parentCampaignId on other events
|
||
|
|
// make sure correct Layout/Campaign is selected for relevant event.
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$CAMPAIGN_EVENT) {
|
||
|
|
$schedule->parentCampaignId = $schedule->campaignId;
|
||
|
|
|
||
|
|
// Make sure we're not directly scheduling an ad campaign
|
||
|
|
$campaign = $this->campaignFactory->getById($schedule->campaignId);
|
||
|
|
if ($campaign->type === 'ad') {
|
||
|
|
throw new InvalidArgumentException(
|
||
|
|
__('Direct scheduling of an Ad Campaign is not allowed'),
|
||
|
|
'campaignId'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($campaign->isLayoutSpecific === 1) {
|
||
|
|
throw new InvalidArgumentException(
|
||
|
|
__('Cannot schedule Layout as a Campaign, please select a Campaign instead.'),
|
||
|
|
'campaignId'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$schedule->parentCampaignId = null;
|
||
|
|
if (!empty($schedule->campaignId)) {
|
||
|
|
$campaign = $this->campaignFactory->getById($schedule->campaignId);
|
||
|
|
if ($campaign->isLayoutSpecific === 0) {
|
||
|
|
throw new InvalidArgumentException(
|
||
|
|
__('Cannot schedule Campaign in selected event type, please select a Layout instead.'),
|
||
|
|
'campaignId'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fields only collected for interrupt events
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
|
||
|
|
$schedule->shareOfVoice = $sanitizedParams->getInt('shareOfVoice', [
|
||
|
|
'throw' => function () {
|
||
|
|
new InvalidArgumentException(
|
||
|
|
__('Share of Voice must be a whole number between 0 and 3600'),
|
||
|
|
'shareOfVoice'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
]);
|
||
|
|
} else {
|
||
|
|
$schedule->shareOfVoice = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fields only collected for data connector events
|
||
|
|
if ($schedule->eventTypeId === \Xibo\Entity\Schedule::$DATA_CONNECTOR_EVENT) {
|
||
|
|
$schedule->dataSetId = $sanitizedParams->getInt('dataSetId', [
|
||
|
|
'throw' => function () {
|
||
|
|
new InvalidArgumentException(
|
||
|
|
__('Please select a DataSet'),
|
||
|
|
'dataSetId'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
]);
|
||
|
|
$schedule->dataSetParams = $sanitizedParams->getString('dataSetParams');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get the campaignId for media/playlist events
|
||
|
|
if ($this->isFullScreenSchedule($schedule->eventTypeId)) {
|
||
|
|
$type = $schedule->eventTypeId === \Xibo\Entity\Schedule::$MEDIA_EVENT ? 'media' : 'playlist';
|
||
|
|
$id = ($type === 'media') ? $sanitizedParams->getInt('mediaId') : $sanitizedParams->getInt('playlistId');
|
||
|
|
|
||
|
|
if (!$id) {
|
||
|
|
throw new InvalidArgumentException(
|
||
|
|
sprintf('%sId is required when scheduling %s events.', ucfirst($type), $type)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create a full screen layout for this event
|
||
|
|
$fsLayout = $this->layoutFactory->createFullScreenLayout(
|
||
|
|
$type,
|
||
|
|
$id,
|
||
|
|
$sanitizedParams->getInt('resolutionId'),
|
||
|
|
$sanitizedParams->getString('backgroundColor'),
|
||
|
|
$sanitizedParams->getInt('layoutDuration'),
|
||
|
|
);
|
||
|
|
|
||
|
|
$schedule->campaignId = $this->layoutFactory->getCampaignIdFromLayoutHistory($fsLayout->layoutId);
|
||
|
|
$schedule->parentCampaignId = $schedule->campaignId;
|
||
|
|
}
|
||
|
|
|
||
|
|
// API request can provide an array of coordinates or valid GeoJSON, handle both cases here.
|
||
|
|
if ($this->isApi($request) && $schedule->isGeoAware === 1) {
|
||
|
|
if ($sanitizedParams->getArray('geoLocation') != null) {
|
||
|
|
// get string array from API
|
||
|
|
$coordinates = $sanitizedParams->getArray('geoLocation');
|
||
|
|
// generate GeoJSON and assign to Schedule
|
||
|
|
$schedule->geoLocation = $this->createGeoJson($coordinates);
|
||
|
|
} else {
|
||
|
|
// we were provided with GeoJSON
|
||
|
|
$schedule->geoLocation = $sanitizedParams->getString('geoLocationJson');
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
// if we are not using API, then valid GeoJSON is created in the front end.
|
||
|
|
$schedule->geoLocation = $sanitizedParams->getString('geoLocation');
|
||
|
|
}
|
||
|
|
|
||
|
|
// if we are editing Layout/Campaign event that was set with Always daypart and change it to Command event type
|
||
|
|
// the daypartId will remain as always, which will then cause the event to "disappear" from calendar
|
||
|
|
// https://github.com/xibosignage/xibo/issues/1982
|
||
|
|
if ($schedule->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) {
|
||
|
|
$schedule->dayPartId = $this->dayPartFactory->getCustomDayPart()->dayPartId;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($sanitizedParams->getIntArray('displayGroupIds', ['default' => []]) as $displayGroupId) {
|
||
|
|
$schedule->assignDisplayGroup($this->displayGroupFactory->getById($displayGroupId));
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!$schedule->isAlwaysDayPart()) {
|
||
|
|
// Handle the dates
|
||
|
|
$fromDt = $sanitizedParams->getDate('fromDt');
|
||
|
|
$toDt = $sanitizedParams->getDate('toDt');
|
||
|
|
$recurrenceRange = $sanitizedParams->getDate('recurrenceRange');
|
||
|
|
|
||
|
|
if ($fromDt === null) {
|
||
|
|
throw new InvalidArgumentException(__('Please enter a from date'). 'fromDt');
|
||
|
|
}
|
||
|
|
|
||
|
|
$logToDt = $toDt?->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$logRecurrenceRange = $recurrenceRange?->format(DateFormatHelper::getSystemFormat());
|
||
|
|
$this->getLog()->debug(
|
||
|
|
'Times received are: FromDt=' . $fromDt->format(DateFormatHelper::getSystemFormat())
|
||
|
|
. '. ToDt=' . $logToDt . '. recurrenceRange=' . $logRecurrenceRange
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!$schedule->isCustomDayPart() && !$schedule->isAlwaysDayPart()) {
|
||
|
|
// Daypart selected
|
||
|
|
// expect only a start date (no time)
|
||
|
|
$schedule->fromDt = $fromDt->startOfDay()->format('U');
|
||
|
|
$schedule->toDt = null;
|
||
|
|
$schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U');
|
||
|
|
|
||
|
|
} else if (!($this->isApi($request) || Str::contains($this->getConfig()->getSetting('DATE_FORMAT'), 's'))) {
|
||
|
|
// In some circumstances we want to trim the seconds from the provided dates.
|
||
|
|
// this happens when the date format provided does not include seconds and when the add
|
||
|
|
// event comes from the UI.
|
||
|
|
$this->getLog()->debug('Date format does not include seconds, removing them');
|
||
|
|
$schedule->fromDt = $fromDt->setTime($fromDt->hour, $fromDt->minute, 0)->format('U');
|
||
|
|
|
||
|
|
// If we have a toDt
|
||
|
|
if ($toDt !== null) {
|
||
|
|
$schedule->toDt = $toDt->setTime($toDt->hour, $toDt->minute, 0)->format('U');
|
||
|
|
}
|
||
|
|
|
||
|
|
$schedule->recurrenceRange = ($recurrenceRange === null)
|
||
|
|
? null
|
||
|
|
: $recurrenceRange->setTime($recurrenceRange->hour, $recurrenceRange->minute, 0)->format('U');
|
||
|
|
} else {
|
||
|
|
$schedule->fromDt = $fromDt->format('U');
|
||
|
|
|
||
|
|
if ($toDt !== null) {
|
||
|
|
$schedule->toDt = $toDt->format('U');
|
||
|
|
}
|
||
|
|
|
||
|
|
$schedule->recurrenceRange = ($recurrenceRange === null) ? null : $recurrenceRange->format('U');
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getLog()->debug('Processed start is: FromDt=' . $fromDt->toRssString());
|
||
|
|
} else {
|
||
|
|
// This is an always day part, which cannot be recurring, make sure we clear the recurring type if it has been set
|
||
|
|
$schedule->recurrenceType = null;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Schedule Criteria
|
||
|
|
$schedule->criteria = [];
|
||
|
|
$criteria = $sanitizedParams->getArray('criteria');
|
||
|
|
if (is_array($criteria)) {
|
||
|
|
foreach ($criteria as $item) {
|
||
|
|
$itemParams = $this->getSanitizer($item);
|
||
|
|
$criterion = $this->scheduleCriteriaFactory->createEmpty();
|
||
|
|
$criterion->metric = $itemParams->getString('metric');
|
||
|
|
$criterion->type = $itemParams->getString('type');
|
||
|
|
$criterion->condition = $itemParams->getString('condition');
|
||
|
|
$criterion->value = $itemParams->getString('value');
|
||
|
|
$schedule->addOrUpdateCriteria($criterion, $itemParams->getInt('id'));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Ready to do the add
|
||
|
|
$schedule->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService());
|
||
|
|
if ($schedule->campaignId != null) {
|
||
|
|
$schedule->setCampaignFactory($this->campaignFactory);
|
||
|
|
}
|
||
|
|
$schedule->save();
|
||
|
|
|
||
|
|
if ($this->isSyncEvent($schedule->eventTypeId)) {
|
||
|
|
$syncGroup = $this->syncGroupFactory->getById($schedule->syncGroupId);
|
||
|
|
$syncGroup->validateForSchedule($sanitizedParams);
|
||
|
|
$schedule->updateSyncLinks($syncGroup, $sanitizedParams);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get form reminders
|
||
|
|
$rows = [];
|
||
|
|
for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value',['default' => []])); $i++) {
|
||
|
|
$entry = [];
|
||
|
|
|
||
|
|
if ($sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i] == null) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$entry['reminder_scheduleReminderId'] = $sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i];
|
||
|
|
$entry['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i];
|
||
|
|
$entry['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i];
|
||
|
|
$entry['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i];
|
||
|
|
$entry['reminder_isEmail'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i];
|
||
|
|
|
||
|
|
$rows[$sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i]] = $entry;
|
||
|
|
}
|
||
|
|
$formReminders = $rows;
|
||
|
|
|
||
|
|
// Compare to delete
|
||
|
|
// Get existing db reminders
|
||
|
|
$scheduleReminders = $this->scheduleReminderFactory->query(null, ['eventId' => $id]);
|
||
|
|
|
||
|
|
$rows = [];
|
||
|
|
foreach ($scheduleReminders as $reminder) {
|
||
|
|
|
||
|
|
$entry = [];
|
||
|
|
$entry['reminder_scheduleReminderId'] = $reminder->scheduleReminderId;
|
||
|
|
$entry['reminder_value'] = $reminder->value;
|
||
|
|
$entry['reminder_type'] = $reminder->type;
|
||
|
|
$entry['reminder_option'] = $reminder->option;
|
||
|
|
$entry['reminder_isEmail'] = $reminder->isEmail;
|
||
|
|
|
||
|
|
$rows[$reminder->scheduleReminderId] = $entry;
|
||
|
|
}
|
||
|
|
$dbReminders = $rows;
|
||
|
|
|
||
|
|
$deleteReminders = $schedule->compareMultidimensionalArrays($dbReminders, $formReminders, false);
|
||
|
|
foreach ($deleteReminders as $reminder) {
|
||
|
|
$reminder = $this->scheduleReminderFactory->getById($reminder['reminder_scheduleReminderId']);
|
||
|
|
$reminder->delete();
|
||
|
|
}
|
||
|
|
|
||
|
|
// API Request
|
||
|
|
$rows = [];
|
||
|
|
if ($this->isApi($request)) {
|
||
|
|
|
||
|
|
$reminders = $sanitizedParams->getArray('scheduleReminders', ['default' => []]);
|
||
|
|
foreach ($reminders as $i => $reminder) {
|
||
|
|
|
||
|
|
$rows[$i]['reminder_scheduleReminderId'] = isset($reminder['reminder_scheduleReminderId']) ? (int) $reminder['reminder_scheduleReminderId'] : null;
|
||
|
|
$rows[$i]['reminder_value'] = (int) $reminder['reminder_value'];
|
||
|
|
$rows[$i]['reminder_type'] = (int) $reminder['reminder_type'];
|
||
|
|
$rows[$i]['reminder_option'] = (int) $reminder['reminder_option'];
|
||
|
|
$rows[$i]['reminder_isEmailHidden'] = (int) $reminder['reminder_isEmailHidden'];
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
|
||
|
|
for ($i=0; $i < count($sanitizedParams->getIntArray('reminder_value', ['default' => []])); $i++) {
|
||
|
|
$rows[$i]['reminder_scheduleReminderId'] = $sanitizedParams->getIntArray('reminder_scheduleReminderId')[$i];
|
||
|
|
$rows[$i]['reminder_value'] = $sanitizedParams->getIntArray('reminder_value')[$i];
|
||
|
|
$rows[$i]['reminder_type'] = $sanitizedParams->getIntArray('reminder_type')[$i];
|
||
|
|
$rows[$i]['reminder_option'] = $sanitizedParams->getIntArray('reminder_option')[$i];
|
||
|
|
$rows[$i]['reminder_isEmailHidden'] = $sanitizedParams->getIntArray('reminder_isEmailHidden')[$i];
|
||
|
|
}
|
||
|
|
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save rest of the reminders
|
||
|
|
foreach ($rows as $reminder) {
|
||
|
|
|
||
|
|
// Do not add reminder if empty value provided for number of minute/hour
|
||
|
|
if ($reminder['reminder_value'] == 0) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$scheduleReminderId = isset($reminder['reminder_scheduleReminderId']) ? $reminder['reminder_scheduleReminderId'] : null;
|
||
|
|
|
||
|
|
try {
|
||
|
|
$scheduleReminder = $this->scheduleReminderFactory->getById($scheduleReminderId);
|
||
|
|
$scheduleReminder->load();
|
||
|
|
} catch (NotFoundException $e) {
|
||
|
|
$scheduleReminder = $this->scheduleReminderFactory->createEmpty();
|
||
|
|
$scheduleReminder->scheduleReminderId = null;
|
||
|
|
$scheduleReminder->eventId = $id;
|
||
|
|
}
|
||
|
|
|
||
|
|
$scheduleReminder->value = $reminder['reminder_value'];
|
||
|
|
$scheduleReminder->type = $reminder['reminder_type'];
|
||
|
|
$scheduleReminder->option = $reminder['reminder_option'];
|
||
|
|
$scheduleReminder->isEmail = $reminder['reminder_isEmailHidden'];
|
||
|
|
|
||
|
|
$this->saveReminder($schedule, $scheduleReminder);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If this is a recurring event delete all schedule exclusions
|
||
|
|
if ($schedule->recurrenceType != '') {
|
||
|
|
// Delete schedule exclusions
|
||
|
|
$scheduleExclusions = $this->scheduleExclusionFactory->query(null, ['eventId' => $schedule->eventId]);
|
||
|
|
foreach ($scheduleExclusions as $exclusion) {
|
||
|
|
$exclusion->delete();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Return
|
||
|
|
$this->getState()->hydrate([
|
||
|
|
'message' => __('Edited Event'),
|
||
|
|
'id' => $schedule->eventId,
|
||
|
|
'data' => $schedule
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Shows the DeleteEvent form
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param $id
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws AccessDeniedException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
*/
|
||
|
|
function deleteForm(Request $request, Response $response, $id)
|
||
|
|
{
|
||
|
|
$schedule = $this->scheduleFactory->getById($id);
|
||
|
|
$schedule->load();
|
||
|
|
|
||
|
|
if (!$this->isEventEditable($schedule)) {
|
||
|
|
throw new AccessDeniedException();
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->getState()->template = 'schedule-form-delete';
|
||
|
|
$this->getState()->setData([
|
||
|
|
'event' => $schedule,
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request,$response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Deletes an Event from all displays
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @param $id
|
||
|
|
* @return \Psr\Http\Message\ResponseInterface|Response
|
||
|
|
* @throws AccessDeniedException
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
* @SWG\Delete(
|
||
|
|
* path="/schedule/{eventId}",
|
||
|
|
* operationId="scheduleDelete",
|
||
|
|
* tags={"schedule"},
|
||
|
|
* summary="Delete Event",
|
||
|
|
* description="Delete a Scheduled Event",
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="eventId",
|
||
|
|
* in="path",
|
||
|
|
* description="The Scheduled Event ID",
|
||
|
|
* type="integer",
|
||
|
|
* required=true
|
||
|
|
* ),
|
||
|
|
* @SWG\Response(
|
||
|
|
* response=204,
|
||
|
|
* description="successful operation"
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
*/
|
||
|
|
public function delete(Request $request, Response $response, $id)
|
||
|
|
{
|
||
|
|
$schedule = $this->scheduleFactory->getById($id);
|
||
|
|
$schedule->load();
|
||
|
|
|
||
|
|
if (!$this->isEventEditable($schedule)) {
|
||
|
|
throw new AccessDeniedException();
|
||
|
|
}
|
||
|
|
|
||
|
|
$schedule
|
||
|
|
->setDisplayNotifyService($this->displayFactory->getDisplayNotifyService())
|
||
|
|
->delete();
|
||
|
|
|
||
|
|
// Return
|
||
|
|
$this->getState()->hydrate([
|
||
|
|
'httpStatus' => 204,
|
||
|
|
'message' => __('Deleted Event')
|
||
|
|
]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Is this event editable?
|
||
|
|
* @param \Xibo\Entity\Schedule $event
|
||
|
|
* @return bool
|
||
|
|
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
||
|
|
*/
|
||
|
|
private function isEventEditable(\Xibo\Entity\Schedule $event): bool
|
||
|
|
{
|
||
|
|
if (!$this->getUser()->featureEnabled('schedule.modify')) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Is this an event coming from an ad campaign?
|
||
|
|
if (!empty($event->parentCampaignId) && $event->eventTypeId === \Xibo\Entity\Schedule::$INTERRUPT_EVENT) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$scheduleWithView = ($this->getConfig()->getSetting('SCHEDULE_WITH_VIEW_PERMISSION') == 1);
|
||
|
|
|
||
|
|
// Work out if this event is editable or not. To do this we need to compare the permissions
|
||
|
|
// of each display group this event is associated with
|
||
|
|
foreach ($event->displayGroups as $displayGroup) {
|
||
|
|
// Can schedule with view, but no view permissions
|
||
|
|
if ($scheduleWithView && !$this->getUser()->checkViewable($displayGroup)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Can't schedule with view, but no edit permissions
|
||
|
|
if (!$scheduleWithView && !$this->getUser()->checkEditable($displayGroup)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return Schedule eventTypeId based on the grid the requests is coming from
|
||
|
|
* @param $from
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
private function getEventTypeId($from)
|
||
|
|
{
|
||
|
|
return match ($from) {
|
||
|
|
'Campaign' => \Xibo\Entity\Schedule::$CAMPAIGN_EVENT,
|
||
|
|
'Library' => \Xibo\Entity\Schedule::$MEDIA_EVENT,
|
||
|
|
'Playlist' => \Xibo\Entity\Schedule::$PLAYLIST_EVENT,
|
||
|
|
default => \Xibo\Entity\Schedule::$LAYOUT_EVENT
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Generates the Schedule events grid
|
||
|
|
*
|
||
|
|
* @SWG\Get(
|
||
|
|
* path="/schedule",
|
||
|
|
* operationId="scheduleSearch",
|
||
|
|
* tags={"schedule"},
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="eventTypeId",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="integer",
|
||
|
|
* description="Filter grid by eventTypeId.
|
||
|
|
* 1=Layout, 2=Command, 3=Overlay, 4=Interrupt, 5=Campaign, 6=Action, 7=Media Library, 8=Playlist"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="fromDt",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="string",
|
||
|
|
* description="From Date in Y-m-d H:i:s format"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="toDt",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="string",
|
||
|
|
* description="To Date in Y-m-d H:i:s format"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="geoAware",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="integer",
|
||
|
|
* description="Flag (0-1), whether to return events using Geo Location"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="recurring",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="integer",
|
||
|
|
* description="Flag (0-1), whether to return Recurring events"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="campaignId",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="integer",
|
||
|
|
* description="Filter events by specific campaignId"
|
||
|
|
* ),
|
||
|
|
* @SWG\Parameter(
|
||
|
|
* name="displayGroupIds",
|
||
|
|
* in="query",
|
||
|
|
* required=false,
|
||
|
|
* type="array",
|
||
|
|
* description="Filter events by an array of Display Group Ids",
|
||
|
|
* @SWG\Items(type="integer")
|
||
|
|
* ),
|
||
|
|
* @SWG\Response(
|
||
|
|
* response=200,
|
||
|
|
* description="successful operation",
|
||
|
|
* @SWG\Schema(
|
||
|
|
* type="array",
|
||
|
|
* @SWG\Items(ref="#/definitions/Schedule")
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
* )
|
||
|
|
* @param Request $request
|
||
|
|
* @param Response $response
|
||
|
|
* @return ResponseInterface|Response
|
||
|
|
* @throws ControllerNotImplemented
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws InvalidArgumentException
|
||
|
|
* @throws NotFoundException
|
||
|
|
*/
|
||
|
|
public function grid(Request $request, Response $response)
|
||
|
|
{
|
||
|
|
$params = $this->getSanitizer($request->getParams());
|
||
|
|
|
||
|
|
$displayGroupIds = $params->getIntArray('displayGroupIds', ['default' => []]);
|
||
|
|
$displaySpecificDisplayGroupIds = $params->getIntArray('displaySpecificGroupIds', ['default' => []]);
|
||
|
|
$originalDisplayGroupIds = array_merge($displayGroupIds, $displaySpecificDisplayGroupIds);
|
||
|
|
|
||
|
|
if (!$this->getUser()->isSuperAdmin()) {
|
||
|
|
$userDisplayGroupIds = array_map(function ($element) {
|
||
|
|
/** @var \Xibo\Entity\DisplayGroup $element */
|
||
|
|
return $element->displayGroupId;
|
||
|
|
}, $this->displayGroupFactory->query(null, ['isDisplaySpecific' => -1]));
|
||
|
|
|
||
|
|
// Reset the list to only those display groups that intersect and if 0 have been provided, only those from
|
||
|
|
// the user list
|
||
|
|
$resolvedDisplayGroupIds = (count($originalDisplayGroupIds) > 0)
|
||
|
|
? array_intersect($originalDisplayGroupIds, $userDisplayGroupIds)
|
||
|
|
: $userDisplayGroupIds;
|
||
|
|
|
||
|
|
$this->getLog()->debug('Resolved list of display groups ['
|
||
|
|
. json_encode($displayGroupIds) . '] from provided list ['
|
||
|
|
. json_encode($originalDisplayGroupIds) . '] and user list ['
|
||
|
|
. json_encode($userDisplayGroupIds) . ']');
|
||
|
|
|
||
|
|
// If we have none, then we do not return any events.
|
||
|
|
if (count($resolvedDisplayGroupIds) <= 0) {
|
||
|
|
$this->getState()->template = 'grid';
|
||
|
|
$this->getState()->recordsTotal = $this->scheduleFactory->countLast();
|
||
|
|
$this->getState()->setData([]);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
$resolvedDisplayGroupIds = $originalDisplayGroupIds;
|
||
|
|
}
|
||
|
|
|
||
|
|
$events = $this->scheduleFactory->query(
|
||
|
|
$this->gridRenderSort($params),
|
||
|
|
$this->gridRenderFilter([
|
||
|
|
'eventTypeId' => $params->getInt('eventTypeId'),
|
||
|
|
'futureSchedulesFrom' => $params->getDate('fromDt')?->format('U'),
|
||
|
|
'futureSchedulesTo' => $params->getDate('toDt')?->format('U'),
|
||
|
|
'geoAware' => $params->getInt('geoAware'),
|
||
|
|
'recurring' => $params->getInt('recurring'),
|
||
|
|
'campaignId' => $params->getInt('campaignId'),
|
||
|
|
'displayGroupIds' => $resolvedDisplayGroupIds,
|
||
|
|
'name' => $params->getString('name'),
|
||
|
|
'useRegexForName' => $params->getCheckbox('useRegexForName'),
|
||
|
|
'logicalOperatorName' => $params->getString('logicalOperatorName'),
|
||
|
|
'directSchedule' => $params->getCheckbox('directSchedule'),
|
||
|
|
'sharedSchedule' => $params->getCheckbox('sharedSchedule'),
|
||
|
|
'gridFilter' => 1,
|
||
|
|
], $params)
|
||
|
|
);
|
||
|
|
|
||
|
|
// Grab some settings which determine how events are displayed.
|
||
|
|
$showLayoutName = ($this->getConfig()->getSetting('SCHEDULE_SHOW_LAYOUT_NAME') == 1);
|
||
|
|
$defaultTimezone = $this->getConfig()->getSetting('defaultTimezone');
|
||
|
|
|
||
|
|
foreach ($events as $event) {
|
||
|
|
$event->load();
|
||
|
|
|
||
|
|
if (count($event->displayGroups) > 0) {
|
||
|
|
$array = array_map(function ($object) {
|
||
|
|
return $object->displayGroup;
|
||
|
|
}, $event->displayGroups);
|
||
|
|
$displayGroupList = implode(', ', $array);
|
||
|
|
} else {
|
||
|
|
$displayGroupList = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
$eventTypes = \Xibo\Entity\Schedule::getEventTypes();
|
||
|
|
foreach ($eventTypes as $eventType) {
|
||
|
|
if ($eventType['eventTypeId'] === $event->eventTypeId) {
|
||
|
|
$event->setUnmatchedProperty('eventTypeName', $eventType['eventTypeName']);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
$event->setUnmatchedProperty('displayGroupList', $displayGroupList);
|
||
|
|
$event->setUnmatchedProperty('recurringEvent', !empty($event->recurrenceType));
|
||
|
|
|
||
|
|
if ($this->isSyncEvent($event->eventTypeId)) {
|
||
|
|
$event->setUnmatchedProperty(
|
||
|
|
'displayGroupList',
|
||
|
|
$event->getUnmatchedProperty('syncGroupName')
|
||
|
|
);
|
||
|
|
$event->setUnmatchedProperty(
|
||
|
|
'syncType',
|
||
|
|
$event->getSyncTypeForEvent()
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!$showLayoutName && !$this->getUser()->isSuperAdmin() && !empty($event->campaignId)) {
|
||
|
|
// Campaign
|
||
|
|
$campaign = $this->campaignFactory->getById($event->campaignId);
|
||
|
|
|
||
|
|
if (!$this->getUser()->checkViewable($campaign)) {
|
||
|
|
$event->campaign = __('Private Item');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($event->recurrenceType)) {
|
||
|
|
$repeatsOn = '';
|
||
|
|
$repeatsUntil = '';
|
||
|
|
|
||
|
|
if ($event->recurrenceType === 'Week' && !empty($event->recurrenceRepeatsOn)) {
|
||
|
|
$weekdays = Carbon::getDays();
|
||
|
|
$repeatDays = explode(',', $event->recurrenceRepeatsOn);
|
||
|
|
$i = 0;
|
||
|
|
foreach ($repeatDays as $repeatDay) {
|
||
|
|
// Carbon getDays starts with Sunday,
|
||
|
|
// return first element from that array if in our array we have 7 (Sunday)
|
||
|
|
$repeatDay = ($repeatDay == 7) ? 0 : $repeatDay;
|
||
|
|
$repeatsOn .= $weekdays[$repeatDay];
|
||
|
|
if ($i < count($repeatDays) - 1) {
|
||
|
|
$repeatsOn .= ', ';
|
||
|
|
}
|
||
|
|
$i++;
|
||
|
|
}
|
||
|
|
} else if ($event->recurrenceType === 'Month') {
|
||
|
|
// Force the timezone for this date (schedule from/to dates are timezone agnostic, but this
|
||
|
|
// date still has timezone information, which could lead to use formatting as the wrong day)
|
||
|
|
$date = Carbon::parse($event->fromDt)->tz($defaultTimezone);
|
||
|
|
$this->getLog()->debug('grid: setting description for monthly event with date: '
|
||
|
|
. $date->toAtomString());
|
||
|
|
|
||
|
|
if ($event->recurrenceMonthlyRepeatsOn === 0) {
|
||
|
|
$repeatsOn = 'the ' . $date->format('jS') . ' day of the month';
|
||
|
|
} else {
|
||
|
|
// Which day of the month is this?
|
||
|
|
$firstDay = Carbon::parse('first ' . $date->format('l') . ' of ' . $date->format('F'));
|
||
|
|
|
||
|
|
$this->getLog()->debug('grid: the first day of the month for this date is: '
|
||
|
|
. $firstDay->toAtomString());
|
||
|
|
|
||
|
|
$nth = $firstDay->diffInDays($date) / 7 + 1;
|
||
|
|
$repeatWeekDayDate = $date->copy()->setDay($nth)->format('jS');
|
||
|
|
$repeatsOn = 'the ' . $repeatWeekDayDate . ' '
|
||
|
|
. $date->format('l')
|
||
|
|
. ' of the month';
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!empty($event->recurrenceRange)) {
|
||
|
|
$repeatsUntil = Carbon::createFromTimestamp($event->recurrenceRange)
|
||
|
|
->format(DateFormatHelper::getSystemFormat());
|
||
|
|
}
|
||
|
|
|
||
|
|
$event->setUnmatchedProperty(
|
||
|
|
'recurringEventDescription',
|
||
|
|
__(sprintf(
|
||
|
|
'Repeats every %d %s %s %s',
|
||
|
|
$event->recurrenceDetail,
|
||
|
|
$event->recurrenceType . ($event->recurrenceDetail > 1 ? 's' : ''),
|
||
|
|
!empty($repeatsOn) ? 'on ' . $repeatsOn : '',
|
||
|
|
!empty($repeatsUntil) ? ' until ' . $repeatsUntil : ''
|
||
|
|
))
|
||
|
|
);
|
||
|
|
} else {
|
||
|
|
$event->setUnmatchedProperty('recurringEventDescription', '');
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!$event->isAlwaysDayPart() && !$event->isCustomDayPart()) {
|
||
|
|
$dayPart = $this->dayPartFactory->getById($event->dayPartId);
|
||
|
|
$dayPart->adjustForDate(Carbon::createFromTimestamp($event->fromDt));
|
||
|
|
$event->fromDt = $dayPart->adjustedStart->format('U');
|
||
|
|
$event->toDt = $dayPart->adjustedEnd->format('U');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($event->eventTypeId == \Xibo\Entity\Schedule::$COMMAND_EVENT) {
|
||
|
|
$event->toDt = $event->fromDt;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Set the row from/to date to be an ISO date for display (no timezone)
|
||
|
|
$event->setUnmatchedProperty(
|
||
|
|
'displayFromDt',
|
||
|
|
Carbon::createFromTimestamp($event->fromDt)->format(DateFormatHelper::getSystemFormat())
|
||
|
|
);
|
||
|
|
$event->setUnmatchedProperty(
|
||
|
|
'displayToDt',
|
||
|
|
Carbon::createFromTimestamp($event->toDt)->format(DateFormatHelper::getSystemFormat())
|
||
|
|
);
|
||
|
|
|
||
|
|
if ($this->isApi($request)) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
$event->includeProperty('buttons');
|
||
|
|
$event->setUnmatchedProperty('isEditable', $this->isEventEditable($event));
|
||
|
|
if ($this->isEventEditable($event)) {
|
||
|
|
$event->buttons[] = [
|
||
|
|
'id' => 'schedule_button_edit',
|
||
|
|
'url' => $this->urlFor(
|
||
|
|
$request,
|
||
|
|
'schedule.edit.form',
|
||
|
|
['id' => $event->eventId]
|
||
|
|
),
|
||
|
|
'dataAttributes' => [
|
||
|
|
['name' => 'event-id', 'value' => $event->eventId],
|
||
|
|
['name' => 'event-start', 'value' => $event->fromDt * 1000],
|
||
|
|
['name' => 'event-end', 'value' => $event->toDt * 1000]
|
||
|
|
],
|
||
|
|
'text' => __('Edit')
|
||
|
|
];
|
||
|
|
|
||
|
|
$event->buttons[] = [
|
||
|
|
'id' => 'schedule_button_delete',
|
||
|
|
'url' => $this->urlFor($request, 'schedule.delete.form', ['id' => $event->eventId]),
|
||
|
|
'text' => __('Delete'),
|
||
|
|
'multi-select' => true,
|
||
|
|
'dataAttributes' => [
|
||
|
|
[
|
||
|
|
'name' => 'commit-url',
|
||
|
|
'value' => $this->urlFor($request, 'schedule.delete', ['id' => $event->eventId])
|
||
|
|
],
|
||
|
|
['name' => 'commit-method', 'value' => 'delete'],
|
||
|
|
['name' => 'id', 'value' => 'schedule_button_delete'],
|
||
|
|
['name' => 'text', 'value' => __('Delete')],
|
||
|
|
['name' => 'sort-group', 'value' => 1],
|
||
|
|
['name' => 'rowtitle', 'value' => $event->eventId]
|
||
|
|
]
|
||
|
|
];
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Store the table rows
|
||
|
|
$this->getState()->template = 'grid';
|
||
|
|
$this->getState()->recordsTotal = $this->scheduleFactory->countLast();
|
||
|
|
$this->getState()->setData($events);
|
||
|
|
|
||
|
|
return $this->render($request, $response);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param \Xibo\Entity\Schedule $schedule
|
||
|
|
* @param ScheduleReminder $scheduleReminder
|
||
|
|
* @throws GeneralException
|
||
|
|
* @throws NotFoundException
|
||
|
|
* @throws \Xibo\Support\Exception\ConfigurationException
|
||
|
|
* @throws \Xibo\Support\Exception\InvalidArgumentException
|
||
|
|
*/
|
||
|
|
private function saveReminder($schedule, $scheduleReminder)
|
||
|
|
{
|
||
|
|
// if someone changes from custom to always
|
||
|
|
// we should keep the definitions, but make sure they don't get executed in the task
|
||
|
|
if ($schedule->isAlwaysDayPart()) {
|
||
|
|
$scheduleReminder->reminderDt = 0;
|
||
|
|
$scheduleReminder->save();
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
switch ($scheduleReminder->type) {
|
||
|
|
case ScheduleReminder::$TYPE_MINUTE:
|
||
|
|
$type = ScheduleReminder::$MINUTE;
|
||
|
|
break;
|
||
|
|
case ScheduleReminder::$TYPE_HOUR:
|
||
|
|
$type = ScheduleReminder::$HOUR;
|
||
|
|
break;
|
||
|
|
case ScheduleReminder::$TYPE_DAY:
|
||
|
|
$type = ScheduleReminder::$DAY;
|
||
|
|
break;
|
||
|
|
case ScheduleReminder::$TYPE_WEEK:
|
||
|
|
$type = ScheduleReminder::$WEEK;
|
||
|
|
break;
|
||
|
|
case ScheduleReminder::$TYPE_MONTH:
|
||
|
|
$type = ScheduleReminder::$MONTH;
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
throw new NotFoundException(__('Unknown type'));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Remind seconds that we will subtract/add from schedule fromDt/toDt to get reminderDt
|
||
|
|
$remindSeconds = $scheduleReminder->value * $type;
|
||
|
|
|
||
|
|
// Set reminder date
|
||
|
|
if ($scheduleReminder->option == ScheduleReminder::$OPTION_BEFORE_START) {
|
||
|
|
$scheduleReminder->reminderDt = $schedule->fromDt - $remindSeconds;
|
||
|
|
} elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_AFTER_START) {
|
||
|
|
$scheduleReminder->reminderDt = $schedule->fromDt + $remindSeconds;
|
||
|
|
} elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_BEFORE_END) {
|
||
|
|
$scheduleReminder->reminderDt = $schedule->toDt - $remindSeconds;
|
||
|
|
} elseif ($scheduleReminder->option == ScheduleReminder::$OPTION_AFTER_END) {
|
||
|
|
$scheduleReminder->reminderDt = $schedule->toDt + $remindSeconds;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Is recurring event?
|
||
|
|
$now = Carbon::now();
|
||
|
|
if ($schedule->recurrenceType != '') {
|
||
|
|
|
||
|
|
// find the next event from now
|
||
|
|
try {
|
||
|
|
$nextReminderDate = $schedule->getNextReminderDate($now, $scheduleReminder, $remindSeconds);
|
||
|
|
} catch (NotFoundException $error) {
|
||
|
|
$nextReminderDate = 0;
|
||
|
|
$this->getLog()->debug('No next occurrence of reminderDt found. ReminderDt set to 0.');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($nextReminderDate != 0) {
|
||
|
|
|
||
|
|
if ($nextReminderDate < $scheduleReminder->lastReminderDt) {
|
||
|
|
|
||
|
|
// handle if someone edit in frontend after notifications were created
|
||
|
|
// we cannot have a reminderDt set to below the lastReminderDt
|
||
|
|
// so we make the lastReminderDt 0
|
||
|
|
$scheduleReminder->lastReminderDt = 0;
|
||
|
|
$scheduleReminder->reminderDt = $nextReminderDate;
|
||
|
|
|
||
|
|
} else {
|
||
|
|
$scheduleReminder->reminderDt = $nextReminderDate;
|
||
|
|
}
|
||
|
|
|
||
|
|
} else {
|
||
|
|
// next event is not found
|
||
|
|
// we make the reminderDt and lastReminderDt as 0
|
||
|
|
$scheduleReminder->lastReminderDt = 0;
|
||
|
|
$scheduleReminder->reminderDt = 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Save
|
||
|
|
$scheduleReminder->save();
|
||
|
|
|
||
|
|
} else { // one off event
|
||
|
|
|
||
|
|
$scheduleReminder->save();
|
||
|
|
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private function createGeoJson($coordinates)
|
||
|
|
{
|
||
|
|
$properties = new \StdClass();
|
||
|
|
$convertedCoordinates = [];
|
||
|
|
|
||
|
|
|
||
|
|
// coordinates come as array of strings, we need convert that to array of arrays with float values for the Geo JSON
|
||
|
|
foreach ($coordinates as $coordinate) {
|
||
|
|
|
||
|
|
// each $coordinate is a comma separated string with 2 coordinates
|
||
|
|
// make it into an array
|
||
|
|
$explodedCords = explode(',', $coordinate);
|
||
|
|
|
||
|
|
// prepare a new array, we will add float values to it, need to be cleared for each set of coordinates
|
||
|
|
$floatCords = [];
|
||
|
|
|
||
|
|
// iterate through the exploded array, change the type to float store in a new array
|
||
|
|
foreach ($explodedCords as $explodedCord) {
|
||
|
|
$explodedCord = (float)$explodedCord;
|
||
|
|
$floatCords[] = $explodedCord;
|
||
|
|
}
|
||
|
|
|
||
|
|
// each set of coordinates will be added to this new array, which we will use in the geo json
|
||
|
|
$convertedCoordinates[] = $floatCords;
|
||
|
|
}
|
||
|
|
|
||
|
|
$geometry = [
|
||
|
|
'type' => 'Polygon',
|
||
|
|
'coordinates' => [
|
||
|
|
$convertedCoordinates
|
||
|
|
]
|
||
|
|
];
|
||
|
|
|
||
|
|
$geoJson = [
|
||
|
|
'type' => 'Feature',
|
||
|
|
'properties' => $properties,
|
||
|
|
'geometry' => $geometry
|
||
|
|
];
|
||
|
|
|
||
|
|
return json_encode($geoJson);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Check if this is event is using full screen schedule with Media or Playlist
|
||
|
|
* @param $eventTypeId
|
||
|
|
* @return bool
|
||
|
|
*/
|
||
|
|
private function isFullScreenSchedule($eventTypeId)
|
||
|
|
{
|
||
|
|
return in_array($eventTypeId, [\Xibo\Entity\Schedule::$MEDIA_EVENT, \Xibo\Entity\Schedule::$PLAYLIST_EVENT]);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param $eventTypeId
|
||
|
|
* @return int
|
||
|
|
*/
|
||
|
|
private function isSyncEvent($eventTypeId): int
|
||
|
|
{
|
||
|
|
return ($eventTypeId === \Xibo\Entity\Schedule::$SYNC_EVENT) ? 1 : 0;
|
||
|
|
}
|
||
|
|
}
|