From 0c7ec9f0c78e4ae53f6d154696b4157f8b635f68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 18 Dec 2022 20:34:25 +0100 Subject: [PATCH 01/61] Added some basic DB fields for the new project system --- migrations/Version20221218192108.php | 33 ++++++++ .../Migrations/ConvertBBCodeCommand.php | 4 +- ...troller.php => ProjectAdminController.php} | 25 +++--- src/Controller/TreeController.php | 6 +- src/Controller/TypeaheadController.php | 4 +- src/DataFixtures/DataStructureFixtures.php | 4 +- src/Entity/Attachments/Attachment.php | 2 +- ...ceAttachment.php => ProjectAttachment.php} | 10 +-- .../Base/AbstractPartsContainingDBElement.php | 3 +- src/Entity/LogSystem/AbstractLogEntry.php | 8 +- .../LogSystem/CollectionElementDeleted.php | 14 ++-- src/Entity/Parameters/AbstractParameter.php | 2 +- ...viceParameter.php => ProjectParameter.php} | 10 +-- src/Entity/Parts/Part.php | 4 +- .../Device.php => ProjectSystem/Project.php} | 81 +++++++++++++++---- .../ProjectBOMEntry.php} | 54 ++++++++----- src/Form/AdminPages/ProjectAdminForm.php | 42 ++++++++++ src/Form/Filters/AttachmentFilterType.php | 4 +- src/Form/Filters/LogFilterType.php | 8 +- src/Form/ParameterType.php | 4 +- src/Repository/Parts/DeviceRepository.php | 9 ++- src/Security/Voter/ParameterVoter.php | 4 +- src/Security/Voter/StructureVoter.php | 4 +- .../Attachments/AttachmentSubmitHandler.php | 4 +- src/Services/ElementTypeNameGenerator.php | 4 +- src/Services/EntityURLGenerator.php | 14 ++-- src/Services/Tools/StatisticsHelper.php | 4 +- src/Services/Trees/ToolsTreeBuilder.php | 4 +- src/Services/Trees/TreeViewGenerator.php | 6 +- src/Twig/EntityExtension.php | 4 +- src/Twig/FormatExtension.php | 2 +- templates/AdminPages/DeviceAdmin.html.twig | 4 + .../AdminPages/DeviceControllerTest.php | 4 +- tests/Entity/Attachments/AttachmentTest.php | 10 +-- .../Entity/LogSystem/AbstractLogEntryTest.php | 8 +- tests/Twig/EntityExtensionTest.php | 4 +- 36 files changed, 281 insertions(+), 130 deletions(-) create mode 100644 migrations/Version20221218192108.php rename src/Controller/AdminPages/{DeviceController.php => ProjectAdminController.php} (74%) rename src/Entity/Attachments/{DeviceAttachment.php => ProjectAttachment.php} (81%) rename src/Entity/Parameters/{DeviceParameter.php => ProjectParameter.php} (87%) rename src/Entity/{Devices/Device.php => ProjectSystem/Project.php} (64%) rename src/Entity/{Devices/DevicePart.php => ProjectSystem/ProjectBOMEntry.php} (58%) create mode 100644 src/Form/AdminPages/ProjectAdminForm.php diff --git a/migrations/Version20221218192108.php b/migrations/Version20221218192108.php new file mode 100644 index 00000000..8de3b04d --- /dev/null +++ b/migrations/Version20221218192108.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE device_parts ADD name LONGTEXT NOT NULL, ADD comment LONGTEXT NOT NULL, ADD last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, ADD datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE quantity quantity DOUBLE PRECISION NOT NULL'); + $this->addSql('ALTER TABLE devices ADD description LONGTEXT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE devices DROP description'); + $this->addSql('ALTER TABLE device_parts DROP name, DROP comment, DROP last_modified, DROP datetime_added, CHANGE quantity quantity INT NOT NULL'); + } +} diff --git a/src/Command/Migrations/ConvertBBCodeCommand.php b/src/Command/Migrations/ConvertBBCodeCommand.php index 0712f6fd..6b2b3fd7 100644 --- a/src/Command/Migrations/ConvertBBCodeCommand.php +++ b/src/Command/Migrations/ConvertBBCodeCommand.php @@ -24,7 +24,7 @@ namespace App\Command\Migrations; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractNamedDBElement; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Manufacturer; use App\Entity\Parts\MeasurementUnit; @@ -94,7 +94,7 @@ class ConvertBBCodeCommand extends Command Part::class => ['description', 'comment'], AttachmentType::class => ['comment'], Storelocation::class => ['comment'], - Device::class => ['comment'], + Project::class => ['comment'], Category::class => ['comment'], Manufacturer::class => ['comment'], MeasurementUnit::class => ['comment'], diff --git a/src/Controller/AdminPages/DeviceController.php b/src/Controller/AdminPages/ProjectAdminController.php similarity index 74% rename from src/Controller/AdminPages/DeviceController.php rename to src/Controller/AdminPages/ProjectAdminController.php index 7f98a218..736d1302 100644 --- a/src/Controller/AdminPages/DeviceController.php +++ b/src/Controller/AdminPages/ProjectAdminController.php @@ -22,10 +22,11 @@ declare(strict_types=1); namespace App\Controller\AdminPages; -use App\Entity\Attachments\DeviceAttachment; -use App\Entity\Devices\Device; -use App\Entity\Parameters\DeviceParameter; +use App\Entity\Attachments\ProjectAttachment; +use App\Entity\ProjectSystem\Project; +use App\Entity\Parameters\ProjectParameter; use App\Form\AdminPages\BaseEntityAdminForm; +use App\Form\AdminPages\ProjectAdminForm; use App\Services\ImportExportSystem\EntityExporter; use App\Services\ImportExportSystem\EntityImporter; use App\Services\Trees\StructuralElementRecursionHelper; @@ -38,19 +39,19 @@ use Symfony\Component\Routing\Annotation\Route; /** * @Route("/device") */ -class DeviceController extends BaseAdminController +class ProjectAdminController extends BaseAdminController { - protected $entity_class = Device::class; + protected $entity_class = Project::class; protected $twig_template = 'AdminPages/DeviceAdmin.html.twig'; - protected $form_class = BaseEntityAdminForm::class; + protected $form_class = ProjectAdminForm::class; protected $route_base = 'device'; - protected $attachment_class = DeviceAttachment::class; - protected $parameter_class = DeviceParameter::class; + protected $attachment_class = ProjectAttachment::class; + protected $parameter_class = ProjectParameter::class; /** * @Route("/{id}", name="device_delete", methods={"DELETE"}) */ - public function delete(Request $request, Device $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse + public function delete(Request $request, Project $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { return $this->_delete($request, $entity, $recursionHelper); } @@ -59,7 +60,7 @@ class DeviceController extends BaseAdminController * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="device_edit") * @Route("/{id}", requirements={"id"="\d+"}) */ - public function edit(Device $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response + public function edit(Project $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { return $this->_edit($entity, $request, $em, $timestamp); } @@ -69,7 +70,7 @@ class DeviceController extends BaseAdminController * @Route("/{id}/clone", name="device_clone") * @Route("/") */ - public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Device $entity = null): Response + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer, ?Project $entity = null): Response { return $this->_new($request, $em, $importer, $entity); } @@ -85,7 +86,7 @@ class DeviceController extends BaseAdminController /** * @Route("/{id}/export", name="device_export") */ - public function exportEntity(Device $entity, EntityExporter $exporter, Request $request): Response + public function exportEntity(Project $entity, EntityExporter $exporter, Request $request): Response { return $this->_exportEntity($entity, $exporter, $request); } diff --git a/src/Controller/TreeController.php b/src/Controller/TreeController.php index fbfd2c18..f5d8bd81 100644 --- a/src/Controller/TreeController.php +++ b/src/Controller/TreeController.php @@ -22,7 +22,7 @@ declare(strict_types=1); namespace App\Controller; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; @@ -136,10 +136,10 @@ class TreeController extends AbstractController * @Route("/device/{id}", name="tree_device") * @Route("/devices", name="tree_device_root") */ - public function deviceTree(?Device $device = null): JsonResponse + public function deviceTree(?Project $device = null): JsonResponse { if ($this->isGranted('@devices.read')) { - $tree = $this->treeGenerator->getTreeView(Device::class, $device, 'devices'); + $tree = $this->treeGenerator->getTreeView(Project::class, $device, 'devices'); } else { return new JsonResponse("Access denied", 403); } diff --git a/src/Controller/TypeaheadController.php b/src/Controller/TypeaheadController.php index 3a08d0d6..efefc78e 100644 --- a/src/Controller/TypeaheadController.php +++ b/src/Controller/TypeaheadController.php @@ -24,7 +24,7 @@ namespace App\Controller; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; -use App\Entity\Parameters\DeviceParameter; +use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; @@ -105,7 +105,7 @@ class TypeaheadController extends AbstractController case 'part': return PartParameter::class; case 'device': - return DeviceParameter::class; + return ProjectParameter::class; case 'footprint': return FootprintParameter::class; case 'manufacturer': diff --git a/src/DataFixtures/DataStructureFixtures.php b/src/DataFixtures/DataStructureFixtures.php index 5697b156..c7416abe 100644 --- a/src/DataFixtures/DataStructureFixtures.php +++ b/src/DataFixtures/DataStructureFixtures.php @@ -24,7 +24,7 @@ namespace App\DataFixtures; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; @@ -51,7 +51,7 @@ class DataStructureFixtures extends Fixture public function load(ObjectManager $manager): void { //Reset autoincrement - $types = [AttachmentType::class, Device::class, Category::class, Footprint::class, Manufacturer::class, + $types = [AttachmentType::class, Project::class, Category::class, Footprint::class, Manufacturer::class, MeasurementUnit::class, Storelocation::class, Supplier::class, ]; foreach ($types as $type) { diff --git a/src/Entity/Attachments/Attachment.php b/src/Entity/Attachments/Attachment.php index 0f603c09..a4807a21 100644 --- a/src/Entity/Attachments/Attachment.php +++ b/src/Entity/Attachments/Attachment.php @@ -44,7 +44,7 @@ use LogicException; * @ORM\DiscriminatorColumn(name="class_name", type="string") * @ORM\DiscriminatorMap({ * "PartDB\Part" = "PartAttachment", "Part" = "PartAttachment", - * "PartDB\Device" = "DeviceAttachment", "Device" = "DeviceAttachment", + * "PartDB\Device" = "ProjectAttachment", "Device" = "ProjectAttachment", * "AttachmentType" = "AttachmentTypeAttachment", "Category" = "CategoryAttachment", * "Footprint" = "FootprintAttachment", "Manufacturer" = "ManufacturerAttachment", * "Currency" = "CurrencyAttachment", "Group" = "GroupAttachment", diff --git a/src/Entity/Attachments/DeviceAttachment.php b/src/Entity/Attachments/ProjectAttachment.php similarity index 81% rename from src/Entity/Attachments/DeviceAttachment.php rename to src/Entity/Attachments/ProjectAttachment.php index 4f394789..1fab6bc8 100644 --- a/src/Entity/Attachments/DeviceAttachment.php +++ b/src/Entity/Attachments/ProjectAttachment.php @@ -22,7 +22,7 @@ declare(strict_types=1); namespace App\Entity\Attachments; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -32,12 +32,12 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; * @ORM\Entity() * @UniqueEntity({"name", "attachment_type", "element"}) */ -class DeviceAttachment extends Attachment +class ProjectAttachment extends Attachment { - public const ALLOWED_ELEMENT_CLASS = Device::class; + public const ALLOWED_ELEMENT_CLASS = Project::class; /** - * @var Device the element this attachment is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Devices\Device", inversedBy="attachments") + * @var Project the element this attachment is associated with + * @ORM\ManyToOne(targetEntity="App\Entity\ProjectSystem\Project", inversedBy="attachments") * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ protected ?AttachmentContainingDBElement $element = null; diff --git a/src/Entity/Base/AbstractPartsContainingDBElement.php b/src/Entity/Base/AbstractPartsContainingDBElement.php index a8ba8e8c..f30819f5 100644 --- a/src/Entity/Base/AbstractPartsContainingDBElement.php +++ b/src/Entity/Base/AbstractPartsContainingDBElement.php @@ -29,6 +29,7 @@ use Doctrine\ORM\Mapping as ORM; * * @ORM\MappedSuperclass(repositoryClass="App\Repository\AbstractPartsContainingRepository") */ -abstract class AbstractPartsContainingDBElement extends AbstractStructuralDBElement +abstract class +AbstractPartsContainingDBElement extends AbstractStructuralDBElement { } diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php index 840d7a31..2fafd8a1 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -25,8 +25,8 @@ namespace App\Entity\LogSystem; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractDBElement; -use App\Entity\Devices\Device; -use App\Entity\Devices\DevicePart; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Category; @@ -124,8 +124,8 @@ abstract class AbstractLogEntry extends AbstractDBElement self::TARGET_TYPE_ATTACHEMENT => Attachment::class, self::TARGET_TYPE_ATTACHEMENTTYPE => AttachmentType::class, self::TARGET_TYPE_CATEGORY => Category::class, - self::TARGET_TYPE_DEVICE => Device::class, - self::TARGET_TYPE_DEVICEPART => DevicePart::class, + self::TARGET_TYPE_DEVICE => Project::class, + self::TARGET_TYPE_DEVICEPART => ProjectBOMEntry::class, self::TARGET_TYPE_FOOTPRINT => Footprint::class, self::TARGET_TYPE_GROUP => Group::class, self::TARGET_TYPE_MANUFACTURER => Manufacturer::class, diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index 0e8c06a0..5b12119a 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -46,7 +46,7 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; -use App\Entity\Attachments\DeviceAttachment; +use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; use App\Entity\Attachments\ManufacturerAttachment; @@ -58,12 +58,12 @@ use App\Entity\Attachments\UserAttachment; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; use App\Entity\Parameters\CurrencyParameter; -use App\Entity\Parameters\DeviceParameter; +use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; @@ -159,8 +159,8 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU return CategoryParameter::class; case Currency::class: return CurrencyParameter::class; - case Device::class: - return DeviceParameter::class; + case Project::class: + return ProjectParameter::class; case Footprint::class: return FootprintParameter::class; case Group::class: @@ -189,8 +189,8 @@ class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventU return CategoryAttachment::class; case Currency::class: return CurrencyAttachment::class; - case Device::class: - return DeviceAttachment::class; + case Project::class: + return ProjectAttachment::class; case Footprint::class: return FootprintAttachment::class; case Group::class: diff --git a/src/Entity/Parameters/AbstractParameter.php b/src/Entity/Parameters/AbstractParameter.php index 55ebd9e5..5a3f00e3 100644 --- a/src/Entity/Parameters/AbstractParameter.php +++ b/src/Entity/Parameters/AbstractParameter.php @@ -62,7 +62,7 @@ use function sprintf; * @ORM\DiscriminatorMap({ * 0 = "CategoryParameter", * 1 = "CurrencyParameter", - * 2 = "DeviceParameter", + * 2 = "ProjectParameter", * 3 = "FootprintParameter", * 4 = "GroupParameter", * 5 = "ManufacturerParameter", diff --git a/src/Entity/Parameters/DeviceParameter.php b/src/Entity/Parameters/ProjectParameter.php similarity index 87% rename from src/Entity/Parameters/DeviceParameter.php rename to src/Entity/Parameters/ProjectParameter.php index d52e9037..2961a843 100644 --- a/src/Entity/Parameters/DeviceParameter.php +++ b/src/Entity/Parameters/ProjectParameter.php @@ -41,7 +41,7 @@ declare(strict_types=1); namespace App\Entity\Parameters; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; @@ -49,13 +49,13 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; * @ORM\Entity(repositoryClass="App\Repository\ParameterRepository") * @UniqueEntity(fields={"name", "group", "element"}) */ -class DeviceParameter extends AbstractParameter +class ProjectParameter extends AbstractParameter { - public const ALLOWED_ELEMENT_CLASS = Device::class; + public const ALLOWED_ELEMENT_CLASS = Project::class; /** - * @var Device the element this para is associated with - * @ORM\ManyToOne(targetEntity="App\Entity\Devices\Device", inversedBy="parameters") + * @var Project the element this para is associated with + * @ORM\ManyToOne(targetEntity="App\Entity\ProjectSystem\Project", inversedBy="parameters") * @ORM\JoinColumn(name="element_id", referencedColumnName="id", nullable=false, onDelete="CASCADE"). */ protected $element; diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 27aeabaa..20bc1020 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -25,7 +25,7 @@ namespace App\Entity\Parts; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; use App\Entity\Attachments\PartAttachment; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parameters\ParametersTrait; use App\Entity\Parameters\PartParameter; use App\Entity\Parts\PartTraits\AdvancedPropertyTrait; @@ -152,7 +152,7 @@ class Part extends AttachmentContainingDBElement /** * Get all devices which uses this part. * - * @return Device[] * all devices which uses this part as a one-dimensional array of Device objects + * @return Project[] * all devices which uses this part as a one-dimensional array of Device objects * (empty array if there are no ones) * * the array is sorted by the devices names */ diff --git a/src/Entity/Devices/Device.php b/src/Entity/ProjectSystem/Project.php similarity index 64% rename from src/Entity/Devices/Device.php rename to src/Entity/ProjectSystem/Project.php index 15fee00a..30c6d684 100644 --- a/src/Entity/Devices/Device.php +++ b/src/Entity/ProjectSystem/Project.php @@ -20,11 +20,12 @@ declare(strict_types=1); -namespace App\Entity\Devices; +namespace App\Entity\ProjectSystem; -use App\Entity\Attachments\DeviceAttachment; -use App\Entity\Base\AbstractPartsContainingDBElement; -use App\Entity\Parameters\DeviceParameter; +use App\Entity\Attachments\ProjectAttachment; +use App\Entity\Base\AbstractStructuralDBElement; +use App\Entity\Parameters\ProjectParameter; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use InvalidArgumentException; @@ -33,27 +34,27 @@ use InvalidArgumentException; * Class AttachmentType. * * @ORM\Entity(repositoryClass="App\Repository\Parts\DeviceRepository") - * @ORM\Table(name="`devices`") + * @ORM\Table(name="devices") */ -class Device extends AbstractPartsContainingDBElement +class Project extends AbstractStructuralDBElement { /** - * @ORM\OneToMany(targetEntity="Device", mappedBy="parent") + * @ORM\OneToMany(targetEntity="Project", mappedBy="parent") * @ORM\OrderBy({"name" = "ASC"}) * @var Collection */ protected $children; /** - * @ORM\ManyToOne(targetEntity="Device", inversedBy="children") + * @ORM\ManyToOne(targetEntity="Project", inversedBy="children") * @ORM\JoinColumn(name="parent_id", referencedColumnName="id") */ protected $parent; /** - * @ORM\OneToMany(targetEntity="DevicePart", mappedBy="device") + * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="device") */ - protected $parts; + protected $bom_entries; /** * @ORM\Column(type="integer") @@ -64,15 +65,21 @@ class Device extends AbstractPartsContainingDBElement * @ORM\Column(type="boolean") */ protected bool $order_only_missing_parts = false; + /** - * @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Attachments\DeviceAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) + * @ORM\Column(type="text", nullable=false) + */ + protected string $description = ''; + + /** + * @var Collection + * @ORM\OneToMany(targetEntity="App\Entity\Attachments\ProjectAttachment", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) * @ORM\OrderBy({"name" = "ASC"}) */ protected $attachments; - /** @var Collection - * @ORM\OneToMany(targetEntity="App\Entity\Parameters\DeviceParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) + /** @var Collection + * @ORM\OneToMany(targetEntity="App\Entity\Parameters\ProjectParameter", mappedBy="element", cascade={"persist", "remove"}, orphanRemoval=true) * @ORM\OrderBy({"group" = "ASC" ,"name" = "ASC"}) */ protected $parameters; @@ -83,6 +90,12 @@ class Device extends AbstractPartsContainingDBElement * *********************************************************************************/ + public function __construct() + { + parent::__construct(); + $this->bom_entries = new ArrayCollection(); + } + /** * Get the order quantity of this device. * @@ -131,7 +144,7 @@ class Device extends AbstractPartsContainingDBElement * * @param bool $new_order_only_missing_parts the new "order_only_missing_parts" attribute * - * @return Device + * @return Project */ public function setOrderOnlyMissingParts(bool $new_order_only_missing_parts): self { @@ -139,4 +152,42 @@ class Device extends AbstractPartsContainingDBElement return $this; } + + /** + * @return mixed + */ + public function getBomEntries() + { + return $this->bom_entries; + } + + /** + * @param mixed $bom_entries + * @return Project + */ + public function setBomEntries($bom_entries) + { + $this->bom_entries = $bom_entries; + return $this; + } + + /** + * @return string + */ + public function getDescription(): string + { + return $this->description; + } + + /** + * @param string $description + * @return Project + */ + public function setDescription(string $description): Project + { + $this->description = $description; + return $this; + } + + } diff --git a/src/Entity/Devices/DevicePart.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php similarity index 58% rename from src/Entity/Devices/DevicePart.php rename to src/Entity/ProjectSystem/ProjectBOMEntry.php index ed627a4c..0b4104c6 100644 --- a/src/Entity/Devices/DevicePart.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -20,42 +20,60 @@ declare(strict_types=1); -namespace App\Entity\Devices; +namespace App\Entity\ProjectSystem; use App\Entity\Base\AbstractDBElement; +use App\Entity\Base\TimestampTrait; use App\Entity\Parts\Part; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; /** - * Class DevicePart. + * The ProjectBOMEntry class represents a entry in a project's BOM. * - * @ORM\Table("`device_parts`") + * @ORM\Table("device_parts") * @ORM\Entity() */ -class DevicePart extends AbstractDBElement +class ProjectBOMEntry extends AbstractDBElement { - /** - * @var int - * @ORM\Column(type="integer", name="quantity") - */ - protected int $quantity; + use TimestampTrait; /** - * @var string + * @var int + * @ORM\Column(type="float", name="quantity") + * @Assert\PositiveOrZero() + */ + protected float $quantity; + + /** + * @var string A comma separated list of the names, where this parts should be placed * @ORM\Column(type="text", name="mountnames") */ protected string $mountnames; - /** - * @var Device - * @ORM\ManyToOne(targetEntity="Device", inversedBy="parts") - * @ORM\JoinColumn(name="id_device", referencedColumnName="id") - */ - protected ?Device $device = null; /** - * @var Part + * @var string An optional name describing this BOM entry (useful for non-part entries) + * @ORM\Column(type="text") + */ + protected string $name; + + /** + * @var string An optional comment for this BOM entry + * @ORM\Column(type="text") + */ + protected string $comment; + + /** + * @var Project + * @ORM\ManyToOne(targetEntity="Project", inversedBy="parts") + * @ORM\JoinColumn(name="id_device", referencedColumnName="id") + */ + protected ?Project $device = null; + + /** + * @var Part|null The part associated with this * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part") - * @ORM\JoinColumn(name="id_part", referencedColumnName="id") + * @ORM\JoinColumn(name="id_part", referencedColumnName="id", nullable=true) */ protected ?Part $part = null; } diff --git a/src/Form/AdminPages/ProjectAdminForm.php b/src/Form/AdminPages/ProjectAdminForm.php new file mode 100644 index 00000000..f382ae6e --- /dev/null +++ b/src/Form/AdminPages/ProjectAdminForm.php @@ -0,0 +1,42 @@ +. + */ + +namespace App\Form\AdminPages; + +use App\Entity\Base\AbstractNamedDBElement; +use App\Form\Type\RichTextEditorType; +use Symfony\Component\Form\FormBuilderInterface; + +class ProjectAdminForm extends BaseEntityAdminForm +{ + protected function additionalFormElements(FormBuilderInterface $builder, array $options, AbstractNamedDBElement $entity): void + { + $builder->add('description', RichTextEditorType::class, [ + 'required' => false, + 'empty_data' => '', + 'label' => 'part.edit.description', + 'mode' => 'markdown-single_line', + 'attr' => [ + 'placeholder' => 'part.edit.description.placeholder', + 'rows' => 2, + ], + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/AttachmentFilterType.php b/src/Form/Filters/AttachmentFilterType.php index eeea0fa9..6ef03220 100644 --- a/src/Form/Filters/AttachmentFilterType.php +++ b/src/Form/Filters/AttachmentFilterType.php @@ -25,7 +25,7 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; -use App\Entity\Attachments\DeviceAttachment; +use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; use App\Entity\Attachments\LabelAttachment; @@ -80,7 +80,7 @@ class AttachmentFilterType extends AbstractType 'attachment_type.label' => AttachmentTypeAttachment::class, 'category.label' => CategoryAttachment::class, 'currency.label' => CurrencyAttachment::class, - 'device.label' => DeviceAttachment::class, + 'device.label' => ProjectAttachment::class, 'footprint.label' => FootprintAttachment::class, 'group.label' => GroupAttachment::class, 'label_profile.label' => LabelAttachment::class, diff --git a/src/Form/Filters/LogFilterType.php b/src/Form/Filters/LogFilterType.php index 39393917..e9cb7a53 100644 --- a/src/Form/Filters/LogFilterType.php +++ b/src/Form/Filters/LogFilterType.php @@ -23,8 +23,8 @@ namespace App\Form\Filters; use App\DataTables\Filters\LogFilter; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; -use App\Entity\Devices\Device; -use App\Entity\Devices\DevicePart; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Entity\LabelSystem\LabelProfile; use App\Entity\LogSystem\AbstractLogEntry; use App\Entity\LogSystem\CollectionElementDeleted; @@ -135,8 +135,8 @@ class LogFilterType extends AbstractType 'attachment.label' => AbstractLogEntry::targetTypeClassToID(Attachment::class), 'attachment_type.label' => AbstractLogEntry::targetTypeClassToID(AttachmentType::class), 'category.label' => AbstractLogEntry::targetTypeClassToID(Category::class), - 'device.label' => AbstractLogEntry::targetTypeClassToID(Device::class), - 'device_part.label' => AbstractLogEntry::targetTypeClassToID(DevicePart::class), + 'device.label' => AbstractLogEntry::targetTypeClassToID(Project::class), + 'device_part.label' => AbstractLogEntry::targetTypeClassToID(ProjectBOMEntry::class), 'footprint.label' => AbstractLogEntry::targetTypeClassToID(Footprint::class), 'group.label' => AbstractLogEntry::targetTypeClassToID(Group::class), 'manufacturer.label' => AbstractLogEntry::targetTypeClassToID(Manufacturer::class), diff --git a/src/Form/ParameterType.php b/src/Form/ParameterType.php index ef993c40..09293b97 100644 --- a/src/Form/ParameterType.php +++ b/src/Form/ParameterType.php @@ -45,7 +45,7 @@ use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; use App\Entity\Parameters\CurrencyParameter; -use App\Entity\Parameters\DeviceParameter; +use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; @@ -158,7 +158,7 @@ class ParameterType extends AbstractType AttachmentTypeParameter::class => 'attachment_type', CategoryParameter::class => 'category', CurrencyParameter::class => 'currency', - DeviceParameter::class => 'device', + ProjectParameter::class => 'device', FootprintParameter::class => 'footprint', GroupParameter::class => 'group', ManufacturerParameter::class => 'manufacturer', diff --git a/src/Repository/Parts/DeviceRepository.php b/src/Repository/Parts/DeviceRepository.php index 013958c1..dc5d5acc 100644 --- a/src/Repository/Parts/DeviceRepository.php +++ b/src/Repository/Parts/DeviceRepository.php @@ -22,18 +22,19 @@ namespace App\Repository\Parts; use App\Entity\Base\AbstractPartsContainingDBElement; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Part; use App\Repository\AbstractPartsContainingRepository; +use App\Repository\StructuralDBElementRepository; use InvalidArgumentException; -class DeviceRepository extends AbstractPartsContainingRepository +class DeviceRepository extends StructuralDBElementRepository { public function getParts(object $element, array $order_by = ['name' => 'ASC']): array { - if (!$element instanceof Device) { + if (!$element instanceof Project) { throw new InvalidArgumentException('$element must be an Device!'); } @@ -44,7 +45,7 @@ class DeviceRepository extends AbstractPartsContainingRepository public function getPartsCount(object $element): int { - if (!$element instanceof Device) { + if (!$element instanceof Project) { throw new InvalidArgumentException('$element must be an Device!'); } diff --git a/src/Security/Voter/ParameterVoter.php b/src/Security/Voter/ParameterVoter.php index c09a8e04..713a14c9 100644 --- a/src/Security/Voter/ParameterVoter.php +++ b/src/Security/Voter/ParameterVoter.php @@ -24,7 +24,7 @@ use App\Entity\Parameters\AbstractParameter; use App\Entity\Parameters\AttachmentTypeParameter; use App\Entity\Parameters\CategoryParameter; use App\Entity\Parameters\CurrencyParameter; -use App\Entity\Parameters\DeviceParameter; +use App\Entity\Parameters\ProjectParameter; use App\Entity\Parameters\FootprintParameter; use App\Entity\Parameters\GroupParameter; use App\Entity\Parameters\ManufacturerParameter; @@ -95,7 +95,7 @@ class ParameterVoter extends ExtendedVoter $param = 'categories'; } elseif ($subject instanceof CurrencyParameter) { $param = 'currencies'; - } elseif ($subject instanceof DeviceParameter) { + } elseif ($subject instanceof ProjectParameter) { $param = 'devices'; } elseif ($subject instanceof FootprintParameter) { $param = 'footprints'; diff --git a/src/Security/Voter/StructureVoter.php b/src/Security/Voter/StructureVoter.php index 50f21fd1..6c7bb6a2 100644 --- a/src/Security/Voter/StructureVoter.php +++ b/src/Security/Voter/StructureVoter.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Security\Voter; use App\Entity\Attachments\AttachmentType; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; @@ -40,7 +40,7 @@ class StructureVoter extends ExtendedVoter protected const OBJ_PERM_MAP = [ AttachmentType::class => 'attachment_types', Category::class => 'categories', - Device::class => 'devices', + Project::class => 'devices', Footprint::class => 'footprints', Manufacturer::class => 'manufacturers', Storelocation::class => 'storelocations', diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index 18ec6896..9640d4b0 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -28,7 +28,7 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; -use App\Entity\Attachments\DeviceAttachment; +use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; use App\Entity\Attachments\ManufacturerAttachment; @@ -82,7 +82,7 @@ class AttachmentSubmitHandler AttachmentTypeAttachment::class => 'attachment_type', CategoryAttachment::class => 'category', CurrencyAttachment::class => 'currency', - DeviceAttachment::class => 'device', + ProjectAttachment::class => 'device', FootprintAttachment::class => 'footprint', GroupAttachment::class => 'group', ManufacturerAttachment::class => 'manufacturer', diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index 3e456da7..fce1395c 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -25,7 +25,7 @@ namespace App\Services; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Contracts\NamedElementInterface; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; use App\Entity\Parts\Category; @@ -59,7 +59,7 @@ class ElementTypeNameGenerator Attachment::class => $this->translator->trans('attachment.label'), Category::class => $this->translator->trans('category.label'), AttachmentType::class => $this->translator->trans('attachment_type.label'), - Device::class => $this->translator->trans('device.label'), + Project::class => $this->translator->trans('device.label'), Footprint::class => $this->translator->trans('footprint.label'), Manufacturer::class => $this->translator->trans('manufacturer.label'), MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 2d0afd88..81d87f00 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -26,7 +26,7 @@ use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; use App\Entity\Base\AbstractDBElement; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -113,7 +113,7 @@ class EntityURLGenerator //As long we does not have own things for it use edit page AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', - Device::class => 'device_edit', + Project::class => 'device_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', Storelocation::class => 'store_location_edit', @@ -204,7 +204,7 @@ class EntityURLGenerator //As long we does not have own things for it use edit page AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', - Device::class => 'device_edit', + Project::class => 'device_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', Storelocation::class => 'store_location_edit', @@ -234,7 +234,7 @@ class EntityURLGenerator Part::class => 'part_edit', AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', - Device::class => 'device_edit', + Project::class => 'device_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', Storelocation::class => 'store_location_edit', @@ -264,7 +264,7 @@ class EntityURLGenerator Part::class => 'part_new', AttachmentType::class => 'attachment_type_new', Category::class => 'category_new', - Device::class => 'device_new', + Project::class => 'device_new', Supplier::class => 'supplier_new', Manufacturer::class => 'manufacturer_new', Storelocation::class => 'store_location_new', @@ -295,7 +295,7 @@ class EntityURLGenerator Part::class => 'part_clone', AttachmentType::class => 'attachment_type_clone', Category::class => 'category_clone', - Device::class => 'device_clone', + Project::class => 'device_clone', Supplier::class => 'supplier_clone', Manufacturer::class => 'manufacturer_clone', Storelocation::class => 'store_location_clone', @@ -338,7 +338,7 @@ class EntityURLGenerator Part::class => 'part_delete', AttachmentType::class => 'attachment_type_delete', Category::class => 'category_delete', - Device::class => 'device_delete', + Project::class => 'device_delete', Supplier::class => 'supplier_delete', Manufacturer::class => 'manufacturer_delete', Storelocation::class => 'store_location_delete', diff --git a/src/Services/Tools/StatisticsHelper.php b/src/Services/Tools/StatisticsHelper.php index 9f54db80..60ed568d 100644 --- a/src/Services/Tools/StatisticsHelper.php +++ b/src/Services/Tools/StatisticsHelper.php @@ -43,7 +43,7 @@ namespace App\Services\Tools; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; @@ -111,7 +111,7 @@ class StatisticsHelper $arr = [ 'attachment_type' => AttachmentType::class, 'category' => Category::class, - 'device' => Device::class, + 'device' => Project::class, 'footprint' => Footprint::class, 'manufacturer' => Manufacturer::class, 'measurement_unit' => MeasurementUnit::class, diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index d94ccf20..48a8d660 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -24,7 +24,7 @@ namespace App\Services\Trees; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -156,7 +156,7 @@ class ToolsTreeBuilder $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } - if ($this->security->isGranted('read', new Device())) { + if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.devices'), $this->urlGenerator->generate('device_new') diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 8a413cc2..00deded3 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -25,7 +25,7 @@ namespace App\Services\Trees; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\AbstractNamedDBElement; use App\Entity\Base\AbstractStructuralDBElement; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; @@ -161,7 +161,7 @@ class TreeViewGenerator return $this->translator->trans('manufacturer.labelp'); case Supplier::class: return $this->translator->trans('supplier.labelp'); - case Device::class: + case Project::class: return $this->translator->trans('device.labelp'); default: return $this->translator->trans('tree.root_node.text'); @@ -182,7 +182,7 @@ class TreeViewGenerator return $icon . 'fa-industry'; case Supplier::class: return $icon . 'fa-truck'; - case Device::class: + case Project::class: return $icon . 'fa-archive'; default: return null; diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index 5c0141ff..6d477d88 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -22,7 +22,7 @@ namespace App\Twig; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -99,7 +99,7 @@ final class EntityExtension extends AbstractExtension Storelocation::class => 'storelocation', Manufacturer::class => 'manufacturer', Category::class => 'category', - Device::class => 'device', + Project::class => 'device', Attachment::class => 'attachment', Supplier::class => 'supplier', User::class => 'user', diff --git a/src/Twig/FormatExtension.php b/src/Twig/FormatExtension.php index 08a4e85c..6d251267 100644 --- a/src/Twig/FormatExtension.php +++ b/src/Twig/FormatExtension.php @@ -24,7 +24,7 @@ namespace App\Twig; use App\Entity\Attachments\Attachment; use App\Entity\Base\AbstractDBElement; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; diff --git a/templates/AdminPages/DeviceAdmin.html.twig b/templates/AdminPages/DeviceAdmin.html.twig index 633d3bb2..b241f831 100644 --- a/templates/AdminPages/DeviceAdmin.html.twig +++ b/templates/AdminPages/DeviceAdmin.html.twig @@ -10,4 +10,8 @@ {% block new_title %} {% trans %}device.new{% endtrans %} +{% endblock %} + +{% block additional_controls %} + {{ form_row(form.description) }} {% endblock %} \ No newline at end of file diff --git a/tests/Controller/AdminPages/DeviceControllerTest.php b/tests/Controller/AdminPages/DeviceControllerTest.php index 122d24ab..be078ebf 100644 --- a/tests/Controller/AdminPages/DeviceControllerTest.php +++ b/tests/Controller/AdminPages/DeviceControllerTest.php @@ -23,7 +23,7 @@ declare(strict_types=1); namespace App\Tests\Controller\AdminPages; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; /** * @group slow @@ -32,5 +32,5 @@ use App\Entity\Devices\Device; class DeviceControllerTest extends AbstractAdminControllerTest { protected static $base_path = '/en'.'/device'; - protected static $entity_class = Device::class; + protected static $entity_class = Project::class; } diff --git a/tests/Entity/Attachments/AttachmentTest.php b/tests/Entity/Attachments/AttachmentTest.php index b6abfb7e..91a3cb53 100644 --- a/tests/Entity/Attachments/AttachmentTest.php +++ b/tests/Entity/Attachments/AttachmentTest.php @@ -27,7 +27,7 @@ use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentTypeAttachment; use App\Entity\Attachments\CategoryAttachment; use App\Entity\Attachments\CurrencyAttachment; -use App\Entity\Attachments\DeviceAttachment; +use App\Entity\Attachments\ProjectAttachment; use App\Entity\Attachments\FootprintAttachment; use App\Entity\Attachments\GroupAttachment; use App\Entity\Attachments\ManufacturerAttachment; @@ -36,7 +36,7 @@ use App\Entity\Attachments\PartAttachment; use App\Entity\Attachments\StorelocationAttachment; use App\Entity\Attachments\SupplierAttachment; use App\Entity\Attachments\UserAttachment; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; @@ -78,7 +78,7 @@ class AttachmentTest extends TestCase [AttachmentTypeAttachment::class, AttachmentType::class], [CategoryAttachment::class, Category::class], [CurrencyAttachment::class, Currency::class], - [DeviceAttachment::class, Device::class], + [ProjectAttachment::class, Project::class], [FootprintAttachment::class, Footprint::class], [GroupAttachment::class, Group::class], [ManufacturerAttachment::class, Manufacturer::class], @@ -117,8 +117,8 @@ class AttachmentTest extends TestCase /** @var Attachment $attachment */ $attachment = new $attachment_class(); - if (Device::class !== $allowed_class) { - $element = new Device(); + if (Project::class !== $allowed_class) { + $element = new Project(); } else { $element = new Category(); } diff --git a/tests/Entity/LogSystem/AbstractLogEntryTest.php b/tests/Entity/LogSystem/AbstractLogEntryTest.php index e2632873..fe4cdb58 100644 --- a/tests/Entity/LogSystem/AbstractLogEntryTest.php +++ b/tests/Entity/LogSystem/AbstractLogEntryTest.php @@ -44,8 +44,8 @@ namespace App\Tests\Entity\LogSystem; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\PartAttachment; -use App\Entity\Devices\Device; -use App\Entity\Devices\DevicePart; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Entity\LogSystem\AbstractLogEntry; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -82,8 +82,8 @@ class AbstractLogEntryTest extends TestCase [2, Attachment::class], [3, AttachmentType::class], [4, Category::class], - [5, Device::class], - [6, DevicePart::class], + [5, Project::class], + [6, ProjectBOMEntry::class], [7, Footprint::class], [8, Group::class], [9, Manufacturer::class], diff --git a/tests/Twig/EntityExtensionTest.php b/tests/Twig/EntityExtensionTest.php index b9124658..9e951ea6 100644 --- a/tests/Twig/EntityExtensionTest.php +++ b/tests/Twig/EntityExtensionTest.php @@ -22,7 +22,7 @@ namespace App\Tests\Twig; use App\Entity\Attachments\Attachment; use App\Entity\Attachments\PartAttachment; -use App\Entity\Devices\Device; +use App\Entity\ProjectSystem\Project; use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; @@ -58,7 +58,7 @@ class EntityExtensionTest extends WebTestCase $this->assertEquals('storelocation', $this->service->getEntityType(new Storelocation())); $this->assertEquals('manufacturer', $this->service->getEntityType(new Manufacturer())); $this->assertEquals('category', $this->service->getEntityType(new Category())); - $this->assertEquals('device', $this->service->getEntityType(new Device())); + $this->assertEquals('device', $this->service->getEntityType(new Project())); $this->assertEquals('attachment', $this->service->getEntityType(new PartAttachment())); $this->assertEquals('supplier', $this->service->getEntityType(new Supplier())); $this->assertEquals('user', $this->service->getEntityType(new User())); From 855b3070bb987cfaad2d54119c42c66736147a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 18 Dec 2022 20:55:38 +0100 Subject: [PATCH 02/61] Fix exception when creating an attachment without an file., --- src/Services/Attachments/AttachmentSubmitHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/Attachments/AttachmentSubmitHandler.php b/src/Services/Attachments/AttachmentSubmitHandler.php index 9640d4b0..4dcf1a65 100644 --- a/src/Services/Attachments/AttachmentSubmitHandler.php +++ b/src/Services/Attachments/AttachmentSubmitHandler.php @@ -223,7 +223,7 @@ class AttachmentSubmitHandler //Determine the old filepath $old_path = $this->pathResolver->placeholderToRealPath($attachment->getPath()); - if (!file_exists($old_path)) { + if (empty($old_path) || !file_exists($old_path)) { return $attachment; } $filename = basename($old_path); From d5b1c6be0a4d3e7d4585d7f5eb38bc3ba7b7f671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 18 Dec 2022 21:58:21 +0100 Subject: [PATCH 03/61] Added simple info page for projects --- src/Controller/ProjectController.php | 61 ++++++ src/DataTables/Column/EntityColumn.php | 6 +- .../Helpers/PartDataTableHelper.php | 91 +++++++++ src/DataTables/PartsDataTable.php | 47 +---- src/DataTables/ProjectBomEntriesDataTable.php | 178 ++++++++++++++++++ src/Entity/ProjectSystem/Project.php | 2 +- src/Entity/ProjectSystem/ProjectBOMEntry.php | 114 ++++++++++- src/Services/EntityURLGenerator.php | 2 + src/Services/Trees/TreeViewGenerator.php | 2 +- templates/Projects/_info_card.html.twig | 133 +++++++++++++ templates/Projects/info.html.twig | 19 ++ translations/messages.en.xlf | 68 ++++--- 12 files changed, 647 insertions(+), 76 deletions(-) create mode 100644 src/Controller/ProjectController.php create mode 100644 src/DataTables/Helpers/PartDataTableHelper.php create mode 100644 src/DataTables/ProjectBomEntriesDataTable.php create mode 100644 templates/Projects/_info_card.html.twig create mode 100644 templates/Projects/info.html.twig diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php new file mode 100644 index 00000000..ede384e0 --- /dev/null +++ b/src/Controller/ProjectController.php @@ -0,0 +1,61 @@ +. + */ + +namespace App\Controller; + +use App\DataTables\ProjectBomEntriesDataTable; +use App\Entity\ProjectSystem\Project; +use Omines\DataTablesBundle\DataTableFactory; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Routing\Annotation\Route; + +/** + * @Route("/project") + */ +class ProjectController extends AbstractController +{ + private DataTableFactory $dataTableFactory; + + public function __construct(DataTableFactory $dataTableFactory) + { + $this->dataTableFactory = $dataTableFactory; + } + + /** + * @Route("/{id}", name="project_info") + */ + public function info(Project $project, Request $request) + { + $this->denyAccessUnlessGranted('read', $project); + + $table = $this->dataTableFactory->createFromType(ProjectBomEntriesDataTable::class, ['project' => $project]) + ->handleRequest($request); + + if ($table->isCallback()) { + return $table->getResponse(); + } + + return $this->render('Projects/info.html.twig', [ + 'datatable' => $table, + 'project' => $project, + ]); + } +} \ No newline at end of file diff --git a/src/DataTables/Column/EntityColumn.php b/src/DataTables/Column/EntityColumn.php index 715e39c4..6d78aac3 100644 --- a/src/DataTables/Column/EntityColumn.php +++ b/src/DataTables/Column/EntityColumn.php @@ -65,8 +65,8 @@ class EntityColumn extends AbstractColumn }); $resolver->setDefault('render', function (Options $options) { - return function ($value, Part $context) use ($options) { - /** @var AbstractDBElement|null $entity */ + return function ($value, $context) use ($options) { + /** @var AbstractNamedDBElement|null $entity */ $entity = $this->accessor->getValue($context, $options['property']); if (null !== $entity) { @@ -74,7 +74,7 @@ class EntityColumn extends AbstractColumn return sprintf( '%s', $this->urlGenerator->listPartsURL($entity), - $value + $entity->getName() ); } diff --git a/src/DataTables/Helpers/PartDataTableHelper.php b/src/DataTables/Helpers/PartDataTableHelper.php new file mode 100644 index 00000000..37737052 --- /dev/null +++ b/src/DataTables/Helpers/PartDataTableHelper.php @@ -0,0 +1,91 @@ +. + */ + +namespace App\DataTables\Helpers; + +use App\Entity\Parts\Part; +use App\Services\Attachments\AttachmentURLGenerator; +use App\Services\Attachments\PartPreviewGenerator; +use App\Services\EntityURLGenerator; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * A helper service which contains common code to render columns for part related tables + */ +class PartDataTableHelper +{ + private PartPreviewGenerator $previewGenerator; + private AttachmentURLGenerator $attachmentURLGenerator; + + private TranslatorInterface $translator; + private EntityURLGenerator $entityURLGenerator; + + public function __construct(PartPreviewGenerator $previewGenerator, AttachmentURLGenerator $attachmentURLGenerator, + EntityURLGenerator $entityURLGenerator, TranslatorInterface $translator) + { + $this->previewGenerator = $previewGenerator; + $this->attachmentURLGenerator = $attachmentURLGenerator; + $this->translator = $translator; + $this->entityURLGenerator = $entityURLGenerator; + } + + public function renderName(Part $context): string + { + $icon = ''; + + //Depending on the part status we show a different icon (the later conditions have higher priority) + if ($context->isFavorite()) { + $icon = sprintf('', $this->translator->trans('part.favorite.badge')); + } + if ($context->isNeedsReview()) { + $icon = sprintf('', $this->translator->trans('part.needs_review.badge')); + } + + + return sprintf( + '%s%s', + $this->entityURLGenerator->infoURL($context), + $icon, + htmlentities($context->getName()) + ); + } + + public function renderPicture(Part $context): string + { + $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context); + if (null === $preview_attachment) { + return ''; + } + + $title = htmlspecialchars($preview_attachment->getName()); + if ($preview_attachment->getFilename()) { + $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')'; + } + + return sprintf( + '%s', + 'Part image', + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment), + $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'), + 'img-fluid hoverpic', + $title + ); + } +} \ No newline at end of file diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index e77cd081..f38215e3 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -34,6 +34,7 @@ use App\DataTables\Column\SIUnitNumberColumn; use App\DataTables\Column\TagsColumn; use App\DataTables\Filters\PartFilter; use App\DataTables\Filters\PartSearchFilter; +use App\DataTables\Helpers\PartDataTableHelper; use App\Entity\Parts\Category; use App\Entity\Parts\Footprint; use App\Entity\Parts\Manufacturer; @@ -63,26 +64,27 @@ final class PartsDataTable implements DataTableTypeInterface private TranslatorInterface $translator; private NodesListBuilder $treeBuilder; private AmountFormatter $amountFormatter; - private PartPreviewGenerator $previewGenerator; private AttachmentURLGenerator $attachmentURLGenerator; private Security $security; + private PartDataTableHelper $partDataTableHelper; + /** * @var EntityURLGenerator */ private $urlGenerator; public function __construct(EntityURLGenerator $urlGenerator, TranslatorInterface $translator, - NodesListBuilder $treeBuilder, AmountFormatter $amountFormatter, - PartPreviewGenerator $previewGenerator, AttachmentURLGenerator $attachmentURLGenerator, Security $security) + NodesListBuilder $treeBuilder, AmountFormatter $amountFormatter,PartDataTableHelper $partDataTableHelper, + AttachmentURLGenerator $attachmentURLGenerator, Security $security) { $this->urlGenerator = $urlGenerator; $this->translator = $translator; $this->treeBuilder = $treeBuilder; $this->amountFormatter = $amountFormatter; - $this->previewGenerator = $previewGenerator; $this->attachmentURLGenerator = $attachmentURLGenerator; $this->security = $security; + $this->partDataTableHelper = $partDataTableHelper; } public function configureOptions(OptionsResolver $optionsResolver): void @@ -122,46 +124,13 @@ final class PartsDataTable implements DataTableTypeInterface 'label' => '', 'className' => 'no-colvis', 'render' => function ($value, Part $context) { - $preview_attachment = $this->previewGenerator->getTablePreviewAttachment($context); - if (null === $preview_attachment) { - return ''; - } - - $title = htmlspecialchars($preview_attachment->getName()); - if ($preview_attachment->getFilename()) { - $title .= ' ('.htmlspecialchars($preview_attachment->getFilename()).')'; - } - - return sprintf( - '%s', - 'Part image', - $this->attachmentURLGenerator->getThumbnailURL($preview_attachment), - $this->attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_md'), - 'img-fluid hoverpic', - $title - ); + return $this->partDataTableHelper->renderPicture($context); }, ]) ->add('name', TextColumn::class, [ 'label' => $this->translator->trans('part.table.name'), 'render' => function ($value, Part $context) { - $icon = ''; - - //Depending on the part status we show a different icon (the later conditions have higher priority) - if ($context->isFavorite()) { - $icon = sprintf('', $this->translator->trans('part.favorite.badge')); - } - if ($context->isNeedsReview()) { - $icon = sprintf('', $this->translator->trans('part.needs_review.badge')); - } - - - return sprintf( - '%s%s', - $this->urlGenerator->infoURL($context), - $icon, - htmlentities($context->getName()) - ); + return $this->partDataTableHelper->renderName($context); }, ]) ->add('id', TextColumn::class, [ diff --git a/src/DataTables/ProjectBomEntriesDataTable.php b/src/DataTables/ProjectBomEntriesDataTable.php new file mode 100644 index 00000000..50586749 --- /dev/null +++ b/src/DataTables/ProjectBomEntriesDataTable.php @@ -0,0 +1,178 @@ +. + */ + +namespace App\DataTables; + +use App\DataTables\Column\EntityColumn; +use App\DataTables\Column\LocaleDateTimeColumn; +use App\DataTables\Column\MarkdownColumn; +use App\DataTables\Column\SelectColumn; +use App\DataTables\Helpers\PartDataTableHelper; +use App\Entity\Attachments\Attachment; +use App\Entity\Parts\Part; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Services\EntityURLGenerator; +use App\Services\Formatters\AmountFormatter; +use Doctrine\ORM\QueryBuilder; +use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider; +use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter; +use Omines\DataTablesBundle\Column\TextColumn; +use Omines\DataTablesBundle\DataTable; +use Omines\DataTablesBundle\DataTableTypeInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +class ProjectBomEntriesDataTable implements DataTableTypeInterface +{ + protected TranslatorInterface $translator; + protected PartDataTableHelper $partDataTableHelper; + protected EntityURLGenerator $entityURLGenerator; + protected AmountFormatter $amountFormatter; + + public function __construct(TranslatorInterface $translator, PartDataTableHelper $partDataTableHelper, + EntityURLGenerator $entityURLGenerator, AmountFormatter $amountFormatter) + { + $this->translator = $translator; + $this->partDataTableHelper = $partDataTableHelper; + $this->entityURLGenerator = $entityURLGenerator; + $this->amountFormatter = $amountFormatter; + } + + + public function configure(DataTable $dataTable, array $options) + { + $dataTable + //->add('select', SelectColumn::class) + ->add('picture', TextColumn::class, [ + 'label' => '', + 'className' => 'no-colvis', + 'render' => function ($value, ProjectBOMEntry $context) { + if($context->getPart() === null) { + return ''; + } + return $this->partDataTableHelper->renderPicture($context->getPart()); + }, + ]) + + ->add('id', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.id'), + 'visible' => false, + ]) + + ->add('quantity', TextColumn::class, [ + 'label' => $this->translator->trans('project.bom.quantity'), + 'className' => 'text-center', + 'render' => function ($value, ProjectBOMEntry $context) { + //If we have a non-part entry, only show the rounded quantity + if ($context->getPart() === null) { + return round($context->getQuantity()); + } + //Otherwise use the unit of the part to format the quantity + return $this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()); + }, + ]) + + ->add('name', TextColumn::class, [ + 'label' => $this->translator->trans('part.table.name'), + 'orderable' => false, + 'render' => function ($value, ProjectBOMEntry $context) { + if($context->getPart() === null) { + return $context->getName(); + } + if($context->getPart() !== null) { + $tmp = $this->partDataTableHelper->renderName($context->getPart()); + if(!empty($context->getName())) { + $tmp .= '
'.htmlspecialchars($context->getName()).''; + } + return $tmp; + } + }, + ]) + + ->add('description', MarkdownColumn::class, [ + 'label' => $this->translator->trans('part.table.description'), + 'data' => function (ProjectBOMEntry $context) { + if($context->getPart() !== null) { + return $context->getPart()->getDescription(); + } + //For non-part BOM entries show the comment field + return $context->getComment(); + }, + ]) + + + ->add('category', EntityColumn::class, [ + 'label' => $this->translator->trans('part.table.category'), + 'property' => 'part.category', + ]) + ->add('footprint', EntityColumn::class, [ + 'property' => 'part.footprint', + 'label' => $this->translator->trans('part.table.footprint'), + ]) + + ->add('manufacturer', EntityColumn::class, [ + 'property' => 'part.manufacturer', + 'label' => $this->translator->trans('part.table.manufacturer'), + ]) + + ->add('mountnames', TextColumn::class, [ + + ]) + + + ->add('addedDate', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.addedDate'), + 'visible' => false, + ]) + ->add('lastModified', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('part.table.lastModified'), + 'visible' => false, + ]) + ; + + $dataTable->createAdapter(ORMAdapter::class, [ + 'entity' => Attachment::class, + 'query' => function (QueryBuilder $builder) use ($options): void { + $this->getQuery($builder, $options); + }, + 'criteria' => [ + function (QueryBuilder $builder) use ($options): void { + $this->buildCriteria($builder, $options); + }, + new SearchCriteriaProvider(), + ], + ]); + } + + private function getQuery(QueryBuilder $builder, array $options): void + { + $builder->select('bom_entry') + ->addSelect('part') + ->from(ProjectBOMEntry::class, 'bom_entry') + ->leftJoin('bom_entry.part', 'part') + ->where('bom_entry.project = :project') + ->setParameter('project', $options['project']); + ; + } + + private function buildCriteria(QueryBuilder $builder, array $options): void + { + + } +} \ No newline at end of file diff --git a/src/Entity/ProjectSystem/Project.php b/src/Entity/ProjectSystem/Project.php index 30c6d684..54970b08 100644 --- a/src/Entity/ProjectSystem/Project.php +++ b/src/Entity/ProjectSystem/Project.php @@ -52,7 +52,7 @@ class Project extends AbstractStructuralDBElement protected $parent; /** - * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="device") + * @ORM\OneToMany(targetEntity="ProjectBOMEntry", mappedBy="project") */ protected $bom_entries; diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 0b4104c6..221c77bd 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -68,7 +68,7 @@ class ProjectBOMEntry extends AbstractDBElement * @ORM\ManyToOne(targetEntity="Project", inversedBy="parts") * @ORM\JoinColumn(name="id_device", referencedColumnName="id") */ - protected ?Project $device = null; + protected ?Project $project = null; /** * @var Part|null The part associated with this @@ -76,4 +76,116 @@ class ProjectBOMEntry extends AbstractDBElement * @ORM\JoinColumn(name="id_part", referencedColumnName="id", nullable=true) */ protected ?Part $part = null; + + /** + * @return float + */ + public function getQuantity(): float + { + return $this->quantity; + } + + /** + * @param float $quantity + * @return ProjectBOMEntry + */ + public function setQuantity(float $quantity): ProjectBOMEntry + { + $this->quantity = $quantity; + return $this; + } + + /** + * @return string + */ + public function getMountnames(): string + { + return $this->mountnames; + } + + /** + * @param string $mountnames + * @return ProjectBOMEntry + */ + public function setMountnames(string $mountnames): ProjectBOMEntry + { + $this->mountnames = $mountnames; + return $this; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + * @return ProjectBOMEntry + */ + public function setName(string $name): ProjectBOMEntry + { + $this->name = $name; + return $this; + } + + /** + * @return string + */ + public function getComment(): string + { + return $this->comment; + } + + /** + * @param string $comment + * @return ProjectBOMEntry + */ + public function setComment(string $comment): ProjectBOMEntry + { + $this->comment = $comment; + return $this; + } + + /** + * @return Project|null + */ + public function getProject(): ?Project + { + return $this->project; + } + + /** + * @param Project|null $project + * @return ProjectBOMEntry + */ + public function setProject(?Project $project): ProjectBOMEntry + { + $this->project = $project; + return $this; + } + + + + /** + * @return Part|null + */ + public function getPart(): ?Part + { + return $this->part; + } + + /** + * @param Part|null $part + * @return ProjectBOMEntry + */ + public function setPart(?Part $part): ProjectBOMEntry + { + $this->part = $part; + return $this; + } + + } diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index 81d87f00..c84ee1c5 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -322,6 +322,8 @@ class EntityURLGenerator public function listPartsURL(AbstractDBElement $entity): string { $map = [ + Project::class => 'project_info', + Category::class => 'part_list_category', Footprint::class => 'part_list_footprint', Manufacturer::class => 'part_list_manufacturer', diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 00deded3..3bba2acf 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -110,7 +110,7 @@ class TreeViewGenerator } if ($mode === 'devices') { - $href_type = ''; + $href_type = 'list_parts'; } $generic = $this->getGenericTree($class, $parent); diff --git a/templates/Projects/_info_card.html.twig b/templates/Projects/_info_card.html.twig new file mode 100644 index 00000000..898aa6b8 --- /dev/null +++ b/templates/Projects/_info_card.html.twig @@ -0,0 +1,133 @@ +{% import "helper.twig" as helper %} +{% import "LabelSystem/dropdown_macro.html.twig" as dropdown %} + +{{ helper.breadcrumb_entity_link(project) }} + +
+
+
+ +
+
+
+ {% if project.description is not empty %} + {{ project.description|format_markdown }} + {% endif %} +
+ +
+
+
+
+
+
+ + {{ project.name }} +
+
+ + + {% if project.parent %} + {{ project.parent.fullPath }} + {% else %} + - + {% endif %} + +
+
+
+ {% block quick_links %}{% endblock %} + + + {% trans %}entity.edit.btn{% endtrans %} + +
+ + {{ project.lastModified | format_datetime("short") }} + +
+ + {{ project.addedDate | format_datetime("short") }} + +
+
+
+
+
+
+
+ + {{ project.children | length }} +
+
+ + {{ project.bomEntries | length }} +
+
+
+ + {% if project.attachments is not empty %} +
+ {% include "Parts/info/_attachments_info.html.twig" with {"part": project} %} +
+ {% endif %} + + {% if project.parameters is not empty %} +
+ {% for name, parameters in project.groupedParameters %} + {% if name is not empty %}
{{ name }}
{% endif %} + {{ helper.parameters_table(project) }} + {% endfor %} +
+ {% endif %} + + {% if project.comment is not empty %} +
+
+ {{ project.comment|format_markdown }} +
+
+ {% endif %} +
+
+
+
+
+
+
\ No newline at end of file diff --git a/templates/Projects/info.html.twig b/templates/Projects/info.html.twig new file mode 100644 index 00000000..861183af --- /dev/null +++ b/templates/Projects/info.html.twig @@ -0,0 +1,19 @@ +{% extends "base.html.twig" %} + +{% import "components/datatables.macro.html.twig" as datatables %} + +{% block title %} + {% trans %}parts_list.category.title{% endtrans %} {{ project.name }} +{% endblock %} + +{% block content %} + + {% include "Projects/_info_card.html.twig" %} + + {{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }} + + {# {% include "Parts/lists/_action_bar.html.twig" with {'url_options': {'category': entity.iD}} %} + + {% include "Parts/lists/_parts_list.html.twig" %} #} + +{% endblock %} \ No newline at end of file diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 6ee35efc..28cebd1a 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -8693,7 +8693,7 @@ Element 3 - + tfa.provider.webauthn_two_factor_provider Security key @@ -9768,184 +9768,190 @@ Element 3 - + entity.info.parts_count_recursive Number of parts with this element or its subelements - + tools.server_infos.title Server Infos - + permission.preset.read_only Read-Only - + permission.preset.read_only.desc Only allow read operations on data - + permission.preset.all_inherit Inherit all - + permission.preset.all_inherit.desc Set all permissions to Inherit - + permission.preset.all_forbid Forbid all - + permission.preset.all_forbid.desc Set all permissions to Forbid - + permission.preset.all_allow Allow all - + permission.preset.all_allow.desc Set all permissions to allow - + perm.server_infos Server infos - + permission.preset.editor Editor - + permission.preset.editor.desc Allow to change parts and data structures - + permission.preset.admin Admin - + permission.preset.admin.desc Allow administrative actions - + permission.preset.button Apply preset - + perm.attachments.show_private Show private attachments - + perm.attachments.list_attachments Show list of all attachments - + user.edit.permission_success Permission preset applied successfully. Check if the new permissions fit your needs. - + perm.group.data Data - + part_list.action.action.group.needs_review Needs Review - + part_list.action.action.set_needs_review Set Needs Review Status - + part_list.action.action.unset_needs_review Unset Needs Review Status - + part.edit.ipn Internal Part Number (IPN) - + part.ipn.not_defined Not defined - + part.table.ipn IPN - + currency.edit.update_rate Retrieve exchange rate - + currency.edit.exchange_rate_update.unsupported_currency The currency is unsupported by the exchange rate provider. Check your exchange rate provider configuration. - + currency.edit.exchange_rate_update.generic_error Unable to retrieve the exchange rate. Check your exchange rate provider configuration. - + currency.edit.exchange_rate_updated.success Retrieved the exchange rate successfully. + + + project.bom.quantity + BOM Qty. + + From 7f38095e82a483d08fc8d1f98cb5ffeb5681842d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 18 Dec 2022 23:58:04 +0100 Subject: [PATCH 04/61] Added a part info tab to show in which projects the part is used. --- src/Entity/Parts/Part.php | 28 ++++++++++++++++--- src/Entity/ProjectSystem/ProjectBOMEntry.php | 2 +- templates/Parts/info/_projects.html.twig | 26 +++++++++++++++++ templates/Parts/info/show_part_info.html.twig | 13 +++++++++ 4 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 templates/Parts/info/_projects.html.twig diff --git a/src/Entity/Parts/Part.php b/src/Entity/Parts/Part.php index 20bc1020..281d577b 100644 --- a/src/Entity/Parts/Part.php +++ b/src/Entity/Parts/Part.php @@ -33,6 +33,7 @@ use App\Entity\Parts\PartTraits\BasicPropertyTrait; use App\Entity\Parts\PartTraits\InstockTrait; use App\Entity\Parts\PartTraits\ManufacturerTrait; use App\Entity\Parts\PartTraits\OrderTrait; +use App\Entity\ProjectSystem\ProjectBOMEntry; use DateTime; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; @@ -65,9 +66,10 @@ class Part extends AttachmentContainingDBElement use ParametersTrait; /** - * TODO. + * @var Collection $project_bom_entries + * @ORM\OneToMany(targetEntity="App\Entity\ProjectSystem\ProjectBOMEntry", mappedBy="part") */ - protected $devices = []; + protected $project_bom_entries = []; /** @var Collection * @Assert\Valid() @@ -120,6 +122,7 @@ class Part extends AttachmentContainingDBElement $this->partLots = new ArrayCollection(); $this->orderdetails = new ArrayCollection(); $this->parameters = new ArrayCollection(); + $this->project_bom_entries = new ArrayCollection(); } public function __clone() @@ -149,6 +152,17 @@ class Part extends AttachmentContainingDBElement parent::__clone(); } + /** + * Returns all ProjectBOMEntries that use this part. + * @return Collection|ProjectBOMEntry[] + */ + public function getProjectBomEntries(): Collection + { + return $this->project_bom_entries; + } + + + /** * Get all devices which uses this part. * @@ -156,8 +170,14 @@ class Part extends AttachmentContainingDBElement * (empty array if there are no ones) * * the array is sorted by the devices names */ - public function getDevices(): array + public function getProjects(): array { - return $this->devices; + $projects = []; + + foreach($this->project_bom_entries as $entry) { + $projects[] = $entry->getProject(); + } + + return $projects; } } diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 221c77bd..2d039411 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -72,7 +72,7 @@ class ProjectBOMEntry extends AbstractDBElement /** * @var Part|null The part associated with this - * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part") + * @ORM\ManyToOne(targetEntity="App\Entity\Parts\Part", inversedBy="project_bom_entries") * @ORM\JoinColumn(name="id_part", referencedColumnName="id", nullable=true) */ protected ?Part $part = null; diff --git a/templates/Parts/info/_projects.html.twig b/templates/Parts/info/_projects.html.twig new file mode 100644 index 00000000..a4c265c8 --- /dev/null +++ b/templates/Parts/info/_projects.html.twig @@ -0,0 +1,26 @@ +{% import "components/attachments.macro.html.twig" as attachments %} + + + + + + + + + + + + + + {% for bom_entry in part.projectBomEntries %} + + + {# Name #} + {# Description #} + + + + {% endfor %} + + +
{% trans %}entity.info.name{% endtrans %}{% trans %}description.label{% endtrans %}{% trans %}project.bom.quantity{% endtrans %}{% trans %}project.bom.mountnames{% endtrans %}
{% if bom_entry.project.masterPictureAttachment is not null %}{{ attachments.attachment_icon(bom_entry.project.masterPictureAttachment, attachment_manager) }}{% endif %}{{ bom_entry.project.name }}{{ bom_entry.project.description|format_markdown }}{{ bom_entry.quantity | format_amount(part.partUnit) }}{{ bom_entry.mountnames }}
\ No newline at end of file diff --git a/templates/Parts/info/show_part_info.html.twig b/templates/Parts/info/show_part_info.html.twig index 021b6666..10cd423e 100644 --- a/templates/Parts/info/show_part_info.html.twig +++ b/templates/Parts/info/show_part_info.html.twig @@ -91,6 +91,15 @@ {% trans %}vendor.partinfo.history{% endtrans %} + {% if part.projectBomEntries is not empty %} + + {% endif %} + {% if project.children is not empty %} + + {% endif %} - + {% if project.attachments is not empty %} + + {% endif %} + {% if project.parameters is not empty %} + + {% endif %}
@@ -76,7 +92,13 @@ {% include "Projects/info/_bom.html.twig" %}
- Attachments + {% include "Parts/info/_attachments_info.html.twig" with {"part": project} %} +
+
+ {% for name, parameters in project.groupedParameters %} + {% if name is not empty %}
{{ name }}
{% endif %} + {{ helper.parameters_table(project.parameters) }} + {% endfor %}
From f62937096f4ef4d57432cc4b5be1ddc34acddc9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Dec 2022 18:42:56 +0100 Subject: [PATCH 31/61] Moved doctrine custom type definitions to Doctrine\Types namespace --- config/packages/doctrine.yaml | 6 +++--- src/{Helpers => Doctrine/Types}/BigDecimalType.php | 2 +- src/{Helpers => Doctrine/Types}/UTCDateTimeType.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/{Helpers => Doctrine/Types}/BigDecimalType.php (98%) rename src/{Helpers => Doctrine/Types}/UTCDateTimeType.php (98%) diff --git a/config/packages/doctrine.yaml b/config/packages/doctrine.yaml index f1435e9d..bc31bb8f 100644 --- a/config/packages/doctrine.yaml +++ b/config/packages/doctrine.yaml @@ -8,11 +8,11 @@ doctrine: types: datetime: - class: App\Helpers\UTCDateTimeType + class: App\Doctrine\Types\UTCDateTimeType date: - class: App\Helpers\UTCDateTimeType + class: App\Doctrine\Types\UTCDateTimeType big_decimal: - class: App\Helpers\BigDecimalType + class: App\Doctrine\Types\BigDecimalType schema_filter: ~^(?!internal)~ # Only enable this when needed diff --git a/src/Helpers/BigDecimalType.php b/src/Doctrine/Types/BigDecimalType.php similarity index 98% rename from src/Helpers/BigDecimalType.php rename to src/Doctrine/Types/BigDecimalType.php index 0471fb5d..f1522857 100644 --- a/src/Helpers/BigDecimalType.php +++ b/src/Doctrine/Types/BigDecimalType.php @@ -18,7 +18,7 @@ * along with this program. If not, see . */ -namespace App\Helpers; +namespace App\Doctrine\Types; use Brick\Math\BigDecimal; use Brick\Math\BigNumber; diff --git a/src/Helpers/UTCDateTimeType.php b/src/Doctrine/Types/UTCDateTimeType.php similarity index 98% rename from src/Helpers/UTCDateTimeType.php rename to src/Doctrine/Types/UTCDateTimeType.php index 9109ec8a..0dd7a461 100644 --- a/src/Helpers/UTCDateTimeType.php +++ b/src/Doctrine/Types/UTCDateTimeType.php @@ -20,7 +20,7 @@ declare(strict_types=1); -namespace App\Helpers; +namespace App\Doctrine\Types; use DateTime; use DateTimeZone; From 0e020dab748bd6229e1f9a79753972fc0a53aa42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 31 Dec 2022 18:57:36 +0100 Subject: [PATCH 32/61] Added an price field to allow defining the price of non-part BOM entries --- migrations/Version20221229125204.php | 39 ---------- migrations/Version20221231173322.php | 48 +++++++++++++ src/Entity/ProjectSystem/ProjectBOMEntry.php | 72 +++++++++++++++++++ .../ProjectSystem/ProjectBOMEntryType.php | 18 +++++ .../Form/collection_types_layout.html.twig | 11 +++ translations/messages.en.xlf | 6 ++ translations/validators.en.xlf | 6 ++ 7 files changed, 161 insertions(+), 39 deletions(-) delete mode 100644 migrations/Version20221229125204.php create mode 100644 migrations/Version20221231173322.php diff --git a/migrations/Version20221229125204.php b/migrations/Version20221229125204.php deleted file mode 100644 index d05471c5..00000000 --- a/migrations/Version20221229125204.php +++ /dev/null @@ -1,39 +0,0 @@ -addSql('ALTER TABLE device_parts ADD name VARCHAR(255) DEFAULT NULL, ADD comment LONGTEXT NOT NULL, ADD last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, ADD datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE quantity quantity DOUBLE PRECISION NOT NULL'); - $this->addSql('ALTER TABLE devices ADD status VARCHAR(64) NOT NULL, ADD description LONGTEXT NOT NULL'); - $this->addSql('ALTER TABLE parts ADD built_project_id INT DEFAULT NULL'); - $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES devices (id)'); - $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE devices DROP status, DROP description'); - $this->addSql('ALTER TABLE device_parts DROP name, DROP comment, DROP last_modified, DROP datetime_added, CHANGE quantity quantity INT NOT NULL'); - $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FEE8AE70D9'); - $this->addSql('DROP INDEX UNIQ_6940A7FEE8AE70D9 ON `parts`'); - $this->addSql('ALTER TABLE `parts` DROP built_project_id'); - } -} diff --git a/migrations/Version20221231173322.php b/migrations/Version20221231173322.php new file mode 100644 index 00000000..06fd0bd8 --- /dev/null +++ b/migrations/Version20221231173322.php @@ -0,0 +1,48 @@ +addSql('ALTER TABLE device_parts ADD price_currency_id INT DEFAULT NULL, ADD name VARCHAR(255) DEFAULT NULL, ADD comment LONGTEXT NOT NULL, ADD price NUMERIC(11, 5) DEFAULT NULL COMMENT \'(DC2Type:big_decimal)\', ADD last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, ADD datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CHANGE quantity quantity DOUBLE PRECISION NOT NULL'); + $this->addSql('ALTER TABLE device_parts ADD CONSTRAINT FK_AFC547993FFDCD60 FOREIGN KEY (price_currency_id) REFERENCES currencies (id)'); + $this->addSql('CREATE INDEX IDX_AFC547993FFDCD60 ON device_parts (price_currency_id)'); + $this->addSql('ALTER TABLE devices ADD status VARCHAR(64) DEFAULT NULL, ADD description LONGTEXT NOT NULL'); + $this->addSql('ALTER TABLE groups CHANGE permissions_data permissions_data LONGTEXT DEFAULT \'[]\' NOT NULL COMMENT \'(DC2Type:json)\''); + $this->addSql('ALTER TABLE parts ADD built_project_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE parts ADD CONSTRAINT FK_6940A7FEE8AE70D9 FOREIGN KEY (built_project_id) REFERENCES devices (id)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_6940A7FEE8AE70D9 ON parts (built_project_id)'); + $this->addSql('ALTER TABLE users CHANGE permissions_data permissions_data LONGTEXT DEFAULT \'[]\' NOT NULL COMMENT \'(DC2Type:json)\''); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE devices DROP status, DROP description'); + $this->addSql('ALTER TABLE device_parts DROP FOREIGN KEY FK_AFC547993FFDCD60'); + $this->addSql('DROP INDEX IDX_AFC547993FFDCD60 ON device_parts'); + $this->addSql('ALTER TABLE device_parts DROP price_currency_id, DROP name, DROP comment, DROP price, DROP last_modified, DROP datetime_added, CHANGE quantity quantity INT NOT NULL'); + $this->addSql('ALTER TABLE `groups` CHANGE permissions_data permissions_data LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\''); + $this->addSql('ALTER TABLE log CHANGE level level TINYINT(1) NOT NULL'); + $this->addSql('ALTER TABLE `parts` DROP FOREIGN KEY FK_6940A7FEE8AE70D9'); + $this->addSql('DROP INDEX UNIQ_6940A7FEE8AE70D9 ON `parts`'); + $this->addSql('ALTER TABLE `parts` DROP built_project_id'); + $this->addSql('ALTER TABLE `users` CHANGE permissions_data permissions_data LONGTEXT NOT NULL COMMENT \'(DC2Type:json)\''); + } +} diff --git a/src/Entity/ProjectSystem/ProjectBOMEntry.php b/src/Entity/ProjectSystem/ProjectBOMEntry.php index 80b0da7c..754283bf 100644 --- a/src/Entity/ProjectSystem/ProjectBOMEntry.php +++ b/src/Entity/ProjectSystem/ProjectBOMEntry.php @@ -25,6 +25,10 @@ namespace App\Entity\ProjectSystem; use App\Entity\Base\AbstractDBElement; use App\Entity\Base\TimestampTrait; use App\Entity\Parts\Part; +use App\Entity\PriceInformations\Currency; +use App\Validator\Constraints\BigDecimal\BigDecimalPositive; +use App\Validator\Constraints\Selectable; +use Brick\Math\BigDecimal; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -86,6 +90,29 @@ class ProjectBOMEntry extends AbstractDBElement */ protected ?Part $part = null; + /** + * @var BigDecimal The price of this non-part BOM entry + * @ORM\Column(type="big_decimal", precision=11, scale=5, nullable=true) + * @Assert\AtLeastOneOf({ + * @BigDecimalPositive(), + * @Assert\IsNull() + * }) + */ + protected ?BigDecimal $price; + + /** + * @var ?Currency The currency for the price of this non-part BOM entry + * @ORM\ManyToOne(targetEntity="App\Entity\PriceInformations\Currency") + * @ORM\JoinColumn(nullable=true) + * @Selectable() + */ + protected ?Currency $price_currency = null; + + public function __construct() + { + $this->price = BigDecimal::zero()->toScale(5); + } + /** * @return float */ @@ -196,6 +223,44 @@ class ProjectBOMEntry extends AbstractDBElement return $this; } + /** + * Returns the price of this BOM entry, if existing. + * Prices are only valid on non-Part BOM entries. + * @return BigDecimal|null + */ + public function getPrice(): ?BigDecimal + { + return $this->price; + } + + /** + * Sets the price of this BOM entry. + * Prices are only valid on non-Part BOM entries. + * @param BigDecimal|null $price + */ + public function setPrice(?BigDecimal $price): void + { + $this->price = $price; + } + + /** + * @return Currency|null + */ + public function getPriceCurrency(): ?Currency + { + return $this->price_currency; + } + + /** + * @param Currency|null $price_currency + */ + public function setPriceCurrency(?Currency $price_currency): void + { + $this->price_currency = $price_currency; + } + + + /** * @Assert\Callback */ @@ -231,6 +296,13 @@ class ProjectBOMEntry extends AbstractDBElement ->addViolation(); } + //Prices are only only allowed on non-part BOM entries + if ($this->part !== null && $this->price !== null) { + $context->buildViolation('project.bom_entry.price_not_allowed_on_parts') + ->atPath('price') + ->addViolation(); + } + //Check that the part is not the build representation part of this device or one of its parents if ($this->part && $this->part->getBuiltProject() !== null) { //Get the associated project diff --git a/src/Form/ProjectSystem/ProjectBOMEntryType.php b/src/Form/ProjectSystem/ProjectBOMEntryType.php index 99730375..49292235 100644 --- a/src/Form/ProjectSystem/ProjectBOMEntryType.php +++ b/src/Form/ProjectSystem/ProjectBOMEntryType.php @@ -4,6 +4,8 @@ namespace App\Form\ProjectSystem; use App\Entity\Parts\Part; use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Form\Type\BigDecimalNumberType; +use App\Form\Type\CurrencyEntityType; use App\Form\Type\PartSelectType; use App\Form\Type\RichTextEditorType; use App\Form\Type\SIUnitType; @@ -63,6 +65,22 @@ class ProjectBOMEntryType extends AbstractType 'rows' => 2, ], ]) + ->add('price', BigDecimalNumberType::class, [ + 'label' => false, + 'required' => false, + 'scale' => 5, + 'html5' => true, + 'attr' => [ + 'min' => 0, + 'step' => 'any', + ], + ]) + ->add('priceCurrency', CurrencyEntityType::class, [ + 'required' => false, + 'label' => false, + 'short' => true, + ]) + ; } diff --git a/templates/Form/collection_types_layout.html.twig b/templates/Form/collection_types_layout.html.twig index f2be11af..02b2090f 100644 --- a/templates/Form/collection_types_layout.html.twig +++ b/templates/Form/collection_types_layout.html.twig @@ -61,6 +61,17 @@
{{ form_row(form.mountnames) }} +
+ +
+
+ {{ form_widget(form.price) }} + {{ form_widget(form.priceCurrency, {'attr': {'class': 'selectpicker', 'data-controller': 'elements--selectpicker'}}) }} +
+ {{ form_errors(form.price) }} + {{ form_errors(form.priceCurrency) }} +
+
{{ form_row(form.comment) }}
diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 9be05717..9213bd48 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -10127,5 +10127,11 @@ Element 3 Subprojects + + + project.bom.price + Price + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index a1983bb2..cc38990f 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -275,5 +275,11 @@ The project BOM has to include all subprojects builds parts. Part %part_name% of project %project_name% missing! + + + project.bom_entry.price_not_allowed_on_parts + Prices are not allowed on BOM entries associated with a part. Define the price on the part instead. + + From ba4085d882e3949aedcff2f4f1d461e93b9e5de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 1 Jan 2023 13:21:50 +0100 Subject: [PATCH 33/61] Added a very basic modal on part info page for changing part instock Related to issue #201 --- .../pages/part_withdraw_modal_controller.js | 41 +++++ src/Controller/PartController.php | 58 +++++++ .../AdminPages/StorelocationAdminForm.php | 8 +- .../Parts/PartLotWithdrawAddHelper.php | 157 ++++++++++++++++++ templates/Parts/info/_part_lots.html.twig | 23 +++ .../Parts/info/_withdraw_modal.html.twig | 56 +++++++ .../Parts/PartLotWithdrawAddHelperTest.php | 149 +++++++++++++++++ 7 files changed, 488 insertions(+), 4 deletions(-) create mode 100644 assets/controllers/pages/part_withdraw_modal_controller.js create mode 100644 src/Services/Parts/PartLotWithdrawAddHelper.php create mode 100644 templates/Parts/info/_withdraw_modal.html.twig create mode 100644 tests/Services/Parts/PartLotWithdrawAddHelperTest.php diff --git a/assets/controllers/pages/part_withdraw_modal_controller.js b/assets/controllers/pages/part_withdraw_modal_controller.js new file mode 100644 index 00000000..f7e6e7a7 --- /dev/null +++ b/assets/controllers/pages/part_withdraw_modal_controller.js @@ -0,0 +1,41 @@ +import {Controller} from "@hotwired/stimulus"; +import {Modal} from "bootstrap"; + +export default class extends Controller +{ + connect() { + + + this.element.addEventListener('show.bs.modal', event => this._handleModalOpen(event)); + + //Register an event to remove the backdrop, when the form is submitted + const form = this.element.querySelector('form'); + form.addEventListener('submit', event => { + //Remove the backdrop + document.querySelector('.modal-backdrop').remove(); + }); + } + + _handleModalOpen(event) { + // Button that triggered the modal + const button = event.relatedTarget; + + const amountInput = this.element.querySelector('input[name="amount"]'); + + // Extract info from button attributes + const action = button.getAttribute('data-action'); + const lotID = button.getAttribute('data-lot-id'); + const lotAmount = button.getAttribute('data-lot-amount'); + + //Set the action and lotID inputs in the form + this.element.querySelector('input[name="action"]').setAttribute('value', action); + this.element.querySelector('input[name="lot_id"]').setAttribute('value', lotID); + + //For adding parts there is no limit on the amount to add + if (action == 'add') { + amountInput.removeAttribute('max'); + } else { //Every other action is limited to the amount of parts in the lot + amountInput.setAttribute('max', lotAmount); + } + } +} \ No newline at end of file diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index dc5b1bf8..7c626b8a 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -40,6 +40,7 @@ use App\Services\LogSystem\EventCommentHelper; use App\Services\LogSystem\HistoryHelper; use App\Services\LogSystem\TimeTravel; use App\Services\Parameters\ParameterExtractor; +use App\Services\Parts\PartLotWithdrawAddHelper; use App\Services\Parts\PricedetailHelper; use App\Services\ProjectSystem\ProjectBuildPartHelper; use DateTime; @@ -52,6 +53,7 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Contracts\Translation\TranslatorInterface; @@ -319,4 +321,60 @@ class PartController extends AbstractController 'form' => $form, ]); } + + /** + * @Route("/{id}/add_withdraw", name="part_add_withdraw", methods={"POST"}) + */ + public function withdrawAddHandler(Part $part, Request $request, EntityManagerInterface $em, PartLotWithdrawAddHelper $withdrawAddHelper): Response + { + if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) { + //Retrieve partlot from the request + $partLot = $em->find(PartLot::class, $request->request->get('lot_id')); + //Ensure that the partlot belongs to the part + if($partLot->getPart() !== $part) { + throw new \RuntimeException("The origin partlot does not belong to the part!"); + } + //Try to determine the target lot (used for move actions) + $targetLot = $em->find(PartLot::class, $request->request->get('target_id')); + if ($targetLot && $targetLot->getPart() !== $part) { + throw new \RuntimeException("The target partlot does not belong to the part!"); + } + + //Extract the amount and comment from the request + $amount = (float) $request->request->get('amount'); + $comment = $request->request->get('comment'); + $action = $request->request->get('action'); + + + + switch ($action) { + case "withdraw": + case "remove": + $withdrawAddHelper->withdraw($partLot, $amount, $comment); + break; + case "add": + $withdrawAddHelper->add($partLot, $amount, $comment); + break; + case "move": + $withdrawAddHelper->move($partLot, $targetLot, $amount, $comment); + break; + default: + throw new \RuntimeException("Unknown action!"); + } + + //Save the changes to the DB + $em->flush(); + $this->addFlash('success', 'part.withdraw.success'); + + } else { + $this->addFlash('error', 'CSRF Token invalid!'); + } + + //If an redirect was passed, then redirect there + if($request->request->get('_redirect')) { + return $this->redirect($request->request->get('_redirect')); + } + //Otherwise just redirect to the part page + return $this->redirectToRoute('part_info', ['id' => $part->getID()]); + } } diff --git a/src/Form/AdminPages/StorelocationAdminForm.php b/src/Form/AdminPages/StorelocationAdminForm.php index 9caf1169..8a85e4ec 100644 --- a/src/Form/AdminPages/StorelocationAdminForm.php +++ b/src/Form/AdminPages/StorelocationAdminForm.php @@ -38,21 +38,21 @@ class StorelocationAdminForm extends BaseEntityAdminForm 'required' => false, 'label' => 'storelocation.edit.is_full.label', 'help' => 'storelocation.edit.is_full.help', - 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'move', $entity), + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); $builder->add('limit_to_existing_parts', CheckboxType::class, [ 'required' => false, 'label' => 'storelocation.limit_to_existing.label', 'help' => 'storelocation.limit_to_existing.help', - 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'move', $entity), + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); $builder->add('only_single_part', CheckboxType::class, [ 'required' => false, 'label' => 'storelocation.only_single_part.label', 'help' => 'storelocation.only_single_part.help', - 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'move', $entity), + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); $builder->add('storage_type', StructuralEntityType::class, [ @@ -61,7 +61,7 @@ class StorelocationAdminForm extends BaseEntityAdminForm 'help' => 'storelocation.storage_type.help', 'class' => MeasurementUnit::class, 'disable_not_selectable' => true, - 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'move', $entity), + 'disabled' => !$this->security->isGranted($is_new ? 'create' : 'edit', $entity), ]); } } diff --git a/src/Services/Parts/PartLotWithdrawAddHelper.php b/src/Services/Parts/PartLotWithdrawAddHelper.php new file mode 100644 index 00000000..3fbfb074 --- /dev/null +++ b/src/Services/Parts/PartLotWithdrawAddHelper.php @@ -0,0 +1,157 @@ +isInstockUnknown()) { + return false; + } + + //So far all other restrictions are defined at the storelocation level + if($partLot->getStorageLocation() === null) { + return true; + } + + //We can not add parts if the storage location of the lot is marked as full + if($partLot->getStorageLocation()->isFull()) { + return false; + } + + return true; + } + + public function canWithdraw(PartLot $partLot): bool + { + //We cannot add or withdraw parts from lots with unknown instock value. + if ($partLot->isInstockUnknown()) { + return false; + } + + return true; + } + + /** + * Withdraw the specified amount of parts from the given part lot. + * Please note that the changes are not flushed to DB yet, you have to do this yourself + * @param PartLot $partLot The partLot from which the instock should be taken (which value should be decreased) + * @param float $amount The amount of parts that should be taken from the part lot + * @param string|null $comment The optional comment describing the reason for the withdrawal + * @return PartLot The modified part lot + */ + public function withdraw(PartLot $partLot, float $amount, ?string $comment = null): PartLot + { + //Ensure that amount is positive + if ($amount <= 0) { + throw new \InvalidArgumentException('Amount must be positive'); + } + + $part = $partLot->getPart(); + + //Check whether we have to round the amount + if (!$part->useFloatAmount()) { + $amount = round($amount); + } + + //Ensure that we can withdraw from the part lot + if (!$this->canWithdraw($partLot)) { + throw new \RuntimeException("Cannot withdraw from this part lot!"); + } + + //Ensure that there is enough stock to withdraw + if ($amount > $partLot->getAmount()) { + throw new \RuntimeException('Not enough stock to withdraw!'); + } + + //Subtract the amount from the part lot + $partLot->setAmount($partLot->getAmount() - $amount); + + return $partLot; + } + + /** + * Add the specified amount of parts to the given part lot. + * Please note that the changes are not flushed to DB yet, you have to do this yourself + * @param PartLot $partLot The partLot from which the instock should be taken (which value should be decreased) + * @param float $amount The amount of parts that should be taken from the part lot + * @param string|null $comment The optional comment describing the reason for the withdrawal + * @return PartLot The modified part lot + */ + public function add(PartLot $partLot, float $amount, ?string $comment = null): PartLot + { + if ($amount <= 0) { + throw new \InvalidArgumentException('Amount must be positive'); + } + + $part = $partLot->getPart(); + + //Check whether we have to round the amount + if (!$part->useFloatAmount()) { + $amount = round($amount); + } + + //Ensure that we can add to the part lot + if (!$this->canAdd($partLot)) { + throw new \RuntimeException("Cannot add to this part lot!"); + } + + //Subtract the amount from the part lot + $partLot->setAmount($partLot->getAmount() + $amount); + + return $partLot; + } + + /** + * Move the specified amount of parts from the given source part lot to the given target part lot. + * Please note that the changes are not flushed to DB yet, you have to do this yourself + * @param PartLot $origin The part lot from which the parts should be taken + * @param PartLot $target The part lot to which the parts should be added + * @param float $amount The amount of parts that should be moved + * @param string|null $comment A comment describing the reason for the move + * @return void + */ + public function move(PartLot $origin, PartLot $target, float $amount, ?string $comment = null): void + { + if ($amount <= 0) { + throw new \InvalidArgumentException('Amount must be positive'); + } + + $part = $origin->getPart(); + + //Ensure that both part lots belong to the same part + if($origin->getPart() !== $target->getPart()) { + throw new \RuntimeException("Cannot move instock between different parts!"); + } + + //Check whether we have to round the amount + if (!$part->useFloatAmount()) { + $amount = round($amount); + } + + //Ensure that we can withdraw from origin and add to target + if (!$this->canWithdraw($origin) || !$this->canAdd($target)) { + throw new \RuntimeException("Cannot move instock between these part lots!"); + } + + //Ensure that there is enough stock to withdraw + if ($amount > $origin->getAmount()) { + throw new \RuntimeException('Not enough stock to withdraw!'); + } + + //Subtract the amount from the part lot + $origin->setAmount($origin->getAmount() - $amount); + //And add it to the target + $target->setAmount($target->getAmount() + $amount); + } +} \ No newline at end of file diff --git a/templates/Parts/info/_part_lots.html.twig b/templates/Parts/info/_part_lots.html.twig index c28dc2ac..3742dac0 100644 --- a/templates/Parts/info/_part_lots.html.twig +++ b/templates/Parts/info/_part_lots.html.twig @@ -1,6 +1,8 @@ {% import "helper.twig" as helper %} {% import "LabelSystem/dropdown_macro.html.twig" as dropdown %} +{% include "Parts/info/_withdraw_modal.html.twig" %} + @@ -8,6 +10,7 @@ {# Tags row #} + {# Button row #} @@ -57,6 +60,26 @@ {% endif %} + diff --git a/templates/Parts/info/_withdraw_modal.html.twig b/templates/Parts/info/_withdraw_modal.html.twig new file mode 100644 index 00000000..0a039d4f --- /dev/null +++ b/templates/Parts/info/_withdraw_modal.html.twig @@ -0,0 +1,56 @@ + \ No newline at end of file diff --git a/tests/Services/Parts/PartLotWithdrawAddHelperTest.php b/tests/Services/Parts/PartLotWithdrawAddHelperTest.php new file mode 100644 index 00000000..f9fc323e --- /dev/null +++ b/tests/Services/Parts/PartLotWithdrawAddHelperTest.php @@ -0,0 +1,149 @@ +service = self::getContainer()->get(PartLotWithdrawAddHelper::class); + + $this->fillTestData(); + } + + private function fillTestData(): void + { + $this->part = new Part(); + + $this->storageLocation = new Storelocation(); + $this->full_storageLocation = new Storelocation(); + $this->full_storageLocation->setIsFull(true); + + $this->partLot1 = new PartLot(); + $this->partLot1->setPart($this->part); + $this->partLot1->setAmount(10); + + $this->partLot2 = new PartLot(); + $this->partLot2->setPart($this->part); + $this->partLot2->setStorageLocation($this->storageLocation); + $this->partLot2->setAmount(2); + + $this->partLot3 = new PartLot(); + $this->partLot3->setPart($this->part); + $this->partLot3->setAmount(0); + + $this->fullLot = new PartLot(); + $this->fullLot->setPart($this->part); + $this->fullLot->setAmount(45); + $this->fullLot->setStorageLocation($this->full_storageLocation); + + $this->lotWithUnknownInstock = new PartLot(); + $this->lotWithUnknownInstock->setPart($this->part); + $this->lotWithUnknownInstock->setAmount(5); + $this->lotWithUnknownInstock->setInstockUnknown(true); + $this->lotWithUnknownInstock->setStorageLocation($this->storageLocation); + } + + public function testCanWithdraw() + { + //Normal lots should be withdrawable + $this->assertTrue($this->service->canWithdraw($this->partLot1)); + $this->assertTrue($this->service->canWithdraw($this->partLot2)); + $this->assertTrue($this->service->canWithdraw($this->partLot3)); + + //Full lots should be withdrawable + $this->assertTrue($this->service->canWithdraw($this->fullLot)); + //Lots with unknown instock should not be withdrawable + $this->assertFalse($this->service->canWithdraw($this->lotWithUnknownInstock)); + } + + public function testCanAdd() + { + //Normal lots should be addable + $this->assertTrue($this->service->canAdd($this->partLot1)); + $this->assertTrue($this->service->canAdd($this->partLot2)); + $this->assertTrue($this->service->canAdd($this->partLot3)); + + //Full lots should not be addable + $this->assertFalse($this->service->canAdd($this->fullLot)); + //Lots with unknown instock should not be addable + $this->assertFalse($this->service->canAdd($this->lotWithUnknownInstock)); + } + + public function testAdd() + { + //Add 5 to lot 1 + $this->service->add($this->partLot1, 5, "Test"); + $this->assertEquals(15, $this->partLot1->getAmount()); + + //Add 3.2 to lot 2 + $this->service->add($this->partLot2, 3.2, "Test"); + $this->assertEquals(5, $this->partLot2->getAmount()); + + //Add 1.5 to lot 3 + $this->service->add($this->partLot3, 1.5, "Test"); + $this->assertEquals(2, $this->partLot3->getAmount()); + + } + + public function testWithdraw() + { + //Withdraw 5 from lot 1 + $this->service->withdraw($this->partLot1, 5, "Test"); + $this->assertEquals(5, $this->partLot1->getAmount()); + + //Withdraw 2.2 from lot 2 + $this->service->withdraw($this->partLot2, 2.2, "Test"); + $this->assertEquals(0, $this->partLot2->getAmount()); + } + + public function testMove() + { + //Move 5 from lot 1 to lot 2 + $this->service->move($this->partLot1, $this->partLot2, 5, "Test"); + $this->assertEquals(5, $this->partLot1->getAmount()); + $this->assertEquals(7, $this->partLot2->getAmount()); + + //Move 2.2 from lot 2 to lot 3 + $this->service->move($this->partLot2, $this->partLot3, 2.2, "Test"); + $this->assertEquals(5, $this->partLot2->getAmount()); + $this->assertEquals(2, $this->partLot3->getAmount()); + } +} From 436aff7533f2f571f02d0f91725c6e117e89a18b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sat, 7 Jan 2023 20:49:36 +0100 Subject: [PATCH 34/61] Improved user experience with part withdraw modal Related to issue #201 --- .../pages/part_withdraw_modal_controller.js | 32 ++++++++++ src/Controller/PartController.php | 6 +- .../Parts/PartLotWithdrawAddHelper.php | 5 ++ templates/Parts/info/_part_lots.html.twig | 19 +++--- .../Parts/info/_withdraw_modal.html.twig | 23 ++++--- translations/messages.en.xlf | 60 +++++++++++++++++++ 6 files changed, 128 insertions(+), 17 deletions(-) diff --git a/assets/controllers/pages/part_withdraw_modal_controller.js b/assets/controllers/pages/part_withdraw_modal_controller.js index f7e6e7a7..2fdba906 100644 --- a/assets/controllers/pages/part_withdraw_modal_controller.js +++ b/assets/controllers/pages/part_withdraw_modal_controller.js @@ -31,6 +31,38 @@ export default class extends Controller this.element.querySelector('input[name="action"]').setAttribute('value', action); this.element.querySelector('input[name="lot_id"]').setAttribute('value', lotID); + //Set the title + const titleElement = this.element.querySelector('.modal-title'); + switch (action) { + case 'withdraw': + titleElement.innerText = titleElement.getAttribute('data-withdraw'); + break; + case 'add': + titleElement.innerText = titleElement.getAttribute('data-add'); + break; + case 'move': + titleElement.innerText = titleElement.getAttribute('data-move'); + break; + } + + //Hide the move to lot select, if the action is not move (and unhide it, if it is) + const moveToLotSelect = this.element.querySelector('#withdraw-modal-move-to'); + if (action === 'move') { + moveToLotSelect.classList.remove('d-none'); + } else { + moveToLotSelect.classList.add('d-none'); + } + + //First unhide all move to lot options and then hide the currently selected lot + const moveToLotOptions = moveToLotSelect.querySelectorAll('input[type="radio"]'); + moveToLotOptions.forEach(option => option.parentElement.classList.remove('d-none')); + moveToLotOptions.forEach(option => { + if (option.getAttribute('value') === lotID) { + option.parentElement.classList.add('d-none'); + option.selected = false; + } + }); + //For adding parts there is no limit on the amount to add if (action == 'add') { amountInput.removeAttribute('max'); diff --git a/src/Controller/PartController.php b/src/Controller/PartController.php index 7c626b8a..125956d5 100644 --- a/src/Controller/PartController.php +++ b/src/Controller/PartController.php @@ -81,7 +81,7 @@ class PartController extends AbstractController * @throws Exception */ public function show(Part $part, Request $request, TimeTravel $timeTravel, HistoryHelper $historyHelper, - DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, ?string $timestamp = null): Response + DataTableFactory $dataTable, ParameterExtractor $parameterExtractor, PartLotWithdrawAddHelper $withdrawAddHelper, ?string $timestamp = null): Response { $this->denyAccessUnlessGranted('read', $part); @@ -122,6 +122,7 @@ class PartController extends AbstractController 'timeTravel' => $timeTravel_timestamp, 'description_params' => $parameterExtractor->extractParameters($part->getDescription()), 'comment_params' => $parameterExtractor->extractParameters($part->getComment()), + 'withdraw_add_helper' => $withdrawAddHelper, ] ); } @@ -330,6 +331,9 @@ class PartController extends AbstractController if ($this->isCsrfTokenValid('part_withraw' . $part->getID(), $request->request->get('_csfr'))) { //Retrieve partlot from the request $partLot = $em->find(PartLot::class, $request->request->get('lot_id')); + if($partLot === null) { + throw new \RuntimeException('Part lot not found!'); + } //Ensure that the partlot belongs to the part if($partLot->getPart() !== $part) { throw new \RuntimeException("The origin partlot does not belong to the part!"); diff --git a/src/Services/Parts/PartLotWithdrawAddHelper.php b/src/Services/Parts/PartLotWithdrawAddHelper.php index 3fbfb074..a757398d 100644 --- a/src/Services/Parts/PartLotWithdrawAddHelper.php +++ b/src/Services/Parts/PartLotWithdrawAddHelper.php @@ -39,6 +39,11 @@ class PartLotWithdrawAddHelper return false; } + //Part must contain more than 0 parts + if ($partLot->getAmount() <= 0) { + return false; + } + return true; } diff --git a/templates/Parts/info/_part_lots.html.twig b/templates/Parts/info/_part_lots.html.twig index 3742dac0..5663ccd8 100644 --- a/templates/Parts/info/_part_lots.html.twig +++ b/templates/Parts/info/_part_lots.html.twig @@ -63,18 +63,23 @@
{% trans %}part_lots.storage_location{% endtrans %} {% trans %}part_lots.amount{% endtrans %}
+
+ + + +
+
{{ dropdown.profile_dropdown('part_lot', lot.id, false) }}
- + diff --git a/templates/Parts/info/_withdraw_modal.html.twig b/templates/Parts/info/_withdraw_modal.html.twig index 0a039d4f..79ae2ea2 100644 --- a/templates/Parts/info/_withdraw_modal.html.twig +++ b/templates/Parts/info/_withdraw_modal.html.twig @@ -1,9 +1,13 @@ -
- {% trans %}part.info.add_part_to_project{% endtrans %} diff --git a/templates/Parts/info/_tools.html.twig b/templates/Parts/info/_tools.html.twig index b9d7e0d4..2c76062b 100644 --- a/templates/Parts/info/_tools.html.twig +++ b/templates/Parts/info/_tools.html.twig @@ -53,7 +53,7 @@ {{ dropdown.profile_dropdown('part', part.id) }} - {% trans %}part.info.add_part_to_project{% endtrans %} diff --git a/templates/Projects/info/_bom.html.twig b/templates/Projects/info/_bom.html.twig index ac19c530..42ffd015 100644 --- a/templates/Projects/info/_bom.html.twig +++ b/templates/Projects/info/_bom.html.twig @@ -4,7 +4,7 @@ {{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }} - {% trans %}project.info.bom_add_parts{% endtrans %} diff --git a/templates/components/datatables.macro.html.twig b/templates/components/datatables.macro.html.twig index d6aa19cd..0c5c170c 100644 --- a/templates/components/datatables.macro.html.twig +++ b/templates/components/datatables.macro.html.twig @@ -49,7 +49,7 @@ - + diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 4d0846f6..b85fcc88 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -6,7 +6,7 @@ ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')], ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], - ['devices', path('tree_device_root'), 'project.labelp', is_granted('@devices.read')], + ['devices', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], ['tools', path('tree_tools'), 'tools.label', true], ] %} diff --git a/tests/Entity/UserSystem/PermissionDataTest.php b/tests/Entity/UserSystem/PermissionDataTest.php index 681c5082..5107c2a0 100644 --- a/tests/Entity/UserSystem/PermissionDataTest.php +++ b/tests/Entity/UserSystem/PermissionDataTest.php @@ -158,4 +158,61 @@ class PermissionDataTest extends TestCase $data->setSchemaVersion(12345); $this->assertEquals(12345, $data->getSchemaVersion()); } + + public function testIsAnyOperationOfPermissionSet() + { + $data = new PermissionData(); + + //Initially no operation of any permission is set + $this->assertFalse($data->isAnyOperationOfPermissionSet('perm1')); + + $data->setPermissionValue('perm1', 'op1', PermissionData::ALLOW); + $this->assertTrue($data->isAnyOperationOfPermissionSet('perm1')); + } + + public function testGetAllDefinedOperationsOfPermission() + { + $data = new PermissionData(); + + $this->assertEmpty($data->getAllDefinedOperationsOfPermission('perm1')); + + $data->setPermissionValue('perm1', 'op1', PermissionData::ALLOW); + $data->setPermissionValue('perm1', 'op2', PermissionData::DISALLOW); + + $this->assertEquals([ + 'op1' => PermissionData::ALLOW, 'op2' => PermissionData::DISALLOW, + ], + $data->getAllDefinedOperationsOfPermission('perm1')); + } + + public function testSetAllOperationsOfPermission() + { + $data = new PermissionData(); + + $data->setAllOperationsOfPermission('perm1', [ + 'op1' => PermissionData::ALLOW, + 'op2' => PermissionData::DISALLOW, + ]); + + $this->assertEquals([ + 'op1' => PermissionData::ALLOW, 'op2' => PermissionData::DISALLOW, + ], + $data->getAllDefinedOperationsOfPermission('perm1')); + } + + public function testRemovePermission() + { + $data = new PermissionData(); + + $data->setPermissionValue('perm1', 'op1', PermissionData::ALLOW); + $data->setPermissionValue('perm1', 'op2', PermissionData::DISALLOW); + + $this->assertTrue($data->isPermissionSet('perm1', 'op1')); + $this->assertTrue($data->isPermissionSet('perm1', 'op2')); + + $data->removePermission('perm1'); + + $this->assertFalse($data->isPermissionSet('perm1', 'op1')); + $this->assertFalse($data->isPermissionSet('perm1', 'op2')); + } } diff --git a/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php b/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php index 3ed48cf5..79abb89c 100644 --- a/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php +++ b/tests/Services/UserSystem/PermissionSchemaUpdaterTest.php @@ -97,4 +97,20 @@ class PermissionSchemaUpdaterTest extends WebTestCase self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('parts_stock', 'add')); self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('parts_stock', 'withdraw')); } + + public function testUpgradeSchemaToVersion2() + { + $perm_data = new PermissionData(); + $perm_data->setSchemaVersion(1); + $perm_data->setPermissionValue('devices', 'read', PermissionData::ALLOW); + $perm_data->setPermissionValue('devices', 'edit', PermissionData::INHERIT); + $perm_data->setPermissionValue('devices', 'delete', PermissionData::DISALLOW); + $user = new TestPermissionHolder($perm_data); + + //After the upgrade all operations should be available under the name "projects" with the same values + self::assertTrue($this->service->upgradeSchema($user, 2)); + self::assertEquals(PermissionData::ALLOW, $user->getPermissions()->getPermissionValue('projects', 'read')); + self::assertEquals(PermissionData::INHERIT, $user->getPermissions()->getPermissionValue('projects', 'edit')); + self::assertEquals(PermissionData::DISALLOW, $user->getPermissions()->getPermissionValue('projects', 'delete')); + } } diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 36b60369..aadc374c 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -8111,14 +8111,14 @@ Element 3 Hersteller - + obsolete obsolete - perm.part.devices - Baugruppen + perm.projects + Projekte diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 5653956e..d66f855a 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -3284,7 +3284,7 @@ Sub elements will be moved upwards.]]> statistics.devices_count - Number of devices + Number of projects @@ -8112,14 +8112,14 @@ Element 3 Manufacturers - + obsolete obsolete - perm.part.devices - Devices + perm.projects + Projects From 751cdc603a645f1aa1ebae96087464825c8183af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 20:14:23 +0100 Subject: [PATCH 54/61] Added comment to the element change log entries on permission schema change. --- src/Command/User/UpgradePermissionsSchemaCommand.php | 7 ++++++- .../UserSystem/UpgradePermissionsSchemaSubscriber.php | 6 +++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Command/User/UpgradePermissionsSchemaCommand.php b/src/Command/User/UpgradePermissionsSchemaCommand.php index a4a02e00..ae5adfff 100644 --- a/src/Command/User/UpgradePermissionsSchemaCommand.php +++ b/src/Command/User/UpgradePermissionsSchemaCommand.php @@ -23,6 +23,7 @@ namespace App\Command\User; use App\Entity\UserSystem\Group; use App\Entity\UserSystem\PermissionData; use App\Entity\UserSystem\User; +use App\Services\LogSystem\EventCommentHelper; use App\Services\UserSystem\PermissionSchemaUpdater; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Command\Command; @@ -39,12 +40,14 @@ final class UpgradePermissionsSchemaCommand extends Command private PermissionSchemaUpdater $permissionSchemaUpdater; private EntityManagerInterface $em; + private EventCommentHelper $eventCommentHelper; - public function __construct(PermissionSchemaUpdater $permissionSchemaUpdater, EntityManagerInterface $entityManager) + public function __construct(PermissionSchemaUpdater $permissionSchemaUpdater, EntityManagerInterface $entityManager, EventCommentHelper $eventCommentHelper) { parent::__construct(self::$defaultName); $this->permissionSchemaUpdater = $permissionSchemaUpdater; + $this->eventCommentHelper = $eventCommentHelper; $this->em = $entityManager; } @@ -112,6 +115,8 @@ final class UpgradePermissionsSchemaCommand extends Command $this->permissionSchemaUpdater->upgradeSchema($user); } + $this->eventCommentHelper->setMessage('Manual permissions schema update via CLI'); + //Write changes to database $this->em->flush(); diff --git a/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php b/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php index cda163c4..7d891df0 100644 --- a/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php +++ b/src/EventSubscriber/UserSystem/UpgradePermissionsSchemaSubscriber.php @@ -21,6 +21,7 @@ namespace App\EventSubscriber\UserSystem; use App\Entity\UserSystem\User; +use App\Services\LogSystem\EventCommentHelper; use App\Services\UserSystem\PermissionSchemaUpdater; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; @@ -38,13 +39,15 @@ class UpgradePermissionsSchemaSubscriber implements EventSubscriberInterface private PermissionSchemaUpdater $permissionSchemaUpdater; private EntityManagerInterface $entityManager; private FlashBagInterface $flashBag; + private EventCommentHelper $eventCommentHelper; - public function __construct(Security $security, PermissionSchemaUpdater $permissionSchemaUpdater, EntityManagerInterface $entityManager, FlashBagInterface $flashBag) + public function __construct(Security $security, PermissionSchemaUpdater $permissionSchemaUpdater, EntityManagerInterface $entityManager, FlashBagInterface $flashBag, EventCommentHelper $eventCommentHelper) { $this->security = $security; $this->permissionSchemaUpdater = $permissionSchemaUpdater; $this->entityManager = $entityManager; $this->flashBag = $flashBag; + $this->eventCommentHelper = $eventCommentHelper; } public function onRequest(RequestEvent $event): void @@ -60,6 +63,7 @@ class UpgradePermissionsSchemaSubscriber implements EventSubscriberInterface } if ($this->permissionSchemaUpdater->isSchemaUpdateNeeded($user)) { + $this->eventCommentHelper->setMessage('Automatic permission schema update'); $this->permissionSchemaUpdater->userUpgradeSchemaRecursively($user); $this->entityManager->flush(); $this->flashBag->add('notice', 'user.permissions_schema_updated'); From 011bda3600eeb10432ab7777c9e767e3f8ccb798 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 22:05:06 +0100 Subject: [PATCH 55/61] Rename indices instead of dropping and creating them again This should hopefully fix the failing migration on test action. --- migrations/Version20230108165410.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/migrations/Version20230108165410.php b/migrations/Version20230108165410.php index 68e3d095..637ab437 100644 --- a/migrations/Version20230108165410.php +++ b/migrations/Version20230108165410.php @@ -44,10 +44,8 @@ final class Version20230108165410 extends AbstractMultiPlatformMigration $this->addSql('ALTER TABLE projects DROP FOREIGN KEY devices_parent_id_fk'); } $this->addSql('ALTER TABLE projects ADD status VARCHAR(64) DEFAULT NULL, ADD description LONGTEXT NOT NULL'); - $this->addSql('DROP INDEX idx_11074e9a727aca70 ON projects'); - $this->addSql('CREATE INDEX IDX_5C93B3A4727ACA70 ON projects (parent_id)'); - $this->addSql('DROP INDEX idx_11074e9a6dedcec2 ON projects'); - $this->addSql('CREATE INDEX IDX_5C93B3A46DEDCEC2 ON projects (id_preview_attachement)'); + $this->addSql('ALTER TABLE projects RENAME INDEX idx_11074e9a727aca70 TO IDX_5C93B3A4727ACA70'); + $this->addSql('ALTER TABLE projects RENAME INDEX idx_11074e9a6dedcec2 TO IDX_5C93B3A46DEDCEC2'); $this->addSql('ALTER TABLE projects ADD CONSTRAINT FK_11074E9A6DEDCEC2 FOREIGN KEY (id_preview_attachement) REFERENCES attachments (id)'); $this->addSql('ALTER TABLE projects ADD CONSTRAINT FK_5C93B3A4727ACA70 FOREIGN KEY (parent_id) REFERENCES projects (id)'); } From cad120617531b08373dbad0f478fd1c1efb30fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 22:44:32 +0100 Subject: [PATCH 56/61] Fixed phpunit tests --- src/Entity/UserSystem/PermissionData.php | 5 +++++ .../Parts/PartLotWithdrawAddHelperTest.php | 21 +++++++++++++------ .../UserSystem/PermissionManagerTest.php | 6 ------ 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/Entity/UserSystem/PermissionData.php b/src/Entity/UserSystem/PermissionData.php index 2d3292a4..0ca9cff3 100644 --- a/src/Entity/UserSystem/PermissionData.php +++ b/src/Entity/UserSystem/PermissionData.php @@ -211,6 +211,11 @@ final class PermissionData implements \JsonSerializable //Filter out all empty or null values foreach ($this->data as $permission => $operations) { + //Skip non-array values + if (!is_array($operations)) { + continue; + } + $ret[$permission] = array_filter($operations, function ($value) { return $value !== null; }); diff --git a/tests/Services/Parts/PartLotWithdrawAddHelperTest.php b/tests/Services/Parts/PartLotWithdrawAddHelperTest.php index f9fc323e..b2f8a2f3 100644 --- a/tests/Services/Parts/PartLotWithdrawAddHelperTest.php +++ b/tests/Services/Parts/PartLotWithdrawAddHelperTest.php @@ -10,6 +10,14 @@ use App\Services\Parts\PartLotWithdrawAddHelper; use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; +class TestPartLot extends PartLot +{ + public function getID(): ?int + { + return 2; + } +} + class PartLotWithdrawAddHelperTest extends WebTestCase { @@ -56,25 +64,25 @@ class PartLotWithdrawAddHelperTest extends WebTestCase $this->full_storageLocation = new Storelocation(); $this->full_storageLocation->setIsFull(true); - $this->partLot1 = new PartLot(); + $this->partLot1 = new TestPartLot(); $this->partLot1->setPart($this->part); $this->partLot1->setAmount(10); - $this->partLot2 = new PartLot(); + $this->partLot2 = new TestPartLot(); $this->partLot2->setPart($this->part); $this->partLot2->setStorageLocation($this->storageLocation); $this->partLot2->setAmount(2); - $this->partLot3 = new PartLot(); + $this->partLot3 = new TestPartLot(); $this->partLot3->setPart($this->part); $this->partLot3->setAmount(0); - $this->fullLot = new PartLot(); + $this->fullLot = new TestPartLot(); $this->fullLot->setPart($this->part); $this->fullLot->setAmount(45); $this->fullLot->setStorageLocation($this->full_storageLocation); - $this->lotWithUnknownInstock = new PartLot(); + $this->lotWithUnknownInstock = new TestPartLot(); $this->lotWithUnknownInstock->setPart($this->part); $this->lotWithUnknownInstock->setAmount(5); $this->lotWithUnknownInstock->setInstockUnknown(true); @@ -86,7 +94,8 @@ class PartLotWithdrawAddHelperTest extends WebTestCase //Normal lots should be withdrawable $this->assertTrue($this->service->canWithdraw($this->partLot1)); $this->assertTrue($this->service->canWithdraw($this->partLot2)); - $this->assertTrue($this->service->canWithdraw($this->partLot3)); + //Empty lots should not be withdrawable + $this->assertFalse($this->service->canWithdraw($this->partLot3)); //Full lots should be withdrawable $this->assertTrue($this->service->canWithdraw($this->fullLot)); diff --git a/tests/Services/UserSystem/PermissionManagerTest.php b/tests/Services/UserSystem/PermissionManagerTest.php index cca721ce..78ce2850 100644 --- a/tests/Services/UserSystem/PermissionManagerTest.php +++ b/tests/Services/UserSystem/PermissionManagerTest.php @@ -165,12 +165,6 @@ class PermissionManagerTest extends WebTestCase //Check for inherit from group $this->assertTrue($this->service->inherit($this->user, 'parts', 'show_history')); $this->assertFalse($this->service->inherit($this->user, 'parts', 'delete')); - $this->assertNull($this->service->inherit($this->user, 'parts', 'search')); - - //Check for inherit from group and parent group - $this->assertTrue($this->service->inherit($this->user, 'parts', 'all_parts')); - $this->assertFalse($this->service->inherit($this->user, 'parts', 'no_price_parts')); - $this->assertNull($this->service->inherit($this->user, 'parts', 'obsolete_parts')); //Test for user without group $this->assertTrue($this->service->inherit($this->user_withoutGroup, 'parts', 'read')); From 1a3253b31af020299c3a2ab4155aab4692d8d376 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 22:53:32 +0100 Subject: [PATCH 57/61] Fixed permission schema migration for PHP < 8.1 --- src/Services/UserSystem/PermissionSchemaUpdater.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Services/UserSystem/PermissionSchemaUpdater.php b/src/Services/UserSystem/PermissionSchemaUpdater.php index b4320c48..e8ebc6d0 100644 --- a/src/Services/UserSystem/PermissionSchemaUpdater.php +++ b/src/Services/UserSystem/PermissionSchemaUpdater.php @@ -66,6 +66,8 @@ class PermissionSchemaUpdater $reflectionClass = new \ReflectionClass(self::class); try { $method = $reflectionClass->getMethod('upgradeSchemaToVersion'.($n + 1)); + //Set the method accessible, so we can call it (needed for PHP < 8.1) + $method->setAccessible(true); $method->invoke($this, $holder); } catch (\ReflectionException $e) { throw new \RuntimeException('Could not find update method for schema version '.($n + 1)); From 7b536fc4ad1a76d4725f45a2fab71b8cdc5baeaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 23:07:25 +0100 Subject: [PATCH 58/61] Renamed device_* routes to project_* routes --- .../AdminPages/ProjectAdminController.php | 18 +++++++++--------- src/Controller/ProjectController.php | 2 +- src/Services/EntityURLGenerator.php | 10 +++++----- src/Services/Trees/ToolsTreeBuilder.php | 2 +- ...eAdmin.html.twig => ProjectAdmin.html.twig} | 0 5 files changed, 16 insertions(+), 16 deletions(-) rename templates/AdminPages/{DeviceAdmin.html.twig => ProjectAdmin.html.twig} (100%) diff --git a/src/Controller/AdminPages/ProjectAdminController.php b/src/Controller/AdminPages/ProjectAdminController.php index 736d1302..982c5b6c 100644 --- a/src/Controller/AdminPages/ProjectAdminController.php +++ b/src/Controller/AdminPages/ProjectAdminController.php @@ -37,19 +37,19 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; /** - * @Route("/device") + * @Route("/project") */ class ProjectAdminController extends BaseAdminController { protected $entity_class = Project::class; - protected $twig_template = 'AdminPages/DeviceAdmin.html.twig'; + protected $twig_template = 'AdminPages/ProjectAdmin.html.twig'; protected $form_class = ProjectAdminForm::class; - protected $route_base = 'device'; + protected $route_base = 'project'; protected $attachment_class = ProjectAttachment::class; protected $parameter_class = ProjectParameter::class; /** - * @Route("/{id}", name="device_delete", methods={"DELETE"}) + * @Route("/{id}", name="project_delete", methods={"DELETE"}) */ public function delete(Request $request, Project $entity, StructuralElementRecursionHelper $recursionHelper): RedirectResponse { @@ -57,8 +57,8 @@ class ProjectAdminController extends BaseAdminController } /** - * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="device_edit") - * @Route("/{id}", requirements={"id"="\d+"}) + * @Route("/{id}/edit/{timestamp}", requirements={"id"="\d+"}, name="project_edit") + * @Route("/{id}/edit", requirements={"id"="\d+"}) */ public function edit(Project $entity, Request $request, EntityManagerInterface $em, ?string $timestamp = null): Response { @@ -66,7 +66,7 @@ class ProjectAdminController extends BaseAdminController } /** - * @Route("/new", name="device_new") + * @Route("/new", name="project_new") * @Route("/{id}/clone", name="device_clone") * @Route("/") */ @@ -76,7 +76,7 @@ class ProjectAdminController extends BaseAdminController } /** - * @Route("/export", name="device_export_all") + * @Route("/export", name="project_export_all") */ public function exportAll(EntityManagerInterface $em, EntityExporter $exporter, Request $request): Response { @@ -84,7 +84,7 @@ class ProjectAdminController extends BaseAdminController } /** - * @Route("/{id}/export", name="device_export") + * @Route("/{id}/export", name="project_export") */ public function exportEntity(Project $entity, EntityExporter $exporter, Request $request): Response { diff --git a/src/Controller/ProjectController.php b/src/Controller/ProjectController.php index 4a78b784..f4e385cb 100644 --- a/src/Controller/ProjectController.php +++ b/src/Controller/ProjectController.php @@ -50,7 +50,7 @@ class ProjectController extends AbstractController } /** - * @Route("/{id}", name="project_info", requirements={"id"="\d+"}) + * @Route("/{id}/info", name="project_info", requirements={"id"="\d+"}) */ public function info(Project $project, Request $request) { diff --git a/src/Services/EntityURLGenerator.php b/src/Services/EntityURLGenerator.php index c84ee1c5..1ed659e4 100644 --- a/src/Services/EntityURLGenerator.php +++ b/src/Services/EntityURLGenerator.php @@ -113,7 +113,7 @@ class EntityURLGenerator //As long we does not have own things for it use edit page AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', - Project::class => 'device_edit', + Project::class => 'project_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', Storelocation::class => 'store_location_edit', @@ -204,7 +204,7 @@ class EntityURLGenerator //As long we does not have own things for it use edit page AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', - Project::class => 'device_edit', + Project::class => 'project_info', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', Storelocation::class => 'store_location_edit', @@ -234,7 +234,7 @@ class EntityURLGenerator Part::class => 'part_edit', AttachmentType::class => 'attachment_type_edit', Category::class => 'category_edit', - Project::class => 'device_edit', + Project::class => 'project_edit', Supplier::class => 'supplier_edit', Manufacturer::class => 'manufacturer_edit', Storelocation::class => 'store_location_edit', @@ -264,7 +264,7 @@ class EntityURLGenerator Part::class => 'part_new', AttachmentType::class => 'attachment_type_new', Category::class => 'category_new', - Project::class => 'device_new', + Project::class => 'project_new', Supplier::class => 'supplier_new', Manufacturer::class => 'manufacturer_new', Storelocation::class => 'store_location_new', @@ -340,7 +340,7 @@ class EntityURLGenerator Part::class => 'part_delete', AttachmentType::class => 'attachment_type_delete', Category::class => 'category_delete', - Project::class => 'device_delete', + Project::class => 'project_delete', Supplier::class => 'supplier_delete', Manufacturer::class => 'manufacturer_delete', Storelocation::class => 'store_location_delete', diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 596d020d..d0384911 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -159,7 +159,7 @@ class ToolsTreeBuilder if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( $this->translator->trans('tree.tools.edit.projects'), - $this->urlGenerator->generate('device_new') + $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } if ($this->security->isGranted('read', new Supplier())) { diff --git a/templates/AdminPages/DeviceAdmin.html.twig b/templates/AdminPages/ProjectAdmin.html.twig similarity index 100% rename from templates/AdminPages/DeviceAdmin.html.twig rename to templates/AdminPages/ProjectAdmin.html.twig From 937f1ab38b7931fd45a91da2a2e5662a320d0c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 23:12:06 +0100 Subject: [PATCH 59/61] Show master picture attachment in poject info page, when existing --- templates/Projects/info/_info.html.twig | 8 +++++++- templates/Projects/info/info.html.twig | 10 ---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/templates/Projects/info/_info.html.twig b/templates/Projects/info/_info.html.twig index fb8c70be..c2d36d67 100644 --- a/templates/Projects/info/_info.html.twig +++ b/templates/Projects/info/_info.html.twig @@ -4,7 +4,13 @@

{{ project.name }} diff --git a/templates/Projects/info/info.html.twig b/templates/Projects/info/info.html.twig index f93c1d52..ce920e9e 100644 --- a/templates/Projects/info/info.html.twig +++ b/templates/Projects/info/info.html.twig @@ -9,16 +9,6 @@ {{ helper.breadcrumb_entity_link(project) }} {{ parent() }} - - {# {% include "Projects/_info_card.html.twig" %} #} - - {# {{ datatables.datatable(datatable, 'elements/datatables/datatables', 'projects') }} #} - - - {# {% include "Parts/lists/_action_bar.html.twig" with {'url_options': {'category': entity.iD}} %} - - {% include "Parts/lists/_parts_list.html.twig" %} #} - {% endblock %} {% block card_title %} From de4c25ac0edb30b5bad476a8171581451233cd3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 23:14:25 +0100 Subject: [PATCH 60/61] Added info link to project edit page --- templates/AdminPages/ProjectAdmin.html.twig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/templates/AdminPages/ProjectAdmin.html.twig b/templates/AdminPages/ProjectAdmin.html.twig index 7e372b28..384f91d2 100644 --- a/templates/AdminPages/ProjectAdmin.html.twig +++ b/templates/AdminPages/ProjectAdmin.html.twig @@ -18,6 +18,14 @@ {% endblock %} +{% block quick_links %} +
+
+ +
+
+{% endblock %} + {% block additional_controls %} {{ form_row(form.description) }} {{ form_row(form.status) }} From d34dbbbb38daa99db467c1ac91f6ca474a88c279 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 8 Jan 2023 23:22:53 +0100 Subject: [PATCH 61/61] Improved tests for project system --- tests/ApplicationAvailabilityFunctionalTest.php | 5 +++++ tests/Controller/AdminPages/AbstractAdminControllerTest.php | 2 +- .../{DeviceControllerTest.php => ProjectControllerTest.php} | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) rename tests/Controller/AdminPages/{DeviceControllerTest.php => ProjectControllerTest.php} (89%) diff --git a/tests/ApplicationAvailabilityFunctionalTest.php b/tests/ApplicationAvailabilityFunctionalTest.php index b9ab07b1..18fac229 100644 --- a/tests/ApplicationAvailabilityFunctionalTest.php +++ b/tests/ApplicationAvailabilityFunctionalTest.php @@ -126,5 +126,10 @@ class ApplicationAvailabilityFunctionalTest extends WebTestCase //Webauthn Register yield ['/webauthn/register']; + + //Projects + yield ['/project/1/info']; + yield ['/project/1/add_parts']; + yield ['/project/1/add_parts?parts=1,2']; } } diff --git a/tests/Controller/AdminPages/AbstractAdminControllerTest.php b/tests/Controller/AdminPages/AbstractAdminControllerTest.php index 47718697..532f0e92 100644 --- a/tests/Controller/AdminPages/AbstractAdminControllerTest.php +++ b/tests/Controller/AdminPages/AbstractAdminControllerTest.php @@ -92,7 +92,7 @@ abstract class AbstractAdminControllerTest extends WebTestCase } //Test read/list access by access /new overview page - $client->request('GET', static::$base_path.'/1'); + $client->request('GET', static::$base_path.'/1/edit'); $this->assertFalse($client->getResponse()->isRedirect()); $this->assertSame($read, $client->getResponse()->isSuccessful(), 'Controller was not successful!'); $this->assertSame($read, !$client->getResponse()->isForbidden(), 'Permission Checking not working!'); diff --git a/tests/Controller/AdminPages/DeviceControllerTest.php b/tests/Controller/AdminPages/ProjectControllerTest.php similarity index 89% rename from tests/Controller/AdminPages/DeviceControllerTest.php rename to tests/Controller/AdminPages/ProjectControllerTest.php index be078ebf..586c1b93 100644 --- a/tests/Controller/AdminPages/DeviceControllerTest.php +++ b/tests/Controller/AdminPages/ProjectControllerTest.php @@ -29,8 +29,8 @@ use App\Entity\ProjectSystem\Project; * @group slow * @group DB */ -class DeviceControllerTest extends AbstractAdminControllerTest +class ProjectControllerTest extends AbstractAdminControllerTest { - protected static $base_path = '/en'.'/device'; + protected static $base_path = '/en'.'/project'; protected static $entity_class = Project::class; }