Allow to undo a change from event log.

This commit is contained in:
Jan Böhmer 2020-03-01 19:46:48 +01:00
parent 15d25cf2b2
commit 5a5d7b24be
24 changed files with 659 additions and 30 deletions

View file

@ -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);
}
}

View file

@ -59,7 +59,7 @@ class IconLinkColumn extends AbstractColumn
if ($href !== null) {
return sprintf(
'<a href="%s" title="%s"><i class="%s"></i></a>',
'<a class="btn btn-primary btn-sm" href="%s" title="%s"><i class="%s"></i></a>',
$href,
$title,
$icon

View file

@ -0,0 +1,69 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 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\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(
'<button type="submit" class="btn btn-outline-secondary btn-sm" name="undo" value="%d"><i class="fas fa-fw %s" title="%s"></i></button>',
$context->getID(),
$icon,
$title
);
}
}

View file

@ -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);
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, [

View file

@ -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"})
*

View file

@ -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

View file

@ -0,0 +1,55 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 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\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;
}

View file

@ -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;

View file

@ -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';
}
}
}

View file

@ -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';
}
}
}

View file

@ -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';
}
}

View file

@ -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';
}
}
}

View file

@ -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

View file

@ -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.

View file

@ -0,0 +1,56 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 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\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);
}
}

View file

@ -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)
{

View file

@ -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.

View file

@ -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)

View file

@ -0,0 +1,90 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 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;
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);
}
}

View file

@ -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(
'<i>%s</i>: %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('<i>%s</i>: %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('<i>%s</i>: %s: %s (%s)',
$this->translator->trans('log.collection_deleted.deleted'),
$this->elementTypeNameGenerator->getLocalizedTypeLabel($context->getDeletedElementClass()),
$context->getOldName() ?? $context->getDeletedElementID(),

View file

@ -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;
}

View file

@ -0,0 +1,14 @@
<form method="post" action="{{ url("log_undo") }}" data-delete-form data-title="{% trans %}log.undo.confirm_title{% endtrans %}"
data-message="{% trans %}log.undo.confirm_message{% endtrans %}">>
<input type="hidden" name="redirect_back" value="{{ app.request.uri }}">
<div id="part_list" class="" data-datatable data-settings='{{ datatable_settings(datatable) }}'>
<div class="card-body">
<div class="card">
<div class="card-body">
<h4>{% trans %}part_list.loading.caption{% endtrans %}</h4>
<h6>{% trans %}part_list.loading.message{% endtrans %}</h6>
</div>
</div>
</div>
</div>
</form>

View file

@ -3,14 +3,5 @@
{% block title %}{% trans %}log.list.title{% endtrans %}{% endblock %}
{% block content %}
<div id="part_list" class="" data-datatable data-settings='{{ datatable_settings(datatable) }}'>
<div class="card-body">
<div class="card">
<div class="card-body">
<h4>{% trans %}part_list.loading.caption{% endtrans %}</h4>
<h6>{% trans %}part_list.loading.message{% endtrans %}</h6>
</div>
</div>
</div>
</div>
{% include "LogSystem/_log_table.html.twig" %}
{% endblock %}

View file

@ -1,3 +1,3 @@
<div class="mt-2">
{% include "Parts/lists/_parts_list.html.twig" %}
{% include "LogSystem/_log_table.html.twig" %}
</div>