added setting to limit the maximum length of a timesheet record (#2612)
This commit is contained in:
@@ -211,6 +211,16 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
}
|
||||
|
||||
// ========== Timesheet configurations ==========
|
||||
/*
|
||||
public function getTimesheetBreakWarningDuration(): int
|
||||
{
|
||||
return (int) $this->find('timesheet.rules.break_warning_duration');
|
||||
}
|
||||
*/
|
||||
public function getTimesheetLongRunningDuration(): int
|
||||
{
|
||||
return (int) $this->find('timesheet.rules.long_running_duration');
|
||||
}
|
||||
|
||||
public function getTimesheetDefaultBeginTime(): string
|
||||
{
|
||||
@@ -352,6 +362,11 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
return (bool) $this->find('theme.colors_limited');
|
||||
}
|
||||
|
||||
public function getThemeAutocompleteCharacters(): int
|
||||
{
|
||||
return (int) $this->find('theme.autocomplete_chars');
|
||||
}
|
||||
|
||||
public function getThemeColorChoices(): ?array
|
||||
{
|
||||
$config = $this->find('theme.color_choices');
|
||||
|
||||
@@ -365,6 +365,22 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
/*
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.break_warning_duration')
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
*/
|
||||
(new Configuration())
|
||||
->setName('timesheet.rules.long_running_duration')
|
||||
->setType(IntegerType::class)
|
||||
->setTranslationDomain('system-configuration')
|
||||
->setConstraints([
|
||||
new GreaterThanOrEqual(['value' => 0])
|
||||
]),
|
||||
]),
|
||||
(new SystemConfigurationModel())
|
||||
->setSection(SystemConfigurationModel::SECTION_LOCKDOWN)
|
||||
|
||||
@@ -242,6 +242,15 @@ class Configuration implements ConfigurationInterface
|
||||
->scalarNode('lockdown_grace_period')
|
||||
->defaultNull()
|
||||
->end()
|
||||
->scalarNode('lockdown_grace_period')
|
||||
->defaultNull()
|
||||
->end()
|
||||
->integerNode('break_warning_duration')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->integerNode('long_running_duration')
|
||||
->defaultValue(0)
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
->end()
|
||||
|
||||
26
src/Validator/Constraints/TimesheetLongRunning.php
Normal file
26
src/Validator/Constraints/TimesheetLongRunning.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
final class TimesheetLongRunning extends TimesheetConstraint
|
||||
{
|
||||
public const LONG_RUNNING = 'kimai-timesheet-long-running-01';
|
||||
|
||||
protected static $errorNames = [
|
||||
self::LONG_RUNNING => 'TIMESHEET_LONG_RUNNING',
|
||||
];
|
||||
|
||||
public $message = 'Maximum duration of {{ value }} hours exceeded.';
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
}
|
||||
69
src/Validator/Constraints/TimesheetLongRunningValidator.php
Normal file
69
src/Validator/Constraints/TimesheetLongRunningValidator.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Validator\Constraints;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetLongRunningValidator extends ConstraintValidator
|
||||
{
|
||||
private $systemConfiguration;
|
||||
|
||||
public function __construct(SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
$this->systemConfiguration = $systemConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate($timesheet, Constraint $constraint)
|
||||
{
|
||||
if (!($constraint instanceof TimesheetLongRunning)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetLongRunning::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($timesheet->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$maxMinutes = $this->systemConfiguration->getTimesheetLongRunningDuration();
|
||||
|
||||
if ($maxMinutes <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$duration = $timesheet->getEnd()->getTimestamp() - $timesheet->getBegin()->getTimestamp();
|
||||
$minutes = (int) $duration / 60;
|
||||
|
||||
if ($minutes < $maxMinutes) {
|
||||
return;
|
||||
}
|
||||
|
||||
$format = new \App\Utils\Duration();
|
||||
$hours = $format->format($maxMinutes * 60);
|
||||
|
||||
// raise a violation for all entries before the start of lockdown period
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setParameter('{{ value }}', $hours)
|
||||
->setTranslationDomain('validators')
|
||||
->atPath('duration')
|
||||
->setCode(TimesheetLongRunning::LONG_RUNNING)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
@@ -275,7 +275,7 @@
|
||||
locale: '{{ app.request.locale }}',
|
||||
first_dow_iso: {{ iso_day_by_name(app.user.firstDayOfWeek) }},
|
||||
twentyFourHours: {{ 'true'|hour24('false') }},
|
||||
autoComplete: {{ theme_config('autocomplete_chars') }},
|
||||
autoComplete: {{ kimai_config.themeAutocompleteCharacters }},
|
||||
defaultColor: '{{ constant('App\\Constants::DEFAULT_COLOR') }}',
|
||||
updateBrowserTitle: {% if app.user.preferenceValue('theme.update_browser_title') %}true{% else %}false{% endif %}
|
||||
},
|
||||
|
||||
@@ -177,6 +177,10 @@
|
||||
<span data-toggle="tooltip" title="{{ 'label.id'|trans }}: {{ user.id }}">{{ 'profile.about_me'|trans }}</span>
|
||||
{% endblock %}
|
||||
{% block box_body %}
|
||||
<p>
|
||||
<strong>{{ 'label.username'|trans }}</strong><br>
|
||||
{{ user.username }}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{{ 'profile.first_entry'|trans }}</strong><br>
|
||||
{# FIXME use a configuration for it #}
|
||||
|
||||
@@ -217,7 +217,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
|
||||
/**
|
||||
* @param Response $response
|
||||
* @param string[] $failedFields
|
||||
* @param array<int, string>|array<string, mixed> $failedFields
|
||||
* @param bool $extraFields test for the error "This form should not contain extra fields"
|
||||
*/
|
||||
protected function assertApiCallValidationError(Response $response, array $failedFields, bool $extraFields = false)
|
||||
@@ -235,9 +235,21 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
self::assertArrayHasKey('children', $result['errors']);
|
||||
$data = $result['errors']['children'];
|
||||
|
||||
foreach ($failedFields as $fieldName) {
|
||||
self::assertArrayHasKey($fieldName, $data, sprintf('Could not find validation error for field: %s', $fieldName));
|
||||
foreach ($failedFields as $key => $value) {
|
||||
$messages = [];
|
||||
$fieldName = $value;
|
||||
if (\is_string($key)) {
|
||||
$fieldName = $key;
|
||||
$messages = $value;
|
||||
if (!\is_array($messages)) {
|
||||
$messages = [$value];
|
||||
}
|
||||
}
|
||||
self::assertArrayHasKey($fieldName, $data, sprintf('Could not find validation error for field "%s" in list: %s', $fieldName, implode(', ', $failedFields)));
|
||||
self::assertArrayHasKey('errors', $data[$fieldName], sprintf('Field %s has no validation problem', $fieldName));
|
||||
foreach ($messages as $i => $message) {
|
||||
self::assertEquals($message, $data[$fieldName]['errors'][$i]);
|
||||
}
|
||||
}
|
||||
|
||||
$foundErrors = [];
|
||||
|
||||
@@ -389,7 +389,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$data = [
|
||||
'activity' => 1,
|
||||
'project' => 1,
|
||||
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
|
||||
'begin' => ($dateTime->createDateTime('-8 hours'))->format('Y-m-d H:m:0'),
|
||||
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
|
||||
'description' => 'foo',
|
||||
'fixedRate' => 2016,
|
||||
@@ -402,7 +402,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
|
||||
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
|
||||
$this->assertEquals(2016, $result['rate']);
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$data = [
|
||||
'activity' => 1,
|
||||
'project' => 1,
|
||||
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
|
||||
'begin' => ($dateTime->createDateTime('-8 hours'))->format('Y-m-d H:m:0'),
|
||||
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
|
||||
'description' => 'foo',
|
||||
'fixedRate' => 2016,
|
||||
@@ -426,7 +426,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('TimesheetEntityFull', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
|
||||
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
|
||||
$this->assertEquals(2016, $result['rate']);
|
||||
}
|
||||
|
||||
@@ -443,7 +443,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
'activity' => 1,
|
||||
'project' => 1,
|
||||
'user' => $user->getId(),
|
||||
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
|
||||
'begin' => ($dateTime->createDateTime('- 8 hours'))->format('Y-m-d H:m:0'),
|
||||
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
|
||||
'description' => 'foo',
|
||||
'fixedRate' => 2016,
|
||||
@@ -744,7 +744,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->importFixtureForUser(User::ROLE_USER);
|
||||
|
||||
$start = new \DateTime('-10 days');
|
||||
$start = new \DateTime('-8 hours');
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture
|
||||
@@ -752,7 +752,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
->setHourlyRate(true)
|
||||
->setAmount(0)
|
||||
->setUser($this->getUserByRole(User::ROLE_USER))
|
||||
->setStartDate($start)
|
||||
->setFixedStartDate($start)
|
||||
->setAmountRunning(1)
|
||||
;
|
||||
$timesheets = $this->importFixture($fixture);
|
||||
@@ -772,6 +772,30 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
|
||||
}
|
||||
|
||||
public function testStopActionTriggersValidationOnLongRunning()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->setSystemConfiguration('timesheet.rules.long_running_duration', 750);
|
||||
$this->importFixtureForUser(User::ROLE_USER);
|
||||
|
||||
$start = new \DateTime('-13 hours');
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture
|
||||
->setFixedRate(true)
|
||||
->setHourlyRate(true)
|
||||
->setAmount(0)
|
||||
->setUser($this->getUserByRole(User::ROLE_USER))
|
||||
->setFixedStartDate($start)
|
||||
->setAmountRunning(1)
|
||||
;
|
||||
$timesheets = $this->importFixture($fixture);
|
||||
$id = $timesheets[0]->getId();
|
||||
|
||||
$this->request($client, '/api/timesheets/' . $id . '/stop', 'PATCH');
|
||||
$this->assertApiCallValidationError($client->getResponse(), ['duration' => 'Maximum 12:30 hours allowed.']);
|
||||
}
|
||||
|
||||
public function testStopActionFailsOnStoppedEntry()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
@@ -995,7 +1019,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$data = [
|
||||
'activity' => 1,
|
||||
'project' => 1,
|
||||
'begin' => ($dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
|
||||
'begin' => ($dateTime->createDateTime('- 8 hours'))->format('Y-m-d H:m:0'),
|
||||
'end' => ($dateTime->createDateTime())->format('Y-m-d H:m:0'),
|
||||
'description' => 'foo',
|
||||
'fixedRate' => 2016,
|
||||
@@ -1008,7 +1032,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
|
||||
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
|
||||
$this->assertEquals(2016, $result['rate']);
|
||||
|
||||
$this->request($client, '/api/timesheets/' . $result['id'] . '/duplicate', 'PATCH');
|
||||
@@ -1018,7 +1042,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('TimesheetEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
|
||||
$this->assertTrue($result['duration'] == 28800 || $result['duration'] == 28860); // 1 minute rounding might be applied
|
||||
$this->assertEquals(2016, $result['rate']);
|
||||
}
|
||||
|
||||
|
||||
@@ -304,10 +304,10 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(50, $timesheet->getRate());
|
||||
|
||||
$expected = new \DateTime('2018-08-02T20:00:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getBegin()->format(\DateTime::ATOM));
|
||||
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getBegin()->format(\DateTimeInterface::ATOM));
|
||||
|
||||
$expected = new \DateTime('2018-08-02T20:30:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getEnd()->format(\DateTime::ATOM));
|
||||
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getEnd()->format(\DateTimeInterface::ATOM));
|
||||
}
|
||||
|
||||
public function testCreateActionWithFromAndToValuesTwice()
|
||||
@@ -338,10 +338,10 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(50, $timesheet->getRate());
|
||||
|
||||
$expected = new \DateTime('2018-08-02T20:00:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getBegin()->format(\DateTime::ATOM));
|
||||
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getBegin()->format(\DateTimeInterface::ATOM));
|
||||
|
||||
$expected = new \DateTime('2018-08-02T20:30:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getEnd()->format(\DateTime::ATOM));
|
||||
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getEnd()->format(\DateTimeInterface::ATOM));
|
||||
|
||||
// create a second entry that is overlapping
|
||||
$this->request($client, '/timesheet/create?from=2018-08-02T20%3A02%3A00&to=2018-08-02T20%3A20%3A00');
|
||||
@@ -489,10 +489,10 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(800, $timesheet->getRate());
|
||||
|
||||
$expected = new \DateTime('2018-08-02T10:00:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getBegin()->format(\DateTime::ATOM));
|
||||
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getBegin()->format(\DateTimeInterface::ATOM));
|
||||
|
||||
$expected = new \DateTime('2018-08-02T18:00:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getEnd()->format(\DateTime::ATOM));
|
||||
$this->assertEquals($expected->format(\DateTimeInterface::ATOM), $timesheet->getEnd()->format(\DateTimeInterface::ATOM));
|
||||
|
||||
$this->assertEquals(['one', 'two', 'three'], $timesheet->getTagsAsArray());
|
||||
}
|
||||
@@ -502,9 +502,9 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
|
||||
$fixture->setStartDate('2017-05-01');
|
||||
$fixture->setFixedStartDate(new \DateTime('-2 hours'));
|
||||
$timesheets = $this->importFixture($fixture);
|
||||
$id = $timesheets[0]->getId();
|
||||
|
||||
@@ -527,6 +527,10 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
]
|
||||
]);
|
||||
|
||||
if (!$client->getResponse()->isRedirect()) {
|
||||
dd($response->getStatusCode(), $response->getContent());
|
||||
}
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||
$client->followRedirect();
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
@@ -645,7 +649,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$fixture->setCallback(function (Timesheet $timesheet) {
|
||||
$timesheet->setDescription('Testing is fun!');
|
||||
$end = clone $timesheet->getBegin();
|
||||
$end->modify('+ 16 hours');
|
||||
$end->modify('+ 8 hours');
|
||||
$timesheet->setEnd($end);
|
||||
$timesheet->setFixedRate(2016);
|
||||
$timesheet->setHourlyRate(127);
|
||||
@@ -675,7 +679,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(2016, $timesheet->getRate());
|
||||
$this->assertEquals(127, $timesheet->getHourlyRate());
|
||||
$this->assertEquals(2016, $timesheet->getFixedRate());
|
||||
$this->assertTrue($timesheet->getDuration() == 57600 || $timesheet->getDuration() == 57660); // 1 minute rounding might be applied
|
||||
$this->assertTrue($timesheet->getDuration() == 28800 || $timesheet->getDuration() == 28860); // 1 minute rounding might be applied
|
||||
$this->assertEquals(2016, $timesheet->getRate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,13 +265,12 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$user = $this->getUserByRole(User::ROLE_USER);
|
||||
$teamlead = $this->getUserByRole(User::ROLE_TEAMLEAD);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setUser($user);
|
||||
$fixture->setStartDate('2017-05-01');
|
||||
$fixture->setFixedStartDate(new \DateTime('-2 hours'));
|
||||
$timesheets = $this->importFixture($fixture);
|
||||
$id = $timesheets[0]->getId();
|
||||
|
||||
@@ -420,7 +419,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
||||
$fixture->setCallback(function (Timesheet $timesheet) {
|
||||
$timesheet->setDescription('Testing is fun!');
|
||||
$end = clone $timesheet->getBegin();
|
||||
$end->modify('+ 16 hours');
|
||||
$end->modify('+ 8 hours');
|
||||
$timesheet->setEnd($end);
|
||||
$timesheet->setFixedRate(2016);
|
||||
$timesheet->setHourlyRate(127);
|
||||
@@ -450,7 +449,7 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals(2016, $timesheet->getRate());
|
||||
$this->assertEquals(127, $timesheet->getHourlyRate());
|
||||
$this->assertEquals(2016, $timesheet->getFixedRate());
|
||||
$this->assertTrue($timesheet->getDuration() == 57600 || $timesheet->getDuration() == 57660); // 1 minute rounding might be applied
|
||||
$this->assertTrue($timesheet->getDuration() == 28800 || $timesheet->getDuration() == 28860); // 1 minute rounding might be applied
|
||||
$this->assertEquals(2016, $timesheet->getRate());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,10 @@ final class TimesheetFixtures implements TestFixture
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $startDate;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $fixedStartDate;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
@@ -129,6 +133,13 @@ final class TimesheetFixtures implements TestFixture
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setFixedStartDate(\DateTime $date): TimesheetFixtures
|
||||
{
|
||||
$this->fixedStartDate = $date;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setAmountRunning(int $amount): TimesheetFixtures
|
||||
{
|
||||
$this->running = $amount;
|
||||
@@ -312,6 +323,10 @@ final class TimesheetFixtures implements TestFixture
|
||||
|
||||
private function getDateTime(int $i): \DateTime
|
||||
{
|
||||
if ($this->fixedStartDate !== null) {
|
||||
return $this->fixedStartDate;
|
||||
}
|
||||
|
||||
if ($this->startDate === null) {
|
||||
$this->startDate = new \DateTime('2018-04-01');
|
||||
}
|
||||
|
||||
@@ -204,6 +204,8 @@ class AppExtensionTest extends TestCase
|
||||
'lockdown_grace_period' => null,
|
||||
'allow_overbooking_budget' => true,
|
||||
'lockdown_period_timezone' => null,
|
||||
'break_warning_duration' => 0,
|
||||
'long_running_duration' => 0,
|
||||
],
|
||||
'default_begin' => 'now',
|
||||
'duration_increment' => null,
|
||||
|
||||
@@ -289,6 +289,8 @@ class ConfigurationTest extends TestCase
|
||||
'lockdown_grace_period' => null,
|
||||
'allow_overbooking_budget' => true,
|
||||
'lockdown_period_timezone' => null,
|
||||
'break_warning_duration' => 0,
|
||||
'long_running_duration' => 0,
|
||||
],
|
||||
'duration_increment' => null,
|
||||
'time_increment' => null,
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Validator\Constraints;
|
||||
|
||||
use App\Configuration\ConfigLoaderInterface;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Validator\Constraints\TimesheetLongRunning;
|
||||
use App\Validator\Constraints\TimesheetLongRunningValidator;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Validator\Constraints\TimesheetLongRunningValidator
|
||||
*/
|
||||
class TimesheetLongRunningValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
protected function createValidator()
|
||||
{
|
||||
return $this->createMyValidator(120);
|
||||
}
|
||||
|
||||
protected function createMyValidator(int $minutes)
|
||||
{
|
||||
$loader = $this->createMock(ConfigLoaderInterface::class);
|
||||
$config = new SystemConfiguration($loader, [
|
||||
'timesheet' => [
|
||||
'rules' => [
|
||||
'long_running_duration' => $minutes,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
return new TimesheetLongRunningValidator($config);
|
||||
}
|
||||
|
||||
public function testConstraintIsInvalid()
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new Timesheet(), new NotBlank());
|
||||
}
|
||||
|
||||
public function testInvalidValueThrowsException()
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetLongRunning(['message' => 'myMessage']));
|
||||
}
|
||||
|
||||
public function testLongRunningTriggers()
|
||||
{
|
||||
$begin = new \DateTime();
|
||||
$end = new \DateTime('+10 hour');
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($end);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetLongRunning());
|
||||
|
||||
$this->buildViolation('Maximum duration of {{ value }} hours exceeded.')
|
||||
->atPath('property.path.duration')
|
||||
->setParameter('{{ value }}', '02:00')
|
||||
->setCode(TimesheetLongRunning::LONG_RUNNING)
|
||||
->assertRaised();
|
||||
}
|
||||
|
||||
public function testLongRunningNotTriggersIfConfiguredToZero()
|
||||
{
|
||||
$this->validator = $this->createMyValidator(0);
|
||||
$this->validator->initialize($this->context);
|
||||
|
||||
$begin = new \DateTime();
|
||||
$end = new \DateTime('+10 hour');
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($end);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetLongRunning());
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testLongRunningNotTriggersIfDurationIsLowerThan()
|
||||
{
|
||||
$this->validator = $this->createMyValidator(121);
|
||||
$this->validator->initialize($this->context);
|
||||
|
||||
$begin = new \DateTime();
|
||||
$end = new \DateTime('+2 hour');
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($end);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetLongRunning());
|
||||
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
|
||||
public function testNotTriggersOnRunningRecord()
|
||||
{
|
||||
$begin = new \DateTime('-10 hour');
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setBegin($begin);
|
||||
|
||||
$this->validator->validate($timesheet, new TimesheetLongRunning());
|
||||
$this->assertNoViolation();
|
||||
}
|
||||
}
|
||||
@@ -298,6 +298,14 @@
|
||||
<source>label.saml_activate</source>
|
||||
<target>SAML Anmeldung</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.timesheet.rules.break_warning_duration">
|
||||
<source>label.timesheet.rules.break_warning_duration</source>
|
||||
<target>Maximale Dauer eines Zeiteintrags in Minuten, bevor eine Pausen-Warnung angezeigt wird (0 = deaktiviert)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.timesheet.rules.long_running_duration">
|
||||
<source>label.timesheet.rules.long_running_duration</source>
|
||||
<target>Maximale Dauer eines Zeiteintrags in Minuten, bevor das Speichern abgelehnt wird (0 = deaktiviert)</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -298,6 +298,14 @@
|
||||
<source>label.saml_activate</source>
|
||||
<target>SAML login</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.timesheet.rules.break_warning_duration">
|
||||
<source>label.timesheet.rules.break_warning_duration</source>
|
||||
<target>Maximum duration of a timesheet record in minutes before a "break warning" will be displayed (0 = deactivated)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="label.timesheet.rules.long_running_duration">
|
||||
<source>label.timesheet.rules.long_running_duration</source>
|
||||
<target>Maximum duration of a timesheet record in minutes before saving is rejected (0 = deactivated)</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
<source>The budget is completely used.</source>
|
||||
<target>Das Budget ist aufgebraucht. Von den vorhandenen %budget% wurden bisher %used% gebucht, noch nutzbar sind %free%.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Maximum duration of {{ value }} hours exceeded.">
|
||||
<source>Maximum duration of {{ value }} hours exceeded.</source>
|
||||
<target>Erlaubt sind max. {{ value }} Stunden.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
@@ -50,6 +50,10 @@
|
||||
<source>The budget is completely used.</source>
|
||||
<target>The budget is used up. Of the available %budget%, %used% has been booked so far, %free% can still be used.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="Maximum duration of {{ value }} hours exceeded.">
|
||||
<source>Maximum duration of {{ value }} hours exceeded.</source>
|
||||
<target>Maximum {{ value }} hours allowed.</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
|
||||
Reference in New Issue
Block a user