Release 2.13 (#4659)
This commit is contained in:
2
.github/release-drafter.yml
vendored
2
.github/release-drafter.yml
vendored
@@ -21,7 +21,7 @@ version-resolver:
|
||||
- 'translation'
|
||||
default: patch
|
||||
template: |
|
||||
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://tobybatch.github.io/kimai2/)
|
||||
[Upgrade Kimai](https://www.kimai.org/documentation/updates.html) - [Install Kimai](https://www.kimai.org/documentation/installation.html) - [Docker](https://www.kimai.org/documentation/docker.html)
|
||||
|
||||
**Compatible with PHP 8.1 to 8.3**
|
||||
|
||||
|
||||
2
.github/workflows/testing.yaml
vendored
2
.github/workflows/testing.yaml
vendored
@@ -106,7 +106,7 @@ jobs:
|
||||
|
||||
- name: Upload code coverage
|
||||
if: matrix.php == '8.2'
|
||||
uses: codecov/codecov-action@v3
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ./coverage.xml
|
||||
|
||||
@@ -47,8 +47,8 @@ Support for PHP 8.2 and 8.3.
|
||||
|
||||
## Installation
|
||||
|
||||
- [Recommended setup](https://www.kimai.org/documentation/installation.html#recommended-setup) — with Git and Composer
|
||||
- [Docker](https://hub.docker.com/r/kimai/kimai2) — containerized by [@tobybatch](https://github.com/tobybatch/kimai2)
|
||||
- [Native setup](https://www.kimai.org/documentation/installation.html) — with Git and Composer
|
||||
- [Docker](https://hub.docker.com/r/kimai/kimai2) — containerized by @tobybatch
|
||||
|
||||
There are also documentations for:
|
||||
- [developer setups](https://www.kimai.org/documentation/developers.html) — on your local machine
|
||||
|
||||
@@ -10,7 +10,7 @@ Perform EACH version specific task between your version and the new one, otherwi
|
||||
|
||||
## [2.0.30](https://github.com/kimai/kimai/releases/tag/2.0.30)
|
||||
|
||||
The `DATABASE_URL` in your environment settings ([.env](https://github.com/kimai/kimai/issues/4246) or in your [Docker](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
The `DATABASE_URL` in your environment settings (e.g. [.env](https://github.com/kimai/kimai/issues/4246), [docker-compose.yaml](https://github.com/tobybatch/kimai2/issues/531) or webserver config)
|
||||
now requires the `charset` and `serverVersion` params, e.g.: `DATABASE_URL=mysql://user:password@127.0.0.1:3306/database?charset=utf8mb4&serverVersion=10.5.8-MariaDB` (examples in `.env`).
|
||||
|
||||
## [2.0](https://github.com/kimai/kimai/releases/tag/2.0)
|
||||
|
||||
812
composer.lock
generated
812
composer.lock
generated
File diff suppressed because it is too large
Load Diff
60
phpstan.neon
60
phpstan.neon
@@ -1107,11 +1107,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Controller/Security/PasswordResetController.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$usernameOrEmail of method App\\\\User\\\\UserService\\:\\:findUserByUsernameOrEmail\\(\\) expects string, bool\\|float\\|int\\|string\\|null given\\.$#"
|
||||
count: 1
|
||||
path: src/Controller/Security/PasswordResetController.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Controller\\\\Security\\\\SelfRegistrationController\\:\\:getTargetUrlFromSession\\(\\) should return string\\|null but returns mixed\\.$#"
|
||||
count: 1
|
||||
@@ -4812,11 +4807,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Saml/SamlLoginAttributes.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Saml\\\\SamlLogoutSubscriber\\:\\:logout\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Saml/SamlLogoutSubscriber.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#4 \\$sessionIndex of method OneLogin\\\\Saml2\\\\Auth\\:\\:logout\\(\\) expects string\\|null, mixed given\\.$#"
|
||||
count: 1
|
||||
@@ -4827,11 +4817,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Saml/SamlProvider.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Saml\\\\SamlProvider\\:\\:getPropertyValue\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: src/Saml/SamlProvider.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Saml\\\\SamlProvider\\:\\:getPropertyValue\\(\\) has parameter \\$attribute with no type specified\\.$#"
|
||||
count: 1
|
||||
@@ -5557,11 +5542,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetBudgetUsedValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#4 \\$duration of method App\\\\Validator\\\\Constraints\\\\TimesheetBudgetUsedValidator\\:\\:checkBudgets\\(\\) expects int, float\\|int\\|null given\\.$#"
|
||||
count: 3
|
||||
@@ -5572,31 +5552,11 @@ parameters:
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetBudgetUsedValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetExportedValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetExportedValidator.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method getTimezone\\(\\) on DateTime\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Validator/Constraints/TimesheetFutureTimesValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$value \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetFutureTimesValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetFutureTimesValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetLockdownValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetLockdownValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetLongRunningValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetLongRunningValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#2 \\$value of method Symfony\\\\Component\\\\Validator\\\\Violation\\\\ConstraintViolationBuilderInterface\\:\\:setParameter\\(\\) expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
@@ -5607,26 +5567,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Validator/Constraints/TimesheetMultiUpdateValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetOverlappingValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetOverlappingValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetRestartValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetRestartValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timesheet \\(App\\\\Entity\\\\Timesheet\\) of method App\\\\Validator\\\\Constraints\\\\TimesheetZeroDurationValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
path: src/Validator/Constraints/TimesheetZeroDurationValidator.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$value \\(App\\\\Entity\\\\User\\) of method App\\\\Validator\\\\Constraints\\\\UserValidator\\:\\:validate\\(\\) should be contravariant with parameter \\$value \\(mixed\\) of method Symfony\\\\Component\\\\Validator\\\\ConstraintValidatorInterface\\:\\:validate\\(\\)$#"
|
||||
count: 2
|
||||
|
||||
@@ -44,10 +44,10 @@ final class ActivityController extends BaseApiController
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Activity_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private ActivityRepository $repository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private ActivityRateRepository $activityRateRepository
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly ActivityRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ActivityRateRepository $activityRateRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -128,6 +128,7 @@ final class ActivityController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_activity', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[IsGranted('view', 'activity')]
|
||||
public function getAction(Activity $activity): Response
|
||||
{
|
||||
$view = new View($activity, 200);
|
||||
|
||||
@@ -44,10 +44,10 @@ final class CustomerController extends BaseApiController
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Customer_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private CustomerRepository $repository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private CustomerRateRepository $customerRateRepository
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly CustomerRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly CustomerRateRepository $customerRateRepository
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -105,6 +105,7 @@ final class CustomerController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_customer', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[IsGranted('view', 'customer')]
|
||||
public function getAction(Customer $customer): Response
|
||||
{
|
||||
$view = new View($customer, 200);
|
||||
|
||||
@@ -46,11 +46,11 @@ final class ProjectController extends BaseApiController
|
||||
public const GROUPS_RATE = ['Default', 'Entity', 'Project_Rate'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private ProjectRepository $repository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private ProjectRateRepository $projectRateRepository,
|
||||
private ProjectService $projectService
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly ProjectRepository $repository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly ProjectRateRepository $projectRateRepository,
|
||||
private readonly ProjectService $projectService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -159,6 +159,7 @@ final class ProjectController extends BaseApiController
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_project', requirements: ['id' => '\d+'])]
|
||||
#[ApiSecurity(name: 'apiUser')]
|
||||
#[ApiSecurity(name: 'apiToken')]
|
||||
#[IsGranted('view', 'project')]
|
||||
public function getAction(Project $project): Response
|
||||
{
|
||||
$view = new View($project, 200);
|
||||
|
||||
@@ -54,11 +54,11 @@ final class TimesheetController extends BaseApiController
|
||||
public const GROUPS_COLLECTION_FULL = ['Default', 'Collection', 'Timesheet', 'Expanded'];
|
||||
|
||||
public function __construct(
|
||||
private ViewHandlerInterface $viewHandler,
|
||||
private TimesheetRepository $repository,
|
||||
private TagRepository $tagRepository,
|
||||
private EventDispatcherInterface $dispatcher,
|
||||
private TimesheetService $service
|
||||
private readonly ViewHandlerInterface $viewHandler,
|
||||
private readonly TimesheetRepository $repository,
|
||||
private readonly TagRepository $tagRepository,
|
||||
private readonly EventDispatcherInterface $dispatcher,
|
||||
private readonly TimesheetService $service
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ final class TimesheetController extends BaseApiController
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
|
||||
{
|
||||
$query = new TimesheetQuery(false);
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$seeAll = false;
|
||||
|
||||
if ($this->isGranted('view_other_timesheet')) {
|
||||
|
||||
@@ -17,11 +17,11 @@ class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.12.0';
|
||||
public const VERSION = '2.13.0';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 21200;
|
||||
public const VERSION_ID = 21300;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -67,7 +67,7 @@ final class CustomerController extends AbstractController
|
||||
$query->setCurrentUser($this->getUser());
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form = $this->getToolbarForm($query, $request);
|
||||
if ($this->handleSearch($form, $request)) {
|
||||
return $this->redirectToRoute('admin_customer');
|
||||
}
|
||||
@@ -473,7 +473,7 @@ final class CustomerController extends AbstractController
|
||||
$query = new CustomerQuery();
|
||||
$query->setCurrentUser($this->getUser());
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form = $this->getToolbarForm($query, $request);
|
||||
$form->setData($query);
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
@@ -526,12 +526,12 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerQuery $query
|
||||
* @return FormInterface<CustomerQuery>
|
||||
*/
|
||||
private function getToolbarForm(CustomerQuery $query): FormInterface
|
||||
private function getToolbarForm(CustomerQuery $query, Request $request): FormInterface
|
||||
{
|
||||
return $this->createSearchForm(CustomerToolbarForm::class, $query, [
|
||||
'locale' => $request->getLocale(),
|
||||
'action' => $this->generateUrl('admin_customer', [
|
||||
'page' => $query->getPage(),
|
||||
])
|
||||
@@ -539,7 +539,6 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CustomerComment $comment
|
||||
* @return FormInterface<CustomerComment>
|
||||
*/
|
||||
private function getCommentForm(CustomerComment $comment): FormInterface
|
||||
@@ -555,7 +554,6 @@ final class CustomerController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @return FormInterface<Customer>
|
||||
*/
|
||||
private function createEditForm(Customer $customer): FormInterface
|
||||
|
||||
@@ -45,21 +45,28 @@ final class PluginController extends AbstractController
|
||||
private function getPluginInformation(HttpClientInterface $client, CacheInterface $cache): array
|
||||
{
|
||||
return $cache->get('kimai.marketplace_extensions', function (ItemInterface $item) use ($client) {
|
||||
$response = $client->request('GET', 'https://www.kimai.org/plugins.json');
|
||||
try {
|
||||
$response = $client->request('GET', 'https://www.kimai.org/plugins.json');
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return [];
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
|
||||
if ($json === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
return $response->toArray();
|
||||
} catch (\Exception $exception) {
|
||||
$this->logException($exception);
|
||||
$this->flashError('Could not download plugin information');
|
||||
}
|
||||
|
||||
$json = json_decode($response->getContent(), true);
|
||||
|
||||
if ($json === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$item->expiresAfter(86400); // one day
|
||||
|
||||
return $response->toArray();
|
||||
return [];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
final class PasswordResetController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private EventDispatcherInterface $eventDispatcher,
|
||||
private UserService $userService,
|
||||
private SystemConfiguration $configuration
|
||||
private readonly EventDispatcherInterface $eventDispatcher,
|
||||
private readonly UserService $userService,
|
||||
private readonly SystemConfiguration $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ final class PasswordResetController extends AbstractController
|
||||
}
|
||||
|
||||
$username = $request->request->get('username');
|
||||
if (!\is_string($username) || trim($username) === '') {
|
||||
throw $this->createAccessDeniedException('Username cannot be empty');
|
||||
}
|
||||
|
||||
$user = $this->userService->findUserByUsernameOrEmail($username);
|
||||
|
||||
if (!$user->isPasswordRequestNonExpired($this->configuration->getPasswordResetRetryLifetime())) {
|
||||
|
||||
@@ -18,7 +18,10 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
final class MenuSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private Security $security, private ContextHelper $helper)
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly ContextHelper $helper
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -134,19 +137,19 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu = $event->getAdminMenu();
|
||||
|
||||
if ($auth->isGranted('view_customer') || $auth->isGranted('view_teamlead_customer') || $auth->isGranted('view_team_customer')) {
|
||||
$customers = new MenuItemModel('customer_admin', 'customers', 'admin_customer', [], 'customer');
|
||||
$customers = new MenuItemModel('customers', 'customers', 'admin_customer', [], 'customer');
|
||||
$customers->setChildRoutes(['admin_customer_create', 'admin_customer_permissions', 'customer_details', 'admin_customer_edit', 'admin_customer_delete']);
|
||||
$menu->addChild($customers);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_project') || $auth->isGranted('view_teamlead_project') || $auth->isGranted('view_team_project')) {
|
||||
$projects = new MenuItemModel('project_admin', 'projects', 'admin_project', [], 'project');
|
||||
$projects = new MenuItemModel('projects', 'projects', 'admin_project', [], 'project');
|
||||
$projects->setChildRoutes(['admin_project_permissions', 'admin_project_create', 'project_details', 'admin_project_edit', 'admin_project_delete']);
|
||||
$menu->addChild($projects);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_activity') || $auth->isGranted('view_teamlead_activity') || $auth->isGranted('view_team_activity')) {
|
||||
$activities = new MenuItemModel('activity_admin', 'activities', 'admin_activity', [], 'activity');
|
||||
$activities = new MenuItemModel('activities', 'activities', 'admin_activity', [], 'activity');
|
||||
$activities->setChildRoutes(['admin_activity_create', 'activity_details', 'admin_activity_edit', 'admin_activity_delete']);
|
||||
$menu->addChild($activities);
|
||||
}
|
||||
@@ -163,18 +166,18 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
$menu = $event->getSystemMenu();
|
||||
|
||||
if ($auth->isGranted('view_user')) {
|
||||
$users = new MenuItemModel('user_admin', 'users', 'admin_user', [], 'users');
|
||||
$users = new MenuItemModel('users', 'users', 'admin_user', [], 'users');
|
||||
$users->setChildRoutes(['admin_user_create', 'admin_user_delete', 'user_profile', 'user_profile_edit', 'user_profile_password', 'user_profile_api_token', 'user_profile_roles', 'user_profile_teams', 'user_profile_preferences', 'user_profile_2fa']);
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('role_permissions')) {
|
||||
$users = new MenuItemModel('admin_user_permissions', 'profile.roles', 'admin_user_permissions', [], 'permissions');
|
||||
$users = new MenuItemModel('roles', 'profile.roles', 'admin_user_permissions', [], 'permissions');
|
||||
$menu->addChild($users);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_team')) {
|
||||
$teams = new MenuItemModel('user_team', 'teams', 'admin_team', [], 'team');
|
||||
$teams = new MenuItemModel('teams', 'teams', 'admin_team', [], 'team');
|
||||
$teams->setChildRoutes(['admin_team_create', 'admin_team_edit']);
|
||||
$menu->addChild($teams);
|
||||
}
|
||||
@@ -190,7 +193,7 @@ final class MenuSubscriber implements EventSubscriberInterface
|
||||
}
|
||||
|
||||
if ($auth->isGranted('system_configuration')) {
|
||||
$systemConfig = new MenuItemModel('system_configuration', 'menu.system_configuration', 'system_configuration', [], 'configuration');
|
||||
$systemConfig = new MenuItemModel('configurations', 'menu.system_configuration', 'system_configuration', [], 'configuration');
|
||||
$systemConfig->setChildRoutes(['system_configuration_update', 'system_configuration_section']);
|
||||
$menu->addChild($systemConfig);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
|
||||
namespace App\Form\Toolbar;
|
||||
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\Query\CustomerQuery;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Intl\Countries;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -22,9 +25,38 @@ final class CustomerToolbarForm extends AbstractType
|
||||
{
|
||||
use ToolbarFormTrait;
|
||||
|
||||
public function __construct(private readonly CustomerRepository $customerRepository)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$this->addSearchTermInputField($builder);
|
||||
|
||||
// fetch countries
|
||||
$qb = $this->customerRepository->createQueryBuilder('c');
|
||||
$qb
|
||||
->select('c.country')
|
||||
->distinct(true);
|
||||
|
||||
$countries = $qb->getQuery()->getSingleColumnResult();
|
||||
$choices = [];
|
||||
foreach ($countries as $country) {
|
||||
if (\is_string($country) && \is_string($options['locale'])) {
|
||||
$choices[$country] = Countries::getName($country, $options['locale']);
|
||||
}
|
||||
}
|
||||
|
||||
if (\count($choices) > 0) {
|
||||
$choices = array_flip($choices);
|
||||
ksort($choices);
|
||||
$builder->add('country', ChoiceType::class, [
|
||||
'label' => 'country',
|
||||
'choices' => $choices,
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->addVisibilityChoice($builder);
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
@@ -37,6 +69,8 @@ final class CustomerToolbarForm extends AbstractType
|
||||
$resolver->setDefaults([
|
||||
'data_class' => CustomerQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'locale' => locale_get_default(),
|
||||
]);
|
||||
$resolver->setAllowedTypes('locale', ['string']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ final class TagsType extends AbstractType
|
||||
private ?int $count = null;
|
||||
|
||||
public function __construct(
|
||||
private AuthorizationCheckerInterface $auth,
|
||||
private TagRepository $repository
|
||||
private readonly AuthorizationCheckerInterface $auth,
|
||||
private readonly TagRepository $repository
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,10 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
{
|
||||
private ?InvoiceModel $model = null;
|
||||
|
||||
public function __construct(private InvoiceRepository $repository, private SystemConfiguration $configuration)
|
||||
public function __construct(
|
||||
private readonly InvoiceRepository $repository,
|
||||
private readonly SystemConfiguration $configuration
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,9 +36,6 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getInvoiceNumber(): string
|
||||
{
|
||||
$format = $this->configuration->find('invoice.number_format');
|
||||
@@ -68,6 +68,10 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
'm' => $invoiceDate->format('n'),
|
||||
'D' => $invoiceDate->format('d'),
|
||||
'd' => $invoiceDate->format('j'),
|
||||
'YY' => (int) $invoiceDate->format('Y') + $increaseBy,
|
||||
'yy' => (int) $invoiceDate->format('y') + $increaseBy,
|
||||
'MM' => (int) $invoiceDate->format('m') + $increaseBy,
|
||||
'DD' => (int) $invoiceDate->format('d') + $increaseBy,
|
||||
'date' => $invoiceDate->format('ymd'),
|
||||
'cc' => $this->repository->getCounterForCustomerAllTime($this->model->getCustomer()) + $increaseBy,
|
||||
'ccy' => $this->repository->getCounterForYear($invoiceDate, $this->model->getCustomer()) + $increaseBy,
|
||||
@@ -92,7 +96,7 @@ final class ConfigurableNumberGenerator implements NumberGeneratorInterface
|
||||
do {
|
||||
$result = $numberGenerator->getNumber($increaseBy);
|
||||
$increaseBy++;
|
||||
} while ($this->repository->hasInvoice($result) && $loops++ < 99);
|
||||
} while ((int) $result < 0 || ($this->repository->hasInvoice($result) && $loops++ < 99));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -130,7 +130,7 @@ final class MPdfConverter implements HtmlToPdfConverter
|
||||
|
||||
// lowercase all font names, otherwise they cannot be loaded
|
||||
// see https://github.com/kimai/www.kimai.org/issues/280
|
||||
if (\array_key_exists('fonts', $options)) {
|
||||
if (\array_key_exists('fonts', $options) && \is_array($options['fonts'])) {
|
||||
$fonts = [];
|
||||
foreach ($options['fonts'] as $name => $values) {
|
||||
$fonts[strtolower($name)] = $values;
|
||||
|
||||
@@ -20,15 +20,15 @@ use Doctrine\ORM\Query\Expr\Join;
|
||||
|
||||
final class CustomerMonthlyProjectsRepository
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository, private EntityManagerInterface $entityManager)
|
||||
public function __construct(
|
||||
private readonly TimesheetRepository $repository,
|
||||
private readonly EntityManagerInterface $entityManager
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param DateTime $begin
|
||||
* @param DateTime $end
|
||||
* @param User[] $users
|
||||
* @param Customer|null $customer
|
||||
* @return array
|
||||
* @internal
|
||||
*/
|
||||
|
||||
@@ -55,10 +55,6 @@ class CustomerRepository extends EntityRepository
|
||||
return $customers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function saveCustomer(Customer $customer): void
|
||||
{
|
||||
$entityManager = $this->getEntityManager();
|
||||
@@ -75,7 +71,7 @@ class CustomerRepository extends EntityRepository
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
|
||||
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = []): void
|
||||
{
|
||||
$permissions = $this->getPermissionCriteria($qb, $user, $teams);
|
||||
if ($permissions->count() > 0) {
|
||||
@@ -124,9 +120,6 @@ class CustomerRepository extends EntityRepository
|
||||
|
||||
/**
|
||||
* Returns a query builder that is used for CustomerType and your own 'query_builder' option.
|
||||
*
|
||||
* @param CustomerFormTypeQuery $query
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getQueryBuilderForFormType(CustomerFormTypeQuery $query): QueryBuilder
|
||||
{
|
||||
@@ -177,6 +170,13 @@ class CustomerRepository extends EntityRepository
|
||||
->from(Customer::class, 'c')
|
||||
;
|
||||
|
||||
if ($query->getCountry() !== null) {
|
||||
$qb
|
||||
->andWhere($qb->expr()->eq('c.country', ':country'))
|
||||
->setParameter('country', $query->getCountry())
|
||||
;
|
||||
}
|
||||
|
||||
foreach ($query->getOrderGroups() as $orderBy => $order) {
|
||||
switch ($orderBy) {
|
||||
case 'vat_id':
|
||||
|
||||
@@ -18,11 +18,24 @@ class CustomerQuery extends BaseQuery implements VisibilityInterface
|
||||
'phone', 'currency', 'address', 'contact', 'company', 'vat_id', 'budget', 'timeBudget', 'visible'
|
||||
];
|
||||
|
||||
private ?string $country = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->setDefaults([
|
||||
'orderBy' => 'name',
|
||||
'visibility' => VisibilityInterface::SHOW_VISIBLE,
|
||||
'country' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getCountry(): ?string
|
||||
{
|
||||
return $this->country;
|
||||
}
|
||||
|
||||
public function setCountry(?string $country): void
|
||||
{
|
||||
$this->country = $country;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ use Symfony\Component\HttpFoundation\RequestStack;
|
||||
class SamlAuthFactory
|
||||
{
|
||||
public function __construct(
|
||||
private RequestStack $request,
|
||||
private SamlConfigurationInterface $configuration
|
||||
private readonly RequestStack $request,
|
||||
private readonly SamlConfigurationInterface $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -35,12 +35,12 @@ class SamlAuthenticator extends AbstractAuthenticator
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private HttpUtils $httpUtils,
|
||||
private SamlAuthenticationSuccessHandler $successHandler,
|
||||
private SamlAuthenticationFailureHandler $failureHandler,
|
||||
private SamlAuthFactory $samlAuthFactory,
|
||||
private SamlProvider $samlProvider,
|
||||
private SamlConfigurationInterface $configuration
|
||||
private readonly HttpUtils $httpUtils,
|
||||
private readonly SamlAuthenticationSuccessHandler $successHandler,
|
||||
private readonly SamlAuthenticationFailureHandler $failureHandler,
|
||||
private readonly SamlAuthFactory $samlAuthFactory,
|
||||
private readonly SamlProvider $samlProvider,
|
||||
private readonly SamlConfigurationInterface $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
final class SamlBadge implements BadgeInterface
|
||||
{
|
||||
public function __construct(private SamlLoginAttributes $samlToken)
|
||||
public function __construct(private readonly SamlLoginAttributes $samlToken)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ use Symfony\Component\Security\Http\Event\LogoutEvent;
|
||||
|
||||
final class SamlLogoutSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private SamlAuthFactory $samlAuth)
|
||||
public function __construct(private readonly SamlAuthFactory $samlAuth)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ final class SamlLogoutSubscriber implements EventSubscriberInterface
|
||||
];
|
||||
}
|
||||
|
||||
public function logout(LogoutEvent $event)
|
||||
public function logout(LogoutEvent $event): void
|
||||
{
|
||||
$token = $event->getToken();
|
||||
|
||||
|
||||
@@ -22,9 +22,9 @@ final class SamlProvider
|
||||
* @param UserProviderInterface<User> $userProvider
|
||||
*/
|
||||
public function __construct(
|
||||
private UserRepository $repository,
|
||||
private UserProviderInterface $userProvider,
|
||||
private SamlConfigurationInterface $configuration
|
||||
private readonly UserRepository $repository,
|
||||
private readonly UserProviderInterface $userProvider,
|
||||
private readonly SamlConfigurationInterface $configuration
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ final class SamlProvider
|
||||
$user->setAuth(User::AUTH_SAML);
|
||||
}
|
||||
|
||||
private function getPropertyValue(SamlLoginAttributes $token, $attribute)
|
||||
private function getPropertyValue(SamlLoginAttributes $token, $attribute): string
|
||||
{
|
||||
$results = [];
|
||||
$attributes = $token->getAttributes();
|
||||
|
||||
@@ -29,11 +29,14 @@ final class RolePermissionManager
|
||||
private bool $isInitialized = false;
|
||||
|
||||
/**
|
||||
* @param PermissionService $service
|
||||
* @param array<string, array<string, bool>> $permissions as defined in kimai.yaml
|
||||
* @param array<string, bool> $permissionNames as defined in kimai.yaml
|
||||
*/
|
||||
public function __construct(private PermissionService $service, private array $permissions, private array $permissionNames)
|
||||
public function __construct(
|
||||
private readonly PermissionService $service,
|
||||
private array $permissions,
|
||||
private readonly array $permissionNames
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -47,11 +50,6 @@ final class RolePermissionManager
|
||||
$perm = (string) $item['permission'];
|
||||
$role = (string) $item['role'];
|
||||
|
||||
// these permissions may not be revoked at any time, because super admin would lose the ability to reactivate any permission
|
||||
if ($role === User::ROLE_SUPER_ADMIN && \array_key_exists($perm, self::SUPER_ADMIN_PERMISSIONS)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!\array_key_exists($role, $this->permissions)) {
|
||||
$this->permissions[$role] = [];
|
||||
}
|
||||
@@ -59,6 +57,7 @@ final class RolePermissionManager
|
||||
$this->permissions[$role][$perm] = (bool) $item['allowed'];
|
||||
}
|
||||
|
||||
// these permissions may not be revoked at any time, because super admin would lose the ability to reactivate any permission
|
||||
foreach (self::SUPER_ADMIN_PERMISSIONS as $perm => $value) {
|
||||
$this->permissions[User::ROLE_SUPER_ADMIN][$perm] = $value;
|
||||
}
|
||||
@@ -68,14 +67,9 @@ final class RolePermissionManager
|
||||
|
||||
/**
|
||||
* Only permissions which were registered through the Symfony configuration stack will be acknowledged here.
|
||||
*
|
||||
* @param string $permission
|
||||
* @return bool
|
||||
*/
|
||||
public function isRegisteredPermission(string $permission): bool
|
||||
{
|
||||
$this->init();
|
||||
|
||||
return \array_key_exists($permission, $this->permissionNames);
|
||||
}
|
||||
|
||||
@@ -89,7 +83,7 @@ final class RolePermissionManager
|
||||
return false;
|
||||
}
|
||||
|
||||
return \array_key_exists($permission, $this->permissions[$role]) ? $this->permissions[$role][$permission] : false;
|
||||
return \array_key_exists($permission, $this->permissions[$role]) && $this->permissions[$role][$permission];
|
||||
}
|
||||
|
||||
public function hasRolePermission(User $user, string $permission): bool
|
||||
@@ -112,8 +106,6 @@ final class RolePermissionManager
|
||||
*/
|
||||
public function getPermissions(): array
|
||||
{
|
||||
$this->init();
|
||||
|
||||
return array_keys($this->permissionNames);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use DateTimeInterface;
|
||||
|
||||
final class TimesheetStatisticService
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository)
|
||||
public function __construct(private readonly TimesheetRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -53,10 +53,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('MONTH(t.date) as month')
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
@@ -125,10 +125,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('IDENTITY(t.activity) as activity')
|
||||
->addSelect('DATE(t.date) as date')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('date')
|
||||
->addGroupBy('project')
|
||||
@@ -173,8 +173,6 @@ final class TimesheetStatisticService
|
||||
|
||||
/**
|
||||
* @internal only for core development
|
||||
* @param DateTimeInterface $begin
|
||||
* @param DateTimeInterface $end
|
||||
* @param User[] $users
|
||||
* @return array
|
||||
*/
|
||||
@@ -203,10 +201,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->addSelect('MONTH(t.date) as month')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
@@ -253,7 +251,7 @@ final class TimesheetStatisticService
|
||||
public function findFirstRecordDate(User $user): ?\DateTimeImmutable
|
||||
{
|
||||
$result = $this->repository->createQueryBuilder('t')
|
||||
->select('MIN(t.begin)')
|
||||
->select('MIN(t.date)')
|
||||
->where('t.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->getQuery()
|
||||
@@ -295,10 +293,10 @@ final class TimesheetStatisticService
|
||||
->addSelect('YEAR(t.date) as year')
|
||||
->addSelect('IDENTITY(t.user) as user')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->in('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $users)
|
||||
->groupBy('year')
|
||||
->addGroupBy('month')
|
||||
|
||||
@@ -192,6 +192,8 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
public function getJavascriptConfiguration(User $user): array
|
||||
{
|
||||
return [
|
||||
'locale' => $this->locale,
|
||||
'language' => $user->getLanguage(),
|
||||
'formatDuration' => $this->localeService->getDurationFormat($this->locale),
|
||||
'formatDate' => $this->localeService->getDateFormat($this->locale),
|
||||
'defaultColor' => Constants::DEFAULT_COLOR,
|
||||
|
||||
@@ -76,7 +76,7 @@ final class PaginationExtension extends AbstractExtension
|
||||
$propertyAccessor = PropertyAccess::createPropertyAccessor();
|
||||
$propertyAccessor->setValue($routeParams, $pagePropertyPath, $page);
|
||||
|
||||
return $router->generate($routeName, $routeParams);
|
||||
return $router->generate($routeName, $routeParams); // @phpstan-ignore-line
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,9 +38,7 @@ final class QuickEntryTimesheetValidator extends ConstraintValidator
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$timesheet = $value;
|
||||
|
||||
if ($timesheet->getId() === null && $timesheet->getDuration(false) === null) {
|
||||
if ($value->getId() === null && $value->getDuration(false) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,7 +47,7 @@ final class QuickEntryTimesheetValidator extends ConstraintValidator
|
||||
->getValidator()
|
||||
->inContext($this->context)
|
||||
->atPath('duration')
|
||||
->validate($timesheet, $innerConstraint, [Constraint::DEFAULT_GROUP]);
|
||||
->validate($value, $innerConstraint, [Constraint::DEFAULT_GROUP]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetBasicValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $systemConfiguration)
|
||||
public function __construct(private readonly SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,10 +36,6 @@ final class TimesheetBasicValidator extends ConstraintValidator
|
||||
$this->validateActivityAndProject($value, $this->context);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateBeginAndEnd(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
|
||||
{
|
||||
$begin = $timesheet->getBegin();
|
||||
@@ -64,10 +60,6 @@ final class TimesheetBasicValidator extends ConstraintValidator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param ExecutionContextInterface $context
|
||||
*/
|
||||
protected function validateActivityAndProject(TimesheetEntity $timesheet, ExecutionContextInterface $context): void
|
||||
{
|
||||
$activity = $timesheet->getActivity();
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Activity\ActivityStatisticService;
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Customer\CustomerStatisticService;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\Timesheet as TimesheetEntity;
|
||||
use App\Model\BudgetStatisticModel;
|
||||
use App\Project\ProjectStatisticService;
|
||||
use App\Repository\TimesheetRepository;
|
||||
@@ -29,53 +29,49 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(
|
||||
private SystemConfiguration $configuration,
|
||||
private CustomerStatisticService $customerStatisticService,
|
||||
private ProjectStatisticService $projectStatisticService,
|
||||
private ActivityStatisticService $activityStatisticService,
|
||||
private TimesheetRepository $timesheetRepository,
|
||||
private RateServiceInterface $rateService,
|
||||
private AuthorizationCheckerInterface $security,
|
||||
private LocaleService $localeService
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly CustomerStatisticService $customerStatisticService,
|
||||
private readonly ProjectStatisticService $projectStatisticService,
|
||||
private readonly ActivityStatisticService $activityStatisticService,
|
||||
private readonly TimesheetRepository $timesheetRepository,
|
||||
private readonly RateServiceInterface $rateService,
|
||||
private readonly AuthorizationCheckerInterface $security,
|
||||
private readonly LocaleService $localeService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetBudgetUsed)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetBudgetUsed::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof Timesheet)) {
|
||||
throw new UnexpectedTypeException($timesheet, Timesheet::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($this->configuration->isTimesheetAllowOverbookingBudget()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$begin = $timesheet->getBegin();
|
||||
$begin = $value->getBegin();
|
||||
|
||||
// we can only work with stopped entries
|
||||
if ($begin === null || $timesheet->getEnd() === null || $timesheet->getUser() === null) {
|
||||
if ($begin === null || $value->getEnd() === null || $value->getUser() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// budgets need only be calculated for billable records
|
||||
if (!$timesheet->isBillable()) {
|
||||
if (!$value->isBillable()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this validator needs a project to calculate the rates
|
||||
if ($timesheet->getProject() === null) {
|
||||
if ($value->getProject() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $timesheet->getId();
|
||||
$id = $value->getId();
|
||||
|
||||
// when changing the date via the calendar and/or the API, the duration will not be reset by the
|
||||
// duration calculator (which runs after validation!) so we manually reset the duration before
|
||||
@@ -92,9 +88,9 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
// $timesheet->setDuration(null);
|
||||
// $duration = $timesheet->getDuration();
|
||||
|
||||
$duration = $timesheet->getCalculatedDuration();
|
||||
$duration = $value->getCalculatedDuration();
|
||||
|
||||
$timeRate = $this->rateService->calculate($timesheet);
|
||||
$timeRate = $this->rateService->calculate($value);
|
||||
$rate = $timeRate->getRate();
|
||||
|
||||
$activityDuration = $duration ?? 0;
|
||||
@@ -116,10 +112,10 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
// this could for example happen when export flag is changed OR if "prevent overbooking" config was recently activated and this is an old entry
|
||||
if ($duration === $rawData['duration'] &&
|
||||
$rate === $rawData['rate'] &&
|
||||
$timesheet->isBillable() === $rawData['billable'] &&
|
||||
$value->isBillable() === $rawData['billable'] &&
|
||||
$begin->format('Y.m.d') === $rawData['begin']->format('Y.m.d') &&
|
||||
$timesheet->getProject()->getId() === $projectId &&
|
||||
($timesheet->getActivity() === null || $timesheet->getActivity()->getId() === $activityId)
|
||||
$value->getProject()->getId() === $projectId &&
|
||||
($value->getActivity() === null || $value->getActivity()->getId() === $activityId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@@ -129,18 +125,18 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
// only subtract the previously logged data in case the record was billable
|
||||
// if it wasn't billable, then its values are not included in the statistic models used later on
|
||||
if ($rawData['billable']) {
|
||||
if (null !== $timesheet->getActivity() && $activityId === $timesheet->getActivity()->getId()) {
|
||||
if (null !== $value->getActivity() && $activityId === $value->getActivity()->getId()) {
|
||||
$activityDuration -= $rawData['duration'];
|
||||
$activityRate -= $rawData['rate'];
|
||||
}
|
||||
|
||||
if (null !== $timesheet->getProject()) {
|
||||
if ($projectId === $timesheet->getProject()->getId()) {
|
||||
if (null !== $value->getProject()) {
|
||||
if ($projectId === $value->getProject()->getId()) {
|
||||
$projectDuration -= $rawData['duration'];
|
||||
$projectRate -= $rawData['rate'];
|
||||
}
|
||||
|
||||
if ($customerId === $timesheet->getProject()->getCustomer()->getId()) {
|
||||
if ($customerId === $value->getProject()->getCustomer()->getId()) {
|
||||
$customerDuration -= $rawData['duration'];
|
||||
$customerRate -= $rawData['rate'];
|
||||
}
|
||||
@@ -153,23 +149,23 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
$now = new DateTime('now', $begin->getTimezone());
|
||||
$recordDate = $begin;
|
||||
|
||||
if (null !== ($activity = $timesheet->getActivity()) && $activity->hasBudgets()) {
|
||||
if (null !== ($activity = $value->getActivity()) && $activity->hasBudgets()) {
|
||||
$dateTime = $activity->isMonthlyBudget() ? $recordDate : $now;
|
||||
if ($activity->isMonthlyBudget() && $monthWasChanged) {
|
||||
$activityDuration = $duration;
|
||||
}
|
||||
$stat = $this->activityStatisticService->getBudgetStatisticModel($activity, $dateTime);
|
||||
$this->checkBudgets($constraint, $stat, $timesheet, $activityDuration, $activityRate, 'activity');
|
||||
$this->checkBudgets($constraint, $stat, $value, $activityDuration, $activityRate, 'activity');
|
||||
}
|
||||
|
||||
if (null !== ($project = $timesheet->getProject())) {
|
||||
if (null !== ($project = $value->getProject())) {
|
||||
if ($project->hasBudgets()) {
|
||||
$dateTime = $project->isMonthlyBudget() ? $recordDate : $now;
|
||||
if ($project->isMonthlyBudget() && $monthWasChanged) {
|
||||
$projectDuration = $duration;
|
||||
}
|
||||
$stat = $this->projectStatisticService->getBudgetStatisticModel($project, $dateTime);
|
||||
$this->checkBudgets($constraint, $stat, $timesheet, $projectDuration, $projectRate, 'project');
|
||||
$this->checkBudgets($constraint, $stat, $value, $projectDuration, $projectRate, 'project');
|
||||
}
|
||||
if (null !== ($customer = $project->getCustomer()) && $customer->hasBudgets()) {
|
||||
$dateTime = $customer->isMonthlyBudget() ? $recordDate : $now;
|
||||
@@ -177,12 +173,12 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
$customerDuration = $duration;
|
||||
}
|
||||
$stat = $this->customerStatisticService->getBudgetStatisticModel($customer, $dateTime);
|
||||
$this->checkBudgets($constraint, $stat, $timesheet, $customerDuration, $customerRate, 'customer');
|
||||
$this->checkBudgets($constraint, $stat, $value, $customerDuration, $customerRate, 'customer');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function checkBudgets(TimesheetBudgetUsed $constraint, BudgetStatisticModel $stat, Timesheet $timesheet, int $duration, float $rate, string $field): bool
|
||||
private function checkBudgets(TimesheetBudgetUsed $constraint, BudgetStatisticModel $stat, TimesheetEntity $timesheet, int $duration, float $rate, string $field): bool
|
||||
{
|
||||
$fullRate = ($stat->getBudgetSpent() + $rate);
|
||||
|
||||
@@ -203,7 +199,7 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
|
||||
return false;
|
||||
}
|
||||
|
||||
private function addBudgetViolation(TimesheetBudgetUsed $constraint, Timesheet $timesheet, string $field, float $budget, float $rate): void
|
||||
private function addBudgetViolation(TimesheetBudgetUsed $constraint, TimesheetEntity $timesheet, string $field, float $budget, float $rate): void
|
||||
{
|
||||
// using the locale of the assigned user is not the best solution, but allows to be independent of the request stack
|
||||
$helper = new LocaleFormatter($this->localeService, $timesheet->getUser()?->getLocale() ?? 'en');
|
||||
|
||||
@@ -17,29 +17,25 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetExportedValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private Security $security)
|
||||
public function __construct(private readonly Security $security)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetExported)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetExported::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($timesheet->getId() === null) {
|
||||
if ($value->getId() === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$timesheet->isExported()) {
|
||||
if (!$value->isExported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +43,7 @@ final class TimesheetExportedValidator extends ConstraintValidator
|
||||
// can trigger is right when the "export" flag ist set from the "edit form".
|
||||
// most teamleads should not have "edit_exported_timesheet" but only "edit_export_other_timesheet"
|
||||
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('edit_export', $timesheet)) {
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('edit_export', $value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,14 +17,10 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetFutureTimesValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
public function __construct(private readonly SystemConfiguration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $value
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetFutureTimes)) {
|
||||
|
||||
@@ -18,29 +18,28 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetLockdownValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private Security $security, private LockdownService $lockdownService)
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly LockdownService $lockdownService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetLockdown)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetLockdown::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if (!$this->lockdownService->isLockdownActive()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (null === ($timesheetStart = $timesheet->getBegin())) {
|
||||
if (null === ($timesheetStart = $value->getBegin())) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,7 +66,7 @@ final class TimesheetLockdownValidator extends ConstraintValidator
|
||||
$allowEditInGracePeriod = true;
|
||||
}
|
||||
|
||||
if ($this->lockdownService->isEditable($timesheet, $now, $allowEditInGracePeriod)) {
|
||||
if ($this->lockdownService->isEditable($value, $now, $allowEditInGracePeriod)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,30 +17,26 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetLongRunningValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $systemConfiguration)
|
||||
public function __construct(private readonly SystemConfiguration $systemConfiguration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetLongRunning)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetLongRunning::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($timesheet->isRunning()) {
|
||||
if ($value->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var int $duration */
|
||||
$duration = $timesheet->getCalculatedDuration();
|
||||
$duration = $value->getCalculatedDuration();
|
||||
|
||||
// one year is currently the maximum that can be logged (which is already not logically)
|
||||
// the database column could hold more data, but let's limit it here
|
||||
|
||||
@@ -18,26 +18,25 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration, private TimesheetRepository $repository)
|
||||
public function __construct(
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly TimesheetRepository $repository
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetOverlapping)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetOverlapping::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$begin = $timesheet->getBegin();
|
||||
$end = $timesheet->getEnd();
|
||||
$begin = $value->getBegin();
|
||||
$end = $value->getEnd();
|
||||
|
||||
// this case is handled in TimesheetValidator and should not raise a second validation
|
||||
if ($begin !== null && $end !== null && $begin > $end) {
|
||||
@@ -48,7 +47,7 @@ final class TimesheetOverlappingValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->repository->hasRecordForTime($timesheet)) {
|
||||
if (!$this->repository->hasRecordForTime($value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,28 +18,27 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetRestartValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private Security $security, private TrackingModeService $trackingModeService)
|
||||
public function __construct(
|
||||
private readonly Security $security,
|
||||
private readonly TrackingModeService $trackingModeService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetRestart)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetRestart::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
// special case that would otherwise need to be validated in several controllers:
|
||||
// an entry is edited and the end date is removed (or duration deleted) would restart the record,
|
||||
// which might be disallowed for the current user
|
||||
if (null !== $timesheet->getEnd()) {
|
||||
if (null !== $value->getEnd()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -47,7 +46,7 @@ final class TimesheetRestartValidator extends ConstraintValidator
|
||||
return;
|
||||
}
|
||||
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('start', $timesheet)) {
|
||||
if (null !== $this->security->getUser() && $this->security->isGranted('start', $value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,18 +28,14 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetEntityConstraint)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetEntityConstraint::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
$groups = [Constraint::DEFAULT_GROUP];
|
||||
@@ -51,7 +47,7 @@ final class TimesheetValidator extends ConstraintValidator
|
||||
$this->context
|
||||
->getValidator()
|
||||
->inContext($this->context)
|
||||
->validate($timesheet, $innerConstraint, $groups);
|
||||
->validate($value, $innerConstraint, $groups);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,35 +17,31 @@ use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class TimesheetZeroDurationValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private SystemConfiguration $configuration)
|
||||
public function __construct(private readonly SystemConfiguration $configuration)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimesheetEntity $timesheet
|
||||
* @param Constraint $constraint
|
||||
*/
|
||||
public function validate(mixed $timesheet, Constraint $constraint): void
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!($constraint instanceof TimesheetZeroDuration)) {
|
||||
throw new UnexpectedTypeException($constraint, TimesheetZeroDuration::class);
|
||||
}
|
||||
|
||||
if (!\is_object($timesheet) || !($timesheet instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($timesheet, TimesheetEntity::class);
|
||||
if (!\is_object($value) || !($value instanceof TimesheetEntity)) {
|
||||
throw new UnexpectedTypeException($value, TimesheetEntity::class);
|
||||
}
|
||||
|
||||
if ($this->configuration->isTimesheetAllowZeroDuration()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($timesheet->isRunning()) {
|
||||
if ($value->isRunning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$duration = 0;
|
||||
if ($timesheet->getEnd() !== null && $timesheet->getBegin() !== null) {
|
||||
$duration = $timesheet->getCalculatedDuration();
|
||||
if ($value->getEnd() !== null && $value->getBegin() !== null) {
|
||||
$duration = $value->getCalculatedDuration();
|
||||
}
|
||||
|
||||
if ($duration <= 0) {
|
||||
|
||||
@@ -35,21 +35,23 @@ final class ActivityVoter extends Voter
|
||||
'permissions',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Activity::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Activity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Activity && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
@@ -65,7 +67,7 @@ final class ActivityVoter extends Voter
|
||||
}
|
||||
|
||||
// those cannot be assigned to teams
|
||||
if (\in_array($attribute, ['create', 'delete'])) {
|
||||
if (\in_array($attribute, ['create', 'delete'], true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,21 +39,23 @@ final class CustomerVoter extends Voter
|
||||
'access',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Customer::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Customer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Customer && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -38,17 +38,22 @@ final class EntityMultiRoleVoter extends Voter
|
||||
'activity',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
if (!$this->supportsAttribute($attribute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS, true)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,7 +74,7 @@ final class EntityMultiRoleVoter extends Voter
|
||||
|
||||
$suffix = null;
|
||||
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS)) {
|
||||
if (\is_string($subject) && \in_array($subject, self::ALLOWED_SUBJECTS, true)) {
|
||||
$suffix = $subject;
|
||||
} elseif ($subject instanceof Activity) {
|
||||
$suffix = 'activity';
|
||||
|
||||
@@ -37,21 +37,23 @@ final class ProjectVoter extends Voter
|
||||
'details',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Project::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Project)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Project && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -20,13 +20,21 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
*/
|
||||
final class QuickEntryVoter extends Voter
|
||||
{
|
||||
public function __construct(private RolePermissionManager $permissionManager, private TrackingModeService $trackingModeService)
|
||||
public function __construct(
|
||||
private readonly RolePermissionManager $permissionManager,
|
||||
private readonly TrackingModeService $trackingModeService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return 'quick-entry' === $attribute;
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return 'quick-entry' === $attribute;
|
||||
return $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -26,13 +26,23 @@ final class ReportingVoter extends Voter
|
||||
'report:user',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return $subjectType === 'null';
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return $subject === null && \in_array($attribute, self::ALLOWED_ATTRIBUTES);
|
||||
return $subject === null && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -21,18 +21,24 @@ use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
*/
|
||||
final class RolePermissionVoter extends Voter
|
||||
{
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return $this->permissionManager->isRegisteredPermission($attribute);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
// we only work on single strings that have no subject
|
||||
return $subjectType === 'null';
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
// we only work on single strings that have no subject
|
||||
if (null !== $subject) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->permissionManager->isRegisteredPermission($attribute);
|
||||
return $subject === null && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -29,21 +29,23 @@ final class TeamVoter extends Voter
|
||||
'delete',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Team::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Team)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Team && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Voter;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Form\Model\MultiUserTimesheet;
|
||||
use App\Security\RolePermissionManager;
|
||||
use App\Timesheet\LockdownService;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
@@ -55,21 +56,26 @@ final class TimesheetVoter extends Voter
|
||||
private ?bool $editExported = null;
|
||||
private ?\DateTime $now = null;
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager, private LockdownService $lockdownService)
|
||||
public function __construct(
|
||||
private readonly RolePermissionManager $permissionManager,
|
||||
private readonly LockdownService $lockdownService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, Timesheet::class) || str_contains($subjectType, MultiUserTimesheet::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof Timesheet)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof Timesheet && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -38,21 +38,23 @@ final class UserVoter extends Voter
|
||||
'supervisor',
|
||||
];
|
||||
|
||||
public function __construct(private RolePermissionManager $permissionManager)
|
||||
public function __construct(private readonly RolePermissionManager $permissionManager)
|
||||
{
|
||||
}
|
||||
|
||||
public function supportsAttribute(string $attribute): bool
|
||||
{
|
||||
return \in_array($attribute, self::ALLOWED_ATTRIBUTES, true);
|
||||
}
|
||||
|
||||
public function supportsType(string $subjectType): bool
|
||||
{
|
||||
return str_contains($subjectType, User::class);
|
||||
}
|
||||
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
if (!($subject instanceof User)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!\in_array($attribute, self::ALLOWED_ATTRIBUTES)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return $subject instanceof User && $this->supportsAttribute($attribute);
|
||||
}
|
||||
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
|
||||
|
||||
@@ -25,7 +25,7 @@ use DateTimeInterface;
|
||||
*/
|
||||
class DailyWorkingTimeChartProvider
|
||||
{
|
||||
public function __construct(private TimesheetRepository $repository)
|
||||
public function __construct(private readonly TimesheetRepository $repository)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -33,9 +33,6 @@ class DailyWorkingTimeChartProvider
|
||||
* In case this method is called with one timezone and the results are from another timezone,
|
||||
* it might return rows outside the time-range.
|
||||
*
|
||||
* @param DateTimeInterface $begin
|
||||
* @param DateTimeInterface $end
|
||||
* @param User|null $user
|
||||
* @return array<mixed>
|
||||
*/
|
||||
protected function getDailyData(DateTimeInterface $begin, DateTimeInterface $end, ?User $user = null): array
|
||||
|
||||
@@ -144,10 +144,10 @@ final class WorkingTimeService
|
||||
->select('COALESCE(SUM(t.duration), 0) as duration')
|
||||
->addSelect('DATE(t.date) as day')
|
||||
->where($qb->expr()->isNotNull('t.end'))
|
||||
->andWhere($qb->expr()->between('t.begin', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->between('t.date', ':begin', ':end'))
|
||||
->andWhere($qb->expr()->eq('t.user', ':user'))
|
||||
->setParameter('begin', $begin)
|
||||
->setParameter('end', $end)
|
||||
->setParameter('begin', $begin->format('Y-m-d'))
|
||||
->setParameter('end', $end->format('Y-m-d'))
|
||||
->setParameter('user', $user->getId())
|
||||
->addGroupBy('day')
|
||||
;
|
||||
|
||||
12
symfony.lock
12
symfony.lock
@@ -20,18 +20,6 @@
|
||||
"ref": "56eaa387b5e48ebcc7c95a893b47dfa1ad51449c"
|
||||
}
|
||||
},
|
||||
"doctrine/annotations": {
|
||||
"version": "1.13",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes",
|
||||
"branch": "master",
|
||||
"version": "1.0",
|
||||
"ref": "a2759dd6123694c8d901d0ec80006e044c2e6457"
|
||||
},
|
||||
"files": [
|
||||
"config/routes/annotations.yaml"
|
||||
]
|
||||
},
|
||||
"doctrine/cache": {
|
||||
"version": "v1.7.1"
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% import "project/actions.html.twig" as projectActions %}
|
||||
{% block box_attributes %}id="activity_details_box"{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ widgets.label_name(activity.name, activity.color) }}
|
||||
{{ widgets.label_dot(activity.name, activity.color) }}
|
||||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
{% if can_edit %}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{% import "macros/widgets.html.twig" as widgets %}
|
||||
{% block box_attributes %}id="customer_details_box"{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ widgets.label_customer(customer) }}
|
||||
{{ widgets.label_dot(customer.name, customer.color) }}
|
||||
{% if customer.company is not empty %} – {{ customer.company }}{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_tools %}
|
||||
|
||||
@@ -176,7 +176,6 @@
|
||||
{% set format = locale_format('date')|js_format %}
|
||||
{% set attr = attr|merge({'data-daterangepicker': 'on', 'autocomplete': 'off', 'data-format': format, 'placeholder': format ~ attr['data-separator'] ~ format}) -%}
|
||||
<div class="input-group">
|
||||
{{ block('form_widget_simple') }}
|
||||
{% if ranges is defined %}
|
||||
<button type="button" class="input-group-text dropdown-toggle btn-duration-preset" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"></button>
|
||||
<div class="dropdown-menu dropdown-menu-start pre-scrollable">
|
||||
@@ -185,6 +184,7 @@
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ block('form_widget_simple') }}
|
||||
</div>
|
||||
{% endblock daterange_widget %}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% macro init_frontend_loader() %}
|
||||
{% set configurations = javascript_configurations(app.user)|merge({
|
||||
login: path('login'),
|
||||
locale: app.request.locale,
|
||||
direction: tabler_bundle.rightToLeft ? 'rtl' : 'ltr',
|
||||
first_dow_iso: iso_day_by_name(app.user.firstDayOfWeek),
|
||||
}) %}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block box_title %}
|
||||
{{ widgets.label_name(project.name, project.color) }}
|
||||
{{ widgets.label_dot(project.name, project.color) }}
|
||||
{% endblock %}
|
||||
{% block box_body_class %}p-0{% endblock %}
|
||||
{% block box_body %}
|
||||
|
||||
@@ -168,7 +168,10 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
}
|
||||
|
||||
$this->assertAccessIsGranted($client, $url, 'GET', $parameters);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
@@ -208,7 +211,10 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
|
||||
$query = ['order' => 'ASC', 'orderBy' => 'project'];
|
||||
$this->assertAccessIsGranted($client, '/api/activities', 'GET', $query);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
@@ -219,11 +225,19 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$this->assertEquals($imports[1]->getId(), $result[2]['project']);
|
||||
}
|
||||
|
||||
public function testGetEntityIsSecure(): void
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/activities/1');
|
||||
}
|
||||
|
||||
public function testGetEntity(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/api/activities/1');
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ActivityEntity', $result);
|
||||
@@ -247,7 +261,10 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/activities', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ActivityEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -262,7 +279,10 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/activities', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ActivityEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -308,7 +328,10 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/activities/1', 'PATCH', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ActivityEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -330,7 +353,10 @@ class ActivityControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/activities/' . $imports[2]->getId(), 'PATCH', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ActivityEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
|
||||
@@ -96,7 +96,10 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/api/customers');
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
@@ -109,7 +112,10 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
$query = ['order' => 'ASC', 'orderBy' => 'name', 'visible' => 3, 'term' => 'test'];
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/api/customers', 'GET', $query);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
@@ -117,11 +123,19 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
self::assertApiResponseTypeStructure('CustomerCollection', $result[0]);
|
||||
}
|
||||
|
||||
public function testGetEntityIsSecure(): void
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/customers/1');
|
||||
}
|
||||
|
||||
public function testGetEntity(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/api/customers/1');
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
@@ -129,7 +143,7 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
|
||||
public function testGetEntityWithFullResponse(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
|
||||
@@ -165,7 +179,10 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
$em->flush();
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/customers/1');
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
@@ -191,7 +208,10 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/customers', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -209,7 +229,10 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/customers', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -262,7 +285,10 @@ class CustomerControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/customers/1', 'PATCH', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('CustomerEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
|
||||
@@ -97,7 +97,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->assertAccessIsGranted($client, '/api/projects');
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
@@ -206,7 +209,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
}
|
||||
|
||||
$this->assertAccessIsGranted($client, $url, 'GET', $parameters);
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEquals(\count($expected), \count($result), 'Found wrong amount of projects');
|
||||
@@ -242,9 +248,27 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
yield ['/api/projects', 1, ['customers' => ['2', '2'], 'visible' => VisibilityInterface::SHOW_HIDDEN, 'start' => '2010-12-11', 'end' => '2030-12-11'], []];
|
||||
}
|
||||
|
||||
public function testGetEntityIsSecure(): void
|
||||
{
|
||||
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/api/projects/1');
|
||||
}
|
||||
|
||||
public function testGetEntity(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/api/projects/1');
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
}
|
||||
|
||||
public function testGetEntityComplex(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$em = $this->getEntityManager();
|
||||
|
||||
$customer = new Customer('first one');
|
||||
@@ -268,7 +292,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$em->flush();
|
||||
|
||||
$this->assertAccessIsGranted($client, '/api/projects/' . $project->getId());
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
@@ -318,7 +345,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -344,7 +374,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -367,7 +400,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -390,7 +426,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -410,7 +449,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/projects', 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -461,7 +503,10 @@ class ProjectControllerTest extends APIControllerBaseTest
|
||||
$this->request($client, '/api/projects/1', 'PATCH', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
self::assertApiResponseTypeStructure('ProjectEntity', $result);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
|
||||
@@ -20,20 +20,14 @@ use App\Entity\User;
|
||||
*/
|
||||
trait RateControllerTestTrait
|
||||
{
|
||||
/**
|
||||
* @param string|int $id
|
||||
* @param string|int|null $rateId
|
||||
* @return string
|
||||
*/
|
||||
abstract protected function getRateUrl($id = '1', $rateId = null): string;
|
||||
abstract protected function getRateUrl(string|int $id = '1', string|int|null $rateId = null): string;
|
||||
|
||||
abstract protected function getRateUrlByRate(RateInterface $rate, bool $isCollection): string;
|
||||
|
||||
/**
|
||||
* @param string|int $id
|
||||
* @return RateInterface[]
|
||||
*/
|
||||
abstract protected function importTestRates($id): array;
|
||||
abstract protected function importTestRates(string|int $id): array;
|
||||
|
||||
public function testAddRateMissingEntityAction(): void
|
||||
{
|
||||
@@ -89,7 +83,10 @@ trait RateControllerTestTrait
|
||||
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertRateStructure($result, null);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -110,7 +107,10 @@ trait RateControllerTestTrait
|
||||
$this->request($client, $this->getRateUrl(), 'POST', [], json_encode($data));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertRateStructure($result, 1);
|
||||
$this->assertNotEmpty($result['id']);
|
||||
@@ -125,7 +125,10 @@ trait RateControllerTestTrait
|
||||
$this->request($client, $this->getRateUrl(1));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
@@ -138,7 +141,10 @@ trait RateControllerTestTrait
|
||||
$this->request($client, $this->getRateUrl(1));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertNotEmpty($result);
|
||||
$this->assertEquals(\count($expectedRates), \count($result));
|
||||
@@ -171,7 +177,10 @@ trait RateControllerTestTrait
|
||||
$this->request($client, $this->getRateUrlByRate($expectedRates[0], true));
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
|
||||
$result = json_decode($client->getResponse()->getContent(), true);
|
||||
$content = $client->getResponse()->getContent();
|
||||
$this->assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
$this->assertIsArray($result);
|
||||
$this->assertEquals(\count($expectedRates) - 1, \count($result));
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
|
||||
}
|
||||
|
||||
public function testGetCollectionForAllUser(): void
|
||||
public function testGetCollectionForAllUserIsSecure(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||
$this->importFixtureForUser(User::ROLE_USER);
|
||||
@@ -136,6 +136,53 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
||||
self::assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertEmpty($result);
|
||||
self::assertEquals(0, \count($result));
|
||||
}
|
||||
|
||||
public function testGetCollectionForAllUserIsSecureForUser(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$this->importFixtureForUser(User::ROLE_USER);
|
||||
|
||||
$fixture = new TimesheetFixtures($this->getUserByRole(User::ROLE_ADMIN), 7);
|
||||
$fixture->setFixedRate(true);
|
||||
$fixture->setHourlyRate(true);
|
||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||
$this->importFixture($fixture);
|
||||
|
||||
$query = ['user' => 'all'];
|
||||
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertNotEmpty($result);
|
||||
self::assertEquals(10, \count($result));
|
||||
self::assertApiResponseTypeStructure('TimesheetCollection', $result[0]);
|
||||
}
|
||||
|
||||
public function testGetCollectionForAllUser(): void
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->importFixtureForUser(User::ROLE_USER);
|
||||
|
||||
$fixture = new TimesheetFixtures($this->getUserByRole(User::ROLE_SUPER_ADMIN), 7);
|
||||
$fixture->setFixedRate(true);
|
||||
$fixture->setHourlyRate(true);
|
||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||
$this->importFixture($fixture);
|
||||
|
||||
$query = ['user' => 'all'];
|
||||
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
|
||||
|
||||
$content = $client->getResponse()->getContent();
|
||||
self::assertIsString($content);
|
||||
$result = json_decode($content, true);
|
||||
|
||||
self::assertIsArray($result);
|
||||
self::assertNotEmpty($result);
|
||||
self::assertEquals(17, \count($result));
|
||||
|
||||
@@ -19,9 +19,9 @@ trait EntityValidationTestTrait
|
||||
{
|
||||
/**
|
||||
* @param object $entity
|
||||
* @param array|string $fieldNames
|
||||
* @param array<string>|string $fieldNames
|
||||
*/
|
||||
public function assertHasViolationForField(object $entity, $fieldNames, $groups = null): void
|
||||
public function assertHasViolationForField(object $entity, array|string $fieldNames, $groups = null): void
|
||||
{
|
||||
self::bootKernel();
|
||||
/** @var ValidatorInterface $validator */
|
||||
|
||||
@@ -24,7 +24,7 @@ class TimesheetValidationTest extends KernelTestCase
|
||||
{
|
||||
use EntityValidationTestTrait;
|
||||
|
||||
protected function getEntity()
|
||||
protected function getEntity(): Timesheet
|
||||
{
|
||||
$customer = new Customer('Test Customer');
|
||||
|
||||
|
||||
@@ -61,6 +61,8 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
||||
public function getTestData(): array
|
||||
{
|
||||
$invoiceDate = new \DateTime();
|
||||
$invoiceDateFixed = new \DateTime('2021-03-07');
|
||||
$invoiceDateFixed1 = new \DateTime('2021-03-27');
|
||||
|
||||
return [
|
||||
// simple tests for single calls
|
||||
@@ -109,6 +111,7 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
||||
['{c+13,3}', '014', $invoiceDate],
|
||||
['{c+13,2}', '14', $invoiceDate],
|
||||
['{c+13}', '14', $invoiceDate],
|
||||
['{c-20}', '0', $invoiceDate],
|
||||
['{cd+111}', '112', $invoiceDate],
|
||||
['{cd+111,5}', '00112', $invoiceDate],
|
||||
['{cd+111,2}', '113', $invoiceDate, 2],
|
||||
@@ -128,7 +131,7 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
||||
['{Y,5}/{cy,5}', '0' . $invoiceDate->format('Y') . '/00002', $invoiceDate],
|
||||
// with decrementing counter
|
||||
['{c-1,2}', '00', $invoiceDate],
|
||||
['{c-2,2}', '-1', $invoiceDate],
|
||||
['{c-2,2}', '00', $invoiceDate],
|
||||
// with incrementing and decrementing counter
|
||||
['{c-5+13,1}', '9', $invoiceDate],
|
||||
['{c+13-5,2}', '09', $invoiceDate],
|
||||
@@ -142,6 +145,43 @@ class ConfigurableNumberGeneratorTest extends TestCase
|
||||
['{cm+22+2+2-1-13-4}', '9', $invoiceDate],
|
||||
['{cm+22+2-2-22}', '6', $invoiceDate, 5],
|
||||
['{cm-21+22+2-2}', '6', $invoiceDate, 5],
|
||||
// increment dates
|
||||
['{YY}', '2022', $invoiceDateFixed],
|
||||
['{YY+1}', '2022', $invoiceDateFixed],
|
||||
['{YY+2}', '2023', $invoiceDateFixed],
|
||||
['{YY+3}', '2024', $invoiceDateFixed],
|
||||
['{YY-1}', '2020', $invoiceDateFixed],
|
||||
['{YY-2}', '2019', $invoiceDateFixed],
|
||||
['{YY-3}', '2018', $invoiceDateFixed],
|
||||
['{yy}', '22', $invoiceDateFixed],
|
||||
['{yy+1}', '22', $invoiceDateFixed],
|
||||
['{yy+2}', '23', $invoiceDateFixed],
|
||||
['{yy+3}', '24', $invoiceDateFixed],
|
||||
['{yy-1}', '20', $invoiceDateFixed],
|
||||
['{yy-2}', '19', $invoiceDateFixed],
|
||||
['{yy-3}', '18', $invoiceDateFixed],
|
||||
['{MM}', '4', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{MM+1}', '4', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{MM+2}', '5', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{MM+3}', '6', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{MM-1}', '2', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{MM-2}', '1', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{MM-3}', '0', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{MM-4}', '0', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD}', '8', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD+1}', '8', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD+2}', '9', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD+3}', '10', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD-1}', '6', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD-2}', '5', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD-3}', '4', $invoiceDateFixed], // cast to int removes leading zero
|
||||
['{DD}', '28', $invoiceDateFixed1], // cast to int removes leading zero
|
||||
['{DD+1}', '28', $invoiceDateFixed1], // cast to int removes leading zero
|
||||
['{DD+2}', '29', $invoiceDateFixed1], // cast to int removes leading zero
|
||||
['{DD+3}', '30', $invoiceDateFixed1], // cast to int removes leading zero
|
||||
['{DD-1}', '26', $invoiceDateFixed1], // cast to int removes leading zero
|
||||
['{DD-2}', '25', $invoiceDateFixed1], // cast to int removes leading zero
|
||||
['{DD-3}', '24', $invoiceDateFixed1], // cast to int removes leading zero
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -463,6 +463,8 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
'twentyFourHours' => false,
|
||||
'updateBrowserTitle' => false,
|
||||
'timezone' => 'America/Edmonton',
|
||||
'locale' => 'en',
|
||||
'language' => 'de',
|
||||
'user' => [
|
||||
'id' => 1,
|
||||
'name' => null,
|
||||
@@ -473,6 +475,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
$user = $this->createMock(User::class);
|
||||
$user->method('getId')->willReturn(1);
|
||||
$user->method('getName')->willReturn('Testing');
|
||||
$user->method('getLanguage')->willReturn('de');
|
||||
$user->method('isAdmin')->willReturn(false);
|
||||
$user->method('isSuperAdmin')->willReturn(false);
|
||||
$user->method('getTimezone')->willReturn('America/Edmonton');
|
||||
|
||||
@@ -121,7 +121,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate('foo', new TimesheetBudgetUsed()); // @phpstan-ignore-line
|
||||
$this->validator->validate('foo', new TimesheetBudgetUsed());
|
||||
}
|
||||
|
||||
public function testWithMissingEnd(): void
|
||||
|
||||
@@ -59,7 +59,7 @@ class TimesheetExportedValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetExported(['message' => 'myMessage'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetExported(['message' => 'myMessage']));
|
||||
}
|
||||
|
||||
public function testTriggersOnMissingPermission(): void
|
||||
|
||||
@@ -60,7 +60,7 @@ class TimesheetFutureTimesValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetFutureTimes(['message' => 'myMessage'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetFutureTimes(['message' => 'myMessage']));
|
||||
}
|
||||
|
||||
public function testFutureBeginIsDisallowed(): void
|
||||
|
||||
@@ -75,7 +75,7 @@ class TimesheetLockdownValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetLockdown(['message' => 'myMessage'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetLockdown(['message' => 'myMessage']));
|
||||
}
|
||||
|
||||
public function testValidatorWithoutNowConstraint(): void
|
||||
|
||||
@@ -55,7 +55,7 @@ class TimesheetLongRunningValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetLongRunning(['message' => 'myMessage'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetLongRunning(['message' => 'myMessage']));
|
||||
}
|
||||
|
||||
public function testLongRunningTriggers(): void
|
||||
|
||||
@@ -58,7 +58,7 @@ class TimesheetOverlappingValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage']));
|
||||
}
|
||||
|
||||
public function testOverlappingDisallowedWithRecords(): void
|
||||
|
||||
@@ -57,7 +57,7 @@ class TimesheetRestartValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetOverlapping(['message' => 'myMessage']));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -44,6 +44,6 @@ class TimesheetValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetConstraint(['message' => 'myMessage'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetConstraint(['message' => 'myMessage']));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ class TimesheetZeroDurationValidatorTest extends ConstraintValidatorTestCase
|
||||
{
|
||||
$this->expectException(UnexpectedTypeException::class);
|
||||
|
||||
$this->validator->validate(new NotBlank(), new TimesheetZeroDuration(['message' => 'Duration cannot be zero.'])); // @phpstan-ignore-line
|
||||
$this->validator->validate(new NotBlank(), new TimesheetZeroDuration(['message' => 'Duration cannot be zero.']));
|
||||
}
|
||||
|
||||
private function prepareTimesheet()
|
||||
|
||||
@@ -202,11 +202,6 @@ parameters:
|
||||
count: 1
|
||||
path: API/ActivityControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
|
||||
count: 12
|
||||
path: API/ActivityControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
|
||||
count: 13
|
||||
@@ -297,11 +292,6 @@ parameters:
|
||||
count: 1
|
||||
path: API/CustomerControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
|
||||
count: 12
|
||||
path: API/CustomerControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
|
||||
count: 13
|
||||
@@ -382,11 +372,6 @@ parameters:
|
||||
count: 1
|
||||
path: API/ProjectControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$json of function json_decode expects string, string\\|false given\\.$#"
|
||||
count: 14
|
||||
path: API/ProjectControllerTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#5 \\$content of method App\\\\Tests\\\\API\\\\APIControllerBaseTest\\:\\:request\\(\\) expects string\\|null, string\\|false given\\.$#"
|
||||
count: 15
|
||||
@@ -1657,21 +1642,11 @@ parameters:
|
||||
count: 1
|
||||
path: Entity/TimesheetValidationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Entity\\\\TimesheetValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$fieldNames with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Entity/TimesheetValidationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Entity\\\\TimesheetValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$groups with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Entity/TimesheetValidationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Entity\\\\TimesheetValidationTest\\:\\:getEntity\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Entity/TimesheetValidationTest.php
|
||||
|
||||
-
|
||||
message: "#^Cannot access offset 0 on iterable\\<App\\\\Entity\\\\Team\\>\\.$#"
|
||||
count: 1
|
||||
@@ -1692,11 +1667,6 @@ parameters:
|
||||
count: 1
|
||||
path: Entity/UserValidationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$fieldNames with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Entity/UserValidationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:assertHasViolationForField\\(\\) has parameter \\$groups with no type specified\\.$#"
|
||||
count: 1
|
||||
|
||||
Reference in New Issue
Block a user