mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'signature-app/wp-576-restorestored-object-version' into 'signature-app-master'
See the list of stored object and restore some versions See merge request Chill-Projet/chill-bundles!733
This commit is contained in:
commit
c4c5c860f0
@ -55,7 +55,7 @@
|
||||
"mime": "^4.0.0",
|
||||
"pdfjs-dist": "^4.3.136",
|
||||
"vis-network": "^9.1.0",
|
||||
"vue": "^3.2.37",
|
||||
"vue": "^3.5.6",
|
||||
"vue-i18n": "^9.1.6",
|
||||
"vue-multiselect": "3.0.0-alpha.2",
|
||||
"vue-toast-notification": "^3.1.2",
|
||||
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class StoredObjectRestoreVersionApiController
|
||||
{
|
||||
public function __construct(private Security $security, private StoredObjectRestoreInterface $storedObjectRestore, private EntityManagerInterface $entityManager, private SerializerInterface $serializer) {}
|
||||
|
||||
#[Route('/api/1.0/doc-store/stored-object/restore-from-version/{id}', methods: ['POST'])]
|
||||
public function restoreStoredObjectVersion(StoredObjectVersion $storedObjectVersion): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::EDIT->value, $storedObjectVersion->getStoredObject())) {
|
||||
throw new AccessDeniedHttpException('not allowed to edit the stored object');
|
||||
}
|
||||
|
||||
$newVersion = $this->storedObjectRestore->restore($storedObjectVersion);
|
||||
|
||||
$this->entityManager->persist($newVersion);
|
||||
$this->entityManager->flush();
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize($newVersion, 'json', [AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Serializer\Model\Collection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
final readonly class StoredObjectVersionApiController
|
||||
{
|
||||
public function __construct(
|
||||
private PaginatorFactoryInterface $paginatorFactory,
|
||||
private SerializerInterface $serializer,
|
||||
private Security $security,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Lists the versions of the specified stored object.
|
||||
*
|
||||
* @param StoredObject $storedObject the stored object whose versions are to be listed
|
||||
*
|
||||
* @return JsonResponse a JSON response containing the serialized versions of the stored object, encapsulated in a collection
|
||||
*
|
||||
* @throws AccessDeniedHttpException if the user is not allowed to see the stored object
|
||||
*/
|
||||
#[Route('/api/1.0/doc-store/stored-object/{uuid}/versions', name: 'chill_doc_store_stored_object_versions_list')]
|
||||
public function listVersions(StoredObject $storedObject): JsonResponse
|
||||
{
|
||||
if (!$this->security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)) {
|
||||
throw new AccessDeniedHttpException('not allowed to see this stored object');
|
||||
}
|
||||
|
||||
$total = $storedObject->getVersions()->count();
|
||||
$paginator = $this->paginatorFactory->create($total);
|
||||
|
||||
$criteria = Criteria::create();
|
||||
$criteria->orderBy(['id' => Order::Ascending]);
|
||||
$criteria->setMaxResults($paginator->getItemsPerPage())->setFirstResult($paginator->getCurrentPageFirstItemNumber());
|
||||
$items = $storedObject->getVersions()->matching($criteria);
|
||||
|
||||
return new JsonResponse(
|
||||
$this->serializer->serialize(
|
||||
new Collection($items, $paginator),
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read', StoredObjectVersionNormalizer::WITH_POINT_IN_TIMES_CONTEXT, StoredObjectVersionNormalizer::WITH_RESTORED_CONTEXT]]
|
||||
),
|
||||
json: true
|
||||
);
|
||||
}
|
||||
}
|
@ -18,6 +18,7 @@ use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\Common\Collections\Selectable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
@ -89,10 +90,10 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
private string $generationErrors = '';
|
||||
|
||||
/**
|
||||
* @var Collection<int, StoredObjectVersion>
|
||||
* @var Collection<int, StoredObjectVersion>&Selectable<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'storedObject', targetEntity: StoredObjectVersion::class, cascade: ['persist'], orphanRemoval: true)]
|
||||
private Collection $versions;
|
||||
private Collection&Selectable $versions;
|
||||
|
||||
/**
|
||||
* @param StoredObject::STATUS_* $status
|
||||
@ -256,7 +257,7 @@ class StoredObject implements Document, TrackCreationInterface
|
||||
return $this->template;
|
||||
}
|
||||
|
||||
public function getVersions(): Collection
|
||||
public function getVersions(): Collection&Selectable
|
||||
{
|
||||
return $this->versions;
|
||||
}
|
||||
|
@ -48,6 +48,25 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
#[ORM\OneToMany(mappedBy: 'objectVersion', targetEntity: StoredObjectPointInTime::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
|
||||
private Collection&Selectable $pointInTimes;
|
||||
|
||||
/**
|
||||
* Previous storedObjectVersion, from which the current stored object version is created.
|
||||
*
|
||||
* If null, the current stored object version is generated by other means.
|
||||
*
|
||||
* Those version may be associated with the same storedObject, or not. In this last case, that means that
|
||||
* the stored object's current version is created from another stored object version.
|
||||
*/
|
||||
#[ORM\ManyToOne(targetEntity: StoredObjectVersion::class)]
|
||||
private ?StoredObjectVersion $createdFrom = null;
|
||||
|
||||
/**
|
||||
* List of stored object versions created from the current version.
|
||||
*
|
||||
* @var Collection<int, StoredObjectVersion>
|
||||
*/
|
||||
#[ORM\OneToMany(mappedBy: 'createdFrom', targetEntity: StoredObjectVersion::class)]
|
||||
private Collection $children;
|
||||
|
||||
public function __construct(
|
||||
/**
|
||||
* The stored object associated with this version.
|
||||
@ -87,6 +106,7 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
) {
|
||||
$this->filename = $filename ?? self::generateFilename($this);
|
||||
$this->pointInTimes = new ArrayCollection();
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function generateFilename(StoredObjectVersion $storedObjectVersion): string
|
||||
@ -149,8 +169,6 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*
|
||||
* @internal use @see{StoredObjectPointInTime} constructor instead
|
||||
*/
|
||||
public function addPointInTime(StoredObjectPointInTime $storedObjectPointInTime): self
|
||||
@ -170,4 +188,42 @@ class StoredObjectVersion implements TrackCreationInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedFrom(): ?StoredObjectVersion
|
||||
{
|
||||
return $this->createdFrom;
|
||||
}
|
||||
|
||||
public function setCreatedFrom(?StoredObjectVersion $createdFrom): StoredObjectVersion
|
||||
{
|
||||
if (null === $createdFrom && null !== $this->createdFrom) {
|
||||
$this->createdFrom->removeChild($this);
|
||||
}
|
||||
|
||||
$createdFrom?->addChild($this);
|
||||
|
||||
$this->createdFrom = $createdFrom;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addChild(StoredObjectVersion $child): self
|
||||
{
|
||||
if (!$this->children->contains($child)) {
|
||||
$this->children->add($child);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeChild(StoredObjectVersion $child): self
|
||||
{
|
||||
$result = $this->children->removeElement($child);
|
||||
|
||||
if (false === $result) {
|
||||
throw new \UnexpectedValueException('the child is not associated with the current stored object version');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import DocumentActionButtonsGroup from "../../vuejs/DocumentActionButtonsGroup.v
|
||||
import {createApp} from "vue";
|
||||
import {StoredObject, StoredObjectStatusChange} from "../../types";
|
||||
import {is_object_ready} from "../../vuejs/StoredObjectButton/helpers";
|
||||
import ToastPlugin from "vue-toast-notification";
|
||||
|
||||
const i18n = _createI18n({});
|
||||
|
||||
@ -48,6 +49,6 @@ window.addEventListener('DOMContentLoaded', function (e) {
|
||||
}
|
||||
});
|
||||
|
||||
app.use(i18n).mount(el);
|
||||
app.use(i18n).use(ToastPlugin).mount(el);
|
||||
})
|
||||
});
|
||||
|
@ -62,6 +62,17 @@ export interface StoredObjectStatusChange {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface StoredObjectVersionWithPointInTime extends StoredObjectVersionPersisted {
|
||||
"point-in-times": StoredObjectPointInTime[];
|
||||
"from-restored": StoredObjectVersionPersisted|null;
|
||||
}
|
||||
|
||||
export interface StoredObjectPointInTime {
|
||||
id: number;
|
||||
byUser: User | null;
|
||||
reason: 'keep-before-conversion'|'keep-by-user';
|
||||
}
|
||||
|
||||
/**
|
||||
* Function executed by the WopiEditButton component.
|
||||
*/
|
||||
|
@ -14,7 +14,10 @@
|
||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||
</li>
|
||||
<li v-if="isDownloadable">
|
||||
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}"></download-button>
|
||||
<download-button :stored-object="props.storedObject" :at-version="props.storedObject.currentVersion" :filename="filename" :classes="{'dropdown-item': true}" :display-action-string-in-button="true"></download-button>
|
||||
</li>
|
||||
<li v-if="isHistoryViewable">
|
||||
<history-button :stored-object="props.storedObject" :can-edit="canEdit && props.storedObject._permissions.canEdit"></history-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -40,6 +43,7 @@ import {
|
||||
WopiEditButtonExecutableBeforeLeaveFunction
|
||||
} from "../types";
|
||||
import DesktopEditButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DesktopEditButton.vue";
|
||||
import HistoryButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton.vue";
|
||||
|
||||
interface DocumentActionButtonsGroupConfig {
|
||||
storedObject: StoredObject,
|
||||
@ -126,7 +130,11 @@ const isConvertibleToPdf = computed<boolean>(() => {
|
||||
&& is_extension_viewable(props.storedObject.currentVersion.type)
|
||||
&& props.storedObject.currentVersion.type !== 'application/pdf'
|
||||
&& props.storedObject.currentVersion.persisted !== false;
|
||||
})
|
||||
});
|
||||
|
||||
const isHistoryViewable = computed<boolean>(() => {
|
||||
return props.storedObject.status === 'ready';
|
||||
});
|
||||
|
||||
const checkForReady = function(): void {
|
||||
if (
|
||||
|
@ -3,6 +3,7 @@
|
||||
import {StoredObject, StoredObjectVersionCreated} from "../../types";
|
||||
import {encryptFile, fetchNewStoredObject, uploadVersion} from "../../js/async-upload/uploader";
|
||||
import {computed, ref, Ref} from "vue";
|
||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||
|
||||
interface DropFileConfig {
|
||||
existingDoc?: StoredObject,
|
||||
@ -111,16 +112,7 @@ const handleFile = async (file: File): Promise<void> => {
|
||||
<div class="drop-file">
|
||||
<div v-if="!uploading" :class="{ area: true, dragging: is_dragging}" @click="onZoneClick" @dragover="onDragOver" @dragleave="onDragLeave" @drop="onDrop">
|
||||
<p v-if="has_existing_doc" class="file-icon">
|
||||
<i class="fa fa-file-pdf-o" v-if="props.existingDoc?.type === 'application/pdf'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.oasis.opendocument.text'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.existingDoc?.type === 'application/msword'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.existingDoc?.type === 'application/vnd.ms-excel'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/jpeg'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.existingDoc?.type === 'image/png'"></i>
|
||||
<i class="fa fa-file-archive-o" v-else-if="props.existingDoc?.type === 'application/x-zip-compressed'"></i>
|
||||
<i class="fa fa-file-code-o" v-else ></i>
|
||||
<file-icon :type="props.existingDoc?.type"></file-icon>
|
||||
</p>
|
||||
|
||||
<p v-if="display_filename !== null" class="display-filename">{{ display_filename }}</p>
|
||||
|
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
interface FileIconConfig {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const props = defineProps<FileIconConfig>();
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<i class="fa fa-file-pdf-o" v-if="props.type === 'application/pdf'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.oasis.opendocument.text'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'"></i>
|
||||
<i class="fa fa-file-word-o" v-else-if="props.type === 'application/msword'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'"></i>
|
||||
<i class="fa fa-file-excel-o" v-else-if="props.type === 'application/vnd.ms-excel'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/jpeg'"></i>
|
||||
<i class="fa fa-file-image-o" v-else-if="props.type === 'image/png'"></i>
|
||||
<i class="fa fa-file-archive-o" v-else-if="props.type === 'application/x-zip-compressed'"></i>
|
||||
<i class="fa fa-file-code-o" v-else ></i>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)">
|
||||
<a v-if="!state.is_ready" :class="props.classes" @click="download_and_open($event)" title="Télécharger">
|
||||
<i class="fa fa-download"></i>
|
||||
Télécharger
|
||||
<template v-if="displayActionStringInButton">Télécharger</template>
|
||||
</a>
|
||||
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button">
|
||||
<a v-else :class="props.classes" target="_blank" :type="props.storedObject.type" :download="buildDocumentName()" :href="state.href_url" ref="open_button" title="Ouvrir">
|
||||
<i class="fa fa-external-link"></i>
|
||||
Ouvrir
|
||||
<template v-if="displayActionStringInButton">Ouvrir</template>
|
||||
</a>
|
||||
</template>
|
||||
|
||||
@ -20,6 +20,7 @@ interface DownloadButtonConfig {
|
||||
atVersion: StoredObjectVersion,
|
||||
classes: { [k: string]: boolean },
|
||||
filename?: string,
|
||||
displayActionStringInButton: boolean,
|
||||
}
|
||||
|
||||
interface DownloadButtonState {
|
||||
@ -28,7 +29,7 @@ interface DownloadButtonState {
|
||||
href_url: string,
|
||||
}
|
||||
|
||||
const props = defineProps<DownloadButtonConfig>();
|
||||
const props = withDefaults(defineProps<DownloadButtonConfig>(), {displayActionStringInButton: true});
|
||||
const state: DownloadButtonState = reactive({is_ready: false, is_running: false, href_url: "#"});
|
||||
|
||||
const open_button = ref<HTMLAnchorElement | null>(null);
|
||||
|
@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import HistoryButtonModal from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonModal.vue";
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../types";
|
||||
import {computed, reactive, ref, useTemplateRef} from "vue";
|
||||
import {get_versions} from "./HistoryButton/api";
|
||||
|
||||
interface HistoryButtonConfig {
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
interface HistoryButtonState {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
loaded: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonConfig>();
|
||||
const state = reactive<HistoryButtonState>({versions: [], loaded: false});
|
||||
const modal = useTemplateRef<typeof HistoryButtonModal>('modal');
|
||||
|
||||
const download_version_and_open_modal = async function (): Promise<void> {
|
||||
if (null !== modal.value) {
|
||||
modal.value.open();
|
||||
} else {
|
||||
console.log("modal is null");
|
||||
}
|
||||
|
||||
if (!state.loaded) {
|
||||
const versions = await get_versions(props.storedObject);
|
||||
|
||||
for (const version of versions) {
|
||||
state.versions.push(version);
|
||||
}
|
||||
state.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
const onRestoreVersion = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
state.versions.unshift(newVersion);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a @click="download_version_and_open_modal" class="dropdown-item">
|
||||
<history-button-modal ref="modal" :versions="state.versions" :stored-object="storedObject" :can-edit="canEdit" @restore-version="onRestoreVersion"></history-button-modal>
|
||||
<i class="fa fa-history"></i>
|
||||
Historique
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
i.fa::before {
|
||||
color: var(--bs-dropdown-link-hover-color);
|
||||
}
|
||||
</style>
|
@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
import HistoryButtonListItem from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonListItem.vue";
|
||||
import {computed, reactive} from "vue";
|
||||
|
||||
interface HistoryButtonListConfig {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
interface HistoryButtonListState {
|
||||
/**
|
||||
* Contains the number of the newly created version when a version is restored.
|
||||
*/
|
||||
restored: number;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonListConfig>();
|
||||
|
||||
const state = reactive<HistoryButtonListState>({restored: -1})
|
||||
|
||||
const higher_version = computed<number>(() => props.versions.reduce(
|
||||
(accumulator: number, version: StoredObjectVersionWithPointInTime) => Math.max(accumulator, version.version),
|
||||
-1
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Executed when a version in child component is restored.
|
||||
*
|
||||
* internally, keep track of the newly restored version
|
||||
*/
|
||||
const onRestored = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
state.restored = newVersion.version;
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="props.versions.length > 0">
|
||||
<div class="container">
|
||||
<template v-for="v in props.versions">
|
||||
<history-button-list-item
|
||||
:version="v"
|
||||
:can-edit="canEdit"
|
||||
:is-current="higher_version === v.version"
|
||||
:is-restored="v.version === state.restored"
|
||||
:stored-object="storedObject"
|
||||
@restore-version="onRestored"
|
||||
></history-button-list-item>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Chargement des versions</p>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObject, StoredObjectPointInTime, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge.vue";
|
||||
import {ISOToDatetime} from "./../../../../../../ChillMainBundle/Resources/public/chill/js/date";
|
||||
import FileIcon from "ChillDocStoreAssets/vuejs/FileIcon.vue";
|
||||
import RestoreVersionButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/RestoreVersionButton.vue";
|
||||
import DownloadButton from "ChillDocStoreAssets/vuejs/StoredObjectButton/DownloadButton.vue";
|
||||
import {computed} from "vue";
|
||||
|
||||
interface HistoryButtonListItemConfig {
|
||||
version: StoredObjectVersionWithPointInTime;
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
isCurrent: boolean;
|
||||
isRestored: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
const props = defineProps<HistoryButtonListItemConfig>();
|
||||
|
||||
const onRestore = ({newVersion}: {newVersion: StoredObjectVersionWithPointInTime}) => {
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
|
||||
const isKeptBeforeConversion = computed<boolean>(() => props.version["point-in-times"].reduce(
|
||||
(accumulator: boolean, pit: StoredObjectPointInTime) => accumulator || "keep-before-conversion" === pit.reason,
|
||||
false
|
||||
),
|
||||
);
|
||||
|
||||
const isRestored = computed<boolean>(() => null !== props.version["from-restored"]);
|
||||
|
||||
const classes = computed<{row: true, 'row-hover': true, 'blinking-1': boolean, 'blinking-2': boolean}>(() => ({row: true, 'row-hover': true, 'blinking-1': props.isRestored && 0 === props.version.version % 2, 'blinking-2': props.isRestored && 1 === props.version.version % 2}));
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="classes">
|
||||
<div class="col-12 tags" v-if="isCurrent || isKeptBeforeConversion || isRestored">
|
||||
<span class="badge bg-success" v-if="isCurrent">Version actuelle</span>
|
||||
<span class="badge bg-info" v-if="isKeptBeforeConversion">Conservée avant conversion dans un autre format</span>
|
||||
<span class="badge bg-info" v-if="isRestored">Restaurée depuis la version {{ version["from-restored"]?.version }}</span>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<file-icon :type="version.type"></file-icon> <span><strong>#{{ version.version + 1 }}</strong></span> <strong v-if="version.version == 0">Créé par</strong><strong v-else>modifié par</strong> <span class="badge-user"><UserRenderBoxBadge :user="version.createdBy"></UserRenderBoxBadge></span> <strong>à</strong> {{ $d(ISOToDatetime(version.createdAt.datetime8601), 'long') }}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<ul class="record_actions small slim on-version-actions">
|
||||
<li v-if="canEdit && !isCurrent">
|
||||
<restore-version-button :stored-object-version="props.version" @restore-version="onRestore"></restore-version-button>
|
||||
</li>
|
||||
<li>
|
||||
<download-button :stored-object="storedObject" :at-version="version" :classes="{btn: true, 'btn-outline-primary': true}" :display-action-string-in-button="false"></download-button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
div.tags {
|
||||
span.badge:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
// to make the animation restart, we have the same animation twice,
|
||||
// and alternate between both
|
||||
.blinking-1 {
|
||||
animation-name: backgroundColorPalette-1;
|
||||
animation-duration: 8s;
|
||||
animation-iteration-count: 1;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@keyframes backgroundColorPalette-1 {
|
||||
0% {
|
||||
background: var(--bs-chill-green-dark);
|
||||
}
|
||||
25% {
|
||||
background: var(--bs-chill-green);
|
||||
}
|
||||
65% {
|
||||
background: var(--bs-chill-beige);
|
||||
}
|
||||
100% {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
.blinking-2 {
|
||||
animation-name: backgroundColorPalette-2;
|
||||
animation-duration: 8s;
|
||||
animation-iteration-count: 1;
|
||||
animation-direction: normal;
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
@keyframes backgroundColorPalette-2 {
|
||||
0% {
|
||||
background: var(--bs-chill-green-dark);
|
||||
}
|
||||
25% {
|
||||
background: var(--bs-chill-green);
|
||||
}
|
||||
65% {
|
||||
background: var(--bs-chill-beige);
|
||||
}
|
||||
100% {
|
||||
background: unset;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
|
||||
import {reactive} from "vue";
|
||||
import HistoryButtonList from "ChillDocStoreAssets/vuejs/StoredObjectButton/HistoryButton/HistoryButtonList.vue";
|
||||
import {StoredObject, StoredObjectVersionWithPointInTime} from "./../../../types";
|
||||
|
||||
interface HistoryButtonListConfig {
|
||||
versions: StoredObjectVersionWithPointInTime[];
|
||||
storedObject: StoredObject;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
interface HistoryButtonModalState {
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<HistoryButtonListConfig>();
|
||||
const state = reactive<HistoryButtonModalState>({opened: false});
|
||||
|
||||
const open = () => {
|
||||
console.log('open');
|
||||
state.opened = true;
|
||||
}
|
||||
|
||||
defineExpose({open});
|
||||
|
||||
</script>
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<modal v-if="state.opened" @close="state.opened = false">
|
||||
<template v-slot:header>
|
||||
<h3>Historique des versions du document</h3>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<p>Les versions sont conservées pendant 90 jours.</p>
|
||||
<history-button-list :versions="props.versions" :can-edit="canEdit" :stored-object="storedObject" @restore-version="(payload) => emit('restoreVersion', payload)"></history-button-list>
|
||||
</template>
|
||||
</modal>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@ -0,0 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import {StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
||||
import {useToast} from "vue-toast-notification";
|
||||
import {restore_version} from "./api";
|
||||
|
||||
interface RestoreVersionButtonProps {
|
||||
storedObjectVersion: StoredObjectVersionPersisted,
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
restoreVersion: [newVersion: StoredObjectVersionWithPointInTime]
|
||||
}>()
|
||||
|
||||
const props = defineProps<RestoreVersionButtonProps>()
|
||||
|
||||
const $toast = useToast();
|
||||
|
||||
const restore_version_fn = async () => {
|
||||
const newVersion = await restore_version(props.storedObjectVersion);
|
||||
|
||||
$toast.success("Version restaurée");
|
||||
emit('restoreVersion', {newVersion});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="btn btn-outline-action" @click="restore_version_fn" title="Restaurer"><i class="fa fa-rotate-left"></i> Restaurer</button>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
</style>
|
@ -0,0 +1,12 @@
|
||||
import {StoredObject, StoredObjectVersionPersisted, StoredObjectVersionWithPointInTime} from "../../../types";
|
||||
import {fetchResults, makeFetch} from "../../../../../../ChillMainBundle/Resources/public/lib/api/apiMethods";
|
||||
|
||||
export const get_versions = async (storedObject: StoredObject): Promise<StoredObjectVersionWithPointInTime[]> => {
|
||||
const versions = await fetchResults<StoredObjectVersionWithPointInTime>(`/api/1.0/doc-store/stored-object/${storedObject.uuid}/versions`);
|
||||
|
||||
return versions.sort((a: StoredObjectVersionWithPointInTime, b: StoredObjectVersionWithPointInTime) => b.version - a.version);
|
||||
}
|
||||
|
||||
export const restore_version = async (version: StoredObjectVersionPersisted): Promise<StoredObjectVersionWithPointInTime> => {
|
||||
return await makeFetch<null, StoredObjectVersionWithPointInTime>("POST", `/api/1.0/doc-store/stored-object/restore-from-version/${version.id}`);
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
|
||||
class StoredObjectPointInTimeNormalizer implements NormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
{
|
||||
/* @var StoredObjectPointInTime $object */
|
||||
return [
|
||||
'id' => $object->getId(),
|
||||
'reason' => $object->getReason()->value,
|
||||
'byUser' => $this->normalizer->normalize($object->getByUser(), $format, [AbstractNormalizer::GROUPS => 'read']),
|
||||
];
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null)
|
||||
{
|
||||
return $data instanceof StoredObjectPointInTime;
|
||||
}
|
||||
}
|
@ -12,6 +12,8 @@ declare(strict_types=1);
|
||||
namespace Chill\DocStoreBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
@ -20,13 +22,17 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
final public const WITH_POINT_IN_TIMES_CONTEXT = 'with-point-in-times';
|
||||
|
||||
final public const WITH_RESTORED_CONTEXT = 'with-restored';
|
||||
|
||||
public function normalize($object, ?string $format = null, array $context = [])
|
||||
{
|
||||
if (!$object instanceof StoredObjectVersion) {
|
||||
throw new \InvalidArgumentException('The object must be an instance of '.StoredObjectVersion::class);
|
||||
}
|
||||
|
||||
return [
|
||||
$data = [
|
||||
'id' => $object->getId(),
|
||||
'filename' => $object->getFilename(),
|
||||
'version' => $object->getVersion(),
|
||||
@ -34,8 +40,18 @@ class StoredObjectVersionNormalizer implements NormalizerInterface, NormalizerAw
|
||||
'keyInfos' => $object->getKeyInfos(),
|
||||
'type' => $object->getType(),
|
||||
'createdAt' => $this->normalizer->normalize($object->getCreatedAt(), $format, $context),
|
||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, $context),
|
||||
'createdBy' => $this->normalizer->normalize($object->getCreatedBy(), $format, [...$context, UserNormalizer::AT_DATE => $object->getCreatedAt()]),
|
||||
];
|
||||
|
||||
if (in_array(self::WITH_POINT_IN_TIMES_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
||||
$data['point-in-times'] = $this->normalizer->normalize($object->getPointInTimes(), $format, $context);
|
||||
}
|
||||
|
||||
if (in_array(self::WITH_RESTORED_CONTEXT, $context[AbstractNormalizer::GROUPS] ?? [], true)) {
|
||||
$data['from-restored'] = $this->normalizer->normalize($object->getCreatedFrom(), $format, [AbstractNormalizer::GROUPS => ['read']]);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null, array $context = [])
|
||||
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
final readonly class StoredObjectRestore implements StoredObjectRestoreInterface
|
||||
{
|
||||
public function __construct(private readonly StoredObjectManagerInterface $storedObjectManager, private readonly LoggerInterface $logger) {}
|
||||
|
||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion
|
||||
{
|
||||
$oldContent = $this->storedObjectManager->read($storedObjectVersion);
|
||||
|
||||
$newVersion = $this->storedObjectManager->write($storedObjectVersion->getStoredObject(), $oldContent, $storedObjectVersion->getType());
|
||||
|
||||
$newVersion->setCreatedFrom($storedObjectVersion);
|
||||
|
||||
$this->logger->info('[StoredObjectRestore] Restore stored object version', [
|
||||
'stored_object_uuid' => $storedObjectVersion->getStoredObject()->getUuid(),
|
||||
'old_version_id' => $storedObjectVersion->getId(),
|
||||
'old_version_version' => $storedObjectVersion->getVersion(),
|
||||
'new_version_id' => $newVersion->getVersion(),
|
||||
]);
|
||||
|
||||
return $newVersion;
|
||||
}
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
|
||||
/**
|
||||
* Restore an old version of the stored object as the current one.
|
||||
*/
|
||||
interface StoredObjectRestoreInterface
|
||||
{
|
||||
public function restore(StoredObjectVersion $storedObjectVersion): StoredObjectVersion;
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectVersion;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestoreInterface;
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectRestoreVersionApiController;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectRestoreVersionApiControllerTest extends TestCase
|
||||
{
|
||||
public function testRestoreStoredObjectVersion(): void
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
||||
|
||||
$security->expects($this->once())
|
||||
->method('isGranted')
|
||||
->willReturn(true);
|
||||
$storedObjectRestore->expects($this->once())
|
||||
->method('restore')
|
||||
->willReturn($storedObjectVersion);
|
||||
$entityManager->expects($this->once())
|
||||
->method('persist');
|
||||
$entityManager->expects($this->once())
|
||||
->method('flush');
|
||||
|
||||
$serializer->expects($this->once())
|
||||
->method('serialize')
|
||||
->willReturn('test');
|
||||
|
||||
$response = $controller->restoreStoredObjectVersion($storedObjectVersion);
|
||||
|
||||
self::assertEquals(200, $response->getStatusCode());
|
||||
self::assertEquals('test', $response->getContent());
|
||||
}
|
||||
|
||||
public function testRestoreStoredObjectVersionAccessDenied(): void
|
||||
{
|
||||
$security = $this->createMock(Security::class);
|
||||
$storedObjectRestore = $this->createMock(StoredObjectRestoreInterface::class);
|
||||
$entityManager = $this->createMock(EntityManagerInterface::class);
|
||||
$serializer = $this->createMock(SerializerInterface::class);
|
||||
$storedObjectVersion = $this->createMock(StoredObjectVersion::class);
|
||||
$controller = new StoredObjectRestoreVersionApiController($security, $storedObjectRestore, $entityManager, $serializer);
|
||||
|
||||
self::expectException(\Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException::class);
|
||||
$security->expects($this->once())
|
||||
->method('isGranted')
|
||||
->willReturn(false);
|
||||
$controller->restoreStoredObjectVersion($storedObjectVersion);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Controller;
|
||||
|
||||
use Chill\DocStoreBundle\Controller\StoredObjectVersionApiController;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Security\Authorization\StoredObjectRoleEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectVersionNormalizer;
|
||||
use Chill\MainBundle\Pagination\Paginator;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactoryInterface;
|
||||
use Chill\MainBundle\Serializer\Normalizer\CollectionNormalizer;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Serializer\Encoder\JsonEncoder;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectVersionApiControllerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testListVersion(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
for ($i = 0; $i < 15; ++$i) {
|
||||
$storedObject->registerVersion();
|
||||
}
|
||||
|
||||
$security = $this->prophesize(Security::class);
|
||||
$security->isGranted(StoredObjectRoleEnum::SEE->value, $storedObject)
|
||||
->willReturn(true)
|
||||
->shouldBeCalledOnce();
|
||||
|
||||
$controller = $this->buildController($security->reveal());
|
||||
|
||||
$response = $controller->listVersions($storedObject);
|
||||
$body = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
|
||||
|
||||
self::assertEquals($response->getStatusCode(), 200);
|
||||
self::assertIsArray($body);
|
||||
self::assertArrayHasKey('results', $body);
|
||||
self::assertCount(10, $body['results']);
|
||||
}
|
||||
|
||||
private function buildController(Security $security): StoredObjectVersionApiController
|
||||
{
|
||||
$paginator = $this->prophesize(Paginator::class);
|
||||
$paginator->getCurrentPageFirstItemNumber()->willReturn(0);
|
||||
$paginator->getItemsPerPage()->willReturn(10);
|
||||
$paginator->getTotalItems()->willReturn(15);
|
||||
$paginator->hasNextPage()->willReturn(false);
|
||||
$paginator->hasPreviousPage()->willReturn(false);
|
||||
|
||||
$paginatorFactory = $this->prophesize(PaginatorFactoryInterface::class);
|
||||
$paginatorFactory->create(Argument::type('int'))->willReturn($paginator);
|
||||
|
||||
$serializer = new Serializer([
|
||||
new StoredObjectVersionNormalizer(), new CollectionNormalizer(),
|
||||
], [new JsonEncoder()]);
|
||||
|
||||
return new StoredObjectVersionApiController($paginatorFactory->reveal(), $serializer, $security);
|
||||
}
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Serializer\Normalizer;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTime;
|
||||
use Chill\DocStoreBundle\Entity\StoredObjectPointInTimeReasonEnum;
|
||||
use Chill\DocStoreBundle\Serializer\Normalizer\StoredObjectPointInTimeNormalizer;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\Argument;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Symfony\Component\Clock\MockClock;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Component\Serializer\Serializer;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectPointInTimeNormalizerTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testNormalize(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->registerVersion();
|
||||
$storedObjectPointInTime = new StoredObjectPointInTime($version, StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION, new User());
|
||||
|
||||
$normalizer = new StoredObjectPointInTimeNormalizer();
|
||||
$normalizer->setNormalizer($this->buildNormalizer());
|
||||
|
||||
$actual = $normalizer->normalize($storedObjectPointInTime, 'json', ['read']);
|
||||
|
||||
self::assertIsArray($actual);
|
||||
self::assertArrayHasKey('id', $actual);
|
||||
self::assertArrayHasKey('byUser', $actual);
|
||||
self::assertArrayHasKey('reason', $actual);
|
||||
self::assertEquals(StoredObjectPointInTimeReasonEnum::KEEP_BEFORE_CONVERSION->value, $actual['reason']);
|
||||
}
|
||||
|
||||
public function buildNormalizer(): NormalizerInterface
|
||||
{
|
||||
$userRender = $this->prophesize(UserRender::class);
|
||||
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn('username');
|
||||
|
||||
return new Serializer(
|
||||
[new UserNormalizer($userRender->reveal(), new MockClock())]
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\DocStoreBundle\Tests\Service;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||
use Chill\DocStoreBundle\Service\StoredObjectRestore;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Prophecy\PhpUnit\ProphecyTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*
|
||||
* @coversNothing
|
||||
*/
|
||||
class StoredObjectRestoreTest extends TestCase
|
||||
{
|
||||
use ProphecyTrait;
|
||||
|
||||
public function testRestore(): void
|
||||
{
|
||||
$storedObject = new StoredObject();
|
||||
$version = $storedObject->registerVersion(type: 'application/test');
|
||||
|
||||
$storedObjectManager = $this->prophesize(StoredObjectManagerInterface::class);
|
||||
$storedObjectManager->read($version)->willReturn('1234')->shouldBeCalledOnce();
|
||||
$storedObjectManager->write($storedObject, '1234', 'application/test')->shouldBeCalledOnce()
|
||||
->will(function ($args) {
|
||||
/** @var StoredObject $object */
|
||||
$object = $args[0];
|
||||
|
||||
return $object->registerVersion();
|
||||
})
|
||||
;
|
||||
|
||||
$restore = new StoredObjectRestore($storedObjectManager->reveal(), new NullLogger());
|
||||
|
||||
$newVersion = $restore->restore($version);
|
||||
|
||||
self::assertNotSame($version, $newVersion);
|
||||
self::assertSame($version, $newVersion->getCreatedFrom());
|
||||
}
|
||||
}
|
@ -105,3 +105,50 @@ paths:
|
||||
404:
|
||||
description: "Not found"
|
||||
|
||||
/1.0/doc-store/stored-object/{uuid}/versions:
|
||||
get:
|
||||
tags:
|
||||
- storedobject
|
||||
summary: Get a signed route to post stored object
|
||||
parameters:
|
||||
- in: path
|
||||
name: uuid
|
||||
required: true
|
||||
allowEmptyValue: false
|
||||
description: The UUID of the storedObjeect
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
403:
|
||||
description: "Unauthorized"
|
||||
404:
|
||||
description: "Not found"
|
||||
|
||||
/1.0/doc-store/stored-object/restore-from-version/{id}:
|
||||
post:
|
||||
tags:
|
||||
- storedobject
|
||||
summary: Restore an old version of a stored object
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
allowEmptyValue: false
|
||||
description: The id of the stored object version
|
||||
schema:
|
||||
type: integer
|
||||
responses:
|
||||
200:
|
||||
description: "OK"
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
|
||||
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Chill\Migrations\DocStore;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
final class Version20240918073234 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add a relation between stored object version when a version is restored';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD createdFrom_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version ADD CONSTRAINT FK_C1D553024DEC38BB FOREIGN KEY (createdFrom_id) REFERENCES chill_doc.stored_object_version (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||
$this->addSql('CREATE INDEX IDX_C1D553024DEC38BB ON chill_doc.stored_object_version (createdFrom_id)');
|
||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d55302232d562b RENAME TO IDX_C1D553024B136083');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('ALTER TABLE chill_doc.stored_object_version DROP createdFrom_id');
|
||||
$this->addSql('ALTER INDEX chill_doc.idx_c1d553024b136083 RENAME TO idx_c1d55302232d562b');
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user