Release 2.42 (#5686)

This commit is contained in:
Kevin Papst
2025-11-12 16:15:04 +01:00
committed by GitHub
parent ea29799b89
commit 8e6764b67a
42 changed files with 377 additions and 416 deletions

View File

@@ -12,8 +12,6 @@ namespace App\API;
use App\Activity\ActivityService;
use App\Entity\Activity;
use App\Entity\ActivityRate;
use App\Entity\User;
use App\Event\ActivityMetaDefinitionEvent;
use App\Form\API\ActivityApiEditForm;
use App\Form\API\ActivityRateApiForm;
use App\Repository\ActivityRateRepository;
@@ -26,7 +24,6 @@ use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use OpenApi\Attributes as OA;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -46,7 +43,6 @@ final class ActivityController extends BaseApiController
public function __construct(
private readonly ViewHandlerInterface $viewHandler,
private readonly ActivityRepository $repository,
private readonly EventDispatcherInterface $dispatcher,
private readonly ActivityRateRepository $activityRateRepository,
private readonly ActivityService $activityService
) {
@@ -63,25 +59,12 @@ final class ActivityController extends BaseApiController
#[Rest\QueryParam(name: 'globals', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Use if you want to fetch only global activities. Allowed values: 0|1 (default: 0 for false)')]
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name|project', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name, project (default: name)')]
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
#[Rest\QueryParam(name: 'term', description: 'Free search term', nullable: true)]
public function cgetAction(ParamFetcherInterface $paramFetcher, ProjectRepository $projectRepository): Response
{
/** @var User $user */
$user = $this->getUser();
$query = new ActivityQuery();
$query->loadTeams();
$query->setCurrentUser($user);
$order = $paramFetcher->get('order');
if (\is_string($order) && $order !== '') {
$query->setOrder($order);
}
$orderBy = $paramFetcher->get('orderBy');
if (\is_string($orderBy) && $orderBy !== '') {
$query->setOrderBy($orderBy);
}
$this->prepareQuery($query, $paramFetcher);
$globals = $paramFetcher->get('globals');
if (\is_string($globals) && ($globals === 'true' || $globals === '1')) {
@@ -113,7 +96,6 @@ final class ActivityController extends BaseApiController
$query->setSearchTerm(new SearchTerm($term));
}
$query->setIsApiCall(true);
$data = $this->repository->getActivitiesForQuery($query);
$view = new View($data, 200);
$view->getContext()->setGroups(self::GROUPS_COLLECTION);
@@ -149,9 +131,7 @@ final class ActivityController extends BaseApiController
}
$activity = new Activity();
$event = new ActivityMetaDefinitionEvent($activity);
$this->dispatcher->dispatch($event);
$this->activityService->loadMetaFields($activity);
$form = $this->createForm(ActivityApiEditForm::class, $activity, [
'include_budget' => $this->isGranted('budget', $activity),
@@ -185,8 +165,7 @@ final class ActivityController extends BaseApiController
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_activity', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Activity $activity): Response
{
$event = new ActivityMetaDefinitionEvent($activity);
$this->dispatcher->dispatch($event);
$this->activityService->loadMetaFields($activity);
$form = $this->createForm(ActivityApiEditForm::class, $activity, [
'include_budget' => $this->isGranted('budget', $activity),
@@ -241,8 +220,7 @@ final class ActivityController extends BaseApiController
#[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')]
public function metaAction(Activity $activity, ParamFetcherInterface $paramFetcher): Response
{
$event = new ActivityMetaDefinitionEvent($activity);
$this->dispatcher->dispatch($event);
$this->activityService->loadMetaFields($activity);
$name = $paramFetcher->get('name');
$value = $paramFetcher->get('value');

View File

@@ -21,6 +21,7 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
abstract class BaseApiController extends AbstractController
{
public const MAX_PAGE_SIZE = 500;
public const DATE_ONLY_FORMAT = 'yyyy-MM-dd';
public const DATE_FORMAT = DateTimeType::HTML5_FORMAT;
public const DATE_FORMAT_PHP = 'Y-m-d\TH:i:s';
@@ -84,8 +85,8 @@ abstract class BaseApiController extends AbstractController
$size = $all['size'];
if (is_numeric($size)) {
$size = (int) $size;
if ($size < 1 || $size > 500) {
throw new BadRequestHttpException('Size must be between 1 and 500');
if ($size < 1 || $size > self::MAX_PAGE_SIZE) {
throw new BadRequestHttpException('Size must be between 1 and ' . self::MAX_PAGE_SIZE);
}
$query->setPageSize($size);
}

View File

@@ -23,16 +23,12 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Default')]
final class ConfigurationController extends BaseApiController
{
public function __construct(private readonly ViewHandlerInterface $viewHandler)
{
}
/**
* Fetch timesheet configuration
*/
#[OA\Response(response: 200, description: 'Returns the instance specific timesheet configuration', content: new OA\JsonContent(ref: new Model(type: TimesheetConfig::class)))]
#[Route(path: '/config/timesheet', methods: ['GET'])]
public function timesheetConfigAction(SystemConfiguration $configuration): Response
public function timesheetConfigAction(SystemConfiguration $configuration, ViewHandlerInterface $viewHandler): Response
{
$model = new TimesheetConfig();
$model->setTrackingMode($configuration->getTimesheetTrackingMode());
@@ -44,7 +40,7 @@ final class ConfigurationController extends BaseApiController
$view = new View($model, 200);
$view->getContext()->setGroups(['Default', 'Config']);
return $this->viewHandler->handle($view);
return $viewHandler->handle($view);
}
/**
@@ -52,11 +48,11 @@ final class ConfigurationController extends BaseApiController
*/
#[OA\Response(response: 200, description: 'Returns the configured color codes and names', content: new OA\JsonContent(type: 'object', example: ['Red' => '#ff0000'], additionalProperties: new OA\AdditionalProperties(type: 'string')))]
#[Route(path: '/config/colors', methods: ['GET'])]
public function colorConfigAction(SystemConfiguration $configuration): Response
public function colorConfigAction(SystemConfiguration $configuration, ViewHandlerInterface $viewHandler): Response
{
$view = new View($configuration->getThemeColors(), 200);
$view->getContext()->setGroups(['Default']);
return $this->viewHandler->handle($view);
return $viewHandler->handle($view);
}
}

View File

@@ -13,7 +13,6 @@ use App\Customer\CustomerService;
use App\Entity\Customer;
use App\Entity\CustomerRate;
use App\Entity\User;
use App\Event\CustomerMetaDefinitionEvent;
use App\Form\API\CustomerApiEditForm;
use App\Form\API\CustomerRateApiForm;
use App\Repository\CustomerRateRepository;
@@ -25,7 +24,6 @@ use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use OpenApi\Attributes as OA;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -45,7 +43,6 @@ final class CustomerController extends BaseApiController
public function __construct(
private readonly ViewHandlerInterface $viewHandler,
private readonly CustomerRepository $repository,
private readonly EventDispatcherInterface $dispatcher,
private readonly CustomerRateRepository $customerRateRepository,
private readonly CustomerService $customerService,
) {
@@ -59,7 +56,7 @@ final class CustomerController extends BaseApiController
#[Rest\QueryParam(name: 'visible', requirements: '1|2|3', default: 1, strict: true, nullable: true, description: 'Visibility status to filter customers: 1=visible, 2=hidden, 3=both')]
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name (default: name)')]
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
#[Rest\QueryParam(name: 'term', description: 'Free search term', nullable: true)]
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
{
/** @var User $user */
@@ -125,9 +122,6 @@ final class CustomerController extends BaseApiController
$customer = $customerService->createNewCustomer('');
$event = new CustomerMetaDefinitionEvent($customer);
$this->dispatcher->dispatch($event);
$form = $this->createForm(CustomerApiEditForm::class, $customer, [
'include_budget' => $this->isGranted('budget', $customer),
'include_time' => $this->isGranted('time', $customer),
@@ -160,8 +154,7 @@ final class CustomerController extends BaseApiController
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_customer', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Customer $customer): Response
{
$event = new CustomerMetaDefinitionEvent($customer);
$this->dispatcher->dispatch($event);
$this->customerService->loadMetaFields($customer);
$form = $this->createForm(CustomerApiEditForm::class, $customer, [
'include_budget' => $this->isGranted('budget', $customer),
@@ -216,8 +209,7 @@ final class CustomerController extends BaseApiController
#[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')]
public function metaAction(Customer $customer, ParamFetcherInterface $paramFetcher): Response
{
$event = new CustomerMetaDefinitionEvent($customer);
$this->dispatcher->dispatch($event);
$this->customerService->loadMetaFields($customer);
$name = $paramFetcher->get('name');
$value = $paramFetcher->get('value');

View File

@@ -23,12 +23,6 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Export')]
final class ExportController extends BaseApiController
{
public function __construct(
private readonly ViewHandlerInterface $viewHandler,
private readonly ExportTemplateRepository $repository,
) {
}
/**
* Delete export template
*/
@@ -36,12 +30,12 @@ final class ExportController extends BaseApiController
#[OA\Delete(responses: [new OA\Response(response: 204, description: 'Delete export template')], x: ['internal' => true])]
#[OA\Parameter(name: 'id', description: 'Export template ID to delete', in: 'path', required: true)]
#[Route(path: '/{id}', name: 'delete_export_template', requirements: ['id' => '\d+'], methods: ['DELETE'])]
public function deleteTemplate(ExportTemplate $exportTemplate): Response
public function deleteTemplate(ExportTemplate $exportTemplate, ExportTemplateRepository $repository, ViewHandlerInterface $viewHandler): Response
{
$this->repository->removeExportTemplate($exportTemplate);
$repository->removeExportTemplate($exportTemplate);
$view = new View(null, Response::HTTP_NO_CONTENT);
return $this->viewHandler->handle($view);
return $viewHandler->handle($view);
}
}

View File

@@ -12,7 +12,6 @@ namespace App\API;
use App\Entity\Project;
use App\Entity\ProjectRate;
use App\Entity\User;
use App\Event\ProjectMetaDefinitionEvent;
use App\Form\API\ProjectApiEditForm;
use App\Form\API\ProjectRateApiForm;
use App\Project\ProjectService;
@@ -26,7 +25,6 @@ use FOS\RestBundle\Request\ParamFetcherInterface;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use OpenApi\Attributes as OA;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -47,7 +45,6 @@ final class ProjectController extends BaseApiController
public function __construct(
private readonly ViewHandlerInterface $viewHandler,
private readonly ProjectRepository $repository,
private readonly EventDispatcherInterface $dispatcher,
private readonly ProjectRateRepository $projectRateRepository,
private readonly ProjectService $projectService
) {
@@ -67,7 +64,7 @@ final class ProjectController extends BaseApiController
#[Rest\QueryParam(name: 'globalActivities', requirements: '0|1', strict: true, nullable: true, description: "If given, filters projects by their 'global activity' support. Allowed values: 1 (supports global activities) and 0 (without global activities) (default: all)")]
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|name|customer', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, name, customer (default: name)')]
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
#[Rest\QueryParam(name: 'term', description: 'Free search term', nullable: true)]
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository): Response
{
/** @var User $user */
@@ -212,8 +209,7 @@ final class ProjectController extends BaseApiController
#[Route(methods: ['PATCH'], path: '/{id}', name: 'patch_project', requirements: ['id' => '\d+'])]
public function patchAction(Request $request, Project $project): Response
{
$event = new ProjectMetaDefinitionEvent($project);
$this->dispatcher->dispatch($event);
$this->projectService->loadMetaFields($project);
$form = $this->createForm(ProjectApiEditForm::class, $project, [
'timezone' => $this->getDateTimeFactory()->getTimezone()->getName(),
@@ -270,8 +266,7 @@ final class ProjectController extends BaseApiController
#[Rest\RequestParam(name: 'value', strict: true, nullable: false, description: 'The meta-field value')]
public function metaAction(Project $project, ParamFetcherInterface $paramFetcher): Response
{
$event = new ProjectMetaDefinitionEvent($project);
$this->dispatcher->dispatch($event);
$this->projectService->loadMetaFields($project);
$name = $paramFetcher->get('name');
$value = $paramFetcher->get('value');

View File

@@ -92,7 +92,7 @@ final class TimesheetController extends BaseApiController
#[Rest\QueryParam(name: 'active', requirements: '0|1', strict: true, nullable: true, description: 'Filter for running/active records. Allowed values: 0=stopped, 1=active (default: all)')]
#[Rest\QueryParam(name: 'billable', requirements: '0|1', strict: true, nullable: true, description: 'Filter for non-/billable records. Allowed values: 0=non-billable, 1=billable (default: all)')]
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
#[Rest\QueryParam(name: 'term', description: 'Free search term', nullable: true)]
#[Rest\QueryParam(name: 'modified_after', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records changed after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
public function cgetAction(ParamFetcherInterface $paramFetcher, CustomerRepository $customerRepository, ProjectRepository $projectRepository, ActivityRepository $activityRepository, UserRepository $userRepository): Response
{

View File

@@ -58,7 +58,7 @@ final class UserController extends BaseApiController
#[Rest\QueryParam(name: 'visible', requirements: '1|2|3', default: 1, strict: true, nullable: true, description: 'Visibility status to filter users: 1=visible, 2=hidden, 3=all')]
#[Rest\QueryParam(name: 'orderBy', requirements: 'id|username|alias|email', strict: true, nullable: true, description: 'The field by which results will be ordered. Allowed values: id, username, alias, email (default: username)')]
#[Rest\QueryParam(name: 'order', requirements: 'ASC|DESC', strict: true, nullable: true, description: 'The result order. Allowed values: ASC, DESC (default: ASC)')]
#[Rest\QueryParam(name: 'term', description: 'Free search term')]
#[Rest\QueryParam(name: 'term', description: 'Free search term', nullable: true)]
#[Rest\QueryParam(name: 'full', requirements: '0|1|true|false', strict: true, nullable: true, description: 'Allows to fetch full objects including subresources. Allowed values: 0|1|false|true (default: false)')]
public function cgetAction(ParamFetcherInterface $paramFetcher): Response
{