Merge branch 'issue511_document_evaluations' into 'master'

AccompanyingCourseWorkEdit: replace document by a new one, fix deleting

See merge request Chill-Projet/chill-bundles!384
This commit is contained in:
Julien Fastré 2022-03-21 15:17:34 +00:00
commit 0a0243eb85
15 changed files with 157 additions and 39 deletions

View File

@ -11,6 +11,9 @@ and this project adheres to
## Unreleased ## Unreleased
<!-- write down unreleased development here --> <!-- write down unreleased development here -->
* [person] AccompanyingCourseWorkEdit: fix deleting evaluation documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/546)
* [person] AccompanyingCourseWorkEdit: download existing documents (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/512)
* [person] AccompanyingCourseWorkEdit: replace document by a new one (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/511)
* [person] AccompanyingPeriodWork: add referrers to work, add doctrine event listener to add logged user to referrers collection and display a referrers list in work list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/502) * [person] AccompanyingPeriodWork: add referrers to work, add doctrine event listener to add logged user to referrers collection and display a referrers list in work list (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/502)
* [person] AccompanyingPeriodWorkEvaluation: fix circular reference when serialising (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/495) * [person] AccompanyingPeriodWorkEvaluation: fix circular reference when serialising (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/495)
* [person] order accompanying period by opening date in search persons, person and household period lists (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/493) * [person] order accompanying period by opening date in search persons, person and household period lists (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/493)

View File

@ -61,13 +61,13 @@ class StoredObject implements AsyncFileInterface, Document
/** /**
* @var int[] * @var int[]
* @ORM\Column(type="json", name="iv") * @ORM\Column(type="json", name="iv")
* @Serializer\Groups({"write"}) * @Serializer\Groups({"read", "write"})
*/ */
private array $iv = []; private array $iv = [];
/** /**
* @ORM\Column(type="json", name="key") * @ORM\Column(type="json", name="key")
* @Serializer\Groups({"write"}) * @Serializer\Groups({"read", "write"})
*/ */
private array $keyInfos = []; private array $keyInfos = [];

View File

@ -73,6 +73,7 @@ var download = (button) => {
button.type = mimeType; button.type = mimeType;
button.textContent = labelReady; button.textContent = labelReady;
if (hasFilename) { if (hasFilename) {
button.download = filename; button.download = filename;
if (extension !== false) { if (extension !== false) {
button.download = button.download + '.' + extension; button.download = button.download + '.' + extension;
@ -92,4 +93,4 @@ window.addEventListener('load', function(e) {
initializeButtons(e.target); initializeButtons(e.target);
}); });
module.exports = initializeButtons; export { initializeButtons, download };

View File

@ -1,6 +1,6 @@
var algo = 'AES-CBC'; var algo = 'AES-CBC';
import Dropzone from 'dropzone'; import Dropzone from 'dropzone';
var initializeDownload = require('./downloader.js'); import { initializeButtons } from './downloader.js';
/** /**
@ -359,7 +359,7 @@ var insertDownloadButton = (zone, zoneData) => {
addBelowButton(newButton, zone, zoneData); addBelowButton(newButton, zone, zoneData);
//zone.appendChild(newButton); //zone.appendChild(newButton);
initializeDownload(zone); initializeButtons(zone);
}; };
window.addEventListener('load', function(e) { window.addEventListener('load', function(e) {

View File

@ -1,5 +1,5 @@
<template> <template>
<a class="btn btn-create" :title="$t(buttonTitle)" @click="openModal"> <a :class="btnClasses" :title="$t(buttonTitle)" @click="openModal">
<span>{{ $t(buttonTitle) }}</span> <span>{{ $t(buttonTitle) }}</span>
</a> </a>
<teleport to="body"> <teleport to="body">
@ -96,10 +96,27 @@ export default {
Modal Modal
}, },
i18n, i18n,
props: [ props: {
'buttonTitle', buttonTitle: {
'options' type: String,
], default: 'Ajouter un document',
},
options: {
type: Object,
default: {
maxFiles: 1,
maxPostSize: 262144000, // 250MB
required: false,
}
},
btnClasses: {
type: Object,
default: {
btn: true,
'btn-create': true
}
}
},
emits: ['addDocument'], emits: ['addDocument'],
data() { data() {
return { return {

View File

@ -0,0 +1,45 @@
<template>
<a
class="btn btn-download"
:title="$t(buttonTitle)"
:data-key=JSON.stringify(storedObject.keyInfos)
:data-iv=JSON.stringify(storedObject.iv)
:data-mime-type=storedObject.type
:data-label-preparing="$t('dataLabelPreparing')"
:data-label-ready="$t('dataLabelReady')"
:data-temp-url-get-generator="url"
@click.once="downloadDocument">
</a>
</template>
<script>
import { download } from '../../module/async_upload/downloader';
const i18n = {
messages: {
fr: {
dataLabelPreparing: "Chargement...",
dataLabelReady: "",
}
}
};
export default {
name: "AddAsyncUploadDownloader",
i18n,
props: [
'buttonTitle',
'storedObject'
],
computed: {
url() {
return `/asyncupload/temp_url/generate/GET?object_name=${this.storedObject.filename}`;
}
},
methods: {
downloadDocument(e) {
download(e.target);
}
}
}
</script>

View File

@ -100,10 +100,8 @@ final class UserControllerTest extends WebTestCase
$crawler = $this->client->followRedirect(); $crawler = $this->client->followRedirect();
// Check data in the show view // Check data in the show view
$this->assertGreaterThan( $this->assertStringContainsString(
0, "Test_user", $crawler->text(), "page contains the name of the user"
$crawler->filter('td:contains("Test_user")')->count(),
'Missing element td:contains("Test user")'
); );
//test the auth of the new client //test the auth of the new client
@ -125,11 +123,7 @@ final class UserControllerTest extends WebTestCase
$this->client->submit($form); $this->client->submit($form);
$crawler = $this->client->followRedirect(); $crawler = $this->client->followRedirect();
// Check the element contains an attribute with value equals "Foo" // Check the element contains an attribute with value equals "Foo"
$this->assertGreaterThan( $this->assertResponseIsSuccessful();
0,
$crawler->filter('[data-username="' . $username . '"]')->count(),
'Missing element [data-username="Foo bar"]'
);
} }
/** /**

View File

@ -15,7 +15,6 @@ use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\Entity\UserRender; use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface; use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

View File

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\EventListener; namespace Chill\PersonBundle\EventListener;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\Security;
@ -25,6 +26,8 @@ class AccompanyingPeriodWorkEventListener
public function prePersistAccompanyingPeriodWork(AccompanyingPeriodWork $work): void public function prePersistAccompanyingPeriodWork(AccompanyingPeriodWork $work): void
{ {
$work->addReferrer($this->security->getUser()); if ($this->security->getUser() instanceof User) {
$work->addReferrer($this->security->getUser());
}
} }
} }

View File

@ -16,8 +16,9 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Security; use DateTime;
use Symfony\Component\Security\Core\Security;
use function count; use function count;
final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface
@ -55,7 +56,7 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
) )
) )
->setParameter('user', $user) ->setParameter('user', $user)
->setParameter('now', new \DateTime('now')) ->setParameter('now', new DateTime('now'))
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT); ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT);
return $qb; return $qb;
@ -140,7 +141,7 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
->setMaxResults($limit); ->setMaxResults($limit);
foreach ($orderBy as $field => $direction) { foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('ap.'.$field, $direction); $qb->addOrderBy('ap.' . $field, $direction);
} }
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();

View File

@ -16,6 +16,8 @@ use Chill\PersonBundle\Entity\Person;
interface AccompanyingPeriodACLAwareRepositoryInterface interface AccompanyingPeriodACLAwareRepositoryInterface
{ {
public function countByUserOpenedAccompanyingPeriod(?User $user): int;
public function findByPerson( public function findByPerson(
Person $person, Person $person,
string $role, string $role,
@ -25,6 +27,4 @@ interface AccompanyingPeriodACLAwareRepositoryInterface
): array; ): array;
public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array;
public function countByUserOpenedAccompanyingPeriod(?User $user): int;
} }

View File

@ -127,7 +127,7 @@
</div> </div>
<ul class="record_actions" v-if="evaluationsForAction.length > 0"> <ul class="record_actions" v-if="evaluationsForAction.length > 0">
<li> <li>
<button :title="$t('add_an_evaluation')" class="btn btn-create" @click="toggleAddEvaluation"></button> <button :title="$t('add_an_evaluation')" class="btn btn-create" @click="toggleAddEvaluation">{{ $t('add_an_evaluation') }}</button>
</li> </li>
</ul> </ul>
<div v-else> <div v-else>

View File

@ -27,7 +27,7 @@
</li> </li>
<li v-if="canDelete"> <li v-if="canDelete">
<a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')"></a> <a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')">{{ $t('delete_evaluation')}}</a>
</li> </li>
</ul> </ul>
</div> </div>
@ -72,7 +72,8 @@ const i18n = {
sure: "Êtes-vous sûr?", sure: "Êtes-vous sûr?",
sure_description: "Cette évaluation sera supprimée de cette action d'accompagnement", sure_description: "Cette évaluation sera supprimée de cette action d'accompagnement",
ok: "Supprimer" ok: "Supprimer"
} },
delete_evaluation: "Supprimer l'évaluation",
} }
} }
}; };

View File

@ -65,7 +65,7 @@
<h5>{{ $t('Documents') }} :</h5> <h5>{{ $t('Documents') }} :</h5>
<div class="flex-table"> <div class="flex-table">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.key"> <div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id">
<div class="item-row"> <div class="item-row">
<div class="input-group input-group-lg mb-3"> <div class="input-group input-group-lg mb-3">
<div> <div>
@ -75,6 +75,7 @@
type="text" type="text"
:value="d.title" :value="d.title"
:id="d.id" :id="d.id"
:data-key="i"
@input="onInputDocumentTitle"/> @input="onInputDocumentTitle"/>
</div> </div>
</div> </div>
@ -84,6 +85,8 @@
<p v-if="d.createdBy" class="createdBy">Créé par {{ d.createdBy.text }}<br/> <p v-if="d.createdBy" class="createdBy">Créé par {{ d.createdBy.text }}<br/>
Le {{ $d(ISOToDatetime(d.createdAt.datetime), 'long') }}</p> Le {{ $d(ISOToDatetime(d.createdAt.datetime), 'long') }}</p>
</div> </div>
</div>
<div class="item-row">
<div class="item-col"> <div class="item-col">
<ul class="record_actions" > <ul class="record_actions" >
<li v-if="d.workflows_availables.length > 0"> <li v-if="d.workflows_availables.length > 0">
@ -98,6 +101,23 @@
@go-to-generate-workflow="goToGenerateWorkflowEvaluationDocument" @go-to-generate-workflow="goToGenerateWorkflowEvaluationDocument"
></list-workflow-modal> ></list-workflow-modal>
</li> </li>
<li>
<add-async-upload
:buttonTitle="$t('replace')"
:options="asyncUploadOptions"
:btnClasses="{'btn': true, 'btn-edit': true}"
@addDocument="(arg) => replaceDocument(d, arg)"
>
</add-async-upload>
</li>
<li>
<add-async-upload-downloader
:buttonTitle="$t('download')"
:storedObject="d.storedObject"
>
</add-async-upload-downloader>
</li>
<li> <li>
<a :href="buildEditLink(d.storedObject)" class="btn btn-wopilink"></a> <a :href="buildEditLink(d.storedObject)" class="btn btn-wopilink"></a>
</li> </li>
@ -152,6 +172,7 @@ import { mapGetters, mapState } from 'vuex';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue'; import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator'; import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload.vue'; import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload.vue';
import AddAsyncUploadDownloader from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUploadDownloader.vue';
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue'; import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js'; import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
@ -176,7 +197,9 @@ const i18n = {
document_upload: "Téléverser un document", document_upload: "Téléverser un document",
document_title: "Titre du document", document_title: "Titre du document",
template_title: "Nom du template", template_title: "Nom du template",
browse: "Ajouter un document" browse: "Ajouter un document",
replace: "Remplacer",
download: "Télécharger le fichier existant"
} }
} }
}; };
@ -188,6 +211,7 @@ export default {
ckeditor: CKEditor.component, ckeditor: CKEditor.component,
PickTemplate, PickTemplate,
AddAsyncUpload, AddAsyncUpload,
AddAsyncUploadDownloader,
ListWorkflowModal, ListWorkflowModal,
}, },
i18n, i18n,
@ -274,8 +298,9 @@ export default {
}, },
onInputDocumentTitle(event) { onInputDocumentTitle(event) {
const id = Number(event.target.id); const id = Number(event.target.id);
const key = Number(event.target.dataset.key) + 1;
const title = event.target.value; const title = event.target.value;
this.$store.commit('updateDocumentTitle', {id: id, evaluationKey: this.evaluation.key, title: title}); this.$store.commit('updateDocumentTitle', {id: id, key: key, evaluationKey: this.evaluation.key, title: title});
}, },
addDocument(storedObject) { addDocument(storedObject) {
let document = { let document = {
@ -285,6 +310,14 @@ export default {
}; };
this.$store.commit('addDocument', {key: this.evaluation.key, document: document}); this.$store.commit('addDocument', {key: this.evaluation.key, document: document});
}, },
replaceDocument(oldDocument, storedObject) {
let document = {
type: 'accompanying_period_work_evaluation_document',
storedObject: storedObject,
title: oldDocument.title
};
this.$store.commit('replaceDocument', {key: this.evaluation.key, document: document, oldDocument: oldDocument});
},
removeDocument(document) { removeDocument(document) {
if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) { if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) {
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document}); this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});

View File

@ -136,9 +136,9 @@ const store = createStore({
endDate: e.endDate !== null ? ISOToDatetime(e.endDate.datetime) : null, endDate: e.endDate !== null ? ISOToDatetime(e.endDate.datetime) : null,
maxDate: e.maxDate !== null ? ISOToDatetime(e.maxDate.datetime) : null, maxDate: e.maxDate !== null ? ISOToDatetime(e.maxDate.datetime) : null,
warningInterval: e.warningInterval !== null ? intervalISOToDays(e.warningInterval) : null, warningInterval: e.warningInterval !== null ? intervalISOToDays(e.warningInterval) : null,
documents: e.documents.map((d, dindex) => { documents: e.documents.map((d, docIndex) => {
return Object.assign(d, { return Object.assign(d, {
key: index key: docIndex
}); });
}), }),
}); });
@ -218,13 +218,26 @@ const store = createStore({
})); }));
}, },
removeDocument(state, {key, document}) { removeDocument(state, {key, document}) {
let evaluations = state.evaluationsPicked.find(e => e.key === key); let evaluation = state.evaluationsPicked.find(e => e.key === key);
if (evaluation === undefined) {
if (evaluations === undefined) { return;
}
evaluation.documents = evaluation.documents.filter(d => d.key !== document.key);
},
replaceDocument(state, payload) {
let evaluation = state.evaluationsPicked.find(e => e.key === payload.key);
if (evaluation === undefined) {
return; return;
} }
evaluations.documents = evaluations.documents.filter(d => d.key !== document.key); let newDocument = Object.assign(
payload.document, {
key: evaluation.documents.length + 1,
workflows_availables: state.work.workflows_availables_evaluation_documents,
workflows: [],
}
);
evaluation.documents = evaluation.documents.map(d => d.id === payload.oldDocument.id ? newDocument : d);
}, },
addEvaluation(state, evaluation) { addEvaluation(state, evaluation) {
let e = { let e = {
@ -326,8 +339,13 @@ const store = createStore({
state.isPosting = st; state.isPosting = st;
}, },
updateDocumentTitle(state, payload) { updateDocumentTitle(state, payload) {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey) if (payload.id === 0) {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
.documents.find(d => d.key === payload.key).title = payload.title;
} else {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
.documents.find(d => d.id === payload.id).title = payload.title; .documents.find(d => d.id === payload.id).title = payload.title;
}
} }
}, },
actions: { actions: {
@ -424,6 +442,9 @@ const store = createStore({
removeDocument({commit}, payload) { removeDocument({commit}, payload) {
commit('removeDocument', payload); commit('removeDocument', payload);
}, },
replaceDocument({commit}, payload) {
commit('replaceDocument', payload);
},
submit({ getters, state, commit }, callback) { submit({ getters, state, commit }, callback) {
let let
payload = getters.buildPayload, payload = getters.buildPayload,