Documents: Add toggle visibility button + test

pull/3989/head
Julio 5 years ago
parent bb93479a47
commit 77a77207bc
  1. 26
      assets/vue/mixins/ListMixin.js
  2. 4
      assets/vue/store/modules/crud.js
  3. 33
      assets/vue/views/documents/List.vue
  4. 23
      src/CoreBundle/Controller/Api/UpdateVisibilityDocument.php
  5. 12
      src/CoreBundle/DataProvider/Extension/CDocumentExtension.php
  6. 27
      src/CoreBundle/Repository/ResourceRepository.php
  7. 40
      src/CourseBundle/Entity/CDocument.php
  8. 83
      tests/CourseBundle/Repository/CDocumentRepositoryTest.php

@ -5,6 +5,8 @@ import toInteger from 'lodash/toInteger';
import { formatDateTime } from '../utils/dates';
import NotificationMixin from './NotificationMixin';
import {ENTRYPOINT} from "../config/entrypoint";
import axios from "axios";
export default {
mixins: [NotificationMixin],
@ -31,7 +33,6 @@ export default {
},
watch: {
$route() {
console.log('watch listmixin');
// react to route changes...
this.resetList = true;
let nodeId = this.$route.params['node'];
@ -89,7 +90,6 @@ export default {
params[`resourceNode.parent`] = this.$route.params.node;
}
console.log(params);
this.resetList = true;
this.getPage(params).then(() => {
this.pagination.sortBy = sortBy;
@ -203,20 +203,16 @@ export default {
this.filters['loadNode'] = 0;
delete this.filters['resourceNode.parent'];
this.resetList = true;
this.$router.push({ name: `${this.$options.servicePrefix}Shared` , query: folderParams});
},
showHandler(item) {
console.log('listmixin showHandler');
let folderParams = this.$route.query;
console.log(folderParams, 'folderParams');
console.log(this.$route.params, 'params');
console.log(item);
if (item) {
folderParams['id'] = item['@id'];
}
console.log(folderParams);
this.$router.push({
name: `${this.$options.servicePrefix}Show`,
@ -238,8 +234,19 @@ export default {
query: folderParams,
});
},
changeVisibilityHandler(item, slotProps) {
let folderParams = this.$route.query;
folderParams['id'] = item['@id'];
axios
.put(item['@id'] + '/toggle_visibility', {
})
.then(response => {
let data = response.data;
item['resourceLinkListFromEntity'] = data['resourceLinkListFromEntity'];
})
;
},
editHandler(item) {
console.log('editHandler');
let folderParams = this.$route.query;
folderParams['id'] = item['@id'];
@ -255,7 +262,8 @@ export default {
folderParams['getFile'] = true;
if (item.resourceNode.resourceFile &&
item.resourceNode.resourceFile.mimeType &&
'text/html' === item.resourceNode.resourceFile.mimeType) {
'text/html' === item.resourceNode.resourceFile.mimeType
) {
//folderParams['getFile'] = true;
}
@ -267,8 +275,6 @@ export default {
}
},
deleteHandler(item) {
console.log('deleteHandler');
console.log(item);
this.pagination.page = 1;
this.deleteItem(item).then(() =>
this.onRequest({pagination: this.pagination})

@ -203,13 +203,9 @@ export default function makeCrudModule({
loadWithQuery: ({ commit }, params= {}) => {
if (!service) throw new Error('No service specified!');
console.log('crud loadWithQuery');
const id = params['id'];
delete params['id'];
/*console.log(id, 'id');
console.log(commit, 'commit');*/
if (isEmpty(id)) {
throw new Error('Incorrect id');
}

@ -187,8 +187,15 @@
<Column :exportable="false">
<template #body="slotProps">
<div class="flex flex-row gap-2">
<Button icon="fa fa-info-circle" class="btn btn-primary " @click="showHandler(slotProps.data)" />
<Button icon="fa fa-info-circle" class="btn btn-primary" @click="showHandler(slotProps.data)" />
<Button v-if="isAuthenticated && isCurrentTeacher" class="btn btn-primary" @click="changeVisibilityHandler(slotProps.data, slotProps)">
<v-icon v-if="slotProps.data.resourceLinkListFromEntity[0].visibility == 2" icon="mdi-eye"/>
<v-icon v-if="slotProps.data.resourceLinkListFromEntity[0].visibility == 0" icon="mdi-eye-off"/>
</Button>
<Button v-if="isAuthenticated && isCurrentTeacher" icon="pi pi-pencil" class="btn btn-primary p-mr-2" @click="editHandler(slotProps.data)" />
<Button v-if="isAuthenticated && isCurrentTeacher" class="btn btn-danger" @click="confirmDeleteItem(slotProps.data)" >
<v-icon icon="mdi-delete"/>
</Button>
@ -297,22 +304,14 @@ import ActionCell from '../../components/ActionCell.vue';
//import Toolbar from '../../components/Toolbar.vue';
import ResourceFileIcon from '../../components/documents/ResourceFileIcon.vue';
import ResourceFileLink from '../../components/documents/ResourceFileLink.vue';
import { useRoute } from 'vue-router'
import DataFilter from '../../components/DataFilter';
import DocumentsFilterForm from '../../components/documents/Filter';
import { ref, reactive, onMounted, computed } from 'vue';
import { useStore } from 'vuex';
import isEmpty from 'lodash/isEmpty';
import moment from "moment";
import toInteger from "lodash/toInteger";
import {RESOURCE_LINK_PUBLISHED} from "../../components/resource_links/visibility";
export default {
name: 'DocumentsList',
servicePrefix: 'Documents',
components: {
//8Toolbar,
ActionCell,
ResourceFileIcon,
ResourceFileLink,
@ -324,12 +323,12 @@ export default {
return {
sortBy: 'title',
sortDesc: false,
columnsQua: [
{align: 'left', name: 'resourceNode.title', label: this.$i18n.t('Title'), field: 'resourceNode.title', sortable: true},
{align: 'left', name: 'resourceNode.updatedAt', label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', sortable: true},
{name: 'resourceNode.resourceFile.size', label: this.$i18n.t('Size'), field: 'resourceNode.resourceFile.size', sortable: true},
{name: 'action', label: this.$i18n.t('Actions'), field: 'action', sortable: false}
],
// columnsQua: [
// {align: 'left', name: 'resourceNode.title', label: this.$i18n.t('Title'), field: 'resourceNode.title', sortable: true},
// {align: 'left', name: 'resourceNode.updatedAt', label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', sortable: true},
// {name: 'resourceNode.resourceFile.size', label: this.$i18n.t('Size'), field: 'resourceNode.resourceFile.size', sortable: true},
// {name: 'action', label: this.$i18n.t('Actions'), field: 'action', sortable: false}
// ],
columns: [
{ label: this.$i18n.t('Title'), field: 'title', name: 'title', sortable: true},
{ label: this.$i18n.t('Modified'), field: 'resourceNode.updatedAt', name: 'updatedAt', sortable: true},
@ -351,12 +350,11 @@ export default {
};
},
created() {
console.log('created - vue/views/documents/List.vue');
//console.log('created - vue/views/documents/List.vue');
this.filters['loadNode'] = 1;
},
mounted() {
this.filters['loadNode'] = 1;
console.log('mounted - vue/views/documents/List.vue');
this.onUpdateOptions(this.options);
// Detect when scrolled to bottom.
@ -461,7 +459,6 @@ export default {
this.createWithFormData(this.item);
this.showMessage('Saved');
}
this.itemDialog = false;
this.item = {};
}

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CourseBundle\Entity\CDocument;
use Chamilo\CourseBundle\Repository\CDocumentRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpKernel\Attribute\AsController;
#[AsController]
class UpdateVisibilityDocument extends AbstractController
{
public function __invoke(CDocument $document, CDocumentRepository $repo): CDocument
{
$repo->toggleVisibilityPublishedDraft($document);
return $document;
}
}

@ -84,10 +84,14 @@ final class CDocumentExtension implements QueryCollectionExtensionInterface //,
->setParameter('visibilityDeleted', ResourceLink::VISIBILITY_DELETED)
;
$queryBuilder
->andWhere('links.visibility != :visibilityDraft')
->setParameter('visibilityDraft', ResourceLink::VISIBILITY_DRAFT)
;
$isAllowedToSeeDraft = $this->security->isGranted(['ROLE_ADMIN', 'ROLE_CURRENT_COURSE_TEACHER']);
if ($isAllowedToSeeDraft) {
$queryBuilder
->andWhere('links.visibility != :visibilityDraft')
->setParameter('visibilityDraft', ResourceLink::VISIBILITY_DRAFT)
;
}
$queryBuilder
->andWhere('links.course = :course')

@ -551,14 +551,24 @@ abstract class ResourceRepository extends ServiceEntityRepository
$this->setLinkVisibility($resource, ResourceLink::VISIBILITY_DELETED);
}
public function setVisibilityPublished(AbstractResource $resource): void
public function toggleVisibilityPublishedDraft(AbstractResource $resource): void
{
$this->setLinkVisibility($resource, ResourceLink::VISIBILITY_PUBLISHED);
$firstLink = $resource->getFirstResourceLink();
if (ResourceLink::VISIBILITY_PUBLISHED === $firstLink->getVisibility()) {
$this->setVisibilityDraft($resource);
return;
}
if (ResourceLink::VISIBILITY_DRAFT === $firstLink->getVisibility()) {
$this->setVisibilityPublished($resource);
}
}
public function setVisibilityDeleted(AbstractResource $resource): void
public function setVisibilityPublished(AbstractResource $resource): void
{
$this->setLinkVisibility($resource, ResourceLink::VISIBILITY_DELETED);
$this->setLinkVisibility($resource, ResourceLink::VISIBILITY_PUBLISHED);
}
public function setVisibilityDraft(AbstractResource $resource): void
@ -566,6 +576,11 @@ abstract class ResourceRepository extends ServiceEntityRepository
$this->setLinkVisibility($resource, ResourceLink::VISIBILITY_DRAFT);
}
public function setVisibilityDeleted(AbstractResource $resource): void
{
$this->setLinkVisibility($resource, ResourceLink::VISIBILITY_DELETED);
}
public function setVisibilityPending(AbstractResource $resource): void
{
$this->setLinkVisibility($resource, ResourceLink::VISIBILITY_PENDING);
@ -805,9 +820,7 @@ abstract class ResourceRepository extends ServiceEntityRepository
$link->setVisibility($visibility);
if (ResourceLink::VISIBILITY_DRAFT === $visibility) {
$editorMask = ResourceNodeVoter::getEditorMask();
//$rights = [];
$resourceRight = new ResourceRight();
$resourceRight
$resourceRight = (new ResourceRight())
->setMask($editorMask)
->setRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_TEACHER)
->setResourceLink($link)

@ -7,12 +7,14 @@ declare(strict_types=1);
namespace Chamilo\CourseBundle\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use Chamilo\CoreBundle\Controller\Api\CreateDocumentFileAction;
use Chamilo\CoreBundle\Controller\Api\UpdateDocumentFileAction;
use Chamilo\CoreBundle\Controller\Api\UpdateVisibilityDocument;
use Chamilo\CoreBundle\Entity\AbstractResource;
use Chamilo\CoreBundle\Entity\ResourceInterface;
use Chamilo\CoreBundle\Traits\ShowCourseResourcesInSessionTrait;
@ -32,12 +34,18 @@ use Symfony\Component\Validator\Constraints as Assert;
* "security" = "is_granted('EDIT', object.resourceNode)",
* "validation_groups"={"media_object_create", "document:write"},
* },
* "put_toggle_visibility" = {
* "method" = "PUT",
* "path"="/documents/{iid}/toggle_visibility",
* "controller"=UpdateVisibilityDocument::class,
* },
* "get" = {
* "security" = "is_granted('VIEW', object.resourceNode)",
* },
* "delete" = {
* "security" = "is_granted('DELETE', object.resourceNode)",
* },
*
* },
* collectionOperations={
* "post"={
@ -135,20 +143,6 @@ use Symfony\Component\Validator\Constraints as Assert;
* },
* )
*
* //resourceNode.resourceLinks.course can be used but instead cid/sid/gid is used
*
* @ApiFilter(
* OrderFilter::class,
* properties={
* "id",
* "filetype",
* "resourceNode.title",
* "resourceNode.createdAt",
* "resourceNode.resourceFile.size",
* "resourceNode.updatedAt"
* }
* )
*
* @ORM\Table(
* name="c_document",
* indexes={
@ -163,36 +157,46 @@ use Symfony\Component\Validator\Constraints as Assert;
'title' => 'partial',
'resourceNode.parent' => 'exact',
])]
//resourceNode.resourceLinks.course can be used but instead cid/sid/gid is used
#[ApiFilter(OrderFilter::class, properties: [
'iid',
'filetype',
'resourceNode.title',
'resourceNode.createdAt',
'resourceNode.resourceFile.size',
'resourceNode.updatedAt',
])]
class CDocument extends AbstractResource implements ResourceInterface
{
use ShowCourseResourcesInSessionTrait;
/**
* @Groups({"document:read"})
* @ORM\Column(name="iid", type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
#[ApiProperty(identifier: true)]
#[Groups(['document:read'])]
protected int $iid;
/**
* @Groups({"document:read", "document:write", "document:browse"})
* @ORM\Column(name="title", type="string", length=255, nullable=false)
*/
#[Groups(['document:read', 'document:write', 'document:browse'])]
#[Assert\NotBlank]
protected string $title;
/**
* @Groups({"document:read", "document:write"})
* @ORM\Column(name="comment", type="text", nullable=true)
*/
#[Groups(['document:read', 'document:write'])]
protected ?string $comment;
/**
* @Groups({"document:read", "document:write"})
* @Assert\Choice({"folder", "file"}, message="Choose a valid filetype.")
* @ORM\Column(name="filetype", type="string", length=10, nullable=false)
*/
#[Groups(['document:read', 'document:write'])]
protected string $filetype;
/**

@ -374,6 +374,81 @@ class CDocumentRepositoryTest extends AbstractApiTest
$this->assertResponseStatusCodeSame(403);
}
public function testChangeVisibility(): void
{
$course = $this->createCourse('Test');
$courseId = $course->getId();
$resourceLinkList = [
[
'cid' => $course->getId(),
'visibility' => ResourceLink::VISIBILITY_PUBLISHED,
],
];
$file = $this->getUploadedFile();
$token = $this->getUserToken([]);
// Upload file.
$response = $this->createClientWithCredentials($token)->request(
'POST',
'/api/documents',
[
'headers' => [
'Content-Type' => 'multipart/form-data',
],
'extra' => [
'files' => [
'uploadFile' => $file,
],
],
'json' => [
'filetype' => 'file',
'size' => $file->getSize(),
'parentResourceNodeId' => $course->getResourceNode()->getId(),
'resourceLinkList' => $resourceLinkList,
],
]
);
// Check uploaded file.
$this->assertResponseIsSuccessful();
$this->assertResponseStatusCodeSame(201);
// Get document iid
$data = json_decode($response->getContent());
$documentId = $data->iid;
// Test access to file with admin. Use getFile param in order to get more info (resource link) of the document.
$this->createClientWithCredentials($token)->request(
'PUT',
'/api/documents/'.$documentId.'',
[
'query' => [
'getFile' => true,
],
]
);
$this->assertResponseIsSuccessful();
$this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8');
$this->assertJsonContains(
[
'@context' => '/api/contexts/Documents',
'@type' => 'Documents',
'title' => $file->getFilename(),
'filetype' => 'file',
'resourceLinkListFromEntity' => [
[
'session' => null,
'course' => [
'@id' => '/api/courses/'.$courseId,
],
'visibility' => ResourceLink::VISIBILITY_PUBLISHED,
],
],
]
);
}
public function testUploadFileInSideASubFolder(): void
{
$course = $this->createCourse('Test');
@ -750,6 +825,14 @@ class CDocumentRepositoryTest extends AbstractApiTest
$link = $document->getFirstResourceLink();
$this->assertSame(ResourceLink::VISIBILITY_DRAFT, $link->getVisibility());
$documentRepo->toggleVisibilityPublishedDraft($document);
$link = $document->getFirstResourceLink();
$this->assertSame(ResourceLink::VISIBILITY_PUBLISHED, $link->getVisibility());
$documentRepo->toggleVisibilityPublishedDraft($document);
$link = $document->getFirstResourceLink();
$this->assertSame(ResourceLink::VISIBILITY_DRAFT, $link->getVisibility());
$documentRepo->setVisibilityPending($document);
$link = $document->getFirstResourceLink();
$this->assertSame(ResourceLink::VISIBILITY_PENDING, $link->getVisibility());

Loading…
Cancel
Save