diff --git a/Controller/BlockController.php b/Controller/BlockController.php index de15a0e1..a5496cf2 100644 --- a/Controller/BlockController.php +++ b/Controller/BlockController.php @@ -2,8 +2,13 @@ namespace CanalTP\MttBundle\Controller; +use Symfony\Component\HttpFoundation\Request; use CanalTP\MttBundle\Entity\Block; use CanalTP\MttBundle\Entity\BlockRepository; +use CanalTP\MttBundle\Entity\Timetable; +use CanalTP\MttBundle\Entity\LineTimetable; +use CanalTP\MttBundle\Entity\StopTimetable; +use CanalTP\MttBundle\Form\Type\Block\BlockType; class BlockController extends AbstractController { @@ -12,40 +17,129 @@ class BlockController extends AbstractController * * @param string $externalNetworkId * @param integer $timetableId - * @param string $blockType - * @param string $domId + * @param string $type + * @param integer $rank + */ + public function addAction(Request $request, $externalNetworkId, $timetableId, $type, $rank) + { + $this->isGranted( + array( + 'BUSINESS_MANAGE_LINE_TIMETABLE', + 'BUSINESS_MANAGE_STOP_TIMETABLE' + ) + ); + + $blockManager = $this->get('canal_tp_mtt.block_manager'); + $timetableManager = $this->get(Timetable::$managers[$type]); + $timetable = $timetableManager->find($timetableId); + + $data = array( + 'rank' => $rank, + 'type' => $type, + 'domId' => null + ); + + $block = $blockManager->findOrCreate(-1, $timetable, $data); + + $form = $this->createForm( + new BlockType(), + $block, + array( + 'action' => $this->generateUrl( + 'canal_tp_mtt_block_add', + array( + 'externalNetworkId' => $externalNetworkId, + 'timetableId' => $timetableId, + 'type' => $type, + 'rank' => $rank + ) + ), + 'em' => $this->get('doctrine.orm.entity_manager') + ) + ); + + $form->handleRequest($request); + if ($form->isValid()) { + $blockManager = $this->get('canal_tp_mtt.block_manager'); + $block = $form->getData(); + $blockManager->save($block, $form['number']->getData()); + + if ($timetable instanceof StopTimetable) { + return $this->redirect( + $this->generateUrl( + 'canal_tp_mtt_stop_timetable_edit', + array( + 'externalNetworkId' => $externalNetworkId, + 'seasonId' => $timetable->getLineConfig()->getSeason()->getId(), + 'externalLineId' => $timetable->getLineConfig()->getExternalLineId(), + 'externalRouteId' => $timetable->getExternalRouteId() + ) + ) + ); + } elseif ($timetable instanceof LineTimetable) { + return $this->redirect( + $this->generateUrl( + 'canal_tp_mtt_line_timetable_render', + array( + 'externalNetworkId' => $externalNetworkId, + 'seasonId' => $timetable->getLineConfig()->getSeason()->getId(), + 'externalLineId' => $timetable->getLineConfig()->getExternalLineId(), + 'mode' => 'edit' + ) + ) + ); + } + } + + return $this->render( + 'CanalTPMttBundle:Block:form.html.twig', + array( + 'form' => $form->createView(), + ) + ); + } + + /** + * returns form for a given block type + * or save content of the block using Form factory */ public function editAction( + Request $request, $externalNetworkId, $timetableId, + $type, + $blockId, $blockType, - $domId - ) - { + $domId, + $rank + ) { + $this->isGranted( + array( + 'BUSINESS_MANAGE_LINE_TIMETABLE', + 'BUSINESS_MANAGE_STOP_TIMETABLE' + ) + ); + + $timetableManager = $this->get(Timetable::$managers[$type]); + $timetable = $timetableManager->find($timetableId); + $blockTypeFactory = $this->get('canal_tp_mtt.form.factory.block'); - $stopTimetableManager = $this->get('canal_tp_mtt.stop_timetable_manager'); + $blockManager = $this->get('canal_tp_mtt.block_manager'); + $perimeterManager = $this->get('nmm.perimeter_manager'); $perimeter = $perimeterManager->findOneByExternalNetworkId( $this->getUser()->getCustomer(), $externalNetworkId ); - $stopTimetable = $stopTimetableManager->getStopTimetableById( - $timetableId, - $perimeter->getExternalCoverageId() - ); $data = array( - 'domId' => $domId, - 'type' => $blockType + 'type' => $blockType, + 'rank' => $rank, + 'domId' => $domId ); - $block = $stopTimetable->getBlockByDomId($domId); - - if (empty($block)) { - $block = new Block(); - $block->setStopTimetable($stopTimetable); - } + $block = $blockManager->findOrCreate($blockId, $timetable, $data); $blockTypeFactory->init( $blockType, @@ -57,42 +151,83 @@ public function editAction( $form = $blockTypeFactory->buildForm() ->setAction($this->getRequest()->getRequestUri()) ->setMethod('POST')->getForm(); - $form->handleRequest($this->getRequest()); + + $form->handleRequest($request); + if ($form->isValid()) { - $blockTypeFactory->buildHandler()->process($form->getData(), $stopTimetable); + $blockTypeFactory->buildHandler()->process($form->getData(), $timetable); - return $this->redirect( - $this->generateUrl( + if ($timetable instanceof StopTimetable) { + return $this->redirectToRoute( 'canal_tp_mtt_stop_timetable_edit', array( - 'externalNetworkId' => $externalNetworkId, - 'seasonId' => $stopTimetable->getLineConfig()->getSeason()->getId(), - 'externalLineId' => $stopTimetable->getLineConfig()->getExternalLineId(), - 'externalRouteId' => $stopTimetable->getExternalRouteId() + 'externalNetworkId' => $externalNetworkId, + 'seasonId' => $timetable->getLineConfig()->getSeason()->getId(), + 'externalLineId' => $timetable->getLineConfig()->getExternalLineId(), + 'externalRouteId' => $timetable->getExternalRouteId() ) - ) - ); + ); + } elseif ($timetable instanceof LineTimetable) { + return $this->redirectToRoute( + 'canal_tp_mtt_line_timetable_render', + array( + 'externalNetworkId' => $externalNetworkId, + 'seasonId' => $timetable->getLineConfig()->getSeason()->getId(), + 'externalLineId' => $timetable->getLineConfig()->getExternalLineId(), + 'mode' => 'edit' + ) + ); + } } return $this->render( 'CanalTPMttBundle:Block:get_form.html.twig', array( - 'form' => $form->createView(), + 'form' => $form->createView(), ) ); } - public function deleteAction($timetableId, $blockId, $externalNetworkId) - { + /** + * Deleting a block from a Timetable + * + * @param string $externalNetworkId + * @param integer $timetableId + * @param string $type + * @param integer $blockId + */ + public function deleteAction( + $externalNetworkId, + $timetableId, + $type, + $blockId + ) { + $this->isGranted( + array( + 'BUSINESS_MANAGE_LINE_TIMETABLE', + 'BUSINESS_MANAGE_STOP_TIMETABLE' + ) + ); + $perimeterManager = $this->get('nmm.perimeter_manager'); $perimeter = $perimeterManager->findOneByExternalNetworkId( $this->getUser()->getCustomer(), $externalNetworkId ); - $stopTimetableManager = $this->get('canal_tp_mtt.stop_timetable_manager'); - $repo = $this->getDoctrine()->getRepository('CanalTPMttBundle:Block'); - $block = $repo->find($blockId); + $timetableManager = $this->get(Timetable::$managers[$type]); + $timetable = $timetableManager->find($timetableId); + + if ($timetable->getLineConfig()->getSeason()->getPerimeter() !== $perimeter) { + throw new Exception('This timetable is not accessible via the network: ' . $externalNetworkId); + } + + $block = $timetable->getBlockById($blockId); + + if (empty($block)) { + throw new Exception('The block ' . $blockId . ' is not linked to Timetable ' . $timetableId); + } + if (!$block) { throw $this->createNotFoundException( $this->get('translator')->trans( @@ -101,24 +236,29 @@ public function deleteAction($timetableId, $blockId, $externalNetworkId) ) ); } else { - $this->getDoctrine()->getEntityManager()->remove($block); - $this->getDoctrine()->getEntityManager()->flush(); + $this->get('canal_tp_mtt.block_manager')->delete($block); } - $stopTimetable = $stopTimetableManager->getStopTimetableById( - $timetableId, - $perimeter->getExternalCoverageId() - ); - return $this->redirect( - $this->generateUrl( + if ($timetable instanceof StopTimetable) { + return $this->redirectToRoute( 'canal_tp_mtt_stop_timetable_edit', array( - 'externalNetworkId' => $externalNetworkId, - 'seasonId' => $stopTimetable->getLineConfig()->getSeason()->getId(), - 'externalLineId' => $stopTimetable->getLineConfig()->getExternalLineId(), - 'externalRouteId' => $stopTimetable->getExternalRouteId() + 'externalNetworkId' => $externalNetworkId, + 'seasonId' => $timetable->getLineConfig()->getSeason()->getId(), + 'externalLineId' => $timetable->getLineConfig()->getExternalLineId(), + 'externalRouteId' => $timetable->getExternalRouteId() ) - ) - ); + ); + } elseif ($timetable instanceof LineTimetable) { + return $this->redirectToRoute( + 'canal_tp_mtt_line_timetable_render', + array( + 'externalNetworkId' => $externalNetworkId, + 'seasonId' => $timetable->getLineConfig()->getSeason()->getId(), + 'externalLineId' => $timetable->getLineConfig()->getExternalLineId(), + 'mode' => 'edit' + ) + ); + } } } diff --git a/Controller/LineTimetableController.php b/Controller/LineTimetableController.php index 5eea872c..e8597986 100644 --- a/Controller/LineTimetableController.php +++ b/Controller/LineTimetableController.php @@ -8,6 +8,10 @@ class LineTimetableController extends AbstractController { + const EDIT_MODE = 'edit'; + const VIEW_MODE = 'view'; + const PRINT_MODE = 'print'; + /** * Listing all available lines and displaying a LineTimetable configuration for selected one * If no externalLineId is provided, selecting first line found in Navitia by default @@ -40,12 +44,6 @@ public function listAction($externalNetworkId, $externalLineId = null, $seasonId ); } - $externalLineData = $navitia->getLine( - $perimeter->getExternalCoverageId(), - $externalNetworkId, - $externalLineId - ); - $lineConfig = $this->getDoctrine() ->getRepository('CanalTPMttBundle:LineConfig') ->findOneBy( @@ -60,6 +58,12 @@ public function listAction($externalNetworkId, $externalLineId = null, $seasonId ->findOrCreateLineTimetable($lineConfig); } + $externalLineData = $navitia->getLine( + $perimeter->getExternalCoverageId(), + $externalNetworkId, + $externalLineId + ); + return $this->render( 'CanalTPMttBundle:LineTimetable:list.html.twig', array( @@ -73,6 +77,90 @@ public function listAction($externalNetworkId, $externalLineId = null, $seasonId ); } + /** + * Rendering a layout + * + * @param string $externalNetworkId + * @param string $externalLineId + * @param string $mode + */ + public function renderAction($externalNetworkId, $externalLineId, $seasonId, $mode) + { + $this->isGranted('BUSINESS_MANAGE_LINE_TIMETABLE'); + + $perimeter = $this->get('nmm.perimeter_manager')->findOneByExternalNetworkId( + $this->getUser()->getCustomer(), + $externalNetworkId + ); + + $seasons = $this->get('canal_tp_mtt.season_manager')->findByPerimeter($perimeter); + $currentSeason = $this->get('canal_tp_mtt.season_manager')->getSelected($seasonId, $seasons); + $this->addFlashIfSeasonLocked($currentSeason); + + $lineConfig = $this->getDoctrine() + ->getRepository('CanalTPMttBundle:LineConfig') + ->findOneBy( + array( + 'externalLineId' => $externalLineId, + 'season' => $currentSeason + ) + ); + + if (!empty($lineConfig)) { + $lineTimetable = $this->get('canal_tp_mtt.line_timetable_manager') + ->findOrCreateLineTimetable($lineConfig); + } + + if (empty($lineTimetable)) { + $this->addFlashMessage('danger', 'error.line_timetable.not_found'); + $this->redirect($request->headers->get('referer')); + } + + // Checking the associated Layout has a Template of type LINE_TYPE before rendering it + if (!$lineTimetable->getLineConfig()->getLayoutConfig()->getLayout()->getTemplate(Template::LINE_TYPE)) { + $this->addFlashMessage('danger', 'error.template.not_found', array('%type%' => Template::LINE_TYPE)); + return $this->redirect($request->headers->get('referer')); + } + + $navitia = $this->get('canal_tp_mtt.navitia'); + + $externalCoverageId = $perimeter->getExternalCoverageId(); + + $line = $navitia->getLine( + $externalCoverageId, + $externalNetworkId, + $externalLineId + ); + + $routes = $navitia->getLineRoutes( + $externalCoverageId, + $externalNetworkId, + $externalLineId + ); + + //setlocale(LC_TIME, "fr_FR.UTF8"); => if needed later, set a parameter in container + $layoutId = $lineTimetable->getLineConfig()->getLayoutConfig()->getLayout()->getId(); + $templateFile = $lineTimetable->getLineConfig()->getLayoutConfig()->getLayout()->getTemplate(Template::LINE_TYPE)->getPath(); + + return $this->render( + 'CanalTPMttBundle:Layouts:' . $templateFile, + array( + 'pageTitle' => 'line_timetable.title.' . $mode, + 'timetable' => $lineTimetable, + 'externalNetworkId' => $externalNetworkId, + 'externalLineId' => $externalLineId, + 'currentSeason' => $currentSeason, + 'orientation' => $lineTimetable->getLineConfig()->getLayoutConfig()->getLayout()->getOrientationAsString(), + 'line' => $line, + 'routes' => $routes, + 'mode' => $mode, + 'templatePath' => '@CanalTPMtt/Layouts/uploads/' . $layoutId . '/', + 'imgPath' => 'bundles/canaltpmtt/img/uploads/' . $layoutId . '/', + 'cssPath' => 'bundles/canaltpmtt/css/uploads/' . $layoutId . '/' + ) + ); + } + /** * Selecting stops displayed in the LineTimetable. * @@ -213,4 +301,48 @@ public function showScheduleAction($externalNetworkId, $externalLineId) ) ); } + + /** + * Loading a calendar (Ajax request) + * + * @param string $externalNetworkId + * @param integer $blockId + * @param integer $columnsLimit + * + */ + public function loadCalendarAction($externalNetworkId, $blockId, $columnsLimit) + { + $this->isGranted('BUSINESS_MANAGE_LINE_TIMETABLE'); + + $perimeter = $this->get('nmm.perimeter_manager')->findOneByExternalNetworkId( + $this->getUser()->getCustomer(), + $externalNetworkId + ); + + $block = $this->get('canal_tp_mtt.block_manager')->findBlock($blockId); + $parameters = array(); + + $selectedStopPoints = $block->getLineTimetable()->getSelectedStopPointsByRoute($block->getExternalRouteId()); + + if (!$selectedStopPoints->isEmpty()) { + $parameters['stopPoints'] = $selectedStopPoints; + } + + $schedule = $this->get('canal_tp_mtt.calendar_manager')->getCalendarForBlock( + $perimeter->getExternalCoverageId(), + $block, + $parameters + ); + + $layoutId = $block->getLineTimetable()->getLineConfig()->getLayoutConfig()->getLayout()->getId(); + + return $this->render( + 'CanalTPMttBundle:LineTimetable:blockCalendar.html.twig', + array( + 'templatePath' => '@CanalTPMtt/Layouts/uploads/' . $layoutId . '/', + 'schedule' => $schedule, + 'columnsLimit' => $columnsLimit + ) + ); + } } diff --git a/Controller/StopTimetableController.php b/Controller/StopTimetableController.php index ad19ae4f..77889d9c 100644 --- a/Controller/StopTimetableController.php +++ b/Controller/StopTimetableController.php @@ -91,7 +91,7 @@ private function renderLayout(Request $request, StopTimetable $stopTimetable, $e 'layout' => $stopTimetable->getLineConfig()->getLayoutConfig(), 'editable' => $editable, 'displayMenu' => $displayMenu, - 'templatePath' => '@CanalTPMtt/Layouts/uploads/' . $stopTimetable->getLineConfig()->getLayoutConfig()->getLayout()->getId() . '/', + 'templatePath' => '@CanalTPMtt/Layouts/uploads/' . $layoutId . '/', 'imgPath' => 'bundles/canaltpmtt/img/uploads/' . $layoutId . '/', 'cssPath' => 'bundles/canaltpmtt/css/uploads/' . $layoutId . '/', 'externalStopPointId' => $externalStopPointId diff --git a/Entity/Block.php b/Entity/Block.php index 0f4fb718..408fcc82 100644 --- a/Entity/Block.php +++ b/Entity/Block.php @@ -311,6 +311,24 @@ public function getStopTimetable() return $this->stopTimetable; } + /** + * Set timetable + * + * @param Timetable + */ + public function setTimetable(Timetable $timetable) + { + if ($timetable instanceof LineTimetable) { + $this->lineTimetable = $timetable; + } elseif ($timetable instanceof StopTimetable) { + $this->stopTimetable = $timetable; + } else { + throw new \Exception('Timetable object is not StopTimetable nor LineTimetable instance'); + } + + return $this; + } + /** * Get timetable * diff --git a/Entity/LineTimetable.php b/Entity/LineTimetable.php index f53770ee..4724d7a8 100644 --- a/Entity/LineTimetable.php +++ b/Entity/LineTimetable.php @@ -123,4 +123,17 @@ public function getSelectedStopPointsByRoute($externalRouteId) return $this->selectedStopPoints->matching($criteria); } + + /** + * Has templateOfType + * + * @param $type + * @return boolean + */ + public function hasTemplateOfType($type) + { + $template = $this->lineConfig->getLayoutConfig()->getLayout()->getTemplate($type); + + return ($template !== null); + } } diff --git a/Entity/Timetable.php b/Entity/Timetable.php index 0ab67fa1..81d4497f 100644 --- a/Entity/Timetable.php +++ b/Entity/Timetable.php @@ -94,6 +94,39 @@ public function getBlockByDomId($domId) return $blocks->isEmpty() ? null : $blocks->first(); } + /** + * Get block by id + * + * @param integer $blockId + * @return Block|null + */ + public function getBlockById($blockId) + { + $criteria = Criteria::create() + ->where(Criteria::expr()->eq('id', $blockId)) + ->setMaxResults(1) + ; + + $blocks = $this->blocks->matching($criteria); + + return $blocks->isEmpty() ? null : $blocks->first(); + } + + /** + * Get ranked blocks + * + * @return Collection + */ + public function getRankedBlocks() + { + $criteria = Criteria::create() + ->where(Criteria::expr()->gt('rank', 0)) + ->orderBy(array('rank' => Criteria::ASC)) + ; + + return $this->blocks->matching($criteria); + } + /** * Is locked * diff --git a/Form/DataTransformer/EntityToIntTransformer.php b/Form/DataTransformer/EntityToIntTransformer.php new file mode 100644 index 00000000..8998e165 --- /dev/null +++ b/Form/DataTransformer/EntityToIntTransformer.php @@ -0,0 +1,61 @@ +om = $om; + $this->class = $class; + } + + /** + * @param mixed $entity + * + * @return integer + */ + public function transform($entity) + { + if (null === $entity || !($entity instanceof $this->class)) { + return null; + } + + return $entity->getId(); + } + + /** + * @param mixed $id + * @throws \Symfony\Component\Form\Exception\TransformationFailedException + * @return mixed|object + */ + public function reverseTransform($identifier) + { + if (!$identifier) { + return null; + } + + $entity = $this->om->getRepository($this->class)->find($identifier); + + if ($entity === null) { + throw new TransformationFailedException(sprintf( + '%s with id %s not found', + $this->class, + $identifier + )); + } + + return $entity; + } +} diff --git a/Form/Handler/Block/AbstractHandler.php b/Form/Handler/Block/AbstractHandler.php index 4f491393..69713c6e 100644 --- a/Form/Handler/Block/AbstractHandler.php +++ b/Form/Handler/Block/AbstractHandler.php @@ -18,8 +18,9 @@ protected function saveBlock(Block $formBlock, $timetable) $this->block->setContent($formBlock->getContent()); $this->block->setType($formBlock->getType()); $this->block->setDomId($formBlock->getDomId()); + $this->block->setRank($formBlock->getRank()); } - $this->block->setStopTimetable($timetable); + $this->block->setTimetable($timetable); $this->om->persist($this->block); $this->om->flush(); diff --git a/Form/Handler/Block/ImgHandler.php b/Form/Handler/Block/ImgHandler.php index 77558aab..55653b00 100644 --- a/Form/Handler/Block/ImgHandler.php +++ b/Form/Handler/Block/ImgHandler.php @@ -6,11 +6,10 @@ use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Filesystem\Filesystem; use Doctrine\Common\Persistence\ObjectManager; - use CanalTP\MttBundle\Services\MediaManager; use CanalTP\MediaManagerBundle\Entity\Media; use CanalTP\MttBundle\Entity\Block; -use CanalTP\MttBundle\Form\TYPE\Block\ImgType; +use CanalTP\MttBundle\Form\Type\Block\ImgType; class ImgHandler extends AbstractHandler { @@ -24,8 +23,7 @@ public function __construct( MediaManager $mediaManager, $block, $lastImgPath - ) - { + ) { $this->co = $co; $this->om = $om; $this->mediaManager = $mediaManager; @@ -55,11 +53,11 @@ public function process(Block $formBlock, $timetable) imagepng($output, $file->getRealPath() . '.png'); imagedestroy($output); imagedestroy($input); - $pngFile = new File($file->getRealPath() . '.png'); - $media = $this->mediaManager->saveByStopTimetable($timetable, $pngFile, $this->block->getDomId()); - } else { - $media = $this->mediaManager->saveByStopTimetable($timetable, $file, $this->block->getDomId()); + $file = new File($file->getRealPath() . '.png'); } + + $media = $this->mediaManager->saveByTimetable($timetable, $file, $this->block->getDomId()); + // TODO: saved with domain, we should store without it. Waiting for mediaDataCollector to be updated $formBlock->setContent($this->mediaManager->getUrlByMedia($media) . '?' . time()); $this->saveBlock($formBlock, $timetable); diff --git a/Form/Type/Block/BlockType.php b/Form/Type/Block/BlockType.php new file mode 100644 index 00000000..b0e9b7c6 --- /dev/null +++ b/Form/Type/Block/BlockType.php @@ -0,0 +1,97 @@ +getData()->getTimetable(); + + if (empty($timetable)) { + throw new \Exception('The block should have a timetable object linked to it'); + } + + $entityTransformer = new EntityToIntTransformer( + $options["em"], + get_class($timetable) + ); + + $builder->add( + $builder->create( + 'timetable', + 'hidden' + )->addModelTransformer($entityTransformer) + ); + + $builder + ->add( + 'type', + 'choice', + array( + 'label' => 'block.labels.type', + 'choices' => array( + BlockRepository::CALENDAR_TYPE => 'block.'.BlockRepository::CALENDAR_TYPE.'.labels.content', + BlockRepository::TEXT_TYPE => 'block.'.BlockRepository::TEXT_TYPE.'.labels.content' + ), + 'required' => true + ) + ) + ; + $builder->add( + 'rank', + 'hidden' + ); + $builder->add( + 'number', + 'choice', + array( + 'label' => 'block.labels.number', + 'mapped' => false, + 'choices' => array(1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5) + ) + ); + $builder->add( + 'externalLineId', + 'hidden' + ); + + $builder->addEventSubscriber(new SeasonLockedSubscriber()); + } + + /** + * @param OptionsResolverInterface $resolver + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver + ->setDefaults(array( + 'data_class' => 'CanalTP\MttBundle\Entity\Block' + )) + ->setRequired(array( + 'em', + )) + ->setAllowedTypes(array( + 'em' => 'Doctrine\Common\Persistence\ObjectManager', + )); + } + + /** + * @return string + */ + public function getName() + { + return 'mtt_block'; + } +} diff --git a/Form/Type/Block/CalendarType.php b/Form/Type/Block/CalendarType.php index 9fa37968..48077627 100644 --- a/Form/Type/Block/CalendarType.php +++ b/Form/Type/Block/CalendarType.php @@ -6,8 +6,9 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormError; use Symfony\Component\Validator\Constraints\NotBlank; - use CanalTP\MttBundle\Form\Type\BlockType; +use CanalTP\MttBundle\Entity\LineTimetable; +use CanalTP\MttBundle\Entity\StopTimetable; class CalendarType extends BlockType { @@ -15,38 +16,76 @@ class CalendarType extends BlockType private $externalCoverageId = null; private $blockInstance = null; private $choices = null; + private $navitia = null; + private $routeList = null; - public function __construct($calendarManager, $instance, $externalCoverageId) + public function __construct($calendarManager, $instance, $externalCoverageId, $navitia) { $this->calendarManager = $calendarManager; $this->blockInstance = $instance; $this->externalCoverageId = $externalCoverageId; + $this->navitia = $navitia; } public function buildForm(FormBuilderInterface $builder, array $options) { - $season = $this->blockInstance->getStopTimetable()->getLineConfig()->getSeason(); - $calendars = $this->calendarManager->getCalendarsForRoute( - $this->externalCoverageId, - $this->blockInstance->getStopTimetable()->getExternalRouteId(), - $season->getStartDate(), - $season->getEndDate() - ); + $season = $this->blockInstance->getTimetable()->getLineConfig()->getSeason(); + + if ($this->blockInstance->getTimetable() instanceof StopTimetable) { + $calendars = $this->calendarManager->getCalendarsForRoute( + $this->externalCoverageId, + $this->blockInstance->getStopTimetable()->getExternalRouteId(), + $season->getStartDate(), + $season->getEndDate() + ); + } elseif ($this->blockInstance->getTimetable() instanceof LineTimetable) { + $calendars = $this->navitia->getLineCalendars( + $this->externalCoverageId, + $this->blockInstance->getLineTimetable()->getLineConfig()->getSeason()->getPerimeter()->getExternalNetworkId(), + $this->blockInstance->getExternalLineId() + ); + + $routes = $this->navitia->getLineRoutes( + $this->externalCoverageId, + $this->blockInstance->getLineTimetable()->getLineConfig()->getSeason()->getPerimeter()->getExternalNetworkId(), + $this->blockInstance->getExternalLineId() + ); + + $this->routeList = $this->getChoices($routes); + ; + + $builder + ->add( + 'externalRouteId', + 'choice', + array( + 'choices' => $this->routeList, + 'disabled' => $this->isDisabled($this->routeList), + 'label' => 'block.calendar.labels.route', + 'constraints' => array( + new NotBlank() + ) + ) + ); + } else { + throw new \Exception('Bad Timetable object'); + } + $this->choices = $this->getChoices($calendars); $builder ->add( 'title', 'text', - array('label' => 'block.calendar.labels.title',) + array('label' => 'block.calendar.labels.title', ) ) ->add( 'content', 'choice', array( - 'choices' => $this->choices, - 'disabled' => $this->isDisabled(), - 'label' => 'block.calendar.labels.content', + 'choices' => $this->choices, + 'disabled' => $this->isDisabled(), + 'label' => 'block.calendar.labels.content', 'attr' => array( // attribute to tell javascript to fill automatically title // when a change occurs on this field @@ -66,11 +105,11 @@ private function isDisabled() return (count($this->choices) == 1 && $this->blockInstance->getContent() != null); } - private function getChoices($calendars) + private function getChoices($list) { $choices = array(); - foreach ($calendars as $calendar) { - $choices[$calendar->id] = $calendar->name; + foreach ($list as $item) { + $choices[$item->id] = $item->name; } return $choices; diff --git a/Form/Type/BlockType.php b/Form/Type/BlockType.php index f046a9ec..f6473fb1 100644 --- a/Form/Type/BlockType.php +++ b/Form/Type/BlockType.php @@ -11,8 +11,9 @@ class BlockType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('domId', 'hidden', array('data' => $options['data']['domId'])); + $builder->add('rank', 'hidden', array('data' => $options['data']['rank'])); $builder->add('type', 'hidden', array('data' => $options['data']['type'])); + $builder->add('domId', 'hidden', array('data' => $options['data']['domId'])); $builder->addEventSubscriber(new SeasonLockedSubscriber()); } diff --git a/MediaManager/Category/CategoryType.php b/MediaManager/Category/CategoryType.php index 096c7c01..338cedd4 100644 --- a/MediaManager/Category/CategoryType.php +++ b/MediaManager/Category/CategoryType.php @@ -7,5 +7,6 @@ class CategoryType const NETWORK = 'network'; const ROUTE = 'route'; const SEASON = 'season'; + const LINE = 'line'; const STOP_POINT = 'stop_point'; } diff --git a/MediaManager/Category/Factory/CategoryFactory.php b/MediaManager/Category/Factory/CategoryFactory.php index 151e04aa..2d4384c5 100644 --- a/MediaManager/Category/Factory/CategoryFactory.php +++ b/MediaManager/Category/Factory/CategoryFactory.php @@ -6,6 +6,7 @@ use CanalTP\MttBundle\MediaManager\Category\CategoryType; use CanalTP\MttBundle\MediaManager\Category\NetworkCategory; use CanalTP\MttBundle\MediaManager\Category\RouteCategory; +use CanalTP\MttBundle\MediaManager\Category\LineCategory; use CanalTP\MttBundle\MediaManager\Category\StopPointCategory; use CanalTP\MttBundle\MediaManager\Category\SeasonCategory; @@ -25,6 +26,9 @@ private function product($type) case CategoryType::STOP_POINT: $category = new StopPointCategory(); break; + case CategoryType::LINE: + $category = new LineCategory(); + break; case CategoryType::SEASON: $category = new SeasonCategory(); break; diff --git a/MediaManager/Category/LineCategory.php b/MediaManager/Category/LineCategory.php new file mode 100644 index 00000000..99c4a377 --- /dev/null +++ b/MediaManager/Category/LineCategory.php @@ -0,0 +1,17 @@ +id = 'line'; + $this->name = 'Line'; + $this->ressourceId = 'lines'; + } +} diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index c4602b9b..1082f52f 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -98,6 +98,15 @@ canal_tp_mtt_line_timetable_list: externalLineId: -?[a-zA-Z0-9_\-:]* seasonId: -?\d+ +canal_tp_mtt_line_timetable_render: + pattern: /line-timetable/render/networks/{externalNetworkId}/line/{externalLineId}/seasons/{seasonId}/mode/{mode} + defaults: { _controller: CanalTPMttBundle:LineTimetable:render } + requirements: + externalNetworkId: -?[a-zA-Z0-9_\-:]* + externalLineId: -?[a-zA-Z0-9_\-:]* + seasonId: \d+ + mode: edit + canal_tp_mtt_line_timetable_select_stops: pattern: /line-timetable/{lineTimetableId}/networks/{externalNetworkId}/season/{seasonId}/select-stops/route/{externalRouteId} defaults: { _controller: CanalTPMttBundle:LineTimetable:selectStops, externalRouteId: null } @@ -116,6 +125,16 @@ canal_tp_mtt_line_timetable_show_schedule: externalNetworkId: -?[a-zA-Z0-9_\-:]* externalLineId: -?[a-zA-Z0-9_\-:]* +canal_tp_mtt_line_timetable_load_calendar: + pattern: /line-timetable/networks/{externalNetworkId}/calendar/load/{blockId}/columns/{columnsLimit} + defaults: { _controller: CanalTPMttBundle:LineTimetable:loadCalendar } + requirements: + externalNetworkId: -?[a-zA-Z0-9_\-:]* + blockId: \d+ + columnsLimit: \d+ + options: + expose: true + #StopTimetable canal_tp_mtt_stop_timetable_edit: pattern: /stopTimetable/edit/networks/{externalNetworkId}/line/{externalLineId}/route/{externalRouteId}/seasons/{seasonId} @@ -153,24 +172,42 @@ canal_tp_mtt_stop_timetable_download_pdf: externalStopPointId: -?[a-zA-Z0-9_\-:]* #Block +canal_tp_mtt_block_add: + pattern: /block/edit/networks/{externalNetworkId}/timetable/{timetableId}/type/{type}/rank/{rank} + defaults: { _controller: CanalTPMttBundle:Block:add } + requirements: + externalNetworkId: -?[a-zA-Z0-9_\-:]* + type: line|stop + timetableId: \d+ + rank: \d+ + options: + expose: true + canal_tp_mtt_block_edit: - pattern: /block/edit/networks/{externalNetworkId}/timetable/{timetableId}/blockType/{blockType}/domId/{domId} - defaults: { _controller: CanalTPMttBundle:Block:edit } + # stop point must be the last param so it can be empty + pattern: /block/edit/networks/{externalNetworkId}/timetable/{timetableId}/type/{type}/block/{blockId}/type/{blockType}/dom/{domId}/rank/{rank} + defaults: { _controller: CanalTPMttBundle:Block:edit, blockId: -1, rank: 0, domId: 0 } requirements: externalNetworkId: -?[a-zA-Z0-9_\-:]* timetableId: \d+ + type: line|stop + blockId: -?\d+ blockType: calendar|text|img domId: -?[a-zA-Z0-9_\-:]* + rank: -?\d+ options: expose: true canal_tp_mtt_block_delete: - pattern: /block/delete/networks/{externalNetworkId}/timetable/{timetableId}/block/{blockId} + pattern: /block/delete/networks/{externalNetworkId}/timetable/{timetableId}/type/{type}/block/{blockId} defaults: { _controller: CanalTPMttBundle:Block:delete } requirements: externalNetworkId: -?[a-zA-Z0-9_\-:]* timetableId: \d+ + type: line|stop blockId: \d+ + options: + expose: true #Calendar canal_tp_mtt_calendar_view: diff --git a/Resources/config/services.yml b/Resources/config/services.yml index fa4dd135..aa52dfac 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -141,7 +141,7 @@ services: arguments: ['@doctrine.orm.entity_manager', '@canal_tp_mtt.navitia', '@jms_serializer'] canal_tp_mtt.form.factory.block: class: %canal_tp_mtt.form.factory.class% - arguments: ['@service_container', '@doctrine.orm.entity_manager', '@form.factory', '@canal_tp_mtt.media_manager'] + arguments: ['@service_container', '@doctrine.orm.entity_manager', '@form.factory', '@canal_tp_mtt.media_manager', '@canal_tp_mtt.navitia'] canal_tp_mtt.calendar_manager: class: %canal_tp_mtt.calendar_manager.class% arguments: ['@canal_tp_mtt.navitia', '@translator'] diff --git a/Resources/public/css/line.css b/Resources/public/css/line.css new file mode 100644 index 00000000..9f221763 --- /dev/null +++ b/Resources/public/css/line.css @@ -0,0 +1,169 @@ +/* general style */ +#layout-1-wrapper { + padding:8px; +} +#layout-main-wrapper { + height:auto; + width:1000px; +}; +.loading-indicator { + position:absolute; + right:50%; + top:40%; +} +/* header style */ +#header { + min-height:60px; + border:2px solid grey; + font-size:30px; + padding:10px; +} +#header-wrapper { + margin-top:15px; +} +#map-block { + width:80%; + text-align:center; + float:left; + padding-left:15px; + margin-bottom:30px; +} +#map-block:before { + content:""; + display:inline-block; + height:100%; + vertical-align:middle; +} +.map-img { + max-height:95%; + max-width:95%; + vertical-align:middle; +} +#map-block, #left-blocks { + height:150px; +} +#left-blocks { + font-size:12px; + padding-right:14px; + width:20%; + border-right:1px solid black; +} +#left-blocks .small-block .title { + font-weight:bold; +} +#line-map-block, #left-blocks { + float:left; + padding-left:15px; +} +/* block style */ +.small-block { + border:1px solid white; + border-bottom-color:black; + margin:5px 0; + height:70px; +} +.small-block:last-child { + border-bottom:none; +} +.editable .block:hover { + border:1px dashed black; +} +.editable .block.empty { + background-color:#eee; +} +/* main-content style */ +#main-content-wrapper { + margin-top:5px; + width:100%; +} +#main-content-wrapper .timegrid-container { + font-size:12px; +} +#main-content-wrapper .block .title, +#main-content-wrapper .timegrid-header { + background-color:white; + border-bottom:2px solid black; + color:black; + font-weight:bold; + font-size:14px; + line-height:25px; + padding:0px 5px; + height:25px; +} +#main-content-wrapper .timegrid-body { + background-color:white; + min-height:150px; + width:100%; +} +#main-content-wrapper .block { + margin-top:5px; + min-height:50px; + line-height:20px; + padding:2px; + position:relative; +} +/* line-timetable style */ +.line-timetable-calendar { + width:100%; +} +.line-timetable-calendar tr { + height:14px; + vertical-align:middle; + font-size:10px; +} +.line-timetable-calendar tr.pattern { + visibility:hidden; +} +.line-timetable-calendar tr.separator { + height:3px; + background-color:#343434; +} +.line-timetable-calendar tr.schedule td { + text-align:center; +} +.line-timetable-calendar tr.schedule td.first-hour { + font-weight:bold; +} +.line-timetable-calendar tr.schedule td.has-note { + background-color:#444; +} +.line-timetable-calendar tr.schedule:nth-child(odd) { + background-color:#eee; +} +.line-timetable-calendar tr.schedule:nth-child(even) { + background-color:inherit; +} +.line-timetable-calendar tr .empty-calendar { + font-weight:bold; + font-size:12px; +} +/* add block style */ +.add-block { + height:25px; + border:1px solid white; + width:100%; + margin:0 auto; + position:relative; +} +.add-block:hover { + cursor:pointer; + border:1px dashed black; +} +.add-block .glyphicon { + position:absolute; + right:5px; + top:5px; +} +/* action bar style */ +.action-bar { + position:absolute; + display:block; + right:-4%; + z-index:1; + font-size:13px; +} +.action-bar ul { + list-style:none; + margin:0; + padding:5px; +} diff --git a/Resources/public/img/ajax-loader.gif b/Resources/public/img/ajax-loader.gif new file mode 100644 index 00000000..9f74292d Binary files /dev/null and b/Resources/public/img/ajax-loader.gif differ diff --git a/Resources/public/img/default-line-layout.png b/Resources/public/img/default-line-layout.png new file mode 100644 index 00000000..0123d50b Binary files /dev/null and b/Resources/public/img/default-line-layout.png differ diff --git a/Resources/public/js/LineTimetable/edit.js b/Resources/public/js/LineTimetable/edit.js new file mode 100644 index 00000000..49814ede --- /dev/null +++ b/Resources/public/js/LineTimetable/edit.js @@ -0,0 +1,108 @@ +define(['jquery'], function($) { + + var layout = {}; + var global_params = {}; + var $icon_tpl = $(''); + + layout.init = function($wrapper, lineTimetableId, externalNetworkId) + { + // store global parameters + global_params.externalNetworkId = externalNetworkId; + global_params.timetableId = lineTimetableId; + global_params.type = 'line'; + + // needed properties + $(document).ready(function() { + $('.action-button').tooltip({ + placement: 'right', + animation: true, + delay: { "show": 500, "hide": 100 } + }); + $('button.edit-note').button({ + icons: { + primary: "ui-icon-gear" + } + }); + }); + + _bind_listeners(); + _bind_add_block_listener(); + _bind_blocks_listeners(); + }; + + var _bind_listeners = function() + { + $('#base-modal').on('loaded.bs.modal', function () { + var $field = $(this).find('*[data-fill-title]'); + if ($field.length == 1 && $field.data('fill-title') === 1) + { + var $titleField = $field.parents('form').find("input[name*='[title]']"); + if ($titleField.length == 1 && $titleField.val() === '') + { + $field.change(function(){ + $titleField.val($field.find(':selected').text()); + }); + // set default value + $field.change(); + // unbind if user types sthg in this title field + $titleField.keypress(function(){ + $field.unbind('change'); + }); + } + } + }); + }; + + + var _bind_blocks_listeners = function() + { + var $blocks = _get_blocks(); + $blocks.each(function() { + var $block = $(this); + // bind click listener if there is no menu inside + if ($block.find('*[role=menu]').length === 0) { + $block.click(_get_remote_modal); + } else { + $block.find('*[role=menu]').siblings('button.btn').dropdown(); + $block.click(function(event) { + if ($block.find('*[role=menu]').parents('.btn-group').hasClass('open') === false) { + $block.find('*[role=menu]').siblings('button.btn').click(); + return false; + } + }); + } + }); + }; + + var _get_remote_modal = function() + { + var params = { + 'rank' : $(this).data('rank'), + 'domId' : $(this).id('domId'), + 'blockType' : $(this).data('type'), + 'blockId' : $(this).data('id') + }; + $.extend(params, global_params); + var url = Routing.generate( + 'canal_tp_mtt_block_edit', + params + ); + $('#base-modal').modal({remote: url}); + }; + + var _get_blocks = function() + { + var $blocks = $('.block').each(function() { + var icon_class = 'glyphicon-'+$(this).data('icon'); + var data = $(this).data(); + $(this).prepend($icon_tpl.clone().addClass(icon_class)); + }); + + // return editable blocks only + return $blocks.filter(function() { + return $(this).data("block-level") == layout.blockLevel; + }); + }; + + return layout; +}); diff --git a/Resources/public/js/layout.js b/Resources/public/js/layout.js index a7998a52..2012cc1b 100644 --- a/Resources/public/js/layout.js +++ b/Resources/public/js/layout.js @@ -1,17 +1,34 @@ define(['jquery'], function($) { var layout = {}; - var url_params = {}; + var global_params = {}; var $icon_tpl = $(''); - layout.init = function($wrapper, stopTimetableId, externalNetworkId) + layout.init = function($wrapper, globalParameters) { - // store url params for later - url_params.externalNetworkId = externalNetworkId; - url_params.timetableId = stopTimetableId; + global_params = globalParameters; _bind_listeners(); + _bind_add_block_listener(); _bind_blocks_listeners(); + _bind_action_bar(); + }; + + var _bind_add_block_listener = function() + { + $(document).on("click", ".add-block", function(event) { + event.preventDefault(); + var params = { + 'rank': $(this).data('rank') + }; + $.extend(params, global_params); + var url = Routing.generate( + 'canal_tp_mtt_block_add', + params + ); + + $('#base-modal').modal({remote: url}); + }); }; var _bind_blocks_listeners = function() @@ -37,10 +54,12 @@ define(['jquery'], function($) { var _get_remote_modal = function() { var params = { - 'domId' : $(this).attr('id'), - 'blockType': $(this).data('type') + 'rank' : $(this).data('rank'), + 'domId' : $(this).attr('id') === undefined ? '' : $(this).attr('id'), + 'blockType' : $(this).data('type'), + 'blockId' : $(this).data('id') }; - $.extend(params, url_params); + $.extend(params, global_params); var url = Routing.generate( 'canal_tp_mtt_block_edit', params @@ -81,5 +100,75 @@ define(['jquery'], function($) { }); }; + var _bind_action_bar = function() + { + $('.action-bar .action-button').each(function() { + var action = $(this); + if (action.data('action-type') !== undefined) { + switch (action.data('action-type')) { + case 'load-calendar': + _action_data_load(action); + break; + case 'delete-calendar': + _action_delete_calendar(action); + break; + default: + // do nothing + } + } + }); + }; + + var _action_data_load = function(action) { + action.on('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + var $container = $(this).parents('.' + $(this).data('action-container')); + var $target = $container.find('.' + $(this).data('action-target')); + + var params = { + 'blockId': $(this).data('id'), + 'columnsLimit': $(this).closest('.line').data('columns') + }; + $.extend(params, {'externalNetworkId': global_params.externalNetworkId}); + $loader = $('.loading-indicator').clone(); + $target.hide().parent().append($loader.show()); + $.ajax({ + type: "POST", + url: Routing.generate('canal_tp_mtt_line_timetable_load_calendar', params), + cache: false, + }).done(function(data) { + $loader.detach(); + $target.html(data.content).show(1000); + }); + + return false; + }); + }; + + var _action_delete_calendar = function(action) { + action.on('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + + var params = { + 'blockId': $(this).data('id') + }; + + $.extend(params, global_params); + + $.ajax({ + type: "POST", + url: Routing.generate('canal_tp_mtt_block_delete', params), + success: function(data) { + window.location = data.location; + }, + error: function(data) { + console.log(data.content); + } + }); + }); + }; + return layout; }); diff --git a/Resources/public/js/layout/validator.js b/Resources/public/js/layout/validator.js index 2d5adcd0..19bd423c 100644 --- a/Resources/public/js/layout/validator.js +++ b/Resources/public/js/layout/validator.js @@ -14,7 +14,7 @@ define(['jquery', 'utils', 'translations/default'], function($, utils) { var msg_added = false; // check if content is bigger than block wrapper $wrapper.find('*[data-validate-size="1"]').each(function(){ - if ($(this).parents('.frequency-content').length != 0 && + if ($(this).parents('.frequency-content').length !== 0 && ($(this).find('span').height() > $(this).height() || $(this).find('span').width() > $(this).width()) ) { $(this).addClass('error'); @@ -42,4 +42,4 @@ define(['jquery', 'utils', 'translations/default'], function($, utils) { }; return validator; -}) +}); diff --git a/Resources/translations/default.fr.yml b/Resources/translations/default.fr.yml index 64201a94..e16ac076 100644 --- a/Resources/translations/default.fr.yml +++ b/Resources/translations/default.fr.yml @@ -39,11 +39,14 @@ line_timetable: title: select_stops: Sélection des points d'arrêt schedule: Horaires de la ligne + edit: Edition + view: Prévisualisation action: select_stops: Choisir les arrêts select_all_stops: Ajouter tous les arrêts remove_all_stops: Vider la sélection des arrêts show_schedule: Voir les horaires + edit: Editer la fiche horaire label: way: vers available_stops: Arrêts disponibles diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 9d25833a..0484db5c 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -58,6 +58,8 @@ error: help: edit_block: Cliquez pour éditer ce bloc drop_here: Déposez les éléments ici + add_block: Ajouter un ou plusieurs blocs + load_calendar: Charger les données du calendrier calendar: start: Heure de début des calendriers @@ -143,6 +145,11 @@ layout_model: archive_path: Chemin de l'archive block: + title: + add: Ajouter un bloc + labels: + type: Type de bloc + number: Nombre d'ajouts img: labels: title: Titre @@ -155,6 +162,7 @@ block: labels: title: Titre content: Calendrier + route: Direction customer: layout_list: Liste des modèles diff --git a/Resources/views/Block/form.html.twig b/Resources/views/Block/form.html.twig new file mode 100644 index 00000000..b56c8010 --- /dev/null +++ b/Resources/views/Block/form.html.twig @@ -0,0 +1,27 @@ +{% extends "::modal.html.twig" %} + +{% form_theme form 'CanalTPMttBundle:Form:fields.html.twig' %} + +{% block open_form %} + {{ form_start(form) }} +{% endblock %} + +{% block modal_title %} + {{ 'block.title.add'|trans({}, 'default') }} +{% endblock %} + +{% block modal_body %} + {{ form_errors(form) }} + {{ form_row(form.type) }} + {{ form_row(form.number) }} + {{ form_row(form.externalLineId) }} + {{ form_row(form.rank) }} + {{ form_row(form.timetable) }} +{% endblock %} + +{% block modal_actions %} + + {{ form_end(form) }} +{% endblock %} diff --git a/Resources/views/Block/get_form.html.twig b/Resources/views/Block/get_form.html.twig index 2cc8ffb2..93e0f59d 100644 --- a/Resources/views/Block/get_form.html.twig +++ b/Resources/views/Block/get_form.html.twig @@ -17,6 +17,9 @@ {{ form_row(form.title) }} {{ form_row(form.content) }} + {% if form.externalRouteId %} + {{ form_row(form.externalRouteId) }} + {% endif %} {% endblock %} {% block modal_actions %} diff --git a/Resources/views/Layouts/base_layout.html.twig b/Resources/views/Layouts/base_layout.html.twig index 62dca683..0c344a09 100644 --- a/Resources/views/Layouts/base_layout.html.twig +++ b/Resources/views/Layouts/base_layout.html.twig @@ -19,7 +19,7 @@ ) }}"> {{'menu.edit_timetables'|trans({}, 'messages')}} -
  • {{ pageTitle|trans({}, 'default') }}
  • +
  • {{ pageTitle|trans }}
  • {% endif %} {% endblock %} @@ -116,8 +116,8 @@ %} {% endif %} -
    - {% block stop_timetable_content %}{% endblock %} +
    + {% block timetable_content %}{% endblock %}
    {% block scripts %}{% endblock %} diff --git a/Resources/views/Layouts/common.html.twig b/Resources/views/Layouts/common.html.twig index 4366a249..fc64baef 100644 --- a/Resources/views/Layouts/common.html.twig +++ b/Resources/views/Layouts/common.html.twig @@ -1,10 +1,15 @@ -{% if editable == true %} +{% if editable == true or mode == 'edit' %}