From 77b7333fea9024a38a6cc6d9c29e8ffa3e4886b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 9 Mar 2016 13:38:05 +0100 Subject: [PATCH] Allow encoding of an activity with configuration preset durations fix #11 The activity form allow to receive pre-configured activity durations. Those duration can be configured through the option chill_activity.form.time_duration : ```yaml chill_activity: form: time_duration: - { label: '12 minutes', seconds: 720 } # - ... ``` If a pre-existing activity receives a different time, the time is added to the list of pre-configured duration time. --- Controller/ActivityController.php | 2 +- .../ChillActivityExtension.php | 2 + DependencyInjection/Configuration.php | 72 +++++- Form/ActivityType.php | 90 +++++++- Resources/config/services.yml | 1 + Resources/translations/messages.fr.yml | 14 ++ Resources/views/Activity/new.html.twig | 4 +- Tests/Controller/ActivityControllerTest.php | 5 +- Tests/Form/ActivityTypeTest.php | 205 ++++++++++++++++++ 9 files changed, 374 insertions(+), 21 deletions(-) create mode 100644 Tests/Form/ActivityTypeTest.php diff --git a/Controller/ActivityController.php b/Controller/ActivityController.php index 3c8cf7906..da03b3150 100644 --- a/Controller/ActivityController.php +++ b/Controller/ActivityController.php @@ -289,7 +289,7 @@ class ActivityController extends Controller $this->get('translator') ->trans('Success : activity updated!') ); - +//die(); return $this->redirect($this->generateUrl('chill_activity_activity_show', array('id' => $id, 'person_id' => $person_id))); } diff --git a/DependencyInjection/ChillActivityExtension.php b/DependencyInjection/ChillActivityExtension.php index 262925889..b66544a65 100644 --- a/DependencyInjection/ChillActivityExtension.php +++ b/DependencyInjection/ChillActivityExtension.php @@ -42,6 +42,8 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf { $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); + + $container->setParameter('chill_activity.form.time_duration', $config['form']['time_duration']); $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('services.yml'); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index a177c28c4..58706054d 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -19,10 +19,76 @@ class Configuration implements ConfigurationInterface { $treeBuilder = new TreeBuilder(); $rootNode = $treeBuilder->root('chill_activity'); + + $rootNode + ->children() + ->arrayNode('form') + ->canBeEnabled() + ->children() + ->arrayNode('time_duration') + ->isRequired() + ->requiresAtLeastOneElement() + ->defaultValue( + array( + [ 'label' => '5 minutes', 'seconds' => 300], + [ 'label' => '10 minutes', 'seconds' => 600], + [ 'label' => '15 minutes', 'seconds' => 900], + [ 'label' => '20 minutes', 'seconds' => 1200], + [ 'label' => '25 minutes', 'seconds' => 1500], + [ 'label' => '30 minutes', 'seconds' => 1800], + [ 'label' => '45 minutes', 'seconds' => 2700], + [ 'label' => '1 hour', 'seconds' => 3600], + [ 'label' => '1 hour 15', 'seconds' => 4500], + [ 'label' => '1 hour 30', 'seconds' => 5400], + [ 'label' => '1 hour 45', 'seconds' => 6300], + [ 'label' => '2 hours', 'seconds' => 7200], + ) + ) + ->info('The intervals of time to show in activity form') - // Here you should define the parameters that are allowed to - // configure your bundle. See the documentation linked above for - // more information on that topic. + ->prototype('array') + ->children() + ->scalarNode('seconds') + ->info("The number of seconds of this duration. Must be an integer.") + ->cannotBeEmpty() + ->validate() + ->ifTrue(function($data) { + return !is_int($data); + })->thenInvalid("The value %s is not a valid integer") + ->end() + ->end() + ->scalarNode('label') + ->cannotBeEmpty() + ->info("The label to show into fields") + ->end() + ->end() + + ->end() +// ->validate() +// +// ->ifTrue(function ($data) { +// // test this is an array +// if (!is_array($data)) { +// return true; +// } +// +// foreach ($data as $k => $v) { +// if (!is_string($k)) { +// return true; +// } +// if (!is_int($v)) { +// return true; +// } +// } +// +// }) +// ->thenInvalid("The data are invalid. The keys must be a string and the value integers") +// ->end() + ->end() + ->end() + + ->end() + ->end(); return $treeBuilder; } diff --git a/Form/ActivityType.php b/Form/ActivityType.php index 1c9580cb4..65e977e42 100644 --- a/Form/ActivityType.php +++ b/Form/ActivityType.php @@ -11,6 +11,13 @@ use Doctrine\Common\Persistence\ObjectManager; use Chill\MainBundle\Templating\TranslatableStringHelper; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Chill\MainBundle\Entity\User; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; +use Chill\ActivityBundle\Form\Type\TranslatableActivityType; +use Chill\ActivityBundle\Form\Type\TranslatableActivityReason; class ActivityType extends AbstractType { @@ -42,9 +49,14 @@ class ActivityType extends AbstractType */ protected $translatableStringHelper; - public function __construct(TokenStorageInterface $tokenStorage, - AuthorizationHelper $authorizationHelper, ObjectManager $om, - TranslatableStringHelper $translatableStringHelper) + protected $timeChoices; + + public function __construct( + TokenStorageInterface $tokenStorage, + AuthorizationHelper $authorizationHelper, ObjectManager $om, + TranslatableStringHelper $translatableStringHelper, + array $timeChoices + ) { if (!$tokenStorage->getToken()->getUser() instanceof User) { throw new \RuntimeException("you should have a valid user"); @@ -53,6 +65,7 @@ class ActivityType extends AbstractType $this->authorizationHelper = $authorizationHelper; $this->om = $om; $this->translatableStringHelper = $translatableStringHelper; + $this->timeChoices = $timeChoices; } /** @@ -61,21 +74,32 @@ class ActivityType extends AbstractType */ public function buildForm(FormBuilderInterface $builder, array $options) { + // handle times choices + $timeChoices = array(); + + foreach ($this->timeChoices as $e) { + $timeChoices[$e['label']] = $e['seconds']; + }; + + $durationTimeTransformer = new DateTimeToTimestampTransformer('GMT', 'GMT'); + $durationTimeOptions = array( + 'choices' => $timeChoices, + 'choices_as_values' => true, + 'placeholder' => 'Choose the duration', + ); + $builder ->add('date', 'date', array( 'required' => true, 'widget' => 'single_text', 'format' => 'dd-MM-yyyy') ) - ->add('durationTime', 'time', array( - 'widget' => 'text', - 'hours' => array(0, 1, 2, 3, 4, 5, 6, 7, 8) - )) + ->add('durationTime', ChoiceType::class, $durationTimeOptions) ->add('remark', 'textarea', array( 'required' => false, 'empty_data' => '' )) - ->add('attendee', 'choice', array( + ->add('attendee', ChoiceType::class, array( 'expanded' => true, 'required' => false, 'choices' => array( @@ -85,17 +109,61 @@ class ActivityType extends AbstractType )) ->add('user') ->add('scope') - ->add('reasons', 'translatable_activity_reason', array( + ->add('reasons', TranslatableActivityReason::class, array( 'multiple' => true, 'required' => false )) - ->add('type', 'translatable_activity_type') - //->add('person') + ->add('type', TranslatableActivityType::class) ; $this->appendScopeChoices($builder, $options['role'], $options['center'], $this->user, $this->authorizationHelper, $this->translatableStringHelper, $this->om); + + $builder->get('durationTime') + ->addModelTransformer($durationTimeTransformer); + + + $builder->get('durationTime') + ->addEventListener( + FormEvents::PRE_SET_DATA, + function(FormEvent $formEvent) use ( + $timeChoices, + $builder, + $durationTimeTransformer, + $durationTimeOptions + ) + { + // set the timezone to GMT, and fix the difference between current and GMT + // the datetimetransformer will then handle timezone as GMT + $timezoneUTC = new \DateTimeZone('GMT'); + /* @var $data \DateTime */ + $data = $formEvent->getData() === NULL ? + \DateTime::createFromFormat('U', 300) : + $formEvent->getData(); + $seconds = $data->getTimezone()->getOffset($data); + $data->setTimeZone($timezoneUTC); + $data->add(new \DateInterval('PT'.$seconds.'S')); + + // test if the timestamp is in the choices. + // If not, recreate the field with the new timestamp + if (!in_array($data->getTimestamp(), $timeChoices)) { + // the data are not in the possible values. add them + $timeChoices[$data->format('H:i')] = $data->getTimestamp(); + $form = $builder->create( + 'durationTime', + ChoiceType::class, + array_merge( + $durationTimeOptions, + array( + 'choices' => $timeChoices, + 'auto_initialize' => false + ) + )); + $form->addModelTransformer($durationTimeTransformer); + $formEvent->getForm()->getParent()->add($form->getForm()); + } + }); } /** diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 39ba42839..751bf979d 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -27,6 +27,7 @@ services: - "@chill.main.security.authorization.helper" - "@doctrine.orm.entity_manager" - "@chill.main.helper.translatable_string" + - "%chill_activity.form.time_duration%" tags: - { name: form.type, alias: chill_activitybundle_activity } diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 387159c9e..b0803985e 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -24,6 +24,20 @@ Create: Créer Back to the list: Retour à la liste Save activity: Sauver l'activité Reset form: Remise à zéro du formulaire +Choose the duration: Choisir la durée +5 minutes: 5 minutes +10 minutes: 10 minutes +15 minutes: 15 minutes +20 minutes: 20 minutes +25 minutes: 25 minutes +30 minutes: 30 minutes +45 minutes: 45 minutes +1 hour: 1 heure +1 hour 15: 1 heure 15 +1 hour 30: 1 heure 30 +1 hour 45: 1 heure 45 +2 hours: 2 heures + #timeline '%user% has done an %activity_type% on %date%': %user% a effectué une activité de type "%activity_type%" le %date% diff --git a/Resources/views/Activity/new.html.twig b/Resources/views/Activity/new.html.twig index dbea1350c..33643c400 100644 --- a/Resources/views/Activity/new.html.twig +++ b/Resources/views/Activity/new.html.twig @@ -18,13 +18,13 @@ {% set activeRouteKey = 'chill_activity_activity_new' %} -{% block title %}{{ 'Activity create' |trans }}{% endblock title %} +{% block title 'Activity creation' |trans %} {% block personcontent %}

{{ "Activity creation"|trans }}

{{ form_start(form) }} - + {{ form_widget(form) }}
diff --git a/Tests/Controller/ActivityControllerTest.php b/Tests/Controller/ActivityControllerTest.php index 485c8e21e..81c280703 100644 --- a/Tests/Controller/ActivityControllerTest.php +++ b/Tests/Controller/ActivityControllerTest.php @@ -114,10 +114,7 @@ class ActivityControllerTest extends WebTestCase $form = $crawler->selectButton('Ajouter une nouvelle activité')->form(array( 'chill_activitybundle_activity'=> array( 'date' => '15-01-2015', - 'durationTime' => array( - 'hour' => '1', - 'minute' => '30' - ), + 'durationTime' => 600, 'remark' => 'blabla', 'scope' => $this->getRandomScope('center a_social', 'Center A')->getId(), 'type' => $this->getRandomActivityType()->getId() diff --git a/Tests/Form/ActivityTypeTest.php b/Tests/Form/ActivityTypeTest.php new file mode 100644 index 000000000..6bb0f032c --- /dev/null +++ b/Tests/Form/ActivityTypeTest.php @@ -0,0 +1,205 @@ + + * + * 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\ActivityBundle\Tests\Form; + +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; +use Chill\ActivityBundle\Form\ActivityType; +use Chill\ActivityBundle\Entity\Activity; +use Symfony\Component\Security\Core\Authentication\Token\AbstractToken; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Form\Extension\Core\Type\FormType; + +/** + * + * + * @author Julien Fastré + */ +class ActivityTypeTest extends KernelTestCase +{ + /** + * + * @var \Symfony\Component\Form\FormBuilderInterface + */ + protected $formBuilder; + + /** + * + * @var \Symfony\Component\DependencyInjection\ContainerInterface + */ + protected $container; + + /** + * + * @var \Symfony\Component\Security\Core\User\UserInterface + */ + protected $user; + + /** + * + * @var \Chill\MainBundle\Entity\Center + */ + protected $center; + + public function setUp() + { + self::bootKernel(); + + $this->container = self::$kernel->getContainer(); + + $prophet = new \Prophecy\Prophet; + + + $this->formBuilder = $this->container + ->get('form.factory') + ->createBuilder(FormType::class, null, array( + 'csrf_protection' => false, + 'csrf_field_name' => '_token' + )); + + $request = new \Symfony\Component\HttpFoundation\Request(); + $request->setLocale('fr'); + + self::$kernel->getContainer() + ->get('request_stack') + ->push($request); + + $this->user = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:User') + ->findOneBy(array('username' => 'center a_social')); + $this->center = $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:Center') + ->findOneBy(array('name' => 'Center A')); + $token = $prophet->prophesize(); + $token->willExtend(AbstractToken::class); + $token->getUser()->willReturn($this->user); + $this->container->get('security.token_storage') + ->setToken($token->reveal()); + + } + + public function testForm() + { + $form = $this->formBuilder + ->add('activity', ActivityType::class, array( + 'center' => $this->center, + 'role' => new Role('CHILL_ACTIVITY_CREATE') + )) + ->getForm(); + + $form->submit(array()); + + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + $this->assertInstanceOf(Activity::class, $form->getData()['activity']); + + } + + public function testFormSubmitting() + { + $form = $this->formBuilder + ->add('activity', ActivityType::class, array( + 'center' => $this->center, + 'role' => new Role('CHILL_ACTIVITY_CREATE') + )) + ->getForm(); + + $form->submit(array( 'activity' => array( + 'date' => '9-3-2015', + 'durationTime' => 300, + 'remark' => 'blabla', + 'attendee' => true + ))); + +// var_dump($form->getErrors()->count()); var_dump($form->isValid()); +// foreach($form->getErrors() as $e) { fwrite(STDOUT, var_dump($e->getMessage())); } +// var_dump($form->getErrors()); + + $this->assertTrue($form->isSynchronized(), "Test the form is synchronized"); + $this->assertTrue($form->isValid(), "test the form is valid"); + $this->assertInstanceOf(Activity::class, $form->getData()['activity']); + + // test the activity + /* @var $activity Activity */ + $activity = $form->getData()['activity']; + + $this->assertEquals('09-03-2015', $activity->getDate()->format('d-m-Y'), + "Test the date is correct"); + $this->assertEquals('00:05', $activity->getDurationTime()->format('H:i'), + "Test the formatted hour is correct"); + $this->assertEquals(true, $activity->getAttendee()); + $this->assertEquals('blabla', $activity->getRemark()); + + } + + /** + * Test that the form correctly build even with a durationTime which is not in + * the listed in the possible durationTime + */ + public function testFormWithActivityHavingDifferentTime() + { + $activity = new Activity(); + $activity->setDurationTime(\DateTime::createFromFormat('U', 60)); + + $builder = $this->container + ->get('form.factory') + ->createBuilder(FormType::class, array('activity' => $activity), array( + 'csrf_protection' => false, + 'csrf_field_name' => '_token' + )); + + $form = $builder + ->add('activity', ActivityType::class, array( + 'center' => $this->center, + 'role' => new Role('CHILL_ACTIVITY_CREATE') + )) + ->getForm(); + + + $form->submit(array( 'activity' => array( + 'date' => '9-3-2015', + 'durationTime' => 60, + 'remark' => 'blabla', + 'attendee' => true + ))); + + $this->assertTrue($form->isSynchronized()); + $this->assertTrue($form->isValid()); + + // test the activity + /* @var $activity Activity */ + $activity = $form->getData()['activity']; + + $this->assertEquals('00:01', $activity->getDurationTime()->format('H:i'), + "Test the formatted hour is correct"); + + // test the view : we want to be sure that the entry with 60 seconds exists + $view = $form->createView(); + + $this->assertTrue(isset($view['activity']['durationTime'])); + + // map all the values in an array + $values = array_map(function($choice) { return $choice->value; }, + $view['activity']['durationTime']->vars['choices']); + + $this->assertContains(60, $values); + + } + +}