diff --git a/phpstan.neon b/phpstan.neon index 638ea20a..6de0f7b6 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6750,11 +6750,6 @@ parameters: count: 2 path: src/Validator/Constraints/TeamValidator.php - - - message: "#^Cannot call method isVisible\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" - count: 1 - path: src/Validator/Constraints/TimesheetBasicValidator.php - - message: "#^Cannot call method format\\(\\) on DateTime\\|null\\.$#" count: 2 diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index 2f6b74fd..ec6ea75f 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -45,6 +45,7 @@ use Symfony\Component\Validator\Constraints as Assert; #[Serializer\VirtualProperty('UserAsId', exp: 'object.getUser().getId()', options: [new Serializer\SerializedName('user'), new Serializer\Type(name: 'integer'), new Serializer\Groups(['Not_Expanded'])])] #[Serializer\VirtualProperty('TagsAsArray', exp: 'object.getTagsAsArray()', options: [new Serializer\SerializedName('tags'), new Serializer\Type(name: 'array'), new Serializer\Groups(['Default'])])] #[Constraints\Timesheet] +#[Constraints\TimesheetDeactivated] class Timesheet implements EntityWithMetaFields, ExportableItem { /** diff --git a/src/Validator/Constraints/TimesheetBasic.php b/src/Validator/Constraints/TimesheetBasic.php index b5fccbf8..8821e093 100644 --- a/src/Validator/Constraints/TimesheetBasic.php +++ b/src/Validator/Constraints/TimesheetBasic.php @@ -16,9 +16,6 @@ final class TimesheetBasic extends TimesheetConstraint public const MISSING_ACTIVITY_ERROR = 'kimai-timesheet-84'; public const MISSING_PROJECT_ERROR = 'kimai-timesheet-85'; public const ACTIVITY_PROJECT_MISMATCH_ERROR = 'kimai-timesheet-86'; - public const DISABLED_ACTIVITY_ERROR = 'kimai-timesheet-87'; - public const DISABLED_PROJECT_ERROR = 'kimai-timesheet-88'; - public const DISABLED_CUSTOMER_ERROR = 'kimai-timesheet-89'; public const PROJECT_NOT_STARTED = 'kimai-timesheet-91'; public const PROJECT_ALREADY_ENDED = 'kimai-timesheet-92'; public const PROJECT_DISALLOWS_GLOBAL_ACTIVITY = 'kimai-timesheet-93'; @@ -29,9 +26,6 @@ final class TimesheetBasic extends TimesheetConstraint self::MISSING_ACTIVITY_ERROR => 'An activity needs to be selected.', self::MISSING_PROJECT_ERROR => 'A project needs to be selected.', self::ACTIVITY_PROJECT_MISMATCH_ERROR => 'Project mismatch, project specific activity and timesheet project are different.', - self::DISABLED_ACTIVITY_ERROR => 'Cannot start a disabled activity.', - self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.', - self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.', self::PROJECT_NOT_STARTED => 'The project has not started at that time.', self::PROJECT_ALREADY_ENDED => 'The project is finished at that time.', self::PROJECT_DISALLOWS_GLOBAL_ACTIVITY => 'Global activities are forbidden for the selected project.', diff --git a/src/Validator/Constraints/TimesheetBasicValidator.php b/src/Validator/Constraints/TimesheetBasicValidator.php index ffeb0afc..199ac84e 100644 --- a/src/Validator/Constraints/TimesheetBasicValidator.php +++ b/src/Validator/Constraints/TimesheetBasicValidator.php @@ -102,33 +102,6 @@ final class TimesheetBasicValidator extends ConstraintValidator ->addViolation(); } - $timesheetEnd = $timesheet->getEnd(); - $newOrStarted = null === $timesheetEnd || $timesheet->getId() === null; - - if ($newOrStarted && $hasActivity && !$activity->isVisible()) { - $context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::DISABLED_ACTIVITY_ERROR)) - ->atPath('activity') - ->setTranslationDomain('validators') - ->setCode(TimesheetBasic::DISABLED_ACTIVITY_ERROR) - ->addViolation(); - } - - if ($newOrStarted && !$project->isVisible()) { - $context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::DISABLED_PROJECT_ERROR)) - ->atPath('project') - ->setTranslationDomain('validators') - ->setCode(TimesheetBasic::DISABLED_PROJECT_ERROR) - ->addViolation(); - } - - if ($newOrStarted && !$project->getCustomer()->isVisible()) { - $context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::DISABLED_CUSTOMER_ERROR)) - ->atPath('customer') - ->setTranslationDomain('validators') - ->setCode(TimesheetBasic::DISABLED_CUSTOMER_ERROR) - ->addViolation(); - } - if ($hasActivity && !$project->isGlobalActivities() && $activity->isGlobal()) { $context->buildViolation(TimesheetBasic::getErrorName(TimesheetBasic::PROJECT_DISALLOWS_GLOBAL_ACTIVITY)) ->atPath('activity') diff --git a/src/Validator/Constraints/TimesheetDeactivated.php b/src/Validator/Constraints/TimesheetDeactivated.php new file mode 100644 index 00000000..7fbdc714 --- /dev/null +++ b/src/Validator/Constraints/TimesheetDeactivated.php @@ -0,0 +1,33 @@ + 'Cannot start a disabled activity.', + self::DISABLED_PROJECT_ERROR => 'Cannot start a disabled project.', + self::DISABLED_CUSTOMER_ERROR => 'Cannot start a disabled customer.', + ]; + + public string $message = 'This timesheet has invalid settings.'; + + public function getTargets(): string|array + { + return self::CLASS_CONSTRAINT; + } +} diff --git a/src/Validator/Constraints/TimesheetDeactivatedValidator.php b/src/Validator/Constraints/TimesheetDeactivatedValidator.php new file mode 100644 index 00000000..574c3380 --- /dev/null +++ b/src/Validator/Constraints/TimesheetDeactivatedValidator.php @@ -0,0 +1,84 @@ +validateActivityAndProject($value, $this->context); + } + + /** + * @param TimesheetEntity $timesheet + * @param ExecutionContextInterface $context + */ + protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context): void + { + $timesheetEnd = $timesheet->getEnd(); + $newOrStarted = null === $timesheetEnd || $timesheet->getId() === null; + + if (!$newOrStarted) { + return; + } + + $activity = $timesheet->getActivity(); + if (null !== $activity && !$activity->isVisible()) { + $context->buildViolation(TimesheetDeactivated::getErrorName(TimesheetDeactivated::DISABLED_ACTIVITY_ERROR)) + ->atPath('activity') + ->setTranslationDomain('validators') + ->setCode(TimesheetDeactivated::DISABLED_ACTIVITY_ERROR) + ->addViolation(); + } + + $project = $timesheet->getProject(); + if ($project === null) { + return; + } + + if (!$project->isVisible()) { + $context->buildViolation(TimesheetDeactivated::getErrorName(TimesheetDeactivated::DISABLED_PROJECT_ERROR)) + ->atPath('project') + ->setTranslationDomain('validators') + ->setCode(TimesheetDeactivated::DISABLED_PROJECT_ERROR) + ->addViolation(); + } + + $customer = $project->getCustomer(); + if ($customer === null) { + return; + } + + if (!$customer->isVisible()) { + $context->buildViolation(TimesheetDeactivated::getErrorName(TimesheetDeactivated::DISABLED_CUSTOMER_ERROR)) + ->atPath('customer') + ->setTranslationDomain('validators') + ->setCode(TimesheetDeactivated::DISABLED_CUSTOMER_ERROR) + ->addViolation(); + } + } +} diff --git a/src/Validator/Constraints/TimesheetValidator.php b/src/Validator/Constraints/TimesheetValidator.php index 172e2966..a76b114b 100644 --- a/src/Validator/Constraints/TimesheetValidator.php +++ b/src/Validator/Constraints/TimesheetValidator.php @@ -38,11 +38,16 @@ final class TimesheetValidator extends ConstraintValidator throw new UnexpectedTypeException($timesheet, TimesheetEntity::class); } + $groups = [Constraint::DEFAULT_GROUP]; + if ($this->context->getGroup() !== null) { + $groups = [$this->context->getGroup()]; + } + foreach ($this->constraints as $innerConstraint) { $this->context ->getValidator() ->inContext($this->context) - ->validate($timesheet, $innerConstraint, [Constraint::DEFAULT_GROUP]); + ->validate($timesheet, $innerConstraint, $groups); } } } diff --git a/tests/Entity/TimesheetValidationTest.php b/tests/Entity/TimesheetValidationTest.php index dd3685f5..ef19d2e9 100644 --- a/tests/Entity/TimesheetValidationTest.php +++ b/tests/Entity/TimesheetValidationTest.php @@ -99,6 +99,7 @@ class TimesheetValidationTest extends KernelTestCase $entity->setActivity($activity); $entity->setProject($project); $entity->setBegin(new \DateTime()); + $entity->setEnd(new \DateTime()); $this->assertHasViolationForField($entity, 'customer'); } @@ -166,6 +167,7 @@ class TimesheetValidationTest extends KernelTestCase $entity->setActivity($activity); $entity->setProject($project); $entity->setBegin(new \DateTime()); + $entity->setEnd(new \DateTime()); $this->assertHasViolationForField($entity, 'project'); } @@ -203,6 +205,7 @@ class TimesheetValidationTest extends KernelTestCase $entity->setActivity($activity); $entity->setProject($project); $entity->setBegin(new \DateTime()); + $entity->setEnd(new \DateTime()); $this->assertHasViolationForField($entity, 'activity'); } diff --git a/tests/Validator/Constraints/TimesheetBasicValidatorTest.php b/tests/Validator/Constraints/TimesheetBasicValidatorTest.php index e411e950..0b3bcc49 100644 --- a/tests/Validator/Constraints/TimesheetBasicValidatorTest.php +++ b/tests/Validator/Constraints/TimesheetBasicValidatorTest.php @@ -143,39 +143,6 @@ class TimesheetBasicValidatorTest extends ConstraintValidatorTestCase ->assertRaised(); } - public function testDisabledValuesDuringStart() - { - $begin = new \DateTime('-10 hour'); - $customer = new Customer('foo'); - $customer->setVisible(false); - $activity = new Activity(); - $activity->setVisible(false); - $project = new Project(); - $project->setVisible(false); - $project->setCustomer($customer); - $activity->setProject($project); - - $timesheet = new Timesheet(); - $timesheet - ->setBegin($begin) - ->setActivity($activity) - ->setProject($project) - ; - - $this->validator->validate($timesheet, new TimesheetBasic(['message' => 'myMessage'])); - - $this->buildViolation('Cannot start a disabled activity.') - ->atPath('property.path.activity') - ->setCode(TimesheetBasic::DISABLED_ACTIVITY_ERROR) - ->buildNextViolation('Cannot start a disabled project.') - ->atPath('property.path.project') - ->setCode(TimesheetBasic::DISABLED_PROJECT_ERROR) - ->buildNextViolation('Cannot start a disabled customer.') - ->atPath('property.path.customer') - ->setCode(TimesheetBasic::DISABLED_CUSTOMER_ERROR) - ->assertRaised(); - } - public function getProjectStartEndTestData() { yield [new \DateTime(), new \DateTime(), [ diff --git a/tests/Validator/Constraints/TimesheetDeactivatedValidatorTest.php b/tests/Validator/Constraints/TimesheetDeactivatedValidatorTest.php new file mode 100644 index 00000000..47ab05c7 --- /dev/null +++ b/tests/Validator/Constraints/TimesheetDeactivatedValidatorTest.php @@ -0,0 +1,86 @@ + + */ +class TimesheetDeactivatedValidatorTest extends ConstraintValidatorTestCase +{ + protected function createValidator(): TimesheetDeactivatedValidator + { + return new TimesheetDeactivatedValidator(); + } + + 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 TimesheetDeactivated(['message' => 'myMessage'])); + } + + public function testDisabledValuesDuringStart(): void + { + $begin = new \DateTime('-10 hour'); + $customer = new Customer('foo'); + $customer->setVisible(false); + $activity = new Activity(); + $activity->setVisible(false); + $project = new Project(); + $project->setVisible(false); + $project->setCustomer($customer); + $activity->setProject($project); + + $timesheet = new Timesheet(); + $timesheet + ->setBegin($begin) + ->setActivity($activity) + ->setProject($project) + ; + + $this->validator->validate($timesheet, new TimesheetDeactivated(['message' => 'myMessage'])); + + $this->buildViolation(TimesheetDeactivated::getErrorName(TimesheetDeactivated::DISABLED_ACTIVITY_ERROR)) + ->atPath('property.path.activity') + ->setCode(TimesheetDeactivated::DISABLED_ACTIVITY_ERROR) + ->buildNextViolation(TimesheetDeactivated::getErrorName(TimesheetDeactivated::DISABLED_PROJECT_ERROR)) + ->atPath('property.path.project') + ->setCode(TimesheetDeactivated::DISABLED_PROJECT_ERROR) + ->buildNextViolation(TimesheetDeactivated::getErrorName(TimesheetDeactivated::DISABLED_CUSTOMER_ERROR)) + ->atPath('property.path.customer') + ->setCode(TimesheetDeactivated::DISABLED_CUSTOMER_ERROR) + ->assertRaised(); + } + + public function testGetTargets(): void + { + $constraint = new TimesheetDeactivated(); + self::assertEquals('class', $constraint->getTargets()); + } +} diff --git a/tests/phpstan.neon b/tests/phpstan.neon index e1f48bf9..8f98348b 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -10522,11 +10522,6 @@ parameters: count: 1 path: Validator/Constraints/TimesheetBasicValidatorTest.php - - - message: "#^Method App\\\\Tests\\\\Validator\\\\Constraints\\\\TimesheetBasicValidatorTest\\:\\:testDisabledValuesDuringStart\\(\\) has no return type specified\\.$#" - count: 1 - path: Validator/Constraints/TimesheetBasicValidatorTest.php - - message: "#^Method App\\\\Tests\\\\Validator\\\\Constraints\\\\TimesheetBasicValidatorTest\\:\\:testEmptyTimesheet\\(\\) has no return type specified\\.$#" count: 1