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
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/workflows/CI/badge.svg"></a>
|
||||
<a href="https://github.com/kimai/kimai/actions"><img alt="CI Status" src="https://github.com/kimai/kimai/actions/workflows/testing.yaml/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/kimai/kimai"><img alt="Code Coverage" src="https://codecov.io/gh/kimai/kimai/branch/main/graph/badge.svg"></a>
|
||||
<a href="https://packagist.org/packages/kimai/kimai"><img alt="Latest stable version" src="https://poser.pugx.org/kimai/kimai/v/stable"></a>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html"><img alt="License" src="https://poser.pugx.org/kimai/kimai/license"></a>
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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<ActivityMeta>
|
||||
*/
|
||||
#[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<App\Entity\ActivityMeta>')]
|
||||
@@ -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'))]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
*
|
||||
* @var Collection<CustomerMeta>
|
||||
*/
|
||||
#[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<App\Entity\CustomerMeta>')]
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<InvoiceMeta>
|
||||
*/
|
||||
#[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<App\Entity\InvoiceMeta>')]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<ProjectMeta>
|
||||
*/
|
||||
#[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<App\Entity\ProjectMeta>')]
|
||||
@@ -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'))]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'])]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -54,7 +54,7 @@ class Tag
|
||||
/**
|
||||
* @var Collection<Timesheet>
|
||||
*/
|
||||
#[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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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<TimesheetMeta>
|
||||
*/
|
||||
#[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<App\Entity\TimesheetMeta>')]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<MultiUpdateTable>|null
|
||||
* @var FormInterface<MultiUpdateTableDTO>|null
|
||||
*/
|
||||
private ?FormInterface $batchForm = null;
|
||||
private array $columns = [];
|
||||
@@ -84,7 +84,7 @@ final class DataTable implements \Countable, \IteratorAggregate
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface<MultiUpdateTable>|null $batchForm
|
||||
* @param FormInterface<MultiUpdateTableDTO>|null $batchForm
|
||||
* @return void
|
||||
*/
|
||||
public function setBatchForm(?FormInterface $batchForm): void
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 ~ '<br>' %}
|
||||
{% set statusTitle = statusTitle ~ ', ' %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<span class="status-dot status-azure" data-toggle="tooltip" data-placement="top" title="{{ statusTitle }}"></span>
|
||||
|
||||
@@ -62,7 +62,7 @@ class SelfRegistrationControllerTest extends ControllerBaseTest
|
||||
$this->assertStringContainsString('<input type="email"', $content);
|
||||
$this->assertStringContainsString('id="user_registration_form_email" name="user_registration_form[email]" required="required"', $content);
|
||||
$this->assertStringContainsString('<input type="text"', $content);
|
||||
$this->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('<input type="password"', $content);
|
||||
$this->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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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);
|
||||
|
||||
34
tests/WorkingTime/Model/DayAddonTest.php
Normal file
34
tests/WorkingTime/Model/DayAddonTest.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?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\Tests\WorkingTime\Model;
|
||||
|
||||
use App\WorkingTime\Model\DayAddon;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\WorkingTime\Model\DayAddon
|
||||
*/
|
||||
class DayAddonTest extends TestCase
|
||||
{
|
||||
public function testDefaults(): void
|
||||
{
|
||||
$sut = new DayAddon('foo-bar', 7200, 0);
|
||||
self::assertEquals('foo-bar', $sut->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());
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user