src/Products/NotificationsBundle/Controller/Dashboard/DefaultController.php line 475

Open in your IDE?
  1. <?php
  2. namespace Products\NotificationsBundle\Controller\Dashboard;
  3. use App\Component\ViewLayer\Views\AbstractHtmlView;
  4. use App\Component\ViewLayer\Views\AjaxHtmlView;
  5. use App\Controller\PaginationTrait;
  6. use App\Entity\System\School;
  7. use App\Entity\System\SocialAccount;
  8. use App\Entity\System\SocialAccounts\FacebookSocialAccount;
  9. use App\Entity\System\SocialAccounts\InstagramSocialAccount;
  10. use App\Entity\System\SocialAccounts\TwitterSocialAccount;
  11. use App\Service\Social\FacebookService;
  12. use App\Service\Social\InstagramService;
  13. use App\Service\Social\TwitterService;
  14. use Cms\ContainerBundle\Entity\Containers\StorageContainer;
  15. use Cms\CoreBundle\Entity\OneRoster\OneRosterUser;
  16. use Cms\CoreBundle\Entity\OneRosterSync;
  17. use Cms\FileBundle\Entity\Nodes\Folder;
  18. use DateTime;
  19. use Products\NotificationsBundle\Controller\AbstractDashboardController;
  20. use Products\NotificationsBundle\Doctrine\Repository\ProfileRepository;
  21. use Products\NotificationsBundle\Doctrine\Repository\StudentRepository;
  22. use Products\NotificationsBundle\Entity\AbstractNotification;
  23. use Products\NotificationsBundle\Entity\Checkup;
  24. use Products\NotificationsBundle\Entity\Lists\ConditionList;
  25. use Products\NotificationsBundle\Entity\Notifications\Message;
  26. use Products\NotificationsBundle\Entity\Notifications\Template;
  27. use Products\NotificationsBundle\Entity\NotificationsConfig;
  28. use Products\NotificationsBundle\Entity\Profile;
  29. use Products\NotificationsBundle\Entity\Recipients\AppRecipient;
  30. use Products\NotificationsBundle\Entity\Recipients\EmailRecipient;
  31. use Products\NotificationsBundle\Entity\Recipients\PhoneRecipient;
  32. use Products\NotificationsBundle\Entity\Student;
  33. use Products\NotificationsBundle\Form\Forms\Templates\TemplateSearchForm;
  34. use Products\NotificationsBundle\Model\Searching\TemplateSearch;
  35. use Products\NotificationsBundle\Util\Preferences;
  36. use Products\NotificationsBundle\Util\Reachability;
  37. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  38. use Symfony\Component\Routing\Annotation\Route;
  39. use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
  40. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  41. use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
  42. use Symfony\Component\Form\Extension\Core\Type\FormType;
  43. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  44. use Symfony\Component\HttpFoundation\RedirectResponse;
  45. use Symfony\Component\HttpFoundation\Response;
  46. /**
  47.  * Class DefaultController
  48.  * @package Products\NotificationsBundle\Controller\Dashboard
  49.  */
  50. final class DefaultController extends AbstractDashboardController
  51. {
  52.     use PaginationTrait;
  53.     const ROUTES__MAIN 'app.notifications.dashboard.default.main';
  54.     const ROUTES__SELECT_MODAL 'app.notifications.dashboard.default.select_modal';
  55.     const ROUTES__BROADCAST_MODAL 'app.notifications.dashboard.default.broadcast_modal';
  56.     const ROUTES__SOCIAL_AVATAR 'app.notifications.dashboard.default.social_avatar';
  57.     /**
  58.      * @return AbstractHtmlView
  59.      *
  60.      * @Route(
  61.      *     "",
  62.      *     name = self::ROUTES__MAIN,
  63.      * )
  64.      */
  65.     public function mainAction(): AbstractHtmlView
  66.     {
  67.         // AUDIT
  68.         $this->denyAccessUnlessMaybeGranted('@app.notifications.access');
  69.         // check that we have a storage container for files
  70.         // make it if not
  71.         // TODO: need to do this somewhere else more reliable...
  72.         $storage $this->getEntityManager()->getRepository(StorageContainer::class)->findOneBy([
  73.             'slug' => 'notifications',
  74.         ]);
  75.         if (empty($storage)) {
  76.             $storage = (new StorageContainer())
  77.                 ->setName('Notifications')
  78.                 ->setSlug('notifications')
  79.                 ->setHidden(true);
  80.             $folder = (new Folder())
  81.                 ->setName('images')
  82.                 ->setContainer($storage);
  83.             $this->getEntityManager()->saveAll([
  84.                 $storage,
  85.                 $folder,
  86.             ]);
  87.         }
  88.         $messages $this->getEntityManager()->getRepository(Message::class)->findByRecentlySent(6);
  89.         return $this->html([
  90.             'sync' => $this->getEntityManager()->getRepository(OneRosterSync::class)->findOneByTenant(
  91.                 $this->getTenant()
  92.             ),
  93.             'messages' => $messages,
  94.             'messagesIsGrantedArray' => array_values(
  95.                 array_filter(
  96.                     array_map(
  97.                         function (Message $message) {
  98.                             try {
  99.                                 $this->audit($message);
  100.                                 return $message->getId();
  101.                             } catch (\Throwable $exception) {
  102.                                 return null;
  103.                             }
  104.                         },
  105.                         $messages,
  106.                     )
  107.                 )
  108.             ),
  109.             'stats' => [
  110.                 'callouts' => [
  111.                     'sis' => $this->getEntityManager()->getRepository(Checkup::class)
  112.                         ->count([
  113.                             'ok' => false,
  114.                             'fixedAt' => null,
  115.                         ]),
  116.                     'messages' => $this->getEntityManager()->getRepository(Message::class)
  117.                         ->createQueryBuilder('messages')
  118.                         ->select('COUNT(messages)')
  119.                         ->andWhere('messages.status IN (:statuses)')
  120.                         ->setParameter('statuses', [
  121.                             AbstractNotification::STATUSES__SENDING,
  122.                             AbstractNotification::STATUSES__COMPLETE,
  123.                         ])
  124.                         ->andWhere('messages.touchedAt >= :timestamp')
  125.                         ->setParameter('timestamp', new DateTime('-1 month'))
  126.                         ->getQuery()
  127.                         ->getSingleScalarResult(),
  128.                     'schools' => $this->getEntityManager()->getRepository(School::class)
  129.                         ->count([]),
  130.                 ],
  131.                 'contacts' => [
  132.                     'total' => $this->getStudentRepository()
  133.                         ->createQueryBuilder('students')
  134.                         ->select('COUNT(students)')
  135.                         ->getQuery()
  136.                         ->getSingleScalarResult(),
  137.                     'reachable' =>  $this->getStudentRepository()
  138.                         ->createQueryBuilder('students')
  139.                         ->select('COUNT(students)')
  140.                         ->andWhere('BIT_AND(students.reachability, :mask) > 0')
  141.                         ->setParameter('mask'Reachability::REACHABLE)
  142.                         ->getQuery()
  143.                         ->getSingleScalarResult(),
  144.                     'unreachable' =>  $this->getStudentRepository()
  145.                         ->createQueryBuilder('students')
  146.                         ->select('COUNT(students)')
  147.                         ->andWhere('BIT_AND(students.reachability, :mask) > 0')
  148.                         ->setParameter('mask'Reachability::UNREACHABLE)
  149.                         ->getQuery()
  150.                         ->getSingleScalarResult(),
  151.                     'no-contact' =>  $this->getStudentRepository()
  152.                         ->createQueryBuilder('students')
  153.                         ->select('COUNT(students)')
  154.                         ->andWhere('students.reachability = :mask')
  155.                         ->setParameter('mask'Reachability::NO_CONTACT)
  156.                         ->getQuery()
  157.                         ->getSingleScalarResult(),
  158.                 ],
  159.                 'communication' => [
  160.                     'total' => $this->getProfileRepository()
  161.                         ->createQueryBuilder('profiles')
  162.                         ->select('COUNT(profiles)')
  163.                         ->getQuery()
  164.                         ->getSingleScalarResult(),
  165.                     'email' => $this->getProfileRepository()
  166.                         ->createQueryBuilder('profiles')
  167.                         ->select('COUNT(DISTINCT profiles)')
  168.                         ->leftJoin('profiles.contacts''xrefs')
  169.                         ->leftJoin(EmailRecipient::class, 'recipients''WITH''IDENTITY(xrefs.recipient) = recipients.id')
  170.                         ->andWhere('recipients.id IS NOT NULL')
  171.                         ->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
  172.                         ->setParameter('preference'Preferences::PREFERENCES__EMAIL)
  173.                         ->getQuery()
  174.                         ->getSingleScalarResult(),
  175.                     'sms' => $this->getProfileRepository()
  176.                         ->createQueryBuilder('profiles')
  177.                         ->select('COUNT(DISTINCT profiles)')
  178.                         ->leftJoin('profiles.contacts''xrefs')
  179.                         ->leftJoin(PhoneRecipient::class, 'recipients''WITH''IDENTITY(xrefs.recipient) = recipients.id')
  180.                         ->andWhere('recipients.id IS NOT NULL')
  181.                         ->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
  182.                         ->setParameter('preference'Preferences::PREFERENCES__SMS)
  183.                         ->andWhere('recipients.method IN (:methods)')
  184.                         ->setParameter('methods', [
  185.                             PhoneRecipient::METHODS__SMS,
  186.                             PhoneRecipient::METHODS__HYBRID,
  187.                         ])
  188.                         ->getQuery()
  189.                         ->getSingleScalarResult(),
  190.                     'voice' => $this->getProfileRepository()
  191.                         ->createQueryBuilder('profiles')
  192.                         ->select('COUNT(DISTINCT profiles)')
  193.                         ->leftJoin('profiles.contacts''xrefs')
  194.                         ->leftJoin(PhoneRecipient::class, 'recipients''WITH''IDENTITY(xrefs.recipient) = recipients.id')
  195.                         ->andWhere('recipients.id IS NOT NULL')
  196.                         ->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
  197.                         ->setParameter('preference'Preferences::PREFERENCES__VOICE)
  198.                         ->andWhere('recipients.method IN (:methods)')
  199.                         ->setParameter('methods', [
  200.                             PhoneRecipient::METHODS__VOICE,
  201.                             PhoneRecipient::METHODS__HYBRID,
  202.                         ])
  203.                         ->getQuery()
  204.                         ->getSingleScalarResult(),
  205.                     'app' => $this->getProfileRepository()
  206.                         ->createQueryBuilder('profiles')
  207.                         ->select('COUNT(DISTINCT profiles)')
  208.                         ->leftJoin('profiles.contacts''xrefs')
  209.                         ->leftJoin(AppRecipient::class, 'recipients''WITH''IDENTITY(xrefs.recipient) = recipients.id')
  210.                         ->andWhere('recipients.id IS NOT NULL')
  211.                         ->andWhere('BIT_AND(xrefs.primaryPreferences, :preference) > 0')
  212.                         ->setParameter('preference'Preferences::PREFERENCES__APP)
  213.                         ->getQuery()
  214.                         ->getSingleScalarResult(),
  215.                 ],
  216.                 'debugging' => (function () {
  217.                     $data = [
  218.                         'all' => [
  219.                             'total' => $this->getProfileRepository()
  220.                                 ->createQueryBuilder('profiles')
  221.                                 ->select('COUNT(profiles)')
  222.                                 ->getQuery()
  223.                                 ->getSingleScalarResult(),
  224.                             'reachable' =>  $this->getProfileRepository()
  225.                                 ->createQueryBuilder('profiles')
  226.                                 ->select('COUNT(profiles)')
  227.                                 ->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
  228.                                 ->setParameter('mask'Reachability::REACHABLE)
  229.                                 ->getQuery()
  230.                                 ->getSingleScalarResult(),
  231.                             'unreachable' =>  $this->getProfileRepository()
  232.                                 ->createQueryBuilder('profiles')
  233.                                 ->select('COUNT(profiles)')
  234.                                 ->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
  235.                                 ->setParameter('mask'Reachability::UNREACHABLE)
  236.                                 ->getQuery()
  237.                                 ->getSingleScalarResult(),
  238.                             'no-contact' =>  $this->getProfileRepository()
  239.                                 ->createQueryBuilder('profiles')
  240.                                 ->select('COUNT(profiles)')
  241.                                 ->andWhere('profiles.reachability = :mask')
  242.                                 ->setParameter('mask'Reachability::NO_CONTACT)
  243.                                 ->getQuery()
  244.                                 ->getSingleScalarResult(),
  245.                         ],
  246.                     ];
  247.                     foreach (OneRosterSync::STRATEGIES__NOTIFICATIONS as $key => $strategy) {
  248.                         if ($this->getOneRosterSync() && $this->getOneRosterSync()->hasStrategy($strategy)) {
  249.                             $key str_replace('notifications__'''$key);
  250.                             $data[$key] = [
  251.                                 'total' => $this->getProfileRepository()
  252.                                     ->createQueryBuilder('profiles')
  253.                                     ->select('COUNT(profiles)')
  254.                                     ->andWhere('profiles.role IN (:roles)')
  255.                                     ->setParameter('roles'OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
  256.                                     ->getQuery()
  257.                                     ->getSingleScalarResult(),
  258.                                 'reachable' =>  $this->getProfileRepository()
  259.                                     ->createQueryBuilder('profiles')
  260.                                     ->select('COUNT(profiles)')
  261.                                     ->andWhere('profiles.role IN (:roles)')
  262.                                     ->setParameter('roles'OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
  263.                                     ->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
  264.                                     ->setParameter('mask'Reachability::REACHABLE)
  265.                                     ->getQuery()
  266.                                     ->getSingleScalarResult(),
  267.                                 'unreachable' =>  $this->getProfileRepository()
  268.                                     ->createQueryBuilder('profiles')
  269.                                     ->select('COUNT(profiles)')
  270.                                     ->andWhere('profiles.role IN (:roles)')
  271.                                     ->setParameter('roles'OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
  272.                                     ->andWhere('BIT_AND(profiles.reachability, :mask) > 0')
  273.                                     ->setParameter('mask'Reachability::UNREACHABLE)
  274.                                     ->getQuery()
  275.                                     ->getSingleScalarResult(),
  276.                                 'no-contact' =>  $this->getProfileRepository()
  277.                                     ->createQueryBuilder('profiles')
  278.                                     ->select('COUNT(profiles)')
  279.                                     ->andWhere('profiles.role IN (:roles)')
  280.                                     ->setParameter('roles'OneRosterUser::TYPES_MAPPING[OneRosterUser::TYPES[$key]])
  281.                                     ->andWhere('profiles.reachability = :mask')
  282.                                     ->setParameter('mask'Reachability::NO_CONTACT)
  283.                                     ->getQuery()
  284.                                     ->getSingleScalarResult(),
  285.                             ];
  286.                         }
  287.                     }
  288.                     return $data;
  289.                 })(),
  290.             ]
  291.         ]);
  292.     }
  293.     /**
  294.      * TODO: this likely should be moved elsewhere as it's not really related to the notifications at all.
  295.      *
  296.      * @param SocialAccount $account
  297.      * @return RedirectResponse
  298.      *
  299.      * @Route(
  300.      *     "/_social_avatar/{account}",
  301.      *     name = self::ROUTES__SOCIAL_AVATAR,
  302.      *     requirements = {
  303.      *         "account" = "[1-9]\d*",
  304.      *     },
  305.      * )
  306.      * @ParamConverter(
  307.      *     "account",
  308.      *     class = SocialAccount::class,
  309.      * )
  310.      */
  311.     public function socialAvatarAction(SocialAccount $account): RedirectResponse
  312.     {
  313.         switch (true) {
  314.             case $account instanceof TwitterSocialAccount:
  315.                 $url $this->getTwitterService()->thumb(
  316.                     $account->getTwitterAccessToken(),
  317.                     $account->getTwitterTokenSecret(),
  318.                 );
  319.                 break;
  320.             case $account instanceof FacebookSocialAccount:
  321.                 $url $this->getFacebookService()->getPagePicture(
  322.                     $account->getFacebookAccessToken(),
  323.                     $account->getFacebookPageId()
  324.                 );
  325.                 break;
  326.             case $account instanceof InstagramSocialAccount:
  327.                 $url $this->getInstagramService()->getProfilePicture(
  328.                     $account->getInstagramAccessToken(),
  329.                     $account->getInstagramProfileId()
  330.                 );
  331.                 break;
  332.             default:
  333.                 throw new \Exception();
  334.         }
  335.         if (empty($url)) {
  336.             throw new \Exception();
  337.         }
  338.         return $this->redirect($url);
  339.     }
  340.     /**
  341.      * @return AjaxHtmlView
  342.      *
  343.      * @Route(
  344.      *     "/_select_modal",
  345.      *     name = self::ROUTES__SELECT_MODAL,
  346.      * )
  347.      */
  348.     public function selectModalAction(): AjaxHtmlView
  349.     {
  350.         // AUDIT
  351.         $this->denyAccessUnlessMaybeGranted([
  352.             'app.notifications.messaging.general',
  353.             'app.notifications.messaging.urgent',
  354.         ]);
  355.         return $this->ajax();
  356.     }
  357.     /**
  358.      * @param string $type
  359.      * @param int $pagination
  360.      * @return AjaxHtmlView|Response
  361.      *
  362.      * @Route(
  363.      *     "/_broadcast_modal/{type}/{pagination}",
  364.      *     name = self::ROUTES__BROADCAST_MODAL,
  365.      *     requirements = {
  366.      *         "type" = "all|general|urgent",
  367.      *         "pagination" = "[0-9]+",
  368.      *     },
  369.      *     defaults = {
  370.      *         "type" = "all",
  371.      *         "pagination" = 0,
  372.      *     },
  373.      * )
  374.      */
  375.     public function broadcastModalAction(string $typeint $pagination 0)
  376.     {
  377.         // AUDIT
  378.         $this->denyAccessUnlessMaybeGranted($type === 'all' ? [
  379.             'app.notifications.messaging.general',
  380.             'app.notifications.messaging.urgent',
  381.         ] : [sprintf('app.notifications.messaging.%s'$type)]);
  382.         // create a search as it uses some more complex logic depending on type
  383.         $search = (new TemplateSearch())
  384.             ->setDefaults(false);
  385.         switch ($type) {
  386.             case 'general':
  387.                 $urgent false;
  388.                 $search->setFilter(TemplateSearch::FILTERS__SENDABLE_GENERAL);
  389.                 break;
  390.             case 'urgent':
  391.                 $urgent true;
  392.                 $search->setFilter(TemplateSearch::FILTERS__SENDABLE_URGENT);
  393.                 break;
  394.             default:
  395.                 $urgent null;
  396.         }
  397.         // call common logic
  398.         $result $this->doSearch(
  399.             Template::class,
  400.             'templates',
  401.             $search,
  402.             $this->createNamed('broadcast_query'TemplateSearchForm::class, $search)
  403.                 ->remove('filter')
  404.                 ->add('filter'HiddenType::class),
  405.             $pagination,
  406.         );
  407.         // find the default
  408.         $default = ($urgent !== null) ? $this->getEntityManager()->getRepository(Template::class)->findSendableDefault($urgent) : null;
  409.         return ($result instanceof Response) ? $result $this->ajax(
  410.             array_merge(
  411.                 $result,
  412.                 [
  413.                     'default' => $default,
  414.                     'type' => ($type !== 'all') ? $type null,
  415.                 ],
  416.             ),
  417.         );
  418.     }
  419.     /**
  420.      * @return TwitterService|object
  421.      */
  422.     private function getTwitterService(): TwitterService
  423.     {
  424.         return $this->get(__METHOD__);
  425.     }
  426.     /**
  427.      * @return FacebookService|object
  428.      */
  429.     private function getFacebookService(): FacebookService
  430.     {
  431.         return $this->get(__METHOD__);
  432.     }
  433.     /**
  434.      * @return InstagramService|object
  435.      */
  436.     private function getInstagramService(): InstagramService
  437.     {
  438.         return $this->get(__METHOD__);
  439.     }
  440.     /**
  441.      * @return OneRosterSync|null
  442.      */
  443.     protected function getOneRosterSync(): ?OneRosterSync
  444.     {
  445.         return $this->getEntityManager()->getRepository(OneRosterSync::class)->findOneByTenant(
  446.             $this->getTenant()
  447.         );
  448.     }
  449.     /**
  450.      * @return ProfileRepository
  451.      */
  452.     protected function getProfileRepository(): ProfileRepository
  453.     {
  454.         return $this->getEntityManager()->getRepository(Profile::class);
  455.     }
  456.     /**
  457.      * @return StudentRepository
  458.      */
  459.     protected function getStudentRepository(): StudentRepository
  460.     {
  461.         return $this->getEntityManager()->getRepository(Student::class);
  462.     }
  463. }