From 07f95bc6ea1c925116fdb18842f26db75dcc7492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 28 Jan 2023 23:24:45 +0100 Subject: [PATCH] Added possibility to create nested structures of elements using Mass Import --- .../AdminPages/BaseAdminController.php | 2 +- .../Base/AbstractStructuralDBElement.php | 17 ++++- src/Entity/UserSystem/User.php | 2 +- .../ImportExportSystem/EntityImporter.php | 34 ++++++++- .../AdminPages/EntityAdminBase.html.twig | 6 +- .../ImportExportSystem/EntityImporterTest.php | 72 ++++++++++++++++++- translations/messages.en.xlf | 5 +- 7 files changed, 126 insertions(+), 12 deletions(-) diff --git a/src/Controller/AdminPages/BaseAdminController.php b/src/Controller/AdminPages/BaseAdminController.php index bbc3be39..70e82981 100644 --- a/src/Controller/AdminPages/BaseAdminController.php +++ b/src/Controller/AdminPages/BaseAdminController.php @@ -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) { diff --git a/src/Entity/Base/AbstractStructuralDBElement.php b/src/Entity/Base/AbstractStructuralDBElement.php index 8b0f2ee8..b784ab2a 100644 --- a/src/Entity/Base/AbstractStructuralDBElement.php +++ b/src/Entity/Base/AbstractStructuralDBElement.php @@ -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); } /** diff --git a/src/Entity/UserSystem/User.php b/src/Entity/UserSystem/User.php index 863e0f26..08badb34 100644 --- a/src/Entity/UserSystem/User.php +++ b/src/Entity/UserSystem/User.php @@ -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 diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index 53667e21..d6e00570 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -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; diff --git a/templates/AdminPages/EntityAdminBase.html.twig b/templates/AdminPages/EntityAdminBase.html.twig index 25c80466..506025cc 100644 --- a/templates/AdminPages/EntityAdminBase.html.twig +++ b/templates/AdminPages/EntityAdminBase.html.twig @@ -25,7 +25,7 @@
- {{ tree.treeview(entity) }} + {{ tree.treeview(entity) }}
@@ -176,7 +176,9 @@
- {% trans %}mass_creation.help{% endtrans %} +
+ {% trans %}mass_creation.help{% endtrans %} +
{{ form(mass_creation_form) }}
diff --git a/tests/Services/ImportExportSystem/EntityImporterTest.php b/tests/Services/ImportExportSystem/EntityImporterTest.php index acc7077b..79599c8e 100644 --- a/tests/Services/ImportExportSystem/EntityImporterTest.php +++ b/tests/Services/ImportExportSystem/EntityImporterTest.php @@ -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 = <<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 = <<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()); diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 19e2c026..0042506a 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -358,7 +358,7 @@ mass_creation.help - Each line will be interpreted as a name of a element, which will be created. + Each line will be interpreted as a name of a element, which will be created. You can create nested structures by indentations. @@ -7255,6 +7255,9 @@ Exampletown mass_creation.lines.placeholder Element 1 + Element 1.1 + Element 1.1.1 + Element 1.2 Element 2 Element 3