Release 2.34 (#5465)

* fix timing issue in timesheet edit form with deactivated rounding
* bump packages
* only show update messages for newer plugin versions
* replace deprecated method
* remove internal from API
* remove technical terms from translation
* prevent calls to internal symfony methods
* helper method to flag entry as modified
* support meta-fields in weekly-hourse view
* fix running timesheets were deleted in weekly-hourse
This commit is contained in:
Kevin Papst
2025-05-09 14:22:47 +02:00
committed by GitHub
parent 452a8d9390
commit dfd97fd6f3
35 changed files with 428 additions and 362 deletions

409
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1907,46 +1907,16 @@ parameters:
count: 1
path: src/Form/Extension/SelectWithApiDataExtension.php
-
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#"
count: 2
path: src/Form/Helper/ActivityHelper.php
-
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
count: 1
path: src/Form/Helper/ActivityHelper.php
-
message: "#^Property App\\\\Form\\\\Helper\\\\ActivityHelper\\:\\:\\$pattern \\(string\\|null\\) does not accept bool\\|float\\|int\\|string\\|null\\.$#"
count: 1
path: src/Form/Helper/ActivityHelper.php
-
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#"
count: 2
path: src/Form/Helper/CustomerHelper.php
-
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
count: 1
path: src/Form/Helper/CustomerHelper.php
-
message: "#^Property App\\\\Form\\\\Helper\\\\CustomerHelper\\:\\:\\$pattern \\(string\\|null\\) does not accept bool\\|float\\|int\\|string\\|null\\.$#"
count: 1
path: src/Form/Helper/CustomerHelper.php
-
message: "#^Parameter \\#1 \\$haystack of function stripos expects string, bool\\|float\\|int\\|string given\\.$#"
count: 2
path: src/Form/Helper/ProjectHelper.php
-
message: "#^Parameter \\#3 \\$subject of function str_replace expects array\\<string\\>\\|string, bool\\|float\\|int\\|string given\\.$#"
count: 1
path: src/Form/Helper/ProjectHelper.php
-
message: "#^Property App\\\\Form\\\\Helper\\\\ProjectHelper\\:\\:\\$pattern \\(string\\|null\\) does not accept bool\\|float\\|int\\|string\\|null\\.$#"
count: 1
@@ -3262,11 +3232,6 @@ parameters:
count: 1
path: src/Ldap/LdapAuthenticator.php
-
message: "#^Instanceof between Symfony\\\\Component\\\\Security\\\\Http\\\\Authenticator\\\\Passport\\\\Passport and Symfony\\\\Component\\\\Security\\\\Http\\\\Authenticator\\\\Passport\\\\Passport will always evaluate to true\\.$#"
count: 1
path: src/Ldap/LdapCredentialsSubscriber.php
-
message: "#^Method App\\\\Ldap\\\\LdapCredentialsSubscriber\\:\\:onCheckPassport\\(\\) has no return type specified\\.$#"
count: 1
@@ -3912,26 +3877,6 @@ parameters:
count: 1
path: src/Timesheet/TimesheetStatisticService.php
-
message: "#^Method App\\\\Timesheet\\\\TrackingMode\\\\AbstractTrackingMode\\:\\:setBeginEndFromRequest\\(\\) has no return type specified\\.$#"
count: 1
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
-
message: "#^Method App\\\\Timesheet\\\\TrackingMode\\\\AbstractTrackingMode\\:\\:setFromToFromRequest\\(\\) has no return type specified\\.$#"
count: 1
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
-
message: "#^Parameter \\#1 \\$datetime of class DateTime constructor expects string, mixed given\\.$#"
count: 2
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
-
message: "#^Parameter \\#2 \\$datetime of static method DateTime\\:\\:createFromFormat\\(\\) expects string, mixed given\\.$#"
count: 2
path: src/Timesheet/TrackingMode/AbstractTrackingMode.php
-
message: "#^Method App\\\\Twig\\\\Configuration\\:\\:__call\\(\\) has no return type specified\\.$#"
count: 1

View File

@@ -23,7 +23,11 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerInterface
{
public function __construct(private TranslatorInterface $translator, private FlattenExceptionHandler $exceptionHandler, private Security $security)
public function __construct(
private readonly TranslatorInterface $translator,
private readonly FlattenExceptionHandler $exceptionHandler, // @phpstan-ignore-line
private readonly Security $security
)
{
}
@@ -47,7 +51,7 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
public function serializeExceptionToJson(JsonSerializationVisitor $visitor, FlattenException $exception, array $type, Context $context)
{
if ($exception->getClass() !== ValidationFailedException::class) {
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context);
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context); // @phpstan-ignore method.internalClass
}
$original = $context->getAttribute('exception');
@@ -55,7 +59,7 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
return $this->serializeValidationExceptionToJson($visitor, $original, $type, $context);
}
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context);
return $this->exceptionHandler->serializeToJson($visitor, $exception, $type, $context); // @phpstan-ignore method.internalClass
}
public function serializeValidationExceptionToJson(JsonSerializationVisitor $visitor, ValidationFailedException $exception, array $type, Context $context)

View File

@@ -483,25 +483,17 @@ final class SystemConfiguration
return (bool) $this->find('theme.avatar_url');
}
/**
* @internal will be made private soon after 2.18.0 - do not access this method directly, but through getThemeColors()
*/
public function getThemeColorChoices(): string
{
$config = $this->find('theme.color_choices');
if (\is_string($config) && $config !== '') {
return $config;
}
return 'Silver|#c0c0c0';
}
/**
* @return array<string, string>
*/
public function getThemeColors(): array
{
$config = explode(',', $this->getThemeColorChoices());
$config = $this->find('theme.color_choices');
if (!\is_string($config) || $config === '') {
return ['Silver' => '#c0c0c0'];
}
$config = explode(',', $config);
$colors = [];
foreach ($config as $item) {

View File

@@ -17,11 +17,11 @@ final class Constants
/**
* The current release version
*/
public const VERSION = '2.33.0';
public const VERSION = '2.34.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 23300;
public const VERSION_ID = 23400;
/**
* The software name
*/

View File

@@ -60,10 +60,10 @@ use Twig\Environment;
final class InvoiceController extends AbstractController
{
public function __construct(
private ServiceInvoice $service,
private InvoiceTemplateRepository $templateRepository,
private InvoiceRepository $invoiceRepository,
private EventDispatcherInterface $dispatcher
private readonly ServiceInvoice $service,
private readonly InvoiceTemplateRepository $templateRepository,
private readonly InvoiceRepository $invoiceRepository,
private readonly EventDispatcherInterface $dispatcher
) {
}
@@ -333,7 +333,7 @@ final class InvoiceController extends AbstractController
{
$invoice = null;
if (null !== ($id = $request->get('id'))) {
if (null !== ($id = $request->query->get('id'))) {
$invoice = $this->invoiceRepository->find($id);
}

View File

@@ -28,7 +28,7 @@ final class PluginController extends AbstractController
$installed = [];
$plugins = $manager->getPlugins();
foreach ($plugins as $plugin) {
$installed[] = $plugin->getId();
$installed[$plugin->getId()] = $plugin;
}
$page = new PageSetup('menu.plugin');
@@ -36,18 +36,23 @@ final class PluginController extends AbstractController
$all = $this->getPluginInformation($client, $cache);
$bundles = [];
$updates = [];
foreach ($all as $item) {
if ($item['bundle'] !== null) {
$bundles[$item['bundle']] = $item;
if (\array_key_exists($item['bundle'], $installed)) {
$updates[$item['bundle']] = version_compare($installed[$item['bundle']]->getMetadata()->getVersion(), $item['latest_release']) === -1;
}
}
}
return $this->render('plugin/index.html.twig', [
'page_setup' => $page,
'plugins' => $plugins,
'installed' => $installed,
'installed' => array_keys($installed),
'extensions' => $all,
'bundles' => $bundles,
'updates' => $updates,
]);
}

View File

@@ -10,6 +10,7 @@
namespace App\Controller;
use App\Configuration\SystemConfiguration;
use App\Event\QuickEntryMetaDisplayEvent;
use App\Form\QuickEntryForm;
use App\Form\WeekByUserForm;
use App\Model\QuickEntryWeek;
@@ -19,6 +20,7 @@ use App\Repository\TimesheetRepository;
use App\Timesheet\FavoriteRecordService;
use App\Timesheet\TimesheetService;
use App\Utils\PageSetup;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -34,7 +36,8 @@ final class QuickEntryController extends AbstractController
private readonly SystemConfiguration $configuration,
private readonly TimesheetService $timesheetService,
private readonly TimesheetRepository $repository,
private readonly FavoriteRecordService $favoriteRecordService
private readonly FavoriteRecordService $favoriteRecordService,
private readonly EventDispatcherInterface $dispatcher,
)
{
}
@@ -149,6 +152,11 @@ final class QuickEntryController extends AbstractController
$defaultHour = (int) $defaultBegin->format('H');
$defaultMinute = (int) $defaultBegin->format('i');
// find additional meta-fields exclusively for QuickEntry view
$event = new QuickEntryMetaDisplayEvent($query);
$this->dispatcher->dispatch($event);
$metaFields = $event->getFields();
$formModel = new QuickEntryWeek($startWeek);
foreach ($rows as $id => $row) {
@@ -168,10 +176,12 @@ final class QuickEntryController extends AbstractController
$model->addTimesheet($day['entry']);
}
}
$model->setMetaFields($metaFields);
}
// create prototype model
$empty = $formModel->createRow($user);
$empty->setMetaFields($metaFields);
$empty->markAsPrototype();
foreach ($week as $dayId => $day) {
$tmp = $this->timesheetService->createNewTimesheet($user);
@@ -219,7 +229,11 @@ final class QuickEntryController extends AbstractController
foreach ($tmpModel->getTimesheets() as $timesheet) {
if ($timesheet->getId() !== null) {
$duration = $timesheet->getDuration(false);
if ($duration === null || $timesheet->isRunning()) {
// previously running timesheets were deleted, which was wrong
// so now we distinguish between running timesheets and null duration
if ($timesheet->isRunning()) {
$saveTimesheets[] = $timesheet;
} elseif ($duration === null) {
$deleteTimesheets[] = $timesheet;
} else {
$saveTimesheets[] = $timesheet;
@@ -263,6 +277,7 @@ final class QuickEntryController extends AbstractController
'page_setup' => $page,
'days' => $week,
'form' => $form->createView(),
'metaColumns' => $metaFields,
]);
}
}

View File

@@ -147,9 +147,7 @@ abstract class TimesheetAbstractController extends AbstractController
$event = new TimesheetMetaDefinitionEvent($entry);
$this->dispatcher->dispatch($event);
$page = $request->get('page');
$page = is_numeric($page) ? (int) $page : 1;
$editForm = $this->getEditForm($entry, $page);
$editForm = $this->getEditForm($entry);
$editForm->handleRequest($request);
if ($editForm->isSubmitted() && $editForm->isValid()) {
@@ -157,7 +155,7 @@ abstract class TimesheetAbstractController extends AbstractController
$this->service->updateTimesheet($entry);
$this->flashSuccess('action.update.success');
return $this->redirectToRoute($this->getTimesheetRoute(), ['page' => $request->get('page', 1)]);
return $this->redirectToRoute($this->getTimesheetRoute());
} catch (\Exception $ex) {
$this->flashUpdateException($ex);
}
@@ -494,14 +492,13 @@ abstract class TimesheetAbstractController extends AbstractController
]);
}
private function getEditForm(Timesheet $entry, int $page): FormInterface
private function getEditForm(Timesheet $entry): FormInterface
{
$mode = $this->getTrackingMode();
return $this->createForm($this->getEditFormClassName(), $entry, [
'action' => $this->generateUrl($this->getEditRoute(), [
'id' => $entry->getId(),
'page' => $page,
]),
'include_rate' => $this->isGranted('edit_rate', $entry),
'include_exported' => $this->isGranted('edit_export', $entry),

View File

@@ -9,9 +9,6 @@
namespace App\Doctrine\Behavior;
/**
* @internal
*/
interface CreatedAt
{
public function getCreatedAt(): ?\DateTimeImmutable;

View File

@@ -25,4 +25,9 @@ trait ModifiedTrait
{
$this->modifiedAt = $dateTime;
}
public function markAsModified(): void
{
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
}
}

View File

@@ -224,7 +224,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
{
$this->tags = new ArrayCollection();
$this->meta = new ArrayCollection();
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
$this->markAsModified();
}
/**
@@ -611,7 +611,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
{
// this needs to be done, otherwise doctrine will not see the item as changed
// and the calculators will not run
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
$this->markAsModified();
if (null === ($current = $this->getMetaField($meta->getName()))) {
$meta->setEntity($this);
@@ -659,7 +659,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem, ModifiedAt
$this->id = null;
}
$this->setModifiedAt(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
$this->markAsModified();
$this->exported = false;
$currentMeta = $this->meta;

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of the Kimai time-tracking app.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace App\Event;
use App\Repository\Query\TimesheetQuery;
/**
* Dynamically find possible meta fields for the quick-entry screen.
*
* @method TimesheetQuery getQuery()
*/
final class QuickEntryMetaDisplayEvent extends AbstractMetaDisplayEvent
{
public function __construct(TimesheetQuery $query)
{
parent::__construct($query, 'quick-entry');
}
}

View File

@@ -21,7 +21,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface;
*/
final class MenuBuilderSubscriber implements EventSubscriberInterface
{
public function __construct(private MenuService $menuService)
public function __construct(private readonly MenuService $menuService)
{
}
@@ -53,7 +53,7 @@ final class MenuBuilderSubscriber implements EventSubscriberInterface
$event->addItem($menuEvent->getSystemMenu());
}
$route = $event->getRequest()->get('_route');
$route = $event->getRequest()->attributes->get('_route');
if (!\is_string($route)) {
return;
}

View File

@@ -33,6 +33,18 @@ final class QuickEntryForm extends AbstractType
$builder->addModelTransformer(new CallbackTransformer(
function ($value) {
// page is loaded, nothing to do
if (!$value instanceof QuickEntryWeek) {
return $value;
}
foreach ($value->getRows() as $row) {
foreach ($row->getTimesheets() as $timesheet) {
foreach ($timesheet->getMetaFields() as $metaField) {
$row->setMetaFieldValue($metaField);
}
}
}
return $value;
},
function (?QuickEntryWeek $value) {
@@ -49,6 +61,24 @@ final class QuickEntryForm extends AbstractType
foreach ($row->getTimesheets() as $timesheet) {
$timesheet->setProject($project);
$timesheet->setActivity($activity);
foreach ($row->getMetaFields() as $metaField) {
if (null === ($name = $metaField->getName())) {
continue;
}
if (null === $timesheet->getMetaField($name)) {
$timesheet->setMetaField($metaField);
}
if (null === ($tmpField = $timesheet->getMetaField($name))) {
continue;
}
if ($tmpField->getValue() !== $metaField->getValue()) {
$tmpField->setValue($metaField->getValue());
$timesheet->markAsModified();
}
}
}
}

View File

@@ -214,9 +214,9 @@ class TimesheetEditForm extends AbstractType
return;
}
// if the user did not change the time, make sure to keep the seconds
$seconds = 0;
if ($data->getBegin()?->format('H:i') === $time->format('H:i')) {
// if the user did not change the time, make sure to keep the seconds (ONLY if the timesheet is already existing)
if ($data->getBegin()?->format('H:i') === $time->format('H:i') && $data->getId() !== null) {
$seconds = $data->getBegin()->format('s') ?? 0;
}
@@ -276,9 +276,9 @@ class TimesheetEditForm extends AbstractType
throw new \Exception('Cannot work with timesheets without start time');
}
// if the user did not change the time, make sure to keep the seconds
$seconds = 0;
if ($oldEnd !== null && $oldEnd->format('H:i') === $end->format('H:i')) {
// if the user did not change the time, make sure to keep the seconds (ONLY if the timesheet is already existing)
if ($oldEnd !== null && $oldEnd->format('H:i') === $end->format('H:i') && $timesheet->getId() !== null) {
$seconds = $oldEnd->format('s') ?? 0;
}

View File

@@ -92,6 +92,8 @@ final class QuickEntryWeekType extends AbstractType
};
$builder->addEventListener(FormEvents::PRE_SUBMIT, $activityPreSubmitFunction);
$builder->add('metaFields', MetaFieldsCollectionType::class);
$builder->add('timesheets', CollectionType::class, [
'entry_type' => QuickEntryTimesheetType::class,
'label' => false,

View File

@@ -13,12 +13,11 @@ use App\Entity\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
final class LdapCredentialsSubscriber implements EventSubscriberInterface
{
public function __construct(private LdapManager $ldapManager)
public function __construct(private readonly LdapManager $ldapManager)
{
}
@@ -40,7 +39,7 @@ final class LdapCredentialsSubscriber implements EventSubscriberInterface
return;
}
if (!$passport instanceof Passport || !$passport->hasBadge(PasswordCredentials::class)) {
if (!$passport->hasBadge(PasswordCredentials::class)) {
throw new \LogicException(\sprintf('LDAP authentication requires a passport containing a user and password credentials, authenticator "%s" does not fulfill these requirements.', \get_class($event->getAuthenticator())));
}
@@ -78,6 +77,6 @@ final class LdapCredentialsSubscriber implements EventSubscriberInterface
}
// make sure that the normal auth process is not triggered
$passwordCredentials->markResolved();
$passwordCredentials->markResolved(); // @phpstan-ignore method.internal
}
}

View File

@@ -10,9 +10,12 @@
namespace App\Model;
use App\Entity\Activity;
use App\Entity\MetaTableTypeInterface;
use App\Entity\Project;
use App\Entity\Timesheet;
use App\Entity\User;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @internal
@@ -24,9 +27,18 @@ class QuickEntryModel
* @var Timesheet[]
*/
private array $timesheets = [];
/**
* @var Collection<int, MetaTableTypeInterface>
*/
private Collection $metaFields;
public function __construct(private User $user, private ?Project $project = null, private ?Activity $activity = null)
public function __construct(
private readonly User $user,
private ?Project $project = null,
private ?Activity $activity = null
)
{
$this->metaFields = new ArrayCollection();
}
public function markAsPrototype(): void
@@ -184,4 +196,36 @@ class QuickEntryModel
$this->timesheets[] = clone $record;
}
}
/**
* @return Collection<int, MetaTableTypeInterface>
*/
public function getMetaFields(): Collection
{
return $this->metaFields;
}
public function setMetaFieldValue(MetaTableTypeInterface $metaField): void
{
// we only update the value for fields which were previously registered
// as we do NOT want new fields to show up by accident
foreach ($this->metaFields as $field) {
if ($field->getName() === $metaField->getName() && $metaField->getValue() !== null) {
$field->setValue($metaField->getValue());
break;
}
}
}
/**
* @param array<MetaTableTypeInterface> $metaFields
*/
public function setMetaFields(array $metaFields): void
{
$collection = new ArrayCollection();
foreach($metaFields as $field) {
$collection->add(clone $field);
}
$this->metaFields = $collection;
}
}

View File

@@ -23,7 +23,7 @@ class QuickEntryWeek
*/
private array $rows = [];
public function __construct(private \DateTime $startDate)
public function __construct(private readonly \DateTime $startDate)
{
}
@@ -79,7 +79,7 @@ class QuickEntryWeek
$result = strcmp((string) $aName, (string) $bName);
}
return $result < 0 ? -1 : 1;
return $result < 0 ? -1 : 1;
}
/**

View File

@@ -9,9 +9,6 @@
namespace App\Model;
/**
* @internal
*/
class TimesheetCountedStatistic implements \JsonSerializable
{
private int $counter = 0;
@@ -34,8 +31,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
/**
* For unified access, used in frontend.
*
* @return int
*/
public function getCounter(): int
{
@@ -84,8 +79,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
/**
* For unified access, used in frontend.
*
* @return int
*/
public function getValue(): int
{
@@ -104,8 +97,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
/**
* For unified access, used in frontend.
*
* @return int
*/
public function getDuration(): int
{
@@ -114,8 +105,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
/**
* For unified access, used in frontend.
*
* @return float
*/
public function getRate(): float
{
@@ -134,8 +123,6 @@ class TimesheetCountedStatistic implements \JsonSerializable
/**
* Returns the total internal rate of all included timesheet records.
*
* @return float
*/
public function getInternalRate(): float
{

View File

@@ -324,7 +324,6 @@ class BaseQuery
* @template T of BaseQuery
* @param T $query
* @return T
* @internal
*/
final public function copyTo(BaseQuery $query): BaseQuery
{

View File

@@ -24,7 +24,7 @@ final class SamlAuthenticationSuccessHandler extends DefaultAuthenticationSucces
protected function determineTargetUrl(Request $request): string
{
$relayState = $request->get('RelayState');
$relayState = $request->request->get('RelayState', $request->query->get('RelayState'));
if (\is_scalar($relayState)) {
$relayState = (string) $relayState;
if ($relayState !== $this->httpUtils->generateUri($request, (string) $this->options['login_path'])) {

View File

@@ -27,9 +27,9 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
$this->setFromToFromRequest($timesheet, $request);
}
protected function setBeginEndFromRequest(Timesheet $entry, Request $request)
protected function setBeginEndFromRequest(Timesheet $entry, Request $request): void
{
$start = $request->get('begin');
$start = $request->query->get('begin');
if (null === $start) {
return;
}
@@ -42,7 +42,7 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
$entry->setBegin($start);
// only check for an end date if a begin date was given
$end = $request->get('end');
$end = $request->query->get('end');
if (null === $end) {
return;
}
@@ -59,33 +59,33 @@ abstract class AbstractTrackingMode implements TrackingModeInterface
$entry->setDuration($end->getTimestamp() - $start->getTimestamp());
}
protected function setFromToFromRequest(Timesheet $entry, Request $request)
protected function setFromToFromRequest(Timesheet $entry, Request $request): void
{
$from = $request->get('from');
if (null === $from) {
$from = $request->query->get('from');
if (!\is_string($from)) {
return;
}
try {
$from = new DateTime($from, $this->getTimezone($entry));
$entry->setBegin($from);
} catch (\Exception $ex) {
return;
}
$entry->setBegin($from);
$to = $request->get('to');
if (null === $to) {
// only check for an end date if a valid begin date was given
$to = $request->query->get('to');
if (!\is_string($to)) {
return;
}
try {
$to = new DateTime($to, $this->getTimezone($entry));
$entry->setEnd($to);
} catch (\Exception $ex) {
return;
}
$entry->setEnd($to);
$entry->setDuration($to->getTimestamp() - $from->getTimestamp());
}
}

View File

@@ -63,12 +63,12 @@ final class ThemeExtension implements RuntimeExtensionInterface
public function getJavascriptTranslations(): array
{
$event = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
$event = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecatedClass
$this->eventDispatcher->dispatch($event);
$all = [];
foreach ($event->getTranslations() as $key => $translation) {
foreach ($event->getTranslations() as $key => $translation) { // @phpstan-ignore method.deprecatedClass
$all[$key] = $this->translator->trans($translation[0], [], $translation[1]);
}

View File

@@ -34,11 +34,11 @@ final class ForbiddenPolicy implements SecurityPolicyInterface
* @param array<string> $forbiddenFunctions
*/
public function __construct(
private array $forbiddenTags = [],
private array $forbiddenFilters = [],
private readonly array $forbiddenTags = [],
private readonly array $forbiddenFilters = [],
array $forbiddenMethods = [],
private array $forbiddenProperties = [],
private array $forbiddenFunctions = []
private readonly array $forbiddenProperties = [],
private readonly array $forbiddenFunctions = []
)
{
$this->forbiddenMethods = [];
@@ -70,7 +70,7 @@ final class ForbiddenPolicy implements SecurityPolicyInterface
public function checkMethodAllowed($obj, $method): void
{
if ($obj instanceof Template || $obj instanceof Markup) {
if ($obj instanceof Template || $obj instanceof Markup) { // @phpstan-ignore instanceof.internalClass
return;
}

View File

@@ -204,7 +204,7 @@ final class InvoicePolicy implements SecurityPolicyInterface
public function checkMethodAllowed($obj, $method): void
{
if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) {
if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) { // @phpstan-ignore instanceof.internalClass
return;
}

View File

@@ -27,9 +27,6 @@ use App\WorkingTime\Model\Year;
use App\WorkingTime\Model\YearPerUserSummary;
use Psr\EventDispatcher\EventDispatcherInterface;
/**
* @internal this API and the entire namespace is experimental: expect changes!
*/
final class WorkingTimeService
{
private const LATEST_APPROVAL_PREF = '_latest_approval';

View File

@@ -35,6 +35,10 @@
{{ form_row(form.duration, {row_attr: {class: 'p-0'}}) }}
{%- endblock %}
{% block _quick_entry_form_rows_entry_metaFields_entry_widget %}
{{ form_row(form, {row_attr: {class: 'p-0'}}) }}
{%- endblock %}
{% block quick_entry_week_row %}
<tr{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group qe-entry-week-row' ~ (not valid ? ' is-invalid'))|trim})} %}{{ block('attributes') }}{% endwith %}>
<td>
@@ -43,6 +47,11 @@
<td>
{{ form_row(form.activity, {row_attr: {class: 'p-0'}}) }}
</td>
{% for meta in form.metaFields|sort((a, b) => a.vars.data.order <=> b.vars.data.order) %}
<td>
{{ form_row(meta.value, {label: false}) }}
</td>
{% endfor %}
{% for timesheet in form.timesheets %}
<td class="text-center{% if timesheet.vars.data.begin is weekend %} weekend{% endif %}{% if timesheet.vars.data.begin is today %} today{% endif %}">
{{ form_widget(timesheet) }}

View File

@@ -29,7 +29,7 @@
</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }}">
{{ plugin.metadata.description }}
{% if bundles[plugin.id] is defined and bundles[plugin.id]['latest_release'] is defined and bundles[plugin.id]['latest_release'] != plugin.metadata.version %}
{% if updates[plugin.id] is defined and updates[plugin.id] == true %}
{{ widgets.alert('warning', 'update_available'|trans({'%version%': bundles[plugin.id]['latest_release']}, 'plugins')) }}
{% endif %}
</td>

View File

@@ -28,6 +28,9 @@
<tr>
<th>{{ 'project'|trans }}</th>
<th>{{ 'activity'|trans }}</th>
{% for field in metaColumns|sort((a, b) => a.order <=> b.order) %}
<th style="min-width: 70px">{{ field.label }}</th>
{% endfor %}
{% for id, week in days %}
<th class="text-center{% if week.day is weekend %} weekend{% endif %}{% if week.day is today %} today{% endif %}">
{{ week.day|date_weekday }}
@@ -43,6 +46,9 @@
<tr class="summary">
<td>{{ 'stats.durationTotal'|trans }}</td>
<td></td>
{% for field in metaColumns %}
<td></td>
{% endfor %}
{% for id, week in days %}
<td class="text-center" id="qe-totals-day-{{ loop.index0 }}"></td>
{% endfor %}

View File

@@ -136,7 +136,6 @@ class SystemConfigurationTest extends TestCase
self::assertEquals('GBP', $sut->find('defaults.customer.currency'));
self::assertFalse($sut->find('timesheet.rules.allow_future_times'));
self::assertEquals(99, $sut->find('timesheet.active_entries.hard_limit'));
self::assertEquals('Maroon|#800000,Brown|#a52a2a,Red|#ff0000,Orange|#ffa500,#ffffff,,|#000000', $sut->getThemeColorChoices());
self::assertEquals(['Maroon' => '#800000', 'Brown' => '#a52a2a', 'Red' => '#ff0000', 'Orange' => '#ffa500', '#ffffff' => '#ffffff', '#000000' => '#000000'], $sut->getThemeColors());
}
@@ -160,7 +159,6 @@ class SystemConfigurationTest extends TestCase
]);
self::assertFalse($sut->find('timesheet.rules.allow_future_times'));
self::assertTrue($sut->isSamlActive());
self::assertEquals('Silver|#c0c0c0', $sut->getThemeColorChoices());
self::assertEquals(['Silver' => '#c0c0c0'], $sut->getThemeColors());
self::assertEquals('2020-03-27', $sut->getFinancialYearStart());
}

View File

@@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase;
/**
* @covers \App\Event\ThemeJavascriptTranslationsEvent
* @group legacy
*/
class ThemeJavascriptTranslationsEventTest extends TestCase
{
@@ -21,18 +22,18 @@ class ThemeJavascriptTranslationsEventTest extends TestCase
public function testDefaultValues(): void
{
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecatedClass
self::assertCount(self::COUNTER, $sut->getTranslations());
self::assertCount(self::COUNTER, $sut->getTranslations()); // @phpstan-ignore method.deprecatedClass
}
public function testGetterAndSetter(): void
{
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecated
$sut->setTranslation('foo', 'bar');
$sut->setTranslation('hello', 'world', 'testing');
$sut = new ThemeJavascriptTranslationsEvent(); // @phpstan-ignore new.deprecatedClass
$sut->setTranslation('foo', 'bar'); // @phpstan-ignore method.deprecatedClass
$sut->setTranslation('hello', 'world', 'testing'); // @phpstan-ignore method.deprecatedClass
$result = $sut->getTranslations();
$result = $sut->getTranslations(); // @phpstan-ignore method.deprecatedClass
self::assertCount(self::COUNTER + 2, $result);
self::assertArrayHasKey('foo', $result);
self::assertEquals(['bar', 'messages'], $result['foo']);

View File

@@ -180,19 +180,19 @@
</trans-unit>
<trans-unit id="jmsE4EY" resname="Ceil">
<source>Ceil</source>
<target>Ceil: Start, Ende und Dauer werden nach oben gerundet</target>
<target>Start, Ende und Dauer werden nach oben gerundet</target>
</trans-unit>
<trans-unit id="Y_qmsDz" resname="Closest">
<source>Closest</source>
<target>Closest: Mathematische Rundung zum nächsten Wert</target>
<target>Mathematische Rundung zum nächsten Wert</target>
</trans-unit>
<trans-unit id="IbERy.5" resname="Default">
<source>Default</source>
<target>Standard: Start wird nach unten, Ende und Dauer nach oben gerundet</target>
<target>Start wird nach unten, Ende und Dauer nach oben gerundet</target>
</trans-unit>
<trans-unit id="0mjJxEk" resname="Floor">
<source>Floor</source>
<target>Floor: Start, Ende und Dauer werden nach unten gerundet</target>
<target>Start, Ende und Dauer werden nach unten gerundet</target>
</trans-unit>
<trans-unit id="_0OBfZf" resname="timesheet.default_begin" xml:space="preserve">
<source>timesheet.default_begin</source>

View File

@@ -180,19 +180,19 @@
</trans-unit>
<trans-unit id="jmsE4EY" resname="Ceil">
<source>Ceil</source>
<target>Ceil: begin, end and duration will be rounded up</target>
<target>Begin, end and duration will be rounded up</target>
</trans-unit>
<trans-unit id="Y_qmsDz" resname="Closest">
<source>Closest</source>
<target>Closest: mathematical rounding to the nearest value</target>
<target>Mathematical rounding to the nearest value</target>
</trans-unit>
<trans-unit id="IbERy.5" resname="Default">
<source>Default</source>
<target>Standard: begin will be rounded down, end and duration up</target>
<target>Begin will be rounded down, end and duration up</target>
</trans-unit>
<trans-unit id="0mjJxEk" resname="Floor">
<source>Floor</source>
<target>Floor: begin, end and duration will be rounded down</target>
<target>Begin, end and duration will be rounded down</target>
</trans-unit>
<trans-unit id="_0OBfZf" resname="timesheet.default_begin">
<source>timesheet.default_begin</source>