Show a table with the old data in log entry details page

This commit is contained in:
Jan Böhmer 2023-05-01 01:38:14 +02:00
parent 4c6ceab8e8
commit 1534f780aa
12 changed files with 435 additions and 84 deletions

View file

@ -24,6 +24,7 @@ namespace App\Services;
use App\Entity\Attachments\Attachment; use App\Entity\Attachments\Attachment;
use App\Entity\Attachments\AttachmentType; use App\Entity\Attachments\AttachmentType;
use App\Entity\Base\AbstractDBElement;
use App\Entity\Contracts\NamedElementInterface; use App\Entity\Contracts\NamedElementInterface;
use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\Project;
use App\Entity\LabelSystem\LabelProfile; use App\Entity\LabelSystem\LabelProfile;
@ -43,17 +44,20 @@ use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Entity\UserSystem\Group; use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Exceptions\EntityNotSupportedException; use App\Exceptions\EntityNotSupportedException;
use Doctrine\ORM\Mapping\Entity;
use function get_class; use function get_class;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
class ElementTypeNameGenerator class ElementTypeNameGenerator
{ {
protected TranslatorInterface $translator; protected TranslatorInterface $translator;
private EntityURLGenerator $entityURLGenerator;
protected array $mapping; protected array $mapping;
public function __construct(TranslatorInterface $translator) public function __construct(TranslatorInterface $translator, EntityURLGenerator $entityURLGenerator)
{ {
$this->translator = $translator; $this->translator = $translator;
$this->entityURLGenerator = $entityURLGenerator;
//Child classes has to become before parent classes //Child classes has to become before parent classes
$this->mapping = [ $this->mapping = [
@ -132,4 +136,81 @@ class ElementTypeNameGenerator
return $type.': '.$entity->getName(); return $type.': '.$entity->getName();
} }
/**
* Returns a HTML formatted label for the given enitity in the format "Type: Name" (on elements with a name) and
* "Type: ID" (on elements without a name). If possible the value is given as a link to the element.
* @param AbstractDBElement $entity The entity for which the label should be generated
* @param bool $include_associated If set to true, the associated entity (like the part belonging to a part lot) is included in the label to give further information
* @return string
*/
public function formatLabelHTMLForEntity(AbstractDBElement $entity, bool $include_associated = false): string
{
//The element is existing
if ($entity instanceof NamedElementInterface && !empty($entity->getName())) {
try {
$tmp = sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($entity),
$this->getTypeNameCombination($entity, true)
);
} catch (EntityNotSupportedException $exception) {
$tmp = $this->getTypeNameCombination($entity, true);
}
} else { //Target does not have a name
$tmp = sprintf(
'<i>%s</i>: %s',
$this->getLocalizedTypeLabel($entity),
$entity->getID()
);
}
//Add a hint to the associated element if possible
if ($include_associated) {
if ($entity instanceof Attachment && null !== $entity->getElement()) {
$on = $entity->getElement();
} elseif ($entity instanceof AbstractParameter && null !== $entity->getElement()) {
$on = $entity->getElement();
} elseif ($entity instanceof PartLot && null !== $entity->getPart()) {
$on = $entity->getPart();
} elseif ($entity instanceof Orderdetail && null !== $entity->getPart()) {
$on = $entity->getPart();
} elseif ($entity instanceof Pricedetail && null !== $entity->getOrderdetail() && null !== $entity->getOrderdetail()->getPart()) {
$on = $entity->getOrderdetail()->getPart();
} elseif ($entity instanceof ProjectBOMEntry && null !== $entity->getProject()) {
$on = $entity->getProject();
}
if (isset($on) && is_object($on)) {
try {
$tmp .= sprintf(
' (<a href="%s">%s</a>)',
$this->entityURLGenerator->infoURL($on),
$this->getTypeNameCombination($on, true)
);
} catch (EntityNotSupportedException $exception) {
}
}
}
return $tmp;
}
/**
* Create a HTML formatted label for a deleted element of which we only know the class and the ID.
* Please note that it is not checked if the element really not exists anymore, so you have to do this yourself.
* @param string $class
* @param int $id
* @return string
*/
public function formatElementDeletedHTML(string $class, int $id): string
{
return sprintf(
'<i>%s</i>: %s [%s]',
$this->getLocalizedTypeLabel($class),
$id,
$this->translator->trans('log.target_deleted')
);
}
} }

View file

@ -0,0 +1,146 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LogSystem;
use App\Entity\LogSystem\AbstractLogEntry;
use App\Services\ElementTypeNameGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class LogDataFormatter
{
private const STRING_MAX_LENGTH = 1024;
private TranslatorInterface $translator;
private EntityManagerInterface $entityManager;
private ElementTypeNameGenerator $elementTypeNameGenerator;
public function __construct(TranslatorInterface $translator, EntityManagerInterface $entityManager, ElementTypeNameGenerator $elementTypeNameGenerator)
{
$this->translator = $translator;
$this->entityManager = $entityManager;
$this->elementTypeNameGenerator = $elementTypeNameGenerator;
}
/**
* Formats the given data of a log entry as HTML
* @param mixed $data
* @param AbstractLogEntry $logEntry
* @param string $fieldName
* @return string
*/
public function formatData($data, AbstractLogEntry $logEntry, string $fieldName): string
{
if (is_string($data)) {
return '"' . mb_strimwidth(htmlspecialchars($data), 0, self::STRING_MAX_LENGTH, ) . '"';
}
if (is_bool($data)) {
return $this->formatBool($data);
}
if (is_int($data)) {
return (string) $data;
}
if (is_float($data)) {
return (string) $data;
}
if (is_null($data)) {
return '<i>null</i>';
}
if (is_array($data)) {
//If the array contains only one element with the key @id, it is a reference to another entity (foreign key)
if (isset($data['@id'])) {
return $this->formatForeignKey($data, $logEntry, $fieldName);
}
//If the array contains a "date", "timezone_type" and "timezone" key, it is a DateTime object
if (isset($data['date'], $data['timezone_type'], $data['timezone'])) {
return $this->formatDateTime($data);
}
return htmlspecialchars(json_encode($data, JSON_PRETTY_PRINT));
}
throw new \RuntimeException('Type of $data not supported (' . gettype($data) . ')');
}
private function formatForeignKey(array $data, AbstractLogEntry $logEntry, string $fieldName): string
{
//Extract the id from the @id key
$id = $data['@id'];
try {
//Retrieve the class type from the logEntry and retrieve the doctrine metadata
$classMetadata = $this->entityManager->getClassMetadata($logEntry->getTargetClass());
$fkTargetClass = $classMetadata->getAssociationTargetClass($fieldName);
//Try to retrieve the entity from the database
$entity = $this->entityManager->getRepository($fkTargetClass)->find($id);
//If the entity was found, return a label for this entity
if ($entity) {
return $this->elementTypeNameGenerator->formatLabelHTMLForEntity($entity, true);
} else { //Otherwise the entity was deleted, so return the id
return $this->elementTypeNameGenerator->formatElementDeletedHTML($fkTargetClass, $id);
}
} catch (\InvalidArgumentException|\ReflectionException $exception) {
return '<i>unknown target class</i>: ' . $id;
}
}
private function formatDateTime(array $data): string
{
if (!isset($data['date'], $data['timezone_type'], $data['timezone'])) {
return '<i>unknown DateTime format</i>';
}
$date = $data['date'];
$timezoneType = $data['timezone_type'];
$timezone = $data['timezone'];
if (!is_string($date) || !is_int($timezoneType) || !is_string($timezone)) {
return '<i>unknown DateTime format</i>';
}
try {
$dateTime = new \DateTime($date, new \DateTimeZone($timezone));
} catch (\Exception $exception) {
return '<i>unknown DateTime format</i>';
}
//Format it to the users locale
$formatter = new \IntlDateFormatter(null, \IntlDateFormatter::MEDIUM, \IntlDateFormatter::MEDIUM);
return $formatter->format($dateTime);
}
private function formatBool(bool $data): string
{
return $data ? $this->translator->trans('true') : $this->translator->trans('false');
}
}

View file

@ -78,64 +78,17 @@ class LogTargetHelper
/** @var AbstractLogEntry $context */ /** @var AbstractLogEntry $context */
$target = $this->entryRepository->getTargetElement($context); $target = $this->entryRepository->getTargetElement($context);
$tmp = ''; //If the target is null and the context has a target, that means that the target was deleted. Show it that way.
if ($target === null) {
//The element is existing if ($context->hasTarget()) {
if ($target instanceof NamedElementInterface && !empty($target->getName())) { return $this->elementTypeNameGenerator->formatElementDeletedHTML($context->getTargetClass(),
try { $context->getTargetId());
$tmp = sprintf(
'<a href="%s">%s</a>',
$this->entityURLGenerator->infoURL($target),
$this->elementTypeNameGenerator->getTypeNameCombination($target, true)
);
} catch (EntityNotSupportedException $exception) {
$tmp = $this->elementTypeNameGenerator->getTypeNameCombination($target, true);
} }
} elseif ($target instanceof AbstractDBElement) { //Target does not have a name //If no target is set, we can't do anything
$tmp = sprintf( return '';
'<i>%s</i>: %s',
$this->elementTypeNameGenerator->getLocalizedTypeLabel($target),
$target->getID()
);
} elseif (null === $target && $context->hasTarget()) { //Element was deleted
$tmp = sprintf(
'<i>%s</i>: %s [%s]',
$this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getTargetClass()),
$context->getTargetID(),
$this->translator->trans('log.target_deleted')
);
} }
//Add a hint to the associated element if possible //Otherwise we can return a label for the target
if (null !== $target && $options['show_associated']) { return $this->elementTypeNameGenerator->formatLabelHTMLForEntity($target, $options['show_associated']);
if ($target instanceof Attachment && null !== $target->getElement()) {
$on = $target->getElement();
} elseif ($target instanceof AbstractParameter && null !== $target->getElement()) {
$on = $target->getElement();
} elseif ($target instanceof PartLot && null !== $target->getPart()) {
$on = $target->getPart();
} elseif ($target instanceof Orderdetail && null !== $target->getPart()) {
$on = $target->getPart();
} elseif ($target instanceof Pricedetail && null !== $target->getOrderdetail() && null !== $target->getOrderdetail()->getPart()) {
$on = $target->getOrderdetail()->getPart();
} elseif ($target instanceof ProjectBOMEntry && null !== $target->getProject()) {
$on = $target->getProject();
}
if (isset($on) && is_object($on)) {
try {
$tmp .= sprintf(
' (<a href="%s">%s</a>)',
$this->entityURLGenerator->infoURL($on),
$this->elementTypeNameGenerator->getTypeNameCombination($on, true)
);
} catch (EntityNotSupportedException $exception) {
$tmp .= ' ('.$this->elementTypeNameGenerator->getTypeNameCombination($target, true).')';
}
}
}
//Log is not associated with an element
return $tmp;
} }
} }

40
src/Twig/LogExtension.php Normal file
View file

@ -0,0 +1,40 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Twig;
use App\Services\LogSystem\LogDataFormatter;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class LogExtension extends AbstractExtension
{
public function __construct(LogDataFormatter $logDataFormatter)
{
$this->logDataFormatter = $logDataFormatter;
}
public function getFunctions()
{
return [
new TwigFunction('format_log_data', [$this->logDataFormatter, 'formatData'], ['is_safe' => ['html']])
];
}
}

View file

@ -0,0 +1,11 @@
{# @var entry \App\Entity\LogSystem\ElementCreatedLogEntry #}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
{{ log_helper.comment_field(entry) }}
{% if entry.creationInstockValue %}
<p>
<b>{% trans %}log.element_created.original_instock{% endtrans %}:</b>
{{ entry.creationInstockValue }}
</p>
{% endif %}

View file

@ -0,0 +1,8 @@
{# @var entry \App\Entity\LogSystem\ElementDeletedLogEntry #}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
<span class="badge badge-notice"></span>
{{ log_helper.comment_field(entry) }}
{{ log_helper.data_change_table(entry) }}

View file

@ -0,0 +1,11 @@
{# @var entry \App\Entity\LogSystem\ElementDeletedLogEntry #}
{% import "log_system/details/helper.macro.html.twig" as log_helper %}
{% if entry.undoEvent %}
<span class="badge badge-info bg-info">Test</span>
{% endif %}
{{ log_helper.comment_field(entry) }}
{{ log_helper.data_change_table(entry) }}

View file

@ -0,0 +1,77 @@
{% macro comment_field(entry) %}
{# @var entry \App\Entity\Contracts\LogWithComment #}
<p class="mb-0">
<b>{% trans %}edit.log_comment{% endtrans %}:</b>
{% if entry.comment %}
{{ entry.comment }}
{% else %}
<span class="text-muted">{% trans %}log.no_comment{% endtrans %}</span>
{% endif %}
</p>
{% endmacro %}
{% macro data_change_table(entry) %}
{# @var entry \App\Entity\LogSystem\ElementEditedLogEntry|\App\Entity\LogSystem\ElementDeletedLogEntry entry #}
{% set fields, old_data, new_data = {}, {}, {} %}
{# For log entries where only the changed fields are saved, this is the last executed assignment #}
{% if attribute(entry, 'changedFieldInfo') is defined and entry.changedFieldsInfo %}
{% set fields = entry.changedFields %}
{% endif %}
{# For log entries, where we know the old data, this is the last exectuted assignment #}
{% if attribute(entry, 'oldDataInformations') is defined and entry.oldDataInformations %}
{# We have to use the keys of oldData here, as changedFields might not be available #}
{% set fields = entry.oldData | keys %}
{% set old_data = entry.oldData %}
{% endif %}
{% if fields is not empty %}
<table class="table table-hover table-striped table-sm table-bordered mt-2">
<thead>
<tr>
<th>{% trans %}log.element_changed.field{% endtrans %}</th>
{% if old_data is not empty %}
<th>{% trans %}log.element_changed.data_before{% endtrans %}</th>
{% endif %}
{% if new_data is not empty %}
<th>{% trans %}log.element_changed.data_after{% endtrans %}</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for field in fields %}
<tr>
<td title="{{ field }}">
{% set trans_key = 'log.element_edited.changed_fields.'~field %}
{# If the translation key is not found, the translation key is returned, and we dont show the translation #}
{% if trans_key|trans != trans_key %}
{{ ('log.element_edited.changed_fields.'~field) | trans }}
<span class="text-muted">({{ field }})</span>
{% else %}
{{ field }}
{% endif %}
</td>
{% if old_data is not empty %}
<td>
{% if old_data[field] is defined %}
{{ format_log_data(old_data[field], entry, field) }}
{% endif %}
</td>
{% endif %}
{% if new_data is not empty %}
<td>
{% if new_data[field] is defined %}
{{ format_log_data(new_data[field], entry, field) }}
{% endif %}
</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endmacro %}

View file

@ -62,6 +62,12 @@
{% set entry = log_entry %} {% set entry = log_entry %}
{% if log_entry is instanceof('App\\Entity\\LogSystem\\DatabaseUpdatedLogEntry') %} {% if log_entry is instanceof('App\\Entity\\LogSystem\\DatabaseUpdatedLogEntry') %}
{% include "log_system/details/_extra_database_updated.html.twig" %} {% include "log_system/details/_extra_database_updated.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementCreatedLogEntry') %}
{% include "log_system/details/_extra_element_created.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementEditedLogEntry') %}
{% include "log_system/details/_extra_element_edited.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\ElementDeletedLogEntry') %}
{% include "log_system/details/_extra_element_deleted.html.twig" %}
{% elseif log_entry is instanceof('App\\Entity\\LogSystem\\UserLoginLogEntry') {% elseif log_entry is instanceof('App\\Entity\\LogSystem\\UserLoginLogEntry')
or log_entry is instanceof('App\\Entity\\LogSystem\\UserLogoutLogEntry') %} or log_entry is instanceof('App\\Entity\\LogSystem\\UserLogoutLogEntry') %}
{% include "log_system/details/_extra_user_login.html.twig" %} {% include "log_system/details/_extra_user_login.html.twig" %}

View file

@ -10960,31 +10960,31 @@ Element 3</target>
</segment> </segment>
</unit> </unit>
<unit id="Qt585vm" name="attachment.max_file_size"> <unit id="Qt585vm" name="attachment.max_file_size">
<segment state="translated"> <segment>
<source>attachment.max_file_size</source> <source>attachment.max_file_size</source>
<target>Maximum file size</target> <target>Maximum file size</target>
</segment> </segment>
</unit> </unit>
<unit id="tkkbiag" name="user.saml_user"> <unit id="tkkbiag" name="user.saml_user">
<segment state="translated"> <segment>
<source>user.saml_user</source> <source>user.saml_user</source>
<target>SSO / SAML user</target> <target>SSO / SAML user</target>
</segment> </segment>
</unit> </unit>
<unit id="fhepjKr" name="user.saml_user.pw_change_hint"> <unit id="fhepjKr" name="user.saml_user.pw_change_hint">
<segment state="translated"> <segment>
<source>user.saml_user.pw_change_hint</source> <source>user.saml_user.pw_change_hint</source>
<target>Your user uses single sign-on (SSO). You can not change the password and 2FA settings here. Configure them on your central SSO provider instead!</target> <target>Your user uses single sign-on (SSO). You can not change the password and 2FA settings here. Configure them on your central SSO provider instead!</target>
</segment> </segment>
</unit> </unit>
<unit id="32beTBH" name="login.sso_saml_login"> <unit id="32beTBH" name="login.sso_saml_login">
<segment state="translated"> <segment>
<source>login.sso_saml_login</source> <source>login.sso_saml_login</source>
<target>Single Sign-On Login (SSO)</target> <target>Single Sign-On Login (SSO)</target>
</segment> </segment>
</unit> </unit>
<unit id="wnMLanX" name="login.local_login_hint"> <unit id="wnMLanX" name="login.local_login_hint">
<segment state="translated"> <segment>
<source>login.local_login_hint</source> <source>login.local_login_hint</source>
<target>The form below is only for log in with a local user. If you want to log in via single sign-on, press the button above.</target> <target>The form below is only for log in with a local user. If you want to log in via single sign-on, press the button above.</target>
</segment> </segment>
@ -11188,103 +11188,103 @@ Element 3</target>
</segment> </segment>
</unit> </unit>
<unit id="qDVJg2i" name="measurement_unit.new"> <unit id="qDVJg2i" name="measurement_unit.new">
<segment state="translated"> <segment>
<source>measurement_unit.new</source> <source>measurement_unit.new</source>
<target>New Measurement Unit</target> <target>New Measurement Unit</target>
</segment> </segment>
</unit> </unit>
<unit id="WKzr7h0" name="measurement_unit.edit"> <unit id="WKzr7h0" name="measurement_unit.edit">
<segment state="translated"> <segment>
<source>measurement_unit.edit</source> <source>measurement_unit.edit</source>
<target>Edit Measurement Unit</target> <target>Edit Measurement Unit</target>
</segment> </segment>
</unit> </unit>
<unit id="gRatnCn" name="user.aboutMe.label"> <unit id="gRatnCn" name="user.aboutMe.label">
<segment state="translated"> <segment>
<source>user.aboutMe.label</source> <source>user.aboutMe.label</source>
<target>About Me</target> <target>About Me</target>
</segment> </segment>
</unit> </unit>
<unit id="WsHXARp" name="storelocation.owner.label"> <unit id="WsHXARp" name="storelocation.owner.label">
<segment state="translated"> <segment>
<source>storelocation.owner.label</source> <source>storelocation.owner.label</source>
<target>Owner</target> <target>Owner</target>
</segment> </segment>
</unit> </unit>
<unit id="VQ97Dh0" name="storelocation.part_owner_must_match.label"> <unit id="VQ97Dh0" name="storelocation.part_owner_must_match.label">
<segment state="translated"> <segment>
<source>storelocation.part_owner_must_match.label</source> <source>storelocation.part_owner_must_match.label</source>
<target>Part Lot owner must match storage location owner</target> <target>Part Lot owner must match storage location owner</target>
</segment> </segment>
</unit> </unit>
<unit id="sddE1L." name="part_lot.owner"> <unit id="sddE1L." name="part_lot.owner">
<segment state="translated"> <segment>
<source>part_lot.owner</source> <source>part_lot.owner</source>
<target>Owner</target> <target>Owner</target>
</segment> </segment>
</unit> </unit>
<unit id="XKIMr8j" name="part_lot.owner.help"> <unit id="XKIMr8j" name="part_lot.owner.help">
<segment state="translated"> <segment>
<source>part_lot.owner.help</source> <source>part_lot.owner.help</source>
<target>Only the owner can withdraw or add stock to this lot.</target> <target>Only the owner can withdraw or add stock to this lot.</target>
</segment> </segment>
</unit> </unit>
<unit id="61.yfNy" name="log.element_edited.changed_fields.owner"> <unit id="61.yfNy" name="log.element_edited.changed_fields.owner">
<segment state="translated"> <segment>
<source>log.element_edited.changed_fields.owner</source> <source>log.element_edited.changed_fields.owner</source>
<target>Owner</target> <target>Owner</target>
</segment> </segment>
</unit> </unit>
<unit id="YkZAIS8" name="log.element_edited.changed_fields.instock_unknown"> <unit id="YkZAIS8" name="log.element_edited.changed_fields.instock_unknown">
<segment state="translated"> <segment>
<source>log.element_edited.changed_fields.instock_unknown</source> <source>log.element_edited.changed_fields.instock_unknown</source>
<target>Amount unknown</target> <target>Amount unknown</target>
</segment> </segment>
</unit> </unit>
<unit id="xf7NNZ9" name="log.element_edited.changed_fields.needs_refill"> <unit id="xf7NNZ9" name="log.element_edited.changed_fields.needs_refill">
<segment state="translated"> <segment>
<source>log.element_edited.changed_fields.needs_refill</source> <source>log.element_edited.changed_fields.needs_refill</source>
<target>Refill needed</target> <target>Refill needed</target>
</segment> </segment>
</unit> </unit>
<unit id="Gfw_MWL" name="part.withdraw.access_denied"> <unit id="Gfw_MWL" name="part.withdraw.access_denied">
<segment state="translated"> <segment>
<source>part.withdraw.access_denied</source> <source>part.withdraw.access_denied</source>
<target>Not allowed to do the desired action. Please check your permissions and the owner of the part lots.</target> <target>Not allowed to do the desired action. Please check your permissions and the owner of the part lots.</target>
</segment> </segment>
</unit> </unit>
<unit id="Dwo4KWP" name="part.info.amount.less_than_desired"> <unit id="Dwo4KWP" name="part.info.amount.less_than_desired">
<segment state="translated"> <segment>
<source>part.info.amount.less_than_desired</source> <source>part.info.amount.less_than_desired</source>
<target>Less than desired</target> <target>Less than desired</target>
</segment> </segment>
</unit> </unit>
<unit id="cdnsW4q" name="log.cli_user"> <unit id="cdnsW4q" name="log.cli_user">
<segment state="translated"> <segment>
<source>log.cli_user</source> <source>log.cli_user</source>
<target>CLI user</target> <target>CLI user</target>
</segment> </segment>
</unit> </unit>
<unit id="4GTAJ9E" name="log.element_edited.changed_fields.part_owner_must_match"> <unit id="4GTAJ9E" name="log.element_edited.changed_fields.part_owner_must_match">
<segment state="translated"> <segment>
<source>log.element_edited.changed_fields.part_owner_must_match</source> <source>log.element_edited.changed_fields.part_owner_must_match</source>
<target>Part owner must match storage location owner</target> <target>Part owner must match storage location owner</target>
</segment> </segment>
</unit> </unit>
<unit id="u6qFa_j" name="part.filter.lessThanDesired"> <unit id="u6qFa_j" name="part.filter.lessThanDesired">
<segment state="translated"> <segment>
<source>part.filter.lessThanDesired</source> <source>part.filter.lessThanDesired</source>
<target>In stock less than desired (total amount &lt; min. amount)</target> <target><![CDATA[In stock less than desired (total amount < min. amount)]]></target>
</segment> </segment>
</unit> </unit>
<unit id="lHTN.a1" name="part.filter.lotOwner"> <unit id="lHTN.a1" name="part.filter.lotOwner">
<segment state="translated"> <segment>
<source>part.filter.lotOwner</source> <source>part.filter.lotOwner</source>
<target>Lot owner</target> <target>Lot owner</target>
</segment> </segment>
</unit> </unit>
<unit id="47OCK_W" name="user.show_email_on_profile.label"> <unit id="47OCK_W" name="user.show_email_on_profile.label">
<segment state="translated"> <segment>
<source>user.show_email_on_profile.label</source> <source>user.show_email_on_profile.label</source>
<target>Show email on public profile page</target> <target>Show email on public profile page</target>
</segment> </segment>
@ -11319,5 +11319,23 @@ Element 3</target>
<target>The request was blocked. No action should be required.</target> <target>The request was blocked. No action should be required.</target>
</segment> </segment>
</unit> </unit>
<unit id="JVE17kW" name="log.no_comment">
<segment>
<source>log.no_comment</source>
<target>No comment</target>
</segment>
</unit>
<unit id="5xvPvME" name="log.element_changed.field">
<segment>
<source>log.element_changed.field</source>
<target>Field</target>
</segment>
</unit>
<unit id="vufWYhV" name="log.element_changed.data_before">
<segment>
<source>log.element_changed.data_before</source>
<target>Data before change</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -8,7 +8,7 @@
</segment> </segment>
</unit> </unit>
<unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml"> <unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml">
<segment state="translated"> <segment>
<source>saml.error.cannot_login_local_user_per_saml</source> <source>saml.error.cannot_login_local_user_per_saml</source>
<target>You can not login as local user via SSO! Use your local user password instead.</target> <target>You can not login as local user via SSO! Use your local user password instead.</target>
</segment> </segment>

View file

@ -300,19 +300,19 @@
</segment> </segment>
</unit> </unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank"> <unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<segment state="translated"> <segment>
<source>validator.attachment.name_not_blank</source> <source>validator.attachment.name_not_blank</source>
<target>Set a value here, or upload a file to automatically use its filename as name for the attachment.</target> <target>Set a value here, or upload a file to automatically use its filename as name for the attachment.</target>
</segment> </segment>
</unit> </unit>
<unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner"> <unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated"> <segment>
<source>validator.part_lot.owner_must_match_storage_location_owner</source> <source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>The owner of this lot must match the owner of the selected storage location (%owner_name%)!</target> <target>The owner of this lot must match the owner of the selected storage location (%owner_name%)!</target>
</segment> </segment>
</unit> </unit>
<unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous"> <unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated"> <segment>
<source>validator.part_lot.owner_must_not_be_anonymous</source> <source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>A lot owner must not be the anonymous user!</target> <target>A lot owner must not be the anonymous user!</target>
</segment> </segment>