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 //Create entries based on input
$errors = []; $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: //Show errors to user:
foreach ($errors as $error) { foreach ($errors as $error) {

View file

@ -148,9 +148,20 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement
return false; return false;
} }
//If this' parents element, is $another_element, then we are finished //If the parent element is equal to the element we want to compare, return true
return ($this->parent->getID() === $another_element->getID()) if ($this->getParent()->getID() === null || $this->getParent()->getID() === null) {
|| $this->parent->isChildOf($another_element); //Otherwise, check recursively //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) * @ORM\Column(type="string", name="config_theme", nullable=true)
* @Assert\Choice(choices=User::AVAILABLE_THEMES) * @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 * @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; namespace App\Services\ImportExportSystem;
use App\Entity\Base\AbstractNamedDBElement;
use App\Entity\Base\AbstractStructuralDBElement; use App\Entity\Base\AbstractStructuralDBElement;
use function count; use function count;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -61,7 +62,7 @@ class EntityImporter
//Expand every line to a single entry: //Expand every line to a single entry:
$names = explode("\n", $lines); $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!'); throw new InvalidArgumentException('$class_name must be a StructuralDBElement type!');
} }
if (null !== $parent && !is_a($parent, $class_name)) { if (null !== $parent && !is_a($parent, $class_name)) {
@ -71,7 +72,31 @@ class EntityImporter
$errors = []; $errors = [];
$valid_entities = []; $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) { 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); $name = trim($name);
if ('' === $name) { if ('' === $name) {
//Skip empty lines (StrucuralDBElements must have a name) //Skip empty lines (StrucuralDBElements must have a name)
@ -81,7 +106,10 @@ class EntityImporter
//Create new element with given name //Create new element with given name
$entity = new $class_name(); $entity = new $class_name();
$entity->setName($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 //Validate entity
$tmp = $this->validator->validate($entity); $tmp = $this->validator->validate($entity);
@ -94,6 +122,8 @@ class EntityImporter
'violations' => $tmp, 'violations' => $tmp,
]; ];
} }
$last_element = $entity;
} }
return $valid_entities; return $valid_entities;

View file

@ -176,7 +176,9 @@
</div> </div>
<div id="mass_creation" class="tab-pane fade"> <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) }} {{ form(mass_creation_form) }}
</div> </div>

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Tests\Services\ImportExportSystem; namespace App\Tests\Services\ImportExportSystem;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Entity\UserSystem\User;
use App\Services\Formatters\AmountFormatter; use App\Services\Formatters\AmountFormatter;
use App\Services\ImportExportSystem\EntityImporter; use App\Services\ImportExportSystem\EntityImporter;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
@ -54,7 +55,7 @@ class EntityImporterTest extends WebTestCase
$this->assertEmpty($errors); $this->assertEmpty($errors);
$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); $results = $this->service->massCreation($lines, AttachmentType::class, null, $errors);
$this->assertCount(0, $errors); $this->assertCount(0, $errors);
$this->assertCount(3, $results); $this->assertCount(3, $results);
@ -72,11 +73,78 @@ class EntityImporterTest extends WebTestCase
$this->assertSame($parent, $results[0]->getParent()); $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 public function testMassCreationErrors(): void
{ {
$errors = []; $errors = [];
//Node 1 and Node 2 are created in datafixtures, so their attemp to create them again must fail. //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); $results = $this->service->massCreation($lines, AttachmentType::class, null, $errors);
$this->assertCount(1, $results); $this->assertCount(1, $results);
$this->assertSame('Test 1', $results[0]->getName()); $this->assertSame('Test 1', $results[0]->getName());

View file

@ -358,7 +358,7 @@
</notes> </notes>
<segment> <segment>
<source>mass_creation.help</source> <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> </segment>
</unit> </unit>
<unit id="a5.CFfq" name="edit.caption"> <unit id="a5.CFfq" name="edit.caption">
@ -7255,6 +7255,9 @@ Exampletown</target>
<segment> <segment>
<source>mass_creation.lines.placeholder</source> <source>mass_creation.lines.placeholder</source>
<target>Element 1 <target>Element 1
Element 1.1
Element 1.1.1
Element 1.2
Element 2 Element 2
Element 3</target> Element 3</target>
</segment> </segment>