mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-07-02 22:44:33 +02:00
Show a table with the old data in log entry details page
This commit is contained in:
parent
4c6ceab8e8
commit
1534f780aa
12 changed files with 435 additions and 84 deletions
|
@ -24,6 +24,7 @@ namespace App\Services;
|
|||
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Contracts\NamedElementInterface;
|
||||
use App\Entity\ProjectSystem\Project;
|
||||
use App\Entity\LabelSystem\LabelProfile;
|
||||
|
@ -43,17 +44,20 @@ use App\Entity\ProjectSystem\ProjectBOMEntry;
|
|||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Exceptions\EntityNotSupportedException;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use function get_class;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ElementTypeNameGenerator
|
||||
{
|
||||
protected TranslatorInterface $translator;
|
||||
private EntityURLGenerator $entityURLGenerator;
|
||||
protected array $mapping;
|
||||
|
||||
public function __construct(TranslatorInterface $translator)
|
||||
public function __construct(TranslatorInterface $translator, EntityURLGenerator $entityURLGenerator)
|
||||
{
|
||||
$this->translator = $translator;
|
||||
$this->entityURLGenerator = $entityURLGenerator;
|
||||
|
||||
//Child classes has to become before parent classes
|
||||
$this->mapping = [
|
||||
|
@ -132,4 +136,81 @@ class ElementTypeNameGenerator
|
|||
|
||||
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')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
146
src/Services/LogSystem/LogDataFormatter.php
Normal file
146
src/Services/LogSystem/LogDataFormatter.php
Normal 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');
|
||||
}
|
||||
}
|
|
@ -78,64 +78,17 @@ class LogTargetHelper
|
|||
/** @var AbstractLogEntry $context */
|
||||
$target = $this->entryRepository->getTargetElement($context);
|
||||
|
||||
$tmp = '';
|
||||
|
||||
//The element is existing
|
||||
if ($target instanceof NamedElementInterface && !empty($target->getName())) {
|
||||
try {
|
||||
$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);
|
||||
//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) {
|
||||
if ($context->hasTarget()) {
|
||||
return $this->elementTypeNameGenerator->formatElementDeletedHTML($context->getTargetClass(),
|
||||
$context->getTargetId());
|
||||
}
|
||||
} elseif ($target instanceof AbstractDBElement) { //Target does not have a name
|
||||
$tmp = sprintf(
|
||||
'<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')
|
||||
);
|
||||
//If no target is set, we can't do anything
|
||||
return '';
|
||||
}
|
||||
|
||||
//Add a hint to the associated element if possible
|
||||
if (null !== $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;
|
||||
//Otherwise we can return a label for the target
|
||||
return $this->elementTypeNameGenerator->formatLabelHTMLForEntity($target, $options['show_associated']);
|
||||
}
|
||||
}
|
40
src/Twig/LogExtension.php
Normal file
40
src/Twig/LogExtension.php
Normal 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']])
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue