Fix file upload

pull/3282/head
Julio Montoya 6 years ago
parent 0b38e9dbd6
commit b1dbf8235b
  1. 1
      assets/vue/components/Toolbar.vue
  2. 25
      assets/vue/components/documents/Form.vue
  3. 5
      assets/vue/mixins/CreateMixin.js
  4. 4
      assets/vue/router/documents.js
  5. 3
      assets/vue/services/api.js
  6. 59
      assets/vue/utils/fetch.js
  7. 5
      assets/vue/views/documents/Create.vue
  8. 5
      assets/vue/views/documents/CreateFile.vue
  9. 1
      src/CoreBundle/Controller/CreateResourceFileAction.php
  10. 31
      src/CoreBundle/Controller/CreateResourceNodeFileAction.php
  11. 51
      src/CoreBundle/Entity/AbstractResource.php
  12. 2
      src/CoreBundle/Entity/Course.php
  13. 2
      src/CoreBundle/Entity/Listener/CourseListener.php
  14. 79
      src/CoreBundle/Entity/Listener/ResourceListener.php
  15. 4
      src/CoreBundle/Entity/ResourceFile.php
  16. 10
      src/CoreBundle/Entity/ResourceToCourseInterface.php
  17. 10
      src/CoreBundle/Entity/ResourceToRootInterface.php
  18. 10
      src/CoreBundle/Entity/ResourceWithUrlInterface.php
  19. 4
      src/CoreBundle/ToolChain.php
  20. 54
      src/CourseBundle/Entity/CDocument.php

@ -47,6 +47,7 @@
<v-btn v-if="handleAdd" color="primary" rounded @click="addItem">
<v-icon>mdi-plus-circle</v-icon>
Add folder
</v-btn>
<v-btn v-if="handleAddDocument" color="primary" rounded @click="addDocument">

@ -11,8 +11,8 @@
@input="$v.item.title.$touch()"
@blur="$v.item.title.$touch()"
/>
<v-file-input v-model="item.resourceNode" show-size counter multiple label="File input"></v-file-input>
<v-file-input v-if="typeIsFile" v-model="item.resourceFile" show-size label="File input"></v-file-input>
<input type="hidden" v-model="item.parentResourceNode" />
</v-col>
</v-row>
</v-container>
@ -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: {
}
}
}

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

@ -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')
},
{

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

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

@ -1,7 +1,7 @@
<template>
<div>
<Toolbar :handle-submit="onSendForm" :handle-reset="resetForm"></Toolbar>
<DocumentsForm ref="createForm" :values="item" :errors="violations" />
<DocumentsForm ref="createForm" :values="item" :errors="violations" :type="type" />
<Loading :visible="isLoading" />
</div>
</template>
@ -32,7 +32,8 @@ export default {
},
data() {
return {
item: {}
item: {},
type: 'folder'
};
},
computed: {

@ -1,7 +1,7 @@
<template>
<div>
<Toolbar :handle-submit="onSendForm" :handle-reset="resetForm"></Toolbar>
<DocumentsForm ref="createForm" :values="item" :errors="violations" />
<DocumentsForm ref="createForm" :values="item" :errors="violations" :type="type" />
<Loading :visible="isLoading" />
</div>
</template>
@ -32,7 +32,8 @@ export default {
},
data() {
return {
item: {}
item: {},
type: 'file',
};
},
computed: {

@ -18,6 +18,7 @@ class CreateResourceFileAction
}
$resourceFile = new ResourceFile();
$resourceFile->setName($uploadedFile->getFilename());
$resourceFile->setFile($uploadedFile);
return $resourceFile;

@ -0,0 +1,31 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\ResourceFile;
use Chamilo\CourseBundle\Entity\CDocument;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class CreateResourceNodeFileAction
{
public function __invoke(Request $request): CDocument
{
/** @var UploadedFile $uploadedFile */
$uploadedFile = $request->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;
}
}

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

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

@ -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();*/

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

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

@ -0,0 +1,10 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
interface ResourceToCourseInterface
{
}

@ -0,0 +1,10 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
interface ResourceToRootInterface
{
}

@ -0,0 +1,10 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
interface ResourceWithUrlInterface
{
public function addUrl(AccessUrl $url);
}

@ -134,7 +134,8 @@ class ToolChain
$tools = $this->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);
}

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

Loading…
Cancel
Save