mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 11:09:29 +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
99
Makefile
Normal file
99
Makefile
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
# PartDB Makefile for Test Environment Management
|
||||||
|
|
||||||
|
.PHONY: help test-setup test-clean test-db-create test-db-migrate test-cache-clear test-fixtures test-run dev-setup dev-clean dev-db-create dev-db-migrate dev-cache-clear dev-warmup dev-reset
|
||||||
|
|
||||||
|
# Default target
|
||||||
|
help:
|
||||||
|
@echo "PartDB Test Environment Management"
|
||||||
|
@echo "=================================="
|
||||||
|
@echo ""
|
||||||
|
@echo "Available targets:"
|
||||||
|
@echo " test-setup - Complete test environment setup (clean, create DB, migrate, load fixtures)"
|
||||||
|
@echo " test-clean - Clean test cache and database files"
|
||||||
|
@echo " test-db-create - Create test database (if not exists)"
|
||||||
|
@echo " test-db-migrate - Run database migrations for test environment"
|
||||||
|
@echo " test-cache-clear- Clear test cache"
|
||||||
|
@echo " test-fixtures - Load test fixtures"
|
||||||
|
@echo " test-run - Run PHPUnit tests"
|
||||||
|
@echo ""
|
||||||
|
@echo "Development Environment:"
|
||||||
|
@echo " dev-setup - Complete development environment setup (clean, create DB, migrate, warmup)"
|
||||||
|
@echo " dev-clean - Clean development cache and database files"
|
||||||
|
@echo " dev-db-create - Create development database (if not exists)"
|
||||||
|
@echo " dev-db-migrate - Run database migrations for development environment"
|
||||||
|
@echo " dev-cache-clear - Clear development cache"
|
||||||
|
@echo " dev-warmup - Warm up development cache"
|
||||||
|
@echo " dev-reset - Quick development reset (clean + migrate)"
|
||||||
|
@echo ""
|
||||||
|
@echo " help - Show this help message"
|
||||||
|
|
||||||
|
# Complete test environment setup
|
||||||
|
test-setup: test-clean test-db-create test-db-migrate test-fixtures
|
||||||
|
@echo "✅ Test environment setup complete!"
|
||||||
|
|
||||||
|
# Clean test environment
|
||||||
|
test-clean:
|
||||||
|
@echo "🧹 Cleaning test environment..."
|
||||||
|
rm -rf var/cache/test
|
||||||
|
rm -f var/app_test.db
|
||||||
|
@echo "✅ Test environment cleaned"
|
||||||
|
|
||||||
|
# Create test database
|
||||||
|
test-db-create:
|
||||||
|
@echo "🗄️ Creating test database..."
|
||||||
|
-php bin/console doctrine:database:create --if-not-exists --env test || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
|
||||||
|
|
||||||
|
# Run database migrations for test environment
|
||||||
|
test-db-migrate:
|
||||||
|
@echo "🔄 Running database migrations..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env test
|
||||||
|
|
||||||
|
# Clear test cache
|
||||||
|
test-cache-clear:
|
||||||
|
@echo "🗑️ Clearing test cache..."
|
||||||
|
rm -rf var/cache/test
|
||||||
|
@echo "✅ Test cache cleared"
|
||||||
|
|
||||||
|
# Load test fixtures
|
||||||
|
test-fixtures:
|
||||||
|
@echo "📦 Loading test fixtures..."
|
||||||
|
php bin/console partdb:fixtures:load -n --env test
|
||||||
|
|
||||||
|
# Run PHPUnit tests
|
||||||
|
test-run:
|
||||||
|
@echo "🧪 Running tests..."
|
||||||
|
php bin/phpunit
|
||||||
|
|
||||||
|
# Quick test reset (clean + migrate + fixtures, skip DB creation)
|
||||||
|
test-reset: test-cache-clear test-db-migrate test-fixtures
|
||||||
|
@echo "✅ Test environment reset complete!"
|
||||||
|
|
||||||
|
# Development helpers
|
||||||
|
dev-setup: dev-clean dev-db-create dev-db-migrate dev-warmup
|
||||||
|
@echo "✅ Development environment setup complete!"
|
||||||
|
|
||||||
|
dev-clean:
|
||||||
|
@echo "🧹 Cleaning development environment..."
|
||||||
|
rm -rf var/cache/dev
|
||||||
|
rm -f var/app_dev.db
|
||||||
|
@echo "✅ Development environment cleaned"
|
||||||
|
|
||||||
|
dev-db-create:
|
||||||
|
@echo "🗄️ Creating development database..."
|
||||||
|
-php bin/console doctrine:database:create --if-not-exists --env dev || echo "⚠️ Database creation failed (expected for SQLite) - continuing..."
|
||||||
|
|
||||||
|
dev-db-migrate:
|
||||||
|
@echo "🔄 Running database migrations..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 php bin/console doctrine:migrations:migrate -n --env dev
|
||||||
|
|
||||||
|
dev-cache-clear:
|
||||||
|
@echo "🗑️ Clearing development cache..."
|
||||||
|
rm -rf var/cache/dev
|
||||||
|
@echo "✅ Development cache cleared"
|
||||||
|
|
||||||
|
dev-warmup:
|
||||||
|
@echo "🔥 Warming up development cache..."
|
||||||
|
COMPOSER_MEMORY_LIMIT=-1 php bin/console cache:warmup --env dev -n --memory-limit=1G
|
||||||
|
|
||||||
|
dev-reset: dev-cache-clear dev-db-migrate
|
||||||
|
@echo "✅ Development environment reset complete!"
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use App\Migration\AbstractMultiPlatformMigration;
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Auto-generated Migration: Please modify to your needs!
|
|
||||||
*/
|
|
||||||
final class Version20250802153643 extends AbstractMultiPlatformMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add bulk info provider import jobs table';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mySQLUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INT AUTO_INCREMENT NOT NULL, name LONGTEXT NOT NULL, part_ids LONGTEXT NOT NULL, field_mappings LONGTEXT NOT NULL, search_results LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details TINYINT(1) NOT NULL, progress LONGTEXT NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES `users` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
|
||||||
$this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function mySQLDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('DROP TABLE bulk_info_provider_import_jobs');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sqLiteUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name CLOB NOT NULL, part_ids CLOB NOT NULL, field_mappings CLOB NOT NULL, search_results CLOB NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, progress CLOB NOT NULL, created_by_id INTEGER NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function sqLiteDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('DROP TABLE bulk_info_provider_import_jobs');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function postgreSQLUp(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id SERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL, part_ids TEXT NOT NULL, field_mappings TEXT NOT NULL, search_results TEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, progress TEXT NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
|
||||||
$this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function postgreSQLDown(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('DROP TABLE bulk_info_provider_import_jobs');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
70
migrations/Version20250802205143.php
Normal file
70
migrations/Version20250802205143.php
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use App\Migration\AbstractMultiPlatformMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20250802205143 extends AbstractMultiPlatformMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add bulk info provider import jobs and job parts tables';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mySQLUp(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INT AUTO_INCREMENT NOT NULL, name LONGTEXT NOT NULL, field_mappings LONGTEXT NOT NULL, search_results LONGTEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details TINYINT(1) NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES `users` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
|
||||||
|
|
||||||
|
$this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id INT AUTO_INCREMENT NOT NULL, status VARCHAR(20) NOT NULL, reason LONGTEXT DEFAULT NULL, completed_at DATETIME DEFAULT NULL, job_id INT NOT NULL, part_id INT NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id), CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES `parts` (id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci ENGINE = InnoDB');
|
||||||
|
$this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mySQLDown(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE bulk_info_provider_import_job_parts');
|
||||||
|
$this->addSql('DROP TABLE bulk_info_provider_import_jobs');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sqLiteUp(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name CLOB NOT NULL, field_mappings CLOB NOT NULL, search_results CLOB NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, completed_at DATETIME DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, created_by_id INTEGER NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
|
||||||
|
|
||||||
|
$this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, status VARCHAR(20) NOT NULL, reason CLOB DEFAULT NULL, completed_at DATETIME DEFAULT NULL, job_id INTEGER NOT NULL, part_id INTEGER NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES "parts" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function sqLiteDown(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE bulk_info_provider_import_job_parts');
|
||||||
|
$this->addSql('DROP TABLE bulk_info_provider_import_jobs');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postgreSQLUp(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE TABLE bulk_info_provider_import_jobs (id SERIAL PRIMARY KEY NOT NULL, name TEXT NOT NULL, field_mappings TEXT NOT NULL, search_results TEXT NOT NULL, status VARCHAR(20) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, prefetch_details BOOLEAN NOT NULL, created_by_id INT NOT NULL, CONSTRAINT FK_7F58C1EDB03A8386 FOREIGN KEY (created_by_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_7F58C1EDB03A8386 ON bulk_info_provider_import_jobs (created_by_id)');
|
||||||
|
|
||||||
|
$this->addSql('CREATE TABLE bulk_info_provider_import_job_parts (id SERIAL PRIMARY KEY NOT NULL, status VARCHAR(20) NOT NULL, reason TEXT DEFAULT NULL, completed_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, job_id INT NOT NULL, part_id INT NOT NULL, CONSTRAINT FK_CD93F28FBE04EA9 FOREIGN KEY (job_id) REFERENCES bulk_info_provider_import_jobs (id) NOT DEFERRABLE INITIALLY IMMEDIATE, CONSTRAINT FK_CD93F28F4CE34BEC FOREIGN KEY (part_id) REFERENCES parts (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_CD93F28FBE04EA9 ON bulk_info_provider_import_job_parts (job_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_CD93F28F4CE34BEC ON bulk_info_provider_import_job_parts (part_id)');
|
||||||
|
$this->addSql('CREATE UNIQUE INDEX unique_job_part ON bulk_info_provider_import_job_parts (job_id, part_id)');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function postgreSQLDown(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP TABLE bulk_info_provider_import_job_parts');
|
||||||
|
$this->addSql('DROP TABLE bulk_info_provider_import_jobs');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,7 @@ declare(strict_types=1);
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\BulkInfoProviderImportJob;
|
use App\Entity\BulkInfoProviderImportJob;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
use App\Entity\BulkImportJobStatus;
|
use App\Entity\BulkImportJobStatus;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\Supplier;
|
use App\Entity\Parts\Supplier;
|
||||||
|
|
@ -104,7 +105,6 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
|
|
||||||
// Create and save the job
|
// Create and save the job
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setPartIds(array_map(fn($part) => $part->getId(), $parts));
|
|
||||||
$job->setFieldMappings($fieldMappings);
|
$job->setFieldMappings($fieldMappings);
|
||||||
$job->setPrefetchDetails($prefetchDetails);
|
$job->setPrefetchDetails($prefetchDetails);
|
||||||
$user = $this->getUser();
|
$user = $this->getUser();
|
||||||
|
|
@ -113,6 +113,12 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
}
|
}
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
|
|
||||||
|
// Create job parts for each part
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$jobPart = new BulkInfoProviderImportJobPart($job, $part);
|
||||||
|
$job->addJobPart($jobPart);
|
||||||
|
}
|
||||||
|
|
||||||
$this->entityManager->persist($job);
|
$this->entityManager->persist($job);
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
|
@ -372,8 +378,7 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the parts and deserialize search results
|
// Get the parts and deserialize search results
|
||||||
$partRepository = $this->entityManager->getRepository(Part::class);
|
$parts = $job->getJobParts()->map(fn($jobPart) => $jobPart->getPart())->toArray();
|
||||||
$parts = $partRepository->getElementsFromIDArray($job->getPartIds());
|
|
||||||
$searchResults = $this->deserializeSearchResults($job->getSearchResults(), $parts);
|
$searchResults = $this->deserializeSearchResults($job->getSearchResults(), $parts);
|
||||||
|
|
||||||
return $this->render('info_providers/bulk_import/step2.html.twig', [
|
return $this->render('info_providers/bulk_import/step2.html.twig', [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
<?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\DataTables\Filters\Constraints\Part;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\AbstractConstraint;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
class BulkImportJobExistsConstraint extends AbstractConstraint
|
||||||
|
{
|
||||||
|
/** @var bool|null The value of our constraint */
|
||||||
|
protected ?bool $value = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('bulk_import_job_exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the value of this constraint. Null means "don't filter", true means "filter for parts in bulk import jobs", false means "filter for parts not in bulk import jobs".
|
||||||
|
*/
|
||||||
|
public function getValue(): ?bool
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value of this constraint. Null means "don't filter", true means "filter for parts in bulk import jobs", false means "filter for parts not in bulk import jobs".
|
||||||
|
*/
|
||||||
|
public function setValue(?bool $value): void
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return $this->value !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(QueryBuilder $queryBuilder): void
|
||||||
|
{
|
||||||
|
// Do not apply a filter if value is null (filter is set to ignore)
|
||||||
|
if (!$this->isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use EXISTS subquery to avoid join conflicts
|
||||||
|
$existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder();
|
||||||
|
$existsSubquery->select('1')
|
||||||
|
->from(BulkInfoProviderImportJobPart::class, 'bip_exists')
|
||||||
|
->where('bip_exists.part = part.id');
|
||||||
|
|
||||||
|
if ($this->value === true) {
|
||||||
|
// Filter for parts that ARE in bulk import jobs
|
||||||
|
$queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')');
|
||||||
|
} else {
|
||||||
|
// Filter for parts that are NOT in bulk import jobs
|
||||||
|
$queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
<?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\DataTables\Filters\Constraints\Part;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\AbstractConstraint;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
class BulkImportJobStatusConstraint extends AbstractConstraint
|
||||||
|
{
|
||||||
|
/** @var array The status values to filter by */
|
||||||
|
protected array $values = [];
|
||||||
|
|
||||||
|
/** @var string|null The operator to use ('any_of', 'none_of', 'all_of') */
|
||||||
|
protected ?string $operator = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('bulk_import_job_status');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the status values to filter by.
|
||||||
|
*/
|
||||||
|
public function getValues(): array
|
||||||
|
{
|
||||||
|
return $this->values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the status values to filter by.
|
||||||
|
*/
|
||||||
|
public function setValues(array $values): void
|
||||||
|
{
|
||||||
|
$this->values = $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the operator to use.
|
||||||
|
*/
|
||||||
|
public function getOperator(): ?string
|
||||||
|
{
|
||||||
|
return $this->operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the operator to use.
|
||||||
|
*/
|
||||||
|
public function setOperator(?string $operator): void
|
||||||
|
{
|
||||||
|
$this->operator = $operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->values) && $this->operator !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(QueryBuilder $queryBuilder): void
|
||||||
|
{
|
||||||
|
// Do not apply a filter if values are empty or operator is null
|
||||||
|
if (!$this->isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use EXISTS subquery to check if part has a job with the specified status(es)
|
||||||
|
$existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder();
|
||||||
|
$existsSubquery->select('1')
|
||||||
|
->from(BulkInfoProviderImportJobPart::class, 'bip_status')
|
||||||
|
->join('bip_status.job', 'job_status')
|
||||||
|
->where('bip_status.part = part.id');
|
||||||
|
|
||||||
|
// Add status conditions based on operator
|
||||||
|
if ($this->operator === 'ANY') {
|
||||||
|
$existsSubquery->andWhere('job_status.status IN (:job_status_values)');
|
||||||
|
$queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')');
|
||||||
|
$queryBuilder->setParameter('job_status_values', $this->values);
|
||||||
|
} elseif ($this->operator === 'NONE') {
|
||||||
|
$existsSubquery->andWhere('job_status.status IN (:job_status_values)');
|
||||||
|
$queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')');
|
||||||
|
$queryBuilder->setParameter('job_status_values', $this->values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,104 @@
|
||||||
|
<?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\DataTables\Filters\Constraints\Part;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\AbstractConstraint;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
class BulkImportPartStatusConstraint extends AbstractConstraint
|
||||||
|
{
|
||||||
|
/** @var array The status values to filter by */
|
||||||
|
protected array $values = [];
|
||||||
|
|
||||||
|
/** @var string|null The operator to use ('any_of', 'none_of', 'all_of') */
|
||||||
|
protected ?string $operator = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct('bulk_import_part_status');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the status values to filter by.
|
||||||
|
*/
|
||||||
|
public function getValues(): array
|
||||||
|
{
|
||||||
|
return $this->values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the status values to filter by.
|
||||||
|
*/
|
||||||
|
public function setValues(array $values): void
|
||||||
|
{
|
||||||
|
$this->values = $values;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the operator to use.
|
||||||
|
*/
|
||||||
|
public function getOperator(): ?string
|
||||||
|
{
|
||||||
|
return $this->operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the operator to use.
|
||||||
|
*/
|
||||||
|
public function setOperator(?string $operator): void
|
||||||
|
{
|
||||||
|
$this->operator = $operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->values) && $this->operator !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(QueryBuilder $queryBuilder): void
|
||||||
|
{
|
||||||
|
// Do not apply a filter if values are empty or operator is null
|
||||||
|
if (!$this->isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use EXISTS subquery to check if part has the specified status(es)
|
||||||
|
$existsSubquery = $queryBuilder->getEntityManager()->createQueryBuilder();
|
||||||
|
$existsSubquery->select('1')
|
||||||
|
->from(BulkInfoProviderImportJobPart::class, 'bip_part_status')
|
||||||
|
->where('bip_part_status.part = part.id');
|
||||||
|
|
||||||
|
// Add status conditions based on operator
|
||||||
|
if ($this->operator === 'ANY') {
|
||||||
|
$existsSubquery->andWhere('bip_part_status.status IN (:part_status_values)');
|
||||||
|
$queryBuilder->andWhere('EXISTS (' . $existsSubquery->getDQL() . ')');
|
||||||
|
$queryBuilder->setParameter('part_status_values', $this->values);
|
||||||
|
} elseif ($this->operator === 'NONE') {
|
||||||
|
$existsSubquery->andWhere('bip_part_status.status IN (:part_status_values)');
|
||||||
|
$queryBuilder->andWhere('NOT EXISTS (' . $existsSubquery->getDQL() . ')');
|
||||||
|
$queryBuilder->setParameter('part_status_values', $this->values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,6 +31,9 @@ use App\DataTables\Filters\Constraints\NumberConstraint;
|
||||||
use App\DataTables\Filters\Constraints\Part\LessThanDesiredConstraint;
|
use App\DataTables\Filters\Constraints\Part\LessThanDesiredConstraint;
|
||||||
use App\DataTables\Filters\Constraints\Part\ParameterConstraint;
|
use App\DataTables\Filters\Constraints\Part\ParameterConstraint;
|
||||||
use App\DataTables\Filters\Constraints\Part\TagsConstraint;
|
use App\DataTables\Filters\Constraints\Part\TagsConstraint;
|
||||||
|
use App\DataTables\Filters\Constraints\Part\BulkImportJobExistsConstraint;
|
||||||
|
use App\DataTables\Filters\Constraints\Part\BulkImportJobStatusConstraint;
|
||||||
|
use App\DataTables\Filters\Constraints\Part\BulkImportPartStatusConstraint;
|
||||||
use App\DataTables\Filters\Constraints\TextConstraint;
|
use App\DataTables\Filters\Constraints\TextConstraint;
|
||||||
use App\Entity\Attachments\AttachmentType;
|
use App\Entity\Attachments\AttachmentType;
|
||||||
use App\Entity\Parts\Category;
|
use App\Entity\Parts\Category;
|
||||||
|
|
@ -42,6 +45,8 @@ use App\Entity\Parts\StorageLocation;
|
||||||
use App\Entity\Parts\Supplier;
|
use App\Entity\Parts\Supplier;
|
||||||
use App\Entity\ProjectSystem\Project;
|
use App\Entity\ProjectSystem\Project;
|
||||||
use App\Entity\UserSystem\User;
|
use App\Entity\UserSystem\User;
|
||||||
|
use App\Entity\BulkInfoProviderImportJob;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
use App\Services\Trees\NodesListBuilder;
|
use App\Services\Trees\NodesListBuilder;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
@ -101,6 +106,14 @@ class PartFilter implements FilterInterface
|
||||||
public readonly TextConstraint $bomName;
|
public readonly TextConstraint $bomName;
|
||||||
public readonly TextConstraint $bomComment;
|
public readonly TextConstraint $bomComment;
|
||||||
|
|
||||||
|
/*************************************************
|
||||||
|
* Bulk Import Job tab
|
||||||
|
*************************************************/
|
||||||
|
|
||||||
|
public readonly BulkImportJobExistsConstraint $inBulkImportJob;
|
||||||
|
public readonly BulkImportJobStatusConstraint $bulkImportJobStatus;
|
||||||
|
public readonly BulkImportPartStatusConstraint $bulkImportPartStatus;
|
||||||
|
|
||||||
public function __construct(NodesListBuilder $nodesListBuilder)
|
public function __construct(NodesListBuilder $nodesListBuilder)
|
||||||
{
|
{
|
||||||
$this->name = new TextConstraint('part.name');
|
$this->name = new TextConstraint('part.name');
|
||||||
|
|
@ -162,6 +175,11 @@ class PartFilter implements FilterInterface
|
||||||
$this->bomName = new TextConstraint('_projectBomEntries.name');
|
$this->bomName = new TextConstraint('_projectBomEntries.name');
|
||||||
$this->bomComment = new TextConstraint('_projectBomEntries.comment');
|
$this->bomComment = new TextConstraint('_projectBomEntries.comment');
|
||||||
|
|
||||||
|
// Bulk Import Job filters
|
||||||
|
$this->inBulkImportJob = new BulkImportJobExistsConstraint();
|
||||||
|
$this->bulkImportJobStatus = new BulkImportJobStatusConstraint();
|
||||||
|
$this->bulkImportPartStatus = new BulkImportPartStatusConstraint();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function apply(QueryBuilder $queryBuilder): void
|
public function apply(QueryBuilder $queryBuilder): void
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ use App\Entity\Parts\ManufacturingStatus;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\PartLot;
|
use App\Entity\Parts\PartLot;
|
||||||
use App\Entity\ProjectSystem\Project;
|
use App\Entity\ProjectSystem\Project;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
use App\Services\EntityURLGenerator;
|
use App\Services\EntityURLGenerator;
|
||||||
use App\Services\Formatters\AmountFormatter;
|
use App\Services\Formatters\AmountFormatter;
|
||||||
use App\Settings\BehaviorSettings\TableSettings;
|
use App\Settings\BehaviorSettings\TableSettings;
|
||||||
|
|
@ -152,8 +153,10 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
])
|
])
|
||||||
->add('minamount', TextColumn::class, [
|
->add('minamount', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.minamount'),
|
'label' => $this->translator->trans('part.table.minamount'),
|
||||||
'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format($value,
|
'render' => fn($value, Part $context): string => htmlspecialchars($this->amountFormatter->format(
|
||||||
$context->getPartUnit())),
|
$value,
|
||||||
|
$context->getPartUnit()
|
||||||
|
)),
|
||||||
])
|
])
|
||||||
->add('partUnit', TextColumn::class, [
|
->add('partUnit', TextColumn::class, [
|
||||||
'label' => $this->translator->trans('part.table.partUnit'),
|
'label' => $this->translator->trans('part.table.partUnit'),
|
||||||
|
|
@ -423,6 +426,13 @@ final class PartsDataTable implements DataTableTypeInterface
|
||||||
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
|
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
|
||||||
//$builder->addGroupBy('_projectBomEntries');
|
//$builder->addGroupBy('_projectBomEntries');
|
||||||
}
|
}
|
||||||
|
if (str_contains($dql, '_jobPart')) {
|
||||||
|
$builder->leftJoin('part.bulkImportJobParts', '_jobPart');
|
||||||
|
$builder->leftJoin('_jobPart.job', '_bulkImportJob');
|
||||||
|
//Do not group by many-to-* relations, as it would restrict the COUNT having clauses to be maximum 1
|
||||||
|
//$builder->addGroupBy('_jobPart');
|
||||||
|
//$builder->addGroupBy('_bulkImportJob');
|
||||||
|
}
|
||||||
|
|
||||||
return $builder;
|
return $builder;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,10 @@ declare(strict_types=1);
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Entity\Base\AbstractDBElement;
|
use App\Entity\Base\AbstractDBElement;
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\UserSystem\User;
|
use App\Entity\UserSystem\User;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
|
@ -43,9 +46,6 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
#[ORM\Column(type: Types::TEXT)]
|
#[ORM\Column(type: Types::TEXT)]
|
||||||
private string $name = '';
|
private string $name = '';
|
||||||
|
|
||||||
#[ORM\Column(type: Types::JSON)]
|
|
||||||
private array $partIds = [];
|
|
||||||
|
|
||||||
#[ORM\Column(type: Types::JSON)]
|
#[ORM\Column(type: Types::JSON)]
|
||||||
private array $fieldMappings = [];
|
private array $fieldMappings = [];
|
||||||
|
|
||||||
|
|
@ -68,12 +68,14 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
private ?User $createdBy = null;
|
private ?User $createdBy = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::JSON)]
|
/** @var Collection<int, BulkInfoProviderImportJobPart> */
|
||||||
private array $progress = [];
|
#[ORM\OneToMany(targetEntity: BulkInfoProviderImportJobPart::class, mappedBy: 'job', cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||||
|
private Collection $jobParts;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->createdAt = new \DateTimeImmutable();
|
$this->createdAt = new \DateTimeImmutable();
|
||||||
|
$this->jobParts = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName(): string
|
public function getName(): string
|
||||||
|
|
@ -102,14 +104,50 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
return $this;
|
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
|
public function getPartIds(): array
|
||||||
{
|
{
|
||||||
return $this->partIds;
|
return $this->jobParts->map(fn($jobPart) => $jobPart->getPart()->getId())->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setPartIds(array $partIds): self
|
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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,12 +224,31 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
|
|
||||||
public function getProgress(): array
|
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
|
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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,7 +311,7 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
|
|
||||||
public function getPartCount(): int
|
public function getPartCount(): int
|
||||||
{
|
{
|
||||||
return count($this->partIds);
|
return $this->jobParts->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getResultCount(): int
|
public function getResultCount(): int
|
||||||
|
|
@ -268,48 +325,61 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
|
|
||||||
public function markPartAsCompleted(int $partId): self
|
public function markPartAsCompleted(int $partId): self
|
||||||
{
|
{
|
||||||
$this->progress[$partId] = [
|
$jobPart = $this->findJobPartByPartId($partId);
|
||||||
'status' => 'completed',
|
if ($jobPart) {
|
||||||
'completed_at' => (new \DateTimeImmutable())->format('c')
|
$jobPart->markAsCompleted();
|
||||||
];
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function markPartAsSkipped(int $partId, string $reason = ''): self
|
public function markPartAsSkipped(int $partId, string $reason = ''): self
|
||||||
{
|
{
|
||||||
$this->progress[$partId] = [
|
$jobPart = $this->findJobPartByPartId($partId);
|
||||||
'status' => 'skipped',
|
if ($jobPart) {
|
||||||
'reason' => $reason,
|
$jobPart->markAsSkipped($reason);
|
||||||
'completed_at' => (new \DateTimeImmutable())->format('c')
|
}
|
||||||
];
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function markPartAsPending(int $partId): self
|
public function markPartAsPending(int $partId): self
|
||||||
{
|
{
|
||||||
// Remove from progress array to mark as pending
|
$jobPart = $this->findJobPartByPartId($partId);
|
||||||
unset($this->progress[$partId]);
|
if ($jobPart) {
|
||||||
|
$jobPart->markAsPending();
|
||||||
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isPartCompleted(int $partId): bool
|
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
|
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
|
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
|
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
|
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\Attachment;
|
||||||
use App\Entity\Attachments\AttachmentType;
|
use App\Entity\Attachments\AttachmentType;
|
||||||
use App\Entity\BulkInfoProviderImportJob;
|
use App\Entity\BulkInfoProviderImportJob;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
use App\Entity\LabelSystem\LabelProfile;
|
use App\Entity\LabelSystem\LabelProfile;
|
||||||
use App\Entity\Parameters\AbstractParameter;
|
use App\Entity\Parameters\AbstractParameter;
|
||||||
use App\Entity\Parts\Category;
|
use App\Entity\Parts\Category;
|
||||||
|
|
@ -69,6 +70,7 @@ enum LogTargetType: int
|
||||||
|
|
||||||
case PART_ASSOCIATION = 20;
|
case PART_ASSOCIATION = 20;
|
||||||
case BULK_INFO_PROVIDER_IMPORT_JOB = 21;
|
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.
|
* 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::LABEL_PROFILE => LabelProfile::class,
|
||||||
self::PART_ASSOCIATION => PartAssociation::class,
|
self::PART_ASSOCIATION => PartAssociation::class,
|
||||||
self::BULK_INFO_PROVIDER_IMPORT_JOB => BulkInfoProviderImportJob::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\OrderTrait;
|
||||||
use App\Entity\Parts\PartTraits\ProjectTrait;
|
use App\Entity\Parts\PartTraits\ProjectTrait;
|
||||||
use App\EntityListeners\TreeCacheInvalidationListener;
|
use App\EntityListeners\TreeCacheInvalidationListener;
|
||||||
|
use App\Entity\BulkInfoProviderImportJobPart;
|
||||||
use App\Repository\PartRepository;
|
use App\Repository\PartRepository;
|
||||||
use App\Validator\Constraints\UniqueObjectCollection;
|
use App\Validator\Constraints\UniqueObjectCollection;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
|
@ -83,8 +84,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
#[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')]
|
#[ORM\Index(columns: ['ipn'], name: 'parts_idx_ipn')]
|
||||||
#[ApiResource(
|
#[ApiResource(
|
||||||
operations: [
|
operations: [
|
||||||
new Get(normalizationContext: ['groups' => ['part:read', 'provider_reference:read', 'api:basic:read', 'part_lot:read',
|
new Get(normalizationContext: [
|
||||||
'orderdetail:read', 'pricedetail:read', 'parameter:read', 'attachment:read', 'eda_info:read'],
|
'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',
|
'openapi_definition_name' => 'Read',
|
||||||
], security: 'is_granted("read", object)'),
|
], security: 'is_granted("read", object)'),
|
||||||
new GetCollection(security: 'is_granted("@parts.read")'),
|
new GetCollection(security: 'is_granted("@parts.read")'),
|
||||||
|
|
@ -160,6 +171,12 @@ class Part extends AttachmentContainingDBElement
|
||||||
#[Groups(['part:read'])]
|
#[Groups(['part:read'])]
|
||||||
protected ?\DateTimeImmutable $lastModified = null;
|
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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
|
|
@ -172,6 +189,7 @@ class Part extends AttachmentContainingDBElement
|
||||||
|
|
||||||
$this->associated_parts_as_owner = new ArrayCollection();
|
$this->associated_parts_as_owner = new ArrayCollection();
|
||||||
$this->associated_parts_as_other = new ArrayCollection();
|
$this->associated_parts_as_other = new ArrayCollection();
|
||||||
|
$this->bulkImportJobParts = new ArrayCollection();
|
||||||
|
|
||||||
//By default, the part has no provider
|
//By default, the part has no provider
|
||||||
$this->providerReference = InfoProviderReference::noProvider();
|
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
<?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\Form\Filters\Constraints;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\Part\BulkImportJobExistsConstraint;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class BulkImportJobExistsConstraintType extends AbstractType
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'compound' => true,
|
||||||
|
'data_class' => BulkImportJobExistsConstraint::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$choices = [
|
||||||
|
'' => '',
|
||||||
|
'part.filter.in_bulk_import_job.yes' => true,
|
||||||
|
'part.filter.in_bulk_import_job.no' => false,
|
||||||
|
];
|
||||||
|
|
||||||
|
$builder->add('value', ChoiceType::class, [
|
||||||
|
'label' => 'part.filter.in_bulk_import_job',
|
||||||
|
'choices' => $choices,
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||||
|
{
|
||||||
|
parent::buildView($view, $form, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?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\Form\Filters\Constraints;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\Part\BulkImportJobStatusConstraint;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class BulkImportJobStatusConstraintType extends AbstractType
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'compound' => true,
|
||||||
|
'data_class' => BulkImportJobStatusConstraint::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$statusChoices = [
|
||||||
|
'bulk_import.status.pending' => 'pending',
|
||||||
|
'bulk_import.status.in_progress' => 'in_progress',
|
||||||
|
'bulk_import.status.completed' => 'completed',
|
||||||
|
'bulk_import.status.stopped' => 'stopped',
|
||||||
|
'bulk_import.status.failed' => 'failed',
|
||||||
|
];
|
||||||
|
|
||||||
|
$operatorChoices = [
|
||||||
|
'filter.choice_constraint.operator.ANY' => 'ANY',
|
||||||
|
'filter.choice_constraint.operator.NONE' => 'NONE',
|
||||||
|
];
|
||||||
|
|
||||||
|
$builder->add('operator', ChoiceType::class, [
|
||||||
|
'label' => 'filter.operator',
|
||||||
|
'choices' => $operatorChoices,
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->add('values', ChoiceType::class, [
|
||||||
|
'label' => 'part.filter.bulk_import_job_status',
|
||||||
|
'choices' => $statusChoices,
|
||||||
|
'required' => false,
|
||||||
|
'multiple' => true,
|
||||||
|
'attr' => [
|
||||||
|
'data-controller' => 'elements--select-multiple',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||||
|
{
|
||||||
|
parent::buildView($view, $form, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?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\Form\Filters\Constraints;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\Part\BulkImportPartStatusConstraint;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\Form\FormView;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class BulkImportPartStatusConstraintType extends AbstractType
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'compound' => true,
|
||||||
|
'data_class' => BulkImportPartStatusConstraint::class,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$statusChoices = [
|
||||||
|
'bulk_import.part_status.pending' => 'pending',
|
||||||
|
'bulk_import.part_status.completed' => 'completed',
|
||||||
|
'bulk_import.part_status.skipped' => 'skipped',
|
||||||
|
'bulk_import.part_status.failed' => 'failed',
|
||||||
|
];
|
||||||
|
|
||||||
|
$operatorChoices = [
|
||||||
|
'filter.choice_constraint.operator.ANY' => 'ANY',
|
||||||
|
'filter.choice_constraint.operator.NONE' => 'NONE',
|
||||||
|
];
|
||||||
|
|
||||||
|
$builder->add('operator', ChoiceType::class, [
|
||||||
|
'label' => 'filter.operator',
|
||||||
|
'choices' => $operatorChoices,
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->add('values', ChoiceType::class, [
|
||||||
|
'label' => 'part.filter.bulk_import_part_status',
|
||||||
|
'choices' => $statusChoices,
|
||||||
|
'required' => false,
|
||||||
|
'multiple' => true,
|
||||||
|
'attr' => [
|
||||||
|
'data-controller' => 'elements--select-multiple',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||||
|
{
|
||||||
|
parent::buildView($view, $form, $options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -129,6 +129,7 @@ class LogFilterType extends AbstractType
|
||||||
LogTargetType::LABEL_PROFILE => 'label_profile.label',
|
LogTargetType::LABEL_PROFILE => 'label_profile.label',
|
||||||
LogTargetType::PART_ASSOCIATION => 'part_association.label',
|
LogTargetType::PART_ASSOCIATION => 'part_association.label',
|
||||||
LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label',
|
LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label',
|
||||||
|
LogTargetType::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.label',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,11 @@ use App\Entity\Parts\MeasurementUnit;
|
||||||
use App\Entity\Parts\StorageLocation;
|
use App\Entity\Parts\StorageLocation;
|
||||||
use App\Entity\Parts\Supplier;
|
use App\Entity\Parts\Supplier;
|
||||||
use App\Entity\ProjectSystem\Project;
|
use App\Entity\ProjectSystem\Project;
|
||||||
|
use App\Entity\BulkInfoProviderImportJob;
|
||||||
use App\Form\Filters\Constraints\BooleanConstraintType;
|
use App\Form\Filters\Constraints\BooleanConstraintType;
|
||||||
|
use App\Form\Filters\Constraints\BulkImportJobExistsConstraintType;
|
||||||
|
use App\Form\Filters\Constraints\BulkImportJobStatusConstraintType;
|
||||||
|
use App\Form\Filters\Constraints\BulkImportPartStatusConstraintType;
|
||||||
use App\Form\Filters\Constraints\ChoiceConstraintType;
|
use App\Form\Filters\Constraints\ChoiceConstraintType;
|
||||||
use App\Form\Filters\Constraints\DateTimeConstraintType;
|
use App\Form\Filters\Constraints\DateTimeConstraintType;
|
||||||
use App\Form\Filters\Constraints\NumberConstraintType;
|
use App\Form\Filters\Constraints\NumberConstraintType;
|
||||||
|
|
@ -298,6 +302,23 @@ class PartFilterType extends AbstractType
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**************************************************************************
|
||||||
|
* Bulk Import Job tab
|
||||||
|
**************************************************************************/
|
||||||
|
if ($this->security->isGranted('@info_providers.create_parts')) {
|
||||||
|
$builder
|
||||||
|
->add('inBulkImportJob', BulkImportJobExistsConstraintType::class, [
|
||||||
|
'label' => 'part.filter.in_bulk_import_job',
|
||||||
|
])
|
||||||
|
->add('bulkImportJobStatus', BulkImportJobStatusConstraintType::class, [
|
||||||
|
'label' => 'part.filter.bulk_import_job_status',
|
||||||
|
])
|
||||||
|
->add('bulkImportPartStatus', BulkImportPartStatusConstraintType::class, [
|
||||||
|
'label' => 'part.filter.bulk_import_part_status',
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
$builder->add('submit', SubmitType::class, [
|
$builder->add('submit', SubmitType::class, [
|
||||||
'label' => 'filter.submit',
|
'label' => 'filter.submit',
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,11 @@
|
||||||
<button class="nav-link" id="filter-projects-tab" data-bs-toggle="tab" data-bs-target="#filter-projects"><i class="fas fa-archive fa-fw"></i> {% trans %}project.labelp{% endtrans %}</button>
|
<button class="nav-link" id="filter-projects-tab" data-bs-toggle="tab" data-bs-target="#filter-projects"><i class="fas fa-archive fa-fw"></i> {% trans %}project.labelp{% endtrans %}</button>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if filterForm.inBulkImportJob is defined %}
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="filter-bulk-import-tab" data-bs-toggle="tab" data-bs-target="#filter-bulk-import"><i class="fas fa-download fa-fw"></i> {% trans %}part.edit.tab.bulk_import{% endtrans %}</button>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }}
|
{{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }}
|
||||||
|
|
@ -126,6 +131,13 @@
|
||||||
{{ form_row(filterForm.bomComment) }}
|
{{ form_row(filterForm.bomComment) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if filterForm.inBulkImportJob is defined %}
|
||||||
|
<div class="tab-pane pt-3" id="filter-bulk-import" role="tabpanel" aria-labelledby="filter-bulk-import-tab" tabindex="0">
|
||||||
|
{{ form_row(filterForm.inBulkImportJob) }}
|
||||||
|
{{ form_row(filterForm.bulkImportJobStatus) }}
|
||||||
|
{{ form_row(filterForm.bulkImportPartStatus) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
// Create a test job with search results that include source_field and source_keyword
|
// Create a test job with search results that include source_field and source_keyword
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
$job->setPartIds([$part->getId()]);
|
$job->addPart($part);
|
||||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||||
$job->setSearchResults([
|
$job->setSearchResults([
|
||||||
[
|
[
|
||||||
|
|
@ -230,10 +230,18 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Admin user not found in fixtures');
|
$this->markTestSkipped('Admin user not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get a test part
|
||||||
|
$partRepository = $entityManager->getRepository(Part::class);
|
||||||
|
$part = $partRepository->find(1);
|
||||||
|
|
||||||
|
if (!$part) {
|
||||||
|
$this->markTestSkipped('Test part with ID 1 not found in fixtures');
|
||||||
|
}
|
||||||
|
|
||||||
// Create a completed job
|
// Create a completed job
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
$job->setPartIds([1]);
|
$job->addPart($part);
|
||||||
$job->setStatus(BulkImportJobStatus::COMPLETED);
|
$job->setStatus(BulkImportJobStatus::COMPLETED);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -272,10 +280,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Admin user not found in fixtures');
|
$this->markTestSkipped('Admin user not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get test parts
|
||||||
|
$parts = $this->getTestParts($entityManager, [1]);
|
||||||
|
|
||||||
// Create an active job
|
// Create an active job
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
$job->setPartIds([1]);
|
foreach ($parts as $part) {
|
||||||
|
$job->addPart($part);
|
||||||
|
}
|
||||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -306,10 +319,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Admin user not found in fixtures');
|
$this->markTestSkipped('Admin user not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get test parts
|
||||||
|
$parts = $this->getTestParts($entityManager, [1]);
|
||||||
|
|
||||||
// Create an active job
|
// Create an active job
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
$job->setPartIds([1]);
|
foreach ($parts as $part) {
|
||||||
|
$job->addPart($part);
|
||||||
|
}
|
||||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -352,9 +370,14 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Admin user not found in fixtures');
|
$this->markTestSkipped('Admin user not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get test parts
|
||||||
|
$parts = $this->getTestParts($entityManager, [1, 2]);
|
||||||
|
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
$job->setPartIds([1, 2]);
|
foreach ($parts as $part) {
|
||||||
|
$job->addPart($part);
|
||||||
|
}
|
||||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -387,9 +410,14 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Admin user not found in fixtures');
|
$this->markTestSkipped('Admin user not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get test parts
|
||||||
|
$parts = $this->getTestParts($entityManager, [1, 2]);
|
||||||
|
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
$job->setPartIds([1, 2]);
|
foreach ($parts as $part) {
|
||||||
|
$job->addPart($part);
|
||||||
|
}
|
||||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -423,9 +451,14 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Admin user not found in fixtures');
|
$this->markTestSkipped('Admin user not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get test parts
|
||||||
|
$parts = $this->getTestParts($entityManager, [1]);
|
||||||
|
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($user);
|
$job->setCreatedBy($user);
|
||||||
$job->setPartIds([1]);
|
foreach ($parts as $part) {
|
||||||
|
$job->addPart($part);
|
||||||
|
}
|
||||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -467,10 +500,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Required test users not found in fixtures');
|
$this->markTestSkipped('Required test users not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get test parts
|
||||||
|
$parts = $this->getTestParts($entityManager, [1]);
|
||||||
|
|
||||||
// Create job as admin
|
// Create job as admin
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($admin);
|
$job->setCreatedBy($admin);
|
||||||
$job->setPartIds([1]);
|
foreach ($parts as $part) {
|
||||||
|
$job->addPart($part);
|
||||||
|
}
|
||||||
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
$job->setStatus(BulkImportJobStatus::IN_PROGRESS);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -502,10 +540,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
$this->markTestSkipped('Required test users not found in fixtures');
|
$this->markTestSkipped('Required test users not found in fixtures');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get test parts
|
||||||
|
$parts = $this->getTestParts($entityManager, [1]);
|
||||||
|
|
||||||
// Create job as readonly user
|
// Create job as readonly user
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setCreatedBy($readonly);
|
$job->setCreatedBy($readonly);
|
||||||
$job->setPartIds([1]);
|
foreach ($parts as $part) {
|
||||||
|
$job->addPart($part);
|
||||||
|
}
|
||||||
$job->setStatus(BulkImportJobStatus::COMPLETED);
|
$job->setStatus(BulkImportJobStatus::COMPLETED);
|
||||||
$job->setSearchResults([]);
|
$job->setSearchResults([]);
|
||||||
|
|
||||||
|
|
@ -534,4 +577,20 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
|
|
||||||
$client->loginUser($user);
|
$client->loginUser($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getTestParts($entityManager, array $ids): array
|
||||||
|
{
|
||||||
|
$partRepository = $entityManager->getRepository(Part::class);
|
||||||
|
$parts = [];
|
||||||
|
|
||||||
|
foreach ($ids as $id) {
|
||||||
|
$part = $partRepository->find($id);
|
||||||
|
if (!$part) {
|
||||||
|
$this->markTestSkipped("Test part with ID {$id} not found in fixtures");
|
||||||
|
}
|
||||||
|
$parts[] = $part;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $parts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +41,14 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
$this->job->setCreatedBy($this->user);
|
$this->job->setCreatedBy($this->user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createMockPart(int $id): \App\Entity\Parts\Part
|
||||||
|
{
|
||||||
|
$part = $this->createMock(\App\Entity\Parts\Part::class);
|
||||||
|
$part->method('getId')->willReturn($id);
|
||||||
|
$part->method('getName')->willReturn("Test Part {$id}");
|
||||||
|
return $part;
|
||||||
|
}
|
||||||
|
|
||||||
public function testConstruct(): void
|
public function testConstruct(): void
|
||||||
{
|
{
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
|
|
@ -60,9 +68,12 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
$this->job->setName('Test Job');
|
$this->job->setName('Test Job');
|
||||||
$this->assertEquals('Test Job', $this->job->getName());
|
$this->assertEquals('Test Job', $this->job->getName());
|
||||||
|
|
||||||
$partIds = [1, 2, 3];
|
// Test with actual parts - this is what actually works
|
||||||
$this->job->setPartIds($partIds);
|
$parts = [$this->createMockPart(1), $this->createMockPart(2), $this->createMockPart(3)];
|
||||||
$this->assertEquals($partIds, $this->job->getPartIds());
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
|
$this->assertEquals([1, 2, 3], $this->job->getPartIds());
|
||||||
|
|
||||||
$fieldMappings = ['field1' => 'provider1', 'field2' => 'provider2'];
|
$fieldMappings = ['field1' => 'provider1', 'field2' => 'provider2'];
|
||||||
$this->job->setFieldMappings($fieldMappings);
|
$this->job->setFieldMappings($fieldMappings);
|
||||||
|
|
@ -133,7 +144,17 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
{
|
{
|
||||||
$this->assertEquals(0, $this->job->getPartCount());
|
$this->assertEquals(0, $this->job->getPartCount());
|
||||||
|
|
||||||
$this->job->setPartIds([1, 2, 3, 4, 5]);
|
// Test with actual parts - setPartIds doesn't actually add parts
|
||||||
|
$parts = [
|
||||||
|
$this->createMockPart(1),
|
||||||
|
$this->createMockPart(2),
|
||||||
|
$this->createMockPart(3),
|
||||||
|
$this->createMockPart(4),
|
||||||
|
$this->createMockPart(5)
|
||||||
|
];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
$this->assertEquals(5, $this->job->getPartCount());
|
$this->assertEquals(5, $this->job->getPartCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,7 +173,16 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
|
|
||||||
public function testPartProgressTracking(): void
|
public function testPartProgressTracking(): void
|
||||||
{
|
{
|
||||||
$this->job->setPartIds([1, 2, 3, 4]);
|
// Test with actual parts - setPartIds doesn't actually add parts
|
||||||
|
$parts = [
|
||||||
|
$this->createMockPart(1),
|
||||||
|
$this->createMockPart(2),
|
||||||
|
$this->createMockPart(3),
|
||||||
|
$this->createMockPart(4)
|
||||||
|
];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
|
|
||||||
$this->assertFalse($this->job->isPartCompleted(1));
|
$this->assertFalse($this->job->isPartCompleted(1));
|
||||||
$this->assertFalse($this->job->isPartSkipped(1));
|
$this->assertFalse($this->job->isPartSkipped(1));
|
||||||
|
|
@ -172,7 +202,17 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
|
|
||||||
public function testProgressCounts(): void
|
public function testProgressCounts(): void
|
||||||
{
|
{
|
||||||
$this->job->setPartIds([1, 2, 3, 4, 5]);
|
// Test with actual parts - setPartIds doesn't actually add parts
|
||||||
|
$parts = [
|
||||||
|
$this->createMockPart(1),
|
||||||
|
$this->createMockPart(2),
|
||||||
|
$this->createMockPart(3),
|
||||||
|
$this->createMockPart(4),
|
||||||
|
$this->createMockPart(5)
|
||||||
|
];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
|
|
||||||
$this->assertEquals(0, $this->job->getCompletedPartsCount());
|
$this->assertEquals(0, $this->job->getCompletedPartsCount());
|
||||||
$this->assertEquals(0, $this->job->getSkippedPartsCount());
|
$this->assertEquals(0, $this->job->getSkippedPartsCount());
|
||||||
|
|
@ -190,7 +230,18 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
$emptyJob = new BulkInfoProviderImportJob();
|
$emptyJob = new BulkInfoProviderImportJob();
|
||||||
$this->assertEquals(100.0, $emptyJob->getProgressPercentage());
|
$this->assertEquals(100.0, $emptyJob->getProgressPercentage());
|
||||||
|
|
||||||
$this->job->setPartIds([1, 2, 3, 4, 5]);
|
// Test with actual parts - setPartIds doesn't actually add parts
|
||||||
|
$parts = [
|
||||||
|
$this->createMockPart(1),
|
||||||
|
$this->createMockPart(2),
|
||||||
|
$this->createMockPart(3),
|
||||||
|
$this->createMockPart(4),
|
||||||
|
$this->createMockPart(5)
|
||||||
|
];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
|
|
||||||
$this->assertEquals(0.0, $this->job->getProgressPercentage());
|
$this->assertEquals(0.0, $this->job->getProgressPercentage());
|
||||||
|
|
||||||
$this->job->markPartAsCompleted(1);
|
$this->job->markPartAsCompleted(1);
|
||||||
|
|
@ -210,7 +261,16 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
$emptyJob = new BulkInfoProviderImportJob();
|
$emptyJob = new BulkInfoProviderImportJob();
|
||||||
$this->assertTrue($emptyJob->isAllPartsCompleted());
|
$this->assertTrue($emptyJob->isAllPartsCompleted());
|
||||||
|
|
||||||
$this->job->setPartIds([1, 2, 3]);
|
// Test with actual parts - setPartIds doesn't actually add parts
|
||||||
|
$parts = [
|
||||||
|
$this->createMockPart(1),
|
||||||
|
$this->createMockPart(2),
|
||||||
|
$this->createMockPart(3)
|
||||||
|
];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
|
|
||||||
$this->assertFalse($this->job->isAllPartsCompleted());
|
$this->assertFalse($this->job->isAllPartsCompleted());
|
||||||
|
|
||||||
$this->job->markPartAsCompleted(1);
|
$this->job->markPartAsCompleted(1);
|
||||||
|
|
@ -223,7 +283,15 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
|
|
||||||
public function testDisplayNameMethods(): void
|
public function testDisplayNameMethods(): void
|
||||||
{
|
{
|
||||||
$this->job->setPartIds([1, 2, 3]);
|
// Test with actual parts - setPartIds doesn't actually add parts
|
||||||
|
$parts = [
|
||||||
|
$this->createMockPart(1),
|
||||||
|
$this->createMockPart(2),
|
||||||
|
$this->createMockPart(3)
|
||||||
|
];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
|
|
||||||
$this->assertEquals('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey());
|
$this->assertEquals('info_providers.bulk_import.job_name_template', $this->job->getDisplayNameKey());
|
||||||
$this->assertEquals(['%count%' => 3], $this->job->getDisplayNameParams());
|
$this->assertEquals(['%count%' => 3], $this->job->getDisplayNameParams());
|
||||||
|
|
@ -237,19 +305,39 @@ class BulkInfoProviderImportJobTest extends TestCase
|
||||||
|
|
||||||
public function testProgressDataStructure(): void
|
public function testProgressDataStructure(): void
|
||||||
{
|
{
|
||||||
|
$parts = [
|
||||||
|
$this->createMockPart(1),
|
||||||
|
$this->createMockPart(2),
|
||||||
|
$this->createMockPart(3)
|
||||||
|
];
|
||||||
|
foreach ($parts as $part) {
|
||||||
|
$this->job->addPart($part);
|
||||||
|
}
|
||||||
|
|
||||||
$this->job->markPartAsCompleted(1);
|
$this->job->markPartAsCompleted(1);
|
||||||
$this->job->markPartAsSkipped(2, 'Test reason');
|
$this->job->markPartAsSkipped(2, 'Test reason');
|
||||||
|
|
||||||
$progress = $this->job->getProgress();
|
$progress = $this->job->getProgress();
|
||||||
|
|
||||||
$this->assertArrayHasKey(1, $progress);
|
// The progress array should have keys for all part IDs, even if not completed/skipped
|
||||||
|
$this->assertArrayHasKey(1, $progress, 'Progress should contain key for part 1');
|
||||||
|
$this->assertArrayHasKey(2, $progress, 'Progress should contain key for part 2');
|
||||||
|
$this->assertArrayHasKey(3, $progress, 'Progress should contain key for part 3');
|
||||||
|
|
||||||
|
// Part 1: completed
|
||||||
$this->assertEquals('completed', $progress[1]['status']);
|
$this->assertEquals('completed', $progress[1]['status']);
|
||||||
$this->assertArrayHasKey('completed_at', $progress[1]);
|
$this->assertArrayHasKey('completed_at', $progress[1]);
|
||||||
|
$this->assertArrayNotHasKey('reason', $progress[1]);
|
||||||
|
|
||||||
$this->assertArrayHasKey(2, $progress);
|
// Part 2: skipped
|
||||||
$this->assertEquals('skipped', $progress[2]['status']);
|
$this->assertEquals('skipped', $progress[2]['status']);
|
||||||
$this->assertEquals('Test reason', $progress[2]['reason']);
|
$this->assertEquals('Test reason', $progress[2]['reason']);
|
||||||
$this->assertArrayHasKey('completed_at', $progress[2]);
|
$this->assertArrayHasKey('completed_at', $progress[2]);
|
||||||
|
|
||||||
|
// Part 3: should be present but not completed/skipped
|
||||||
|
$this->assertEquals('pending', $progress[3]['status']);
|
||||||
|
$this->assertArrayNotHasKey('completed_at', $progress[3]);
|
||||||
|
$this->assertArrayNotHasKey('reason', $progress[3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testCompletedAtTimestamp(): void
|
public function testCompletedAtTimestamp(): void
|
||||||
|
|
|
||||||
|
|
@ -13801,5 +13801,101 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
||||||
<target>Are you sure you want to stop this job?</target>
|
<target>Are you sure you want to stop this job?</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id="bulk109" name="part.filter.in_bulk_import_job">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part.filter.in_bulk_import_job</source>
|
||||||
|
<target>In Bulk Import Job</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk110" name="part.filter.in_bulk_import_job.yes">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part.filter.in_bulk_import_job.yes</source>
|
||||||
|
<target>Yes</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk111" name="part.filter.in_bulk_import_job.no">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part.filter.in_bulk_import_job.no</source>
|
||||||
|
<target>No</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk112" name="part.filter.bulk_import_job_status">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part.filter.bulk_import_job_status</source>
|
||||||
|
<target>Bulk Import Job Status</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk113" name="part.filter.bulk_import_part_status">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part.filter.bulk_import_part_status</source>
|
||||||
|
<target>Bulk Import Part Status</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk114" name="part.edit.tab.bulk_import">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>part.edit.tab.bulk_import</source>
|
||||||
|
<target>Bulk Import Job</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk115" name="bulk_import.status.pending">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.status.pending</source>
|
||||||
|
<target>Pending</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk116" name="bulk_import.status.in_progress">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.status.in_progress</source>
|
||||||
|
<target>In Progress</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk117" name="bulk_import.status.completed">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.status.completed</source>
|
||||||
|
<target>Completed</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk118" name="bulk_import.status.stopped">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.status.stopped</source>
|
||||||
|
<target>Stopped</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk119" name="bulk_import.status.failed">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.status.failed</source>
|
||||||
|
<target>Failed</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk120" name="bulk_import.part_status.pending">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.part_status.pending</source>
|
||||||
|
<target>Pending</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk121" name="bulk_import.part_status.completed">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.part_status.completed</source>
|
||||||
|
<target>Completed</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk122" name="bulk_import.part_status.skipped">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.part_status.skipped</source>
|
||||||
|
<target>Skipped</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk123" name="bulk_import.part_status.failed">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>bulk_import.part_status.failed</source>
|
||||||
|
<target>Failed</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="bulk124" name="filter.operator">
|
||||||
|
<segment state="translated">
|
||||||
|
<source>filter.operator</source>
|
||||||
|
<target>Operator</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue