src/Controller/BrowseController.php line 264

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Document\Subject;
  4. use App\Entity\ExploreTile;
  5. use App\Entity\Roster;
  6. use App\Service\FavoriteManager;
  7. use App\Service\SystemSettings;
  8. use Doctrine\Common\Collections\ArrayCollection;
  9. use Symfony\Component\Routing\Annotation\Route;
  10. use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
  11. use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
  12. use Symfony\Component\Form\Extension\Core\Type\FormType;
  13. use Symfony\Component\Form\Extension\Core\Type\HiddenType;
  14. use Symfony\Component\Form\Extension\Core\Type\TextType;
  15. use Symfony\Component\HttpFoundation\JsonResponse;
  16. use Symfony\Component\HttpFoundation\RedirectResponse;
  17. use Symfony\Component\HttpFoundation\Request;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
  20. use Symfony\Component\HttpKernel\Exception\GoneHttpException;
  21. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  22. /**
  23.  * Roster Landing, Subject, and Search pages
  24.  *
  25.  * @author "Cornell University, Student Services IT"
  26.  */
  27. class BrowseController extends AbstractController
  28. {
  29.     // bring in our privileges trait
  30.     use \CU\SASCommonBundle\Traits\UserPrivilegesTrait;
  31.     /**
  32.      * Application landing. Redirects to 'Browse' for the default, publicly available roster.
  33.      *
  34.      * Uses Symfony gateway cache only
  35.      *
  36.      * @Route("/", name="index", methods={"GET"})
  37.      *
  38.      * @return \Symfony\Component\HttpFoundation\RedirectResponse
  39.      */
  40.     public function defaultAction()
  41.     {
  42.         return $this->shortUrlAction();
  43.     }
  44.     /**
  45.      * Alternate application landing. Redirects to 'Browse' for the default, publicly available roster.
  46.      *
  47.      * Uses Symfony gateway cache only
  48.      *
  49.      * @Route("/browse", name="app_browse_default", methods={"GET"})
  50.      *
  51.      * @return \Symfony\Component\HttpFoundation\RedirectResponse
  52.      */
  53.     public function shortUrlAction()
  54.     {
  55.         // get the current default roster
  56.         // in the event that no default=Y/available=Y/authRequired=N roster, an exception is thrown
  57.         $rosterSlug $this->getSystemSettings()->getRosterPrintDefault()->getSlug();
  58.         // intentionally use 302 here, because when new roster becomes available, the default will change.
  59.         $response $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $rosterSlug]), '302');
  60.         if ($this->isGatewayCacheEnabled()) {
  61.             $response->setPublic();
  62.             $response->setMaxAge($this->getGatewayCacheLifetime());
  63.             $response->setSharedMaxAge($this->getGatewayCacheLifetime());
  64.         }
  65.         return $response;
  66.     }
  67.     /**
  68.      * Shortcut to explorr for default roster
  69.      *
  70.      * Uses Symfony gateway cache only
  71.      *
  72.      * @Route("/explore", name="app_browse_default_alt", methods={"GET"})
  73.      *
  74.      * @return \Symfony\Component\HttpFoundation\RedirectResponse
  75.      */
  76.     public function shortUrlExploreAction()
  77.     {
  78.         // get the current default roster
  79.         // in the event that no default=Y/available=Y/authRequired=N roster, an exception is thrown
  80.         $rosterSlug $this->getSystemSettings()->getRosterPrintDefault()->getSlug();
  81.         // intentionally use 302 here, because when new roster becomes available, the default will change.
  82.         $response $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $rosterSlug]) . '#explore''302');
  83.         if ($this->isGatewayCacheEnabled()) {
  84.             $response->setPublic();
  85.             $response->setMaxAge($this->getGatewayCacheLifetime());
  86.             $response->setSharedMaxAge($this->getGatewayCacheLifetime());
  87.         }
  88.         return $response;
  89.     }
  90.     /**
  91.      * Plain page to show last updated date/times for publicly available rosters.
  92.      *
  93.      * Uses Symfony gateway cache only
  94.      *
  95.      * @Route("/browse/last-updated", name="app_browse_last_updated", methods={"GET"})
  96.      *
  97.      * @param Request $request
  98.      * @return \Symfony\Component\HttpFoundation\Response
  99.      */
  100.     public function lastUpdatedAction(Request $request)
  101.     {
  102.         $response = new Response();
  103.         if ($this->isGatewayCacheEnabled()) {
  104.             $response->setPublic();
  105.             $response->setMaxAge($this->getGatewayCacheLifetime());
  106.             $response->setSharedMaxAge($this->getGatewayCacheLifetime());
  107.         }
  108.         $roster $this->getSystemSettings()->getRosterPrintDefault();
  109.         $availRosters $this->getSystemSettings()->getDropdownRosters();
  110.         $canonicalUrl $this->generateCanonicalUrl($request->getRequestUri());
  111.         return $this->render('Browse/lastUpdated.html.twig', ['roster' => $roster,
  112.             'availRosters' => $availRosters,
  113.             'faqNavItems' => $this->getFaqNavItems(),
  114.             'canonicalUrl' => $canonicalUrl
  115.         ], $response);
  116.     }
  117.     /**
  118.      * Plain page to show archived rosters
  119.      * Uses Symfony gateway cache only
  120.      *
  121.      * @Route("/browse/archived-rosters", name="app_browse_archived", methods={"GET"})
  122.      *
  123.      * @return \Symfony\Component\HttpFoundation\Response
  124.      */
  125.     public function archivedAction()
  126.     {
  127.         $response = new Response();
  128.         if ($this->isGatewayCacheEnabled()) {
  129.             $response->setPublic();
  130.             $response->setMaxAge($this->getGatewayCacheLifetime());
  131.             $response->setSharedMaxAge($this->getGatewayCacheLifetime());
  132.         }
  133.         $archivedRosters $this->getSystemSettings()->getArchivedRosters();
  134.         $archivedRosters array_reverse($archivedRosters->toArray());
  135.         $roster $this->getSystemSettings()->getRosterPrintDefault();
  136.         $availRosters $this->getSystemSettings()->getDropdownRosters();
  137.         return $this->render('Browse/archived.html.twig', ['roster' => $roster,
  138.             'availRosters' => $availRosters,
  139.             'faqNavItems' => $this->getFaqNavItems(),
  140.             'archivedRosters' => $archivedRosters,
  141.         ], $response);
  142.     }
  143.     /**
  144.      * A roster's default page shows available subjects and academic groups
  145.      *
  146.      * Uses Symfony gateway cache
  147.      *
  148.      * @Route("/browse/roster/{rosterSlug}", name="app_browse_roster", defaults={"rosterSlug": ""}, methods={"GET"})
  149.      *
  150.      * @param Request $request
  151.      * @param string  $rosterSlug
  152.      * @return \Symfony\Component\HttpFoundation\Response
  153.      */
  154.     public function rosterAction(Request $request$rosterSlug '')
  155.     {
  156.         list($roster$availRosters) = $this->getSpecifiedAndAvailRosters($rosterSlug);
  157.         $response $this->getResponseForRoster($roster);
  158.         // we use HTTP cache with a short lifetime
  159.         // get all the subjects!
  160.         $termSetup $this->getSystemSettings()->getTermSetup($roster);
  161.         $acadGroups $termSetup->getAcadGroupsScheduled();
  162.         list($form$formFields) = $this->getSearchForm($roster);
  163.         $subjectsScheduledSorted $termSetup->getSubjectsScheduled();
  164.         $total $subjectsScheduledSorted->count();
  165.         $firstHalf floor($total 2) + (($total == 0) ? 1);
  166.         $subjectsFirst $subjectsScheduledSorted->slice(0$firstHalf);
  167.         $subjectsSecond $subjectsScheduledSorted->slice($firstHalf$firstHalf);
  168.         $canonicalUrl $this->generateCanonicalUrl($request->getRequestUri());
  169.         // Tab: Cornell Tech
  170.         $cornellTechSubjects = new ArrayCollection();
  171.         $collection $this->getDocumentManager()->getDocumentCollection('\App\Document\ClassSchedule');
  172.         $cornellTechSubjectsRaw $collection->distinct('subject', ['versionId' => $roster->getVersion()->getId(), 'enrlGroups.classSections.location' => 'NYCTECH']);
  173.         foreach ($cornellTechSubjectsRaw AS $subjectCode) {
  174.             $cornellTechSubjects[] = $termSetup->getSubject($subjectCode);
  175.         }
  176.         $iterator $cornellTechSubjects->getIterator();
  177.         $iterator->uasort(function($first$second) {
  178.             /* @var $first Subject */
  179.             /* @var $second Subject */
  180.             return strcmp($first->getDescr(), $second->getDescr());
  181.         });
  182.         $cornellTechSubjectsSorted = new ArrayCollection(iterator_to_array($iterator));
  183.         $exploreTileVersions $this->getExploreManager()->getTilesAvailableForRoster($roster);
  184.         return $this->render('Browse/roster.html.twig', ['availRosters' => $availRosters'roster' => $roster'acadGroups' => $acadGroups'subjectCount' => $total,
  185.             'subjectsFirst' => $subjectsFirst'subjectsSecond' => $subjectsSecond'filterForm' => $form->createView(), 'formFields' => $formFields'canonicalUrl' => $canonicalUrl,
  186.             'cornellTechSubjects' => $cornellTechSubjectsSorted,
  187.             'exploreTileVersions' => $exploreTileVersions
  188.         ], $response);
  189.     }
  190.     /**
  191.      * Allows publishing static URL for a subject to always point to most recent
  192.      * Redirects to the current default subject
  193.      *
  194.      * Uses Symfony gateway cache only
  195.      *
  196.      * Path should never change, published.
  197.      * @Route("/browse/inbound/subject/{subjectCode}", name="app_browse_roster_subject_inbound", methods={"GET"})
  198.      *
  199.      * @param string $subjectCode
  200.      * @return \Symfony\Component\HttpFoundation\RedirectResponse
  201.      */
  202.     public function subjectInboundAction($subjectCode)
  203.     {
  204.         // get the current default roster
  205.         $roster $this->getSystemSettings()->getRosterPrintDefault();
  206.         $response $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subjectCode]), '302');
  207.         if ($this->isGatewayCacheEnabled()) {
  208.             $response->setPublic();
  209.             $response->setMaxAge($this->getGatewayCacheLifetime());
  210.             $response->setSharedMaxAge($this->getGatewayCacheLifetime());
  211.         }
  212.         $this->getLogger()->info('inbound subject link', [$subjectCode]);
  213.         return $response;
  214.     }
  215.     /**
  216.      * Subject
  217.      *
  218.      * Uses Symfony gateway cache
  219.      *
  220.      * @Route("/browse/roster/{rosterSlug}/subject/{subjectCode}", name="app_browse_roster_subject", methods={"GET"})
  221.      *
  222.      * @param Request $request
  223.      * @param string  $rosterSlug
  224.      * @param string  $subjectCode
  225.      * @return \Symfony\Component\HttpFoundation\Response
  226.      */
  227.     public function viewSubjectAction(Request $request$rosterSlug$subjectCode)
  228.     {
  229.         /* @var $roster Roster */
  230.         list($roster$availRosters) = $this->getSpecifiedAndAvailRosters($rosterSlug);
  231.         $response $this->getResponseForRoster($roster);
  232.         // we use HTTP cache with a short lifetime
  233.         $termSetup $this->getSystemSettings()->getTermSetup($roster);
  234.         $subject $termSetup->getSubject($subjectCode);
  235.         // if a user enters a subject code that isn't found, rather than throw a 404, check if simply should be uppercased
  236.         // this could also have been accomplished with rewriterules but gets too complicated!
  237.         // this is done to keep case sensitive canonical URLs
  238.         // note we DON'T, CAN'T and SHOULDN'T do this for rosterSlug as they are stored as case sensitive unique in roster table
  239.         if (!$subject && strtoupper($subjectCode) != $subjectCode) {
  240.             $subjectCodeUC strtoupper($subjectCode);
  241.             // does subject exist as all uppercase?
  242.             $subjectAttempt2 $termSetup->getSubject($subjectCodeUC);
  243.             if (!$subjectAttempt2) {
  244.                 throw new NotFoundHttpException('Subject not found');
  245.             }
  246.             // subject exists as all Upper, so redirect
  247.             $response $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subjectCodeUC]), '302');
  248.             if ($this->isGatewayCacheEnabled()) {
  249.                 $response->setPublic();
  250.                 $response->setMaxAge($this->getGatewayCacheLifetime());
  251.                 $response->setSharedMaxAge($this->getGatewayCacheLifetime());
  252.             }
  253.             $this->getLogger()->info('redirect subject link for case sensitivity', [$rosterSlug$subjectCodeUC]);
  254.             return $response;
  255.         }
  256.         if (!$subject) {
  257.             throw new NotFoundHttpException('Subject not found');
  258.         }
  259.         $classes $this->getClassRepository()->findBySubject($roster->getVersion()->getId(), $subjectCode);
  260.         // if no classes, throw subject not found
  261.         if (!count($classes->toArray())) {
  262.             throw new NotFoundHttpException('Subject not found');
  263.         }
  264.         // find the academic groups for this subject
  265.         $acadGroups $termSetup->getAcadGroupsWithSubject($subjectCode);
  266.         // set default for subject search!
  267.         list($form$formFields) = $this->getSearchForm($roster, ['subjects' => [$subjectCode]]);
  268.         return $this->render('Browse/viewSubject.html.twig', [
  269.             'availRosters' => $availRosters,
  270.             'roster' => $roster,
  271.             'filterForm' => $form->createView(),
  272.             'formFields' => $formFields,
  273.             'subject' => $subject,
  274.             'acadGroups' => $acadGroups,
  275.             'classes' => $classes,
  276.             'classRenderOptions' => [
  277.                 'showFacility' => $this->getCalculatedShowFacility($roster),
  278.             ],
  279.             'canonicalUrl' => $this->generateCanonicalUrl($request->getRequestUri()),
  280.         ], $response);
  281.     }
  282.     /**
  283.      * Course Detail
  284.      *
  285.      * Uses Symfony gateway cache
  286.      *
  287.      * @Route("/browse/roster/{rosterSlug}/class/{subjectCode}/{catalogNbr}", name="app_browse_roster_class", requirements={"catalogNbr": "\w+"}, methods={"GET"})
  288.      *
  289.      * @param Request $request
  290.      * @param string  $rosterSlug
  291.      * @param string  $subjectCode
  292.      * @param string  $catalogNbr
  293.      * @return \Symfony\Component\HttpFoundation\Response
  294.      */
  295.     public function viewClassAction(Request $request$rosterSlug$subjectCode$catalogNbr)
  296.     {
  297.         list($roster$availRosters) = $this->getSpecifiedAndAvailRosters($rosterSlug);
  298.         $response $this->getResponseForRoster($roster);
  299.         // we use HTTP cache with a short lifetime
  300.         $class $this->getClassRepository()->findSingleClass($roster->getVersion()->getId(), $subjectCode$catalogNbr);
  301.         if (!$class) {
  302.             throw new GoneHttpException('Class Not Found. Offerings Change Daily');
  303.         }
  304.         $classes = new ArrayCollection();
  305.         $classes->add($class);
  306.         $classRenderOptions = [
  307.             'collapseSchedule' => false,
  308.             'showAllFields' => true,
  309.             'showFacility' => $this->getCalculatedShowFacility($roster),
  310.         ];
  311.         $canonicalUrl $this->generateCanonicalUrl($request->getRequestUri());
  312.         return $this->render('Browse/viewClass.html.twig', ['roster' => $roster,
  313.             'classes' => $classes,
  314.             'class' => $class,
  315.             'availRosters' => $availRosters,
  316.             'classRenderOptions' => $classRenderOptions,
  317.             'canonicalUrl' => $canonicalUrl,
  318.         ], $response);
  319.     }
  320.     /**
  321.      * Support a blind search from Students Essentials (Student Administrative Portal)
  322.      * Allows publishing static URL for folks to point against.
  323.      *
  324.      * /search/inbound-roster/{rosterSlug}/?q=<SEARCHTERM>
  325.      *
  326.      * Redirects to the current default search
  327.      *
  328.      * Path not publicized, only really need is redirects from old Course and Time Roster search
  329.      * @Route("/search/inbound-roster/{rosterSlug}/", name="app_search_inbound_roster", methods={"GET"})
  330.      *
  331.      * @param Request $request
  332.      * @param string  $rosterSlug
  333.      * @return RedirectResponse
  334.      */
  335.     public function searchInboundRosterAction(Request $request$rosterSlug)
  336.     {
  337.         $roster $this->getSystemSettings()->getRosterBySlug($rosterSlug);
  338.         $searchTerm trim($request->query->get('q'''));
  339.         // if search term contains @cornell.edu, strip it - likely searching for an instructor
  340.         $searchTerm str_replace('@cornell.edu'''$searchTerm);
  341.         // if search term is empty, send to browse
  342.         if (!$searchTerm) {
  343.             $this->getLogger()->info('inbound roster search empty, redirecting to browse', [$searchTerm$roster->getSlug()]);
  344.             return $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $roster->getSlug()]), '302');
  345.         }
  346.         // check if the search term is a subject code, if so, direct to that subject page
  347.         $termSetup $this->getSystemSettings()->getTermSetup($roster);
  348.         $subject $termSetup->getSubjectByInsensitiveSearch($searchTerm);
  349.         if ($subject) {
  350.             $this->getLogger()->info('inbound roster search redirecting to subject', [$searchTerm$roster->getSlug()]);
  351.             return $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subject->getSubject()]), '302');
  352.         }
  353.         // is it a netid?
  354.         $repository $this->getDocumentManager()->getRepository('AppBundle:Instructor');
  355.         $instructor $repository->findOneByNetid($roster->getVersion()->getId(), $searchTerm);
  356.         if ($instructor) {
  357.             $searchTerm 'pi:' $searchTerm;
  358.             $this->getLogger()->info('inbound roster search converted to instructor', [$searchTerm$roster->getSlug()]);
  359.         } else {
  360.             $this->getLogger()->info('inbound roster search', [$searchTerm$roster->getSlug()]);
  361.         }
  362.         return $this->redirect($this->generateUrl('app_search', ['rosterSlug' => $roster->getSlug(), 'q' => $searchTerm]), '302');
  363.     }
  364.     /**
  365.      * Support a blind search from Students Essentials (Student Administrative Portal)
  366.      * Allows publishing static URL for folks to point against.
  367.      *
  368.      * /search/inbound/?q=<SEARCHTERM>
  369.      *
  370.      * Redirects to the current default search
  371.      *
  372.      * As of 2014-09-05, /search/inbound/q?= has been shared with CWD and cannot be changed.
  373.      * @Route("/search/inbound/", name="app_search_inbound", methods={"GET"})
  374.      *
  375.      * @param Request $request
  376.      * @return RedirectResponse
  377.      */
  378.     public function searchInboundAction(Request $request)
  379.     {
  380.         // get the current default roster
  381.         $roster $this->getSystemSettings()->getRosterPrintDefault();
  382.         $searchTerm trim($request->query->get('q'''));
  383.         // if search term contains @cornell.edu, strip it - likely searching for an instructor
  384.         $searchTerm str_replace('@cornell.edu'''$searchTerm);
  385.         // if search term is empty, send to browse
  386.         if (!$searchTerm) {
  387.             $this->getLogger()->info('inbound search empty, redirecting to browse', [$searchTerm$roster->getSlug()]);
  388.             return $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $roster->getSlug()]), '302');
  389.         }
  390.         // check if the search term is a subject code, if so, direct to that subject page
  391.         $termSetup $this->getSystemSettings()->getTermSetup($roster);
  392.         $subject $termSetup->getSubjectByInsensitiveSearch($searchTerm);
  393.         if ($subject) {
  394.             $this->getLogger()->info('inbound search redirecting to subject', [$searchTerm$roster->getSlug()]);
  395.             return $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subject->getSubject()]), '302');
  396.         }
  397.         // is it a netid?
  398.         $repository $this->getDocumentManager()->getRepository('AppBundle:Instructor');
  399.         $instructor $repository->findOneByNetid($roster->getVersion()->getId(), $searchTerm);
  400.         if ($instructor) {
  401.             $searchTerm 'pi:' $searchTerm;
  402.             $this->getLogger()->info('inbound search converted to instructor', [$searchTerm$roster->getSlug()]);
  403.         } else {
  404.             $this->getLogger()->info('inbound search', [$searchTerm$roster->getSlug()]);
  405.         }
  406.         return $this->redirect($this->generateUrl('app_search', ['rosterSlug' => $roster->getSlug(), 'q' => $searchTerm]), '302');
  407.     }
  408.     /**
  409.      * Search
  410.      *
  411.      * Uses Symfony gateway cache
  412.      *
  413.      * @Route("/search/roster/{rosterSlug}", name="app_search", methods={"GET"})
  414.      *
  415.      * @param Request $request
  416.      * @param string  $rosterSlug
  417.      * @return \Symfony\Component\HttpFoundation\Response
  418.      */
  419.     public function searchAction(Request $request$rosterSlug)
  420.     {
  421.         $roster $this->getSystemSettings()->getRosterBySlug($rosterSlug);
  422.         $response $this->getResponseForRoster($roster);
  423.         $availRosters $this->getSystemSettings()->getDropdownRosters();
  424.         list($form$formFields) = $this->getSearchForm($roster);
  425.         // support inbound search pi: shortcut to search for a
  426.         $simpleSearchQ $request->query->get('q');
  427.         if ($simpleSearchQ && mb_substr($simpleSearchQ03) == 'pi:') {
  428.             $request->query->set('pi'mb_substr($simpleSearchQ3));
  429.             $request->query->set('q''');
  430.         }
  431.         $form->handleRequest($request);
  432.         $searchParams = [];
  433.         if ($form->isValid()) {
  434.             // data is an array with "name", "email", and "message" keys
  435.             $searchParams $form->getData();
  436.         } else {
  437.             throw new AccessDeniedHttpException('Unsupported search params');
  438.         }
  439.         // using searchParams, update open fields
  440.         $formFields $this->updateFormFields($formFields$searchParams);
  441.         return $this->render('Browse/searchResults.html.twig', [
  442.             'availRosters' => $availRosters,
  443.             'roster' => $roster,
  444.             'formFields' => $formFields,
  445.             'filterForm' => $form->createView()], $response);
  446.     }
  447.     /**
  448.      * Uses Symfony gateway cache
  449.      *
  450.      * @Route("/search/ajax/roster/{rosterSlug}", name="app_search_ajax_results", defaults={"rosterSlug": ""}, methods={"GET"})
  451.      *
  452.      * @param Request $request
  453.      * @param string  $rosterSlug
  454.      * @param array   $all
  455.      * @return Response
  456.      */
  457.     public function ajaxSearchResultsAction(Request $request$rosterSlug$all = [])
  458.     {
  459.         $roster $this->getSystemSettings()->getRosterBySlug($rosterSlug);
  460.         $response $this->getResponseForRoster($roster);
  461.         list($form) = $this->getSearchForm($roster);
  462.         foreach ($all AS $key => $value) {
  463.             $request->query->set($key$value);
  464.         }
  465.         // fix for controller() subcall adding _path post 2.3. upgrade in late July
  466.         $request->query->remove('_path');
  467.         $form->handleRequest($request);
  468.         $searchParams = [];
  469.         if ($form->isValid()) {
  470.             // data is an array with "name", "email", and "message" keys
  471.             $searchParams $form->getData();
  472.         } else {
  473.             throw new AccessDeniedHttpException('Unsupported search params');
  474.         }
  475.         $searchResults = new ArrayCollection();
  476.         $foundHash = [];
  477.         // STEP 1: new in v3
  478.         // first pass search requires:
  479.         //  1.) q present and length >= 2 and length <= 13
  480.         //  2.) empty course descr search
  481.         //  3.) empty SUBJECT search
  482.         $stage1SearchUsed = !empty($searchParams['q']) && (strlen($searchParams['q']) >= 2) && (strlen($searchParams['q']) <= 13)
  483.             && empty($searchParams['emptyDescr']) && empty($searchParams['subjects']);
  484.         if ($stage1SearchUsed) {
  485.             $classResults $this->getClassRepository()->findBySearchPriority($roster$searchParams);
  486.             foreach ($classResults AS $classResult) {
  487.                 $foundHash[] = $classResult->getStrm() . '/' $classResult->getCrseId() . '/' $classResult->getCrseOfferNbr();
  488.                 $searchResults[] = $classResult;
  489.             }
  490.         }
  491.         $exploreFilterUsed = !empty($searchParams['explore']);
  492.         $exploreCriteriaIds = [];
  493.         // do any explore filters used require marks?
  494.         if ($exploreFilterUsed) {
  495.             $classEnrlGroupExploreCriteriaId = [];
  496.             $classSectionExploreCriteriaId = [];
  497.             $searchParams['classEnrlGroupExploreCriteriaIds'] = [];
  498.             $searchParams['classSectionExploreCriteriaIds'] = [];
  499.             // get criteria for each explore tile id
  500.             foreach ($searchParams['explore'] as $exploreTileId) {
  501.                 // get criteria for exploreTileId
  502.                 $exploreTile $this->getEntityManager()->find(ExploreTile::class, $exploreTileId);
  503.                 $exploreCriteria $exploreTile->getExploreCriteria();
  504.                 $exploreCriteriaIds[] = $exploreCriteria->getId();
  505.                 if ($exploreCriteria->hasClassEnrlGroupFilter()) {
  506.                     $classEnrlGroupExploreCriteriaId[] = $exploreCriteria->getId();
  507.                 }
  508.                 if ($exploreCriteria->hasClassSectionFilter()) {
  509.                     $classSectionExploreCriteriaId[] = $exploreCriteria->getId();
  510.                 }
  511.             }
  512.             // "searchParams" here are only used for the MARKS algorithm
  513.             if (count($exploreCriteriaIds) === 1) {
  514.                 $searchParams['classEnrlGroupExploreCriteriaIds'] = $classEnrlGroupExploreCriteriaId;
  515.                 $searchParams['classSectionExploreCriteriaIds'] = $classSectionExploreCriteriaId;
  516.             }
  517.             $searchParams['exploreCriteriaIds'] = $exploreCriteriaIds;
  518.             unset($searchParams['explore']);
  519.         }
  520.         // STEP 2: original search using searchParams, update open fields
  521.         $classResults $this->getClassRepository()->findBySearch($roster$searchParams);
  522.         foreach ($classResults AS $classResult) {
  523.             if ($stage1SearchUsed) {
  524.                 $itemHash $classResult->getStrm() . '/' $classResult->getCrseId() . '/' $classResult->getCrseOfferNbr();
  525.                 if (in_array($itemHash$foundHash)) {
  526.                     continue;
  527.                 }
  528.                 // only allow 250 results
  529.                 if (count($foundHash) > 250) {
  530.                     break;
  531.                 }
  532.                 $foundHash[] = $classResult->getStrm() . '/' $classResult->getCrseId() . '/' $classResult->getCrseOfferNbr();
  533.             }
  534.             $searchResults[] = $classResult;
  535.         }
  536.         $this->getLogger()->debug('Search, result count', [$rosterSlug$searchResults->count()]);
  537.         $classRenderOptions = [
  538.             'showAllFields' => false,
  539.             'showOnlyMarked' => true,
  540.             'showFacility' => $this->getCalculatedShowFacility($roster),
  541.         ];
  542.         return $this->render('Browse/ajaxSearchResults.html.twig', [
  543.             'roster' => $roster,
  544.             'searchParams' => $searchParams,
  545.             'classes' => $searchResults,
  546.             'classRenderOptions' => $classRenderOptions
  547.         ], $response);
  548.     }
  549.     /**
  550.      * JSON Response used to update favorites, sign in/sign out, roster list (validation), dismiss announcements and catalog
  551.      *
  552.      * No caching, user specific
  553.      *
  554.      * @Route("/browse/roster/{rosterSlug}/user-data", name="app_browse_user_data", defaults={"rosterSlug": ""}, methods={"GET"})
  555.      *
  556.      * @param Request $request
  557.      * @param string  $rosterSlug
  558.      * @return \Symfony\Component\HttpFoundation\JsonResponse
  559.      */
  560.     public function ajaxUserDataAction(Request $requestFavoriteManager $favoriteManager$rosterSlug)
  561.     {
  562.         $jsonResponse $this->getNoCacheJsonResponse();
  563.         $userData = [];
  564.         $roster $this->getSystemSettings()->getRosterBySlug($rosterSlug);
  565.         $userData['roster'] = $roster->getSlug();
  566.         $userData['time'] = time();
  567.         /* @var $user \CU\SASCommonBundle\Security\User */
  568.         list($loggedIn$user) = $this->getUserLoggedInStatus();
  569.         if ($loggedIn) {
  570.             $userData['login'] = ['status' => true'name' => ($user->getNickname()?$user->getNickname():$user->getFirstName()) . ' ' $user->getLastName(), 'netid' => $user->getNetid()];
  571.         } else {
  572.             $userData['login'] = ['status' => false'name' => '''email' => ''];
  573.         }
  574.         $favorites = [];
  575.         if ($loggedIn) {
  576.             $favoritesAll $favoriteManager->getUserFavorites($roster$this->getUser()->getUserId());
  577.             $favorites array_map(function($favorite) {
  578.                 return ['favId' => $favorite->getId(), 'classNbr' => $favorite->getClassNbr()];
  579.             }, $favoritesAll);
  580.         }
  581.         $userData['favorites'] = $favorites;
  582.         $tokens = [];
  583.         $tokens['favorites'] = $this->generateCsrfToken('favorites');
  584.         $tokens['announcement'] = $this->generateCsrfToken('announcement');
  585.         $userData['tokens'] = $tokens;
  586.         // additional rosters (auth required)
  587.         $authRosters = [];
  588.         if ($loggedIn) {
  589.             // Private rosters
  590.             $rosterPermits $this->getParameter('roster_permits');
  591.             $intersection $this->hasUserActiveDirectoryGroupIntersection($rosterPermits['private']);
  592.             if ($intersection) {
  593.                 $authRequiredRosters $this->getSystemSettings()->getAuthRequiredRosters('Y1');
  594.                 foreach ($authRequiredRosters AS $authRequiredRoster) {
  595.                     if ($authRequiredRoster->isArchived()) {
  596.                         continue;
  597.                     }
  598.                     $currentPath $request->get('currentPath');
  599.                     $nextPath str_replace('ROSTER-REPLACE'$authRequiredRoster->getSlug(), $currentPath);
  600.                     $authRosters[] = ['url' => $nextPath,
  601.                         'descr' => $authRequiredRoster->getDescr(),
  602.                         'title' => $authRequiredRoster->getStrm()];
  603.                 }
  604.             }
  605.             // Super Private rosters
  606.             $intersection $this->hasUserActiveDirectoryGroupIntersection($rosterPermits['super_private']);
  607.             if ($intersection) {
  608.                 $authRequiredRosters $this->getSystemSettings()->getAuthRequiredRosters('Y2');
  609.                 foreach ($authRequiredRosters AS $authRequiredRoster) {
  610.                     $currentPath $request->get('currentPath');
  611.                     $nextPath str_replace('ROSTER-REPLACE'$authRequiredRoster->getSlug(), $currentPath);
  612.                     $authRosters[] = ['url' => $nextPath,
  613.                         'descr' => $authRequiredRoster->getDescr(),
  614.                         'title' => $authRequiredRoster->getStrm()];
  615.                 }
  616.             }
  617.         }
  618.         $userData['authRosters'] = $authRosters;
  619.         $userDismissedAnnouncements = [];
  620.         if ($loggedIn) {
  621.             $announcementQuery '
  622.                 SELECT a.announce_id
  623.                   FROM user_announcement ua, announcement a
  624.                  WHERE ua.announce_id=a.announce_id
  625.                    AND a.active="Y"
  626.                    AND a.allow_dismiss="Y"
  627.                    AND ua.user_id=?
  628.                    AND ua.roster_id=?';
  629.             $announceResults $this->getConnection()->fetchAll($announcementQuery, [$user->getUserId(), $roster->getId()]);
  630.             foreach ($announceResults AS $announceResult) {
  631.                 $userDismissedAnnouncements[] = $announceResult['announce_id'];
  632.             }
  633.         }
  634.         // identify announcements user should see
  635.         $announcements = [];
  636.         foreach ($roster->getAnnouncements() AS $announcement) {
  637.             if ($announcement->visibileTo('Browse') && $announcement->shouldDisplay() && !in_array($announcement->getId(), $userDismissedAnnouncements)) {
  638.                 $announcements[] = [
  639.                     'id' => $announcement->getId(),
  640.                     'showOnAllPages' => true,
  641.                     'canDismiss' => $announcement->canDismiss(),
  642.                     'title' => $announcement->getTitle(),
  643.                     'body' => $announcement->getBody(),
  644.                     'alertLevel' => $announcement->getAlertLevel(),
  645.                 ];
  646.             }
  647.         }
  648.         $userData['announcements'] = $announcements;
  649.         $alertHtml '';
  650.         if ($loggedIn) {
  651.             $alertHtml '';
  652.             foreach ($this->getFlashBag()->all() AS $flashBagType => $flashBagMsgs) {
  653.                 foreach ($flashBagMsgs AS $flashBagMsg) {
  654.                     $alertHtml .= '<div class="alert alert-' $flashBagType '" role="alert">';
  655.                     $alertHtml .= $flashBagMsg;
  656.                     $alertHtml .= '</div>';
  657.                 }
  658.             }
  659.         }
  660.         $userData['alertHtml'] = $alertHtml;
  661.         $settingsUrl '';
  662.         if ($loggedIn && $user->hasPriv('SETTINGS')) {
  663.             $settingsUrl $this->generateUrl('app_settings');
  664.         }
  665.         $userData['extraMenu'] = ['settingsUrl' => $settingsUrl];
  666.         // are we impersonating?
  667.         $footerText '';
  668.         if ($loggedIn && $this->getSecurityAuthorizationChecker()->isGranted('ROLE_PREVIOUS_ADMIN')) {
  669.             $footerText .= '<p><strong>Impersonating:</strong> ' $user->getLastname() . ', ' . ($user->getNickname() ? $user->getNickname() : $user->getFirstName()) . ' (' $user->getUsername() . ')';
  670.             $footerText .= ' <a class="footer-link" href="' $this->generateUrl('app_favs_default', ['_switch_netid' => '_exit']) . '">End Impersonation</a></p>';
  671.         }
  672.         $userData['footerText'] = $footerText;
  673.         $content json_encode($userData);
  674.         $jsonResponse->setContent($content);
  675.         return $jsonResponse;
  676.     }
  677.     /**
  678.      * Record a user dismissing an announcement
  679.      *
  680.      * No cache, user-specific
  681.      *
  682.      * @Route("/browse/user-dimiss-announcement", name="app_browse_dimiss_announcement", methods={"POST"})
  683.      *
  684.      * @param Request $request
  685.      * @return \Symfony\Component\HttpFoundation\JsonResponse
  686.      */
  687.     public function ajaxDismissAnnouncementAction(Request $request)
  688.     {
  689.         $rosterSlug $request->request->get('roster');
  690.         $announceId $request->request->get('announceId''');
  691.         $token $request->request->get('token''');
  692.         $roster $this->getSystemSettings()->getRosterBySlug($rosterSlug);
  693.         $intention 'announcement';
  694.         if ($this->isCsrfTokenValid($intention$token)) {
  695.             // does announcement exist and is it
  696.             $announcement $roster->getAnnouncement($announceId);
  697.             if ($announcement && $announcement->shouldDisplay() && $announcement->canDismiss()) {
  698.                 $this->getConnection()->insert('user_announcement', ['roster_id' => $roster->getId(),
  699.                     'user_id' => $this->getSecurityTokenStorage()->getToken()->getUser()->getUserId(),
  700.                     'announce_id' => $announceId,
  701.                     'dismiss_dttm' => date('Y-m-d H:i:s')
  702.                 ]);
  703.                 $content json_encode(['success' => true]);
  704.             } else {
  705.                 $content json_encode(['success' => false'message' => 'Announcement not found or cannot dismiss.']);
  706.             }
  707.         } else {
  708.             $content json_encode(['success' => false'message' => 'Invalid token']);
  709.         }
  710.         $jsonResponse $this->getNoCacheJsonResponse();
  711.         $jsonResponse->setContent($content);
  712.         return $jsonResponse;
  713.     }
  714.     /**
  715.      * Handle select2 dynamic search (instructor)
  716.      *
  717.      * Uses Symfony gateway cache only
  718.      *
  719.      * Results like following:
  720.      * {"results":[{"id":456,"text":"Plant Science 466"},{"id":678,"text":"Statler Hall B30"}]}
  721.      *
  722.      * @Route("/search/instructor/{rosterSlug}/{field}", name="app_browse_roster_instructor_search", methods={"GET"})
  723.      *
  724.      * @param Request $request
  725.      * @param string  $rosterSlug
  726.      * @param string  $field
  727.      * @return JsonResponse
  728.      */
  729.     public function ajaxSelectSearchAction(Request $request$rosterSlug$field)
  730.     {
  731.         $q $request->query->get('q''');
  732.         // catch the exception so we can fail nicely for JSON.
  733.         try {
  734.             $roster $this->getSystemSettings()->getRosterBySlug($rosterSlug);
  735.         } catch (\Exception $e) {
  736.             // just break the search, so that we can return no results
  737.             $q '';
  738.         }
  739.         $dm $this->getDocumentManager();
  740.         $response = new JsonResponse();
  741.         if ($this->isGatewayCacheEnabled()) {
  742.             $response->setPublic();
  743.             $response->setMaxAge($this->getGatewayCacheLifetime());
  744.             $response->setSharedMaxAge($this->getGatewayCacheLifetime());
  745.         }
  746.         $results = [];
  747.         if (strlen($q) >= 2) {
  748.             switch ($field) {
  749.                 case 'instructor':
  750.                     /* @var $repository FacilityRepository */
  751.                     $repository $dm->getRepository('AppBundle:Instructor');
  752.                     $instructors $repository->findByName($roster->getVersion()->getId(), $q);
  753.                     foreach ($instructors AS $instructor) {
  754.                         $results[] = array('id' => $instructor->getNetid(), 'text' => $instructor->getName());
  755.                     }
  756.                     break;
  757.                 default:
  758.                     break;
  759.             }
  760.         }
  761.         $response->setData(['results' => $results]);
  762.         return $response;
  763.     }
  764.     /**
  765.      * Course Details appearing in a modal
  766.      *
  767.      * @Route("/browse/course-details-modal/{rosterSlug}/{subjectCode}/{catalogNbr}", name="app_browse_course_details_modal", defaults={"rosterSlug":"", "subjectCode":"", "catalogNbr":""}, methods={"GET"})
  768.      * @param string $rosterSlug
  769.      * @param string $subjectCode
  770.      * @param string $catalogNbr
  771.      * @return \Symfony\Component\HttpFoundation\Response
  772.      */
  773.     public function courseDetailsAction($rosterSlug$subjectCode$catalogNbr)
  774.     {
  775.         list($roster) = $this->getSpecifiedAndAvailRosters($rosterSlug);
  776.         $class $this->getClassRepository()->findSingleClass($roster->getVersion()->getId(), $subjectCode$catalogNbr);
  777.         if (!$class) {
  778.             throw new GoneHttpException('Class Not Found. Offerings Change Daily');
  779.         }
  780.         $classes = new ArrayCollection();
  781.         $classes->add($class);
  782.         $classRenderOptions = [
  783.             'collapseSchedule' => false,
  784.             'showAllFields' => true,
  785.             'showFacility' => $this->getCalculatedShowFacility($roster),
  786.         ];
  787.         $response $this->getNoCacheResponse();
  788.         return $this->render('Browse/courseDetailsModal.html.twig', ['roster' => $roster,
  789.             'classes' => $classes,
  790.             'class' => $class,
  791.             'classRenderOptions' => $classRenderOptions
  792.         ], $response);
  793.     }
  794.     protected function updateFormFields($formFields$searchParams)
  795.     {
  796.         foreach ($formFields AS $k => $formField) {
  797.             $open = !empty($searchParams[$formField['name']]);
  798.             $formFields[$k]['open'] = $open;
  799.         }
  800.         return $formFields;
  801.     }
  802.     private function getUserLoggedInStatus()
  803.     {
  804.         /* @var $user \CU\SASCommonBundle\Security\User */
  805.         $user $this->getUser();
  806.         return array(
  807.             // the result of this condition is essentially '$loggedIn'
  808.             $this->getSecurityTokenStorage()->getToken()->isAuthenticated() && is_object($user),
  809.             $user,
  810.         );
  811.     }
  812.     /**
  813.      * Set up showFacility value for based on roster settings and, if
  814.      * necessary, user login status
  815.     */
  816.     private function getCalculatedShowFacility(Roster $roster)
  817.     {
  818.         switch ($roster->getShowFacility()) {
  819.             case 'Y':
  820.                 $showFacility true;
  821.                 break;
  822.             case 'R':
  823.                 list($showFacility) = $this->getUserLoggedInStatus();
  824.                 break;
  825.             default:
  826.                 $showFacility false;
  827.         }
  828.         return $showFacility;
  829.     }
  830. }