mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-21 10:29:31 +00:00
Add makefile to help with development setup, change part_ids in bulk import jobs to junction table and implement filtering based on bulk import jobs status and its associated parts' statuses.
This commit is contained in:
parent
9b4d5e9c27
commit
cc9d50a8fe
22 changed files with 1357 additions and 120 deletions
|
|
@ -23,7 +23,10 @@ declare(strict_types=1);
|
|||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Entity\UserSystem\User;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
|
|
@ -43,9 +46,6 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
|||
#[ORM\Column(type: Types::TEXT)]
|
||||
private string $name = '';
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $partIds = [];
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $fieldMappings = [];
|
||||
|
||||
|
|
@ -68,12 +68,14 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
|||
#[ORM\JoinColumn(nullable: false)]
|
||||
private ?User $createdBy = null;
|
||||
|
||||
#[ORM\Column(type: Types::JSON)]
|
||||
private array $progress = [];
|
||||
/** @var Collection<int, BulkInfoProviderImportJobPart> */
|
||||
#[ORM\OneToMany(targetEntity: BulkInfoProviderImportJobPart::class, mappedBy: 'job', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection $jobParts;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
$this->jobParts = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
|
|
@ -102,14 +104,50 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getJobParts(): Collection
|
||||
{
|
||||
return $this->jobParts;
|
||||
}
|
||||
|
||||
public function addJobPart(BulkInfoProviderImportJobPart $jobPart): self
|
||||
{
|
||||
if (!$this->jobParts->contains($jobPart)) {
|
||||
$this->jobParts->add($jobPart);
|
||||
$jobPart->setJob($this);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeJobPart(BulkInfoProviderImportJobPart $jobPart): self
|
||||
{
|
||||
if ($this->jobParts->removeElement($jobPart)) {
|
||||
if ($jobPart->getJob() === $this) {
|
||||
$jobPart->setJob(null);
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPartIds(): array
|
||||
{
|
||||
return $this->partIds;
|
||||
return $this->jobParts->map(fn($jobPart) => $jobPart->getPart()->getId())->toArray();
|
||||
}
|
||||
|
||||
public function setPartIds(array $partIds): self
|
||||
{
|
||||
$this->partIds = $partIds;
|
||||
// This method is kept for backward compatibility but should be replaced with addJobPart
|
||||
// Clear existing job parts
|
||||
$this->jobParts->clear();
|
||||
|
||||
// Add new job parts (this would need the actual Part entities, not just IDs)
|
||||
// This is a simplified implementation - in practice, you'd want to pass Part entities
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addPart(Part $part): self
|
||||
{
|
||||
$jobPart = new BulkInfoProviderImportJobPart($this, $part);
|
||||
$this->addJobPart($jobPart);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -186,12 +224,31 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
|||
|
||||
public function getProgress(): array
|
||||
{
|
||||
return $this->progress;
|
||||
$progress = [];
|
||||
foreach ($this->jobParts as $jobPart) {
|
||||
$progressData = [
|
||||
'status' => $jobPart->getStatus()->value
|
||||
];
|
||||
|
||||
// Only include completed_at if it's not null
|
||||
if ($jobPart->getCompletedAt() !== null) {
|
||||
$progressData['completed_at'] = $jobPart->getCompletedAt()->format('c');
|
||||
}
|
||||
|
||||
// Only include reason if it's not null
|
||||
if ($jobPart->getReason() !== null) {
|
||||
$progressData['reason'] = $jobPart->getReason();
|
||||
}
|
||||
|
||||
$progress[$jobPart->getPart()->getId()] = $progressData;
|
||||
}
|
||||
return $progress;
|
||||
}
|
||||
|
||||
public function setProgress(array $progress): self
|
||||
{
|
||||
$this->progress = $progress;
|
||||
// This method is kept for backward compatibility
|
||||
// The progress is now managed through the jobParts relationship
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
|
@ -254,7 +311,7 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
|||
|
||||
public function getPartCount(): int
|
||||
{
|
||||
return count($this->partIds);
|
||||
return $this->jobParts->count();
|
||||
}
|
||||
|
||||
public function getResultCount(): int
|
||||
|
|
@ -268,48 +325,61 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
|||
|
||||
public function markPartAsCompleted(int $partId): self
|
||||
{
|
||||
$this->progress[$partId] = [
|
||||
'status' => 'completed',
|
||||
'completed_at' => (new \DateTimeImmutable())->format('c')
|
||||
];
|
||||
$jobPart = $this->findJobPartByPartId($partId);
|
||||
if ($jobPart) {
|
||||
$jobPart->markAsCompleted();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markPartAsSkipped(int $partId, string $reason = ''): self
|
||||
{
|
||||
$this->progress[$partId] = [
|
||||
'status' => 'skipped',
|
||||
'reason' => $reason,
|
||||
'completed_at' => (new \DateTimeImmutable())->format('c')
|
||||
];
|
||||
$jobPart = $this->findJobPartByPartId($partId);
|
||||
if ($jobPart) {
|
||||
$jobPart->markAsSkipped($reason);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markPartAsPending(int $partId): self
|
||||
{
|
||||
// Remove from progress array to mark as pending
|
||||
unset($this->progress[$partId]);
|
||||
$jobPart = $this->findJobPartByPartId($partId);
|
||||
if ($jobPart) {
|
||||
$jobPart->markAsPending();
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPartCompleted(int $partId): bool
|
||||
{
|
||||
return isset($this->progress[$partId]) && $this->progress[$partId]['status'] === 'completed';
|
||||
$jobPart = $this->findJobPartByPartId($partId);
|
||||
return $jobPart ? $jobPart->isCompleted() : false;
|
||||
}
|
||||
|
||||
public function isPartSkipped(int $partId): bool
|
||||
{
|
||||
return isset($this->progress[$partId]) && $this->progress[$partId]['status'] === 'skipped';
|
||||
$jobPart = $this->findJobPartByPartId($partId);
|
||||
return $jobPart ? $jobPart->isSkipped() : false;
|
||||
}
|
||||
|
||||
public function getCompletedPartsCount(): int
|
||||
{
|
||||
return count(array_filter($this->progress, fn($p) => $p['status'] === 'completed'));
|
||||
return $this->jobParts->filter(fn($jobPart) => $jobPart->isCompleted())->count();
|
||||
}
|
||||
|
||||
public function getSkippedPartsCount(): int
|
||||
{
|
||||
return count(array_filter($this->progress, fn($p) => $p['status'] === 'skipped'));
|
||||
return $this->jobParts->filter(fn($jobPart) => $jobPart->isSkipped())->count();
|
||||
}
|
||||
|
||||
private function findJobPartByPartId(int $partId): ?BulkInfoProviderImportJobPart
|
||||
{
|
||||
foreach ($this->jobParts as $jobPart) {
|
||||
if ($jobPart->getPart()->getId() === $partId) {
|
||||
return $jobPart;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getProgressPercentage(): float
|
||||
|
|
|
|||
172
src/Entity/BulkInfoProviderImportJobPart.php
Normal file
172
src/Entity/BulkInfoProviderImportJobPart.php
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Parts\Part;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
enum BulkImportPartStatus: string
|
||||
{
|
||||
case PENDING = 'pending';
|
||||
case COMPLETED = 'completed';
|
||||
case SKIPPED = 'skipped';
|
||||
case FAILED = 'failed';
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'bulk_info_provider_import_job_parts')]
|
||||
#[ORM\UniqueConstraint(name: 'unique_job_part', columns: ['job_id', 'part_id'])]
|
||||
class BulkInfoProviderImportJobPart extends AbstractDBElement
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: BulkInfoProviderImportJob::class, inversedBy: 'jobParts')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private BulkInfoProviderImportJob $job;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: Part::class)]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private Part $part;
|
||||
|
||||
#[ORM\Column(type: Types::STRING, length: 20, enumType: BulkImportPartStatus::class)]
|
||||
private BulkImportPartStatus $status = BulkImportPartStatus::PENDING;
|
||||
|
||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $reason = null;
|
||||
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
|
||||
private ?\DateTimeImmutable $completedAt = null;
|
||||
|
||||
public function __construct(BulkInfoProviderImportJob $job, Part $part)
|
||||
{
|
||||
$this->job = $job;
|
||||
$this->part = $part;
|
||||
}
|
||||
|
||||
public function getJob(): BulkInfoProviderImportJob
|
||||
{
|
||||
return $this->job;
|
||||
}
|
||||
|
||||
public function setJob(?BulkInfoProviderImportJob $job): self
|
||||
{
|
||||
$this->job = $job;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPart(): Part
|
||||
{
|
||||
return $this->part;
|
||||
}
|
||||
|
||||
public function setPart(?Part $part): self
|
||||
{
|
||||
$this->part = $part;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): BulkImportPartStatus
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(BulkImportPartStatus $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getReason(): ?string
|
||||
{
|
||||
return $this->reason;
|
||||
}
|
||||
|
||||
public function setReason(?string $reason): self
|
||||
{
|
||||
$this->reason = $reason;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCompletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->completedAt;
|
||||
}
|
||||
|
||||
public function setCompletedAt(?\DateTimeImmutable $completedAt): self
|
||||
{
|
||||
$this->completedAt = $completedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsCompleted(): self
|
||||
{
|
||||
$this->status = BulkImportPartStatus::COMPLETED;
|
||||
$this->completedAt = new \DateTimeImmutable();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsSkipped(string $reason = ''): self
|
||||
{
|
||||
$this->status = BulkImportPartStatus::SKIPPED;
|
||||
$this->reason = $reason;
|
||||
$this->completedAt = new \DateTimeImmutable();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsFailed(string $reason = ''): self
|
||||
{
|
||||
$this->status = BulkImportPartStatus::FAILED;
|
||||
$this->reason = $reason;
|
||||
$this->completedAt = new \DateTimeImmutable();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function markAsPending(): self
|
||||
{
|
||||
$this->status = BulkImportPartStatus::PENDING;
|
||||
$this->reason = null;
|
||||
$this->completedAt = null;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isPending(): bool
|
||||
{
|
||||
return $this->status === BulkImportPartStatus::PENDING;
|
||||
}
|
||||
|
||||
public function isCompleted(): bool
|
||||
{
|
||||
return $this->status === BulkImportPartStatus::COMPLETED;
|
||||
}
|
||||
|
||||
public function isSkipped(): bool
|
||||
{
|
||||
return $this->status === BulkImportPartStatus::SKIPPED;
|
||||
}
|
||||
|
||||
public function isFailed(): bool
|
||||
{
|
||||
return $this->status === BulkImportPartStatus::FAILED;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ namespace App\Entity\LogSystem;
|
|||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\BulkInfoProviderImportJob;
|
||||
use App\Entity\BulkInfoProviderImportJobPart;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
use App\Entity\Parameters\AbstractParameter;
|
||||
use App\Entity\Parts\Category;
|
||||
|
|
@ -69,6 +70,7 @@ enum LogTargetType: int
|
|||
|
||||
case PART_ASSOCIATION = 20;
|
||||
case BULK_INFO_PROVIDER_IMPORT_JOB = 21;
|
||||
case BULK_INFO_PROVIDER_IMPORT_JOB_PART = 22;
|
||||
|
||||
/**
|
||||
* Returns the class name of the target type or null if the target type is NONE.
|
||||
|
|
@ -99,6 +101,7 @@ enum LogTargetType: int
|
|||
self::LABEL_PROFILE => LabelProfile::class,
|
||||
self::PART_ASSOCIATION => PartAssociation::class,
|
||||
self::BULK_INFO_PROVIDER_IMPORT_JOB => BulkInfoProviderImportJob::class,
|
||||
self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => BulkInfoProviderImportJobPart::class,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ use App\Entity\Parts\PartTraits\ManufacturerTrait;
|
|||
use App\Entity\Parts\PartTraits\OrderTrait;
|
||||
use App\Entity\Parts\PartTraits\ProjectTrait;
|
||||
use App\EntityListeners\TreeCacheInvalidationListener;
|
||||
use App\Entity\BulkInfoProviderImportJobPart;
|
||||
use App\Repository\PartRepository;
|
||||
use App\Validator\Constraints\UniqueObjectCollection;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
|
@ -83,8 +84,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||
#[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')]
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read',
|
||||
'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read'],
|
||||
new Get(normalizationContext: [
|
||||
'groups' => [
|
||||
'part:read',
|
||||
'provider_reference:read',
|
||||
'api:basic:read',
|
||||
'part_lot:read',
|
||||
'orderdetail:read',
|
||||
'pricedetail:read',
|
||||
'parameter:read',
|
||||
'attachment:read',
|
||||
'eda_info:read'
|
||||
],
|
||||
'openapi_definition_name' => 'Read',
|
||||
], security: 'is_granted("read", object)'),
|
||||
new GetCollection(security: 'is_granted("@parts.read")'),
|
||||
|
|
@ -92,7 +103,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||
new Patch(security: 'is_granted("edit", object)'),
|
||||
new Delete(security: 'is_granted("delete", object)'),
|
||||
],
|
||||
normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'],
|
||||
normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read'], 'openapi_definition_name' => 'Read'],
|
||||
denormalizationContext: ['groups' => ['part:write', 'api:basic:write', 'eda_info:write', 'attachment:write', 'parameter:write'], 'openapi_definition_name' => 'Write'],
|
||||
)]
|
||||
#[ApiFilter(PropertyFilter::class)]
|
||||
|
|
@ -100,7 +111,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|||
#[ApiFilter(PartStoragelocationFilter::class, properties: ["storage_location"])]
|
||||
#[ApiFilter(LikeFilter::class, properties: ["name", "comment", "description", "ipn", "manufacturer_product_number"])]
|
||||
#[ApiFilter(TagFilter::class, properties: ["tags"])]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ["favorite" , "needs_review"])]
|
||||
#[ApiFilter(BooleanFilter::class, properties: ["favorite", "needs_review"])]
|
||||
#[ApiFilter(RangeFilter::class, properties: ["mass", "minamount"])]
|
||||
#[ApiFilter(DateFilter::class, strategy: DateFilterInterface::EXCLUDE_NULL)]
|
||||
#[ApiFilter(OrderFilter::class, properties: ['name', 'id', 'addedDate', 'lastModified'])]
|
||||
|
|
@ -160,6 +171,12 @@ class Part extends AttachmentContainingDBElement
|
|||
#[Groups(['part:read'])]
|
||||
protected ?\DateTimeImmutable $lastModified = null;
|
||||
|
||||
/**
|
||||
* @var Collection<int, BulkInfoProviderImportJobPart>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'part', targetEntity: BulkInfoProviderImportJobPart::class, cascade: ['remove'], orphanRemoval: true)]
|
||||
protected Collection $bulkImportJobParts;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
|
@ -172,6 +189,7 @@ class Part extends AttachmentContainingDBElement
|
|||
|
||||
$this->associated_parts_as_owner = new ArrayCollection();
|
||||
$this->associated_parts_as_other = new ArrayCollection();
|
||||
$this->bulkImportJobParts = new ArrayCollection();
|
||||
|
||||
//By default, the part has no provider
|
||||
$this->providerReference = InfoProviderReference::noProvider();
|
||||
|
|
@ -230,4 +248,38 @@ class Part extends AttachmentContainingDBElement
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bulk import job parts for this part
|
||||
* @return Collection<int, BulkInfoProviderImportJobPart>
|
||||
*/
|
||||
public function getBulkImportJobParts(): Collection
|
||||
{
|
||||
return $this->bulkImportJobParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a bulk import job part to this part
|
||||
*/
|
||||
public function addBulkImportJobPart(BulkInfoProviderImportJobPart $jobPart): self
|
||||
{
|
||||
if (!$this->bulkImportJobParts->contains($jobPart)) {
|
||||
$this->bulkImportJobParts->add($jobPart);
|
||||
$jobPart->setPart($this);
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a bulk import job part from this part
|
||||
*/
|
||||
public function removeBulkImportJobPart(BulkInfoProviderImportJobPart $jobPart): self
|
||||
{
|
||||
if ($this->bulkImportJobParts->removeElement($jobPart)) {
|
||||
if ($jobPart->getPart() === $this) {
|
||||
$jobPart->setPart(null);
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue