diff --git a/src/Configuration/SystemConfiguration.php b/src/Configuration/SystemConfiguration.php
index cd5eda90..cfa36239 100644
--- a/src/Configuration/SystemConfiguration.php
+++ b/src/Configuration/SystemConfiguration.php
@@ -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');
diff --git a/src/Controller/SystemConfigurationController.php b/src/Controller/SystemConfigurationController.php
index 23cd2c27..df12da95 100644
--- a/src/Controller/SystemConfigurationController.php
+++ b/src/Controller/SystemConfigurationController.php
@@ -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)
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 6f5088f3..9cb98574 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -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()
diff --git a/src/Validator/Constraints/TimesheetLongRunning.php b/src/Validator/Constraints/TimesheetLongRunning.php
new file mode 100644
index 00000000..9694875c
--- /dev/null
+++ b/src/Validator/Constraints/TimesheetLongRunning.php
@@ -0,0 +1,26 @@
+ 'TIMESHEET_LONG_RUNNING',
+ ];
+
+ public $message = 'Maximum duration of {{ value }} hours exceeded.';
+
+ public function getTargets()
+ {
+ return self::CLASS_CONSTRAINT;
+ }
+}
diff --git a/src/Validator/Constraints/TimesheetLongRunningValidator.php b/src/Validator/Constraints/TimesheetLongRunningValidator.php
new file mode 100644
index 00000000..2c586d0e
--- /dev/null
+++ b/src/Validator/Constraints/TimesheetLongRunningValidator.php
@@ -0,0 +1,69 @@
+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();
+ }
+}
diff --git a/templates/base.html.twig b/templates/base.html.twig
index c249a685..7d425f3c 100644
--- a/templates/base.html.twig
+++ b/templates/base.html.twig
@@ -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 %}
},
diff --git a/templates/user/stats.html.twig b/templates/user/stats.html.twig
index b0d49f8f..2470139b 100644
--- a/templates/user/stats.html.twig
+++ b/templates/user/stats.html.twig
@@ -177,6 +177,10 @@
{{ 'profile.about_me'|trans }}
{% endblock %}
{% block box_body %}
+
+ {{ 'label.username'|trans }}
+ {{ user.username }}
+
{{ 'profile.first_entry'|trans }}
{# FIXME use a configuration for it #}
diff --git a/tests/API/APIControllerBaseTest.php b/tests/API/APIControllerBaseTest.php
index cb7fc1f1..66459e2e 100644
--- a/tests/API/APIControllerBaseTest.php
+++ b/tests/API/APIControllerBaseTest.php
@@ -217,7 +217,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
/**
* @param Response $response
- * @param string[] $failedFields
+ * @param array|array $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 = [];
diff --git a/tests/API/TimesheetControllerTest.php b/tests/API/TimesheetControllerTest.php
index d254c10e..d00646b1 100644
--- a/tests/API/TimesheetControllerTest.php
+++ b/tests/API/TimesheetControllerTest.php
@@ -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']);
}
diff --git a/tests/Controller/TimesheetControllerTest.php b/tests/Controller/TimesheetControllerTest.php
index 0ec8c0e8..ca83126c 100644
--- a/tests/Controller/TimesheetControllerTest.php
+++ b/tests/Controller/TimesheetControllerTest.php
@@ -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());
}
}
diff --git a/tests/Controller/TimesheetTeamControllerTest.php b/tests/Controller/TimesheetTeamControllerTest.php
index f30af226..c7f00848 100644
--- a/tests/Controller/TimesheetTeamControllerTest.php
+++ b/tests/Controller/TimesheetTeamControllerTest.php
@@ -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());
}
}
diff --git a/tests/DataFixtures/TimesheetFixtures.php b/tests/DataFixtures/TimesheetFixtures.php
index c26b4edc..1fe6c566 100644
--- a/tests/DataFixtures/TimesheetFixtures.php
+++ b/tests/DataFixtures/TimesheetFixtures.php
@@ -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');
}
diff --git a/tests/DependencyInjection/AppExtensionTest.php b/tests/DependencyInjection/AppExtensionTest.php
index f7318e14..2dc22b99 100644
--- a/tests/DependencyInjection/AppExtensionTest.php
+++ b/tests/DependencyInjection/AppExtensionTest.php
@@ -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,
diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php
index 5c16775f..4c40d832 100644
--- a/tests/DependencyInjection/ConfigurationTest.php
+++ b/tests/DependencyInjection/ConfigurationTest.php
@@ -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,
diff --git a/tests/Validator/Constraints/TimesheetLongRunningValidatorTest.php b/tests/Validator/Constraints/TimesheetLongRunningValidatorTest.php
new file mode 100644
index 00000000..4d18cf20
--- /dev/null
+++ b/tests/Validator/Constraints/TimesheetLongRunningValidatorTest.php
@@ -0,0 +1,117 @@
+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();
+ }
+}
diff --git a/translations/system-configuration.de.xlf b/translations/system-configuration.de.xlf
index 196f0227..545a333e 100644
--- a/translations/system-configuration.de.xlf
+++ b/translations/system-configuration.de.xlf
@@ -298,6 +298,14 @@
label.saml_activate
SAML Anmeldung
+
+ label.timesheet.rules.break_warning_duration
+ Maximale Dauer eines Zeiteintrags in Minuten, bevor eine Pausen-Warnung angezeigt wird (0 = deaktiviert)
+
+
+ label.timesheet.rules.long_running_duration
+ Maximale Dauer eines Zeiteintrags in Minuten, bevor das Speichern abgelehnt wird (0 = deaktiviert)
+