Release 2.36.0 (#5514)
This commit is contained in:
383
composer.lock
generated
383
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -63,24 +63,6 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
|
|||||||
}
|
}
|
||||||
|
|
||||||
public function serializeValidationExceptionToJson(JsonSerializationVisitor $visitor, ValidationFailedException $exception, array $type, Context $context)
|
public function serializeValidationExceptionToJson(JsonSerializationVisitor $visitor, ValidationFailedException $exception, array $type, Context $context)
|
||||||
{
|
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
/** @var ConstraintViolationInterface $error */
|
|
||||||
foreach (iterator_to_array($exception->getViolations()) as $error) {
|
|
||||||
$errors[$error->getPropertyPath()]['errors'][] = $this->getErrorMessage($error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'code' => '400',
|
|
||||||
'message' => $this->translator->trans($exception->getMessage(), [], 'validators'),
|
|
||||||
'errors' => [
|
|
||||||
'children' => $errors
|
|
||||||
],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function getErrorMessage(ConstraintViolationInterface $error): string
|
|
||||||
{
|
{
|
||||||
$locale = \Locale::getDefault();
|
$locale = \Locale::getDefault();
|
||||||
/** @var User $user */
|
/** @var User $user */
|
||||||
@@ -90,6 +72,24 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
|
|||||||
$locale = $user->getLanguage();
|
$locale = $user->getLanguage();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$errors = [];
|
||||||
|
|
||||||
|
/** @var ConstraintViolationInterface $error */
|
||||||
|
foreach (iterator_to_array($exception->getViolations()) as $error) {
|
||||||
|
$errors[$error->getPropertyPath()]['errors'][] = $this->getErrorMessage($error, $locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'code' => '400',
|
||||||
|
'message' => $this->translator->trans($exception->getMessage(), [], 'validators', $locale),
|
||||||
|
'errors' => [
|
||||||
|
'children' => $errors
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getErrorMessage(ConstraintViolationInterface $error, string $locale): string
|
||||||
|
{
|
||||||
if (null !== $error->getPlural()) {
|
if (null !== $error->getPlural()) {
|
||||||
return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getPlural()] + $error->getParameters(), 'validators', $locale);
|
return $this->translator->trans($error->getMessageTemplate(), ['%count%' => $error->getPlural()] + $error->getParameters(), 'validators', $locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ final class TimesheetController extends BaseApiController
|
|||||||
|
|
||||||
if ($form->isValid()) {
|
if ($form->isValid()) {
|
||||||
try {
|
try {
|
||||||
$this->service->saveNewTimesheet($timesheet);
|
$this->service->saveTimesheet($timesheet);
|
||||||
|
|
||||||
$view = new View($timesheet, 200);
|
$view = new View($timesheet, 200);
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ final class TimesheetController extends BaseApiController
|
|||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->service->updateTimesheet($timesheet);
|
$this->service->saveTimesheet($timesheet);
|
||||||
|
|
||||||
$view = new View($timesheet, Response::HTTP_OK);
|
$view = new View($timesheet, Response::HTTP_OK);
|
||||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||||
@@ -547,7 +547,7 @@ final class TimesheetController extends BaseApiController
|
|||||||
$copyTimesheet = clone $timesheet;
|
$copyTimesheet = clone $timesheet;
|
||||||
|
|
||||||
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
|
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
|
||||||
$this->service->saveNewTimesheet($copyTimesheet);
|
$this->service->saveTimesheet($copyTimesheet);
|
||||||
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
|
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
|
||||||
|
|
||||||
$view = new View($copyTimesheet, 200);
|
$view = new View($copyTimesheet, 200);
|
||||||
@@ -571,7 +571,7 @@ final class TimesheetController extends BaseApiController
|
|||||||
|
|
||||||
$timesheet->setExported(!$timesheet->isExported());
|
$timesheet->setExported(!$timesheet->isExported());
|
||||||
|
|
||||||
$this->service->updateTimesheet($timesheet);
|
$this->service->saveTimesheet($timesheet);
|
||||||
|
|
||||||
$view = new View($timesheet, 200);
|
$view = new View($timesheet, 200);
|
||||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||||
@@ -601,7 +601,7 @@ final class TimesheetController extends BaseApiController
|
|||||||
|
|
||||||
$meta->setValue($paramFetcher->get('value'));
|
$meta->setValue($paramFetcher->get('value'));
|
||||||
|
|
||||||
$this->service->updateTimesheet($timesheet);
|
$this->service->saveTimesheet($timesheet);
|
||||||
|
|
||||||
$view = new View($timesheet, 200);
|
$view = new View($timesheet, 200);
|
||||||
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
$view->getContext()->setGroups(self::GROUPS_ENTITY);
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class ActivityService
|
|||||||
$errors = $this->validator->validate($activity, null, $groups);
|
$errors = $this->validator->validate($activity, null, $groups);
|
||||||
|
|
||||||
if ($errors->count() > 0) {
|
if ($errors->count() > 0) {
|
||||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
throw new ValidationFailedException($errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ final class Constants
|
|||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
public const VERSION = '2.35.1';
|
public const VERSION = '2.36.0';
|
||||||
/**
|
/**
|
||||||
* The current release: major * 10000 + minor * 100 + patch
|
* The current release: major * 10000 + minor * 100 + patch
|
||||||
*/
|
*/
|
||||||
public const VERSION_ID = 23501;
|
public const VERSION_ID = 23600;
|
||||||
/**
|
/**
|
||||||
* The software name
|
* The software name
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
|
|
||||||
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
if ($editForm->isSubmitted() && $editForm->isValid()) {
|
||||||
try {
|
try {
|
||||||
$this->service->updateTimesheet($entry);
|
$this->service->saveTimesheet($entry);
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||||
@@ -184,7 +184,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
|
|
||||||
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||||
try {
|
try {
|
||||||
$this->service->saveNewTimesheet($entry);
|
$this->service->saveTimesheet($entry);
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
return $this->redirectToRoute($this->getTimesheetRoute());
|
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||||
@@ -216,7 +216,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
try {
|
try {
|
||||||
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
|
$this->dispatcher->dispatch(new TimesheetDuplicatePreEvent($copyTimesheet, $timesheet));
|
||||||
$this->service->saveNewTimesheet($copyTimesheet);
|
$this->service->saveTimesheet($copyTimesheet);
|
||||||
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
|
$this->dispatcher->dispatch(new TimesheetDuplicatePostEvent($copyTimesheet, $timesheet));
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ final class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($newTimesheets as $newTimesheet) {
|
foreach ($newTimesheets as $newTimesheet) {
|
||||||
$this->service->saveNewTimesheet($newTimesheet);
|
$this->service->saveTimesheet($newTimesheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ final class CustomerService
|
|||||||
$errors = $this->validator->validate($customer, null, $groups);
|
$errors = $this->validator->validate($customer, null, $groups);
|
||||||
|
|
||||||
if ($errors->count() > 0) {
|
if ($errors->count() > 0) {
|
||||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
throw new ValidationFailedException($errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -59,8 +59,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
$timezone = $options['timezone'];
|
$timezone = $options['timezone'];
|
||||||
$isNew = true;
|
$isNew = true;
|
||||||
|
|
||||||
if (isset($options['data'])) {
|
if (isset($options['data']) && $options['data'] instanceof Timesheet) {
|
||||||
/** @var Timesheet $entry */
|
|
||||||
$entry = $options['data'];
|
$entry = $options['data'];
|
||||||
|
|
||||||
$activity = $entry->getActivity();
|
$activity = $entry->getActivity();
|
||||||
@@ -338,7 +337,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
function (FormEvent $event) {
|
function (FormEvent $event) {
|
||||||
/** @var Timesheet|null $timesheet */
|
/** @var Timesheet|null $timesheet */
|
||||||
$timesheet = $event->getData();
|
$timesheet = $event->getData();
|
||||||
if (null === $timesheet || $timesheet->isRunning()) {
|
if (null === $timesheet || ($timesheet instanceof Timesheet && $timesheet->isRunning())) {
|
||||||
$event->getForm()->get('duration')->setData(null);
|
$event->getForm()->get('duration')->setData(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -218,9 +218,7 @@ final class InvoiceModel
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the user who is currently creating the invoice.
|
* Returns the user currently creating the invoice.
|
||||||
*
|
|
||||||
* @return User|null
|
|
||||||
*/
|
*/
|
||||||
public function getUser(): ?User
|
public function getUser(): ?User
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ final class ProjectService
|
|||||||
$errors = $this->validator->validate($project, null, $groups);
|
$errors = $this->validator->validate($project, null, $groups);
|
||||||
|
|
||||||
if ($errors->count() > 0) {
|
if ($errors->count() > 0) {
|
||||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
throw new ValidationFailedException($errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace App\Saml;
|
|||||||
use App\Configuration\SamlConfigurationInterface;
|
use App\Configuration\SamlConfigurationInterface;
|
||||||
use App\Saml\Security\SamlAuthenticationFailureHandler;
|
use App\Saml\Security\SamlAuthenticationFailureHandler;
|
||||||
use App\Saml\Security\SamlAuthenticationSuccessHandler;
|
use App\Saml\Security\SamlAuthenticationSuccessHandler;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
@@ -40,7 +41,8 @@ class SamlAuthenticator extends AbstractAuthenticator
|
|||||||
private readonly SamlAuthenticationFailureHandler $failureHandler,
|
private readonly SamlAuthenticationFailureHandler $failureHandler,
|
||||||
private readonly SamlAuthFactory $samlAuthFactory,
|
private readonly SamlAuthFactory $samlAuthFactory,
|
||||||
private readonly SamlProvider $samlProvider,
|
private readonly SamlProvider $samlProvider,
|
||||||
private readonly SamlConfigurationInterface $configuration
|
private readonly SamlConfigurationInterface $configuration,
|
||||||
|
private readonly LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,6 +88,7 @@ class SamlAuthenticator extends AbstractAuthenticator
|
|||||||
// file_put_contents(__DIR__ . '/../../var/log/saml.xml', $oneLoginAuth->getLastResponseXML());
|
// file_put_contents(__DIR__ . '/../../var/log/saml.xml', $oneLoginAuth->getLastResponseXML());
|
||||||
|
|
||||||
if (\count($oneLoginAuth->getErrors()) > 0) {
|
if (\count($oneLoginAuth->getErrors()) > 0) {
|
||||||
|
$this->logger->critical('SAML login failed: ' . $oneLoginAuth->getLastErrorReason());
|
||||||
throw new AuthenticationException($oneLoginAuth->getLastErrorReason());
|
throw new AuthenticationException($oneLoginAuth->getLastErrorReason());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,7 +105,9 @@ class SamlAuthenticator extends AbstractAuthenticator
|
|||||||
|
|
||||||
if (isset($this->options['username_attribute'])) {
|
if (isset($this->options['username_attribute'])) {
|
||||||
if (!\array_key_exists($this->options['username_attribute'], $attributes)) {
|
if (!\array_key_exists($this->options['username_attribute'], $attributes)) {
|
||||||
throw new \Exception(\sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute']));
|
$errorMessage = \sprintf("Attribute '%s' not found in SAML data", $this->options['username_attribute']);
|
||||||
|
$this->logger->critical($errorMessage);
|
||||||
|
throw new \Exception($errorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
$username = $attributes[$this->options['username_attribute']][0];
|
$username = $attributes[$this->options['username_attribute']][0];
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ final class TimesheetService
|
|||||||
private array $doNotValidateCodes = [];
|
private array $doNotValidateCodes = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private SystemConfiguration $configuration,
|
private readonly SystemConfiguration $configuration,
|
||||||
private TimesheetRepository $repository,
|
private readonly TimesheetRepository $repository,
|
||||||
private TrackingModeService $trackingModeService,
|
private readonly TrackingModeService $trackingModeService,
|
||||||
private EventDispatcherInterface $dispatcher,
|
private readonly EventDispatcherInterface $dispatcher,
|
||||||
private AuthorizationCheckerInterface $auth,
|
private readonly AuthorizationCheckerInterface $auth,
|
||||||
private ValidatorInterface $validator
|
private readonly ValidatorInterface $validator
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +101,7 @@ final class TimesheetService
|
|||||||
public function restartTimesheet(Timesheet $timesheet, Timesheet $copyFrom): Timesheet
|
public function restartTimesheet(Timesheet $timesheet, Timesheet $copyFrom): Timesheet
|
||||||
{
|
{
|
||||||
$this->dispatcher->dispatch(new TimesheetRestartPreEvent($timesheet, $copyFrom));
|
$this->dispatcher->dispatch(new TimesheetRestartPreEvent($timesheet, $copyFrom));
|
||||||
$this->saveNewTimesheet($timesheet);
|
$this->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
|
||||||
$this->dispatcher->dispatch(new TimesheetRestartPostEvent($timesheet, $copyFrom));
|
$this->dispatcher->dispatch(new TimesheetRestartPostEvent($timesheet, $copyFrom));
|
||||||
|
|
||||||
return $timesheet;
|
return $timesheet;
|
||||||
@@ -111,6 +111,7 @@ final class TimesheetService
|
|||||||
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
|
* @throws ValidationFailedException for invalid timesheets or running timesheets that should be stopped
|
||||||
* @throws InvalidArgumentException for already persisted timesheets
|
* @throws InvalidArgumentException for already persisted timesheets
|
||||||
* @throws AccessDeniedException if user is not allowed to start timesheet
|
* @throws AccessDeniedException if user is not allowed to start timesheet
|
||||||
|
* @deprecated since 2.36.0 - use saveTimesheet() instead
|
||||||
*/
|
*/
|
||||||
public function saveNewTimesheet(Timesheet $timesheet): Timesheet
|
public function saveNewTimesheet(Timesheet $timesheet): Timesheet
|
||||||
{
|
{
|
||||||
@@ -150,12 +151,19 @@ final class TimesheetService
|
|||||||
return $timesheet;
|
return $timesheet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function saveTimesheet(Timesheet $timesheet): Timesheet
|
||||||
|
{
|
||||||
|
if ($timesheet->getId() === null) {
|
||||||
|
return $this->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
|
||||||
|
} else {
|
||||||
|
return $this->updateTimesheet($timesheet); // @phpstan-ignore method.deprecated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does NOT validate the given timesheet!
|
* Does NOT validate the given timesheet.
|
||||||
*
|
*
|
||||||
* @param Timesheet $timesheet
|
* @deprecated since 2.36.0 - use saveTimesheet() instead
|
||||||
* @return Timesheet
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
*/
|
||||||
public function updateTimesheet(Timesheet $timesheet): Timesheet
|
public function updateTimesheet(Timesheet $timesheet): Timesheet
|
||||||
{
|
{
|
||||||
@@ -245,7 +253,7 @@ final class TimesheetService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
throw new ValidationFailedException($errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ class UserService
|
|||||||
$errors = $this->validator->validate($user, null, $groups);
|
$errors = $this->validator->validate($user, null, $groups);
|
||||||
|
|
||||||
if ($errors->count() > 0) {
|
if ($errors->count() > 0) {
|
||||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
throw new ValidationFailedException($errors);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
26
src/Validator/Constraints/TimesheetNegativeDuration.php
Normal file
26
src/Validator/Constraints/TimesheetNegativeDuration.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 TimesheetNegativeDuration extends TimesheetConstraint
|
||||||
|
{
|
||||||
|
public const NEGATIVE_DURATION_ERROR = 'kimai-timesheet-negative-duration-01';
|
||||||
|
|
||||||
|
protected const ERROR_NAMES = [
|
||||||
|
self::NEGATIVE_DURATION_ERROR => 'Duration cannot be negative.',
|
||||||
|
];
|
||||||
|
|
||||||
|
public string $message = 'Duration cannot be negative.';
|
||||||
|
|
||||||
|
public function getTargets(): string
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?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\Entity\Timesheet as TimesheetEntity;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
|
||||||
|
final class TimesheetNegativeDurationValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
public function validate(mixed $value, Constraint $constraint): void
|
||||||
|
{
|
||||||
|
if (!($constraint instanceof TimesheetNegativeDuration)) {
|
||||||
|
throw new UnexpectedTypeException($constraint, TimesheetNegativeDuration::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||||
|
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value->isRunning()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$duration = $value->getCalculatedDuration();
|
||||||
|
|
||||||
|
if ($duration !== null && $duration < 0) {
|
||||||
|
$this->context->buildViolation($constraint->message)
|
||||||
|
->atPath('duration')
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode(TimesheetNegativeDuration::NEGATIVE_DURATION_ERROR)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,7 +44,7 @@ final class TimesheetZeroDurationValidator extends ConstraintValidator
|
|||||||
$duration = $value->getCalculatedDuration();
|
$duration = $value->getCalculatedDuration();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($duration <= 0) {
|
if ($duration === 0) {
|
||||||
$this->context->buildViolation($constraint->message)
|
$this->context->buildViolation($constraint->message)
|
||||||
->atPath('duration')
|
->atPath('duration')
|
||||||
->setTranslationDomain('validators')
|
->setTranslationDomain('validators')
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ final class ValidationException extends \RuntimeException
|
|||||||
public function __construct(string $message = null)
|
public function __construct(string $message = null)
|
||||||
{
|
{
|
||||||
if ($message === null) {
|
if ($message === null) {
|
||||||
$message = 'Validation failed';
|
$message = 'Validation Failed';
|
||||||
}
|
}
|
||||||
parent::__construct($message, 400);
|
parent::__construct($message, 400);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ use Symfony\Component\Validator\ConstraintViolationListInterface;
|
|||||||
|
|
||||||
final class ValidationFailedException extends \RuntimeException
|
final class ValidationFailedException extends \RuntimeException
|
||||||
{
|
{
|
||||||
public function __construct(private ConstraintViolationListInterface $violations, ?string $message = null)
|
public function __construct(private readonly ConstraintViolationListInterface $violations, ?string $message = null)
|
||||||
{
|
{
|
||||||
if ($message === null) {
|
if ($message === null) {
|
||||||
$message = 'Validation failed';
|
$message = 'Validation Failed';
|
||||||
}
|
}
|
||||||
parent::__construct($message, 400);
|
parent::__construct($message, 400);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -251,7 +251,7 @@ class TimesheetValidationTest extends KernelTestCase
|
|||||||
$entity->setBegin($begin);
|
$entity->setBegin($begin);
|
||||||
$entity->setEnd($end);
|
$entity->setEnd($end);
|
||||||
|
|
||||||
$this->assertHasViolationForField($entity, 'end_date');
|
$this->assertHasViolationForField($entity, ['end_date', 'duration']);
|
||||||
|
|
||||||
// allow same begin and end
|
// allow same begin and end
|
||||||
$entity = $this->getEntity();
|
$entity = $this->getEntity();
|
||||||
|
|||||||
@@ -65,6 +65,9 @@ class TimesheetServiceTest extends TestCase
|
|||||||
return $service;
|
return $service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
public function testCannotSavePersistedTimesheetAsNew(): void
|
public function testCannotSavePersistedTimesheetAsNew(): void
|
||||||
{
|
{
|
||||||
$timesheet = $this->createMock(Timesheet::class);
|
$timesheet = $this->createMock(Timesheet::class);
|
||||||
@@ -75,7 +78,7 @@ class TimesheetServiceTest extends TestCase
|
|||||||
$this->expectException(\InvalidArgumentException::class);
|
$this->expectException(\InvalidArgumentException::class);
|
||||||
$this->expectExceptionMessage('Cannot create timesheet, already persisted');
|
$this->expectExceptionMessage('Cannot create timesheet, already persisted');
|
||||||
|
|
||||||
$sut->saveNewTimesheet($timesheet);
|
$sut->saveNewTimesheet($timesheet); // @phpstan-ignore method.deprecated
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCannotStartTimesheet(): void
|
public function testCannotStartTimesheet(): void
|
||||||
@@ -88,7 +91,7 @@ class TimesheetServiceTest extends TestCase
|
|||||||
$this->expectException(AccessDeniedHttpException::class);
|
$this->expectException(AccessDeniedHttpException::class);
|
||||||
$this->expectExceptionMessage('You are not allowed to start this timesheet record');
|
$this->expectExceptionMessage('You are not allowed to start this timesheet record');
|
||||||
|
|
||||||
$sut->saveNewTimesheet(new Timesheet());
|
$sut->saveTimesheet(new Timesheet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSaveNewTimesheetHasValidationError(): void
|
public function testSaveNewTimesheetHasValidationError(): void
|
||||||
@@ -107,7 +110,7 @@ class TimesheetServiceTest extends TestCase
|
|||||||
$this->expectException(ValidationFailedException::class);
|
$this->expectException(ValidationFailedException::class);
|
||||||
$this->expectExceptionMessage('Validation Failed');
|
$this->expectExceptionMessage('Validation Failed');
|
||||||
|
|
||||||
$sut->saveNewTimesheet(new Timesheet());
|
$sut->saveTimesheet(new Timesheet());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSaveNewTimesheetStopsActiveRecords(): void
|
public function testSaveNewTimesheetStopsActiveRecords(): void
|
||||||
@@ -134,7 +137,7 @@ class TimesheetServiceTest extends TestCase
|
|||||||
|
|
||||||
$sut = $this->getSut($authorizationChecker, null, null, $repository);
|
$sut = $this->getSut($authorizationChecker, null, null, $repository);
|
||||||
|
|
||||||
$sut->saveNewTimesheet($newTimesheet);
|
$sut->saveTimesheet($newTimesheet);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testSaveNewTimesheetFixesTimezone(): void
|
public function testSaveNewTimesheetFixesTimezone(): void
|
||||||
@@ -155,11 +158,14 @@ class TimesheetServiceTest extends TestCase
|
|||||||
$authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true);
|
$authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true);
|
||||||
$sut = $this->getSut($authorizationChecker);
|
$sut = $this->getSut($authorizationChecker);
|
||||||
|
|
||||||
$sut->saveNewTimesheet($timesheet);
|
$sut->saveTimesheet($timesheet);
|
||||||
|
|
||||||
self::assertEquals('Europe/Paris', $timesheet->getTimezone());
|
self::assertEquals('Europe/Paris', $timesheet->getTimezone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
public function testUpdateTimesheetFixesTimezone(): void
|
public function testUpdateTimesheetFixesTimezone(): void
|
||||||
{
|
{
|
||||||
$user = new User();
|
$user = new User();
|
||||||
@@ -176,7 +182,7 @@ class TimesheetServiceTest extends TestCase
|
|||||||
|
|
||||||
$sut = $this->getSut();
|
$sut = $this->getSut();
|
||||||
|
|
||||||
$sut->updateTimesheet($timesheet);
|
$sut->updateTimesheet($timesheet); // @phpstan-ignore method.deprecated
|
||||||
|
|
||||||
self::assertEquals('Europe/Paris', $timesheet->getTimezone());
|
self::assertEquals('Europe/Paris', $timesheet->getTimezone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?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\Validator\Constraints\TimesheetConstraint;
|
||||||
|
use App\Validator\Constraints\TimesheetNegativeDuration;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Validator\Constraints\TimesheetNegativeDuration
|
||||||
|
*/
|
||||||
|
class TimesheetNegativeDurationTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testIsTimesheetConstraint(): void
|
||||||
|
{
|
||||||
|
self::assertInstanceOf(TimesheetConstraint::class, new TimesheetNegativeDuration());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<?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\Entity\Timesheet;
|
||||||
|
use App\Validator\Constraints\TimesheetNegativeDuration;
|
||||||
|
use App\Validator\Constraints\TimesheetNegativeDurationValidator;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Validator\Constraints\TimesheetNegativeDuration
|
||||||
|
* @covers \App\Validator\Constraints\TimesheetNegativeDurationValidator
|
||||||
|
* @extends ConstraintValidatorTestCase<TimesheetNegativeDurationValidator>
|
||||||
|
*/
|
||||||
|
class TimesheetNegativeDurationValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
protected function createValidator(): TimesheetNegativeDurationValidator
|
||||||
|
{
|
||||||
|
return new TimesheetNegativeDurationValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstraintIsInvalid(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UnexpectedTypeException::class);
|
||||||
|
|
||||||
|
$this->validator->validate(new Timesheet(), new NotBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidValueThrowsException(): void
|
||||||
|
{
|
||||||
|
$this->expectException(UnexpectedTypeException::class);
|
||||||
|
|
||||||
|
$this->validator->validate(new NotBlank(), new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNegativeDurationIsNotAllowed(): void
|
||||||
|
{
|
||||||
|
$begin = new \DateTime();
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin(clone $begin);
|
||||||
|
$timesheet->setEnd(clone $begin);
|
||||||
|
$timesheet->setBreak(3600);
|
||||||
|
|
||||||
|
$this->validator->validate($timesheet, new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
|
||||||
|
|
||||||
|
$this->buildViolation('Duration cannot be negative.')
|
||||||
|
->atPath('property.path.duration')
|
||||||
|
->setCode(TimesheetNegativeDuration::NEGATIVE_DURATION_ERROR)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testZeroDurationIsAllowed(): void
|
||||||
|
{
|
||||||
|
$begin = new \DateTime();
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin(clone $begin);
|
||||||
|
$timesheet->setEnd(clone $begin);
|
||||||
|
|
||||||
|
$this->validator = $this->createValidator();
|
||||||
|
$this->validator->initialize($this->context);
|
||||||
|
|
||||||
|
$this->validator->validate($timesheet, new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDoesNotTriggerOnRunningTimesheet(): void
|
||||||
|
{
|
||||||
|
$begin = new \DateTime();
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet->setBegin(clone $begin);
|
||||||
|
$timesheet->setBreak(3600);
|
||||||
|
|
||||||
|
$this->validator = $this->createValidator();
|
||||||
|
$this->validator->initialize($this->context);
|
||||||
|
|
||||||
|
$this->validator->validate($timesheet, new TimesheetNegativeDuration(['message' => 'Duration cannot be negative.']));
|
||||||
|
|
||||||
|
$this->assertNoViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ class ValidationExceptionTest extends TestCase
|
|||||||
{
|
{
|
||||||
$sut = new ValidationException();
|
$sut = new ValidationException();
|
||||||
self::assertEquals(400, $sut->getCode());
|
self::assertEquals(400, $sut->getCode());
|
||||||
self::assertEquals('Validation failed', $sut->getMessage());
|
self::assertEquals('Validation Failed', $sut->getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testConstruct(): void
|
public function testConstruct(): void
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class ValidationFailedExceptionTest extends TestCase
|
|||||||
$list = new ConstraintViolationList();
|
$list = new ConstraintViolationList();
|
||||||
$sut = new ValidationFailedException($list);
|
$sut = new ValidationFailedException($list);
|
||||||
self::assertEquals(400, $sut->getCode());
|
self::assertEquals(400, $sut->getCode());
|
||||||
self::assertEquals('Validation failed', $sut->getMessage());
|
self::assertEquals('Validation Failed', $sut->getMessage());
|
||||||
self::assertSame($list, $sut->getViolations());
|
self::assertSame($list, $sut->getViolations());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,10 @@
|
|||||||
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
|
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
|
||||||
<target>Ausgewählter Zeitraum kann nicht gesperrt werden: unbestätigte Abwesenheitsanträge stehen an.</target>
|
<target>Ausgewählter Zeitraum kann nicht gesperrt werden: unbestätigte Abwesenheitsanträge stehen an.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="KVyAzBQ" resname="Duration cannot be negative.">
|
||||||
|
<source>Duration cannot be negative.</source>
|
||||||
|
<target>Die Dauer kann nicht negativ sein.</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -162,6 +162,10 @@
|
|||||||
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
|
<source>Selected period cannot be locked: unconfirmed absence requests are pending.</source>
|
||||||
<target>Selected period cannot be locked: unconfirmed absence requests are pending.</target>
|
<target>Selected period cannot be locked: unconfirmed absence requests are pending.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="KVyAzBQ" resname="Duration cannot be negative.">
|
||||||
|
<source>Duration cannot be negative.</source>
|
||||||
|
<target>Duration cannot be negative.</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user