<?php
namespace Products\NotificationsBundle\Controller\Dashboard;
use App\Component\ViewLayer\AbstractView;
use App\Component\ViewLayer\ViewLayerService;
use App\Component\ViewLayer\Views\AbstractHtmlView;
use App\Component\ViewLayer\Views\AjaxHtmlView;
use App\Component\ViewLayer\Views\JsonView;
use App\Controller\PaginationTrait;
use App\Entity\System\School;
use App\Form\Forms\DummyForm;
use App\Model\Query\ConditionQuery\ConditionQuery;
use App\Service\Query\ConditionQueryPreviewer;
use App\Util\Json;
use App\Util\Pagination;
use Cms\CoreBundle\Util\Doctrine\EntityManager;
use Cms\FrontendBundle\Service\Resolvers\SchoolResolver;
use Cms\LogBundle\Service\LoggingService;
use Products\NotificationsBundle\Controller\AbstractDashboardController;
use Products\NotificationsBundle\Entity\AbstractList;
use Products\NotificationsBundle\Entity\Lists\Components\ListSubscription;
use Products\NotificationsBundle\Entity\Lists\ConditionList;
use Products\NotificationsBundle\Entity\Lists\StaticList;
use Products\NotificationsBundle\Entity\NotificationsConfig;
use Products\NotificationsBundle\Entity\Profile;
use Products\NotificationsBundle\Form\Forms\Lists\ListDataForm;
use Products\NotificationsBundle\Form\Forms\Lists\ListSearchForm;
use Products\NotificationsBundle\Model\Searching\ListSearch;
use Products\NotificationsBundle\Model\Searching\ProfileSearch;
use Products\NotificationsBundle\Service\ListLogic;
use Products\NotificationsBundle\Service\ListItemProvider;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class ListsController
* @package Products\NotificationsBundle\Controller\Dashboard
*
* @Route(
* "/lists",
* )
*/
final class ListsController extends AbstractDashboardController
{
use PaginationTrait;
const ROUTES__MAIN = 'app.notifications.dashboard.lists.main';
const ROUTES__SELECT_MODAL = 'app.notifications.dashboard.lists.select_modal';
const ROUTES__VIEW = 'app.notifications.dashboard.lists.view';
const ROUTES__CREATE = 'app.notifications.dashboard.lists.create';
const ROUTES__BUILD_CONTACTS = 'app.notifications.dashboard.lists._build_contacts';
const ROUTES__BUILD_SEARCH = 'app.notifications.dashboard.lists._build_search';
const ROUTES__UPDATE = 'app.notifications.dashboard.lists.update';
const ROUTES__DELETE = 'app.notifications.dashboard.lists.delete';
private ListItemProvider $listItemProvider;
private ConditionQueryPreviewer $conditionQueryPreviewer;
/**
* @param ListItemProvider $listItemProvider
* @param ConditionQueryPreviewer $conditionQueryPreviewer
*/
public function __construct(ListItemProvider $listItemProvider, ConditionQueryPreviewer $conditionQueryPreviewer)
{
$this->listItemProvider = $listItemProvider;
$this->conditionQueryPreviewer = $conditionQueryPreviewer;
}
/**
* @param int $pagination
* @return AbstractHtmlView|Response
*
* @Route(
* "/list/{pagination}",
* name = self::ROUTES__MAIN,
* requirements = {
* "pagination" = "[1-9]\d*",
* },
* defaults = {
* "pagination" = 0,
* },
* )
*/
public function mainAction(int $pagination = 0)
{
// AUDIT
$this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
// perform the basic search
$result = $this->doSearch(
AbstractList::class,
'lists',
ListSearch::class,
ListSearchForm::class,
$pagination,
);
// lists need counts run and their counts tied to the objects
if (is_array($result)) {
foreach ($result['lists'] as $list) {
$list->setCount(
$this->listItemProvider->countByLists($list),
);
}
}
return ($result instanceof Response) ? $result : $this->html($result);
}
/**
* @return AjaxHtmlView
*
* @Route(
* "/_select_modal",
* name = self::ROUTES__SELECT_MODAL,
* )
*/
public function selectModalAction(): AjaxHtmlView
{
// AUDIT
$this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
return $this->ajax();
}
/**
* @param Request $request
* @param string $discr
* @return AbstractHtmlView|RedirectResponse
*
* @Route(
* "/create/{discr}",
* name = self::ROUTES__CREATE,
* requirements = {
* "discr" = "static|internal|condition",
* },
* )
*/
public function createAction(Request $request, string $discr)
{
// AUDIT
$this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
$list = $this->getListLogic()->init(
$discr,
$variant = $request->get('variant'),
);
$form = $this->createForm(
ListDataForm::class,
$list,
);
if ($this->handleForm($form)) {
if ($list instanceof ConditionList) {
$conditionQuery = $form->get('builder')->getData();
$list->setConditionQuery($conditionQuery);
}
// AUDIT
// now that we have an object to check against, we need to just verify that the user can make a list given the configuration
// this mainly double checks that the "school" tied to a list is one that the user has access to
$this->denyAccessUnlessGranted('app.notifications.lists.admin', $list);
$this->getEntityManager()->saveAll([
$list,
...(($list instanceof StaticList) ? $list->getSubscriptionsAsArray() : []),
]);
$this->getLoggingService()->createLog($list);
return $this->redirectToRoute(self::ROUTES__MAIN);
}
return $this->html([
'list' => $list,
'form' => $form->createView(),
'ajax' => $this->buildContactsAction($this->subrequest(
self::ROUTES__BUILD_CONTACTS,
[
'discr' => $list::DISCR,
'query[state]' => base64_encode(json_encode(
$this->getListLogic()->dump($list),
true
)),
'query[school]' => $list->getSchool() ? $list->getSchool()->getId() : null,
]
), $discr)->getData(),
'discr' => $list::DISCR,
'variant' => $variant,
'overridable' => ($list instanceof ConditionList && $list->isEntityOverridable()),
]);
}
/**
* @param Request $request
* @return JsonView
*
* @Route(
* "/_build_search",
* name = self::ROUTES__BUILD_SEARCH,
* )
*/
public function buildSearchAction(Request $request): JsonView
{
// AUDIT
$this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
return $this->jsonView(array_map(
static function (Profile $profile) {
return [
'id' => $profile->getId(),
'uid' => $profile->getUidString(),
'name' => $profile->getFullName(),
'type' => $profile->getRoleTypeName(),
'role' => $profile->getRole(),
];
},
$this->getEntityManager()->getRepository(Profile::class)->findBySearch(
(new ProfileSearch())
->setLookup($request->request->get('search'))
->setLimit(Pagination::PAGE_LIMIT * 4)
)->getIterator()->getArrayCopy(),
));
}
/**
* @param Request $request
* @param string $discr
* @param int $pagination
* @return JsonView|RedirectResponse
*
* @Route(
* "/_build_contacts/{discr}/{pagination}",
* name = self::ROUTES__BUILD_CONTACTS,
* requirements = {
* "discr" = "static|internal|condition",
* "pagination" = "[1-9]\d*",
* },
* defaults = {
* "pagination" = 0,
* },
* )
*/
public function buildContactsAction(Request $request, string $discr, int $pagination = 0)
{
// AUDIT
$this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
// TODO: list serialization is currently a mess, due to the fact that this method really needs the query state and the list state, not just the query state; this needs improved significantly...
// obtain the state
$qstate = null;
$state = null;
if ($request->query->has('query') && isset($request->query->get('query')['state'])) {
$state = Json::decode(
base64_decode($qstate = $request->query->get('query')['state']),
true,
);
}
// determine what kind of list we are dealing with
$class = AbstractList::DISCRS[$discr];
// parse the state and make a dummy list
$dummy = $this->getListLogic()->assemble(
$class,
$state
);
// needed for some condition list stuff to work...
$dummy->setTenant($this->getTenant());
// if we have a condition list, we have some prep work to do
// the condition query will need access to the condition config to do some more advanced stuff
if ($dummy instanceof ConditionList) {
if (isset($request->query->get('query')['school'])) {
$dummy->setSchool(
$this->getEntityManager()->getRepository(School::class)->find(
$request->query->get('query')['school']
)
);
// AUDIT with list context
$this->denyAccessUnlessGranted('app.notifications.lists.admin', [$dummy]);
}
$config = $this->getEntityManager()->getRepository(NotificationsConfig::class)->findForTenant(
$this->getTenant(),
);
if ( ! $config) {
throw new \Exception();
}
if (isset($state['types'])) {
$dummy->setTypes($state['types']);
}
$dummy->getConditionQuery()
->setConditionConfig($config->getConfig())
->setConditionContext($this->getTenant())
->setEntityOverride($state['query']['override']);
}
// get the type of things we are going to show in the ui
$itemType = $this->listItemProvider->getItemType($dummy);
$searchClass = ListItemProvider::SEARCH__CLASS_MAP[$itemType];
// search form logic
$form = $this->createForm(
ListItemProvider::BUILDER__SEARCH_FORM_CLASS_MAP[$itemType],
$search = new $searchClass(),
[
// need to override the action as the form has to have an action set
// blank/missing action would make the browser use the current url in the address bar
'action' => $this->generateUrl(
$request->get('_route'),
array_merge(
$request->get('_route_params'),
[
Pagination::PAGE_VAR => null,
],
),
),
],
);
$form->get('state')->setData($qstate);
$this->handleSearch($form);
// use the list item provider to get the results
$items = $this->listItemProvider->getItems(
$dummy,
$search,
$this->getPageSize($search),
$this->getPageOffset($pagination, $search),
);
// determine if we are out of bounds on the pagination
if ($this->isPageOutOfBounds($items, $pagination, $search)) {
return $this->handlePageOutOfBounds($items, $search);
}
if ( ($dummy instanceof ConditionList) && ($dummy->getConditionQuery() instanceof ConditionQuery) ) {
[$additionalTitles, $additionalValues] = $this->conditionQueryPreviewer->getAdditionalPreviewFields($dummy->getConditionQuery(), $items);
}
return $this->jsonView([
'stats_label' => $itemType === ListItemProvider::ITEM__TYPE_STUDENTS ? 'Students' : 'Contacts',
'stats' => $this->listItemProvider->countByLists($dummy),
'overrides_label' => $itemType === ListItemProvider::ITEM__TYPE_STUDENTS ? 'Contacts' : 'Students',
'content' => $this->getViewLayerService()->handle(
$this->ajax([
'discr' => $discr,
'search' => $search,
'form' => $view = $form->createView(),
'pagination' => array_merge(
$this->generatePagination(
$items,
$pagination,
),
[
'target' => '_ajax',
'route' => self::ROUTES__BUILD_CONTACTS,
'params' => array_merge(
[
'discr' => $discr,
],
array_combine(
array_map(
static function (FormView $field) use ($form) {
return sprintf(
'%s[%s]',
$form->getName(),
$field->vars['name'],
);
},
$view->children
),
array_map(
static function (FormView $field) {
return $field->vars['value'];
},
$view->children
),
),
$request->get('_route_params'),
),
]
),
'list' => $dummy,
'items' => $items,
'itemType' => $itemType,
'additionalTitles' => $additionalTitles ?? [],
'additionalValues' => $additionalValues ?? [],
]),
$request,
)->getContent(),
]);
}
/**
* @param AbstractList $list
* @param int $pagination
* @return AbstractHtmlView|RedirectResponse
*
* @Route(
* "/list/{list}/view/{pagination}",
* name = self::ROUTES__VIEW,
* requirements = {
* "list" = "[1-9]\d*",
* "pagination" = "[1-9]\d*",
* },
* defaults = {
* "pagination" = 0,
* },
* )
* @ParamConverter(
* "list",
* class = AbstractList::class,
* )
*/
public function viewAction(AbstractList $list, int $pagination = 0)
{
// AUDIT
$this->denyAccessUnlessGranted('app.notifications.lists.admin', [$list]);
// if we have a condition list, we have some prep work to do
// the condition query will need access to the condition config to do some more advanced stuff
if ($list instanceof ConditionList) {
$config = $this->getEntityManager()->getRepository(NotificationsConfig::class)->findForTenant(
$this->getTenant(),
);
if ( ! $config) {
throw new \Exception();
}
$list->getConditionQuery()
->setConditionConfig($config->getConfig())
->setConditionContext($list);
}
// get the type of things we are going to show in the ui
$itemType = $this->listItemProvider->getItemType($list);
$searchClass = ListItemProvider::SEARCH__CLASS_MAP[$itemType];
// search form logic
$this->handleSearch(
$form = $this->createForm(
ListItemProvider::SEARCH__FORM_CLASS_MAP[$itemType],
$search = new $searchClass(),
),
);
// use the list item provider to get the results
$items = $this->listItemProvider->getItems(
$list,
$search,
$this->getPageSize($search),
$this->getPageOffset($pagination, $search),
);
// determine if we are out of bounds on the pagination
if ($this->isPageOutOfBounds($items, $pagination, $search)) {
return $this->handlePageOutOfBounds($items, $search);
}
return $this->html([
'search' => $search,
'form' => $form->createView(),
'pagination' => $this->generatePagination(
$items,
$pagination,
),
'list' => $list,
'items' => $items,
'itemType' => $itemType,
]);
}
/**
* @param AbstractList $list
* @return AbstractHtmlView|RedirectResponse
*
* @Route(
* "/{list}/update",
* name = self::ROUTES__UPDATE,
* requirements = {
* "list" = "[1-9]\d*",
* },
* )
* @ParamConverter(
* "list",
* class = AbstractList::class,
* )
*/
public function updateAction(AbstractList $list)
{
// AUDIT
$this->denyAccessUnlessGranted('app.notifications.lists.admin', [$list]);
// if the list does not have a school, attempt to set to the district
if ( ! $list->getSchool()) {
$district = $this->getSchoolResolver()->resolveDistrictByTenant($list);
if ($district && $this->isGranted('app.notifications.lists.admin', $district)) {
$list->setSchool($district);
}
}
$form = $this->createForm(ListDataForm::class, $list);
if ($this->handleForm($form)) {
if ($list instanceof ConditionList) {
$conditionQuery = $form->get('builder')->getData();
$list->setConditionQuery($conditionQuery);
}
$this->getEntityManager()->transactional(
function (EntityManager $em) use ($list) {
switch (true) {
case $list instanceof StaticList:
$em->createQueryBuilder()
->delete(ListSubscription::class, 'subscriptions')
->andWhere('subscriptions.list = :list')
->setParameter('list', $list)
->getQuery()
->execute();
$em->persistAll($list->getSubscriptionsAsArray());
break;
}
$em->save($list);
}
);
$this->getLoggingService()->createLog($list);
return $this->redirectToRoute(self::ROUTES__MAIN);
}
return $this->html([
'list' => $list,
'form' => $form->createView(),
'ajax' => $this->buildContactsAction($this->subrequest(
self::ROUTES__BUILD_CONTACTS,
[
'discr' => $list::DISCR,
'query[state]' => base64_encode(json_encode(
$this->getListLogic()->dump($list),
true
)),
'query[school]' => $list->getSchool() ? $list->getSchool()->getId() : null,
]
), $list::DISCR)->getData(),
'discr' => $list::DISCR,
]);
}
/**
* @param Request $request
* @param AbstractList $list
* @return AbstractView|AjaxHtmlView|JsonView
*
* @Route(
* "/{list}/delete",
* name = self::ROUTES__DELETE,
* requirements = {
* "list" = "[1-9]\d*",
* },
* )
* @ParamConverter(
* "list",
* class = AbstractList::class,
* )
*/
public function deleteAction(Request $request, AbstractList $list): AbstractView
{
// AUDIT
$this->denyAccessUnlessGranted('app.notifications.lists.admin', [$list]);
if ( ! $request->isXmlHttpRequest()) {
throw new NotFoundHttpException();
}
$form = $this->createForm(DummyForm::class);
if ($this->handleForm($form)) {
$id = $list->getId();
$this->getEntityManager()->transactional(
function (EntityManager $em) use ($list) {
$em->delete($list);
}
);
$this->getLoggingService()->createLog($list, $id);
return $this->jsonView([
'redirect' => true,
]);
}
return $this->ajax([
'list' => $list,
'form' => $form->createView(),
]);
}
/**
* @return ListLogic|object
*/
private function getListLogic(): ListLogic
{
return $this->get(__METHOD__);
}
/**
* @return ViewLayerService|object
*/
private function getViewLayerService(): ViewLayerService
{
return $this->get(__METHOD__);
}
/**
* @return SchoolResolver
*/
private function getSchoolResolver(): SchoolResolver
{
return $this->get(__METHOD__);
}
}