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.
This commit is contained in:
Julien Fastré 2016-03-09 13:38:05 +01:00
parent f28a6e9fa0
commit 77b7333fea
9 changed files with 374 additions and 21 deletions

View File

@ -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)));
}

View File

@ -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');

View File

@ -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;
}

View File

@ -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());
}
});
}
/**

View File

@ -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 }

View File

@ -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%

View File

@ -18,13 +18,13 @@
{% set activeRouteKey = 'chill_activity_activity_new' %}
{% block title %}{{ 'Activity create' |trans }}{% endblock title %}
{% block title 'Activity creation' |trans %}
{% block personcontent %}
<h2 class="chill-red">{{ "Activity creation"|trans }}</h1>
{{ form_start(form) }}
{{ form_widget(form) }}
<div class="grid-12 centered sticky-form-buttons">
<button class="sc-button green margin-10" type="submit"><i class="fa fa-save"></i> {{ 'Add a new activity'|trans }}</button>

View File

@ -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()

View File

@ -0,0 +1,205 @@
<?php
/*
* Copyright (C) 2016 Champs-Libres <info@champs-libres.coop>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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é <julien.fastre@champs-libres.coop>
*/
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);
}
}