Add documents refactor using api calls

pull/3262/head
Julio Montoya 5 years ago
parent 2a68cd6a43
commit e0f24d5723
  1. 84
      assets/vue/App.vue
  2. 16
      assets/vue/components/Toolbar.vue
  3. 2
      assets/vue/components/course/Form.vue
  4. 43
      assets/vue/components/documents/Filter.vue
  5. 9
      assets/vue/components/documents/Layout.vue
  6. 32
      assets/vue/main.js
  7. 19
      assets/vue/mixins/ListMixin.js
  8. 2
      assets/vue/router/course.js
  9. 2
      assets/vue/router/coursecategory.js
  10. 33
      assets/vue/router/documents.js
  11. 9
      assets/vue/router/index.js
  12. 3
      assets/vue/services/documents.js
  13. 2
      assets/vue/utils/fetch.js
  14. 4
      assets/vue/views/course/List.vue
  15. 3
      assets/vue/views/coursecategory/List.vue
  16. 45
      assets/vue/views/documents/Create.vue
  17. 45
      assets/vue/views/documents/CreateFile.vue
  18. 105
      assets/vue/views/documents/List.vue
  19. 98
      assets/vue/views/documents/Show.vue
  20. 61
      assets/vue/views/documents/Update.vue
  21. 4
      config/packages/api_platform.yaml
  22. 5
      config/routes.yaml
  23. 25
      src/CoreBundle/Controller/CreateMediaObjectAction.php
  24. 2
      src/CoreBundle/Controller/ResourceController.php
  25. 2
      src/CoreBundle/Entity/Course.php
  26. 4
      src/CoreBundle/Entity/CourseCategory.php
  27. 80
      src/CoreBundle/Entity/Resource/ResourceFile.php
  28. 4
      src/CoreBundle/Entity/Resource/ResourceLink.php
  29. 23
      src/CoreBundle/Entity/Resource/ResourceNode.php
  30. 72
      src/CoreBundle/EventSubscriber/ResolveResourceFileContentUrlSubscriber.php
  31. 33
      src/CoreBundle/Resources/views/Index/courses.html.twig
  32. 14
      src/CoreBundle/Resources/views/Layout/head.html.twig
  33. 4
      src/CoreBundle/Resources/views/login.html.twig
  34. 22
      src/CourseBundle/Entity/CDocument.php
  35. 3255
      yarn.lock

@ -1,7 +1,12 @@
<template> <template>
<v-app id="inspire"> <v-app id="inspire">
<snackbar></snackbar> <snackbar></snackbar>
<v-navigation-drawer v-model="drawer" app>
<v-navigation-drawer
v-model="drawer"
:clipped="$vuetify.breakpoint.lgAndUp"
app
>
<v-list dense> <v-list dense>
<v-list-item> <v-list-item>
<v-list-item-action> <v-list-item-action>
@ -38,24 +43,95 @@
</v-list-item-action> </v-list-item-action>
<v-list-item-content> <v-list-item-content>
<v-list-item-title> <v-list-item-title>
<router-link :to="{ name: 'ReviewList' }">Reviews</router-link> <router-link :to="{ name: 'DocumentsList' }">Documents</router-link>
</v-list-item-title> </v-list-item-title>
</v-list-item-content> </v-list-item-content>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-navigation-drawer> </v-navigation-drawer>
<v-app-bar app color="indigo" dark>
<!-- <v-navigation-drawer-->
<!-- v-model="drawerRight"-->
<!-- app-->
<!-- clipped-->
<!-- right-->
<!-- >-->
<!-- <v-list dense>-->
<!-- <v-list-item @click.stop="right = !right">-->
<!-- <v-list-item-action>-->
<!-- <v-icon>mdi-exit-to-app</v-icon>-->
<!-- </v-list-item-action>-->
<!-- <v-list-item-content>-->
<!-- <v-list-item-title>Open Temporary Drawer</v-list-item-title>-->
<!-- </v-list-item-content>-->
<!-- </v-list-item>-->
<!-- </v-list>-->
<!-- </v-navigation-drawer>-->
<!-- <v-app-bar app color="indigo" dark>-->
<!-- <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>-->
<!-- <v-toolbar-title>Chamilo</v-toolbar-title>-->
<!-- </v-app-bar>-->
<v-app-bar
:clipped-left="$vuetify.breakpoint.lgAndUp"
app
color="blue darken-3"
dark
>
<!-- <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>-->
<v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon> <v-app-bar-nav-icon @click.stop="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>Application</v-toolbar-title>
<v-toolbar-title
style="width: 300px"
class="ml-0 pl-4"
>
<span class="hidden-sm-and-down">Chamilo</span>
</v-toolbar-title>
<!-- <v-text-field-->
<!-- flat-->
<!-- solo-inverted-->
<!-- hide-details-->
<!-- prepend-inner-icon="mdi-magnify"-->
<!-- label="Search"-->
<!-- class="hidden-sm-and-down"-->
<!-- ></v-text-field>-->
<v-spacer></v-spacer>
<v-btn icon>
<v-icon>mdi-apps</v-icon>
</v-btn>
<v-btn icon>
<v-icon>mdi-bell</v-icon>
</v-btn>
<v-btn
icon
large
>
<v-avatar
size="32px"
item
>
<v-img
src="https://cdn.vuetifyjs.com/images/logos/logo.svg"
alt="Vuetify"
></v-img></v-avatar>
</v-btn>
</v-app-bar> </v-app-bar>
<v-content> <v-content>
<Breadcrumb layout-class="pl-3 py-3" /> <Breadcrumb layout-class="pl-3 py-3" />
<router-view></router-view> <router-view></router-view>
</v-content> </v-content>
<v-footer color="indigo" app> <v-footer color="indigo" app>
<span class="white--text">&copy; 2019</span> <span class="white--text">&copy; 2019</span>
</v-footer> </v-footer>
</v-app> </v-app>
</template> </template>

@ -48,6 +48,11 @@
<v-btn v-if="handleAdd" color="primary" rounded @click="addItem"> <v-btn v-if="handleAdd" color="primary" rounded @click="addItem">
<v-icon>mdi-plus-circle</v-icon> <v-icon>mdi-plus-circle</v-icon>
</v-btn> </v-btn>
<v-btn v-if="handleAddDocument" color="primary" rounded @click="addDocument">
Add document
</v-btn>
</div> </div>
<ConfirmDelete <ConfirmDelete
v-if="handleDelete" v-if="handleDelete"
@ -96,6 +101,11 @@ export default {
type: Function, type: Function,
required: false required: false
}, },
handleAddDocument: {
type: Function,
required: false
},
title: { title: {
type: String, type: String,
required: false required: false
@ -117,6 +127,12 @@ export default {
this.handleAdd(); this.handleAdd();
} }
}, },
addDocument() {
if (this.addDocument) {
this.handleAddDocument();
}
},
editItem() { editItem() {
if (this.handleEdit) { if (this.handleEdit) {
this.handleEdit(); this.handleEdit();

@ -6,7 +6,7 @@
<v-text-field <v-text-field
v-model="item.title" v-model="item.title"
:error-messages="titleErrors" :error-messages="titleErrors"
:label="$t('title')" :label="$t('Title')"
required required
@input="$v.item.title.$touch()" @input="$v.item.title.$touch()"
@blur="$v.item.title.$touch()" @blur="$v.item.title.$touch()"

@ -0,0 +1,43 @@
<template>
<v-container fluid>
<v-row>
<v-col cols="12" sm="6" md="6">
<v-text-field
v-model="item.title"
:label="$t('title')"
type="text"
/>
</v-col>
<v-row cols="12"></v-row>
</v-row>
</v-container>
</template>
<script>
export default {
name: 'DocumentsFilter',
props: {
values: {
type: Object,
required: true
}
},
data() {
return {};
},
mounted() {
},
computed: {
// eslint-disable-next-line
item() {
return this.initialValues || this.values;
}
},
methods: {
}
};
</script>

@ -0,0 +1,9 @@
<template>
<router-view></router-view>
</template>
<script>
export default {
name: 'DocumentsLayout'
}
</script>

@ -3,6 +3,7 @@ import App from "./App";
import router from "./router"; import router from "./router";
import store from "./store"; import store from "./store";
import courseCategoryService from './services/coursecategory'; import courseCategoryService from './services/coursecategory';
import documentsService from './services/documents';
import courseService from './services/course'; import courseService from './services/course';
import makeCrudModule from './store/modules/crud'; import makeCrudModule from './store/modules/crud';
@ -52,16 +53,23 @@ store.registerModule(
}) })
); );
Vue.config.productionTip = false; store.registerModule(
'documents',
makeCrudModule({
service: documentsService
})
);
new Vue({ Vue.config.productionTip = false;
vuetify, if (document.getElementById('app')) {
i18n, new Vue({
components: {App}, vuetify,
apolloProvider, i18n,
data: {}, components: {App},
store, apolloProvider,
router, data: {},
render: h => h(App) store,
}). router,
$mount("#app"); render: h => h(App)
}).$mount("#app");
}

@ -33,7 +33,7 @@ export default {
methods: { methods: {
onUpdateOptions(props) { onUpdateOptions(props) {
const { page, itemsPerPage, sortBy, descending, totalItems } = props; const { page, itemsPerPage, sortBy, sortDesc, descending, totalItems } = props;
let params = { let params = {
...this.filters ...this.filters
}; };
@ -41,10 +41,21 @@ export default {
params = { ...params, itemsPerPage, page }; params = { ...params, itemsPerPage, page };
} }
let sortDescVuetify = false;
let vueDescending = descending;
if (sortBy.length === 1 && sortDesc.length === 1) {
if (sortDesc[0]) {
sortDescVuetify = true;
}
vueDescending = sortDescVuetify;
}
if (!isEmpty(sortBy)) { if (!isEmpty(sortBy)) {
params[`order[${sortBy}]`] = descending ? 'desc' : 'asc'; params[`order[${sortBy}]`] = vueDescending ? 'desc' : 'asc';
} }
this.resetList = true;
this.getPage(params).then(() => { this.getPage(params).then(() => {
this.options.sortBy = sortBy; this.options.sortBy = sortBy;
this.options.descending = descending; this.options.descending = descending;
@ -66,6 +77,10 @@ export default {
this.$router.push({ name: `${this.$options.servicePrefix}Create` }); this.$router.push({ name: `${this.$options.servicePrefix}Create` });
}, },
addDocumentHandler() {
this.$router.push({ name: `${this.$options.servicePrefix}CreateFile` });
},
showHandler(item) { showHandler(item) {
this.$router.push({ this.$router.push({
name: `${this.$options.servicePrefix}Show`, name: `${this.$options.servicePrefix}Show`,

@ -1,5 +1,5 @@
export default { export default {
path: '/courses', path: '/resources/courses',
name: 'courses', name: 'courses',
component: () => import('../components/course/Layout'), component: () => import('../components/course/Layout'),
redirect: { name: 'CourseList' }, redirect: { name: 'CourseList' },

@ -1,5 +1,5 @@
export default { export default {
path: '/course_categories', path: '/resources/course_categories',
name: 'course_categories', name: 'course_categories',
component: () => import('../components/coursecategory/Layout'), component: () => import('../components/coursecategory/Layout'),
redirect: { name: 'CourseCategoryList' }, redirect: { name: 'CourseCategoryList' },

@ -0,0 +1,33 @@
export default {
path: '/resources/documents',
name: 'documents',
component: () => import('../components/documents/Layout'),
redirect: { name: 'DocumentsList' },
children: [
{
name: 'DocumentsList',
path: '',
component: () => import('../views/documents/List')
},
{
name: 'DocumentsCreate',
path: 'new',
component: () => import('../views/documents/Create')
},
{
name: 'DocumentsCreateFile',
path: 'new',
component: () => import('../views/documents/CreateFile')
},
{
name: 'DocumentsUpdate',
path: ':id/edit',
component: () => import('../views/documents/Update')
},
{
name: 'DocumentsShow',
path: ':id',
component: () => import('../views/documents/Show')
}
]
};

@ -4,15 +4,14 @@ import VueRouter from "vue-router";
Vue.use(VueRouter); Vue.use(VueRouter);
import courseRoutes from './course'; import courseRoutes from './course';
import coursecategoryRoutes from './coursecategory'; import courseCategoryRoutes from './coursecategory';
import sessionRoutes from './../../quasar/router/session'; import documents from './documents';
export default new VueRouter({ export default new VueRouter({
mode: "history", mode: "history",
routes: [ routes: [
courseRoutes, courseRoutes,
...sessionRoutes, courseCategoryRoutes,
coursecategoryRoutes, documents
// { path: "*", redirect: "/home" }
] ]
}); });

@ -0,0 +1,3 @@
import makeService from './api';
export default makeService('documents');

@ -39,8 +39,6 @@ export default function(id, options = {}) {
if (isObject(payload) && payload['@id']) if (isObject(payload) && payload['@id'])
options.body = JSON.stringify(normalize(payload)); options.body = JSON.stringify(normalize(payload));
//console.log(id); console.log(new URL(id, entryPoint));
return global.fetch(new URL(id, entryPoint), options).then(response => { return global.fetch(new URL(id, entryPoint), options).then(response => {
if (response.ok) return response; if (response.ok) return response;

@ -34,7 +34,9 @@
> >
<template slot="item.category" slot-scope="{ item }"> <template slot="item.category" slot-scope="{ item }">
<div v-if="item['category']"> <div v-if="item['category']">
{{ item['category'].name }} <router-link :to="{ name: 'CourseCategoryUpdate', params: {id: item['category']['@id']}}">
{{ item['category'].name }}
</router-link>
</div> </div>
<div v-else> <div v-else>
- -

@ -4,9 +4,6 @@
<v-container grid-list-xl fluid> <v-container grid-list-xl fluid>
<v-layout row wrap> <v-layout row wrap>
<v-flex sm12>
<h1>CourseCategory List</h1>
</v-flex>
<v-flex lg12> <v-flex lg12>
<DataFilter :handle-filter="onSendFilter" :handle-reset="resetFilter"> <DataFilter :handle-filter="onSendFilter" :handle-reset="resetFilter">
<CourseCategoryFilterForm <CourseCategoryFilterForm

@ -0,0 +1,45 @@
<template>
<div>
<Toolbar :handle-submit="onSendForm" :handle-reset="resetForm"></Toolbar>
<DocumentsForm ref="createForm" :values="item" :errors="violations" />
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import DocumentsForm from '../../components/documents/Form';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import CreateMixin from '../../mixins/CreateMixin';
const servicePrefix = 'Documents';
const { mapFields } = createHelpers({
getterType: 'documents/getField',
mutationType: 'documents/updateField'
});
export default {
name: 'DocumentsCreate',
servicePrefix,
mixins: [CreateMixin],
components: {
Loading,
Toolbar,
DocumentsForm
},
data() {
return {
item: {}
};
},
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations'])
},
methods: {
...mapActions('documents', ['create', 'reset'])
}
};
</script>

@ -0,0 +1,45 @@
<template>
<div>
<Toolbar :handle-submit="onSendForm" :handle-reset="resetForm"></Toolbar>
<DocumentsForm ref="createForm" :values="item" :errors="violations" />
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions } from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import DocumentsForm from '../../components/documents/Form';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import CreateMixin from '../../mixins/CreateMixin';
const servicePrefix = 'Documents';
const { mapFields } = createHelpers({
getterType: 'documents/getField',
mutationType: 'documents/updateField'
});
export default {
name: 'DocumentsCreate',
servicePrefix,
mixins: [CreateMixin],
components: {
Loading,
Toolbar,
DocumentsForm
},
data() {
return {
item: {}
};
},
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations'])
},
methods: {
...mapActions('documents', ['create', 'reset'])
}
};
</script>

@ -0,0 +1,105 @@
<template>
<div class="documents-list">
<Toolbar
:handle-add="addHandler"
:handle-add-document="addDocumentHandler"
/>
<v-container grid-list-xl fluid>
<v-layout row wrap>
<v-flex lg12>
<DataFilter :handle-filter="onSendFilter" :handle-reset="resetFilter">
<DocumentsFilterForm
ref="filterForm"
:values="filters"
slot="filter"
/>
</DataFilter>
<br />
<v-data-table
v-model="selected"
:headers="headers"
:items="items"
:items-per-page.sync="options.itemsPerPage"
:loading="isLoading"
:loading-text="$t('Loading...')"
:options.sync="options"
:server-items-length="totalItems"
class="elevation-1"
item-key="@id"
show-select
@update:options="onUpdateOptions"
>
<template slot="item.resourceNode" slot-scope="{ item }">
{{ item['@id'] }}
</template>
<ActionCell
slot="item.action"
slot-scope="props"
:handle-show="() => showHandler(props.item)"
:handle-edit="() => editHandler(props.item)"
:handle-delete="() => deleteHandler(props.item)"
></ActionCell>
</v-data-table>
</v-flex>
</v-layout>
</v-container>
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ListMixin from '../../mixins/ListMixin';
import ActionCell from '../../components/ActionCell';
import DocumentsFilterForm from '../../components/documents/Filter';
import DataFilter from '../../components/DataFilter';
import Toolbar from '../../components/Toolbar';
export default {
name: 'DocumentsList',
servicePrefix: 'Documents',
mixins: [ListMixin],
components: {
Toolbar,
ActionCell,
DocumentsFilterForm,
DataFilter
},
data() {
return {
headers: [
{text: 'Title', value: 'resourceNode.title', sortable: true},
{text: 'Last modified', value: 'resourceNode.updatedAt', sortable: true},
{
text: 'Actions',
value: 'action',
sortable: false
}
],
selected: []
};
},
computed: {
...mapGetters('documents', {
items: 'list'
}),
...mapFields('documents', {
deletedItem: 'deleted',
error: 'error',
isLoading: 'isLoading',
resetList: 'resetList',
totalItems: 'totalItems',
view: 'view'
})
},
methods: {
...mapActions('documents', {
getPage: 'fetchAll',
deleteItem: 'del'
})
}
};
</script>

@ -0,0 +1,98 @@
<template>
<div>
<Toolbar :handle-edit="editHandler" :handle-delete="del">
<template slot="left">
<v-toolbar-title v-if="item">{{
`${$options.servicePrefix} ${item['@id']}`
}}</v-toolbar-title>
</template>
</Toolbar>
<br />
<div v-if="item" class="table-documents-show">
<v-simple-table>
<template slot="default">
<thead>
<tr>
<th>Field</th>
<th>Value</th>
<th>Field</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>{{ $t('title') }}</strong></td>
<td>
{{ item['title'] }}
</td>
<td><strong>{{ $t('comment') }}</strong></td>
<td>
{{ item['comment'] }}
</td>
</tr>
<tr>
<td><strong>{{ $t('resourceNode') }}</strong></td>
<td>
{{ item['resourceNode'] && item['resourceNode'].name }}
</td>
<td></td>
</tr>
<tr>
<td><strong>{{ $t('file') }}</strong></td>
<td>
<div v-if="item['resourceNode']['resourceFile']">
<img v-bind:src=" item['resourceNode']['resourceFile']['file'] " />
</div>
<div v-else>
-
</div>
</td>
<td></td>
</tr>
</tbody>
</template>
</v-simple-table>
</div>
<Loading :visible="isLoading" />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import Loading from '../../components/Loading';
import ShowMixin from '../../mixins/ShowMixin';
import Toolbar from '../../components/Toolbar';
const servicePrefix = 'Documents';
export default {
name: 'DocumentsShow',
servicePrefix,
components: {
Loading,
Toolbar
},
mixins: [ShowMixin],
computed: {
...mapFields('documents', {
isLoading: 'isLoading'
}),
...mapGetters('documents', ['find'])
},
methods: {
...mapActions('documents', {
deleteItem: 'del',
reset: 'resetShow',
retrieve: 'load'
})
}
};
</script>

@ -0,0 +1,61 @@
<template>
<div>
<Toolbar
:handle-submit="onSendForm"
:handle-reset="resetForm"
:handle-delete="del"
/>
<DocumentsForm
ref="updateForm"
v-if="item"
:values="item"
:errors="violations"
/>
<Loading :visible="isLoading || deleteLoading" />
</div>
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import DocumentsForm from '../../components/documents/Form.vue';
import Loading from '../../components/Loading';
import Toolbar from '../../components/Toolbar';
import UpdateMixin from '../../mixins/UpdateMixin';
const servicePrefix = 'Documents';
export default {
name: 'DocumentsUpdate',
servicePrefix,
mixins: [UpdateMixin],
components: {
Loading,
Toolbar,
DocumentsForm
},
computed: {
...mapFields('documents', {
deleteLoading: 'isLoading',
isLoading: 'isLoading',
error: 'error',
updated: 'updated',
violations: 'violations'
}),
...mapGetters('documents', ['find'])
},
methods: {
...mapActions('documents', {
createReset: 'resetCreate',
deleteItem: 'del',
delReset: 'resetDelete',
retrieve: 'load',
update: 'update',
updateReset: 'resetUpdate'
})
}
};
</script>

@ -12,5 +12,9 @@ api_platform:
json: ['application/json'] json: ['application/json']
html: ['text/html'] html: ['text/html']
graphql: ['application/graphql'] graphql: ['application/graphql']
collection:
pagination:
client_items_per_page: true # Disabled by default
items_per_page_parameter_name: itemsPerPage # Default value
# mercure: # mercure:
# hub_url: '%env(MERCURE_SUBSCRIBE_URL)%' # hub_url: '%env(MERCURE_SUBSCRIBE_URL)%'

@ -32,6 +32,11 @@ sessions_vue:
requirements: requirements:
wildcard: .* wildcard: .*
resources_vue:
path: /resources/{wildcard}
controller: Chamilo\CoreBundle\Controller\IndexController::courses
requirements:
wildcard: .*
# web url shortcuts for legacy templates # web url shortcuts for legacy templates
web.ajax: web.ajax:

@ -0,0 +1,25 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\Resource\ResourceFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class CreateMediaObjectAction
{
public function __invoke(Request $request): ResourceFile
{
$uploadedFile = $request->files->get('file');
if (!$uploadedFile) {
throw new BadRequestHttpException('"file" is required');
}
$resourceFile = new ResourceFile();
$resourceFile->setFile($uploadedFile);
return $resourceFile;
}
}

@ -47,7 +47,7 @@ use ZipStream\ZipStream;
/** /**
* Class ResourceController. * Class ResourceController.
* *
* @Route("/resources") * @Route("/resources2")
* *
* @author Julio Montoya <gugli100@gmail.com>. * @author Julio Montoya <gugli100@gmail.com>.
*/ */

@ -29,7 +29,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ApiResource( * @ApiResource(
* iri="https://schema.org/Course", * iri="https://schema.org/Course",
* normalizationContext={"groups"={"course:read"}, "swagger_definition_name"="Read"}, * normalizationContext={"groups"={"course:read"}, "swagger_definition_name"="Read"},
* denormalizationContext={"groups"={"course:write"}}, * denormalizationContext={"groups"={"course:write","course_category:write"}},
* ) * )
* *
* @ApiFilter(SearchFilter::class, properties={"title": "partial", "code": "partial", "category": "partial"}) * @ApiFilter(SearchFilter::class, properties={"title": "partial", "code": "partial", "category": "partial"})

@ -17,8 +17,8 @@ use Symfony\Component\Validator\Constraints as Assert;
/** /**
* CourseCategory. * CourseCategory.
* @ApiResource( * @ApiResource(
* normalizationContext={"groups"={"course_category:read"}, "swagger_definition_name"="Read"}, * normalizationContext={"groups"={"course_category:read", "course:read"}, "swagger_definition_name"="Read"},
* denormalizationContext={"groups"={"course_category:write"}}, * denormalizationContext={"groups"={"course_category:write", "course:write"}},
* ) * )
* @ApiFilter(SearchFilter::class, properties={"name": "partial", "code": "partial"}) * @ApiFilter(SearchFilter::class, properties={"name": "partial", "code": "partial"})
* @ApiFilter(PropertyFilter::class) * @ApiFilter(PropertyFilter::class)

@ -4,8 +4,12 @@
namespace Chamilo\CoreBundle\Entity\Resource; namespace Chamilo\CoreBundle\Entity\Resource;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiProperty; use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource; 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 Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Gedmo\Timestampable\Traits\TimestampableEntity; use Gedmo\Timestampable\Traits\TimestampableEntity;
use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\File;
@ -13,12 +17,46 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Vich\UploaderBundle\Mapping\Annotation as Vich; use Vich\UploaderBundle\Mapping\Annotation as Vich;
use Chamilo\CoreBundle\Controller\CreateMediaObjectAction;
//
//* attributes={"security"="is_granted('ROLE_ADMIN')"},
/** /**
* @ApiResource( * @ApiResource(
* attributes={"security"="is_granted('ROLE_ADMIN')"}, * iri="http://schema.org/MediaObject",
* normalizationContext={"groups"={"resource_file:read"}} * normalizationContext={"groups"={"resource_file:read", "media_object_read"}},
* collectionOperations={
* "post"={
* "controller"=CreateMediaObjectAction::class,
* "deserialize"=false,
* "security"="is_granted('ROLE_USER')",
* "validation_groups"={"Default", "media_object_create"},
* "openapi_context"={
* "requestBody"={
* "content"={
* "multipart/form-data"={
* "schema"={
* "type"="object",
* "properties"={
* "file"={
* "type"="string",
* "format"="binary"
* }
* }
* }
* }
* }
* }
* }
* },
* "get"
* },
* itemOperations={
* "get"
* }
* ) * )
* @ApiFilter(SearchFilter::class, properties={"name": "partial"})
* @ApiFilter(PropertyFilter::class)
* @ORM\Entity * @ORM\Entity
* @Vich\Uploadable * @Vich\Uploadable
* *
@ -37,7 +75,7 @@ class ResourceFile
/** /**
* @Assert\NotBlank() * @Assert\NotBlank()
* @Groups({"resource_file:read", "document:read"}) * @Groups({"resource_file:read", "resource_node:read", "document:read"})
* *
* @var string * @var string
* *
@ -45,40 +83,10 @@ class ResourceFile
*/ */
protected $name; protected $name;
/**
* @var string
*/
//protected $description;
/**
* @var bool
*/
//protected $enabled;
/**
* @var int
*/
//protected $width;
/**
* @var int
*/
//protected $height;
/**
* @var float
*/
//protected $length;
/**
* @var string
*/
//protected $copyright;
/** /**
* @var string * @var string
* *
* @Groups({"resource_file:read", "document:read"}) * @Groups({"resource_file:read", "resource_node:read", "document:read"})
* @ORM\Column(type="text", nullable=true) * @ORM\Column(type="text", nullable=true)
*/ */
protected $mimeType; protected $mimeType;
@ -100,7 +108,7 @@ class ResourceFile
/** /**
* @var int * @var int
* @Groups({"resource_file:read", "document:read"}) * @Groups({"resource_file:read", "resource_node:read", "document:read"})
* *
* @ORM\Column(type="integer", nullable=true) * @ORM\Column(type="integer", nullable=true)
*/ */
@ -108,7 +116,7 @@ class ResourceFile
/** /**
* @var File * @var File
* @Groups({"resource_file:read", "document:read"}) * @Groups({"resource_file:read", "resource_node:read", "document:read"})
* @Assert\NotNull(groups={"media_object_create"}) * @Assert\NotNull(groups={"media_object_create"})
* @Vich\UploadableField( * @Vich\UploadableField(
* mapping="resources", * mapping="resources",

@ -14,9 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
/** /**
* @ApiResource( * @ApiResource()
* attributes={"security"="is_granted('ROLE_ADMIN')"}
* )
* @ORM\Entity * @ORM\Entity
* @ORM\Table(name="resource_link") * @ORM\Table(name="resource_link")
*/ */

@ -5,7 +5,11 @@
namespace Chamilo\CoreBundle\Entity\Resource; namespace Chamilo\CoreBundle\Entity\Resource;
use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiSubresource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\Session;
use Chamilo\CoreBundle\Entity\User; use Chamilo\CoreBundle\Entity\User;
@ -19,16 +23,19 @@ use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
//* attributes={"security"="is_granted('ROLE_ADMIN')"},
/** /**
* Base entity for all resources. * Base entity for all resources.
* *
* @ApiResource( * @ApiResource(
* attributes={"security"="is_granted('ROLE_ADMIN')"},
* collectionOperations={"get"}, * collectionOperations={"get"},
* normalizationContext={"groups"={"resource_node:read", "document:read"}}, * normalizationContext={"groups"={"resource_node:read", "document:read"}},
* denormalizationContext={"groups"={"resource_node:write", "document:write"}} * denormalizationContext={"groups"={"resource_node:write", "document:write"}}
* ) * )
* @ApiFilter(SearchFilter::class, properties={"title": "partial"})
* @ApiFilter(PropertyFilter::class) * @ApiFilter(PropertyFilter::class)
* @ApiFilter(OrderFilter::class, properties={"id", "title", "createdAt", "updatedAt"})
* @ORM\Entity(repositoryClass="Chamilo\CoreBundle\Repository\ResourceNodeRepository") * @ORM\Entity(repositoryClass="Chamilo\CoreBundle\Repository\ResourceNodeRepository")
* *
* @ORM\Table(name="resource_node") * @ORM\Table(name="resource_node")
@ -51,7 +58,7 @@ class ResourceNode
/** /**
* @Assert\NotBlank() * @Assert\NotBlank()
* @Groups({"resource_node:read", "document:read"}) * @Groups({"resource_node:read", "resource_node:write", "document:read"})
* @Gedmo\TreePathSource * @Gedmo\TreePathSource
* *
* @ORM\Column(name="title", type="string", length=255, nullable=false) * @ORM\Column(name="title", type="string", length=255, nullable=false)
@ -67,12 +74,16 @@ class ResourceNode
protected $slug; protected $slug;
/** /**
* @Groups({"resource_node:read", "resource_node:write"})
*
* @ORM\ManyToOne(targetEntity="ResourceType") * @ORM\ManyToOne(targetEntity="ResourceType")
* @ORM\JoinColumn(name="resource_type_id", referencedColumnName="id", nullable=false) * @ORM\JoinColumn(name="resource_type_id", referencedColumnName="id", nullable=false)
*/ */
protected $resourceType; protected $resourceType;
/** /**
* @Groups({"resource_node:read", "resource_node:write"})
*
* @var ResourceLink[] * @var ResourceLink[]
* *
* @ORM\OneToMany(targetEntity="ResourceLink", mappedBy="resourceNode", cascade={"remove"}) * @ORM\OneToMany(targetEntity="ResourceLink", mappedBy="resourceNode", cascade={"remove"})
@ -82,7 +93,7 @@ class ResourceNode
/** /**
* @var ResourceFile available file for this node * @var ResourceFile available file for this node
* *
* @Groups({"resource_node:read", "document:read"}) * @Groups({"resource_node:read", "resource_node:write", "document:read"})
* *
* @ORM\OneToOne(targetEntity="ResourceFile", inversedBy="resourceNode", orphanRemoval=true) * @ORM\OneToOne(targetEntity="ResourceFile", inversedBy="resourceNode", orphanRemoval=true)
* @ORM\JoinColumn(name="resource_file_id", referencedColumnName="id", onDelete="CASCADE") * @ORM\JoinColumn(name="resource_file_id", referencedColumnName="id", onDelete="CASCADE")
@ -92,7 +103,7 @@ class ResourceNode
/** /**
* @var User the creator of this node * @var User the creator of this node
* @Assert\Valid() * @Assert\Valid()
* @Groups({"resource_node:read", "document:read"}) * @Groups({"resource_node:read", "resource_node:write"})
* @ORM\ManyToOne( * @ORM\ManyToOne(
* targetEntity="Chamilo\CoreBundle\Entity\User", inversedBy="resourceNodes" * targetEntity="Chamilo\CoreBundle\Entity\User", inversedBy="resourceNodes"
* ) * )
@ -152,7 +163,7 @@ class ResourceNode
/** /**
* @var \DateTime * @var \DateTime
* *
* @Groups({"resource_node:read", "list"}) * @Groups({"resource_node:read", "document:read"})
* @Gedmo\Timestampable(on="create") * @Gedmo\Timestampable(on="create")
* @ORM\Column(type="datetime") * @ORM\Column(type="datetime")
*/ */
@ -161,7 +172,7 @@ class ResourceNode
/** /**
* @var \DateTime * @var \DateTime
* *
* @Groups({"resource_node:read", "list"}) * @Groups({"resource_node:read", "document:read"})
* @Gedmo\Timestampable(on="update") * @Gedmo\Timestampable(on="update")
* @ORM\Column(type="datetime") * @ORM\Column(type="datetime")
*/ */

@ -0,0 +1,72 @@
<?php
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\EventSubscriber;
use ApiPlatform\Core\EventListener\EventPriorities;
use ApiPlatform\Core\Util\RequestAttributesExtractor;
use Chamilo\CoreBundle\Entity\Resource\ResourceFile;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ViewEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Vich\UploaderBundle\Storage\StorageInterface;
class ResolveResourceFileContentUrlSubscriber implements EventSubscriberInterface
{
private $storage;
private $generator;
public function __construct(StorageInterface $storage, UrlGeneratorInterface $generator)
{
$this->storage = $storage;
$this->generator = $generator;
}
public static function getSubscribedEvents(): array
{
return [
KernelEvents::VIEW => ['onPreSerialize', EventPriorities::PRE_SERIALIZE],
];
}
public function onPreSerialize(ViewEvent $event): void
{
$controllerResult = $event->getControllerResult();
$request = $event->getRequest();
if ($controllerResult instanceof Response || !$request->attributes->getBoolean('_api_respond', true)) {
return;
}
if (!($attributes = RequestAttributesExtractor::extractAttributes($request)) ||
!\is_a($attributes['resource_class'], ResourceFile::class, true)
) {
return;
}
$mediaObjects = $controllerResult;
if (!is_iterable($mediaObjects)) {
$mediaObjects = [$mediaObjects];
}
foreach ($mediaObjects as $mediaObject) {
if (!$mediaObject instanceof ResourceFile) {
continue;
}
$params = [
'id' => $mediaObject->getResourceNode()->getId(),
'tool' => $mediaObject->getResourceNode()->getResourceType()->getTool()->getName(),
'type' => $mediaObject->getResourceNode()->getResourceType()->getName(),
];
$mediaObject->contentUrl = $this->generator->generate('chamilo_core_resource_view_file', $params);
//$mediaObject->contentUrl = $this->storage->resolveUri($mediaObject, 'file');
}
}
}

@ -0,0 +1,33 @@
{% extends "@ChamiloCore/Layout/no_layout.html.twig" %}
{% block content %}
<div id="app">
</div>
<script>
/*window.quasarConfig = {
brand: { // this will NOT work on IE 11
primary: '#e46262',
// ... or all other brand colors
},
notify: {
position: 'top',
multiLine: true,
timeout: 0,
},
directives: ['ClosePopup'],
plugins: ['Notify'],
config: {
notify: {
position: 'top',
multiLine: true,
timeout: 0,
},
},
loading: {}, // default set of options for Loading Quasar plugin
loadingBar: {}, // settings for LoadingBar Quasar plugin
}*/
</script>
{{ encore_entry_script_tags('vue') }}
{% endblock %}

@ -33,21 +33,23 @@
<link rel="stylesheet" href="{{ url('home') ~ 'build/css/themes/'~ theme ~'/default.css' }}"/> <link rel="stylesheet" href="{{ url('home') ~ 'build/css/themes/'~ theme ~'/default.css' }}"/>
{% endif %} {% endif %}
<link rel="stylesheet" media="print" href="{{ url('home') ~ 'build/css/print.css' }}"/> <link rel="stylesheet" media="print" href="{{ url('home') ~ 'build/css/print.css' }}"/>
{{ encore_entry_link_tags('resource') }} {{ encore_entry_link_tags('vue') }}
{% endblock %} {% endblock %}
{# app.js is generated using the file webpack.config.js and using yarn read /assets/README.md for more info #} {# app.js is generated using the file webpack.config.js and using yarn read /assets/README.md for more info #}
{#<script src="{{ url('home') ~ 'build/runtime.js' }}"></script>#} <script src="{{ url('home') ~ 'build/runtime.js' }}"></script>
<script src="{{ url('home') ~ 'build/app.js' }}"></script>
{#{{ encore_entry_script_tags('app') }}#}
<script src="{{ url('home') ~ 'libs/ckeditor/ckeditor.js' }}"></script> <script src="{{ url('home') ~ 'libs/ckeditor/ckeditor.js' }}"></script>
{#<script src="{{ url('home') ~ 'build/app.js' }}"></script>#}
{# Add third party js libraries that can't be loaded using webpack #} {# Add third party js libraries that can't be loaded using webpack #}
{#<script src="{{ asset('libs/readmore-js/readmore.js') }}"></script>#} {#<script src="{{ asset('libs/readmore-js/readmore.js') }}"></script>#}
<script src="{{ url('home') ~ 'build/libs/js-cookie/src/js.cookie.js' }}"></script> <script src="{{ url('home') ~ 'build/libs/js-cookie/src/js.cookie.js' }}"></script>
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
{# Check chamilo_js key in assetic.yml #} {# Check chamilo_js key in assetic.yml #}
{% block javascripts %} {% block javascripts %}
{{ encore_entry_script_tags('app') }}
{{ encore_entry_script_tags('resource') }}
{% block chamilo_header_js %} {% block chamilo_header_js %}
{# Loading legacy js using the $htmlHeadXtra array #} {# Loading legacy js using the $htmlHeadXtra array #}
{% autoescape false %} {% autoescape false %}

@ -13,17 +13,15 @@
<div class="login-box-body"> <div class="login-box-body">
{% block sonata_user_login_form %} {% block sonata_user_login_form %}
<form id="form-login-user" action="{{ path("login") }}" method="post" role="form"> <form id="form-login-user" action="{{ path("login") }}" method="post" role="form">
<div class="wrap-input validate-input m-b-23" data-validate = "{{ 'Username is required' | trans }}"> <div class="wrap-input validate-input m-b-23" data-validate = "{{ 'Username is required' | trans }}">
<label for="inputUsername">Username</label> <label for="inputUsername">Username</label>
<input type="text" value="{{ last_username }}" name="username" id="inputUsername" class="form-control" required autofocus> <input type="text" value="{{ last_username }}" name="username" id="inputUsername" class="form-control" required autofocus>
<i class="focus-input fas fa-user fa-lg"></i>
</div> </div>
<div class="wrap-input validate-input" data-validate="Password is required"> <div class="wrap-input validate-input" data-validate="Password is required">
<label for="inputPassword">Password</label> <label for="inputPassword">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" required> <input type="password" name="password" id="inputPassword" class="form-control" required>
<i class="focus-input fas fa-lock fa-lg"></i>
</div> </div>
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/> <input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate') }}"/>

@ -5,7 +5,9 @@
namespace Chamilo\CourseBundle\Entity; namespace Chamilo\CourseBundle\Entity;
use ApiPlatform\Core\Annotation\ApiFilter; use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiProperty;
use ApiPlatform\Core\Annotation\ApiResource; use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Annotation\ApiSubresource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter; use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Core\Serializer\Filter\PropertyFilter; use ApiPlatform\Core\Serializer\Filter\PropertyFilter;
@ -18,17 +20,18 @@ use Chamilo\CourseBundle\Traits\ShowCourseResourcesInSessionTrait;
use Doctrine\ORM\Event\LifecycleEventArgs; use Doctrine\ORM\Event\LifecycleEventArgs;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
//* attributes={"security"="is_granted('ROLE_ADMIN')"},
/** /**
* @ApiResource( * @ApiResource(
* shortName="Documents", * shortName="Documents",
* attributes={"security"="is_granted('ROLE_ADMIN')"}, * normalizationContext={"groups"={"document:read", "resource_node:node"}},
* normalizationContext={"groups"={"document:read", "resource_node"}},
* denormalizationContext={"groups"={"document:write"}} * denormalizationContext={"groups"={"document:write"}}
* ) * )
* @ApiFilter(PropertyFilter::class)
* @ApiFilter(SearchFilter::class, properties={"title": "partial"}) * @ApiFilter(SearchFilter::class, properties={"title": "partial"})
* @ApiFilter(OrderFilter::class) * @ApiFilter(
* OrderFilter::class,
* properties={"id", "resourceNode.title", "resourceNode.createdAt", "resourceNode.updatedAt"}
* )
* *
* @ORM\Table( * @ORM\Table(
* name="c_document", * name="c_document",
@ -51,7 +54,7 @@ class CDocument extends AbstractResource implements ResourceInterface
/** /**
* @var int * @var int
* @Groups({"list", "document:read"}) * @Groups({"document:read"})
* @ORM\Column(name="iid", type="integer") * @ORM\Column(name="iid", type="integer")
* @ORM\Id * @ORM\Id
* @ORM\GeneratedValue * @ORM\GeneratedValue
@ -75,19 +78,21 @@ class CDocument extends AbstractResource implements ResourceInterface
/** /**
* @var string * @var string
* *
* @Groups({"document:read", "document:write"})
*
* @ORM\Column(name="comment", type="text", nullable=true) * @ORM\Column(name="comment", type="text", nullable=true)
*/ */
protected $comment; protected $comment;
/** /**
* @var string * @var string
* @Groups({"list"}) * @Groups({"document:read", "document:write"})
* @ORM\Column(name="title", type="string", length=255, nullable=true) * @ORM\Column(name="title", type="string", length=255, nullable=true)
*/ */
protected $title; protected $title;
/** /**
* @var string * @var string File type, it can be 'folder' or 'file'
* *
* @ORM\Column(name="filetype", type="string", length=10, nullable=false) * @ORM\Column(name="filetype", type="string", length=10, nullable=false)
*/ */
@ -133,6 +138,7 @@ class CDocument extends AbstractResource implements ResourceInterface
*/ */
public function __construct() public function __construct()
{ {
$this->filetype = 'folder';
$this->readonly = false; $this->readonly = false;
$this->template = false; $this->template = false;
$this->size = 0; $this->size = 0;

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save