src/Products/NotificationsBundle/Controller/Dashboard/ListsController.php line 90

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Controller\Dashboard;
  3. use App\Component\ViewLayer\AbstractView;
  4. use App\Component\ViewLayer\ViewLayerService;
  5. use App\Component\ViewLayer\Views\AbstractHtmlView;
  6. use App\Component\ViewLayer\Views\AjaxHtmlView;
  7. use App\Component\ViewLayer\Views\JsonView;
  8. use App\Controller\PaginationTrait;
  9. use App\Entity\System\School;
  10. use App\Form\Forms\DummyForm;
  11. use App\Model\Query\ConditionQuery\ConditionQuery;
  12. use App\Service\Query\ConditionQueryPreviewer;
  13. use App\Util\Json;
  14. use App\Util\Pagination;
  15. use Cms\CoreBundle\Util\Doctrine\EntityManager;
  16. use Cms\FrontendBundle\Service\Resolvers\SchoolResolver;
  17. use Cms\LogBundle\Service\LoggingService;
  18. use Products\NotificationsBundle\Controller\AbstractDashboardController;
  19. use Products\NotificationsBundle\Entity\AbstractList;
  20. use Products\NotificationsBundle\Entity\Lists\Components\ListSubscription;
  21. use Products\NotificationsBundle\Entity\Lists\ConditionList;
  22. use Products\NotificationsBundle\Entity\Lists\StaticList;
  23. use Products\NotificationsBundle\Entity\NotificationsConfig;
  24. use Products\NotificationsBundle\Entity\Profile;
  25. use Products\NotificationsBundle\Form\Forms\Lists\ListDataForm;
  26. use Products\NotificationsBundle\Form\Forms\Lists\ListSearchForm;
  27. use Products\NotificationsBundle\Model\Searching\ListSearch;
  28. use Products\NotificationsBundle\Model\Searching\ProfileSearch;
  29. use Products\NotificationsBundle\Service\ListLogic;
  30. use Products\NotificationsBundle\Service\ListItemProvider;
  31. use Symfony\Component\Form\FormView;
  32. use Symfony\Component\HttpFoundation\RedirectResponse;
  33. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  34. use Symfony\Component\Routing\Annotation\Route;
  35. use Symfony\Component\HttpFoundation\Request;
  36. use Symfony\Component\HttpFoundation\Response;
  37. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  38. /**
  39.  * Class ListsController
  40.  * @package Products\NotificationsBundle\Controller\Dashboard
  41.  *
  42.  * @Route(
  43.  *     "/lists",
  44.  * )
  45.  */
  46. final class ListsController extends AbstractDashboardController
  47. {
  48.     use PaginationTrait;
  49.     const ROUTES__MAIN 'app.notifications.dashboard.lists.main';
  50.     const ROUTES__SELECT_MODAL 'app.notifications.dashboard.lists.select_modal';
  51.     const ROUTES__VIEW 'app.notifications.dashboard.lists.view';
  52.     const ROUTES__CREATE 'app.notifications.dashboard.lists.create';
  53.     const ROUTES__BUILD_CONTACTS 'app.notifications.dashboard.lists._build_contacts';
  54.     const ROUTES__BUILD_SEARCH 'app.notifications.dashboard.lists._build_search';
  55.     const ROUTES__UPDATE 'app.notifications.dashboard.lists.update';
  56.     const ROUTES__DELETE 'app.notifications.dashboard.lists.delete';
  57.     private ListItemProvider $listItemProvider;
  58.     private ConditionQueryPreviewer $conditionQueryPreviewer;
  59.     /**
  60.      * @param ListItemProvider $listItemProvider
  61.      * @param ConditionQueryPreviewer $conditionQueryPreviewer
  62.      */
  63.     public function __construct(ListItemProvider $listItemProviderConditionQueryPreviewer $conditionQueryPreviewer)
  64.     {
  65.         $this->listItemProvider $listItemProvider;
  66.         $this->conditionQueryPreviewer $conditionQueryPreviewer;
  67.     }
  68.     /**
  69.      * @param int $pagination
  70.      * @return AbstractHtmlView|Response
  71.      *
  72.      * @Route(
  73.      *     "/list/{pagination}",
  74.      *     name = self::ROUTES__MAIN,
  75.      *     requirements = {
  76.      *         "pagination" = "[1-9]\d*",
  77.      *     },
  78.      *     defaults = {
  79.      *         "pagination" = 0,
  80.      *     },
  81.      * )
  82.      */
  83.     public function mainAction(int $pagination 0)
  84.     {
  85.         // AUDIT
  86.         $this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
  87.         // perform the basic search
  88.         $result $this->doSearch(
  89.             AbstractList::class,
  90.             'lists',
  91.             ListSearch::class,
  92.             ListSearchForm::class,
  93.             $pagination,
  94.         );
  95.         // lists need counts run and their counts tied to the objects
  96.         if (is_array($result)) {
  97.             foreach ($result['lists'] as $list) {
  98.                 $list->setCount(
  99.                     $this->listItemProvider->countByLists($list),
  100.                 );
  101.             }
  102.         }
  103.         return ($result instanceof Response) ? $result $this->html($result);
  104.     }
  105.     /**
  106.      * @return AjaxHtmlView
  107.      *
  108.      * @Route(
  109.      *     "/_select_modal",
  110.      *     name = self::ROUTES__SELECT_MODAL,
  111.      * )
  112.      */
  113.     public function selectModalAction(): AjaxHtmlView
  114.     {
  115.         // AUDIT
  116.         $this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
  117.         return $this->ajax();
  118.     }
  119.     /**
  120.      * @param Request $request
  121.      * @param string $discr
  122.      * @return AbstractHtmlView|RedirectResponse
  123.      *
  124.      * @Route(
  125.      *     "/create/{discr}",
  126.      *     name = self::ROUTES__CREATE,
  127.      *     requirements = {
  128.      *         "discr" = "static|internal|condition",
  129.      *     },
  130.      * )
  131.      */
  132.     public function createAction(Request $requeststring $discr)
  133.     {
  134.         // AUDIT
  135.         $this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
  136.         $list $this->getListLogic()->init(
  137.             $discr,
  138.             $variant $request->get('variant'),
  139.         );
  140.         $form $this->createForm(
  141.             ListDataForm::class,
  142.             $list,
  143.         );
  144.         if ($this->handleForm($form)) {
  145.             if ($list instanceof ConditionList) {
  146.                 $conditionQuery $form->get('builder')->getData();
  147.                 $list->setConditionQuery($conditionQuery);
  148.             }
  149.             // AUDIT
  150.             // now that we have an object to check against, we need to just verify that the user can make a list given the configuration
  151.             // this mainly double checks that the "school" tied to a list is one that the user has access to
  152.             $this->denyAccessUnlessGranted('app.notifications.lists.admin'$list);
  153.             $this->getEntityManager()->saveAll([
  154.                 $list,
  155.                 ...(($list instanceof StaticList) ? $list->getSubscriptionsAsArray() : []),
  156.             ]);
  157.             $this->getLoggingService()->createLog($list);
  158.             return $this->redirectToRoute(self::ROUTES__MAIN);
  159.         }
  160.         return $this->html([
  161.             'list' => $list,
  162.             'form' => $form->createView(),
  163.             'ajax' => $this->buildContactsAction($this->subrequest(
  164.                 self::ROUTES__BUILD_CONTACTS,
  165.                 [
  166.                     'discr' => $list::DISCR,
  167.                     'query[state]' => base64_encode(json_encode(
  168.                         $this->getListLogic()->dump($list),
  169.                         true
  170.                     )),
  171.                     'query[school]' => $list->getSchool() ? $list->getSchool()->getId() : null,
  172.                 ]
  173.             ), $discr)->getData(),
  174.             'discr' => $list::DISCR,
  175.             'variant' => $variant,
  176.             'overridable' => ($list instanceof ConditionList && $list->isEntityOverridable()),
  177.         ]);
  178.     }
  179.     /**
  180.      * @param Request $request
  181.      * @return JsonView
  182.      *
  183.      * @Route(
  184.      *     "/_build_search",
  185.      *     name = self::ROUTES__BUILD_SEARCH,
  186.      * )
  187.      */
  188.     public function buildSearchAction(Request $request): JsonView
  189.     {
  190.         // AUDIT
  191.         $this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
  192.         return $this->jsonView(array_map(
  193.             static function (Profile $profile) {
  194.                 return [
  195.                     'id' => $profile->getId(),
  196.                     'uid' => $profile->getUidString(),
  197.                     'name' => $profile->getFullName(),
  198.                     'type' => $profile->getRoleTypeName(),
  199.                     'role' => $profile->getRole(),
  200.                 ];
  201.             },
  202.             $this->getEntityManager()->getRepository(Profile::class)->findBySearch(
  203.                 (new ProfileSearch())
  204.                     ->setLookup($request->request->get('search'))
  205.                     ->setLimit(Pagination::PAGE_LIMIT 4)
  206.             )->getIterator()->getArrayCopy(),
  207.         ));
  208.     }
  209.     /**
  210.      * @param Request $request
  211.      * @param string $discr
  212.      * @param int $pagination
  213.      * @return JsonView|RedirectResponse
  214.      *
  215.      * @Route(
  216.      *     "/_build_contacts/{discr}/{pagination}",
  217.      *     name = self::ROUTES__BUILD_CONTACTS,
  218.      *     requirements = {
  219.      *         "discr" = "static|internal|condition",
  220.      *         "pagination" = "[1-9]\d*",
  221.      *     },
  222.      *     defaults = {
  223.      *         "pagination" = 0,
  224.      *     },
  225.      * )
  226.      */
  227.     public function buildContactsAction(Request $requeststring $discrint $pagination 0)
  228.     {
  229.         // AUDIT
  230.         $this->denyAccessUnlessMaybeGranted('app.notifications.lists.admin');
  231.         // 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...
  232.         // obtain the state
  233.         $qstate null;
  234.         $state null;
  235.         if ($request->query->has('query') && isset($request->query->get('query')['state'])) {
  236.             $state Json::decode(
  237.                 base64_decode($qstate $request->query->get('query')['state']),
  238.                 true,
  239.             );
  240.         }
  241.         // determine what kind of list we are dealing with
  242.         $class AbstractList::DISCRS[$discr];
  243.         // parse the state and make a dummy list
  244.         $dummy $this->getListLogic()->assemble(
  245.             $class,
  246.             $state
  247.         );
  248.         // needed for some condition list stuff to work...
  249.         $dummy->setTenant($this->getTenant());
  250.         // if we have a condition list, we have some prep work to do
  251.         // the condition query will need access to the condition config to do some more advanced stuff
  252.         if ($dummy instanceof ConditionList) {
  253.             if (isset($request->query->get('query')['school'])) {
  254.                 $dummy->setSchool(
  255.                     $this->getEntityManager()->getRepository(School::class)->find(
  256.                         $request->query->get('query')['school']
  257.                     )
  258.                 );
  259.                 // AUDIT with list context
  260.                 $this->denyAccessUnlessGranted('app.notifications.lists.admin', [$dummy]);
  261.             }
  262.             $config $this->getEntityManager()->getRepository(NotificationsConfig::class)->findForTenant(
  263.                 $this->getTenant(),
  264.             );
  265.             if ( ! $config) {
  266.                 throw new \Exception();
  267.             }
  268.             if (isset($state['types'])) {
  269.                 $dummy->setTypes($state['types']);
  270.             }
  271.             $dummy->getConditionQuery()
  272.                 ->setConditionConfig($config->getConfig())
  273.                 ->setConditionContext($this->getTenant())
  274.                 ->setEntityOverride($state['query']['override']);
  275.         }
  276.         // get the type of things we are going to show in the ui
  277.         $itemType $this->listItemProvider->getItemType($dummy);
  278.         $searchClass ListItemProvider::SEARCH__CLASS_MAP[$itemType];
  279.         // search form logic
  280.         $form $this->createForm(
  281.             ListItemProvider::BUILDER__SEARCH_FORM_CLASS_MAP[$itemType],
  282.             $search = new $searchClass(),
  283.             [
  284.                 // need to override the action as the form has to have an action set
  285.                 // blank/missing action would make the browser use the current url in the address bar
  286.                 'action' => $this->generateUrl(
  287.                     $request->get('_route'),
  288.                     array_merge(
  289.                         $request->get('_route_params'),
  290.                         [
  291.                             Pagination::PAGE_VAR => null,
  292.                         ],
  293.                     ),
  294.                 ),
  295.             ],
  296.         );
  297.         $form->get('state')->setData($qstate);
  298.         $this->handleSearch($form);
  299.         // use the list item provider to get the results
  300.         $items $this->listItemProvider->getItems(
  301.             $dummy,
  302.             $search,
  303.             $this->getPageSize($search),
  304.             $this->getPageOffset($pagination$search),
  305.         );
  306.         // determine if we are out of bounds on the pagination
  307.         if ($this->isPageOutOfBounds($items$pagination$search)) {
  308.             return $this->handlePageOutOfBounds($items$search);
  309.         }
  310.         if ( ($dummy instanceof ConditionList) && ($dummy->getConditionQuery() instanceof ConditionQuery) ) {
  311.             [$additionalTitles$additionalValues] = $this->conditionQueryPreviewer->getAdditionalPreviewFields($dummy->getConditionQuery(), $items);
  312.         }
  313.         return $this->jsonView([
  314.             'stats_label' => $itemType === ListItemProvider::ITEM__TYPE_STUDENTS 'Students' 'Contacts',
  315.             'stats' => $this->listItemProvider->countByLists($dummy),
  316.             'overrides_label' => $itemType === ListItemProvider::ITEM__TYPE_STUDENTS 'Contacts' 'Students',
  317.             'content' => $this->getViewLayerService()->handle(
  318.                 $this->ajax([
  319.                     'discr' => $discr,
  320.                     'search' => $search,
  321.                     'form' => $view $form->createView(),
  322.                     'pagination' => array_merge(
  323.                         $this->generatePagination(
  324.                             $items,
  325.                             $pagination,
  326.                         ),
  327.                         [
  328.                             'target' => '_ajax',
  329.                             'route' => self::ROUTES__BUILD_CONTACTS,
  330.                             'params' => array_merge(
  331.                                 [
  332.                                     'discr' => $discr,
  333.                                 ],
  334.                                 array_combine(
  335.                                     array_map(
  336.                                         static function (FormView $field) use ($form) {
  337.                                             return sprintf(
  338.                                                 '%s[%s]',
  339.                                                 $form->getName(),
  340.                                                 $field->vars['name'],
  341.                                             );
  342.                                         },
  343.                                         $view->children
  344.                                     ),
  345.                                     array_map(
  346.                                         static function (FormView $field) {
  347.                                             return $field->vars['value'];
  348.                                         },
  349.                                         $view->children
  350.                                     ),
  351.                                 ),
  352.                                 $request->get('_route_params'),
  353.                             ),
  354.                         ]
  355.                     ),
  356.                     'list' => $dummy,
  357.                     'items' => $items,
  358.                     'itemType' => $itemType,
  359.                     'additionalTitles' => $additionalTitles ?? [],
  360.                     'additionalValues' => $additionalValues ?? [],
  361.                 ]),
  362.                 $request,
  363.             )->getContent(),
  364.         ]);
  365.     }
  366.     /**
  367.      * @param AbstractList $list
  368.      * @param int $pagination
  369.      * @return AbstractHtmlView|RedirectResponse
  370.      *
  371.      * @Route(
  372.      *     "/list/{list}/view/{pagination}",
  373.      *     name = self::ROUTES__VIEW,
  374.      *     requirements = {
  375.      *         "list" = "[1-9]\d*",
  376.      *         "pagination" = "[1-9]\d*",
  377.      *     },
  378.      *     defaults = {
  379.      *         "pagination" = 0,
  380.      *     },
  381.      * )
  382.      * @ParamConverter(
  383.      *     "list",
  384.      *     class = AbstractList::class,
  385.      * )
  386.      */
  387.     public function viewAction(AbstractList $listint $pagination 0)
  388.     {
  389.         // AUDIT
  390.         $this->denyAccessUnlessGranted('app.notifications.lists.admin', [$list]);
  391.         // if we have a condition list, we have some prep work to do
  392.         // the condition query will need access to the condition config to do some more advanced stuff
  393.         if ($list instanceof ConditionList) {
  394.             $config $this->getEntityManager()->getRepository(NotificationsConfig::class)->findForTenant(
  395.                 $this->getTenant(),
  396.             );
  397.             if ( ! $config) {
  398.                 throw new \Exception();
  399.             }
  400.             $list->getConditionQuery()
  401.                 ->setConditionConfig($config->getConfig())
  402.                 ->setConditionContext($list);
  403.         }
  404.         // get the type of things we are going to show in the ui
  405.         $itemType $this->listItemProvider->getItemType($list);
  406.         $searchClass ListItemProvider::SEARCH__CLASS_MAP[$itemType];
  407.         // search form logic
  408.         $this->handleSearch(
  409.             $form $this->createForm(
  410.                 ListItemProvider::SEARCH__FORM_CLASS_MAP[$itemType],
  411.                 $search = new $searchClass(),
  412.             ),
  413.         );
  414.         // use the list item provider to get the results
  415.         $items $this->listItemProvider->getItems(
  416.             $list,
  417.             $search,
  418.             $this->getPageSize($search),
  419.             $this->getPageOffset($pagination$search),
  420.         );
  421.         // determine if we are out of bounds on the pagination
  422.         if ($this->isPageOutOfBounds($items$pagination$search)) {
  423.             return $this->handlePageOutOfBounds($items$search);
  424.         }
  425.         return $this->html([
  426.             'search' => $search,
  427.             'form' => $form->createView(),
  428.             'pagination' => $this->generatePagination(
  429.                 $items,
  430.                 $pagination,
  431.             ),
  432.             'list' => $list,
  433.             'items' => $items,
  434.             'itemType' => $itemType,
  435.         ]);
  436.     }
  437.     /**
  438.      * @param AbstractList $list
  439.      * @return AbstractHtmlView|RedirectResponse
  440.      *
  441.      * @Route(
  442.      *     "/{list}/update",
  443.      *     name = self::ROUTES__UPDATE,
  444.      *     requirements = {
  445.      *         "list" = "[1-9]\d*",
  446.      *     },
  447.      * )
  448.      * @ParamConverter(
  449.      *     "list",
  450.      *     class = AbstractList::class,
  451.      * )
  452.      */
  453.     public function updateAction(AbstractList $list)
  454.     {
  455.         // AUDIT
  456.         $this->denyAccessUnlessGranted('app.notifications.lists.admin', [$list]);
  457.         // if the list does not have a school, attempt to set to the district
  458.         if ( ! $list->getSchool()) {
  459.             $district $this->getSchoolResolver()->resolveDistrictByTenant($list);
  460.             if ($district && $this->isGranted('app.notifications.lists.admin'$district)) {
  461.                 $list->setSchool($district);
  462.             }
  463.         }
  464.         $form $this->createForm(ListDataForm::class, $list);
  465.         if ($this->handleForm($form)) {
  466.             if ($list instanceof ConditionList) {
  467.                 $conditionQuery $form->get('builder')->getData();
  468.                 $list->setConditionQuery($conditionQuery);
  469.             }
  470.             $this->getEntityManager()->transactional(
  471.                 function (EntityManager $em) use ($list) {
  472.                     switch (true) {
  473.                         case $list instanceof StaticList:
  474.                             $em->createQueryBuilder()
  475.                                 ->delete(ListSubscription::class, 'subscriptions')
  476.                                 ->andWhere('subscriptions.list = :list')
  477.                                 ->setParameter('list'$list)
  478.                                 ->getQuery()
  479.                                 ->execute();
  480.                             $em->persistAll($list->getSubscriptionsAsArray());
  481.                             break;
  482.                     }
  483.                     $em->save($list);
  484.                 }
  485.             );
  486.             $this->getLoggingService()->createLog($list);
  487.             return $this->redirectToRoute(self::ROUTES__MAIN);
  488.         }
  489.         return $this->html([
  490.             'list' => $list,
  491.             'form' => $form->createView(),
  492.             'ajax' => $this->buildContactsAction($this->subrequest(
  493.                 self::ROUTES__BUILD_CONTACTS,
  494.                 [
  495.                     'discr' => $list::DISCR,
  496.                     'query[state]' => base64_encode(json_encode(
  497.                         $this->getListLogic()->dump($list),
  498.                         true
  499.                     )),
  500.                     'query[school]' => $list->getSchool() ? $list->getSchool()->getId() : null,
  501.                 ]
  502.             ), $list::DISCR)->getData(),
  503.             'discr' => $list::DISCR,
  504.         ]);
  505.     }
  506.     /**
  507.      * @param Request $request
  508.      * @param AbstractList $list
  509.      * @return AbstractView|AjaxHtmlView|JsonView
  510.      *
  511.      * @Route(
  512.      *     "/{list}/delete",
  513.      *     name = self::ROUTES__DELETE,
  514.      *     requirements = {
  515.      *         "list" = "[1-9]\d*",
  516.      *     },
  517.      * )
  518.      * @ParamConverter(
  519.      *     "list",
  520.      *     class = AbstractList::class,
  521.      * )
  522.      */
  523.     public function deleteAction(Request $requestAbstractList $list): AbstractView
  524.     {
  525.         // AUDIT
  526.         $this->denyAccessUnlessGranted('app.notifications.lists.admin', [$list]);
  527.         if ( ! $request->isXmlHttpRequest()) {
  528.             throw new NotFoundHttpException();
  529.         }
  530.         $form $this->createForm(DummyForm::class);
  531.         if ($this->handleForm($form)) {
  532.             $id $list->getId();
  533.             $this->getEntityManager()->transactional(
  534.                 function (EntityManager $em) use ($list) {
  535.                     $em->delete($list);
  536.                 }
  537.             );
  538.             $this->getLoggingService()->createLog($list$id);
  539.             return $this->jsonView([
  540.                 'redirect' => true,
  541.             ]);
  542.         }
  543.         return $this->ajax([
  544.             'list' => $list,
  545.             'form' => $form->createView(),
  546.         ]);
  547.     }
  548.     /**
  549.      * @return ListLogic|object
  550.      */
  551.     private function getListLogic(): ListLogic
  552.     {
  553.         return $this->get(__METHOD__);
  554.     }
  555.     /**
  556.      * @return ViewLayerService|object
  557.      */
  558.     private function getViewLayerService(): ViewLayerService
  559.     {
  560.         return $this->get(__METHOD__);
  561.     }
  562.     /**
  563.      * @return SchoolResolver
  564.      */
  565.     private function getSchoolResolver(): SchoolResolver
  566.     {
  567.         return $this->get(__METHOD__);
  568.     }
  569. }