From c99583b6651b437fd53c217a8cf3b90399eca336 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 25 Apr 2018 15:35:52 +0200 Subject: [PATCH] implementing workflow on tasks --- ChillTaskBundle.php | 9 +- Controller/TaskController.php | 26 ++++- DependencyInjection/ChillTaskExtension.php | 5 +- .../TaskWorkflowDefinitionCompilerPass.php | 46 ++++++++ Resources/config/services/templating.yml | 6 ++ Resources/config/services/workflow.yml | 6 +- Resources/views/Task/index.html.twig | 6 +- Templating/TaskTwigExtension.php | 59 ++++++++++ Tests/Controller/SingleTaskControllerTest.php | 102 ++++++++++++++++++ Tests/Controller/TaskControllerTest.php | 9 ++ Workflow/Definition/DefaultTaskDefinition.php | 92 ++++++++++++++++ Workflow/TaskWorkflowDefinition.php | 27 +++++ Workflow/TaskWorkflowManager.php | 48 ++++++++- 13 files changed, 429 insertions(+), 12 deletions(-) create mode 100644 DependencyInjection/Compiler/TaskWorkflowDefinitionCompilerPass.php create mode 100644 Resources/config/services/templating.yml create mode 100644 Templating/TaskTwigExtension.php create mode 100644 Tests/Controller/SingleTaskControllerTest.php create mode 100644 Tests/Controller/TaskControllerTest.php create mode 100644 Workflow/Definition/DefaultTaskDefinition.php create mode 100644 Workflow/TaskWorkflowDefinition.php diff --git a/ChillTaskBundle.php b/ChillTaskBundle.php index 6155882cd..acd8b8607 100644 --- a/ChillTaskBundle.php +++ b/ChillTaskBundle.php @@ -18,6 +18,8 @@ namespace Chill\TaskBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Chill\TaskBundle\DependencyInjection\Compiler\TaskWorkflowDefinitionCompilerPass; /** * @@ -25,5 +27,10 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; */ class ChillTaskBundle extends Bundle { - + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new TaskWorkflowDefinitionCompilerPass()); + } } diff --git a/Controller/TaskController.php b/Controller/TaskController.php index 525abab35..94e356941 100644 --- a/Controller/TaskController.php +++ b/Controller/TaskController.php @@ -9,12 +9,30 @@ use Symfony\Component\Workflow\Registry; use Symfony\Component\HttpFoundation\Response; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; class TaskController extends Controller { + /** + * Apply a transition to a task + * + * @Route( + * "/{_locale}/task/transition/{kind}/{taskId}/{transition}", + * name="chill_task_task_transition" + * ) + * + * @param string $kind + * @param int $taskId + * @param string $transition + * @param SingleTaskRepository $singleTaskRepository + * @param Registry $registry + * @param EntityManagerInterface $em + * @param Request $request + * @return Response + */ public function applyTransitionAction( - $type, + $kind, $taskId, $transition, SingleTaskRepository $singleTaskRepository, @@ -22,7 +40,7 @@ class TaskController extends Controller EntityManagerInterface $em, Request $request ) { - switch ($type) { + switch ($kind) { case 'single-task': $task = $singleTaskRepository ->find($taskId) @@ -33,7 +51,7 @@ class TaskController extends Controller ); break; default: - return new Response("The type '$type' is not implemented", + return new Response("The type '$kind' is not implemented", Response::HTTP_BAD_REQUEST); } @@ -44,7 +62,7 @@ class TaskController extends Controller // we simply check that the user can see the task. Other ACL checks // should be performed using `guard` events. - $this->denyAccessUnlessGranted($task, TaskVoter::SHOW); + $this->denyAccessUnlessGranted(TaskVoter::SHOW, $task); $workflow = $registry->get($task); diff --git a/DependencyInjection/ChillTaskExtension.php b/DependencyInjection/ChillTaskExtension.php index 519391f88..31dcd3d4a 100644 --- a/DependencyInjection/ChillTaskExtension.php +++ b/DependencyInjection/ChillTaskExtension.php @@ -30,6 +30,7 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface $loader->load('services/security.yml'); $loader->load('services/repositories.yml'); $loader->load('services/workflow.yml'); + $loader->load('services/templating.yml'); } public function prepend(ContainerBuilder $container) @@ -72,7 +73,7 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface 'currentStates' ], ], - 'type' => 'workflow', + 'type' => 'state_machine', 'support_strategy' => TaskWorkflowManager::class, 'places' => [ 'new', 'in_progress', 'closed', 'canceled'], 'initial_place' => 'new', @@ -82,7 +83,7 @@ class ChillTaskExtension extends Extension implements PrependExtensionInterface 'to' => 'in_progress' ], 'close' => [ - 'from' => 'in_progress', + 'from' => ['new', 'in_progress'], 'to' => 'closed' ], 'cancel' => [ diff --git a/DependencyInjection/Compiler/TaskWorkflowDefinitionCompilerPass.php b/DependencyInjection/Compiler/TaskWorkflowDefinitionCompilerPass.php new file mode 100644 index 000000000..d52e5484d --- /dev/null +++ b/DependencyInjection/Compiler/TaskWorkflowDefinitionCompilerPass.php @@ -0,0 +1,46 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\TaskBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Chill\TaskBundle\Workflow\TaskWorkflowManager; +use Symfony\Component\DependencyInjection\Reference; + +/** + * + * + * @author Julien Fastré + */ +class TaskWorkflowDefinitionCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition(TaskWorkflowManager::class)) { + throw new \LogicException("The service ".TaskWorkflowManager::class." is " + . "not registered"); + } + + $workflowManagerDefinition = $container->getDefinition(TaskWorkflowManager::class); + + foreach ($container->findTaggedServiceIds('chill_task.workflow_definition') as $id => $tags) { + $workflowManagerDefinition + ->addMethodCall('addDefinition', [new Reference($id)]); + } + } +} diff --git a/Resources/config/services/templating.yml b/Resources/config/services/templating.yml new file mode 100644 index 000000000..dca4e36f6 --- /dev/null +++ b/Resources/config/services/templating.yml @@ -0,0 +1,6 @@ +services: + Chill\TaskBundle\Templating\TaskTwigExtension: + arguments: + $taskWorkflowManager: '@Chill\TaskBundle\Workflow\TaskWorkflowManager' + tags: + - { name: 'twig.extension' } diff --git a/Resources/config/services/workflow.yml b/Resources/config/services/workflow.yml index acf583d43..01a26f1fc 100644 --- a/Resources/config/services/workflow.yml +++ b/Resources/config/services/workflow.yml @@ -1,2 +1,6 @@ services: - Chill\TaskBundle\Workflow\TaskWorkflowManager: ~ \ No newline at end of file + Chill\TaskBundle\Workflow\TaskWorkflowManager: ~ + + Chill\TaskBundle\Workflow\Definition\DefaultTaskDefinition: + tags: + - { name: 'chill_task.workflow_definition' } \ No newline at end of file diff --git a/Resources/views/Task/index.html.twig b/Resources/views/Task/index.html.twig index c28665a12..6e745b9c5 100644 --- a/Resources/views/Task/index.html.twig +++ b/Resources/views/Task/index.html.twig @@ -48,7 +48,11 @@ {{ task.title }} {{ task.type }} - todo + + {% for transition in workflow_transitions(task) %} + {{ task_workflow_metadata(task, 'transition.verb', transition) }} + {% endfor %} + {% if task.startDate is not null %}{{ task.startDate|localizeddate('medium', 'none') }}{% endif %} {% if task.warningDate is not null %}{{ task.warningDate|localizeddate('medium', 'none') }}{% endif %} {% if task.endDate is not null %}{{ task.endDate|localizeddate('medium', 'none') }}{% endif %} diff --git a/Templating/TaskTwigExtension.php b/Templating/TaskTwigExtension.php new file mode 100644 index 000000000..fd799dd34 --- /dev/null +++ b/Templating/TaskTwigExtension.php @@ -0,0 +1,59 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\TaskBundle\Templating; + +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; +use Chill\TaskBundle\Entity\AbstractTask; +use Chill\TaskBundle\Workflow\TaskWorkflowManager; + +/** + * + * + * @author Julien Fastré + */ +class TaskTwigExtension extends \Twig_Extension +{ + /** + * + * @var TaskWorkflowManager + */ + protected $taskWorkflowManager; + + public function __construct(TaskWorkflowManager $taskWorkflowManager) + { + $this->taskWorkflowManager = $taskWorkflowManager; + } + + + public function getFunctions() + { + return [ + new TwigFunction('task_workflow_metadata', [ $this, 'getWorkflowMetadata' ] ) + ]; + } + + public function getWorkflowMetadata( + AbstractTask $task, + string $key, + $metadataSubject = null, + string $name = null + ) { + return $this->taskWorkflowManager->getWorkflowMetadata($task, $key, $metadataSubject, $name); + } +} diff --git a/Tests/Controller/SingleTaskControllerTest.php b/Tests/Controller/SingleTaskControllerTest.php new file mode 100644 index 000000000..d7337029d --- /dev/null +++ b/Tests/Controller/SingleTaskControllerTest.php @@ -0,0 +1,102 @@ +faker = Faker\Factory::create('fr'); + } + + /** + * + * @return \Chill\PersonBundle\Entity\Person + */ + protected function getRandomPerson($centerName) + { + $em = self::$kernel + ->getContainer() + ->get('doctrine.orm.entity_manager') + ; + + $centers = $em + ->getRepository(Center::class) + ->findAll(); + + $center = \array_filter( + $centers, + function(Center $c) use ($centerName) { + return $centerName === $c->getName(); + })[0]; + + $ids = $em + ->createQuery('SELECT p.id FROM ChillPersonBundle:Person p ' + . 'WHERE p.center = :center' + ) + ->setParameter('center', $center) + ->getResult() + ; + + $id = $ids[\array_rand($ids)]; + + return self::$kernel + ->getContainer() + ->get('doctrine.orm.entity_manager') + ->getRepository(\Chill\PersonBundle\Entity\Person::class) + ->find($id) + ; + } + + public function testNew() + { + $client = static::createClient( + array(), + TestHelper::getAuthenticatedClientOptions() + ); + $person = $this->getRandomPerson('Center A'); + + $crawler = $client->request('GET', '/fr/task/single-task/new', [ + 'person_id' => $person->getId() + ]); + var_dump($crawler->text()); + + $this->assertTrue($client->getResponse()->isSuccessful()); + + + + $form = $crawler->selectButton('Envoi')->form(); + + $title = $this->faker->sentence; + $circles = $form->get('circle') + ->availableOptionsValues() + ; + + $client->submit($form, [ + 'title' => $title, + 'circle' => $circles[\array_rand($circles)] + ]); + + $this->assertTrue($client->getResponse()->isRedirect(sprintf( + '/fr/task/task/list/%d', $person->getId()))); + + $crawler = $client->followRedirect(); + + $this->assertContains($title, $crawler->text(), + "Assert that newly created task title is shown in list page") + ; + } + +} diff --git a/Tests/Controller/TaskControllerTest.php b/Tests/Controller/TaskControllerTest.php new file mode 100644 index 000000000..18b980329 --- /dev/null +++ b/Tests/Controller/TaskControllerTest.php @@ -0,0 +1,9 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\TaskBundle\Workflow\Definition; + +use Chill\TaskBundle\Entity\AbstractTask; +use Chill\TaskBundle\Entity\SingleTask; +use Symfony\Component\Workflow\Transition; + +/** + * + * + * @author Julien Fastré + */ +class DefaultTaskDefinition implements \Chill\TaskBundle\Workflow\TaskWorkflowDefinition +{ + const TRANSITION_METADATA = [ + 'close' => [ + 'verb' => 'close', + 'background-color' => 'var(--yellow)', // css variable, see https://developer.mozilla.org/fr/docs/Web/CSS/Les_variables_CSS + 'text-color' => 'black' + ], + 'cancel' => [ + 'verb' => 'cancel', + 'background-color' => 'var(--red)', // css variable, see https://developer.mozilla.org/fr/docs/Web/CSS/Les_variables_CSS + 'text-color' => 'black' + ], + 'start' => [ + 'verb' => 'start', + 'background-color' => 'var(--green)', // css variable, see https://developer.mozilla.org/fr/docs/Web/CSS/Les_variables_CSS + 'text-color' => 'black' + ] + ]; + + public function supports(AbstractTask $task) + { + + return $task instanceof SingleTask + && $task->getType() === 'task_default'; + } + + public function getAssociatedWorkflowName() + { + return 'task_default'; + } + + public function getWorkflowMetadata( + string $key, + $metadataSubject = null + ) { + $keys = \explode('.', $key); + + switch($keys[0]) { + case 'transition': + if (!$metadataSubject instanceof Transition) { + throw new \LogicException("You must give a transition as metadatasubject"); + } + + return $this->getTransitionMetadata(\implode('.', \array_slice($keys, 1)), $metadataSubject); + default: + throw new \LogicException("this key '$key' is not implemented"); + } + } + + protected function getTransitionMetadata($key, Transition $transition) + { + if (!\array_key_exists($transition->getName(), self::TRANSITION_METADATA)) { + throw new \LogicException("the metadata for this transition are not defined"); + } + + if (!\array_key_exists($key, self::TRANSITION_METADATA[$transition->getName()])) { + throw new \LogicException("The metadata ".$key." is not " + . "defined for the transition ".$transition.getName()); + } + + return self::TRANSITION_METADATA[$transition->getName()][$key]; + } +} diff --git a/Workflow/TaskWorkflowDefinition.php b/Workflow/TaskWorkflowDefinition.php new file mode 100644 index 000000000..d49591a64 --- /dev/null +++ b/Workflow/TaskWorkflowDefinition.php @@ -0,0 +1,27 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\TaskBundle\Workflow; + +/** + * + * @author Julien Fastré + */ +interface TaskWorkflowDefinition +{ + +} diff --git a/Workflow/TaskWorkflowManager.php b/Workflow/TaskWorkflowManager.php index e942e303b..8675f828e 100644 --- a/Workflow/TaskWorkflowManager.php +++ b/Workflow/TaskWorkflowManager.php @@ -28,14 +28,56 @@ use Symfony\Component\Workflow\Workflow; */ class TaskWorkflowManager implements SupportStrategyInterface { + /** + * + * @var TaskWorkflowDefinition[] + */ + protected $definitions = array(); + + public function addDefinition(TaskWorkflowDefinition $definition) { + $this->definitions[] = $definition; + } + + /** + * + * @param AbstractTask $task + * @return TaskWorkflowDefinition + * @throws \LogicException + */ + public function getTaskWorkflowDefinition(AbstractTask $task) + { + $definitions = array(); + + foreach($this->definitions as $tested) { + if ($tested->supports($task)) { + $definitions[] = $tested; + } + } + + $count = count($definitions); + if ($count > 1) { + throw new \LogicException("More than one TaskWorkflowDefinition supports " + . "this task. This should not happens."); + } elseif ($count === 0) { + throw new \LogicException("No taskWorkflowDefinition supports this task."); + } + + return $definitions[0]; + } + public function supports(Workflow $workflow, $subject): bool { if (!$subject instanceof AbstractTask) { return false; } - dump($workflow->getName()); - - return $workflow->getName() === 'task_default'; + return $workflow->getName() === $this + ->getTaskWorkflowDefinition($subject)->getAssociatedWorkflowName(); + } + + public function getWorkflowMetadata(AbstractTask $task, string $key, $metadataSubject = null, string $name = null) + { + return $this->getTaskWorkflowDefinition($task) + ->getWorkflowMetadata($key, $metadataSubject); } }