From d0b3750594e85c63fffe94b3f298f092a1a05c8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Fri, 24 Jan 2020 22:57:04 +0100 Subject: [PATCH] Added an basic table to show log entries. --- src/Controller/LogController.php | 58 +++ .../Column/LogEntryTargetColumn.php | 104 +++++ src/DataTables/LogDataTable.php | 120 ++++++ src/Entity/Base/DBElement.php | 2 +- src/Entity/LogSystem/AbstractLogEntry.php | 375 ++++++++++++++++++ .../LogSystem/ConfigChangedLogEntry.php | 39 ++ .../LogSystem/DatabaseUpdatedLogEntry.php | 39 ++ .../LogSystem/ElementCreatedLogEntry.php | 34 ++ .../LogSystem/ElementDeletedLogEntry.php | 33 ++ .../LogSystem/ElementEditedLogEntry.php | 33 ++ src/Entity/LogSystem/ExceptionLogEntry.php | 40 ++ .../LogSystem/InstockChangedLogEntry.php | 34 ++ src/Entity/LogSystem/UserLoginLogEntry.php | 34 ++ src/Entity/LogSystem/UserLogoutLogEntry.php | 34 ++ .../LogSystem/UserNotAllowedLogEntry.php | 41 ++ src/Exceptions/LogEntryObsoleteException.php | 28 ++ src/Repository/LogEntryRepository.php | 75 ++++ src/Services/ElementTypeNameGenerator.php | 14 +- templates/LogSystem/log_list.html.twig | 16 + .../Entity/LogSystem/AbstractLogEntryTest.php | 143 +++++++ .../Services/ElementTypeNameGeneratorTest.php | 3 + 21 files changed, 1292 insertions(+), 7 deletions(-) create mode 100644 src/Controller/LogController.php create mode 100644 src/DataTables/Column/LogEntryTargetColumn.php create mode 100644 src/DataTables/LogDataTable.php create mode 100644 src/Entity/LogSystem/AbstractLogEntry.php create mode 100644 src/Entity/LogSystem/ConfigChangedLogEntry.php create mode 100644 src/Entity/LogSystem/DatabaseUpdatedLogEntry.php create mode 100644 src/Entity/LogSystem/ElementCreatedLogEntry.php create mode 100644 src/Entity/LogSystem/ElementDeletedLogEntry.php create mode 100644 src/Entity/LogSystem/ElementEditedLogEntry.php create mode 100644 src/Entity/LogSystem/ExceptionLogEntry.php create mode 100644 src/Entity/LogSystem/InstockChangedLogEntry.php create mode 100644 src/Entity/LogSystem/UserLoginLogEntry.php create mode 100644 src/Entity/LogSystem/UserLogoutLogEntry.php create mode 100644 src/Entity/LogSystem/UserNotAllowedLogEntry.php create mode 100644 src/Exceptions/LogEntryObsoleteException.php create mode 100644 src/Repository/LogEntryRepository.php create mode 100644 templates/LogSystem/log_list.html.twig create mode 100644 tests/Entity/LogSystem/AbstractLogEntryTest.php diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php new file mode 100644 index 00000000..1bd3620e --- /dev/null +++ b/src/Controller/LogController.php @@ -0,0 +1,58 @@ +createFromType(LogDataTable::class) + ->handleRequest($request); + + if ($table->isCallback()) { + return $table->getResponse(); + } + + return $this->render('LogSystem/log_list.html.twig', [ + 'datatable' => $table + ]); + } +} \ No newline at end of file diff --git a/src/DataTables/Column/LogEntryTargetColumn.php b/src/DataTables/Column/LogEntryTargetColumn.php new file mode 100644 index 00000000..75d9535d --- /dev/null +++ b/src/DataTables/Column/LogEntryTargetColumn.php @@ -0,0 +1,104 @@ +em = $entityManager; + $this->entryRepository = $entityManager->getRepository(AbstractLogEntry::class); + + $this->entityURLGenerator = $entityURLGenerator; + $this->elementTypeNameGenerator = $elementTypeNameGenerator; + $this->translator = $translator; + } + + /** + * @inheritDoc + */ + public function normalize($value) + { + return $value; + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + } + + public function render($value, $context) + { + /** @var AbstractLogEntry $context */ + $target = $this->entryRepository->getTargetElement($context); + + //The element is existing + if ($target instanceof NamedDBElement) { + return sprintf( + '%s', + $this->entityURLGenerator->infoURL($target), + $this->elementTypeNameGenerator->getTypeNameCombination($target, true) + ); + } + + //Target does not have a name + if ($target instanceof DBElement) { + return sprintf( + '%s: %s', + $this->elementTypeNameGenerator->getLocalizedTypeLabel($target), + $target->getID() + ); + } + + //Element was deleted + if ($target === null && $context->hasTarget()) { + return sprintf( + '%s: %s [%s]', + $this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getTargetClass()), + $context->getTargetID(), + $this->translator->trans('log.target_deleted') + ); + } + + //Log is not associated with an element + return ""; + } +} \ No newline at end of file diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php new file mode 100644 index 00000000..31f25e8b --- /dev/null +++ b/src/DataTables/LogDataTable.php @@ -0,0 +1,120 @@ +elementTypeNameGenerator = $elementTypeNameGenerator; + $this->translator = $translator; + } + + public function configure(DataTable $dataTable, array $options) + { + $dataTable->add('id', TextColumn::class, [ + 'label' => $this->translator->trans('log.id'), + 'visible' => false, + ]); + + $dataTable->add('timestamp', LocaleDateTimeColumn::class, [ + 'label' => $this->translator->trans('log.timestamp'), + 'timeFormat' => 'medium' + ]); + + $dataTable->add('type', TextColumn::class, [ + 'label' => $this->translator->trans('log.type'), + 'propertyPath' => 'type', + 'render' => function (string $value, AbstractLogEntry $context) { + return $this->translator->trans('log.type.' . $value); + } + + ]); + + $dataTable->add('level', TextColumn::class, [ + 'label' => $this->translator->trans('log.level'), + 'propertyPath' => 'levelString', + 'render' => function (string $value, AbstractLogEntry $context) { + return $this->translator->trans('log.level.' . $value); + } + ]); + + + $dataTable->add('user', TextColumn::class, [ + 'label' => $this->translator->trans('log.user'), + 'propertyPath' => 'user.name', + ]); + + + + $dataTable->add('target_type', TextColumn::class, [ + 'label' => $this->translator->trans('log.target_type'), + 'visible' => false, + 'render' => function ($value, AbstractLogEntry $context) { + $class = $context->getTargetClass(); + if ($class !== null) { + return $this->elementTypeNameGenerator->getLocalizedTypeLabel($class); + } + return ''; + } + ]); + + $dataTable->add('target', LogEntryTargetColumn::class, [ + 'label' => $this->translator->trans('log.target') + ]); + + $dataTable->addOrderBy('timestamp', DataTable::SORT_DESCENDING); + + $dataTable->createAdapter(ORMAdapter::class, [ + 'entity' => AbstractLogEntry::class, + 'query' => function (QueryBuilder $builder): void { + $this->getQuery($builder); + }, + ]); + } + + protected function getQuery(QueryBuilder $builder): void + { + $builder->distinct()->select('log') + ->addSelect('user') + ->from(AbstractLogEntry::class, 'log') + ->leftJoin('log.user', 'user'); + } +} \ No newline at end of file diff --git a/src/Entity/Base/DBElement.php b/src/Entity/Base/DBElement.php index 174e24e7..8d260e34 100644 --- a/src/Entity/Base/DBElement.php +++ b/src/Entity/Base/DBElement.php @@ -80,7 +80,7 @@ abstract class DBElement * * @return int|null the ID of this element */ - final public function getID(): ?int + public function getID(): ?int { return $this->id; } diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php new file mode 100644 index 00000000..21d8b3ae --- /dev/null +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -0,0 +1,375 @@ + LogLevel::EMERGENCY, + self::LEVEL_ALERT => LogLevel::ALERT, + self::LEVEL_CRITICAL => LogLevel::CRITICAL, + self::LEVEL_ERROR => LogLevel::ERROR, + self::LEVEL_WARNING => LogLevel::WARNING, + self::LEVEL_NOTICE => LogLevel::NOTICE, + self::LEVEL_INFO => LogLevel::INFO, + self::LEVEL_DEBUG => LogLevel::DEBUG, + ]; + + + protected const TARGET_CLASS_MAPPING = [ + self::TARGET_TYPE_USER => User::class, + 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_FOOTPRINT => Footprint::class, + self::TARGET_TYPE_GROUP => Group::class, + self::TARGET_TYPE_MANUFACTURER => Manufacturer::class, + self::TARGET_TYPE_PART => Part::class, + self::TARGET_TYPE_STORELOCATION => Storelocation::class, + self::TARGET_TYPE_SUPPLIER => Supplier::class, + ]; + + /** @var User $user The user which has caused this log entry + * @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User") + * @ORM\JoinColumn(name="id_user") + */ + protected $user; + + /** @var DateTime The datetime the event associated with this log entry has occured. + * @ORM\Column(type="datetime", name="datetime") + */ + protected $timestamp; + + /** @var integer The priority level of the associated level. 0 is highest, 7 lowest + * @ORM\Column(type="integer", name="level", columnDefinition="TINYINT") + */ + protected $level; + + /** @var int $target_id The ID of the element targeted by this event + * @ORM\Column(name="target_id", type="integer", nullable=false) + */ + protected $target_id = 0; + + /** @var int $target_type The Type of the targeted element + * @ORM\Column(name="target_type", type="smallint", nullable=false) + */ + protected $target_type = 0; + + /** @var string The type of this log entry, aka the description what has happened. + * The mapping between the log entry class and the discriminator column is done by doctrine. + * Each subclass should override this string to specify a better string. + */ + protected $typeString = "unknown"; + + /** + * Get the user that caused the event associated with this log entry. + * @return User + */ + public function getUser(): User + { + return $this->user; + } + + /** + * Sets the user that caused the event. + * @param User $user + * @return $this + */ + public function setUser(User $user): self + { + $this->user = $user; + return $this; + } + + /** + * Returns the timestamp when the event that caused this log entry happened + * @return DateTime + */ + public function getTimestamp(): DateTime + { + return $this->timestamp; + } + + /** + * Sets the timestamp when the event happened. + * @param DateTime $timestamp + * @return $this + */ + public function setTimestamp(DateTime $timestamp): AbstractLogEntry + { + $this->timestamp = $timestamp; + return $this; + } + + /** + * Get the priority level of this log entry. 0 is highest and 7 lowest level. + * See LEVEL_* consts in this class for more info + * @return int + */ + public function getLevel(): int + { + //It is always alerting when a wrong int is saved in DB... + if ($this->level < 0 || $this->level > 7) { + return self::LEVEL_ALERT; + } + return $this->level; + } + + /** + * Sets the new level of this log entry. + * @param int $level + * @return $this + */ + public function setLevel(int $level): AbstractLogEntry + { + if ($level < 0 || $this->level > 7) { + throw new \InvalidArgumentException(sprintf('$level must be between 0 and 7! %d given!', $level)); + } + $this->level = $level; + return $this; + } + + /** + * Get the priority level of this log entry as PSR3 compatible string + * @return string + */ + public function getLevelString(): string + { + return self::levelIntToString($this->getLevel()); + } + + /** + * Sets the priority level of this log entry as PSR3 compatible string + * @param string $level + * @return $this + */ + public function setLevelString(string $level): AbstractLogEntry + { + $this->setLevel(self::levelStringToInt($level)); + return $this; + } + + /** + * Returns the type of the event this log entry is associated with. + * @return string + */ + public function getType(): string + { + return $this->typeString; + } + + /** + * @inheritDoc + */ + public function getIDString(): string + { + return "LOG".$this->getID(); + } + + /** + * Returns the class name of the target element associated with this log entry. + * Returns null, if this log entry is not associated with an log entry. + * @return string|null The class name of the target class. + */ + public function getTargetClass(): ?string + { + if ($this->target_type === self::TARGET_TYPE_NONE) { + return null; + } + + return self::targetTypeIdToClass($this->target_type); + } + + /** + * Returns the ID of the target element associated with this log entry. + * Returns null, if this log entry is not associated with an log entry. + * @return int|null The ID of the associated element. + */ + public function getTargetID(): ?int + { + if ($this->target_id === 0) { + return null; + } + + return $this->target_id; + } + + /** + * Checks if this log entry is associated with an element + * @return bool True if this log entry is associated with an element, false otherwise. + */ + public function hasTarget(): bool + { + return $this->getTargetID() !== null && $this->getTargetClass() !== null; + } + + /** + * Sets the target element associated with this element + * @param DBElement $element The element that should be associated with this element. + * @return $this + */ + public function setTargetElement(?DBElement $element): self + { + if ($element === null) { + $this->target_id = 0; + $this->target_type = self::TARGET_TYPE_NONE; + return $this; + } + + $this->target_type = static::targetTypeClassToID(get_class($element)); + $this->target_id = $element->getID(); + + return $this; + } + + /** + * This function converts the internal numeric log level into an PSR3 compatible level string. + * @param int $level The numerical log level + * @return string The PSR3 compatible level string + */ + final public static function levelIntToString(int $level): string + { + if (!isset(self::LEVEL_ID_TO_STRING[$level])) { + throw new \InvalidArgumentException('No level with this int is existing!'); + } + + return self::LEVEL_ID_TO_STRING[$level]; + } + + /** + * This function converts a PSR3 compatible string to the internal numeric level string. + * @param string $level the PSR3 compatible string that should be converted + * @return int The internal int representation. + */ + final public static function levelStringToInt(string $level): int + { + $tmp = array_flip(self::LEVEL_ID_TO_STRING); + if (!isset($tmp[$level])) { + throw new \InvalidArgumentException('No level with this string is existing!'); + } + + return $tmp[$level]; + } + + /** + * Converts an target type id to an full qualified class name. + * @param int $type_id The target type ID + * @return string + */ + final public static function targetTypeIdToClass(int $type_id): string + { + if (!isset(self::TARGET_CLASS_MAPPING[$type_id])) { + throw new \InvalidArgumentException('No target type with this ID is existing!'); + } + + return self::TARGET_CLASS_MAPPING[$type_id]; + } + + /** + * Convert a class name to a target type ID. + * @param string $class The name of the class (FQN) that should be converted to id + * @return int The ID of the associated target type ID. + */ + final public static function targetTypeClassToID(string $class): int + { + $tmp = array_flip(self::TARGET_CLASS_MAPPING); + //Check if we can use a key directly + if (isset($tmp[$class])) { + return $tmp[$class]; + } + + //Otherwise we have to iterate over everything and check for inheritance + foreach ($tmp as $compare_class => $class_id) { + if (is_a($class, $compare_class, true)) { + return $class_id; + } + } + + throw new \InvalidArgumentException('No target ID for this class is existing!'); + } + + +} \ No newline at end of file diff --git a/src/Entity/LogSystem/ConfigChangedLogEntry.php b/src/Entity/LogSystem/ConfigChangedLogEntry.php new file mode 100644 index 00000000..2c06cd51 --- /dev/null +++ b/src/Entity/LogSystem/ConfigChangedLogEntry.php @@ -0,0 +1,39 @@ +findBy(['element' => $element], ['timestamp' => $order], $limit, $offset); + } + + /** + * Gets the target element associated with the logentry. + * @param AbstractLogEntry $logEntry + * @return DBElement|null Returns the associated DBElement or null if the log either has no target or the element + * was deleted from DB. + */ + public function getTargetElement(AbstractLogEntry $logEntry): ?DBElement + { + $class = $logEntry->getTargetClass(); + $id = $logEntry->getTargetID(); + + if ($class === null || $id === null) { + return null; + } + + return $this->getEntityManager()->find($class, $id); + } +} \ No newline at end of file diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index f2338053..ed539e22 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -82,22 +82,24 @@ class ElementTypeNameGenerator * Useful when the type should be shown to user. * Throws an exception if the class is not supported. * - * @param DBElement $entity The element for which the label should be generated + * @param DBElement|string $entity The element or class for which the label should be generated * - * @return string the locatlized label for the entity type + * @return string the localized label for the entity type * * @throws EntityNotSupportedException when the passed entity is not supported */ - public function getLocalizedTypeLabel(DBElement $entity): string + public function getLocalizedTypeLabel($entity): string { + $class = is_string($entity) ? $entity : get_class($entity); + //Check if we have an direct array entry for our entity class, then we can use it - if (isset($this->mapping[get_class($entity)])) { - return $this->mapping[get_class($entity)]; + if (isset($this->mapping[$class])) { + return $this->mapping[$class]; } //Otherwise iterate over array and check for inheritance (needed when the proxy element from doctrine are passed) foreach ($this->mapping as $class => $translation) { - if ($entity instanceof $class) { + if (is_a($entity, $class, true)) { return $translation; } } diff --git a/templates/LogSystem/log_list.html.twig b/templates/LogSystem/log_list.html.twig new file mode 100644 index 00000000..1b2cc2ed --- /dev/null +++ b/templates/LogSystem/log_list.html.twig @@ -0,0 +1,16 @@ +{% extends "base.html.twig" %} + +{% block title %}{% trans %}log.list.title{% endtrans %}{% endblock %} + +{% block content %} +
+
+
+
+

{% trans %}part_list.loading.caption{% endtrans %}

+
{% trans %}part_list.loading.message{% endtrans %}
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/tests/Entity/LogSystem/AbstractLogEntryTest.php b/tests/Entity/LogSystem/AbstractLogEntryTest.php new file mode 100644 index 00000000..fdbcf1e3 --- /dev/null +++ b/tests/Entity/LogSystem/AbstractLogEntryTest.php @@ -0,0 +1,143 @@ +expectException(\InvalidArgumentException::class); + } + $this->assertSame($expected_string, AbstractLogEntry::levelIntToString($int)); + } + + /** + * @dataProvider levelDataProvider + */ + public function testLevelStringToInt(int $expected_int, string $string, bool $expect_exception = false) + { + if ($expect_exception) { + $this->expectException(\InvalidArgumentException::class); + } + $this->assertSame($expected_int, AbstractLogEntry::levelStringToInt($string)); + } + + /** + * @dataProvider targetTypeDataProvider + */ + public function testTargetTypeIdToClass(int $int, string $expected_class, bool $expect_exception = false) + { + if ($expect_exception) { + $this->expectException(\InvalidArgumentException::class); + } + $this->assertSame($expected_class, AbstractLogEntry::targetTypeIdToClass($int)); + } + + /** + * @dataProvider targetTypeDataProvider + */ + public function testTypeClassToID(int $expected_id, string $class, bool $expect_exception = false) + { + if ($expect_exception) { + $this->expectException(\InvalidArgumentException::class); + } + $this->assertSame($expected_id, AbstractLogEntry::targetTypeClassToID($class)); + } + + public function testTypeClassToIDSubclasses() + { + //Test if class mapping works for subclasses + $this->assertSame(2, AbstractLogEntry::targetTypeClassToID(PartAttachment::class)); + } + + public function testSetGetTarget() + { + $part = $this->createMock(Part::class); + $part->method('getID')->willReturn(10); + + $log = new class extends AbstractLogEntry {}; + $log->setTargetElement($part); + + $this->assertSame(Part::class, $log->getTargetClass()); + $this->assertSame(10, $log->getTargetID()); + + $log->setTargetElement(null); + $this->assertSame(null, $log->getTargetClass()); + $this->assertSame(null, $log->getTargetID()); + } +} diff --git a/tests/Services/ElementTypeNameGeneratorTest.php b/tests/Services/ElementTypeNameGeneratorTest.php index eb55e2cb..ab2f2f41 100644 --- a/tests/Services/ElementTypeNameGeneratorTest.php +++ b/tests/Services/ElementTypeNameGeneratorTest.php @@ -59,6 +59,9 @@ class ElementTypeNameGeneratorTest extends WebTestCase //Test inheritance $this->assertSame('Attachment', $this->service->getLocalizedTypeLabel(new PartAttachment())); + //Test for class name + $this->assertSame('Part', $this->service->getLocalizedTypeLabel(Part::class)); + //Test exception for unknpwn type $this->expectException(EntityNotSupportedException::class); $this->service->getLocalizedTypeLabel(new class() extends DBElement {