Merge branch 'ofaj19044-coursintro2'

pull/4004/head
Julio 4 years ago
commit 9556f9d86f
  1. 154
      assets/vue/components/ctoolintro/Form.vue
  2. 9
      assets/vue/components/ctoolintro/Layout.vue
  3. 8
      assets/vue/main.js
  4. 4
      assets/vue/mixins/UpdateMixin.js
  5. 25
      assets/vue/router/ctoolintro.js
  6. 4
      assets/vue/router/index.js
  7. 3
      assets/vue/services/ctoolintro.js
  8. 9
      assets/vue/store/modules/crud.js
  9. 1
      assets/vue/views/ccalendarevent/List.vue
  10. 129
      assets/vue/views/course/Home.vue
  11. 97
      assets/vue/views/ctoolintro/Create.vue
  12. 118
      assets/vue/views/ctoolintro/Show.vue
  13. 76
      assets/vue/views/ctoolintro/Update.vue
  14. 5
      assets/vue/views/message/List.vue
  15. 9
      config/services.yaml
  16. 49
      src/CoreBundle/Entity/AbstractResource.php
  17. 10
      src/CoreBundle/Entity/Course.php
  18. 2
      src/CoreBundle/Entity/ResourceLink.php
  19. 2
      src/CoreBundle/Entity/ResourceNode.php
  20. 6
      src/CoreBundle/Framework/Container.php
  21. 39
      src/CoreBundle/Tool/ToolIntro.php
  22. 27
      src/CourseBundle/Entity/CToolIntro.php
  23. 22
      tests/CourseBundle/Repository/CToolIntroRepositoryTest.php
  24. 4
      tests/README.md

@ -0,0 +1,154 @@
<template>
<q-form>
<TinyEditor
id="introText"
v-model="item.introText"
required
:init="{
skin_url: '/build/libs/tinymce/skins/ui/oxide',
content_css: '/build/libs/tinymce/skins/content/default/content.css',
branding: false,
relative_urls: false,
height: 500,
toolbar_mode: 'sliding',
file_picker_callback : browser,
autosave_ask_before_unload: true,
plugins: [
'fullpage advlist autolink lists link image charmap print preview anchor',
'searchreplace visualblocks code fullscreen',
'insertdatetime media table paste wordcount emoticons ' + extraPlugins
],
toolbar: 'undo redo | bold italic underline strikethrough | insertfile image media template link | fontselect fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | forecolor backcolor removeformat | pagebreak | charmap emoticons | fullscreen preview save print | code codesample | ltr rtl | ' + extraPlugins,
}
"
/>
<!-- For extra content-->
<slot></slot>
</q-form>
</template>
<script>
import useVuelidate from '@vuelidate/core';
import { required } from '@vuelidate/validators';
import {ref} from "vue";
import isEmpty from "lodash/isEmpty";
export default {
name: 'ToolIntroForm',
setup () {
const config = ref([]);
const extraPlugins = ref('');
if (!isEmpty(window.config)) {
config.value = window.config;
if (config.value['editor.translate_html']) {
extraPlugins.value = 'translatehtml';
}
}
return { v$: useVuelidate(), extraPlugins }
},
props: {
values: {
type: Object,
required: true
},
errors: {
type: Object,
default: () => {}
},
initialValues: {
type: Object,
default: () => {}
},
},
data() {
return {
introText: null,
parentResourceNodeId: null,
resourceNode: null,
};
},
computed: {
item() {
return this.initialValues || this.values;
},
violations() {
return this.errors || {};
}
},
methods: {
browser (callback, value, meta) {
//const route = useRoute();
let nodeId = this.$route.params['node'];
let folderParams = this.$route.query;
let url = this.$router.resolve({ name: 'DocumentForHtmlEditor', params: { id: nodeId }, query: folderParams })
url = url.fullPath;
console.log(url);
if (meta.filetype === 'image') {
url = url + "&type=images";
} else {
url = url + "&type=files";
}
console.log(url);
window.addEventListener('message', function (event) {
var data = event.data;
if (data.url) {
url = data.url;
console.log(meta); // {filetype: "image", fieldname: "src"}
callback(url);
}
});
tinymce.activeEditor.windowManager.openUrl({
url: url,// use an absolute path!
title: 'file manager',
/*width: 900,
height: 450,
resizable: 'yes'*/
}, {
oninsert: function (file, fm) {
var url, reg, info;
// URL normalization
url = fm.convAbsUrl(file.url);
// Make file info
info = file.name + ' (' + fm.formatSize(file.size) + ')';
// Provide file and text for the link dialog
if (meta.filetype === 'file') {
callback(url, {text: info, title: info});
}
// Provide image and alt text for the image dialog
if (meta.filetype === 'image') {
callback(url, {alt: info});
}
// Provide alternative source and posted for the media dialog
if (meta.filetype === 'media') {
callback(url);
}
}
});
return false;
},
},
validations: {
item: {
introText: {
//required,
},
parentResourceNodeId: {
},
resourceNode:{
}
}
}
};
</script>

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

@ -20,6 +20,7 @@ import userService from './services/user';
import userGroupService from './services/usergroup';
import userRelUserService from './services/userreluser';
import calendarEventService from './services/ccalendarevent';
import toolIntroService from './services/ctoolintro';
import makeCrudModule from './store/modules/crud';
//import vuetify from './plugins/vuetify' // path to vuetify export
@ -68,6 +69,13 @@ store.registerModule(
})
);
store.registerModule(
'ctoolintro',
makeCrudModule({
service: toolIntroService
})
);
store.registerModule(
'personalfile',
makeCrudModule({

@ -10,13 +10,11 @@ export default {
};
},
created() {
console.log('mixin update created');
// Changed
let id = this.$route.params.id;
if (isEmpty(id)) {
id = this.$route.query.id;
}
console.log(id);
if (!isEmpty(id)) {
// Ajax call
this.retrieve(decodeURIComponent(id));
@ -54,8 +52,6 @@ export default {
methods: {
del() {
console.log('mixin del');
//let item = this.retrieved;
console.log(this.item);
this.deleteItem(this.item).then(() => {

@ -0,0 +1,25 @@
export default {
path: '/resources/ctoolintro/',
meta: { requiresAuth: true, showBreadcrumb: true },
name: 'ctoolintro',
component: () => import('../components/ctoolintro/Layout.vue'),
redirect: { name: 'ToolIntroList' },
children: [
{
name: 'ToolIntroCreate',
path: 'new/:courseTool',
component: () => import('../views/ctoolintro/Create.vue')
},
{
name: 'ToolIntroUpdate',
//path: ':id/edit',
path: 'edit',
component: () => import('../views/ctoolintro/Update.vue')
},
{
name: 'ToolIntroShow',
path: '',
component: () => import('../views/ctoolintro/Show.vue')
}
]
};

@ -7,6 +7,7 @@ import userRoutes from './user';
import userGroupRoutes from './usergroup';
import userRelUserRoutes from './userreluser';
import calendarEventRoutes from './ccalendarevent';
import toolIntroRoutes from './ctoolintro';
//import courseCategoryRoutes from './coursecategory';
import documents from './documents';
@ -103,7 +104,8 @@ const router = createRouter({
userRoutes,
userGroupRoutes,
userRelUserRoutes,
calendarEventRoutes
calendarEventRoutes,
toolIntroRoutes
]
});

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

@ -145,7 +145,6 @@ export default function makeCrudModule({
findAll: ({ commit, state }, params) => {
if (!service) throw new Error('No service specified!');
console.log('crud.js findAll');
//commit(ACTIONS.TOGGLE_LOADING);
return service
@ -162,8 +161,6 @@ export default function makeCrudModule({
fetchAll: ({ commit, state }, params) => {
if (!service) throw new Error('No service specified!');
console.log('crud.js fetchAll');
commit(ACTIONS.TOGGLE_LOADING);
return service
@ -385,13 +382,13 @@ export default function makeCrudModule({
});
},
[ACTIONS.SET_CREATED]: (state, created) => {
console.log('set _created');
console.log(created);
//console.log('set _created');
//console.log(created);
Object.assign(state, { created });
state.created = created;
},
[ACTIONS.SET_DELETED]: (state, deleted) => {
console.log('SET_DELETED');
//console.log('SET_DELETED');
if (!state.allIds.includes(deleted['@id'])) {
return;
}

@ -123,7 +123,6 @@ export default {
const currentUser = computed(() => store.getters['security/getUser']);
const { t, locale } = useI18n();
let currentEvent = null;
const sessionState = reactive({

@ -50,30 +50,60 @@
</div>
</div>
</div>
</div>
<div v-if="isCurrentTeacher && course"
<div v-if="isCurrentTeacher"
class="bg-gradient-to-r from-gray-100 to-gray-50 flex flex-col rounded-md text-center p-2"
>
<div class="p-10 text-center">
<div>
<v-icon
icon="mdi-book-open-page-variant"
size="72px"
class="font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-ch-primary to-ch-primary-light"
/>
</div>
<div class="mt-2 font-bold">
{{ $t("You don't have course content") }}
</div>
<div>
{{ $t('Add a course introduction to display to your students') }}
</div>
<a class="mt-2 btn btn-info">
<div v-if="intro" class="p-10 text-center">
<span v-html="intro.introText" />
<button
class="mt-2 btn btn-info"
@click="updateIntro(intro)"
>
<v-icon>mdi-pencil</v-icon>
{{ $t('Update') }}
</button>
</div>
<div v-else>
<div>
<v-icon
icon="mdi-book-open-page-variant"
size="72px"
class="font-extrabold text-transparent bg-clip-text bg-gradient-to-br from-ch-primary to-ch-primary-light"
/>
</div>
<div class="mt-2 font-bold">
{{ $t("You don't have course content") }}
</div>
<div>
{{ $t('Add a course introduction to display to your students') }}
</div>
<!-- <router-link-->
<!-- v-if="introTool"-->
<!-- :to="{ name: 'ToolIntroCreate', params: {'courseTool': introTool.iid, cid: course.id, sid: route.query.sid} }"-->
<!-- tag="button"-->
<!-- class="mt-2 btn btn-info">-->
<!-- <v-icon>mdi-plus</v-icon>-->
<!-- {{ $t('Course introduction') }}-->
<!-- </router-link>-->
<button
class="mt-2 btn btn-info"
v-if="introTool"
@click="addIntro(course, introTool)"
>
<v-icon>mdi-plus</v-icon>
{{ $t('Course introduction') }}
</a>
</button>
</div>
</div>
<div v-else>
<div v-if="intro" class="p-10 text-center">
<span v-html="intro.introText" />
</div>
</div>
@ -135,11 +165,12 @@ import Toolbar from '../../components/Toolbar.vue';
import CourseToolList from '../../components/course/CourseToolList.vue';
import ShortCutList from '../../components/course/ShortCutList.vue';
import {useRoute} from 'vue-router'
import isEmpty from "lodash/isEmpty";
import {useRoute, useRouter} from 'vue-router'
import axios from "axios";
import {ENTRYPOINT} from '../../config/entrypoint';
import {reactive, toRefs} from 'vue'
import {mapGetters} from "vuex";
import {computed, onMounted, reactive, toRefs} from 'vue'
import {mapGetters, useStore} from "vuex";
export default {
name: 'Home',
@ -156,12 +187,19 @@ export default {
tools: [],
shortcuts: [],
dropdownOpen: false,
intro: null,
introTool: null,
goToCourseTool,
changeVisibility,
goToSettingCourseTool,
goToShortCut
goToShortCut,
addIntro,
updateIntro,
});
const route = useRoute()
const store = useStore();
const router = useRouter();
let courseId = route.params.id;
let sessionId = route.query.sid ?? 0;
@ -169,10 +207,53 @@ export default {
state.course = response.data.course;
state.tools = response.data.tools;
state.shortcuts = response.data.shortcuts;
getIntro();
}).catch(function (error) {
console.log(error);
});
async function getIntro() {
const introTool = state.course.tools.find(element => element.name === 'course_homepage');
const filter = {
courseTool : introTool.iid,
cid : courseId,
sid : sessionId,
};
state.introTool = introTool;
store.dispatch('ctoolintro/findAll', filter).then(response => {
if (!isEmpty(response)) {
// first item
state.intro = response[0];
}
});
}
function addIntro(course, introTool) {
return router.push({
name: 'ToolIntroCreate',
params: {'courseTool': introTool.iid },
query: {
'cid': courseId,
'sid': sessionId,
'parentResourceNodeId': course.resourceNode.id
}
});
}
function updateIntro(intro) {
return router.push({
name: 'ToolIntroUpdate',
params: {'id': intro['@id'] },
query: {
'cid': courseId,
'sid': sessionId,
'id': intro['@id']
}
});
}
function goToSettingCourseTool(course, tool) {
return '/course/' + courseId + '/settings/' + tool.tool.name + '?sid=' + sessionId;
}
@ -182,8 +263,8 @@ export default {
}
function goToShortCut(shortcut) {
var url = new URLSearchParams('?')
//let url = new URL(shortcut.url);
const url = new URLSearchParams('?')
url.append('cid', courseId);
url.append('sid', sessionId);

@ -0,0 +1,97 @@
<template>
<Toolbar
:handle-submit="onSendForm"
/>
<ToolIntroForm
ref="createForm"
:values="item"
:errors="violations"
/>
<Loading :visible="isLoading" />
</template>
<script>
import {mapActions, mapGetters, useStore} from 'vuex';
import { createHelpers } from 'vuex-map-fields';
import ToolIntroForm from '../../components/ctoolintro/Form.vue';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import CreateMixin from '../../mixins/CreateMixin';
import {computed, onMounted, reactive, ref, toRefs} from "vue";
import useVuelidate from "@vuelidate/core";
import {useRoute, useRouter} from "vue-router";
import isEmpty from "lodash/isEmpty";
import {RESOURCE_LINK_PUBLISHED} from "../../components/resource_links/visibility.js";
import axios from 'axios'
import { ENTRYPOINT } from '../../config/entrypoint'
import useNotification from "../../components/Notification";
import {useI18n} from "vue-i18n";
const servicePrefix = 'ctoolintro';
const { mapFields } = createHelpers({
getterType: 'ctoolintro/getField',
mutationType: 'ctoolintro/updateField'
});
export default {
name: 'ToolIntroCreate',
servicePrefix,
mixins: [CreateMixin],
components: {
Loading,
Toolbar,
ToolIntroForm
},
setup() {
const users = ref([]);
const isLoadingSelect = ref(false);
const item = ref({});
const route = useRoute();
const router = useRouter();
const {showNotification} = useNotification();
const { t } = useI18n();
let id = route.params.id;
if (isEmpty(id)) {
id = route.query.id;
}
let toolId = route.params.courseTool;
// Get the current intro text.
axios.get(ENTRYPOINT + 'c_tool_intros/' + toolId).then(response => {
let data = response.data;
item.value['introText'] = data.introText;
}).catch(function (error) {
console.log(error);
});
item.value['parentResourceNodeId'] = Number(route.query.parentResourceNodeId);
item.value['courseTool'] = '/api/c_tools/'+toolId;
item.value['resourceLinkList'] = [{
sid: route.query.sid,
cid: route.query.cid,
visibility: RESOURCE_LINK_PUBLISHED, // visible by default
}];
function onCreated(item) {
showNotification(t('Updated'));
router.go(-1);
}
return {v$: useVuelidate(), users, isLoadingSelect, item, onCreated};
},
computed: {
...mapFields(['error', 'isLoading', 'created', 'violations']),
...mapGetters({
'isAuthenticated': 'security/isAuthenticated',
'currentUser': 'security/getUser',
}),
},
methods: {
...mapActions('ctoolintro', ['create', 'createWithFormData'])
}
};
</script>

@ -0,0 +1,118 @@
<template>
<div>
<Toolbar
:handle-delete="del"
:handle-list="list"
>
<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-course-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('code') }}</strong></td>
<td>
{{ item['code'] }}
</td>
</tr>
<tr>
<td><strong>{{ $t('courseLanguage') }}</strong></td>
<td>
{{ item['courseLanguage'] }}
</td>
<td><strong>{{ $t('category') }}</strong></td>
<td>
<div v-if="item['category']">
{{ item['category'].name }}
</div>
<div v-else>
-
</div>
</td>
</tr>
<tr>
<td><strong>{{ $t('visibility') }}</strong></td>
<td>
{{ $n(item['visibility']) }} </td>
<td><strong>{{ $t('departmentName') }}</strong></td>
<td>
{{ item['departmentName'] }}
</td>
</tr>
<tr>
<td><strong>{{ $t('departmentUrl') }}</strong></td>
<td>
{{ item['departmentUrl'] }}
</td>
<td><strong>{{ $t('expirationDate') }}</strong></td>
<td>
{{ formatDateTime(item['expirationDate'], 'long') }} </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.vue';
import ShowMixin from '../../mixins/ShowMixin';
import Toolbar from '../../components/Toolbar.vue';
const servicePrefix = 'ctoolintro';
export default {
name: 'ToolIntroShow',
servicePrefix,
components: {
Loading,
Toolbar
},
mixins: [ShowMixin],
computed: {
...mapFields('ctoolintro', {
isLoading: 'isLoading'
}),
...mapGetters('ctoolintro', ['find'])
},
methods: {
...mapActions('ctoolintro', {
deleteItem: 'del',
reset: 'resetShow',
retrieve: 'load'
})
}
};
</script>

@ -0,0 +1,76 @@
<template>
<Toolbar
:handle-submit="onSendForm"
/>
<ToolIntroForm
ref="updateForm"
v-if="item"
:values="item"
:errors="violations"
/>
<Loading :visible="isLoading || deleteLoading" />
</template>
<script>
import { mapActions, mapGetters } from 'vuex';
import { mapFields } from 'vuex-map-fields';
import ToolIntroForm from '../../components/ctoolintro/Form.vue';
import Loading from '../../components/Loading.vue';
import Toolbar from '../../components/Toolbar.vue';
import UpdateMixin from '../../mixins/UpdateMixin';
import useNotification from "../../components/Notification";
import {useI18n} from "vue-i18n";
import {useRouter} from "vue-router";
import {watch} from "vue";
const servicePrefix = 'ctoolintro';
export default {
name: 'ToolIntroUpdate',
servicePrefix,
mixins: [UpdateMixin],
components: {
Loading,
Toolbar,
ToolIntroForm
},
setup() {
const {showNotification} = useNotification();
const { t } = useI18n();
const router = useRouter();
/*function updated(val) {
showNotification(t('Updated'));
router.go(-1);
}*/
//return {updated};
return;
},
computed: {
...mapFields('ctoolintro', {
deleteLoading: 'isLoading',
isLoading: 'isLoading',
error: 'error',
updated: 'updated',
violations: 'violations'
}),
...mapGetters('ctoolintro', ['find']),
...mapGetters({
'isCurrentTeacher': 'security/isCurrentTeacher',
}),
},
methods: {
...mapActions('ctoolintro', {
createReset: 'resetCreate',
deleteItem: 'del',
delReset: 'resetDelete',
retrieve: 'load',
update: 'update',
updateWithFormData: 'updateWithFormData',
updateReset: 'resetUpdate'
})
}
};
</script>

@ -272,6 +272,8 @@ import {RESOURCE_LINK_PUBLISHED} from "../../components/resource_links/visibilit
import {MESSAGE_TYPE_INBOX} from "../../components/message/msgType";
import useNotification from "../../components/Notification";
import {useI18n} from "vue-i18n";
export default {
name: 'MessageList',
servicePrefix: 'Message',
@ -295,6 +297,7 @@ export default {
const index = ref('inbox');
const {showNotification} = useNotification();
const { t } = useI18n();
// Inbox
const inBoxFilter = {
@ -373,7 +376,7 @@ export default {
deleteItemDialog.value = false;
showNotification('Deleted');
showNotification(t('Deleted'));
goToInbox();
}

@ -45,15 +45,6 @@ services:
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
# App\ApiPlatform\AutoGroupResourceMetadataFactory:
# # causes this to decorate around the cached factory so that
# # our service is never cached (which, of course, can have performance
# # implications!
# decoration_priority: -20
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
Vich\UploaderBundle\Naming\SubdirDirectoryNamer:
public: true

@ -51,22 +51,19 @@ abstract class AbstractResource
* Resource illustration URL - Property set by ResourceNormalizer.php.
*
* @ApiProperty(iri="http://schema.org/contentUrl")
* @Groups({
* "resource_node:read",
* "document:read",
* "media_object_read",
* "course:read",
* "session:read",
* "course_rel_user:read",
* "session_rel_course_rel_user:read"
* })
*/
#[Groups([
'resource_node:read',
'document:read',
'media_object_read',
'course:read',
'session:read',
'course_rel_user:read',
'session_rel_course_rel_user:read',
])]
public ?string $illustrationUrl = null;
/**
* @Assert\Valid()
* @ApiSubresource()
* @Groups({"resource_node:read", "resource_node:write", "personal_file:write", "document:write", "ctool:read", "course:read", "illustration:read", "message:read"})
* @ORM\OneToOne(
* targetEntity="Chamilo\CoreBundle\Entity\ResourceNode",
* cascade={"persist", "remove"},
@ -74,6 +71,19 @@ abstract class AbstractResource
* )
* @ORM\JoinColumn(name="resource_node_id", referencedColumnName="id", onDelete="CASCADE")
*/
#[Assert\Valid]
#[ApiSubresource]
#[Groups([
'resource_node:read',
'resource_node:write',
'personal_file:write',
'document:write',
'ctool:read',
'course:read',
'illustration:read',
'message:read',
'c_tool_intro:read',
])]
public ?ResourceNode $resourceNode = null;
/**
@ -92,15 +102,14 @@ abstract class AbstractResource
*/
public $parentResource;
/**
* @Groups({"resource_node:read", "document:read"})
*/
#[Groups(['resource_node:read', 'document:read'])]
public ?array $resourceLinkListFromEntity = null;
/**
* Use when sending a request to Api platform.
* Temporal array that saves the resource link list that will be filled by CreateDocumentFileAction.php.
*/
#[Groups(['c_tool_intro:write', 'resource_node:write'])]
public array $resourceLinkList = [];
/**
@ -291,6 +300,11 @@ abstract class AbstractResource
return $this;
}
public function getResourceLinkArray()
{
return $this->resourceLinkList;
}
public function setResourceLinkArray(array $links)
{
$this->resourceLinkList = $links;
@ -298,11 +312,6 @@ abstract class AbstractResource
return $this;
}
public function getResourceLinkArray()
{
return $this->resourceLinkList;
}
public function getResourceLinkListFromEntity()
{
return $this->resourceLinkListFromEntity;

@ -59,20 +59,19 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
public const HIDDEN = 4;
/**
* @Groups({"course:read", "course_rel_user:read", "session_rel_user:read", "session_rel_course_rel_user:read", "session_rel_user:read"})
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
#[Groups(['course:read', 'course_rel_user:read', 'session_rel_course_rel_user:read', 'session_rel_user:read'])]
protected ?int $id = null;
/**
* The course title.
*
* @Groups({"course:read", "course:write", "course_rel_user:read", "session_rel_course_rel_user:read", "session_rel_user:read"})
*
* @ORM\Column(name="title", type="string", length=250, nullable=true, unique=false)
*/
#[Groups(['course:read', 'course:write', 'course_rel_user:read', 'session_rel_course_rel_user:read', 'session_rel_user:read'])]
#[Assert\NotBlank(message: 'A Course requires a title')]
protected ?string $title = null;
@ -84,7 +83,6 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
* maxMessage = "Code cannot be longer than {{ limit }} characters"
* )
* @ApiProperty(iri="http://schema.org/courseCode")
* @Groups({"course:read", "course:write", "course_rel_user:read"})
*
* @Gedmo\Slug(
* fields={"title"},
@ -95,6 +93,7 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
* )
* @ORM\Column(name="code", type="string", length=40, nullable=false, unique=true)
*/
#[Groups(['course:read', 'user:write', 'course_rel_user:read'])]
#[Assert\NotBlank]
protected string $code;
@ -110,13 +109,13 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
/**
* @var Collection|CourseRelUser[]
*
* @Groups({"course:read", "user:read"})
* "orphanRemoval" is needed to delete the CourseRelUser relation
* in the CourseAdmin class. The setUsers, getUsers, removeUsers and
* addUsers methods need to be added.
*
* @ORM\OneToMany(targetEntity="CourseRelUser", mappedBy="course", cascade={"persist"}, orphanRemoval=true)
*/
#[Groups(['course:read', 'user:read'])]
#[ApiSubresource]
protected Collection $users;
@ -149,6 +148,7 @@ class Course extends AbstractResource implements ResourceInterface, ResourceWith
*
* @ORM\OneToMany(targetEntity="Chamilo\CourseBundle\Entity\CTool", mappedBy="course", cascade={"persist", "remove"})
*/
#[Groups(['course:read'])]
protected Collection $tools;
protected Session $currentSession;

@ -86,7 +86,7 @@ class ResourceLink
/**
* @ORM\Column(name="visibility", type="integer", nullable=false)
*/
#[Groups(['ctool:read'])]
#[Groups(['ctool:read', 'c_tool_intro:read'])]
protected int $visibility;
/**

@ -106,7 +106,7 @@ class ResourceNode
* @ORM\OneToMany(targetEntity="ResourceLink", mappedBy="resourceNode", cascade={"persist", "remove"})
*/
#[ApiSubresource]
#[Groups(['ctool:read'])]
#[Groups(['ctool:read', 'c_tool_intro:read'])]
protected Collection $resourceLinks;
/**

@ -68,6 +68,7 @@ use Chamilo\CourseBundle\Repository\CSurveyRepository;
use Chamilo\CourseBundle\Repository\CThematicAdvanceRepository;
use Chamilo\CourseBundle\Repository\CThematicPlanRepository;
use Chamilo\CourseBundle\Repository\CThematicRepository;
use Chamilo\CourseBundle\Repository\CToolIntroRepository;
use Chamilo\CourseBundle\Repository\CWikiRepository;
use Chamilo\CourseBundle\Settings\SettingsCourseManager;
use Chamilo\LtiBundle\Repository\ExternalToolRepository;
@ -555,6 +556,11 @@ class Container
return self::$container->get(CWikiRepository::class);
}
public static function getToolIntroRepository(): CToolIntroRepository
{
return self::$container->get(CToolIntroRepository::class);
}
public static function getFormFactory(): FormFactory
{
return self::$container->get('form.factory');

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Tool;
use Chamilo\CourseBundle\Entity\CToolIntro;
class ToolIntro extends AbstractTool implements ToolInterface
{
public function getName(): string
{
return 'tool_intro';
}
public function getIcon(): string
{
return 'mdi-certificate';
}
public function getLink(): string
{
return '/resources/ctoolintro';
}
public function getCategory(): string
{
return 'tool';
}
public function getResourceTypes(): ?array
{
return [
'tool_intro' => CToolIntro::class,
];
}
}

@ -6,7 +6,9 @@ declare(strict_types=1);
namespace Chamilo\CourseBundle\Entity;
use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use Chamilo\CoreBundle\Entity\AbstractResource;
use Chamilo\CoreBundle\Entity\ResourceInterface;
use Chamilo\CoreBundle\Entity\ResourceShowCourseResourcesInSessionInterface;
@ -23,6 +25,26 @@ use Symfony\Component\Validator\Constraints as Assert;
* @ORM\Entity
*/
#[ApiResource(
collectionOperations: [
'get' => [
//'security' => "is_granted('VIEW', object)", // the get collection is also filtered by MessageExtension.php
'security' => "is_granted('ROLE_USER')",
],
'post' => [
'security_post_denormalize' => "is_granted('CREATE', object)",
],
],
itemOperations: [
'get' => [
'security' => "is_granted('VIEW', object)",
],
'put' => [
'security' => "is_granted('EDIT', object)",
],
'delete' => [
'security' => "is_granted('DELETE', object)",
],
],
attributes: [
'security' => "is_granted('ROLE_ADMIN') or is_granted('ROLE_CURRENT_COURSE_TEACHER')",
],
@ -33,6 +55,10 @@ use Symfony\Component\Validator\Constraints as Assert;
'groups' => ['c_tool_intro:read'],
],
)]
#[ApiFilter(SearchFilter::class, properties: [
'courseTool' => 'exact',
])]
class CToolIntro extends AbstractResource implements ResourceInterface, ResourceShowCourseResourcesInSessionInterface
{
/**
@ -40,6 +66,7 @@ class CToolIntro extends AbstractResource implements ResourceInterface, Resource
* @ORM\Id
* @ORM\GeneratedValue
*/
#[Groups(['c_tool_intro:read'])]
protected int $iid;
/**

@ -6,6 +6,7 @@ declare(strict_types=1);
namespace Chamilo\Tests\CourseBundle\Repository;
use Chamilo\CoreBundle\Entity\ResourceLink;
use Chamilo\CourseBundle\Entity\CTool;
use Chamilo\CourseBundle\Entity\CToolIntro;
use Chamilo\CourseBundle\Repository\CToolIntroRepository;
@ -75,9 +76,7 @@ class CToolIntroRepositoryTest extends AbstractApiTest
public function testCreateIntroApi(): void
{
$course = $this->createCourse('new');
$token = $this->getUserToken();
$resourceNodeId = $course->getResourceNode()->getId();
/** @var CTool $courseTool */
@ -85,7 +84,7 @@ class CToolIntroRepositoryTest extends AbstractApiTest
$iri = '/api/c_tools/'.$courseTool->getIid();
$this->createClientWithCredentials($token)->request(
$response = $this->createClientWithCredentials($token)->request(
'POST',
'/api/c_tool_intros',
[
@ -93,6 +92,12 @@ class CToolIntroRepositoryTest extends AbstractApiTest
'introText' => 'introduction here',
'courseTool' => $iri,
'parentResourceNodeId' => $resourceNodeId,
'resourceLinkList' => [
[
'cid' => $course->getId(),
'visibility' => ResourceLink::VISIBILITY_PUBLISHED,
],
],
],
]
);
@ -105,6 +110,17 @@ class CToolIntroRepositoryTest extends AbstractApiTest
'@type' => 'CToolIntro',
'introText' => 'introduction here',
]);
$repo = self::getContainer()->get(CToolIntroRepository::class);
$id = $response->toArray()['iid'];
/** @var CToolIntro $toolIntro */
$toolIntro = $repo->find($id);
$this->assertNotNull($toolIntro);
$this->assertNotNull($toolIntro->getResourceNode());
$this->assertsame(1, $toolIntro->getResourceNode()->getResourceLinks()->count());
$this->assertSame(1, $repo->count([]));
}
public function testUpdateIntroApi(): void

@ -50,6 +50,10 @@ If there are DB changes you can migrate your test installation with:
Those commands will install Chamilo in the chamilo_test database.
In order to delete the test database and restart the process use:
`php bin/console --env=test doctrine:database:drop`
### Use
Execute the tests with:

Loading…
Cancel
Save