Added a very basic import dialog for Parts

This commit is contained in:
Jan Böhmer 2023-03-12 19:53:55 +01:00
parent 8f033910ce
commit 7a9b7c87a4
7 changed files with 149 additions and 22 deletions

View file

@ -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());

View file

@ -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 ?? [],
]);
}
/**

View file

@ -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' => [

View file

@ -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,16 +250,10 @@ 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);
}
}
//Save changes to database, when no error happened, or we should continue on error.
$this->em->flush();

View file

@ -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 %}

View 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 %}

View file

@ -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>