Release 2.1.0 (#4321)
* fix deprecations * remove unused config * replace invalid annotation type with attribute * use AsDoctrineListener to fix deprecation * new ModifiedSubscriber to support custom logic and fix deprecation * removed inheritdoc comment * new ModifiedSubscriber to support custom logic and fix deprecation * cleanup event dispatcher interface * re-order annotation params * one more doctrine based deprecation * fix query to count active timesheets * link to "all times" to identify active timesheets * link icon instead of text * fix "skin" translation in wizard * use duration filter to show duration * added login link command and controller * bump tabler theme to 1.0 * added wizard to force password reset by user * allow to configure that new accounts need to reset their password * prevent uploading twig templates by default * bump composer packages * enable sandbox and basic security measures for custom twig templates for invoice and export * bump to symfony 6.3.5 * allow to export single user reports to excel * removed broken method to reload twig cache * added api parameter to fetch user collection fully serialized * allow to replace or append description via timesheet batch update * show api username above form
This commit is contained in:
@@ -44,7 +44,7 @@ class Activity implements EntityWithMetaFields, EntityWithBudget
|
||||
#[Exporter\Expose(label: 'id', type: 'integer')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||
#[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: ActivityMeta::class, mappedBy: 'activity', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'activity', targetEntity: ActivityMeta::class, 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: Team::class, cascade: ['persist'], inversedBy: 'activities')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'activities', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Activity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
|
||||
@@ -23,7 +23,7 @@ class ActivityMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Activity::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Activity $activity = null;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ActivityRate implements RateInterface
|
||||
use Rate;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Activity $activity = null;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ class Bookmark
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'type', type: 'string', length: 20, nullable: false)]
|
||||
|
||||
@@ -22,7 +22,7 @@ trait CommentTableTypeTrait
|
||||
#[Assert\NotNull]
|
||||
private ?string $message = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $createdBy = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
|
||||
@@ -156,7 +156,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
*
|
||||
* @var Collection<CustomerMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: CustomerMeta::class, mappedBy: 'customer', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'customer', targetEntity: CustomerMeta::class, 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: Team::class, cascade: ['persist'], inversedBy: 'customers')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'customers', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Customer'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
@@ -180,7 +180,7 @@ class Customer implements EntityWithMetaFields, EntityWithBudget
|
||||
* Default invoice template for this customer
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: InvoiceTemplate::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
private ?InvoiceTemplate $invoiceTemplate = null;
|
||||
#[ORM\Column(name: 'invoice_text', type: 'text', nullable: true)]
|
||||
private ?string $invoiceText = null;
|
||||
|
||||
@@ -21,7 +21,7 @@ class CustomerComment implements CommentInterface
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private Customer $customer;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class CustomerMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class CustomerRate implements RateInterface
|
||||
use Rate;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
|
||||
|
||||
@@ -56,11 +56,11 @@ class Invoice implements EntityWithMetaFields
|
||||
#[Exporter\Expose(label: 'comment')]
|
||||
private ?string $comment = null;
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Customer $customer = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?User $user = null;
|
||||
#[ORM\Column(name: 'created_at', type: 'datetime', nullable: false)]
|
||||
@@ -109,7 +109,7 @@ class Invoice implements EntityWithMetaFields
|
||||
*
|
||||
* @var Collection<InvoiceMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: InvoiceMeta::class, mappedBy: 'invoice', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'invoice', targetEntity: InvoiceMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Invoice'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\InvoiceMeta>')]
|
||||
|
||||
@@ -23,7 +23,7 @@ class InvoiceMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Invoice::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Invoice $invoice = null;
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
* Customer for this project
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: Customer::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
@@ -137,7 +137,7 @@ class Project implements EntityWithMetaFields, EntityWithBudget
|
||||
*
|
||||
* @var Collection<ProjectMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: ProjectMeta::class, mappedBy: 'project', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'project', targetEntity: ProjectMeta::class, 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: Team::class, cascade: ['persist'], inversedBy: 'projects')]
|
||||
#[ORM\ManyToMany(targetEntity: Team::class, inversedBy: 'projects', cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Project'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Team'))]
|
||||
|
||||
@@ -21,7 +21,7 @@ class ProjectComment implements CommentInterface
|
||||
use CommentTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private Project $project;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ class ProjectMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Project $project = null;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class ProjectRate implements RateInterface
|
||||
use Rate;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Project $project = null;
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ trait Rate
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: true)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'CASCADE')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[OA\Property(ref: '#/components/schemas/User')]
|
||||
|
||||
@@ -25,7 +25,7 @@ class RolePermission
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: Role::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Role $role = null;
|
||||
#[ORM\Column(name: 'permission', type: 'string', length: 50, nullable: false)]
|
||||
|
||||
@@ -39,7 +39,7 @@ class Tag
|
||||
#[ORM\Column(name: 'name', type: 'string', length: 100, nullable: false)]
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Length(min: 2, max: 100, normalizer: 'trim')]
|
||||
#[Assert\Regex(pattern: '/,/', match: false, message: 'Tag name cannot contain comma')]
|
||||
#[Assert\Regex(pattern: '/,/', message: 'Tag name cannot contain comma', match: false)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?string $name = null;
|
||||
|
||||
@@ -47,7 +47,7 @@ class Team
|
||||
*
|
||||
* @var Collection<TeamMember>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'team', fetch: 'LAZY', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
#[ORM\OneToMany(mappedBy: 'team', targetEntity: TeamMember::class, cascade: ['persist', 'remove'], fetch: 'LAZY', orphanRemoval: true)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE')]
|
||||
#[Assert\Count(min: 1)]
|
||||
#[Serializer\Expose]
|
||||
@@ -59,7 +59,7 @@ class Team
|
||||
*
|
||||
* @var Collection<Customer>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
||||
#[ORM\ManyToMany(targetEntity: Customer::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Customer'))]
|
||||
@@ -69,7 +69,7 @@ class Team
|
||||
*
|
||||
* @var Collection<Project>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
||||
#[ORM\ManyToMany(targetEntity: Project::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Project'))]
|
||||
@@ -79,7 +79,7 @@ class Team
|
||||
*
|
||||
* @var Collection<Activity>
|
||||
*/
|
||||
#[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
|
||||
#[ORM\ManyToMany(targetEntity: Activity::class, mappedBy: 'teams', cascade: ['persist'], fetch: 'EXTRA_LAZY')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Team_Entity', 'Expanded'])]
|
||||
#[OA\Property(type: 'array', items: new OA\Items(ref: '#/components/schemas/Activity'))]
|
||||
|
||||
@@ -26,14 +26,14 @@ class TeamMember
|
||||
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
|
||||
private ?int $id = null;
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'memberships')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default', 'Entity', 'Team_Entity'])]
|
||||
#[OA\Property(ref: '#/components/schemas/User')]
|
||||
private ?User $user = null;
|
||||
#[ORM\ManyToOne(targetEntity: Team::class, inversedBy: 'members')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default', 'Entity', 'User_Entity'])]
|
||||
|
||||
@@ -15,7 +15,6 @@ use DateTimeZone;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Gedmo\Mapping\Annotation as Gedmo;
|
||||
use JMS\Serializer\Annotation as Serializer;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -128,21 +127,21 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
#[Serializer\Groups(['Default'])]
|
||||
private ?int $duration = 0;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(name: '`user`', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/User')]
|
||||
private ?User $user = null;
|
||||
#[ORM\ManyToOne(targetEntity: Activity::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
#[OA\Property(ref: '#/components/schemas/ActivityExpanded')]
|
||||
private ?Activity $activity = null;
|
||||
#[ORM\ManyToOne(targetEntity: Project::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Subresource', 'Expanded'])]
|
||||
@@ -189,12 +188,8 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
#[ORM\Column(name: 'category', type: 'string', length: 10, nullable: false, options: ['default' => 'work'])]
|
||||
#[Assert\NotNull]
|
||||
private ?string $category = self::WORK;
|
||||
/**
|
||||
* @internal used for limiting queries, eg. via API sync
|
||||
*/
|
||||
#[ORM\Column(name: 'modified_at', type: 'datetime', nullable: true)]
|
||||
#[Gedmo\Timestampable]
|
||||
private ?\DateTime $modifiedAt = null;
|
||||
private \DateTimeInterface $modifiedAt;
|
||||
/**
|
||||
* Tags
|
||||
*
|
||||
@@ -211,7 +206,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
*
|
||||
* @var Collection<TimesheetMeta>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TimesheetMeta::class, mappedBy: 'timesheet', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'timesheet', targetEntity: TimesheetMeta::class, cascade: ['persist'])]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Timesheet'])]
|
||||
#[Serializer\Type(name: 'array<App\Entity\TimesheetMeta>')]
|
||||
@@ -223,6 +218,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
{
|
||||
$this->tags = new ArrayCollection();
|
||||
$this->meta = new ArrayCollection();
|
||||
$this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -590,11 +586,16 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModifiedAt(): ?DateTime
|
||||
public function getModifiedAt(): \DateTimeInterface
|
||||
{
|
||||
return $this->modifiedAt;
|
||||
}
|
||||
|
||||
public function setModifiedAt(\DateTimeInterface $dateTime): void
|
||||
{
|
||||
$this->modifiedAt = $dateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection|MetaTableTypeInterface[]
|
||||
*/
|
||||
@@ -687,7 +688,7 @@ class Timesheet implements EntityWithMetaFields, ExportableItem
|
||||
}
|
||||
|
||||
// field will not be set, if it contains a value
|
||||
$this->modifiedAt = null;
|
||||
$this->modifiedAt = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$this->exported = false;
|
||||
|
||||
$currentMeta = $this->meta;
|
||||
|
||||
@@ -23,7 +23,7 @@ class TimesheetMeta implements MetaTableTypeInterface
|
||||
use MetaTableTypeTrait;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Timesheet::class, inversedBy: 'meta')]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
private ?Timesheet $timesheet = null;
|
||||
|
||||
|
||||
@@ -42,9 +42,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
#[Exporter\Expose(name: 'username', label: 'username', exp: 'object.getUserIdentifier()')]
|
||||
#[Exporter\Expose(name: 'timezone', label: 'timezone', exp: 'object.getTimezone()')]
|
||||
#[Exporter\Expose(name: 'language', label: 'language', exp: 'object.getLanguage()')]
|
||||
#[Exporter\Expose(name: 'last_login', label: 'lastLogin', exp: 'object.getLastLogin()', type: 'datetime')]
|
||||
#[Exporter\Expose(name: 'roles', label: 'roles', exp: 'object.getRoles()', type: 'array')]
|
||||
#[Exporter\Expose(name: 'active', label: 'active', exp: 'object.isEnabled()', type: 'boolean')]
|
||||
#[Exporter\Expose(name: 'last_login', label: 'lastLogin', type: 'datetime', exp: 'object.getLastLogin()')]
|
||||
#[Exporter\Expose(name: 'roles', label: 'roles', type: 'array', exp: 'object.getRoles()')]
|
||||
#[Exporter\Expose(name: 'active', label: 'active', type: 'boolean', exp: 'object.isEnabled()')]
|
||||
#[Constraints\User(groups: ['UserCreate', 'Registration', 'Default', 'Profile'])]
|
||||
class User implements UserInterface, EquatableInterface, ThemeUserInterface, PasswordAuthenticatedUserInterface, TwoFactorInterface
|
||||
{
|
||||
@@ -127,15 +127,15 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
*
|
||||
* @var Collection<UserPreference>|null
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: UserPreference::class, mappedBy: 'user', cascade: ['persist'])]
|
||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: UserPreference::class, cascade: ['persist'])]
|
||||
private ?Collection $preferences = null;
|
||||
/**
|
||||
* List of all team memberships.
|
||||
*
|
||||
* @var Collection<TeamMember>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: TeamMember::class, mappedBy: 'user', fetch: 'LAZY', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[ORM\JoinColumn(onDelete: 'CASCADE', nullable: false)]
|
||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: TeamMember::class, cascade: ['persist'], fetch: 'LAZY', orphanRemoval: true)]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
#[Assert\NotNull]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
@@ -217,7 +217,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
#[ORM\Column(name: 'system_account', type: 'boolean', nullable: false, options: ['default' => false])]
|
||||
private bool $systemAccount = false;
|
||||
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||
#[ORM\JoinColumn(onDelete: 'SET NULL', nullable: true)]
|
||||
#[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(ref: '#/components/schemas/User')]
|
||||
@@ -297,6 +297,14 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('apiToken')]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
public function hasApiToken(): bool
|
||||
{
|
||||
return $this->apiToken !== null;
|
||||
}
|
||||
|
||||
public function getPlainApiToken(): ?string
|
||||
{
|
||||
return $this->plainApiToken;
|
||||
@@ -836,9 +844,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this->email !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
@@ -864,9 +869,6 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this->confirmationToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = $this->roles;
|
||||
@@ -963,7 +965,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setPasswordRequestedAt(\DateTime $date = null): User
|
||||
public function setPasswordRequestedAt(?\DateTime $date = null): User
|
||||
{
|
||||
$this->passwordRequestedAt = $date;
|
||||
|
||||
@@ -1113,6 +1115,20 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this->getDisplayName();
|
||||
}
|
||||
|
||||
public function requiresPasswordReset(): bool
|
||||
{
|
||||
if (!$this->isInternalUser() || !$this->isEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->getPreferenceValue('__pw_reset__') === '1';
|
||||
}
|
||||
|
||||
public function setRequiresPasswordReset(bool $require = true): void
|
||||
{
|
||||
$this->setPreferenceValue('__pw_reset__', ($require ? '1' : '0'));
|
||||
}
|
||||
|
||||
public function hasSeenWizard(string $wizard): bool
|
||||
{
|
||||
$wizards = $this->getPreferenceValue('__wizards__');
|
||||
|
||||
Reference in New Issue
Block a user