diff --git a/assets/ts_src/ajax_ui.ts b/assets/ts_src/ajax_ui.ts index 5c293e9b..0004eeea 100644 --- a/assets/ts_src/ajax_ui.ts +++ b/assets/ts_src/ajax_ui.ts @@ -68,8 +68,12 @@ class AjaxUI { * Starts the ajax ui und execute handlers registered in addStartAction(). * Should be called in a document.ready, after handlers are set. */ - public start() + public start(disabled : boolean = false) { + if(disabled) { + return; + } + console.info("AjaxUI started!"); this.BASE = $("body").data("base-url") + "/"; @@ -227,6 +231,19 @@ class AjaxUI { { let options : JQueryFormOptions = { success: this.onAjaxComplete, + beforeSerialize: function() : boolean { + + //Update the content of textarea fields using CKEDITOR before submitting. + + //@ts-ignore + for(let name in CKEDITOR.instances) + { + //@ts-ignore + CKEDITOR.instances[name].updateElement(); + } + + return true; + }, beforeSubmit: function (arr, $form, options) : boolean { //When data-with-progbar is specified, then show progressbar. if($form.data("with-progbar") != undefined) { diff --git a/src/Controller/AttachmentTypeController.php b/src/Controller/AttachmentTypeController.php new file mode 100644 index 00000000..13cc82f5 --- /dev/null +++ b/src/Controller/AttachmentTypeController.php @@ -0,0 +1,97 @@ +createForm(BaseEntityAdminForm::class, $entity); + + $form->handleRequest($request); + if ($form->isSubmitted() && $form->isValid()) { + $em->persist($entity); + $em->flush(); + } + + return $this->render('AdminPages/AttachmentTypeAdmin.html.twig', [ + 'entity' => $entity, + 'form' => $form->createView() + ]); + } + + /** + * @Route("/new") + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function new(Request $request, EntityManagerInterface $em) + { + $new_entity = new AttachmentType(); + + $this->denyAccessUnlessGranted('create', $new_entity); + + $form = $this->createForm(BaseEntityAdminForm::class, $new_entity); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $em->persist($new_entity); + $em->flush(); + //$this->addFlash('success', $translator->trans('part.created_flash')); + + return $this->redirectToRoute('attachment_type_edit', ['id' => $new_entity->getID()]); + } + + return $this->render('AdminPages/AttachmentTypeAdmin.html.twig', [ + 'entity' => $new_entity, + 'form' => $form->createView() + ]); + } +} \ No newline at end of file diff --git a/src/Entity/AttachmentType.php b/src/Entity/AttachmentType.php index 5c99c437..7d56ee4a 100644 --- a/src/Entity/AttachmentType.php +++ b/src/Entity/AttachmentType.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Entity; +use App\Validator\Constraints\NoneOfItsChildren; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; diff --git a/src/Entity/DBElement.php b/src/Entity/DBElement.php index 0211b27b..32cd3cfa 100644 --- a/src/Entity/DBElement.php +++ b/src/Entity/DBElement.php @@ -50,9 +50,11 @@ abstract class DBElement * Get the ID. The ID can be zero, or even negative (for virtual elements). If an elemenent is virtual, can be * checked with isVirtualElement(). * - * @return int the ID of this element + * Returns null, if the element is not saved to the DB yet. + * + * @return int|null the ID of this element */ - final public function getID(): int + final public function getID(): ?int { return (int) $this->id; } diff --git a/src/Entity/NamedDBElement.php b/src/Entity/NamedDBElement.php index 7ed29de7..da6dd995 100644 --- a/src/Entity/NamedDBElement.php +++ b/src/Entity/NamedDBElement.php @@ -74,20 +74,22 @@ abstract class NamedDBElement extends DBElement /** * Returns the last time when the element was modified. + * Returns null if the element was not yet saved to DB yet. * - * @return \DateTime The time of the last edit. + * @return \DateTime|null The time of the last edit. */ - public function getLastModified(): \DateTime + public function getLastModified(): ?\DateTime { return $this->lastModified; } /** * Returns the date/time when the element was created. + * Returns null if the element was not yet saved to DB yet. * - * @return \DateTime The creation time of the part. + * @return \DateTime|null The creation time of the part. */ - public function getAddedDate(): \DateTime + public function getAddedDate(): ?\DateTime { return $this->addedDate; } diff --git a/src/Entity/StructuralDBElement.php b/src/Entity/StructuralDBElement.php index c76e090a..d1fa2063 100644 --- a/src/Entity/StructuralDBElement.php +++ b/src/Entity/StructuralDBElement.php @@ -23,9 +23,9 @@ declare(strict_types=1); namespace App\Entity; -use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\PersistentCollection; +use App\Validator\Constraints\NoneOfItsChildren; /** * All elements with the fields "id", "name" and "parent_id" (at least). @@ -53,6 +53,7 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement protected $children; /** * @var StructuralDBElement + * @NoneOfItsChildren() */ protected $parent; @@ -85,7 +86,7 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement * Check if this element is a child of another element (recursive). * * @param StructuralDBElement $another_element the object to compare - * IMPORTANT: both objects to compare must be from the same class (for example two "Device" objects)! + * IMPORTANT: both objects to compare must be from the same class (for example two "Device" objects)! * * @return bool True, if this element is child of $another_element. * @@ -97,10 +98,10 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement //Check if both elements compared, are from the same type: if ($class_name != \get_class($another_element)) { - throw new \InvalidArgumentException('isChildOf() funktioniert nur mit Elementen des gleichen Typs!'); + throw new \InvalidArgumentException('isChildOf() only works for objects of the same type!'); } - if (null == $this->getID()) { // this is the root node + if (null == $this->getParent()) { // this is the root node return false; } @@ -116,13 +117,13 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement ******************************************************************************/ /** - * @brief Get the parent-ID + * Get the parent-ID * - * @retval integer * the ID of the parent element + * @return integer * the ID of the parent element * * NULL means, the parent is the root node * * the parent ID of the root node is -1 */ - public function getParentID(): int + protected function getParentID(): int { return $this->parent_id ?? self::ID_ROOT_ELEMENT; //Null means root element } @@ -139,14 +140,12 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement /** * Get the comment of the element. - * - * @param bool $parse_bbcode Should BBCode converted to HTML, before returning - * + * @return string the comment */ - public function getComment(bool $parse_bbcode = true): string + public function getComment(): ?string { - return htmlspecialchars($this->comment ?? ''); + return $this->comment; } /** @@ -179,8 +178,6 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement * @param string $delimeter the delimeter of the returned string * * @return string the full path (incl. the name of this element), delimeted by $delimeter - * - * @throws Exception if there was an error */ public function getFullPath(string $delimeter = self::PATH_DELIMITER_ARROW): string { @@ -189,9 +186,13 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement $this->full_path_strings[] = $this->getName(); $element = $this; - while (null != $element->parent) { + $overflow = 20; //We only allow 20 levels depth + + while (null != $element->parent && $overflow >= 0) { $element = $element->parent; $this->full_path_strings[] = $element->getName(); + //Decrement to prevent mem overflow. + $overflow--; } $this->full_path_strings = array_reverse($this->full_path_strings); @@ -219,143 +220,31 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement ******************************************************************************/ /** - * Change the parent ID of this element. - * - * @param int|null $new_parent_id * the ID of the new parent element - * * NULL if the parent should be the root node + * Sets the new parent object + * @param self $new_parent The new parent object + * @return StructuralDBElement */ - public function setParentID($new_parent_id): self + public function setParent(?self $new_parent) : self { - $this->parent_id = $new_parent_id; + /* + if ($new_parent->isChildOf($this)) { + throw new \InvalidArgumentException('You can not use one of the element childs as parent!'); + } */ + + $this->parent = $new_parent; return $this; } /** * Set the comment. - * * @param string $new_comment the new comment - * - * @throws Exception if there was an error + * @return StructuralDBElement */ - public function setComment(string $new_comment): self + public function setComment(?string $new_comment): self { $this->comment = $new_comment; return $this; } - - /******************************************************************************** - * - * Tree / Table Builders - * - *********************************************************************************/ - - /** - * Build a HTML tree with all subcategories of this element. - * - * This method prints a '; - } else { - $root_level = $this->getLevel() + 1; - } - - // get all subelements - $subelements = $this->getSubelements($recursive); - - foreach ($subelements as $element) { - $level = $element->getLevel() - $root_level; - $selected = ($element->getID() == $selected_id) ? 'selected' : ''; - - $html[] = ''; - } - - return implode("\n", $html); - } - - /** - * Creates a template loop for a Breadcrumb bar, representing the structural DB element. - * - * @param $page string The base page, to which the breadcrumb links should be directing to. - * @param $parameter string The parameter, which selects the ID of the StructuralDBElement. - * @param bool $show_root Show the root as its own breadcrumb. - * @param string $root_name The label which should be used for the root breadcrumb. - * - * @return array An Loop containing multiple arrays, which contains href and caption for the breadcrumb. - */ - public function buildBreadcrumbLoop(string $page, string $parameter, bool $show_root = false, $root_name = '$$', bool $element_is_link = false): array - { - $breadcrumb = array(); - - if ('$$' == $root_name) { - $root_name = _('Oberste Ebene'); - } - - if ($show_root) { - $breadcrumb[] = array('label' => $root_name, - 'disabled' => true, ); - } - - if (!$this->current_user->canDo(static::getPermissionName(), StructuralPermission::READ)) { - return array('label' => '???', - 'disabled' => true, ); - } - - $tmp = array(); - - if ($element_is_link) { - $tmp[] = array('label' => $this->getName(), 'href' => $page.'?'.$parameter.'='.$this->getID(), 'selected' => true); - } else { - $tmp[] = array('label' => $this->getName(), 'selected' => true); - } - - $parent_id = $this->getParentID(); - while ($parent_id > 0) { - /** @var StructuralDBElement $element */ - $element = static::getInstance($this->database, $this->current_user, $this->log, $parent_id); - $parent_id = $element->getParentID(); - $tmp[] = array('label' => $element->getName(), 'href' => $page.'?'.$parameter.'='.$element->getID()); - } - $tmp = array_reverse($tmp); - - $breadcrumb = array_merge($breadcrumb, $tmp); - - return $breadcrumb; - } } diff --git a/src/Form/BaseEntityAdminForm.php b/src/Form/BaseEntityAdminForm.php new file mode 100644 index 00000000..fd570ded --- /dev/null +++ b/src/Form/BaseEntityAdminForm.php @@ -0,0 +1,76 @@ +security = $security; + } + + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $entity = $options['data']; + + $builder + ->add('name', TextType::class, ['empty_data' => '', 'label' => 'name.label', + 'attr' => ['placeholder' => 'part.name.placeholder'], + 'disabled' => !$this->security->isGranted('edit', $entity), ]) + + ->add('parent', EntityType::class, ['class' => get_class($entity), 'choice_label' => 'full_path', + 'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'required' => false, 'label' => 'parent.label', + 'disabled' => !$this->security->isGranted('move', $entity), ]) + + ->add('comment', CKEditorType::class, ['required' => false, + 'label' => 'comment.label', 'attr' => ['rows' => 4], 'help' => 'bbcode.hint', + 'disabled' => !$this->security->isGranted('edit', $entity)]) + + //Buttons + ->add('save', SubmitType::class, ['label' => 'part.edit.save']) + ->add('reset', ResetType::class, ['label' => 'part.edit.reset']); + } +} \ No newline at end of file diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php new file mode 100644 index 00000000..2917a948 --- /dev/null +++ b/src/Security/Voter/StructureVoter.php @@ -0,0 +1,109 @@ +instanceToPermissionName($subject); + //If permission name is null, then the subject is not supported + return ($permission_name !== null) && $this->resolver->isValidOperation($permission_name, $attribute); + + } + + /** + * Maps a instance type to the permission name. + * @param $subject mixed The subject for which the permission name should be generated. + * @return string|null The name of the permission for the subject's type or null, if the subject is not supported. + */ + protected function instanceToPermissionName($subject) : ?string + { + $class_name = get_class($subject); + switch ($class_name) { + case AttachmentType::class: + return 'attachment_types'; + case Category::class: + return 'categories'; + case Device::class: + return 'devices'; + case Footprint::class: + return 'footprints'; + case Manufacturer::class: + return 'manufacturers'; + case Storelocation::class: + return 'storelocations'; + case Supplier::class: + return 'suppliers'; + } + //When the class is not supported by this class return null + return null; + } + + /** + * Similar to voteOnAttribute, but checking for the anonymous user is already done. + * The current user (or the anonymous user) is passed by $user. + * + * @param $attribute + * @param $subject + * @param User $user + * + * @return bool + */ + protected function voteOnUser($attribute, $subject, User $user): bool + { + $permission_name = $this->instanceToPermissionName($subject); + //Just resolve the permission + return $this->resolver->inherit($user, $permission_name, $attribute) ?? false; + } + + +} \ No newline at end of file diff --git a/src/Validator/Constraints/NoneOfItsChildren.php b/src/Validator/Constraints/NoneOfItsChildren.php new file mode 100644 index 00000000..6b7c36c6 --- /dev/null +++ b/src/Validator/Constraints/NoneOfItsChildren.php @@ -0,0 +1,54 @@ +context->getObject(); + /** @var StructuralDBElement $value */ + + // Check if the targeted parent is the object itself: + $entity_id = $entity->getID(); + if ($entity_id !== null && $entity_id === $value->getID()) { + //Set the entity to a valid state + $entity->setParent(null); + $this->context->buildViolation($constraint->self_message)->addViolation(); + //The other things can not happen. + return; + } + + // Check if the targeted parent is a child object + if ($value->isChildOf($entity)) { + //Set the entity to a valid state + $entity->setParent(null); + $this->context->buildViolation($constraint->children_message)->addViolation(); + return; + } + } +} \ No newline at end of file diff --git a/templates/AdminPages/AttachmentTypeAdmin.html.twig b/templates/AdminPages/AttachmentTypeAdmin.html.twig new file mode 100644 index 00000000..85ba9366 --- /dev/null +++ b/templates/AdminPages/AttachmentTypeAdmin.html.twig @@ -0,0 +1,5 @@ +{% extends "AdminPages/EntityAdminBase.html.twig" %} + +{% block card_title %} + {% trans %}attachment_type.caption{% endtrans %} +{% endblock %} \ No newline at end of file diff --git a/templates/AdminPages/EntityAdminBase.html.twig b/templates/AdminPages/EntityAdminBase.html.twig new file mode 100644 index 00000000..94149446 --- /dev/null +++ b/templates/AdminPages/EntityAdminBase.html.twig @@ -0,0 +1,82 @@ +{% extends "main_card.html.twig" %} + +{% block card_content %} +