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'