Only one running timesheet: automatically stop others (#386)

This commit is contained in:
Lukas
2019-01-21 12:49:22 +01:00
committed by Kevin Papst
parent 5539f68a54
commit 47ac50b4d0
18 changed files with 215 additions and 77 deletions

View File

@@ -41,14 +41,21 @@ class TimesheetController extends BaseApiController
*/
protected $viewHandler;
/**
* @var int
*/
protected $hardLimit;
/**
* @param ViewHandlerInterface $viewHandler
* @param TimesheetRepository $repository
* @param int $hardLimit
*/
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository)
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, int $hardLimit)
{
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->hardLimit = $hardLimit;
}
/**
@@ -161,8 +168,7 @@ class TimesheetController extends BaseApiController
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
'csrf_protection' => false,
'method' => 'POST',
'duration_only' => false,
'include_rate' => $this->isGranted('edit_rate', $timesheet),
]);
$form->setData($timesheet);
@@ -177,6 +183,21 @@ class TimesheetController extends BaseApiController
return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST);
}
if ($form->has('duration')) {
$duration = $form->get('duration')->getData();
if ($duration > 0) {
/** @var Timesheet $record */
$record = $form->getData();
$end = clone $record->getBegin();
$end->modify('+ ' . $duration . 'seconds');
$record->setEnd($end);
}
}
$this->repository->stopActiveEntries(
$timesheet->getUser(),
$this->hardLimit
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($timesheet);
$entityManager->flush();

View File

@@ -42,15 +42,7 @@ abstract class AbstractController extends Controller
*/
protected function flashSuccess($translationKey, $parameter = [])
{
if (!empty($parameter)) {
$translationKey = $this->getTranslator()->trans(
$translationKey,
$parameter,
self::DOMAIN_FLASH
);
}
$this->addFlash(self::FLASH_SUCCESS, $translationKey);
$this->addFlashTranslated(self::FLASH_SUCCESS, $translationKey, $parameter);
}
/**
@@ -61,15 +53,7 @@ abstract class AbstractController extends Controller
*/
protected function flashWarning($translationKey, $parameter = [])
{
if (!empty($parameter)) {
$translationKey = $this->getTranslator()->trans(
$translationKey,
$parameter,
self::DOMAIN_FLASH
);
}
$this->addFlash(self::FLASH_WARNING, $translationKey);
$this->addFlashTranslated(self::FLASH_WARNING, $translationKey, $parameter);
}
/**
@@ -79,15 +63,30 @@ abstract class AbstractController extends Controller
* @param array $parameter
*/
protected function flashError($translationKey, $parameter = [])
{
$this->addFlashTranslated(self::FLASH_ERROR, $translationKey, $parameter);
}
/**
* Adds a fully translated (both $message and all keys in $parameter) flash message to the stack.
*
* @param string $type
* @param string $message
* @param array $parameter
*/
protected function addFlashTranslated(string $type, string $message, array $parameter = [])
{
if (!empty($parameter)) {
$translationKey = $this->getTranslator()->trans(
$translationKey,
foreach ($parameter as $key => $value) {
$parameter[$key] = $this->getTranslator()->trans($value, [], self::DOMAIN_FLASH);
}
$message = $this->getTranslator()->trans(
$message,
$parameter,
self::DOMAIN_FLASH
);
}
$this->addFlash(self::FLASH_ERROR, $translationKey);
$this->addFlash($type, $message);
}
}

View File

@@ -30,15 +30,6 @@ class TimesheetController extends AbstractController
{
use TimesheetControllerTrait;
/**
* TimesheetController constructor.
* @param bool $durationOnly
*/
public function __construct(bool $durationOnly)
{
$this->setDurationMode($durationOnly);
}
/**
* @Route(path="/", defaults={"page": 1}, name="admin_timesheet", methods={"GET"})
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated", methods={"GET"})
@@ -179,8 +170,6 @@ class TimesheetController extends AbstractController
{
return $this->createForm(TimesheetEditForm::class, $entry, [
'action' => $this->generateUrl('admin_timesheet_create'),
'method' => 'POST',
'duration_only' => $this->isDurationOnlyMode(),
'include_rate' => $this->isGranted('edit_rate', $entry),
'include_user' => true,
]);
@@ -198,8 +187,6 @@ class TimesheetController extends AbstractController
'id' => $entry->getId(),
'page' => $page
]),
'method' => 'POST',
'duration_only' => $this->isDurationOnlyMode(),
'include_rate' => $this->isGranted('edit_rate', $entry),
'include_user' => true,
]);

View File

@@ -30,11 +30,11 @@ class TimesheetController extends AbstractController
use TimesheetControllerTrait;
/**
* @param bool $durationOnly
* @param int $hardLimit
*/
public function __construct(bool $durationOnly)
public function __construct(int $hardLimit)
{
$this->setDurationMode($durationOnly);
$this->setHardLimit($hardLimit);
}
/**
@@ -168,6 +168,8 @@ class TimesheetController extends AbstractController
} else {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entry);
$this->stopActiveEntries($user);
$entityManager->flush();
$this->flashSuccess('timesheet.start.success');
}
@@ -238,9 +240,7 @@ class TimesheetController extends AbstractController
{
return $this->createForm(TimesheetEditForm::class, $entry, [
'action' => $this->generateUrl('timesheet_create'),
'method' => 'POST',
'include_rate' => $this->isGranted('edit_rate', $entry),
'duration_only' => $this->isDurationOnlyMode(),
]);
}
@@ -256,9 +256,7 @@ class TimesheetController extends AbstractController
'id' => $entry->getId(),
'page' => $page
]),
'method' => 'POST',
'include_rate' => $this->isGranted('edit_rate', $entry),
'duration_only' => $this->isDurationOnlyMode(),
]);
}

View File

@@ -10,6 +10,7 @@
namespace App\Controller;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Repository\TimesheetRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -22,24 +23,24 @@ use Symfony\Component\HttpFoundation\Response;
trait TimesheetControllerTrait
{
/**
* @var bool
* @var int
*/
private $durationOnly = false;
private $hardLimit = 1;
/**
* @param bool $durationOnly
* @param int $hardLimit
*/
protected function setDurationMode(bool $durationOnly)
protected function setHardLimit(int $hardLimit)
{
$this->durationOnly = $durationOnly;
$this->hardLimit = $hardLimit;
}
/**
* @return bool
* @return int
*/
protected function isDurationOnlyMode()
protected function getHardLimit()
{
return $this->durationOnly;
return $this->hardLimit;
}
/**
@@ -169,11 +170,16 @@ trait TimesheetControllerTrait
}
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($entry);
$entityManager->flush();
try {
$this->stopActiveEntries($entry->getUser());
$entityManager->persist($entry);
$entityManager->flush();
$this->flashSuccess('action.update.success');
$this->flashSuccess('action.update.success');
} catch (\Exception $ex) {
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
}
return $this->redirectToRoute($redirectRoute);
}
@@ -184,6 +190,17 @@ trait TimesheetControllerTrait
]);
}
/**
* @param User $user
* @throws \App\Repository\RepositoryException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
protected function stopActiveEntries(User $user)
{
$this->getRepository()->stopActiveEntries($user, $this->getHardLimit());
}
/**
* @param Timesheet $entry
* @return \Symfony\Component\Form\FormInterface

View File

@@ -118,6 +118,8 @@ class AppExtension extends Extension implements PrependExtensionInterface
$container->setParameter('kimai.timesheet.rounding', $config['rounding']);
$container->setParameter('kimai.timesheet.duration_only', $config['duration_only']);
$container->setParameter('kimai.timesheet.markdown', $config['markdown_content']);
$container->setParameter('kimai.timesheet.active_entries.soft_limit', $config['active_entries']['soft_limit']);
$container->setParameter('kimai.timesheet.active_entries.hard_limit', $config['active_entries']['hard_limit']);
}
/**

View File

@@ -27,7 +27,9 @@ class TwigContextCompilerPass implements CompilerPassInterface
$theme = $container->getParameter('kimai.theme');
$durationOnly = $container->getParameter('kimai.timesheet.duration_only');
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
$twig->addMethodCall('addGlobal', ['kimai_context', array_merge($theme, [
'active_warning' => $container->getParameter('kimai.timesheet.active_entries.soft_limit')
])]);
$twig->addMethodCall('addGlobal', ['duration_only', $durationOnly]);
if ($container->hasDefinition('twig.loader.native_filesystem')) {

View File

@@ -85,7 +85,6 @@ class Configuration implements ConfigurationInterface
->end()
->defaultValue([])
->end()
->arrayNode('rates')
->requiresAtLeastOneElement()
->useAttributeAsKey('key')
@@ -112,6 +111,30 @@ class Configuration implements ConfigurationInterface
->end()
->defaultValue([])
->end()
->arrayNode('active_entries')
->addDefaultsIfNotSet()
->children()
->integerNode('soft_limit')
->defaultValue(1)
->validate()
->ifTrue(function ($value) {
return $value <= 0;
})
->thenInvalid('The soft_limit must be at least 1')
->end()
->end()
->integerNode('hard_limit')
->defaultValue(1)
->validate()
->ifTrue(function ($value) {
return $value <= 0;
})
->thenInvalid('The hard_limit must be at least 1')
->end()
->end()
->end()
->end()
->end()
->end()
;
@@ -212,6 +235,7 @@ class Configuration implements ConfigurationInterface
->children()
->integerNode('active_warning')
->defaultValue(3)
->setDeprecated('The node "%node%" at path "%path%" is deprecated, please use "kimai.timesheet.active_entries.soft_limit" instead.')
->end()
->scalarNode('box_color')
->defaultValue('green')

View File

@@ -36,19 +36,27 @@ class TimesheetEditForm extends AbstractType
* @var CustomerRepository
*/
private $customers;
/**
* @var ProjectRepository
*/
private $projects;
/**
* @var bool
*/
private $durationOnly = false;
/**
* @param CustomerRepository $customer
* @param ProjectRepository $project
* @param bool $durationOnly
*/
public function __construct(CustomerRepository $customer, ProjectRepository $project)
public function __construct(CustomerRepository $customer, ProjectRepository $project, bool $durationOnly)
{
$this->customers = $customer;
$this->projects = $project;
$this->durationOnly = $durationOnly;
}
/**
@@ -227,10 +235,11 @@ class TimesheetEditForm extends AbstractType
'csrf_protection' => true,
'csrf_field_name' => '_token',
'csrf_token_id' => 'timesheet_edit',
'duration_only' => false,
'duration_only' => $this->durationOnly,
'include_user' => false,
'include_rate' => true,
'docu_chapter' => 'timesheet',
'method' => 'POST',
]);
}
}

View File

@@ -260,6 +260,42 @@ class TimesheetRepository extends AbstractRepository
return $qb->getQuery()->execute($params);
}
/**
* @param User $user
* @param int $limit
* @return int
* @throws RepositoryException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function stopActiveEntries(User $user, int $hardLimit)
{
$counter = 0;
$activeEntries = $this->getActiveEntries($user);
// reduce limit by one:
// this method is only called when a new entry is started
// -> all entries, including the new one must not exceed the $limit
$limit = $hardLimit - 1;
if (count($activeEntries) > $limit) {
$i = 1;
foreach ($activeEntries as $activeEntry) {
if ($i > $limit) {
if ($hardLimit > 1) {
throw new \Exception('timesheet.start.exceeded_limit');
}
$this->stopRecording($activeEntry);
$counter++;
}
$i++;
}
}
return $counter;
}
/**
* @param TimesheetQuery $query
* @return QueryBuilder|Pagerfanta|array