Added possibility to create nested structures of elements using Mass Import

This commit is contained in:
Jan Böhmer 2023-01-28 23:24:45 +01:00
parent 22950f2476
commit 07f95bc6ea
7 changed files with 126 additions and 12 deletions

View file

@ -364,7 +364,7 @@ abstract class BaseAdminController extends AbstractController
//Create entries based on input
$errors = [];
$results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'], $errors);
$results = $importer->massCreation($data['lines'], $this->entity_class, $data['parent'] ?? null, $errors);
//Show errors to user:
foreach ($errors as $error) {

View file

@ -148,9 +148,20 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
return false;
}
//If this' parents element, is $another_element, then we are finished
return ($this->parent->getID() === $another_element->getID())
|| $this->parent->isChildOf($another_element); //Otherwise, check recursively
//If the parent element is equal to the element we want to compare, return true
if ($this->getParent()->getID() === null || $this->getParent()->getID() === null) {
//If the IDs are not yet defined, we have to compare the objects itself
if ($this->getParent() === $another_element) {
return true;
}
} else { //If the IDs are defined, we can compare the IDs
if ($this->getParent()->getID() === $another_element->getID()) {
return true;
}
}
//Otherwise, check recursively
return $this->parent->isChildOf($another_element);
}
/**

View file

@ -83,7 +83,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
* @ORM\Column(type="string", name="config_theme", nullable=true)
* @Assert\Choice(choices=User::AVAILABLE_THEMES)
*/
protected ?string $theme = '';
protected ?string $theme = null;
/**
* @var string|null the hash of a token the user must provide when he wants to reset his password

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Services\ImportExportSystem;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement;
use function count;
use Doctrine\ORM\EntityManagerInterface;
@ -61,7 +62,7 @@ class EntityImporter
//Expand every line to a single entry:
$names = explode("\n", $lines);
if (!is_a($class_name, AbstractStructuralDBElement::class, true)) {
if (!is_a($class_name, AbstractNamedDBElement::class, true)) {
throw new InvalidArgumentException('$class_name must be a StructuralDBElement type!');
}
if (null !== $parent && !is_a($parent, $class_name)) {
@ -71,7 +72,31 @@ class EntityImporter
$errors = [];
$valid_entities = [];
$current_parent = $parent;
$last_element = $parent;
//We use this array to store all levels of indentation as a stack.
$indentations = [0];
foreach ($names as $name) {
//Count intendation level (whitespace characters at the beginning of the line)
$identSize = strlen($name)-strlen(ltrim($name));
//If the line is intendet more than the last line, we have a new parent element
if ($identSize > end($indentations)) {
$current_parent = $last_element;
//Add the new indentation level to the stack
$indentations[] = $identSize;
}
while ($identSize < end($indentations)) {
//If the line is intendet less than the last line, we have to go up in the tree
if ($current_parent instanceof AbstractStructuralDBElement) {
$current_parent = $current_parent->getParent();
} else {
$current_parent = null;
}
array_pop($indentations);
}
$name = trim($name);
if ('' === $name) {
//Skip empty lines (StrucuralDBElements must have a name)
@ -81,7 +106,10 @@ class EntityImporter
//Create new element with given name
$entity = new $class_name();
$entity->setName($name);
$entity->setParent($parent);
//Only set the parent if the entity is a StructuralDBElement
if ($entity instanceof AbstractStructuralDBElement) {
$entity->setParent($current_parent);
}
//Validate entity
$tmp = $this->validator->validate($entity);
@ -94,6 +122,8 @@ class EntityImporter
'violations' => $tmp,
];
}
$last_element = $entity;
}
return $valid_entities;

View file

@ -25,7 +25,7 @@
<div class="col-sm-4">
<turbo-frame id="admin-tree-frame" target="admin-content-frame" data-turbo-action="advance">
{{ tree.treeview(entity) }}
{{ tree.treeview(entity) }}
</turbo-frame>
</div>
@ -176,7 +176,9 @@
</div>
<div id="mass_creation" class="tab-pane fade">
<span class="text-muted">{% trans %}mass_creation.help{% endtrans %}</span>
<div class="row mb-2">
<span class="text-muted offset-sm-3">{% trans %}mass_creation.help{% endtrans %}</span>
</div>
{{ form(mass_creation_form) }}
</div>

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Tests\Services\ImportExportSystem;
use App\Entity\Attachments\AttachmentType;
use App\Entity\UserSystem\User;
use App\Services\Formatters\AmountFormatter;
use App\Services\ImportExportSystem\EntityImporter;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
@ -54,7 +55,7 @@ class EntityImporterTest extends WebTestCase
$this->assertEmpty($errors);
$errors = [];
$lines = "Test 1 \n Test 2 \n Test 3";
$lines = "Test 1\nTest 2 \nTest 3";
$results = $this->service->massCreation($lines, AttachmentType::class, null, $errors);
$this->assertCount(0, $errors);
$this->assertCount(3, $results);
@ -72,11 +73,78 @@ class EntityImporterTest extends WebTestCase
$this->assertSame($parent, $results[0]->getParent());
}
public function testNonStructuralClass(): void
{
$input = <<<EOT
Test1
Test1.1
Test2
EOT;
$errors = [];
$results = $this->service->massCreation($input, User::class, null, $errors);
//Import must not fail, even with non-structural classes
$this->assertCount(3, $results);
$this->assertCount(0, $errors);
$this->assertSame('Test1', $results[0]->getName());
$this->assertSame('Test1.1', $results[1]->getName());
$this->assertSame('Test2', $results[2]->getName());
}
public function testMassCreationNested(): void
{
$input = <<<EOT
Test 1
Test 1.1
Test 1.1.1
Test 1.1.2
Test 1.2
Test 1.2.1
Test 2
EOT;
$errors = [];
$parent = new AttachmentType();
$results = $this->service->massCreation($input, AttachmentType::class, $parent, $errors);
//We have 7 elements, an now errros
$this->assertCount(0, $errors);
$this->assertCount(7, $results);
$element1 = $results[0];
$element11 = $results[1];
$element111 = $results[2];
$element112 = $results[3];
$element12 = $results[4];
$element121 = $results[5];
$element2 = $results[6];
$this->assertSame('Test 1', $element1->getName());
$this->assertSame('Test 1.1', $element11->getName());
$this->assertSame('Test 1.1.1', $element111->getName());
$this->assertSame('Test 1.1.2', $element112->getName());
$this->assertSame('Test 1.2', $element12->getName());
$this->assertSame('Test 1.2.1', $element121->getName());
$this->assertSame('Test 2', $element2->getName());
//Check parents
$this->assertSame($parent, $element1->getParent());
$this->assertSame($element1, $element11->getParent());
$this->assertSame($element11, $element111->getParent());
$this->assertSame($element11, $element112->getParent());
$this->assertSame($element1, $element12->getParent());
$this->assertSame($element12, $element121->getParent());
$this->assertSame($parent, $element2->getParent());
}
public function testMassCreationErrors(): void
{
$errors = [];
//Node 1 and Node 2 are created in datafixtures, so their attemp to create them again must fail.
$lines = "Test 1 \n Node 1 \n Node 2";
$lines = "Test 1\nNode 1\nNode 2";
$results = $this->service->massCreation($lines, AttachmentType::class, null, $errors);
$this->assertCount(1, $results);
$this->assertSame('Test 1', $results[0]->getName());

View file

@ -358,7 +358,7 @@
</notes>
<segment>
<source>mass_creation.help</source>
<target>Each line will be interpreted as a name of a element, which will be created.</target>
<target>Each line will be interpreted as a name of a element, which will be created. You can create nested structures by indentations.</target>
</segment>
</unit>
<unit id="a5.CFfq" name="edit.caption">
@ -7255,6 +7255,9 @@ Exampletown</target>
<segment>
<source>mass_creation.lines.placeholder</source>
<target>Element 1
Element 1.1
Element 1.1.1
Element 1.2
Element 2
Element 3</target>
</segment>