Added functions to retrieve associated parts of an element within twig labels

This fixes #1239
This commit is contained in:
Jan Böhmer 2026-02-15 13:52:56 +01:00
parent 233c5e8550
commit aed2652f1d
3 changed files with 60 additions and 13 deletions

View file

@ -91,7 +91,7 @@ in [official documentation](https://twig.symfony.com/doc/3.x/).
Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create Twig allows you for much more complex and dynamic label generation. You can use loops, conditions, and functions to create
the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment, the label content and you can access almost all data Part-DB offers. The label templates are evaluated in a special sandboxed environment,
where only certain operations are allowed. Only read access to entities is allowed. However as it circumvents Part-DB normal permission system, where only certain operations are allowed. Only read access to entities is allowed. However, as it circumvents Part-DB normal permission system,
the twig mode is only available to users with the "Twig mode" permission. the twig mode is only available to users with the "Twig mode" permission.
The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` ( The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` (
@ -99,10 +99,10 @@ or `{% raw %}{{ variable.property }}{% endraw %}`):
| Variable name | Description | | Variable name | Description |
|--------------------------------------------|--------------------------------------------------------------------------------------| |--------------------------------------------|--------------------------------------------------------------------------------------|
| `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. | | `{% raw %}{{ element }}{% endraw %}` | The target element, selected in label dialog. |
| `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in | | `{% raw %}{{ user }}{% endraw %}` | The current logged in user. Null if you are not logged in |
| `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). | | `{% raw %}{{ install_title }}{% endraw %}` | The name of the current Part-DB instance (similar to [[INSTALL_NAME]] placeholder). |
| `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated | | `{% raw %}{{ page }}{% endraw %}` | The page number (the nth-element for which the label is generated ) |
| `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels | | `{% raw %}{{ last_page }}{% endraw %}` | The page number of the last element. Equals the number of all pages / element labels |
| `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm | | `{% raw %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm |
| `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm | | `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm |
@ -236,12 +236,16 @@ certain data:
#### Functions #### Functions
| Function name | Description | | Function name | Description |
|----------------------------------------------|-----------------------------------------------------------------------------------------------| |------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `placeholder(placeholder, element)` | Get the value of a placeholder of an element | | `placeholder(placeholder, element)` | Get the value of a placeholder of an element |
| `entity_type(element)` | Get the type of an entity as string | | `entity_type(element)` | Get the type of an entity as string |
| `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) | | `entity_url(element, type)` | Get the URL to a specific entity type page (e.g. `info`, `edit`, etc.) |
| `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. | | `barcode_svg(content, type)` | Generate a barcode SVG from the content and type (e.g. `QRCODE`, `CODE128` etc.). A svg string is returned, which you need to data uri encode to inline it. |
| `associated_parts(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. Only the directly associated parts are returned |
| `associated_parts_r(element)` | Get the associated parts of an element like a storagelocation, footprint, etc. including all sub-entities recursively (e.g. sub-locations) |
| `associated_parts_count(element)` | Get the count of associated parts of an element like a storagelocation, footprint, excluding sub-entities |
| `associated_parts_count_r(element)` | Get the count of associated parts of an element like a storagelocation, footprint, including all sub-entities recursively (e.g. sub-locations) |
### Filters ### Filters

View file

@ -114,6 +114,7 @@ final class SandboxedTwigFactory
'barcode_svg', 'barcode_svg',
//SandboxedLabelExtension //SandboxedLabelExtension
'placeholder', 'placeholder',
'associated_parts', 'associated_parts_count', 'associated_parts_r', 'associated_parts_count_r',
]; ];
private const ALLOWED_METHODS = [ private const ALLOWED_METHODS = [

View file

@ -23,14 +23,18 @@ declare(strict_types=1);
namespace App\Twig\Sandbox; namespace App\Twig\Sandbox;
use App\Entity\Base\AbstractPartsContainingDBElement;
use App\Entity\Parts\Part;
use App\Repository\AbstractPartsContainingRepository;
use App\Services\LabelSystem\LabelTextReplacer; use App\Services\LabelSystem\LabelTextReplacer;
use Doctrine\ORM\EntityManagerInterface;
use Twig\Extension\AbstractExtension; use Twig\Extension\AbstractExtension;
use Twig\TwigFilter; use Twig\TwigFilter;
use Twig\TwigFunction; use Twig\TwigFunction;
class SandboxedLabelExtension extends AbstractExtension class SandboxedLabelExtension extends AbstractExtension
{ {
public function __construct(private readonly LabelTextReplacer $labelTextReplacer) public function __construct(private readonly LabelTextReplacer $labelTextReplacer, private readonly EntityManagerInterface $em)
{ {
} }
@ -39,6 +43,11 @@ class SandboxedLabelExtension extends AbstractExtension
{ {
return [ return [
new TwigFunction('placeholder', fn(string $text, object $label_target) => $this->labelTextReplacer->handlePlaceholderOrReturnNull($text, $label_target)), new TwigFunction('placeholder', fn(string $text, object $label_target) => $this->labelTextReplacer->handlePlaceholderOrReturnNull($text, $label_target)),
new TwigFunction("associated_parts", $this->associatedParts(...)),
new TwigFunction("associated_parts_count", $this->associatedPartsCount(...)),
new TwigFunction("associated_parts_r", $this->associatedPartsRecursive(...)),
new TwigFunction("associated_parts_count_r", $this->associatedPartsCountRecursive(...)),
]; ];
} }
@ -48,4 +57,37 @@ class SandboxedLabelExtension extends AbstractExtension
new TwigFilter('placeholders', fn(string $text, object $label_target) => $this->labelTextReplacer->replace($text, $label_target)), new TwigFilter('placeholders', fn(string $text, object $label_target) => $this->labelTextReplacer->replace($text, $label_target)),
]; ];
} }
/**
* Returns all parts associated with the given element.
* @param AbstractPartsContainingDBElement $element
* @return Part[]
*/
public function associatedParts(AbstractPartsContainingDBElement $element): array
{
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->em->getRepository($element::class);
return $repo->getParts($element);
}
public function associatedPartsCount(AbstractPartsContainingDBElement $element): int
{
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->em->getRepository($element::class);
return $repo->getPartsCount($element);
}
public function associatedPartsRecursive(AbstractPartsContainingDBElement $element): array
{
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->em->getRepository($element::class);
return $repo->getPartsRecursive($element);
}
public function associatedPartsCountRecursive(AbstractPartsContainingDBElement $element): int
{
/** @var AbstractPartsContainingRepository $repo */
$repo = $this->em->getRepository($element::class);
return $repo->getPartsCountRecursive($element);
}
} }