diff --git a/src/Controller/LogController.php b/src/Controller/LogController.php index bb978c4a..98823e15 100644 --- a/src/Controller/LogController.php +++ b/src/Controller/LogController.php @@ -43,9 +43,20 @@ declare(strict_types=1); namespace App\Controller; use App\DataTables\LogDataTable; +use App\Entity\Base\AbstractDBElement; +use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\CollectionElementDeleted; +use App\Entity\LogSystem\ElementCreatedLogEntry; +use App\Entity\LogSystem\ElementDeletedLogEntry; +use App\Entity\LogSystem\ElementEditedLogEntry; +use App\Services\LogSystem\EventUndoHelper; +use App\Services\LogSystem\TimeTravel; +use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; +use phpDocumentor\Reflection\Element; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -55,6 +66,18 @@ use Symfony\Component\Routing\Annotation\Route; */ class LogController extends AbstractController { + protected $entityManager; + protected $timeTravel; + protected $dbRepository; + + + public function __construct(EntityManagerInterface $entityManager, TimeTravel $timeTravel) + { + $this->entityManager = $entityManager; + $this->timeTravel = $timeTravel; + $this->dbRepository = $entityManager->getRepository(AbstractDBElement::class); + } + /** * @Route("/", name="log_view") * @@ -77,4 +100,63 @@ class LogController extends AbstractController 'datatable' => $table, ]); } + + /** + * @Route("/undo", name="log_undo", methods={"POST"}) + * @param Request $request + */ + public function undoLog(Request $request, EventUndoHelper $eventUndoHelper) + { + $id = $request->request->get('undo'); + $log_element = $this->entityManager->find(AbstractLogEntry::class, $id); + + $eventUndoHelper->setMode(EventUndoHelper::MODE_UNDO); + $eventUndoHelper->setUndoneEvent($log_element); + + if ($log_element instanceof ElementDeletedLogEntry || $log_element instanceof CollectionElementDeleted) { + if ($log_element instanceof ElementDeletedLogEntry) { + $element_class = $log_element->getTargetClass(); + $element_id = $log_element->getTargetID(); + } else { + $element_class = $log_element->getDeletedElementClass(); + $element_id = $log_element->getDeletedElementID(); + } + + //Check if the element we want to undelete already exits + if ($this->entityManager->find($element_class, $element_id) == null) { + $undeleted_element = $this->timeTravel->undeleteEntity($element_class, $element_id); + $this->entityManager->persist($undeleted_element); + $this->entityManager->flush(); + $this->dbRepository->changeID($undeleted_element, $element_id); + $this->addFlash('success', 'log.undo.element_undelete_success'); + } else { + $this->addFlash('warning', 'log.undo.element_element_already_undeleted'); + } + } elseif ($log_element instanceof ElementCreatedLogEntry) { + $element = $this->entityManager->find($log_element->getTargetClass(), $log_element->getTargetID()); + if ($element !== null) { + $this->entityManager->remove($element); + $this->entityManager->flush(); + $this->addFlash('success', 'log.undo.element_delete_success'); + } else { + $this->addFlash('warning', 'log.undo.element.element_already_delted'); + } + } elseif ($log_element instanceof ElementEditedLogEntry) { + $element = $this->entityManager->find($log_element->getTargetClass(), $log_element->getTargetID()); + if ($element instanceof AbstractDBElement) { + $this->timeTravel->applyEntry($element, $log_element); + $this->entityManager->flush(); + $this->addFlash('success', 'log.undo.element_change_undone'); + } else { + $this->addFlash('error', 'log.undo.do_undelete_before'); + } + } else { + $this->addFlash('error', 'log.undo.log_type_invalid'); + } + + $eventUndoHelper->clearUndoneEvent(); + + $redirect = $request->request->get('redirect_back'); + return $this->redirect($redirect); + } } diff --git a/src/DataTables/Column/IconLinkColumn.php b/src/DataTables/Column/IconLinkColumn.php index 96171052..32577f83 100644 --- a/src/DataTables/Column/IconLinkColumn.php +++ b/src/DataTables/Column/IconLinkColumn.php @@ -59,7 +59,7 @@ class IconLinkColumn extends AbstractColumn if ($href !== null) { return sprintf( - '', + '', $href, $title, $icon diff --git a/src/DataTables/Column/RevertLogColumn.php b/src/DataTables/Column/RevertLogColumn.php new file mode 100644 index 00000000..18814d6b --- /dev/null +++ b/src/DataTables/Column/RevertLogColumn.php @@ -0,0 +1,69 @@ +. + */ + +namespace App\DataTables\Column; + + +use App\Entity\LogSystem\AbstractLogEntry; +use App\Entity\LogSystem\CollectionElementDeleted; +use App\Entity\LogSystem\ElementCreatedLogEntry; +use App\Entity\LogSystem\ElementDeletedLogEntry; +use App\Entity\LogSystem\ElementEditedLogEntry; +use Omines\DataTablesBundle\Column\AbstractColumn; +use Symfony\Contracts\Translation\TranslatorInterface; + +class RevertLogColumn extends AbstractColumn +{ + protected $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + /** + * @inheritDoc + */ + public function normalize($value) + { + return $value; + } + + public function render($value, $context) + { + if ($context instanceof ElementDeletedLogEntry || $context instanceof CollectionElementDeleted) { + $icon = 'fa-trash-restore'; + $title = $this->translator->trans('log.undo.undelete'); + } elseif ($context instanceof ElementEditedLogEntry || $context instanceof ElementCreatedLogEntry) { + $icon = 'fa-undo'; + $title = $this->translator->trans('log.undo.undo'); + } else { + return ''; + } + + + return sprintf( + '', + $context->getID(), + $icon, + $title + ); + } +} \ No newline at end of file diff --git a/src/DataTables/LogDataTable.php b/src/DataTables/LogDataTable.php index e894355c..8aff2994 100644 --- a/src/DataTables/LogDataTable.php +++ b/src/DataTables/LogDataTable.php @@ -46,6 +46,7 @@ use App\DataTables\Column\IconLinkColumn; use App\DataTables\Column\LocaleDateTimeColumn; use App\DataTables\Column\LogEntryExtraColumn; use App\DataTables\Column\LogEntryTargetColumn; +use App\DataTables\Column\RevertLogColumn; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\TimeTravelInterface; use App\Entity\LogSystem\AbstractLogEntry; @@ -218,8 +219,10 @@ class LogDataTable implements DataTableTypeInterface ) { try { $target = $this->logRepo->getTargetElement($context); - $str = $this->entityURLGenerator->timeTravelURL($target, $context->getTimestamp()); - return $str; + if($target !== null) { + $str = $this->entityURLGenerator->timeTravelURL($target, $context->getTimestamp()); + return $str; + } } catch (EntityNotSupportedException $exception) { return null; } @@ -228,6 +231,10 @@ class LogDataTable implements DataTableTypeInterface } ]); + $dataTable->add('actionRevert', RevertLogColumn::class, [ + 'label' => '' + ]); + $dataTable->addOrderBy('timestamp', DataTable::SORT_DESCENDING); $dataTable->createAdapter(ORMAdapter::class, [ diff --git a/src/Entity/Base/AbstractDBElement.php b/src/Entity/Base/AbstractDBElement.php index b9bc23aa..46dac31f 100644 --- a/src/Entity/Base/AbstractDBElement.php +++ b/src/Entity/Base/AbstractDBElement.php @@ -34,7 +34,7 @@ use Symfony\Component\Serializer\Annotation\Groups; * Every database table which are managed with this class (or a subclass of it) * must have the table row "id"!! The ID is the unique key to identify the elements. * - * @ORM\MappedSuperclass() + * @ORM\MappedSuperclass(repositoryClass="App\Repository\DBElementRepository") * * @ORM\EntityListeners({"App\Security\EntityListeners\ElementPermissionListener"}) * diff --git a/src/Entity/Base/AbstractNamedDBElement.php b/src/Entity/Base/AbstractNamedDBElement.php index df64cf33..212211f3 100644 --- a/src/Entity/Base/AbstractNamedDBElement.php +++ b/src/Entity/Base/AbstractNamedDBElement.php @@ -31,7 +31,7 @@ use Symfony\Component\Validator\Constraints as Assert; /** * All subclasses of this class have an attribute "name". * - * @ORM\MappedSuperclass(repositoryClass="App\Repository\UserRepository") + * @ORM\MappedSuperclass(repositoryClass="App\Repository\NamedDBElement") * @ORM\HasLifecycleCallbacks() */ abstract class AbstractNamedDBElement extends AbstractDBElement implements NamedElementInterface, TimeStampableInterface diff --git a/src/Entity/Contracts/LogWithEventUndoInterface.php b/src/Entity/Contracts/LogWithEventUndoInterface.php new file mode 100644 index 00000000..7ce7f611 --- /dev/null +++ b/src/Entity/Contracts/LogWithEventUndoInterface.php @@ -0,0 +1,55 @@ +. + */ + +namespace App\Entity\Contracts; + + +use App\Entity\LogSystem\AbstractLogEntry; + +interface LogWithEventUndoInterface +{ + /** + * Checks if this element undoes another event. + * @return bool + */ + public function isUndoEvent(): bool; + + /** + * Returns the ID of the undone event or null if no event is undone. + * @return int|null + */ + public function getUndoEventID(): ?int; + + /** + * Sets the event that is undone, and the undo mode. + * @param AbstractLogEntry $event + * @param string $mode + * @return $this + */ + public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): self; + + /** + * Returns the mode how the event was undone: + * "undo" = Only a single event was applied to element + * "revert" = Element was reverted to the state it was to the timestamp of the log. + * @return string + */ + public function getUndoMode(): string; +} \ No newline at end of file diff --git a/src/Entity/LogSystem/AbstractLogEntry.php b/src/Entity/LogSystem/AbstractLogEntry.php index 9d211e2b..79d9b3aa 100644 --- a/src/Entity/LogSystem/AbstractLogEntry.php +++ b/src/Entity/LogSystem/AbstractLogEntry.php @@ -368,6 +368,17 @@ abstract class AbstractLogEntry extends AbstractDBElement return $this; } + /** + * Sets the target ID of the element associated with this element. + * @param int $target_id + * @return $this + */ + public function setTargetElementID(int $target_id): self + { + $this->target_id = $target_id; + return $this; + } + public function getExtraData(): array { return $this->extra; diff --git a/src/Entity/LogSystem/CollectionElementDeleted.php b/src/Entity/LogSystem/CollectionElementDeleted.php index 5b7155e1..3d127e41 100644 --- a/src/Entity/LogSystem/CollectionElementDeleted.php +++ b/src/Entity/LogSystem/CollectionElementDeleted.php @@ -22,6 +22,7 @@ namespace App\Entity\LogSystem; use App\Entity\Base\AbstractDBElement; +use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; use Doctrine\ORM\Mapping as ORM; @@ -30,7 +31,7 @@ use Doctrine\ORM\Mapping as ORM; * This log entry is created when an element is deleted, that is used in a collection of an other entity. * This is needed to signal time travel, that it has to undelete the deleted entity. */ -class CollectionElementDeleted extends AbstractLogEntry +class CollectionElementDeleted extends AbstractLogEntry implements LogWithEventUndoInterface { protected $typeString = 'collection_element_deleted'; protected $level = self::LEVEL_INFO; @@ -85,4 +86,51 @@ class CollectionElementDeleted extends AbstractLogEntry { return $this->extra['i']; } + + /** + * @inheritDoc + */ + public function isUndoEvent(): bool + { + return isset($this->extra['u']); + } + + /** + * @inheritDoc + */ + public function getUndoEventID(): ?int + { + return $this->extra['u'] ?? null; + } + + /** + * @inheritDoc + */ + public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface + { + $this->extra['u'] = $event->getID(); + + if ($mode === 'undo') { + $this->extra['um'] = 1; + } elseif ($mode === 'revert') { + $this->extra['um'] = 2; + } else { + throw new \InvalidArgumentException('Passed invalid $mode!'); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getUndoMode(): string + { + $mode_int = $this->extra['um'] ?? 1; + if ($mode_int === 1) { + return 'undo'; + } else { + return 'revert'; + } + } } \ No newline at end of file diff --git a/src/Entity/LogSystem/ElementCreatedLogEntry.php b/src/Entity/LogSystem/ElementCreatedLogEntry.php index b8cac342..e49e4d0d 100644 --- a/src/Entity/LogSystem/ElementCreatedLogEntry.php +++ b/src/Entity/LogSystem/ElementCreatedLogEntry.php @@ -44,12 +44,13 @@ namespace App\Entity\LogSystem; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithCommentInterface; +use App\Entity\Contracts\LogWithEventUndoInterface; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ -class ElementCreatedLogEntry extends AbstractLogEntry implements LogWithCommentInterface +class ElementCreatedLogEntry extends AbstractLogEntry implements LogWithCommentInterface, LogWithEventUndoInterface { protected $typeString = 'element_created'; @@ -104,4 +105,51 @@ class ElementCreatedLogEntry extends AbstractLogEntry implements LogWithCommentI $this->extra['m'] = $new_comment; return $this; } + + /** + * @inheritDoc + */ + public function isUndoEvent(): bool + { + return isset($this->extra['u']); + } + + /** + * @inheritDoc + */ + public function getUndoEventID(): ?int + { + return $this->extra['u'] ?? null; + } + + /** + * @inheritDoc + */ + public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface + { + $this->extra['u'] = $event->getID(); + + if ($mode === 'undo') { + $this->extra['um'] = 1; + } elseif ($mode === 'revert') { + $this->extra['um'] = 2; + } else { + throw new \InvalidArgumentException('Passed invalid $mode!'); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getUndoMode(): string + { + $mode_int = $this->extra['um'] ?? 1; + if ($mode_int === 1) { + return 'undo'; + } else { + return 'revert'; + } + } } diff --git a/src/Entity/LogSystem/ElementDeletedLogEntry.php b/src/Entity/LogSystem/ElementDeletedLogEntry.php index 50c5628b..cfcb6844 100644 --- a/src/Entity/LogSystem/ElementDeletedLogEntry.php +++ b/src/Entity/LogSystem/ElementDeletedLogEntry.php @@ -44,6 +44,7 @@ namespace App\Entity\LogSystem; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithCommentInterface; +use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\NamedElementInterface; use App\Entity\Contracts\TimeTravelInterface; use Doctrine\ORM\Mapping as ORM; @@ -51,7 +52,7 @@ use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ -class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface +class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface, LogWithEventUndoInterface { protected $typeString = 'element_deleted'; @@ -137,4 +138,50 @@ class ElementDeletedLogEntry extends AbstractLogEntry implements TimeTravelInter $this->extra['m'] = $new_comment; return $this; } + + /** + * @inheritDoc + */ + public function isUndoEvent(): bool + { + return isset($this->extra['u']); + } + + /** + * @inheritDoc + */ + public function getUndoEventID(): ?int + { + return $this->extra['u'] ?? null; + } + + /** + * @inheritDoc + */ + public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface + { + $this->extra['u'] = $event->getID(); + + if ($mode === 'undo') { + $this->extra['um'] = 1; + } elseif ($mode === 'revert') { + $this->extra['um'] = 2; + } else { + throw new \InvalidArgumentException('Passed invalid $mode!'); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getUndoMode(): string + { + $mode_int = $this->extra['um'] ?? 1; + if ($mode_int === 1) { + return 'undo'; + } + return 'revert'; + } } diff --git a/src/Entity/LogSystem/ElementEditedLogEntry.php b/src/Entity/LogSystem/ElementEditedLogEntry.php index 59be889c..2d22edc9 100644 --- a/src/Entity/LogSystem/ElementEditedLogEntry.php +++ b/src/Entity/LogSystem/ElementEditedLogEntry.php @@ -44,13 +44,14 @@ namespace App\Entity\LogSystem; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\LogWithCommentInterface; +use App\Entity\Contracts\LogWithEventUndoInterface; use App\Entity\Contracts\TimeTravelInterface; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity() */ -class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface +class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterface, LogWithCommentInterface, LogWithEventUndoInterface { protected $typeString = 'element_edited'; @@ -150,4 +151,51 @@ class ElementEditedLogEntry extends AbstractLogEntry implements TimeTravelInterf $this->extra['m'] = $new_comment; return $this; } + + /** + * @inheritDoc + */ + public function isUndoEvent(): bool + { + return isset($this->extra['u']); + } + + /** + * @inheritDoc + */ + public function getUndoEventID(): ?int + { + return $this->extra['u'] ?? null; + } + + /** + * @inheritDoc + */ + public function setUndoneEvent(AbstractLogEntry $event, string $mode = 'undo'): LogWithEventUndoInterface + { + $this->extra['u'] = $event->getID(); + + if ($mode === 'undo') { + $this->extra['um'] = 1; + } elseif ($mode === 'revert') { + $this->extra['um'] = 2; + } else { + throw new \InvalidArgumentException('Passed invalid $mode!'); + } + + return $this; + } + + /** + * @inheritDoc + */ + public function getUndoMode(): string + { + $mode_int = $this->extra['um'] ?? 1; + if ($mode_int === 1) { + return 'undo'; + } else { + return 'revert'; + } + } } diff --git a/src/EventSubscriber/EventLoggerSubscriber.php b/src/EventSubscriber/EventLoggerSubscriber.php index 10d7f73c..34cf3b01 100644 --- a/src/EventSubscriber/EventLoggerSubscriber.php +++ b/src/EventSubscriber/EventLoggerSubscriber.php @@ -36,6 +36,7 @@ use App\Entity\PriceInformations\Pricedetail; use App\Entity\UserSystem\User; use App\Services\LogSystem\EventCommentHelper; use App\Services\LogSystem\EventLogger; +use App\Services\LogSystem\EventUndoHelper; use Doctrine\Common\EventSubscriber; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Event\OnFlushEventArgs; @@ -65,18 +66,21 @@ class EventLoggerSubscriber implements EventSubscriber protected $logger; protected $serializer; protected $eventCommentHelper; + protected $eventUndoHelper; protected $save_changed_fields; protected $save_changed_data; protected $save_removed_data; protected $propertyAccessor; public function __construct(EventLogger $logger, SerializerInterface $serializer, EventCommentHelper $commentHelper, - bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor) + bool $save_changed_fields, bool $save_changed_data, bool $save_removed_data, PropertyAccessorInterface $propertyAccessor, + EventUndoHelper $eventUndoHelper) { $this->logger = $logger; $this->serializer = $serializer; $this->eventCommentHelper = $commentHelper; $this->propertyAccessor = $propertyAccessor; + $this->eventUndoHelper = $eventUndoHelper; $this->save_changed_fields = $save_changed_fields; $this->save_changed_data = $save_changed_data; @@ -120,6 +124,18 @@ class EventLoggerSubscriber implements EventSubscriber if ($this->eventCommentHelper->isMessageSet()) { $log->setComment($this->eventCommentHelper->getMessage()); } + if ($this->eventUndoHelper->isUndo()) { + $undoEvent = $this->eventUndoHelper->getUndoneEvent(); + + $log->setUndoneEvent($undoEvent, $this->eventUndoHelper->getMode()); + + if($undoEvent instanceof ElementDeletedLogEntry && $undoEvent->getTargetClass() === $log->getTargetClass()) { + $log->setTargetElementID($undoEvent->getTargetID()); + } + if($undoEvent instanceof CollectionElementDeleted && $undoEvent->getDeletedElementClass() === $log->getTargetClass()) { + $log->setTargetElementID($undoEvent->getDeletedElementID()); + } + } $this->logger->log($log); } } @@ -135,6 +151,7 @@ class EventLoggerSubscriber implements EventSubscriber //Clear the message provided by user. $this->eventCommentHelper->clearMessage(); + $this->eventUndoHelper->clearUndoneEvent(); } protected function logElementDeleted(AbstractDBElement $entity, EntityManagerInterface $em): void @@ -144,6 +161,9 @@ class EventLoggerSubscriber implements EventSubscriber if ($this->eventCommentHelper->isMessageSet()) { $log->setComment($this->eventCommentHelper->getMessage()); } + if ($this->eventUndoHelper->isUndo()) { + $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode()); + } if ($this->save_removed_data) { //The 4th param is important here, as we delete the element... $this->saveChangeSet($entity, $log, $em, true); @@ -162,6 +182,9 @@ class EventLoggerSubscriber implements EventSubscriber if (in_array($field, $whitelist)) { $changed = $this->propertyAccessor->getValue($entity, $field); $log = new CollectionElementDeleted($changed, $mapping['inversedBy'], $entity); + if ($this->eventUndoHelper->isUndo()) { + $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode()); + } $this->logger->log($log); } } @@ -179,12 +202,16 @@ class EventLoggerSubscriber implements EventSubscriber $this->saveChangeSet($entity, $log, $em); } elseif ($this->save_changed_fields) { $changed_fields = array_keys($uow->getEntityChangeSet($entity)); + unset($changed_fields['lastModified']); $log->setChangedFields($changed_fields); } //Add user comment to log entry if ($this->eventCommentHelper->isMessageSet()) { $log->setComment($this->eventCommentHelper->getMessage()); } + if ($this->eventUndoHelper->isUndo()) { + $log->setUndoneEvent($this->eventUndoHelper->getUndoneEvent(), $this->eventUndoHelper->getMode()); + } $this->logger->log($log); } @@ -212,6 +239,8 @@ class EventLoggerSubscriber implements EventSubscriber */ protected function filterFieldRestrictions(AbstractDBElement $element, array $fields): array { + unset($fields['lastModified']); + if (!$this->hasFieldRestrictions($element)) { return $fields; } @@ -256,9 +285,9 @@ class EventLoggerSubscriber implements EventSubscriber $old_data = $uow->getOriginalEntityData($entity); } else { //Otherwise we have to get it from entity changeset $changeSet = $uow->getEntityChangeSet($entity); - $old_data = array_diff(array_combine(array_keys($changeSet), array_column($changeSet, 0)), [null]); + $old_data = array_combine(array_keys($changeSet), array_column($changeSet, 0)); } - $this->filterFieldRestrictions($entity, $old_data); + $old_data = $this->filterFieldRestrictions($entity, $old_data); //Restrict length of string fields, to save memory... $old_data = array_map(function ($value) { @@ -271,6 +300,7 @@ class EventLoggerSubscriber implements EventSubscriber $logEntry->setOldData($old_data); } + /** * Check if the given entity can be logged. * @param object $entity diff --git a/src/Repository/AttachmentRepository.php b/src/Repository/AttachmentRepository.php index f2c6c774..f8b78e41 100644 --- a/src/Repository/AttachmentRepository.php +++ b/src/Repository/AttachmentRepository.php @@ -23,7 +23,7 @@ namespace App\Repository; use Doctrine\ORM\EntityRepository; -class AttachmentRepository extends EntityRepository +class AttachmentRepository extends DBElementRepository { /** * Gets the count of all private/secure attachments. diff --git a/src/Repository/DBElementRepository.php b/src/Repository/DBElementRepository.php new file mode 100644 index 00000000..94b19ec2 --- /dev/null +++ b/src/Repository/DBElementRepository.php @@ -0,0 +1,56 @@ +. + */ + +namespace App\Repository; + + +use App\Entity\Base\AbstractDBElement; +use Doctrine\ORM\EntityRepository; + +class DBElementRepository extends EntityRepository +{ + /** + * Changes the ID of the given element to a new value. + * You should only use it to undelete former existing elements, everything else is most likely a bad idea! + * @param AbstractDBElement $element The element whose ID should be changed + * @param int $new_id The new ID + */ + public function changeID(AbstractDBElement $element, int $new_id): void + { + $qb = $this->createQueryBuilder('element'); + $q = $qb->update(get_class($element), 'element') + ->set('element.id', $new_id) + ->where('element.id = ?1') + ->setParameter(1, $element->getID()) + ->getQuery(); + + $p = $q->execute(); + + $this->setField($element, 'id', $new_id); + } + + protected function setField(AbstractDBElement $element, string $field, $new_value) + { + $reflection = new \ReflectionClass(get_class($element)); + $property = $reflection->getProperty($field); + $property->setAccessible(true); + $property->setValue($element, $new_value); + } +} \ No newline at end of file diff --git a/src/Repository/LogEntryRepository.php b/src/Repository/LogEntryRepository.php index 71d4a5f5..53c9b4d9 100644 --- a/src/Repository/LogEntryRepository.php +++ b/src/Repository/LogEntryRepository.php @@ -52,7 +52,7 @@ use App\Entity\UserSystem\User; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; -class LogEntryRepository extends EntityRepository +class LogEntryRepository extends DBElementRepository { public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null) { diff --git a/src/Repository/NamedDBElementRepository.php b/src/Repository/NamedDBElementRepository.php index 8fa4ecc1..404efed0 100644 --- a/src/Repository/NamedDBElementRepository.php +++ b/src/Repository/NamedDBElementRepository.php @@ -46,7 +46,7 @@ use App\Entity\Base\AbstractNamedDBElement; use App\Helpers\Trees\TreeViewNode; use Doctrine\ORM\EntityRepository; -class NamedDBElementRepository extends EntityRepository +class NamedDBElementRepository extends DBElementRepository { /** * Gets a tree of TreeViewNode elements. The root elements has $parent as parent. diff --git a/src/Repository/PartRepository.php b/src/Repository/PartRepository.php index bd069632..0ac94932 100644 --- a/src/Repository/PartRepository.php +++ b/src/Repository/PartRepository.php @@ -46,7 +46,7 @@ use App\Entity\Parts\PartLot; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; -class PartRepository extends EntityRepository +class PartRepository extends NamedDBElementRepository { /** * Gets the summed up instock of all parts (only parts without an measurent unit) diff --git a/src/Services/LogSystem/EventUndoHelper.php b/src/Services/LogSystem/EventUndoHelper.php new file mode 100644 index 00000000..cd0bdb4c --- /dev/null +++ b/src/Services/LogSystem/EventUndoHelper.php @@ -0,0 +1,90 @@ +. + */ + +namespace App\Services\LogSystem; + + +use App\Entity\LogSystem\AbstractLogEntry; + +class EventUndoHelper +{ + public const MODE_UNDO = 'undo'; + public const MODE_REVERT = 'revert'; + + protected const ALLOWED_MODES = [self::MODE_REVERT, self::MODE_UNDO]; + + protected $undone_event; + protected $mode; + + public function __construct() + { + $undone_event = null; + $this->mode = self::MODE_UNDO; + } + + public function setMode(string $mode): void + { + if (!in_array($mode, self::ALLOWED_MODES)) { + throw new \InvalidArgumentException('Invalid mode passed!'); + } + $this->mode = $mode; + } + + public function getMode(): string + { + return $this->mode; + } + + /** + * Set which event log is currently undone. + * After the flush this message is cleared. + * @param AbstractLogEntry|null $message + */ + public function setUndoneEvent(?AbstractLogEntry $undone_event): void + { + $this->undone_event = $undone_event; + } + + /** + * Returns event that is currently undone. + * @return string|null + */ + public function getUndoneEvent(): ?AbstractLogEntry + { + return $this->undone_event; + } + + /** + * Clear the currently the set undone event. + */ + public function clearUndoneEvent(): void + { + $this->undone_event = null; + } + + /** + * Check if a event is undone + * @return bool + */ + public function isUndo(): bool + { + return ($this->undone_event instanceof AbstractLogEntry); + } +} \ No newline at end of file diff --git a/src/Services/LogSystem/LogEntryExtraFormatter.php b/src/Services/LogSystem/LogEntryExtraFormatter.php index c4e83187..bf386373 100644 --- a/src/Services/LogSystem/LogEntryExtraFormatter.php +++ b/src/Services/LogSystem/LogEntryExtraFormatter.php @@ -123,8 +123,15 @@ class LogEntryExtraFormatter if ($context instanceof ElementCreatedLogEntry ) { $comment = ''; + if ($context->isUndoEvent()) { + if ($context->getUndoMode() === 'undo') { + $comment .= $this->translator->trans('log.undo_mode.undo').': '.$context->getUndoEventID().';'; + } elseif($context->getUndoMode() === 'revert') { + $comment .= $this->translator->trans('log.undo_mode.revert').': '.$context->getUndoEventID().';'; + } + } if ($context->hasComment()) { - $comment = htmlspecialchars($context->getComment()) . '; '; + $comment .= htmlspecialchars($context->getComment()) . '; '; } if($context->hasCreationInstockValue()) { return $comment . sprintf( @@ -138,8 +145,15 @@ class LogEntryExtraFormatter if ($context instanceof ElementDeletedLogEntry) { $comment = ''; + if ($context->isUndoEvent()) { + if ($context->getUndoMode() === 'undo') { + $comment .= $this->translator->trans('log.undo_mode.undo').': '.$context->getUndoEventID().';'; + } elseif($context->getUndoMode() === 'revert') { + $comment .= $this->translator->trans('log.undo_mode.revert').': '.$context->getUndoEventID().';'; + } + } if ($context->hasComment()) { - $comment = htmlspecialchars($context->getComment()) . '; '; + $comment .= htmlspecialchars($context->getComment()) . '; '; } return $comment . sprintf( '%s: %s', @@ -150,6 +164,13 @@ class LogEntryExtraFormatter if ($context instanceof ElementEditedLogEntry) { $str = ''; + if ($context->isUndoEvent()) { + if ($context->getUndoMode() === 'undo') { + $str .= $this->translator->trans('log.undo_mode.undo').': '.$context->getUndoEventID().';'; + } elseif($context->getUndoMode() === 'revert') { + $str .= $this->translator->trans('log.undo_mode.revert').': '.$context->getUndoEventID().';'; + } + } if ($context->hasComment()) { $str .= htmlspecialchars($context->getComment()); } @@ -180,7 +201,16 @@ class LogEntryExtraFormatter } if ($context instanceof CollectionElementDeleted) { - return sprintf('%s: %s: %s (%s)', + $comment = ''; + if ($context->isUndoEvent()) { + if ($context->getUndoMode() === 'undo') { + $comment .= $this->translator->trans('log.undo_mode.undo').': '.$context->getUndoEventID().';'; + } elseif($context->getUndoMode() === 'revert') { + $comment .= $this->translator->trans('log.undo_mode.revert').': '.$context->getUndoEventID().';'; + } + } + + return $comment . sprintf('%s: %s: %s (%s)', $this->translator->trans('log.collection_deleted.deleted'), $this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getDeletedElementClass()), $context->getOldName() ?? $context->getDeletedElementID(), diff --git a/src/Services/LogSystem/TimeTravel.php b/src/Services/LogSystem/TimeTravel.php index aada08b9..54e9fa72 100644 --- a/src/Services/LogSystem/TimeTravel.php +++ b/src/Services/LogSystem/TimeTravel.php @@ -60,6 +60,9 @@ class TimeTravel //Set internal ID so the element can be reverted $this->setField($element, 'id', $id); + //Let database determine when it will be created + $this->setField($element,'addedDate', null); + return $element; } diff --git a/templates/LogSystem/_log_table.html.twig b/templates/LogSystem/_log_table.html.twig new file mode 100644 index 00000000..cd121bfb --- /dev/null +++ b/templates/LogSystem/_log_table.html.twig @@ -0,0 +1,14 @@ +
> + +
+
+
+
+

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

+
{% trans %}part_list.loading.message{% endtrans %}
+
+
+
+
+
\ No newline at end of file diff --git a/templates/LogSystem/log_list.html.twig b/templates/LogSystem/log_list.html.twig index 1b2cc2ed..5ad1d7a3 100644 --- a/templates/LogSystem/log_list.html.twig +++ b/templates/LogSystem/log_list.html.twig @@ -3,14 +3,5 @@ {% block title %}{% trans %}log.list.title{% endtrans %}{% endblock %} {% block content %} -
-
-
-
-

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

-
{% trans %}part_list.loading.message{% endtrans %}
-
-
-
-
+ {% include "LogSystem/_log_table.html.twig" %} {% endblock %} \ No newline at end of file diff --git a/templates/Parts/info/_history.html.twig b/templates/Parts/info/_history.html.twig index b4fad04d..32409462 100644 --- a/templates/Parts/info/_history.html.twig +++ b/templates/Parts/info/_history.html.twig @@ -1,3 +1,3 @@
- {% include "Parts/lists/_parts_list.html.twig" %} + {% include "LogSystem/_log_table.html.twig" %}
\ No newline at end of file