<?php
namespace App\Controller;
use App\Document\Subject;
use App\Entity\ExploreTile;
use App\Entity\Roster;
use App\Service\FavoriteManager;
use App\Service\SystemSettings;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\GoneHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Roster Landing, Subject, and Search pages
*
* @author "Cornell University, Student Services IT"
*/
class BrowseController extends AbstractController
{
// bring in our privileges trait
use \CU\SASCommonBundle\Traits\UserPrivilegesTrait;
/**
* Application landing. Redirects to 'Browse' for the default, publicly available roster.
*
* Uses Symfony gateway cache only
*
* @Route("/", name="index", methods={"GET"})
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function defaultAction()
{
return $this->shortUrlAction();
}
/**
* Alternate application landing. Redirects to 'Browse' for the default, publicly available roster.
*
* Uses Symfony gateway cache only
*
* @Route("/browse", name="app_browse_default", methods={"GET"})
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function shortUrlAction()
{
// get the current default roster
// in the event that no default=Y/available=Y/authRequired=N roster, an exception is thrown
$rosterSlug = $this->getSystemSettings()->getRosterPrintDefault()->getSlug();
// intentionally use 302 here, because when new roster becomes available, the default will change.
$response = $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $rosterSlug]), '302');
if ($this->isGatewayCacheEnabled()) {
$response->setPublic();
$response->setMaxAge($this->getGatewayCacheLifetime());
$response->setSharedMaxAge($this->getGatewayCacheLifetime());
}
return $response;
}
/**
* Shortcut to explorr for default roster
*
* Uses Symfony gateway cache only
*
* @Route("/explore", name="app_browse_default_alt", methods={"GET"})
*
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function shortUrlExploreAction()
{
// get the current default roster
// in the event that no default=Y/available=Y/authRequired=N roster, an exception is thrown
$rosterSlug = $this->getSystemSettings()->getRosterPrintDefault()->getSlug();
// intentionally use 302 here, because when new roster becomes available, the default will change.
$response = $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $rosterSlug]) . '#explore', '302');
if ($this->isGatewayCacheEnabled()) {
$response->setPublic();
$response->setMaxAge($this->getGatewayCacheLifetime());
$response->setSharedMaxAge($this->getGatewayCacheLifetime());
}
return $response;
}
/**
* Plain page to show last updated date/times for publicly available rosters.
*
* Uses Symfony gateway cache only
*
* @Route("/browse/last-updated", name="app_browse_last_updated", methods={"GET"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\Response
*/
public function lastUpdatedAction(Request $request)
{
$response = new Response();
if ($this->isGatewayCacheEnabled()) {
$response->setPublic();
$response->setMaxAge($this->getGatewayCacheLifetime());
$response->setSharedMaxAge($this->getGatewayCacheLifetime());
}
$roster = $this->getSystemSettings()->getRosterPrintDefault();
$availRosters = $this->getSystemSettings()->getDropdownRosters();
$canonicalUrl = $this->generateCanonicalUrl($request->getRequestUri());
return $this->render('Browse/lastUpdated.html.twig', ['roster' => $roster,
'availRosters' => $availRosters,
'faqNavItems' => $this->getFaqNavItems(),
'canonicalUrl' => $canonicalUrl
], $response);
}
/**
* Plain page to show archived rosters
* Uses Symfony gateway cache only
*
* @Route("/browse/archived-rosters", name="app_browse_archived", methods={"GET"})
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function archivedAction()
{
$response = new Response();
if ($this->isGatewayCacheEnabled()) {
$response->setPublic();
$response->setMaxAge($this->getGatewayCacheLifetime());
$response->setSharedMaxAge($this->getGatewayCacheLifetime());
}
$archivedRosters = $this->getSystemSettings()->getArchivedRosters();
$archivedRosters = array_reverse($archivedRosters->toArray());
$roster = $this->getSystemSettings()->getRosterPrintDefault();
$availRosters = $this->getSystemSettings()->getDropdownRosters();
return $this->render('Browse/archived.html.twig', ['roster' => $roster,
'availRosters' => $availRosters,
'faqNavItems' => $this->getFaqNavItems(),
'archivedRosters' => $archivedRosters,
], $response);
}
/**
* A roster's default page shows available subjects and academic groups
*
* Uses Symfony gateway cache
*
* @Route("/browse/roster/{rosterSlug}", name="app_browse_roster", defaults={"rosterSlug": ""}, methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @return \Symfony\Component\HttpFoundation\Response
*/
public function rosterAction(Request $request, $rosterSlug = '')
{
list($roster, $availRosters) = $this->getSpecifiedAndAvailRosters($rosterSlug);
$response = $this->getResponseForRoster($roster);
// we use HTTP cache with a short lifetime
// get all the subjects!
$termSetup = $this->getSystemSettings()->getTermSetup($roster);
$acadGroups = $termSetup->getAcadGroupsScheduled();
list($form, $formFields) = $this->getSearchForm($roster);
$subjectsScheduledSorted = $termSetup->getSubjectsScheduled();
$total = $subjectsScheduledSorted->count();
$firstHalf = floor($total / 2) + (($total % 2 == 0) ? 0 : 1);
$subjectsFirst = $subjectsScheduledSorted->slice(0, $firstHalf);
$subjectsSecond = $subjectsScheduledSorted->slice($firstHalf, $firstHalf);
$canonicalUrl = $this->generateCanonicalUrl($request->getRequestUri());
// Tab: Cornell Tech
$cornellTechSubjects = new ArrayCollection();
$collection = $this->getDocumentManager()->getDocumentCollection('\App\Document\ClassSchedule');
$cornellTechSubjectsRaw = $collection->distinct('subject', ['versionId' => $roster->getVersion()->getId(), 'enrlGroups.classSections.location' => 'NYCTECH']);
foreach ($cornellTechSubjectsRaw AS $subjectCode) {
$cornellTechSubjects[] = $termSetup->getSubject($subjectCode);
}
$iterator = $cornellTechSubjects->getIterator();
$iterator->uasort(function($first, $second) {
/* @var $first Subject */
/* @var $second Subject */
return strcmp($first->getDescr(), $second->getDescr());
});
$cornellTechSubjectsSorted = new ArrayCollection(iterator_to_array($iterator));
$exploreTileVersions = $this->getExploreManager()->getTilesAvailableForRoster($roster);
return $this->render('Browse/roster.html.twig', ['availRosters' => $availRosters, 'roster' => $roster, 'acadGroups' => $acadGroups, 'subjectCount' => $total,
'subjectsFirst' => $subjectsFirst, 'subjectsSecond' => $subjectsSecond, 'filterForm' => $form->createView(), 'formFields' => $formFields, 'canonicalUrl' => $canonicalUrl,
'cornellTechSubjects' => $cornellTechSubjectsSorted,
'exploreTileVersions' => $exploreTileVersions
], $response);
}
/**
* Allows publishing static URL for a subject to always point to most recent
* Redirects to the current default subject
*
* Uses Symfony gateway cache only
*
* Path should never change, published.
* @Route("/browse/inbound/subject/{subjectCode}", name="app_browse_roster_subject_inbound", methods={"GET"})
*
* @param string $subjectCode
* @return \Symfony\Component\HttpFoundation\RedirectResponse
*/
public function subjectInboundAction($subjectCode)
{
// get the current default roster
$roster = $this->getSystemSettings()->getRosterPrintDefault();
$response = $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subjectCode]), '302');
if ($this->isGatewayCacheEnabled()) {
$response->setPublic();
$response->setMaxAge($this->getGatewayCacheLifetime());
$response->setSharedMaxAge($this->getGatewayCacheLifetime());
}
$this->getLogger()->info('inbound subject link', [$subjectCode]);
return $response;
}
/**
* Subject
*
* Uses Symfony gateway cache
*
* @Route("/browse/roster/{rosterSlug}/subject/{subjectCode}", name="app_browse_roster_subject", methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @param string $subjectCode
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewSubjectAction(Request $request, $rosterSlug, $subjectCode)
{
/* @var $roster Roster */
list($roster, $availRosters) = $this->getSpecifiedAndAvailRosters($rosterSlug);
$response = $this->getResponseForRoster($roster);
// we use HTTP cache with a short lifetime
$termSetup = $this->getSystemSettings()->getTermSetup($roster);
$subject = $termSetup->getSubject($subjectCode);
// if a user enters a subject code that isn't found, rather than throw a 404, check if simply should be uppercased
// this could also have been accomplished with rewriterules but gets too complicated!
// this is done to keep case sensitive canonical URLs
// note we DON'T, CAN'T and SHOULDN'T do this for rosterSlug as they are stored as case sensitive unique in roster table
if (!$subject && strtoupper($subjectCode) != $subjectCode) {
$subjectCodeUC = strtoupper($subjectCode);
// does subject exist as all uppercase?
$subjectAttempt2 = $termSetup->getSubject($subjectCodeUC);
if (!$subjectAttempt2) {
throw new NotFoundHttpException('Subject not found');
}
// subject exists as all Upper, so redirect
$response = $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subjectCodeUC]), '302');
if ($this->isGatewayCacheEnabled()) {
$response->setPublic();
$response->setMaxAge($this->getGatewayCacheLifetime());
$response->setSharedMaxAge($this->getGatewayCacheLifetime());
}
$this->getLogger()->info('redirect subject link for case sensitivity', [$rosterSlug, $subjectCodeUC]);
return $response;
}
if (!$subject) {
throw new NotFoundHttpException('Subject not found');
}
$classes = $this->getClassRepository()->findBySubject($roster->getVersion()->getId(), $subjectCode);
// if no classes, throw subject not found
if (!count($classes->toArray())) {
throw new NotFoundHttpException('Subject not found');
}
// find the academic groups for this subject
$acadGroups = $termSetup->getAcadGroupsWithSubject($subjectCode);
// set default for subject search!
list($form, $formFields) = $this->getSearchForm($roster, ['subjects' => [$subjectCode]]);
return $this->render('Browse/viewSubject.html.twig', [
'availRosters' => $availRosters,
'roster' => $roster,
'filterForm' => $form->createView(),
'formFields' => $formFields,
'subject' => $subject,
'acadGroups' => $acadGroups,
'classes' => $classes,
'classRenderOptions' => [
'showFacility' => $this->getCalculatedShowFacility($roster),
],
'canonicalUrl' => $this->generateCanonicalUrl($request->getRequestUri()),
], $response);
}
/**
* Course Detail
*
* Uses Symfony gateway cache
*
* @Route("/browse/roster/{rosterSlug}/class/{subjectCode}/{catalogNbr}", name="app_browse_roster_class", requirements={"catalogNbr": "\w+"}, methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @param string $subjectCode
* @param string $catalogNbr
* @return \Symfony\Component\HttpFoundation\Response
*/
public function viewClassAction(Request $request, $rosterSlug, $subjectCode, $catalogNbr)
{
list($roster, $availRosters) = $this->getSpecifiedAndAvailRosters($rosterSlug);
$response = $this->getResponseForRoster($roster);
// we use HTTP cache with a short lifetime
$class = $this->getClassRepository()->findSingleClass($roster->getVersion()->getId(), $subjectCode, $catalogNbr);
if (!$class) {
throw new GoneHttpException('Class Not Found. Offerings Change Daily');
}
$classes = new ArrayCollection();
$classes->add($class);
$classRenderOptions = [
'collapseSchedule' => false,
'showAllFields' => true,
'showFacility' => $this->getCalculatedShowFacility($roster),
];
$canonicalUrl = $this->generateCanonicalUrl($request->getRequestUri());
return $this->render('Browse/viewClass.html.twig', ['roster' => $roster,
'classes' => $classes,
'class' => $class,
'availRosters' => $availRosters,
'classRenderOptions' => $classRenderOptions,
'canonicalUrl' => $canonicalUrl,
], $response);
}
/**
* Support a blind search from Students Essentials (Student Administrative Portal)
* Allows publishing static URL for folks to point against.
*
* /search/inbound-roster/{rosterSlug}/?q=<SEARCHTERM>
*
* Redirects to the current default search
*
* Path not publicized, only really need is redirects from old Course and Time Roster search
* @Route("/search/inbound-roster/{rosterSlug}/", name="app_search_inbound_roster", methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @return RedirectResponse
*/
public function searchInboundRosterAction(Request $request, $rosterSlug)
{
$roster = $this->getSystemSettings()->getRosterBySlug($rosterSlug);
$searchTerm = trim($request->query->get('q', ''));
// if search term contains @cornell.edu, strip it - likely searching for an instructor
$searchTerm = str_replace('@cornell.edu', '', $searchTerm);
// if search term is empty, send to browse
if (!$searchTerm) {
$this->getLogger()->info('inbound roster search empty, redirecting to browse', [$searchTerm, $roster->getSlug()]);
return $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $roster->getSlug()]), '302');
}
// check if the search term is a subject code, if so, direct to that subject page
$termSetup = $this->getSystemSettings()->getTermSetup($roster);
$subject = $termSetup->getSubjectByInsensitiveSearch($searchTerm);
if ($subject) {
$this->getLogger()->info('inbound roster search redirecting to subject', [$searchTerm, $roster->getSlug()]);
return $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subject->getSubject()]), '302');
}
// is it a netid?
$repository = $this->getDocumentManager()->getRepository('AppBundle:Instructor');
$instructor = $repository->findOneByNetid($roster->getVersion()->getId(), $searchTerm);
if ($instructor) {
$searchTerm = 'pi:' . $searchTerm;
$this->getLogger()->info('inbound roster search converted to instructor', [$searchTerm, $roster->getSlug()]);
} else {
$this->getLogger()->info('inbound roster search', [$searchTerm, $roster->getSlug()]);
}
return $this->redirect($this->generateUrl('app_search', ['rosterSlug' => $roster->getSlug(), 'q' => $searchTerm]), '302');
}
/**
* Support a blind search from Students Essentials (Student Administrative Portal)
* Allows publishing static URL for folks to point against.
*
* /search/inbound/?q=<SEARCHTERM>
*
* Redirects to the current default search
*
* As of 2014-09-05, /search/inbound/q?= has been shared with CWD and cannot be changed.
* @Route("/search/inbound/", name="app_search_inbound", methods={"GET"})
*
* @param Request $request
* @return RedirectResponse
*/
public function searchInboundAction(Request $request)
{
// get the current default roster
$roster = $this->getSystemSettings()->getRosterPrintDefault();
$searchTerm = trim($request->query->get('q', ''));
// if search term contains @cornell.edu, strip it - likely searching for an instructor
$searchTerm = str_replace('@cornell.edu', '', $searchTerm);
// if search term is empty, send to browse
if (!$searchTerm) {
$this->getLogger()->info('inbound search empty, redirecting to browse', [$searchTerm, $roster->getSlug()]);
return $this->redirect($this->generateUrl('app_browse_roster', ['rosterSlug' => $roster->getSlug()]), '302');
}
// check if the search term is a subject code, if so, direct to that subject page
$termSetup = $this->getSystemSettings()->getTermSetup($roster);
$subject = $termSetup->getSubjectByInsensitiveSearch($searchTerm);
if ($subject) {
$this->getLogger()->info('inbound search redirecting to subject', [$searchTerm, $roster->getSlug()]);
return $this->redirect($this->generateUrl('app_browse_roster_subject', ['rosterSlug' => $roster->getSlug(), 'subjectCode' => $subject->getSubject()]), '302');
}
// is it a netid?
$repository = $this->getDocumentManager()->getRepository('AppBundle:Instructor');
$instructor = $repository->findOneByNetid($roster->getVersion()->getId(), $searchTerm);
if ($instructor) {
$searchTerm = 'pi:' . $searchTerm;
$this->getLogger()->info('inbound search converted to instructor', [$searchTerm, $roster->getSlug()]);
} else {
$this->getLogger()->info('inbound search', [$searchTerm, $roster->getSlug()]);
}
return $this->redirect($this->generateUrl('app_search', ['rosterSlug' => $roster->getSlug(), 'q' => $searchTerm]), '302');
}
/**
* Search
*
* Uses Symfony gateway cache
*
* @Route("/search/roster/{rosterSlug}", name="app_search", methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @return \Symfony\Component\HttpFoundation\Response
*/
public function searchAction(Request $request, $rosterSlug)
{
$roster = $this->getSystemSettings()->getRosterBySlug($rosterSlug);
$response = $this->getResponseForRoster($roster);
$availRosters = $this->getSystemSettings()->getDropdownRosters();
list($form, $formFields) = $this->getSearchForm($roster);
// support inbound search pi: shortcut to search for a
$simpleSearchQ = $request->query->get('q');
if ($simpleSearchQ && mb_substr($simpleSearchQ, 0, 3) == 'pi:') {
$request->query->set('pi', mb_substr($simpleSearchQ, 3));
$request->query->set('q', '');
}
$form->handleRequest($request);
$searchParams = [];
if ($form->isValid()) {
// data is an array with "name", "email", and "message" keys
$searchParams = $form->getData();
} else {
throw new AccessDeniedHttpException('Unsupported search params');
}
// using searchParams, update open fields
$formFields = $this->updateFormFields($formFields, $searchParams);
return $this->render('Browse/searchResults.html.twig', [
'availRosters' => $availRosters,
'roster' => $roster,
'formFields' => $formFields,
'filterForm' => $form->createView()], $response);
}
/**
* Uses Symfony gateway cache
*
* @Route("/search/ajax/roster/{rosterSlug}", name="app_search_ajax_results", defaults={"rosterSlug": ""}, methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @param array $all
* @return Response
*/
public function ajaxSearchResultsAction(Request $request, $rosterSlug, $all = [])
{
$roster = $this->getSystemSettings()->getRosterBySlug($rosterSlug);
$response = $this->getResponseForRoster($roster);
list($form) = $this->getSearchForm($roster);
foreach ($all AS $key => $value) {
$request->query->set($key, $value);
}
// fix for controller() subcall adding _path post 2.3. upgrade in late July
$request->query->remove('_path');
$form->handleRequest($request);
$searchParams = [];
if ($form->isValid()) {
// data is an array with "name", "email", and "message" keys
$searchParams = $form->getData();
} else {
throw new AccessDeniedHttpException('Unsupported search params');
}
$searchResults = new ArrayCollection();
$foundHash = [];
// STEP 1: new in v3
// first pass search requires:
// 1.) q present and length >= 2 and length <= 13
// 2.) empty course descr search
// 3.) empty SUBJECT search
$stage1SearchUsed = !empty($searchParams['q']) && (strlen($searchParams['q']) >= 2) && (strlen($searchParams['q']) <= 13)
&& empty($searchParams['emptyDescr']) && empty($searchParams['subjects']);
if ($stage1SearchUsed) {
$classResults = $this->getClassRepository()->findBySearchPriority($roster, $searchParams);
foreach ($classResults AS $classResult) {
$foundHash[] = $classResult->getStrm() . '/' . $classResult->getCrseId() . '/' . $classResult->getCrseOfferNbr();
$searchResults[] = $classResult;
}
}
$exploreFilterUsed = !empty($searchParams['explore']);
$exploreCriteriaIds = [];
// do any explore filters used require marks?
if ($exploreFilterUsed) {
$classEnrlGroupExploreCriteriaId = [];
$classSectionExploreCriteriaId = [];
$searchParams['classEnrlGroupExploreCriteriaIds'] = [];
$searchParams['classSectionExploreCriteriaIds'] = [];
// get criteria for each explore tile id
foreach ($searchParams['explore'] as $exploreTileId) {
// get criteria for exploreTileId
$exploreTile = $this->getEntityManager()->find(ExploreTile::class, $exploreTileId);
$exploreCriteria = $exploreTile->getExploreCriteria();
$exploreCriteriaIds[] = $exploreCriteria->getId();
if ($exploreCriteria->hasClassEnrlGroupFilter()) {
$classEnrlGroupExploreCriteriaId[] = $exploreCriteria->getId();
}
if ($exploreCriteria->hasClassSectionFilter()) {
$classSectionExploreCriteriaId[] = $exploreCriteria->getId();
}
}
// "searchParams" here are only used for the MARKS algorithm
if (count($exploreCriteriaIds) === 1) {
$searchParams['classEnrlGroupExploreCriteriaIds'] = $classEnrlGroupExploreCriteriaId;
$searchParams['classSectionExploreCriteriaIds'] = $classSectionExploreCriteriaId;
}
$searchParams['exploreCriteriaIds'] = $exploreCriteriaIds;
unset($searchParams['explore']);
}
// STEP 2: original search using searchParams, update open fields
$classResults = $this->getClassRepository()->findBySearch($roster, $searchParams);
foreach ($classResults AS $classResult) {
if ($stage1SearchUsed) {
$itemHash = $classResult->getStrm() . '/' . $classResult->getCrseId() . '/' . $classResult->getCrseOfferNbr();
if (in_array($itemHash, $foundHash)) {
continue;
}
// only allow 250 results
if (count($foundHash) > 250) {
break;
}
$foundHash[] = $classResult->getStrm() . '/' . $classResult->getCrseId() . '/' . $classResult->getCrseOfferNbr();
}
$searchResults[] = $classResult;
}
$this->getLogger()->debug('Search, result count', [$rosterSlug, $searchResults->count()]);
$classRenderOptions = [
'showAllFields' => false,
'showOnlyMarked' => true,
'showFacility' => $this->getCalculatedShowFacility($roster),
];
return $this->render('Browse/ajaxSearchResults.html.twig', [
'roster' => $roster,
'searchParams' => $searchParams,
'classes' => $searchResults,
'classRenderOptions' => $classRenderOptions
], $response);
}
/**
* JSON Response used to update favorites, sign in/sign out, roster list (validation), dismiss announcements and catalog
*
* No caching, user specific
*
* @Route("/browse/roster/{rosterSlug}/user-data", name="app_browse_user_data", defaults={"rosterSlug": ""}, methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function ajaxUserDataAction(Request $request, FavoriteManager $favoriteManager, $rosterSlug)
{
$jsonResponse = $this->getNoCacheJsonResponse();
$userData = [];
$roster = $this->getSystemSettings()->getRosterBySlug($rosterSlug);
$userData['roster'] = $roster->getSlug();
$userData['time'] = time();
/* @var $user \CU\SASCommonBundle\Security\User */
list($loggedIn, $user) = $this->getUserLoggedInStatus();
if ($loggedIn) {
$userData['login'] = ['status' => true, 'name' => ($user->getNickname()?$user->getNickname():$user->getFirstName()) . ' ' . $user->getLastName(), 'netid' => $user->getNetid()];
} else {
$userData['login'] = ['status' => false, 'name' => '', 'email' => ''];
}
$favorites = [];
if ($loggedIn) {
$favoritesAll = $favoriteManager->getUserFavorites($roster, $this->getUser()->getUserId());
$favorites = array_map(function($favorite) {
return ['favId' => $favorite->getId(), 'classNbr' => $favorite->getClassNbr()];
}, $favoritesAll);
}
$userData['favorites'] = $favorites;
$tokens = [];
$tokens['favorites'] = $this->generateCsrfToken('favorites');
$tokens['announcement'] = $this->generateCsrfToken('announcement');
$userData['tokens'] = $tokens;
// additional rosters (auth required)
$authRosters = [];
if ($loggedIn) {
// Private rosters
$rosterPermits = $this->getParameter('roster_permits');
$intersection = $this->hasUserActiveDirectoryGroupIntersection($rosterPermits['private']);
if ($intersection) {
$authRequiredRosters = $this->getSystemSettings()->getAuthRequiredRosters('Y1');
foreach ($authRequiredRosters AS $authRequiredRoster) {
if ($authRequiredRoster->isArchived()) {
continue;
}
$currentPath = $request->get('currentPath');
$nextPath = str_replace('ROSTER-REPLACE', $authRequiredRoster->getSlug(), $currentPath);
$authRosters[] = ['url' => $nextPath,
'descr' => $authRequiredRoster->getDescr(),
'title' => $authRequiredRoster->getStrm()];
}
}
// Super Private rosters
$intersection = $this->hasUserActiveDirectoryGroupIntersection($rosterPermits['super_private']);
if ($intersection) {
$authRequiredRosters = $this->getSystemSettings()->getAuthRequiredRosters('Y2');
foreach ($authRequiredRosters AS $authRequiredRoster) {
$currentPath = $request->get('currentPath');
$nextPath = str_replace('ROSTER-REPLACE', $authRequiredRoster->getSlug(), $currentPath);
$authRosters[] = ['url' => $nextPath,
'descr' => $authRequiredRoster->getDescr(),
'title' => $authRequiredRoster->getStrm()];
}
}
}
$userData['authRosters'] = $authRosters;
$userDismissedAnnouncements = [];
if ($loggedIn) {
$announcementQuery = '
SELECT a.announce_id
FROM user_announcement ua, announcement a
WHERE ua.announce_id=a.announce_id
AND a.active="Y"
AND a.allow_dismiss="Y"
AND ua.user_id=?
AND ua.roster_id=?';
$announceResults = $this->getConnection()->fetchAll($announcementQuery, [$user->getUserId(), $roster->getId()]);
foreach ($announceResults AS $announceResult) {
$userDismissedAnnouncements[] = $announceResult['announce_id'];
}
}
// identify announcements user should see
$announcements = [];
foreach ($roster->getAnnouncements() AS $announcement) {
if ($announcement->visibileTo('Browse') && $announcement->shouldDisplay() && !in_array($announcement->getId(), $userDismissedAnnouncements)) {
$announcements[] = [
'id' => $announcement->getId(),
'showOnAllPages' => true,
'canDismiss' => $announcement->canDismiss(),
'title' => $announcement->getTitle(),
'body' => $announcement->getBody(),
'alertLevel' => $announcement->getAlertLevel(),
];
}
}
$userData['announcements'] = $announcements;
$alertHtml = '';
if ($loggedIn) {
$alertHtml = '';
foreach ($this->getFlashBag()->all() AS $flashBagType => $flashBagMsgs) {
foreach ($flashBagMsgs AS $flashBagMsg) {
$alertHtml .= '<div class="alert alert-' . $flashBagType . '" role="alert">';
$alertHtml .= $flashBagMsg;
$alertHtml .= '</div>';
}
}
}
$userData['alertHtml'] = $alertHtml;
$settingsUrl = '';
if ($loggedIn && $user->hasPriv('SETTINGS')) {
$settingsUrl = $this->generateUrl('app_settings');
}
$userData['extraMenu'] = ['settingsUrl' => $settingsUrl];
// are we impersonating?
$footerText = '';
if ($loggedIn && $this->getSecurityAuthorizationChecker()->isGranted('ROLE_PREVIOUS_ADMIN')) {
$footerText .= '<p><strong>Impersonating:</strong> ' . $user->getLastname() . ', ' . ($user->getNickname() ? $user->getNickname() : $user->getFirstName()) . ' (' . $user->getUsername() . ')';
$footerText .= ' <a class="footer-link" href="' . $this->generateUrl('app_favs_default', ['_switch_netid' => '_exit']) . '">End Impersonation</a></p>';
}
$userData['footerText'] = $footerText;
$content = json_encode($userData);
$jsonResponse->setContent($content);
return $jsonResponse;
}
/**
* Record a user dismissing an announcement
*
* No cache, user-specific
*
* @Route("/browse/user-dimiss-announcement", name="app_browse_dimiss_announcement", methods={"POST"})
*
* @param Request $request
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function ajaxDismissAnnouncementAction(Request $request)
{
$rosterSlug = $request->request->get('roster');
$announceId = $request->request->get('announceId', '');
$token = $request->request->get('token', '');
$roster = $this->getSystemSettings()->getRosterBySlug($rosterSlug);
$intention = 'announcement';
if ($this->isCsrfTokenValid($intention, $token)) {
// does announcement exist and is it
$announcement = $roster->getAnnouncement($announceId);
if ($announcement && $announcement->shouldDisplay() && $announcement->canDismiss()) {
$this->getConnection()->insert('user_announcement', ['roster_id' => $roster->getId(),
'user_id' => $this->getSecurityTokenStorage()->getToken()->getUser()->getUserId(),
'announce_id' => $announceId,
'dismiss_dttm' => date('Y-m-d H:i:s')
]);
$content = json_encode(['success' => true]);
} else {
$content = json_encode(['success' => false, 'message' => 'Announcement not found or cannot dismiss.']);
}
} else {
$content = json_encode(['success' => false, 'message' => 'Invalid token']);
}
$jsonResponse = $this->getNoCacheJsonResponse();
$jsonResponse->setContent($content);
return $jsonResponse;
}
/**
* Handle select2 dynamic search (instructor)
*
* Uses Symfony gateway cache only
*
* Results like following:
* {"results":[{"id":456,"text":"Plant Science 466"},{"id":678,"text":"Statler Hall B30"}]}
*
* @Route("/search/instructor/{rosterSlug}/{field}", name="app_browse_roster_instructor_search", methods={"GET"})
*
* @param Request $request
* @param string $rosterSlug
* @param string $field
* @return JsonResponse
*/
public function ajaxSelectSearchAction(Request $request, $rosterSlug, $field)
{
$q = $request->query->get('q', '');
// catch the exception so we can fail nicely for JSON.
try {
$roster = $this->getSystemSettings()->getRosterBySlug($rosterSlug);
} catch (\Exception $e) {
// just break the search, so that we can return no results
$q = '';
}
$dm = $this->getDocumentManager();
$response = new JsonResponse();
if ($this->isGatewayCacheEnabled()) {
$response->setPublic();
$response->setMaxAge($this->getGatewayCacheLifetime());
$response->setSharedMaxAge($this->getGatewayCacheLifetime());
}
$results = [];
if (strlen($q) >= 2) {
switch ($field) {
case 'instructor':
/* @var $repository FacilityRepository */
$repository = $dm->getRepository('AppBundle:Instructor');
$instructors = $repository->findByName($roster->getVersion()->getId(), $q);
foreach ($instructors AS $instructor) {
$results[] = array('id' => $instructor->getNetid(), 'text' => $instructor->getName());
}
break;
default:
break;
}
}
$response->setData(['results' => $results]);
return $response;
}
/**
* Course Details appearing in a modal
*
* @Route("/browse/course-details-modal/{rosterSlug}/{subjectCode}/{catalogNbr}", name="app_browse_course_details_modal", defaults={"rosterSlug":"", "subjectCode":"", "catalogNbr":""}, methods={"GET"})
* @param string $rosterSlug
* @param string $subjectCode
* @param string $catalogNbr
* @return \Symfony\Component\HttpFoundation\Response
*/
public function courseDetailsAction($rosterSlug, $subjectCode, $catalogNbr)
{
list($roster) = $this->getSpecifiedAndAvailRosters($rosterSlug);
$class = $this->getClassRepository()->findSingleClass($roster->getVersion()->getId(), $subjectCode, $catalogNbr);
if (!$class) {
throw new GoneHttpException('Class Not Found. Offerings Change Daily');
}
$classes = new ArrayCollection();
$classes->add($class);
$classRenderOptions = [
'collapseSchedule' => false,
'showAllFields' => true,
'showFacility' => $this->getCalculatedShowFacility($roster),
];
$response = $this->getNoCacheResponse();
return $this->render('Browse/courseDetailsModal.html.twig', ['roster' => $roster,
'classes' => $classes,
'class' => $class,
'classRenderOptions' => $classRenderOptions
], $response);
}
protected function updateFormFields($formFields, $searchParams)
{
foreach ($formFields AS $k => $formField) {
$open = !empty($searchParams[$formField['name']]);
$formFields[$k]['open'] = $open;
}
return $formFields;
}
private function getUserLoggedInStatus()
{
/* @var $user \CU\SASCommonBundle\Security\User */
$user = $this->getUser();
return array(
// the result of this condition is essentially '$loggedIn'
$this->getSecurityTokenStorage()->getToken()->isAuthenticated() && is_object($user),
$user,
);
}
/**
* Set up showFacility value for based on roster settings and, if
* necessary, user login status
*/
private function getCalculatedShowFacility(Roster $roster)
{
switch ($roster->getShowFacility()) {
case 'Y':
$showFacility = true;
break;
case 'R':
list($showFacility) = $this->getUserLoggedInStatus();
break;
default:
$showFacility = false;
}
return $showFacility;
}
}