From b1dbf8235be255c8ce0680da138c0d7d370d3c5a Mon Sep 17 00:00:00 2001 From: Julio Montoya Date: Thu, 4 Jun 2020 10:05:42 +0200 Subject: [PATCH] Fix file upload --- assets/vue/components/Toolbar.vue | 1 + assets/vue/components/documents/Form.vue | 25 +++--- assets/vue/mixins/CreateMixin.js | 5 ++ assets/vue/router/documents.js | 4 +- assets/vue/services/api.js | 3 +- assets/vue/utils/fetch.js | 59 ++++++++------ assets/vue/views/documents/Create.vue | 5 +- assets/vue/views/documents/CreateFile.vue | 5 +- .../Controller/CreateResourceFileAction.php | 1 + .../CreateResourceNodeFileAction.php | 31 ++++++++ src/CoreBundle/Entity/AbstractResource.php | 51 +++++++++++- src/CoreBundle/Entity/Course.php | 2 +- .../Entity/Listener/CourseListener.php | 2 +- .../Entity/Listener/ResourceListener.php | 79 ++++++++++++++++--- src/CoreBundle/Entity/ResourceFile.php | 4 +- .../Entity/ResourceToCourseInterface.php | 10 +++ .../Entity/ResourceToRootInterface.php | 10 +++ .../Entity/ResourceWithUrlInterface.php | 10 +++ src/CoreBundle/ToolChain.php | 4 +- src/CourseBundle/Entity/CDocument.php | 54 +++++++++++-- 20 files changed, 296 insertions(+), 69 deletions(-) create mode 100644 src/CoreBundle/Controller/CreateResourceNodeFileAction.php create mode 100644 src/CoreBundle/Entity/ResourceToCourseInterface.php create mode 100644 src/CoreBundle/Entity/ResourceToRootInterface.php create mode 100644 src/CoreBundle/Entity/ResourceWithUrlInterface.php diff --git a/assets/vue/components/Toolbar.vue b/assets/vue/components/Toolbar.vue index b78f31d967..fa3e5fbe74 100644 --- a/assets/vue/components/Toolbar.vue +++ b/assets/vue/components/Toolbar.vue @@ -47,6 +47,7 @@ mdi-plus-circle + Add folder diff --git a/assets/vue/components/documents/Form.vue b/assets/vue/components/documents/Form.vue index 39e991be89..16e95264d1 100644 --- a/assets/vue/components/documents/Form.vue +++ b/assets/vue/components/documents/Form.vue @@ -11,8 +11,8 @@ @input="$v.item.title.$touch()" @blur="$v.item.title.$touch()" /> - - + + @@ -34,22 +34,25 @@ export default { type: Object, required: true }, - errors: { type: Object, default: () => {} }, - initialValues: { type: Object, default: () => {} + }, + type: { + type: String, } }, - mounted() { + created () { }, data() { return { title: null, + parentResourceNode: null, + resourceFile: null }; }, computed: { @@ -57,20 +60,18 @@ export default { item() { return this.initialValues || this.values; }, - titleErrors() { const errors = []; if (!this.$v.item.title.$dirty) return errors; - has(this.violations, 'title') && errors.push(this.violations.title); - !this.$v.item.title.required && errors.push(this.$t('Field is required')); return errors; }, - - + typeIsFile() { + return this.type === 'file'; + }, violations() { return this.errors || {}; } @@ -81,6 +82,10 @@ export default { item: { title: { required, + }, + parentResourceNode: { + }, + resourceFile: { } } } diff --git a/assets/vue/mixins/CreateMixin.js b/assets/vue/mixins/CreateMixin.js index c263c10614..8bd5d2f62d 100644 --- a/assets/vue/mixins/CreateMixin.js +++ b/assets/vue/mixins/CreateMixin.js @@ -16,6 +16,11 @@ export default { onSendForm() { const createForm = this.$refs.createForm; createForm.$v.$touch(); + if (createForm.$v.item.parentResourceNode) { + let nodeId = this.$route.params.node; + createForm.$v.item.$model.parentResourceNode = '/api/resource_nodes/' + nodeId; + console.log(createForm.$v.item.$model.parentResourceNode); + } if (!createForm.$v.$invalid) { this.create(createForm.$v.item.$model); } diff --git a/assets/vue/router/documents.js b/assets/vue/router/documents.js index b3aadf0e77..52539e7ea9 100644 --- a/assets/vue/router/documents.js +++ b/assets/vue/router/documents.js @@ -1,5 +1,5 @@ export default { - path: '/resources/documents/:node/', + path: '/resources/document/:node/', meta: { requiresAuth: true }, name: 'documents', component: () => import('../components/documents/Layout'), @@ -17,7 +17,7 @@ export default { }, { name: 'DocumentsCreateFile', - path: 'new', + path: 'new_file', component: () => import('../views/documents/CreateFile') }, { diff --git a/assets/vue/services/api.js b/assets/vue/services/api.js index 69576af72a..1e6a2be09f 100644 --- a/assets/vue/services/api.js +++ b/assets/vue/services/api.js @@ -9,7 +9,8 @@ export default function makeService(endpoint) { return fetch(endpoint, params); }, create(payload) { - return fetch(endpoint, { method: 'POST', body: JSON.stringify(payload) }); + return fetch(endpoint, { method: 'POST', body: payload }); + //return fetch(endpoint, { method: 'POST', body: JSON.stringify(payload) }); }, del(item) { return fetch(item['@id'], { method: 'DELETE' }); diff --git a/assets/vue/utils/fetch.js b/assets/vue/utils/fetch.js index ceee663e24..67fc823e36 100644 --- a/assets/vue/utils/fetch.js +++ b/assets/vue/utils/fetch.js @@ -9,35 +9,46 @@ const makeParamArray = (key, arr) => arr.map(val => `${key}[]=${val}`).join('&'); export default function(id, options = {}) { - if ('undefined' === typeof options.headers) options.headers = new Headers(); + if ('undefined' === typeof options.headers) options.headers = new Headers(); - if (null === options.headers.get('Accept')) - options.headers.set('Accept', MIME_TYPE); + if (null === options.headers.get('Accept')) + options.headers.set('Accept', MIME_TYPE); - if ( - 'undefined' !== options.body && - !(options.body instanceof FormData) && - null === options.headers.get('Content-Type') - ) - options.headers.set('Content-Type', MIME_TYPE); + /*if ( + 'undefined' !== options.body && + !(options.body instanceof FormData) && + null === options.headers.get('Content-Type') + ) + options.headers.set('Content-Type', MIME_TYPE);*/ - if (options.params) { - const params = normalize(options.params); - let queryString = Object.keys(params) - .map(key => - Array.isArray(params[key]) - ? makeParamArray(key, params[key]) - : `${key}=${params[key]}` - ) - .join('&'); - id = `${id}?${queryString}`; - } + if (options.params) { + const params = normalize(options.params); + let queryString = Object.keys(params) + .map(key => + Array.isArray(params[key]) + ? makeParamArray(key, params[key]) + : `${key}=${params[key]}` + ) + .join('&'); + id = `${id}?${queryString}`; + } - const entryPoint = ENTRYPOINT + (ENTRYPOINT.endsWith('/') ? '' : '/'); + const entryPoint = ENTRYPOINT + (ENTRYPOINT.endsWith('/') ? '' : '/'); - const payload = options.body && JSON.parse(options.body); - if (isObject(payload) && payload['@id']) - options.body = JSON.stringify(normalize(payload)); + let formData = new FormData(); + if (options.body) { + Object.keys(options.body).forEach(function (key) { + // key: the name of the object key + // index: the ordinal position of the key within the object + formData.append(key, options.body[key]); + }); + + options.body = formData; + } + + /*const payload = options.body && JSON.parse(options.body); + if (isObject(payload) && payload['@id']) + options.body = JSON.stringify(normalize(payload));*/ return global.fetch(new URL(id, entryPoint), options).then(response => { if (response.ok) return response; diff --git a/assets/vue/views/documents/Create.vue b/assets/vue/views/documents/Create.vue index 044dc585f5..ec3da34268 100644 --- a/assets/vue/views/documents/Create.vue +++ b/assets/vue/views/documents/Create.vue @@ -1,7 +1,7 @@ @@ -32,7 +32,8 @@ export default { }, data() { return { - item: {} + item: {}, + type: 'folder' }; }, computed: { diff --git a/assets/vue/views/documents/CreateFile.vue b/assets/vue/views/documents/CreateFile.vue index 044dc585f5..50cfd10eb2 100644 --- a/assets/vue/views/documents/CreateFile.vue +++ b/assets/vue/views/documents/CreateFile.vue @@ -1,7 +1,7 @@ @@ -32,7 +32,8 @@ export default { }, data() { return { - item: {} + item: {}, + type: 'file', }; }, computed: { diff --git a/src/CoreBundle/Controller/CreateResourceFileAction.php b/src/CoreBundle/Controller/CreateResourceFileAction.php index 2df9a4ece6..84e2a8820b 100644 --- a/src/CoreBundle/Controller/CreateResourceFileAction.php +++ b/src/CoreBundle/Controller/CreateResourceFileAction.php @@ -18,6 +18,7 @@ class CreateResourceFileAction } $resourceFile = new ResourceFile(); + $resourceFile->setName($uploadedFile->getFilename()); $resourceFile->setFile($uploadedFile); return $resourceFile; diff --git a/src/CoreBundle/Controller/CreateResourceNodeFileAction.php b/src/CoreBundle/Controller/CreateResourceNodeFileAction.php new file mode 100644 index 0000000000..e370ce4879 --- /dev/null +++ b/src/CoreBundle/Controller/CreateResourceNodeFileAction.php @@ -0,0 +1,31 @@ +files->get('resourceFile'); + if (!$uploadedFile) { + throw new BadRequestHttpException('"resourceFile" is required'); + } + $document = new CDocument(); + $document->setTitle($request->get('title')); + $document->setComment($request->get('comment')); + $nodeId = (int) str_replace('/api/resource_nodes/','',$request->get('parentResourceNode')); + $document->setParentResourceNode($nodeId); + $document->setResourceFile($uploadedFile); + + return $document; + } +} diff --git a/src/CoreBundle/Entity/AbstractResource.php b/src/CoreBundle/Entity/AbstractResource.php index 05a9071232..91920ed0ca 100644 --- a/src/CoreBundle/Entity/AbstractResource.php +++ b/src/CoreBundle/Entity/AbstractResource.php @@ -28,17 +28,62 @@ abstract class AbstractResource public $contentUrl; /** - * SerializedName("description"). - * * @Assert\Valid() * @ApiSubresource() - * @Groups({"resource_node:read", "resource_node:write", "document:read","document:write"}) + * @Groups({"resource_node:read", "resource_node:write"}) * @GRID\Column(field="resourceNode.createdAt", title="Date added", type="datetime") * @ORM\OneToOne(targetEntity="Chamilo\CoreBundle\Entity\ResourceNode", cascade={"persist", "remove"}, orphanRemoval=true) * @ORM\JoinColumn(name="resource_node_id", referencedColumnName="id", onDelete="CASCADE") */ public $resourceNode; + /** + * @Groups({"resource_node:read", "resource_node:write", "document:read","document:write"}) + */ + public $parentResourceNode; + + /** + * @ApiProperty(iri="http://schema.org/image") + * @Groups({"resource_node:read", "resource_node:write", "document:read","document:write"}) + */ + public $resourceFile; + + public function hasParentResourceNode(): bool + { + return null !== $this->parentResourceNode; + } + + public function setParentResourceNode($resourceNode): self + { + $this->parentResourceNode = $resourceNode; + + return $this; + } + + public function getParentResourceNode() + { + return $this->parentResourceNode; + } + + public function hasResourceFile(): bool + { + return null !== $this->resourceFile; + } + + public function getResourceFile() + { + return $this->resourceFile; + } + + public function setResourceFile($file) + { + $this->resourceFile = $file; + return $this; + } + + + + public function setResourceNode(ResourceNode $resourceNode): self { $this->resourceNode = $resourceNode; diff --git a/src/CoreBundle/Entity/Course.php b/src/CoreBundle/Entity/Course.php index deb7edfa8a..9e4ad527e8 100644 --- a/src/CoreBundle/Entity/Course.php +++ b/src/CoreBundle/Entity/Course.php @@ -48,7 +48,7 @@ use Symfony\Component\Validator\Constraints as Assert; * @ORM\Entity * @ORM\EntityListeners({"Chamilo\CoreBundle\Entity\Listener\ResourceListener", "Chamilo\CoreBundle\Entity\Listener\CourseListener"}) */ -class Course extends AbstractResource implements ResourceInterface +class Course extends AbstractResource implements ResourceInterface, ResourceWithUrlInterface, ResourceToRootInterface { public const CLOSED = 0; public const REGISTERED = 1; diff --git a/src/CoreBundle/Entity/Listener/CourseListener.php b/src/CoreBundle/Entity/Listener/CourseListener.php index 7f435bffd1..e64b5307e2 100644 --- a/src/CoreBundle/Entity/Listener/CourseListener.php +++ b/src/CoreBundle/Entity/Listener/CourseListener.php @@ -54,7 +54,7 @@ class CourseListener */ public function prePersist(Course $course, LifecycleEventArgs $args) { - /** @var AccessUrlRelCourse $urlRelCourse */ + error_log('Course listener prePersist'); if ($course) { /*$urlRelCourse = $course->getUrls()->first(); $url = $urlRelCourse->getUrl();*/ diff --git a/src/CoreBundle/Entity/Listener/ResourceListener.php b/src/CoreBundle/Entity/Listener/ResourceListener.php index 5a695b5587..d60bf77448 100644 --- a/src/CoreBundle/Entity/Listener/ResourceListener.php +++ b/src/CoreBundle/Entity/Listener/ResourceListener.php @@ -6,6 +6,7 @@ namespace Chamilo\CoreBundle\Entity\Listener; use Chamilo\CoreBundle\Entity\AbstractResource; use Chamilo\CoreBundle\Entity\Course; +use Chamilo\CoreBundle\Entity\ResourceFile; use Chamilo\CoreBundle\Entity\ResourceNode; use Chamilo\CoreBundle\Entity\ResourceToCourseInterface; use Chamilo\CoreBundle\Entity\ResourceToRootInterface; @@ -15,6 +16,7 @@ use Chamilo\CoreBundle\ToolChain; use Cocur\Slugify\SlugifyInterface; use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Event\PreUpdateEventArgs; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Security; @@ -25,6 +27,7 @@ class ResourceListener { protected $slugify; protected $request; + protected $accessUrl; /** * ResourceListener constructor. @@ -35,21 +38,59 @@ class ResourceListener $this->security = $security; $this->toolChain = $toolChain; $this->request = $request; + $this->accessUrl = null; + } + + public function getAccessUrl($em) + { + if (null === $this->accessUrl) { + $request = $this->request->getCurrentRequest(); + if (null === $request) { + throw new \Exception('An Request is needed'); + } + $sessionRequest = $request->getSession(); + + if (null === $sessionRequest) { + throw new \Exception('An Session is needed'); + } + + $id = $sessionRequest->get('access_url_id'); + $url = $em->getRepository('ChamiloCoreBundle:AccessUrl')->find($id); + + if ($url) { + $this->accessUrl = $url; + + return $url; + } + } + + if (null === $this->accessUrl) { + throw new \Exception('An AccessUrl is needed'); + } + + return $this->accessUrl; } public function prePersist(AbstractResource $resource, LifecycleEventArgs $args) { + error_log('ResourceListener prePersist '.get_class($resource)); $em = $args->getEntityManager(); - $request = $this->request->getCurrentRequest(); + $request = $this->request; - if ($request && $resource instanceof ResourceWithUrlInterface) { - $sessionRequest = $request->getSession(); - if (null !== $sessionRequest) { - $id = $sessionRequest->get('access_url_id'); - $url = $em->getRepository('ChamiloCoreBundle:AccessUrl')->find($id); - $resource->addUrl($url); + $url = null; + if ($resource instanceof ResourceWithUrlInterface) { + $url = $this->getAccessUrl($em); + $resource->addUrl($url); + } + + if ($resource->hasResourceNode()) { + if ($resource instanceof ResourceToRootInterface) { + $url = $this->getAccessUrl($em); + $resource->getResourceNode()->setParent($url->getResourceNode()); } - throw new \Exception('A Url is needed'); + + // Do not override resource node already added. + return true; } // Add resource node @@ -79,18 +120,34 @@ class ResourceListener ; if ($resource instanceof ResourceToRootInterface) { + $url = $this->getAccessUrl($em); $resourceNode->setParent($url->getResourceNode()); } + if ($resource->hasParentResourceNode()) { + $nodeRepo = $em->getRepository('ChamiloCoreBundle:ResourceNode'); + $parent = $nodeRepo->find($resource->getParentResourceNode()); + $resourceNode->setParent($parent); + } + + if ($resource->hasResourceFile()) { + /** @var File $uploadedFile */ + $uploadedFile = $request->getCurrentRequest()->files->get('resourceFile'); + $resourceFile = new ResourceFile(); + $resourceFile->setName($uploadedFile->getFilename()); + $resourceFile->setOriginalName($uploadedFile->getFilename()); + $resourceFile->setFile($uploadedFile); + + $em->persist($resourceFile); + $resourceNode->setResourceFile($resourceFile); + } + if ($resource instanceof ResourceToCourseInterface) { //$this->request->getCurrentRequest()->getSession()->get('access_url_id'); //$resourceNode->setParent($url->getResourceNode()); } - - $resource->setResourceNode($resourceNode); - $em->persist($resourceNode); return $resourceNode; diff --git a/src/CoreBundle/Entity/ResourceFile.php b/src/CoreBundle/Entity/ResourceFile.php index 40afade9e1..00e1070dc7 100644 --- a/src/CoreBundle/Entity/ResourceFile.php +++ b/src/CoreBundle/Entity/ResourceFile.php @@ -32,7 +32,7 @@ use Vich\UploaderBundle\Mapping\Annotation as Vich; * "controller"=CreateResourceFileAction::class, * "deserialize"=false, * "security"="is_granted('ROLE_USER')", - * "validation_groups"={"Default", "media_object_create"}, + * "validation_groups"={"Default", "media_object_create", "document:write"}, * "openapi_context"={ * "requestBody"={ * "content"={ @@ -128,7 +128,7 @@ class ResourceFile /** * @var File * - * @Assert\NotNull(groups={"media_object_create"}) + * @Assert\NotNull(groups={"media_object_create", "document:write"}) * @Vich\UploadableField( * mapping="resources", * fileNameProperty="name", diff --git a/src/CoreBundle/Entity/ResourceToCourseInterface.php b/src/CoreBundle/Entity/ResourceToCourseInterface.php new file mode 100644 index 0000000000..e545e11c0c --- /dev/null +++ b/src/CoreBundle/Entity/ResourceToCourseInterface.php @@ -0,0 +1,10 @@ +getTools(); $manager = $this->entityManager; $toolVisibility = $this->settingsManager->getSetting('course.active_tools_on_create'); - $user = $this->security->getToken()->getUser(); + $token = $this->security->getToken(); + $user = $token->getUser(); // Hardcoded tool list order $toolList = [ @@ -184,7 +185,6 @@ class ToolChain ->setVisibility($visibility) ->setCategory($tool->getCategory()) ; - $toolRepository->addResourceToCourse($courseTool, ResourceLink::VISIBILITY_PUBLISHED, $user, $course); $course->addTool($courseTool); } diff --git a/src/CourseBundle/Entity/CDocument.php b/src/CourseBundle/Entity/CDocument.php index ebcf31500b..8a53a50f42 100644 --- a/src/CourseBundle/Entity/CDocument.php +++ b/src/CourseBundle/Entity/CDocument.php @@ -12,7 +12,9 @@ use APY\DataGridBundle\Grid\Mapping as GRID; use Chamilo\CoreBundle\Entity\AbstractResource; use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\ResourceInterface; +use Chamilo\CoreBundle\Entity\ResourceToCourseInterface; use Chamilo\CoreBundle\Entity\Session; +use Chamilo\CoreBundle\Controller\CreateResourceNodeFileAction; use Chamilo\CourseBundle\Traits\ShowCourseResourcesInSessionTrait; use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Mapping as ORM; @@ -23,13 +25,49 @@ use Symfony\Component\Serializer\Annotation\Groups; * @ApiResource( * shortName="Documents", * normalizationContext={"groups"={"document:read", "resource_node:read"}}, - * denormalizationContext={"groups"={"document:write"}} + * denormalizationContext={"groups"={"document:write"}}, + * collectionOperations={ + * "post"={ + * "controller"=CreateResourceNodeFileAction::class, + * "deserialize"=false, + * "security"="is_granted('ROLE_USER')", + * "validation_groups"={"Default", "media_object_create", "document:write"}, + * "openapi_context"={ + * "requestBody"={ + * "content"={ + * "multipart/form-data"={ + * "schema"={ + * "type"="object", + * "properties"={ + * "resourceFile"={ + * "type"="string", + * "format"="binary" + * }, + * "title"={ + * "type"="string", + * }, + * "comment"={ + * "type"="string", + * }, +* "parentResourceNode"={ + * "type"="object", + * }, + * } + * } + * } + * } + * } + * } + * }, + * "get" + * }, * ) * @ApiFilter(SearchFilter::class, properties={"title": "partial", "resourceNode.parent": "exact"}) * @ApiFilter( * OrderFilter::class, * properties={ * "id", + * "filetype", * "resourceNode.title", * "resourceNode.createdAt", * "resourceNode.resourceFile.size", @@ -52,7 +90,7 @@ use Symfony\Component\Serializer\Annotation\Groups; * @GRID\Source(columns="iid, title", filterable=false, groups={"editor"}) * @ORM\Entity */ -class CDocument extends AbstractResource implements ResourceInterface +class CDocument extends AbstractResource implements ResourceInterface, ResourceToCourseInterface { use ShowCourseResourcesInSessionTrait; @@ -81,19 +119,19 @@ class CDocument extends AbstractResource implements ResourceInterface /** * @var string - * * @Groups({"document:read", "document:write"}) - * - * @ORM\Column(name="comment", type="text", nullable=true) + * @ORM\Column(name="title", type="string", length=255, nullable=true) */ - protected $comment; + protected $title; /** * @var string + * * @Groups({"document:read", "document:write"}) - * @ORM\Column(name="title", type="string", length=255, nullable=true) + * + * @ORM\Column(name="comment", type="text", nullable=true) */ - protected $title; + protected $comment; /** * @var string File type, it can be 'folder' or 'file'