mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Added a very basic import dialog for Parts
This commit is contained in:
parent
8f033910ce
commit
7a9b7c87a4
7 changed files with 149 additions and 22 deletions
|
@ -343,7 +343,7 @@ abstract class BaseAdminController extends AbstractController
|
|||
'preserve_children' => $data['preserve_children'],
|
||||
'format' => $data['format'],
|
||||
'class' => $this->entity_class,
|
||||
'csv_separator' => $data['csv_separator'],
|
||||
'csv_delimiter' => $data['csv_delimiter'],
|
||||
];
|
||||
|
||||
$this->commentHelper->setMessage('Import '.$file->getClientOriginalName());
|
||||
|
|
|
@ -20,24 +20,76 @@
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Form\AdminPages\ImportType;
|
||||
use App\Services\ImportExportSystem\EntityExporter;
|
||||
use App\Services\ImportExportSystem\EntityImporter;
|
||||
use App\Services\LogSystem\EventCommentHelper;
|
||||
use App\Services\Parts\PartsTableActionHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
|
||||
class PartImportExportController extends AbstractController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
private PartsTableActionHandler $partsTableActionHandler;
|
||||
private EntityImporter $entityImporter;
|
||||
private EventCommentHelper $commentHelper;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, PartsTableActionHandler $partsTableActionHandler)
|
||||
public function __construct(PartsTableActionHandler $partsTableActionHandler,
|
||||
EntityImporter $entityImporter, EventCommentHelper $commentHelper)
|
||||
{
|
||||
$this->entityManager = $entityManager;
|
||||
$this->partsTableActionHandler = $partsTableActionHandler;
|
||||
$this->entityImporter = $entityImporter;
|
||||
$this->commentHelper = $commentHelper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/parts/import", name="parts_import")
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function importParts(Request $request): Response
|
||||
{
|
||||
$import_form = $this->createForm(ImportType::class, ['entity_class' => Part::class]);
|
||||
$import_form->handleRequest($request);
|
||||
|
||||
if ($import_form->isSubmitted() && $import_form->isValid()) {
|
||||
/** @var UploadedFile $file */
|
||||
$file = $import_form['file']->getData();
|
||||
$data = $import_form->getData();
|
||||
|
||||
$options = [
|
||||
'preserve_children' => $data['preserve_children'],
|
||||
'format' => $data['format'],
|
||||
'part_category' => $data['part_category'],
|
||||
'class' => Part::class,
|
||||
'csv_delimiter' => $data['csv_delimiter'],
|
||||
];
|
||||
|
||||
$this->commentHelper->setMessage('Import '.$file->getClientOriginalName());
|
||||
|
||||
$entities = [];
|
||||
|
||||
$errors = $this->entityImporter->importFileAndPersistToDB($file, $options, $entities);
|
||||
|
||||
if ($errors) {
|
||||
$this->addFlash('error', 'parts.import.flash.error');
|
||||
} else {
|
||||
$this->addFlash('success', 'parts.import.flash.success');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->renderForm('parts/import/parts_import.html.twig', [
|
||||
'import_form' => $import_form,
|
||||
'imported_entities' => $entities ?? [],
|
||||
'import_errors' => $errors ?? [],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -23,6 +23,8 @@ declare(strict_types=1);
|
|||
namespace App\Form\AdminPages;
|
||||
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Form\Type\StructuralEntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
|
@ -63,7 +65,7 @@ class ImportType extends AbstractType
|
|||
'label' => 'export.format',
|
||||
'disabled' => $disabled,
|
||||
])
|
||||
->add('csv_separator', TextType::class, [
|
||||
->add('csv_delimiter', TextType::class, [
|
||||
'data' => ';',
|
||||
'label' => 'import.csv_separator',
|
||||
'disabled' => $disabled,
|
||||
|
@ -78,6 +80,17 @@ class ImportType extends AbstractType
|
|||
]);
|
||||
}
|
||||
|
||||
if ($entity instanceof Part) {
|
||||
$builder->add('part_category', StructuralEntityType::class, [
|
||||
'class' => Category::class,
|
||||
'required' => false,
|
||||
'label' => 'category.label',
|
||||
'disabled' => $disabled,
|
||||
'disable_not_selectable' => true,
|
||||
'allow_add' => true
|
||||
]);
|
||||
}
|
||||
|
||||
$builder->add('file', FileType::class, [
|
||||
'label' => 'import.file',
|
||||
'attr' => [
|
||||
|
|
|
@ -24,6 +24,8 @@ namespace App\Services\ImportExportSystem;
|
|||
|
||||
use App\Entity\Base\AbstractNamedDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Part;
|
||||
use Symplify\EasyCodingStandard\ValueObject\Option;
|
||||
use function count;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
@ -157,7 +159,7 @@ class EntityImporter
|
|||
$entities = $this->serializer->deserialize($data, $options['class'].'[]', $options['format'],
|
||||
[
|
||||
'groups' => $groups,
|
||||
'csv_delimiter' => $options['csv_separator'],
|
||||
'csv_delimiter' => $options['csv_delimiter'],
|
||||
]);
|
||||
|
||||
//Ensure we have an array of entity elements.
|
||||
|
@ -175,17 +177,16 @@ class EntityImporter
|
|||
if ($entity instanceof AbstractStructuralDBElement) {
|
||||
$entity->setParent($options['parent']);
|
||||
}
|
||||
if ($entity instanceof Part && $options['part_category']) {
|
||||
$entity->setCategory($options['part_category']);
|
||||
}
|
||||
}
|
||||
|
||||
//Validate the entities
|
||||
$errors = [];
|
||||
|
||||
//Iterate over each $entity write it to DB.
|
||||
foreach ($entities as $entity) {
|
||||
/** @var AbstractStructuralDBElement $entity */
|
||||
//Move every imported entity to the selected parent
|
||||
$entity->setParent($options['parent']);
|
||||
|
||||
foreach ($entities as $key => $entity) {
|
||||
//Validate entity
|
||||
$tmp = $this->validator->validate($entity);
|
||||
|
||||
|
@ -196,6 +197,9 @@ class EntityImporter
|
|||
'violations' => $tmp,
|
||||
'entity' => $entity,
|
||||
];
|
||||
|
||||
//Remove the invalid entity from the array
|
||||
unset($entities[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -205,14 +209,21 @@ class EntityImporter
|
|||
protected function configureOptions(OptionsResolver $resolver): OptionsResolver
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'csv_separator' => ';',
|
||||
'format' => 'json',
|
||||
'csv_delimiter' => ';', //The separator to use when importing csv files
|
||||
'format' => 'json', //The format of the file that should be imported
|
||||
'class' => AbstractNamedDBElement::class,
|
||||
'preserve_children' => true,
|
||||
'parent' => null, //The parent element to which the imported elements should be added
|
||||
'abort_on_validation_error' => true,
|
||||
'part_category' => null
|
||||
]);
|
||||
|
||||
$resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']);
|
||||
$resolver->setAllowedTypes('csv_delimiter', 'string');
|
||||
$resolver->setAllowedTypes('preserve_children', 'bool');
|
||||
$resolver->setAllowedTypes('class', 'string');
|
||||
$resolver->setAllowedTypes('part_category', [Category::class, 'null']);
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
|
@ -222,11 +233,12 @@ class EntityImporter
|
|||
*
|
||||
* @param File $file the file that should be used for importing
|
||||
* @param array $options options for the import process
|
||||
* @param AbstractNamedDBElement[] $entities The imported entities are returned in this array
|
||||
*
|
||||
* @return array An associative array containing an ConstraintViolationList and the entity name as key are returned,
|
||||
* if an error happened during validation. When everything was successfully, the array should be empty.
|
||||
*/
|
||||
public function importFileAndPersistToDB(File $file, array $options = []): array
|
||||
public function importFileAndPersistToDB(File $file, array $options = [], array &$entities = []): array
|
||||
{
|
||||
$options = $this->configureOptions(new OptionsResolver())->resolve($options);
|
||||
|
||||
|
@ -238,15 +250,9 @@ class EntityImporter
|
|||
return $errors;
|
||||
}
|
||||
|
||||
//Iterate over each $entity write it to DB.
|
||||
//Iterate over each $entity write it to DB (the invalid entities were already filtered out).
|
||||
foreach ($entities as $entity) {
|
||||
//Validate entity
|
||||
$tmp = $this->validator->validate($entity);
|
||||
|
||||
//When no validation error occurred, persist entity to database (cascade must be set in entity)
|
||||
if (null === $tmp) {
|
||||
$this->em->persist($entity);
|
||||
}
|
||||
$this->em->persist($entity);
|
||||
}
|
||||
|
||||
//Save changes to database, when no error happened, or we should continue on error.
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
{% extends "base.html.twig" %}
|
||||
|
||||
{% block content %}
|
||||
{% block before_card %}{% endblock %}
|
||||
|
||||
<div class="card {% block card_border %}border-primary{% endblock %}">
|
||||
{% block card_header %}
|
||||
<div class="card-header {% block card_type %}bg-primary text-white{% endblock %}">
|
||||
|
@ -14,5 +16,7 @@
|
|||
{% endblock %}
|
||||
</div>
|
||||
|
||||
{% block after_card %}{% endblock %}
|
||||
|
||||
{% block additional_content %}{% endblock %}
|
||||
{% endblock %}
|
34
templates/parts/import/parts_import.html.twig
Normal file
34
templates/parts/import/parts_import.html.twig
Normal file
|
@ -0,0 +1,34 @@
|
|||
{% extends "main_card.html.twig" %}
|
||||
|
||||
{% block title %}{% trans %}parts.import.title{% endtrans %}{% endblock %}
|
||||
|
||||
{% block card_title %}
|
||||
<i class="fa-solid fa-file-import fa-fw"></i> {% trans %}parts.import.title{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block before_card %}
|
||||
{% if import_errors %}
|
||||
<div class="alert alert-danger">
|
||||
<h4><i class="fa-solid fa-exclamation-triangle fa-fw"></i> {% trans %}parts.import.errors.title{% endtrans %}</h4>
|
||||
<ul>
|
||||
{% for name, error in import_errors %}
|
||||
<li><b>{{ name }}:</b> {{ error.violations }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block card_content %}
|
||||
{{ form(import_form) }}
|
||||
|
||||
{% if imported_entities %}
|
||||
<hr>
|
||||
<h4>{% trans %}parts.import.errors.imported_entities{% endtrans %} ({{ imported_entities | length }}):</h4>
|
||||
<ul>
|
||||
{% for entity in imported_entities %}
|
||||
{# @var \App\Entity\Parts\Part entity #}
|
||||
<li><a href="{{ entity_url(entity) }}">{{ entity.name }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
|
@ -11029,5 +11029,23 @@ Element 3</target>
|
|||
<target>Export to XML</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kCT2Emc" name="parts.import.title">
|
||||
<segment>
|
||||
<source>parts.import.title</source>
|
||||
<target>Import parts</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="6oVjTY." name="parts.import.errors.title">
|
||||
<segment>
|
||||
<source>parts.import.errors.title</source>
|
||||
<target>Import violations</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="2sWmr2k" name="parts.import.flash.error">
|
||||
<segment>
|
||||
<source>parts.import.flash.error</source>
|
||||
<target>Errors during import. This is most likely caused by some invalid data.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue