mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-04 06:19:36 +00:00
Compare commits
5 commits
233c5e8550
...
97a74815d3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a74815d3 | ||
|
|
7998cdcd71 | ||
|
|
5e9f7a11a3 | ||
|
|
1c6bf3f472 | ||
|
|
aed2652f1d |
7 changed files with 88 additions and 35 deletions
|
|
@ -91,18 +91,20 @@ 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
|
||||
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.
|
||||
|
||||
It is useful to use the HTML embed feature of the editor, to have a block where you can write the twig code without worrying about the WYSIWYG editor messing with your code.
|
||||
|
||||
The following variables are in injected into Twig and can be accessed using `{% raw %}{{ variable }}{% endraw %}` (
|
||||
or `{% raw %}{{ variable.property }}{% endraw %}`):
|
||||
|
||||
| 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 %}{{ 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 %}{{ paper_width }}{% endraw %}` | The width of the label paper in mm |
|
||||
| `{% raw %}{{ paper_height }}{% endraw %}` | The height of the label paper in mm |
|
||||
|
|
@ -236,12 +238,18 @@ certain data:
|
|||
|
||||
#### Functions
|
||||
|
||||
| Function name | Description |
|
||||
|----------------------------------------------|-----------------------------------------------------------------------------------------------|
|
||||
| `placeholder(placeholder, element)` | Get the value of a placeholder of an element |
|
||||
| `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.) |
|
||||
| `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. |
|
||||
| Function name | Description |
|
||||
|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `placeholder(placeholder, element)` | Get the value of a placeholder of an element |
|
||||
| `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.) |
|
||||
| `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) |
|
||||
| `type_label(element)` | Get the name of the type of an element (e.g. "Part", "Storage location", etc.) |
|
||||
| `type_label_p(element)` | Get the name of the type of an element in plural form (e.g. "Parts", "Storage locations", etc.) |
|
||||
|
||||
### Filters
|
||||
|
||||
|
|
@ -285,5 +293,5 @@ If you want to use a different (more beautiful) font, you can use the [custom fo
|
|||
feature.
|
||||
There is the [Noto](https://www.google.com/get/noto/) font family from Google, which supports a lot of languages and is
|
||||
available in different styles (regular, bold, italic, bold-italic).
|
||||
For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese,
|
||||
and Korean characters.
|
||||
For example, you can use [Noto CJK](https://github.com/notofonts/noto-cjk) for more beautiful Chinese, Japanese,
|
||||
and Korean characters.
|
||||
|
|
|
|||
|
|
@ -42,15 +42,14 @@ declare(strict_types=1);
|
|||
namespace App\Exceptions;
|
||||
|
||||
use RuntimeException;
|
||||
use Twig\Error\Error;
|
||||
|
||||
class TwigModeException extends RuntimeException
|
||||
{
|
||||
private const PROJECT_PATH = __DIR__ . '/../../';
|
||||
|
||||
public function __construct(?Error $previous = null)
|
||||
public function __construct(?\Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($previous->getMessage(), 0, $previous);
|
||||
parent::__construct($previous?->getMessage() ?? "Unknown message", 0, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -266,11 +266,14 @@ class EntityExporter
|
|||
//Sanitize the filename
|
||||
$filename = FilenameSanatizer::sanitizeFilename($filename);
|
||||
|
||||
//Remove percent for fallback
|
||||
$fallback = str_replace("%", "_", $filename);
|
||||
|
||||
// Create the disposition of the file
|
||||
$disposition = $response->headers->makeDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
$filename,
|
||||
u($filename)->ascii()->toString(),
|
||||
$fallback,
|
||||
);
|
||||
// Set the content disposition
|
||||
$response->headers->set('Content-Disposition', $disposition);
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ final class LabelHTMLGenerator
|
|||
'paper_height' => $options->getHeight(),
|
||||
]
|
||||
);
|
||||
} catch (Error $exception) {
|
||||
} catch (\Throwable $exception) {
|
||||
throw new TwigModeException($exception);
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -86,11 +86,11 @@ use Twig\Sandbox\SecurityPolicyInterface;
|
|||
*/
|
||||
final class SandboxedTwigFactory
|
||||
{
|
||||
private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'verbatim', 'with'];
|
||||
private const ALLOWED_TAGS = ['apply', 'autoescape', 'do', 'for', 'if', 'set', 'types', 'verbatim', 'with'];
|
||||
private const ALLOWED_FILTERS = ['abs', 'batch', 'capitalize', 'column', 'country_name',
|
||||
'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'first', 'format',
|
||||
'currency_name', 'currency_symbol', 'date', 'date_modify', 'data_uri', 'default', 'escape', 'filter', 'find', 'first', 'format',
|
||||
'format_currency', 'format_date', 'format_datetime', 'format_number', 'format_time', 'html_to_markdown', 'join', 'keys',
|
||||
'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'raw', 'number_format',
|
||||
'language_name', 'last', 'length', 'locale_name', 'lower', 'map', 'markdown_to_html', 'merge', 'nl2br', 'number_format', 'raw',
|
||||
'reduce', 'replace', 'reverse', 'round', 'slice', 'slug', 'sort', 'spaceless', 'split', 'striptags', 'timezone_name', 'title',
|
||||
'trim', 'u', 'upper', 'url_encode',
|
||||
|
||||
|
|
@ -104,16 +104,17 @@ final class SandboxedTwigFactory
|
|||
];
|
||||
|
||||
private const ALLOWED_FUNCTIONS = ['country_names', 'country_timezones', 'currency_names', 'cycle',
|
||||
'date', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names',
|
||||
'template_from_string', 'timezone_names',
|
||||
'date', 'enum', 'enum_cases', 'html_classes', 'language_names', 'locale_names', 'max', 'min', 'random', 'range', 'script_names',
|
||||
'timezone_names',
|
||||
|
||||
//Part-DB specific extensions:
|
||||
//EntityExtension:
|
||||
'entity_type', 'entity_url',
|
||||
'entity_type', 'entity_url', 'type_label', 'type_label_plural',
|
||||
//BarcodeExtension:
|
||||
'barcode_svg',
|
||||
//SandboxedLabelExtension
|
||||
'placeholder',
|
||||
'associated_parts', 'associated_parts_count', 'associated_parts_r', 'associated_parts_count_r',
|
||||
];
|
||||
|
||||
private const ALLOWED_METHODS = [
|
||||
|
|
@ -130,7 +131,7 @@ final class SandboxedTwigFactory
|
|||
'getValueTypical', 'getUnit', 'getValueText', ],
|
||||
MeasurementUnit::class => ['getUnit', 'isInteger', 'useSIPrefix'],
|
||||
PartLot::class => ['isExpired', 'getDescription', 'getComment', 'getExpirationDate', 'getStorageLocation',
|
||||
'getPart', 'isInstockUnknown', 'getAmount', 'getNeedsRefill', 'getVendorBarcode'],
|
||||
'getPart', 'isInstockUnknown', 'getAmount', 'getOwner', 'getLastStocktakeAt', 'getNeedsRefill', 'getVendorBarcode'],
|
||||
StorageLocation::class => ['isFull', 'isOnlySinglePart', 'isLimitToExistingParts', 'getStorageType'],
|
||||
Supplier::class => ['getShippingCosts', 'getDefaultCurrency'],
|
||||
Part::class => ['isNeedsReview', 'getTags', 'getMass', 'getIpn', 'getProviderReference',
|
||||
|
|
@ -141,13 +142,13 @@ final class SandboxedTwigFactory
|
|||
'getParameters', 'getGroupedParameters',
|
||||
'isProjectBuildPart', 'getBuiltProject',
|
||||
'getAssociatedPartsAsOwner', 'getAssociatedPartsAsOther', 'getAssociatedPartsAll',
|
||||
'getEdaInfo'
|
||||
'getEdaInfo', 'getGtin'
|
||||
],
|
||||
Currency::class => ['getIsoCode', 'getInverseExchangeRate', 'getExchangeRate'],
|
||||
Orderdetail::class => ['getPart', 'getSupplier', 'getSupplierPartNr', 'getObsolete',
|
||||
'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl'],
|
||||
'getPricedetails', 'findPriceForQty', 'isObsolete', 'getSupplierProductUrl', 'getPricesIncludesVAT'],
|
||||
Pricedetail::class => ['getOrderdetail', 'getPrice', 'getPricePerUnit', 'getPriceRelatedQuantity',
|
||||
'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode'],
|
||||
'getMinDiscountQuantity', 'getCurrency', 'getCurrencyISOCode', 'getIncludesVat'],
|
||||
InfoProviderReference:: class => ['getProviderKey', 'getProviderId', 'getProviderUrl', 'getLastUpdated', 'isProviderCreated'],
|
||||
PartAssociation::class => ['getType', 'getComment', 'getOwner', 'getOther', 'getOtherType'],
|
||||
|
||||
|
|
|
|||
|
|
@ -23,14 +23,18 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Twig\Sandbox;
|
||||
|
||||
use App\Entity\Base\AbstractPartsContainingDBElement;
|
||||
use App\Entity\Parts\Part;
|
||||
use App\Repository\AbstractPartsContainingRepository;
|
||||
use App\Services\LabelSystem\LabelTextReplacer;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
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 [
|
||||
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)),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ final class EntityExporterTest extends WebTestCase
|
|||
|
||||
private function getEntities(): array
|
||||
{
|
||||
$entity1 = (new Category())->setName('Enitity 1')->setComment('Test');
|
||||
$entity1_1 = (new Category())->setName('Enitity 1.1')->setParent($entity1);
|
||||
$entity2 = (new Category())->setName('Enitity 2');
|
||||
$entity1 = (new Category())->setName('Entity%1')->setComment('Test');
|
||||
$entity1_1 = (new Category())->setName('Entity 1.1')->setParent($entity1);
|
||||
$entity2 = (new Category())->setName('Entity 2');
|
||||
|
||||
return [$entity1, $entity1_1, $entity2];
|
||||
}
|
||||
|
|
@ -55,12 +55,12 @@ final class EntityExporterTest extends WebTestCase
|
|||
$entities = $this->getEntities();
|
||||
|
||||
$json_without_children = $this->service->exportEntities($entities, ['format' => 'json', 'level' => 'simple']);
|
||||
$this->assertJsonStringEqualsJsonString('[{"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]',
|
||||
$this->assertJsonStringEqualsJsonString('[{"name":"Entity%1","type":"category","full_name":"Entity%1"},{"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"},{"name":"Entity 2","type":"category","full_name":"Entity 2"}]',
|
||||
$json_without_children);
|
||||
|
||||
$json_with_children = $this->service->exportEntities($entities,
|
||||
['format' => 'json', 'level' => 'simple', 'include_children' => true]);
|
||||
$this->assertJsonStringEqualsJsonString('[{"children":[{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"}],"name":"Enitity 1","type":"category","full_name":"Enitity 1"},{"children":[],"name":"Enitity 1.1","type":"category","full_name":"Enitity 1->Enitity 1.1"},{"children":[],"name":"Enitity 2","type":"category","full_name":"Enitity 2"}]',
|
||||
$this->assertJsonStringEqualsJsonString('[{"children":[{"children":[],"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"}],"name":"Entity%1","type":"category","full_name":"Entity%1"},{"children":[],"name":"Entity 1.1","type":"category","full_name":"Entity%1->Entity 1.1"},{"children":[],"name":"Entity 2","type":"category","full_name":"Entity 2"}]',
|
||||
$json_with_children);
|
||||
}
|
||||
|
||||
|
|
@ -95,8 +95,8 @@ final class EntityExporterTest extends WebTestCase
|
|||
$this->assertSame('name', $worksheet->getCell('A1')->getValue());
|
||||
$this->assertSame('full_name', $worksheet->getCell('B1')->getValue());
|
||||
|
||||
$this->assertSame('Enitity 1', $worksheet->getCell('A2')->getValue());
|
||||
$this->assertSame('Enitity 1', $worksheet->getCell('B2')->getValue());
|
||||
$this->assertSame('Entity%1', $worksheet->getCell('A2')->getValue());
|
||||
$this->assertSame('Entity%1', $worksheet->getCell('B2')->getValue());
|
||||
|
||||
unlink($tempFile);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue