From a392f61d6187208d5ae3dc4dc9f1a215b79ddd64 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Mon, 21 Aug 2023 20:24:24 +0200 Subject: [PATCH] Release 2.0.31 (#4245) * replace strings by class references * allow 64 chars for username * fix time can be optional and null * fix timezone is not respected * bump version * fix br in attribute * support different visible duration from booked duration * fix image URL --- README.md | 2 +- src/Constants.php | 4 +- src/Entity/Activity.php | 8 ++-- src/Entity/ActivityMeta.php | 2 +- src/Entity/ActivityRate.php | 2 +- src/Entity/Bookmark.php | 2 +- src/Entity/CommentTableTypeTrait.php | 2 +- src/Entity/Customer.php | 6 +-- src/Entity/CustomerComment.php | 2 +- src/Entity/CustomerMeta.php | 2 +- src/Entity/CustomerRate.php | 2 +- src/Entity/Invoice.php | 6 +-- src/Entity/InvoiceMeta.php | 2 +- src/Entity/Project.php | 6 +-- src/Entity/ProjectComment.php | 2 +- src/Entity/ProjectMeta.php | 2 +- src/Entity/ProjectRate.php | 2 +- src/Entity/Rate.php | 2 +- src/Entity/RolePermission.php | 2 +- src/Entity/Tag.php | 2 +- src/Entity/TeamMember.php | 4 +- src/Entity/Timesheet.php | 10 ++--- src/Entity/TimesheetMeta.php | 2 +- src/Entity/User.php | 6 +-- src/Entity/UserPreference.php | 2 +- src/Entity/WorkingTime.php | 4 +- src/Form/Type/DateTimePickerType.php | 36 ++++++++++++++++-- src/Form/Type/TimePickerType.php | 12 +++++- src/Utils/DataTable.php | 6 +-- src/WorkingTime/Model/DayAddon.php | 11 +++--- templates/contract/status.html.twig | 4 +- .../SelfRegistrationControllerTest.php | 2 +- tests/Entity/UserTest.php | 38 +++++++++++++++---- tests/Entity/UserValidationTest.php | 12 +++--- tests/WorkingTime/Model/DayAddonTest.php | 34 +++++++++++++++++ tests/phpstan.neon | 20 ---------- 36 files changed, 169 insertions(+), 94 deletions(-) create mode 100644 tests/WorkingTime/Model/DayAddonTest.php diff --git a/README.md b/README.md index bdff6aea..967e7b5b 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

- CI Status + CI Status Code Coverage Latest stable version License diff --git a/src/Constants.php b/src/Constants.php index f308a849..049de959 100644 --- a/src/Constants.php +++ b/src/Constants.php @@ -17,11 +17,11 @@ class Constants /** * The current release version */ - public const VERSION = '2.0.30'; + public const VERSION = '2.0.31'; /** * The current release: major * 10000 + minor * 100 + patch */ - public const VERSION_ID = 20030; + public const VERSION_ID = 20031; /** * The software name */ diff --git a/src/Entity/Activity.php b/src/Entity/Activity.php index 1429f107..430a0ed4 100644 --- a/src/Entity/Activity.php +++ b/src/Entity/Activity.php @@ -43,8 +43,8 @@ class Activity implements EntityWithMetaFields, EntityWithBudget #[Serializer\Groups(['Default'])] #[Exporter\Expose(label: 'id', type: 'integer')] private ?int $id = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')] - #[ORM\JoinColumn(onDelete: 'CASCADE')] + #[ORM\ManyToOne(targetEntity: Project::class)] + #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Subresource', 'Expanded'])] #[OA\Property(ref: '#/components/schemas/ProjectExpanded')] @@ -87,7 +87,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget * * @var Collection */ - #[ORM\OneToMany(targetEntity: 'App\Entity\ActivityMeta', mappedBy: 'activity', cascade: ['persist'])] + #[ORM\OneToMany(targetEntity: ActivityMeta::class, mappedBy: 'activity', cascade: ['persist'])] #[Serializer\Expose] #[Serializer\Groups(['Activity'])] #[Serializer\Type(name: 'array')] @@ -102,7 +102,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget #[ORM\JoinTable(name: 'kimai2_activities_teams')] #[ORM\JoinColumn(name: 'activity_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - #[ORM\ManyToMany(targetEntity: 'App\Entity\Team', cascade: ['persist'], inversedBy: 'activities')] + #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'activities')] #[Serializer\Expose] #[Serializer\Groups(['Activity'])] #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))] diff --git a/src/Entity/ActivityMeta.php b/src/Entity/ActivityMeta.php index 0631fcd5..f4f1561a 100644 --- a/src/Entity/ActivityMeta.php +++ b/src/Entity/ActivityMeta.php @@ -22,7 +22,7 @@ class ActivityMeta implements MetaTableTypeInterface { use MetaTableTypeTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Activity', inversedBy: 'meta')] + #[ORM\ManyToOne(targetEntity: Activity::class, inversedBy: 'meta')] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Activity $activity = null; diff --git a/src/Entity/ActivityRate.php b/src/Entity/ActivityRate.php index 033d8405..ff4de09b 100644 --- a/src/Entity/ActivityRate.php +++ b/src/Entity/ActivityRate.php @@ -24,7 +24,7 @@ class ActivityRate implements RateInterface { use Rate; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Activity')] + #[ORM\ManyToOne(targetEntity: Activity::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Activity $activity = null; diff --git a/src/Entity/Bookmark.php b/src/Entity/Bookmark.php index 9c9b7846..25af24a7 100644 --- a/src/Entity/Bookmark.php +++ b/src/Entity/Bookmark.php @@ -28,7 +28,7 @@ class Bookmark #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User')] + #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?User $user = null; diff --git a/src/Entity/CommentTableTypeTrait.php b/src/Entity/CommentTableTypeTrait.php index 64208f45..314e4b86 100644 --- a/src/Entity/CommentTableTypeTrait.php +++ b/src/Entity/CommentTableTypeTrait.php @@ -21,7 +21,7 @@ trait CommentTableTypeTrait #[ORM\Column(name: 'message', type: 'text', nullable: false)] #[Assert\NotNull] private ?string $message = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User')] + #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?User $createdBy = null; diff --git a/src/Entity/Customer.php b/src/Entity/Customer.php index 4b9de8d6..0289cbf5 100644 --- a/src/Entity/Customer.php +++ b/src/Entity/Customer.php @@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget * * @var Collection */ - #[ORM\OneToMany(targetEntity: 'App\Entity\CustomerMeta', mappedBy: 'customer', cascade: ['persist'])] + #[ORM\OneToMany(targetEntity: CustomerMeta::class, mappedBy: 'customer', cascade: ['persist'])] #[Serializer\Expose] #[Serializer\Groups(['Customer'])] #[Serializer\Type(name: 'array')] @@ -171,7 +171,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget #[ORM\JoinTable(name: 'kimai2_customers_teams')] #[ORM\JoinColumn(name: 'customer_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - #[ORM\ManyToMany(targetEntity: 'App\Entity\Team', cascade: ['persist'], inversedBy: 'customers')] + #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'customers')] #[Serializer\Expose] #[Serializer\Groups(['Customer'])] #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))] @@ -179,7 +179,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget /** * Default invoice template for this customer */ - #[ORM\ManyToOne(targetEntity: 'App\Entity\InvoiceTemplate')] + #[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)] #[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)] private ?InvoiceTemplate $invoiceTemplate = null; #[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)] diff --git a/src/Entity/CustomerComment.php b/src/Entity/CustomerComment.php index fea10ce8..012e9770 100644 --- a/src/Entity/CustomerComment.php +++ b/src/Entity/CustomerComment.php @@ -20,7 +20,7 @@ class CustomerComment implements CommentInterface { use CommentTableTypeTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')] + #[ORM\ManyToOne(targetEntity: Customer::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private Customer $customer; diff --git a/src/Entity/CustomerMeta.php b/src/Entity/CustomerMeta.php index 8bee41e4..f396e82a 100644 --- a/src/Entity/CustomerMeta.php +++ b/src/Entity/CustomerMeta.php @@ -22,7 +22,7 @@ class CustomerMeta implements MetaTableTypeInterface { use MetaTableTypeTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer', inversedBy: 'meta')] + #[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'meta')] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Customer $customer = null; diff --git a/src/Entity/CustomerRate.php b/src/Entity/CustomerRate.php index 48062776..4d747459 100644 --- a/src/Entity/CustomerRate.php +++ b/src/Entity/CustomerRate.php @@ -24,7 +24,7 @@ class CustomerRate implements RateInterface { use Rate; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')] + #[ORM\ManyToOne(targetEntity: Customer::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Customer $customer = null; diff --git a/src/Entity/Invoice.php b/src/Entity/Invoice.php index 2bc15fca..c5188b0e 100644 --- a/src/Entity/Invoice.php +++ b/src/Entity/Invoice.php @@ -55,11 +55,11 @@ class Invoice implements EntityWithMetaFields #[Serializer\Groups(['Customer_Entity'])] #[Exporter\Expose(label: 'comment')] private ?string $comment = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')] + #[ORM\ManyToOne(targetEntity: Customer::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Customer $customer = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User')] + #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?User $user = null; @@ -109,7 +109,7 @@ class Invoice implements EntityWithMetaFields * * @var Collection */ - #[ORM\OneToMany(targetEntity: 'App\Entity\InvoiceMeta', mappedBy: 'invoice', cascade: ['persist'])] + #[ORM\OneToMany(targetEntity: InvoiceMeta::class, mappedBy: 'invoice', cascade: ['persist'])] #[Serializer\Expose] #[Serializer\Groups(['Invoice'])] #[Serializer\Type(name: 'array')] diff --git a/src/Entity/InvoiceMeta.php b/src/Entity/InvoiceMeta.php index 6c7029ad..d41db1e9 100644 --- a/src/Entity/InvoiceMeta.php +++ b/src/Entity/InvoiceMeta.php @@ -22,7 +22,7 @@ class InvoiceMeta implements MetaTableTypeInterface { use MetaTableTypeTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Invoice', inversedBy: 'meta')] + #[ORM\ManyToOne(targetEntity: Invoice::class, inversedBy: 'meta')] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Invoice $invoice = null; diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 7bc1b781..f26e5b8d 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -47,7 +47,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget /** * Customer for this project */ - #[ORM\ManyToOne(targetEntity: 'App\Entity\Customer')] + #[ORM\ManyToOne(targetEntity: Customer::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] #[Serializer\Expose] @@ -137,7 +137,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget * * @var Collection */ - #[ORM\OneToMany(targetEntity: 'App\Entity\ProjectMeta', mappedBy: 'project', cascade: ['persist'])] + #[ORM\OneToMany(targetEntity: ProjectMeta::class, mappedBy: 'project', cascade: ['persist'])] #[Serializer\Expose] #[Serializer\Groups(['Project'])] #[Serializer\Type(name: 'array')] @@ -152,7 +152,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget #[ORM\JoinTable(name: 'kimai2_projects_teams')] #[ORM\JoinColumn(name: 'project_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\InverseJoinColumn(name: 'team_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - #[ORM\ManyToMany(targetEntity: 'App\Entity\Team', cascade: ['persist'], inversedBy: 'projects')] + #[ORM\ManyToMany(targetEntity: Team::class, cascade: ['persist'], inversedBy: 'projects')] #[Serializer\Expose] #[Serializer\Groups(['Project'])] #[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))] diff --git a/src/Entity/ProjectComment.php b/src/Entity/ProjectComment.php index 33c01985..ddba3cd2 100644 --- a/src/Entity/ProjectComment.php +++ b/src/Entity/ProjectComment.php @@ -20,7 +20,7 @@ class ProjectComment implements CommentInterface { use CommentTableTypeTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')] + #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private Project $project; diff --git a/src/Entity/ProjectMeta.php b/src/Entity/ProjectMeta.php index bcb0c341..89e7fac2 100644 --- a/src/Entity/ProjectMeta.php +++ b/src/Entity/ProjectMeta.php @@ -22,7 +22,7 @@ class ProjectMeta implements MetaTableTypeInterface { use MetaTableTypeTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Project', inversedBy: 'meta')] + #[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'meta')] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Project $project = null; diff --git a/src/Entity/ProjectRate.php b/src/Entity/ProjectRate.php index 0a21b852..7bdc8695 100644 --- a/src/Entity/ProjectRate.php +++ b/src/Entity/ProjectRate.php @@ -24,7 +24,7 @@ class ProjectRate implements RateInterface { use Rate; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')] + #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Project $project = null; diff --git a/src/Entity/Rate.php b/src/Entity/Rate.php index 76259208..15e1c31b 100644 --- a/src/Entity/Rate.php +++ b/src/Entity/Rate.php @@ -22,7 +22,7 @@ trait Rate #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?int $id = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User')] + #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)] #[Serializer\Expose] #[Serializer\Groups(['Default'])] diff --git a/src/Entity/RolePermission.php b/src/Entity/RolePermission.php index 41643e8f..5c18e179 100644 --- a/src/Entity/RolePermission.php +++ b/src/Entity/RolePermission.php @@ -24,7 +24,7 @@ class RolePermission #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Role')] + #[ORM\ManyToOne(targetEntity: Role::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Role $role = null; diff --git a/src/Entity/Tag.php b/src/Entity/Tag.php index 35cbe8e3..febc8546 100644 --- a/src/Entity/Tag.php +++ b/src/Entity/Tag.php @@ -54,7 +54,7 @@ class Tag /** * @var Collection */ - #[ORM\ManyToMany(targetEntity: 'App\Entity\Timesheet', mappedBy: 'tags', fetch: 'EXTRA_LAZY')] + #[ORM\ManyToMany(targetEntity: Timesheet::class, mappedBy: 'tags', fetch: 'EXTRA_LAZY')] private Collection $timesheets; public function __construct() diff --git a/src/Entity/TeamMember.php b/src/Entity/TeamMember.php index 571d494a..946ceb14 100644 --- a/src/Entity/TeamMember.php +++ b/src/Entity/TeamMember.php @@ -25,14 +25,14 @@ class TeamMember #[ORM\Id] #[ORM\GeneratedValue(strategy: 'IDENTITY')] private ?int $id = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User', inversedBy: 'memberships')] + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'memberships')] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Default', 'Entity', 'Team_Entity'])] #[OA\Property(ref: '#/components/schemas/User')] private ?User $user = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Team', inversedBy: 'members')] + #[ORM\ManyToOne(targetEntity: Team::class, inversedBy: 'members')] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] #[Serializer\Expose] diff --git a/src/Entity/Timesheet.php b/src/Entity/Timesheet.php index ed5d93f0..253c1f3d 100644 --- a/src/Entity/Timesheet.php +++ b/src/Entity/Timesheet.php @@ -127,21 +127,21 @@ class Timesheet implements EntityWithMetaFields, ExportableItem #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?int $duration = 0; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User')] + #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Subresource', 'Expanded'])] #[OA\Property(ref: '#/components/schemas/User')] private ?User $user = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Activity')] + #[ORM\ManyToOne(targetEntity: Activity::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] #[Serializer\Expose] #[Serializer\Groups(['Subresource', 'Expanded'])] #[OA\Property(ref: '#/components/schemas/ActivityExpanded')] private ?Activity $activity = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Project')] + #[ORM\ManyToOne(targetEntity: Project::class)] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] #[Serializer\Expose] @@ -203,7 +203,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem #[ORM\JoinTable(name: 'kimai2_timesheet_tags')] #[ORM\JoinColumn(name: 'timesheet_id', referencedColumnName: 'id', onDelete: 'CASCADE')] #[ORM\InverseJoinColumn(name: 'tag_id', referencedColumnName: 'id', onDelete: 'CASCADE')] - #[ORM\ManyToMany(targetEntity: 'App\Entity\Tag', inversedBy: 'timesheets', cascade: ['persist'])] + #[ORM\ManyToMany(targetEntity: Tag::class, inversedBy: 'timesheets', cascade: ['persist'])] #[Assert\Valid] private Collection $tags; /** @@ -211,7 +211,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem * * @var Collection */ - #[ORM\OneToMany(targetEntity: 'App\Entity\TimesheetMeta', mappedBy: 'timesheet', cascade: ['persist'])] + #[ORM\OneToMany(targetEntity: TimesheetMeta::class, mappedBy: 'timesheet', cascade: ['persist'])] #[Serializer\Expose] #[Serializer\Groups(['Timesheet'])] #[Serializer\Type(name: 'array')] diff --git a/src/Entity/TimesheetMeta.php b/src/Entity/TimesheetMeta.php index 559251c7..4e116f64 100644 --- a/src/Entity/TimesheetMeta.php +++ b/src/Entity/TimesheetMeta.php @@ -22,7 +22,7 @@ class TimesheetMeta implements MetaTableTypeInterface { use MetaTableTypeTrait; - #[ORM\ManyToOne(targetEntity: 'App\Entity\Timesheet', inversedBy: 'meta')] + #[ORM\ManyToOne(targetEntity: Timesheet::class, inversedBy: 'meta')] #[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)] #[Assert\NotNull] private ?Timesheet $timesheet = null; diff --git a/src/Entity/User.php b/src/Entity/User.php index 3b827fe4..3770f546 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -159,7 +159,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas #[ORM\Column(name: 'username', type: 'string', length: 180, nullable: false)] #[Assert\NotBlank(groups: ['Registration', 'UserCreate', 'Profile'])] #[Assert\Regex(pattern: '/\//', match: false, groups: ['Registration', 'UserCreate', 'Profile'])] - #[Assert\Length(min: 2, max: 60, groups: ['Registration', 'UserCreate', 'Profile'])] + #[Assert\Length(min: 2, max: 64, groups: ['Registration', 'UserCreate', 'Profile'])] #[Serializer\Expose] #[Serializer\Groups(['Default'])] private ?string $username = null; @@ -1253,9 +1253,9 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas $this->setPreferenceValue(UserPreference::PUBLIC_HOLIDAY_GROUP, $group); } - public function setHolidaysPerYear(int $holidays): void + public function setHolidaysPerYear(?int $holidays): void { - $this->setPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, $holidays); + $this->setPreferenceValue(UserPreference::HOLIDAYS_PER_YEAR, $holidays ?? 0); } public function hasContractSettings(): bool diff --git a/src/Entity/UserPreference.php b/src/Entity/UserPreference.php index 770c2f25..9835a52b 100644 --- a/src/Entity/UserPreference.php +++ b/src/Entity/UserPreference.php @@ -46,7 +46,7 @@ class UserPreference #[ORM\GeneratedValue] #[ORM\Column(name: 'id', type: 'integer')] private ?int $id = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User', inversedBy: 'preferences')] + #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'preferences')] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private ?User $user = null; diff --git a/src/Entity/WorkingTime.php b/src/Entity/WorkingTime.php index 0898c026..f2e62902 100644 --- a/src/Entity/WorkingTime.php +++ b/src/Entity/WorkingTime.php @@ -25,7 +25,7 @@ class WorkingTime #[ORM\GeneratedValue] #[ORM\Column(name: 'id', type: 'integer')] private ?int $id = null; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User')] + #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')] #[Assert\NotNull] private ?User $user = null; @@ -38,7 +38,7 @@ class WorkingTime #[ORM\Column(name: 'actual', type: 'integer', nullable: false)] #[Assert\NotNull] private int $actualTime = 0; - #[ORM\ManyToOne(targetEntity: 'App\Entity\User')] + #[ORM\ManyToOne(targetEntity: User::class)] #[ORM\JoinColumn(name: 'approved_by', nullable: true, onDelete: 'SET NULL')] private ?User $approvedBy = null; #[ORM\Column(name: 'approved_at', type: 'datetime', nullable: true)] diff --git a/src/Form/Type/DateTimePickerType.php b/src/Form/Type/DateTimePickerType.php index 5afb8498..242f6ad1 100644 --- a/src/Form/Type/DateTimePickerType.php +++ b/src/Form/Type/DateTimePickerType.php @@ -11,6 +11,7 @@ namespace App\Form\Type; use App\API\BaseApiController; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\ArrayToPartsTransformer; use Symfony\Component\Form\Extension\Core\DataTransformer\DataTransformerChain; use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToArrayTransformer; @@ -22,10 +23,6 @@ class DateTimePickerType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options): void { - $parts = ['year', 'month', 'day', 'hour', 'minute']; - $dateParts = ['year', 'month', 'day']; - $timeParts = ['hour', 'minute']; - // Only pass a subset of the options to children $dateOptions = array_intersect_key($options, array_flip([ 'years', @@ -37,6 +34,8 @@ class DateTimePickerType extends AbstractType 'translation_domain', 'invalid_message', 'invalid_message_parameters', + 'model_timezone', + 'view_timezone', ])); $timeOptions = array_intersect_key($options, array_flip([ @@ -45,6 +44,8 @@ class DateTimePickerType extends AbstractType 'translation_domain', 'invalid_message', 'invalid_message_parameters', + 'model_timezone', + 'view_timezone', ])); if (false === $options['label']) { @@ -63,6 +64,10 @@ class DateTimePickerType extends AbstractType $dateOptions['input'] = $timeOptions['input'] = 'array'; $dateOptions['error_bubbling'] = $timeOptions['error_bubbling'] = true; + $dateParts = ['year', 'month', 'day']; + $timeParts = ['hour', 'minute']; + $parts = array_merge($dateParts, $timeParts); + $builder ->addViewTransformer(new DataTransformerChain([ new DateTimeToArrayTransformer($options['model_timezone'], $options['view_timezone'], $parts), @@ -70,6 +75,29 @@ class DateTimePickerType extends AbstractType 'date' => $dateParts, 'time' => $timeParts, ]), + new CallbackTransformer( + function ($transform) { + return $transform; + }, + function ($reverseTransform) { + if (\array_key_exists('date', $reverseTransform) && $reverseTransform['date'] === null) { + $reverseTransform['time'] = [ + 'year' => '', + 'month' => '', + 'day' => '', + ]; + } + // happened in DateTimePickerType - made it impossible to create an empty DateTime + if (\array_key_exists('time', $reverseTransform) && $reverseTransform['time'] === null) { + $reverseTransform['time'] = [ + 'hour' => '', + 'minute' => '', + ]; + } + + return $reverseTransform; + } + ), ])) ->add('date', DatePickerType::class, $dateOptions) ->add('time', TimePickerType::class, $timeOptions) diff --git a/src/Form/Type/TimePickerType.php b/src/Form/Type/TimePickerType.php index 01704abf..f2af56d8 100644 --- a/src/Form/Type/TimePickerType.php +++ b/src/Form/Type/TimePickerType.php @@ -51,9 +51,17 @@ final class TimePickerType extends AbstractType // DateTimePickerType if ($options['input'] === 'array' && \is_array($data)) { + if (!\array_key_exists('hour', $data) || $data['hour'] === '' || $data['hour'] === null) { + return null; + } + + if (!\array_key_exists('minute', $data) || $data['minute'] === '' || $data['minute'] === null) { + return null; + } + $now = new \DateTime('now', new \DateTimeZone($options['model_timezone'])); - $hour = $data['hour'] === '' || !is_numeric($data['hour']) ? 0 : (int) $data['hour']; - $minute = $data['minute'] === '' || !is_numeric($data['minute']) ? 0 : (int) $data['minute']; + $hour = !is_numeric($data['hour']) ? 0 : (int) $data['hour']; + $minute = !is_numeric($data['minute']) ? 0 : (int) $data['minute']; $now->setTime($hour, $minute, 0); $data = $now; } diff --git a/src/Utils/DataTable.php b/src/Utils/DataTable.php index 30ab8109..65dc06a1 100644 --- a/src/Utils/DataTable.php +++ b/src/Utils/DataTable.php @@ -9,7 +9,7 @@ namespace App\Utils; -use App\Form\MultiUpdate\MultiUpdateTable; +use App\Form\MultiUpdate\MultiUpdateTableDTO; use App\Repository\Query\BaseQuery; use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormView; @@ -20,7 +20,7 @@ final class DataTable implements \Countable, \IteratorAggregate private ?Pagination $pagination = null; private ?FormInterface $searchForm = null; /** - * @var FormInterface|null + * @var FormInterface|null */ private ?FormInterface $batchForm = null; private array $columns = []; @@ -84,7 +84,7 @@ final class DataTable implements \Countable, \IteratorAggregate } /** - * @param FormInterface|null $batchForm + * @param FormInterface|null $batchForm * @return void */ public function setBatchForm(?FormInterface $batchForm): void diff --git a/src/WorkingTime/Model/DayAddon.php b/src/WorkingTime/Model/DayAddon.php index 8a2ec956..65055543 100644 --- a/src/WorkingTime/Model/DayAddon.php +++ b/src/WorkingTime/Model/DayAddon.php @@ -11,14 +11,10 @@ namespace App\WorkingTime\Model; final class DayAddon { - private string $title; - private int $duration; private bool $billable = true; - public function __construct(string $title, int $duration) + public function __construct(private string $title, private int $duration, private int $visibleDuration) { - $this->title = $title; - $this->duration = $duration; } public function getTitle(): string @@ -40,4 +36,9 @@ final class DayAddon { $this->billable = $billable; } + + public function getVisibleDuration(): int + { + return $this->visibleDuration; + } } diff --git a/templates/contract/status.html.twig b/templates/contract/status.html.twig index b576978b..8d47eef0 100644 --- a/templates/contract/status.html.twig +++ b/templates/contract/status.html.twig @@ -226,9 +226,9 @@ {% if day.hasAddons() %} {% set statusTitle = '' %} {% for addon in day.getAddons() %} - {% set statusTitle = statusTitle ~ (addon.title|trans) ~ ' (' ~ addon.duration|duration(decimal) ~ ')' %} + {% set statusTitle = statusTitle ~ (addon.title|trans) ~ ' (' ~ addon.visibleDuration|duration(decimal) ~ ')' %} {% if not loop.last %} - {% set statusTitle = statusTitle ~ '
' %} + {% set statusTitle = statusTitle ~ ', ' %} {% endif %} {% endfor %} diff --git a/tests/Controller/Security/SelfRegistrationControllerTest.php b/tests/Controller/Security/SelfRegistrationControllerTest.php index 7a2767eb..06cc49dd 100644 --- a/tests/Controller/Security/SelfRegistrationControllerTest.php +++ b/tests/Controller/Security/SelfRegistrationControllerTest.php @@ -62,7 +62,7 @@ class SelfRegistrationControllerTest extends ControllerBaseTest $this->assertStringContainsString('assertStringContainsString('id="user_registration_form_email" name="user_registration_form[email]" required="required"', $content); $this->assertStringContainsString('assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="60" pattern="', $content); + $this->assertStringContainsString('id="user_registration_form_username" name="user_registration_form[username]" required="required" maxlength="64" pattern="', $content); $this->assertStringContainsString('assertStringContainsString('id="user_registration_form_plainPassword_first" name="user_registration_form[plainPassword][first]" required="required"', $content); $this->assertStringContainsString('id="user_registration_form_plainPassword_second" name="user_registration_form[plainPassword][second]" required="required"', $content); diff --git a/tests/Entity/UserTest.php b/tests/Entity/UserTest.php index f7249ed0..ebba943b 100644 --- a/tests/Entity/UserTest.php +++ b/tests/Entity/UserTest.php @@ -91,6 +91,22 @@ class UserTest extends TestCase { $user = new User(); + $monday = new \DateTime('2023-05-08 12:00:00', new \DateTimeZone('Europe/Berlin')); + $tuesday = new \DateTime('2023-05-09 12:00:00', new \DateTimeZone('Europe/Berlin')); + $wednesday = new \DateTime('2023-05-10 12:00:00', new \DateTimeZone('Europe/Berlin')); + $thursday = new \DateTime('2023-05-11 12:00:00', new \DateTimeZone('Europe/Berlin')); + $friday = new \DateTime('2023-05-12 12:00:00', new \DateTimeZone('Europe/Berlin')); + $saturday = new \DateTime('2023-05-13 12:00:00', new \DateTimeZone('Europe/Berlin')); + $sunday = new \DateTime('2023-05-14 12:00:00', new \DateTimeZone('Europe/Berlin')); + + self::assertFalse($user->isWorkDay($monday)); + self::assertFalse($user->isWorkDay($tuesday)); + self::assertFalse($user->isWorkDay($wednesday)); + self::assertFalse($user->isWorkDay($thursday)); + self::assertFalse($user->isWorkDay($friday)); + self::assertFalse($user->isWorkDay($saturday)); + self::assertFalse($user->isWorkDay($sunday)); + $user->setWorkHoursMonday(7200); self::assertTrue($user->hasWorkHourConfiguration()); $user->setWorkHoursTuesday(7300); @@ -111,13 +127,21 @@ class UserTest extends TestCase self::assertEquals(7800, $user->getWorkHoursSunday()); self::assertEquals(10, $user->getHolidaysPerYear()); - self::assertEquals(7200, $user->getWorkHoursForDay(new \DateTime('2023-05-08 12:00:00', new \DateTimeZone('Europe/Berlin')))); - self::assertEquals(7300, $user->getWorkHoursForDay(new \DateTime('2023-05-09 12:00:00', new \DateTimeZone('Europe/Berlin')))); - self::assertEquals(7400, $user->getWorkHoursForDay(new \DateTime('2023-05-10 12:00:00', new \DateTimeZone('Europe/Berlin')))); - self::assertEquals(7500, $user->getWorkHoursForDay(new \DateTime('2023-05-11 12:00:00', new \DateTimeZone('Europe/Berlin')))); - self::assertEquals(7600, $user->getWorkHoursForDay(new \DateTime('2023-05-12 12:00:00', new \DateTimeZone('Europe/Berlin')))); - self::assertEquals(7700, $user->getWorkHoursForDay(new \DateTime('2023-05-13 12:00:00', new \DateTimeZone('Europe/Berlin')))); - self::assertEquals(7800, $user->getWorkHoursForDay(new \DateTime('2023-05-14 12:00:00', new \DateTimeZone('Europe/Berlin')))); + self::assertEquals(7200, $user->getWorkHoursForDay($monday)); + self::assertEquals(7300, $user->getWorkHoursForDay($tuesday)); + self::assertEquals(7400, $user->getWorkHoursForDay($wednesday)); + self::assertEquals(7500, $user->getWorkHoursForDay($thursday)); + self::assertEquals(7600, $user->getWorkHoursForDay($friday)); + self::assertEquals(7700, $user->getWorkHoursForDay($saturday)); + self::assertEquals(7800, $user->getWorkHoursForDay($sunday)); + + self::assertTrue($user->isWorkDay($monday)); + self::assertTrue($user->isWorkDay($tuesday)); + self::assertTrue($user->isWorkDay($wednesday)); + self::assertTrue($user->isWorkDay($thursday)); + self::assertTrue($user->isWorkDay($friday)); + self::assertTrue($user->isWorkDay($saturday)); + self::assertTrue($user->isWorkDay($sunday)); $user->setPublicHolidayGroup('10'); self::assertEquals('10', $user->getPublicHolidayGroup()); diff --git a/tests/Entity/UserValidationTest.php b/tests/Entity/UserValidationTest.php index 391426a2..b8d14e9f 100644 --- a/tests/Entity/UserValidationTest.php +++ b/tests/Entity/UserValidationTest.php @@ -25,14 +25,14 @@ class UserValidationTest extends KernelTestCase return [ ['', ''], ['x', 'test@'], // too short username - [str_pad('#', 61, '-'), 'test@x.'], // too long username + [str_pad('#', 65, '-'), 'test@x.'], // too long username ]; } /** * @dataProvider getInvalidTestData */ - public function testInvalidValues($username, $email, $roles = []) + public function testInvalidValues($username, $email, $roles = []): void { $defaultFields = [ 'username', 'email' @@ -49,7 +49,7 @@ class UserValidationTest extends KernelTestCase $this->assertHasViolationForField($user, $defaultFields, ['Profile']); } - public function testInvalidRoles() + public function testInvalidRoles(): void { $user = new User(); $user->setUserIdentifier('foo'); @@ -59,7 +59,7 @@ class UserValidationTest extends KernelTestCase $this->assertHasViolationForField($user, ['roles'], ['RolesUpdate']); } - public function testValidRoles() + public function testValidRoles(): void { $user = new User(); $user->setUserIdentifier('foo'); @@ -73,14 +73,14 @@ class UserValidationTest extends KernelTestCase { return [ [str_pad('#', 8, '-'), 'test@x.x'], // shortest possible username - [str_pad('#', 60, '-'), 'test@x.x'], // longest possible username + [str_pad('#', 64, '-'), 'test@x.x'], // longest possible username ]; } /** * @dataProvider getValidTestData */ - public function testValidValues($username, $email, $roles = []) + public function testValidValues($username, $email, $roles = []): void { $user = new User(); $user->setUserIdentifier($username); diff --git a/tests/WorkingTime/Model/DayAddonTest.php b/tests/WorkingTime/Model/DayAddonTest.php new file mode 100644 index 00000000..2f468560 --- /dev/null +++ b/tests/WorkingTime/Model/DayAddonTest.php @@ -0,0 +1,34 @@ +getTitle()); + self::assertEquals(7200, $sut->getDuration()); + self::assertEquals(0, $sut->getVisibleDuration()); + self::assertTrue($sut->isBillable()); + + $sut->setBillable(false); + self::assertFalse($sut->isBillable()); + + $sut->setBillable(true); + self::assertTrue($sut->isBillable()); + } +} diff --git a/tests/phpstan.neon b/tests/phpstan.neon index d76a5b59..ec8e43ed 100644 --- a/tests/phpstan.neon +++ b/tests/phpstan.neon @@ -4472,16 +4472,6 @@ parameters: count: 1 path: Entity/UserValidationTest.php - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testInvalidRoles\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserValidationTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testInvalidValues\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserValidationTest.php - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testInvalidValues\\(\\) has parameter \\$email with no type specified\\.$#" count: 1 @@ -4497,16 +4487,6 @@ parameters: count: 1 path: Entity/UserValidationTest.php - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testValidRoles\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserValidationTest.php - - - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testValidValues\\(\\) has no return type specified\\.$#" - count: 1 - path: Entity/UserValidationTest.php - - message: "#^Method App\\\\Tests\\\\Entity\\\\UserValidationTest\\:\\:testValidValues\\(\\) has parameter \\$email with no type specified\\.$#" count: 1