Merge pull request #5225 from christianbeeznest/course-create

Course: Implement course creation process for trainers
pull/5228/head
christianbeeznest 2 years ago committed by GitHub
commit d484968a11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      assets/css/scss/atoms/_checkbox.scss
  2. 9
      assets/css/scss/atoms/_dropdown.scss
  3. 8
      assets/vue/components/basecomponents/BaseAutocomplete.vue
  4. 8
      assets/vue/components/basecomponents/BaseDropdown.vue
  5. 58
      assets/vue/components/basecomponents/BaseInputTags.vue
  6. 34
      assets/vue/components/basecomponents/BaseInputText.vue
  7. 67
      assets/vue/components/basecomponents/BaseMultiSelect.vue
  8. 19
      assets/vue/components/basecomponents/BaseToolbar.vue
  9. 312
      assets/vue/components/course/Form.vue
  10. 7
      assets/vue/layouts/MyCourses.vue
  11. 38
      assets/vue/services/courseService.js
  12. 85
      assets/vue/views/course/Create.vue
  13. 10
      public/main/forum/forumfunction.inc.php
  14. 30
      public/main/inc/global.inc.php
  15. 265
      public/main/inc/lib/add_course.lib.inc.php
  16. 41
      public/main/inc/lib/course.lib.php
  17. 37
      public/main/inc/lib/plugin.class.php
  18. 105
      src/CoreBundle/Controller/CourseController.php
  19. 2
      src/CoreBundle/Repository/CourseCategoryRepository.php
  20. 18
      src/CoreBundle/Repository/Node/CourseRepository.php

@ -2,7 +2,7 @@
@apply h-4 w-4; @apply h-4 w-4;
.p-checkbox-box { .p-checkbox-box {
@apply border border-solid border-gray-50 bg-white h-4 rounded w-4 transition-none; @apply border border-solid border-gray-50 bg-white h-4 rounded w-4 transition-none invisible;
.p-checkbox-icon { .p-checkbox-icon {
@apply text-caption font-semibold text-white; @apply text-caption font-semibold text-white;

@ -1,4 +1,4 @@
.p-dropdown { .p-dropdown, .p-multiselect {
@apply border border-support-3 bg-white rounded-lg transition w-full duration-200 @apply border border-support-3 bg-white rounded-lg transition w-full duration-200
hover:border-primary hover:text-gray-90 hover:outline-0 hover:outline-none; hover:border-primary hover:text-gray-90 hover:outline-0 hover:outline-none;
@ -8,7 +8,7 @@
} }
} }
.p-dropdown-label { .p-dropdown-label, .p-multiselect-label {
@apply bg-transparent border-none @apply bg-transparent border-none
focus:outline-0 focus:outline-none; focus:outline-0 focus:outline-none;
@ -29,7 +29,7 @@
} }
} }
.p-dropdown-panel { .p-dropdown-panel, .p-autocomplete-panel, .p-multiselect-panel {
@apply bg-white rounded-lg text-gray-90 shadow-lg; @apply bg-white rounded-lg text-gray-90 shadow-lg;
.p-dropdown-header { .p-dropdown-header {
@ -88,4 +88,7 @@
} }
} }
} }
.p-multiselect-label-container {
@apply p-2;
}

@ -86,8 +86,12 @@ const baseModel = ref([])
const suggestions = ref([]) const suggestions = ref([])
const onComplete = async (event) => { const onComplete = async (event) => {
try {
const members = await props.search(event.query) const members = await props.search(event.query)
suggestions.value = members && members.length ? members : []
suggestions.value = members.length > 0 ? members : [] } catch (error) {
console.error('Error during onComplete:', error)
suggestions.value = []
}
} }
</script> </script>

@ -15,11 +15,12 @@
<label :for="inputId" v-text="label" /> <label :for="inputId" v-text="label" />
</div> </div>
<small v-if="isInvalid" :class="{ 'p-error': isInvalid }" v-text="errorText" /> <small v-if="isInvalid" :class="{ 'p-error': isInvalid }" v-text="errorText" />
<small v-if="helpText" class="form-text text-muted">{{ helpText }}</small>
</div> </div>
</template> </template>
<script setup> <script setup>
import Dropdown from "primevue/dropdown"; import Dropdown from "primevue/dropdown"
defineProps({ defineProps({
name: { name: {
@ -71,7 +72,8 @@ defineProps({
required: false, required: false,
default: false, default: false,
}, },
}); helpText: String,
})
defineEmits(["update:modelValue"]); defineEmits(["update:modelValue"])
</script> </script>

@ -0,0 +1,58 @@
<template>
<div class="border border-gray-300 p-1 rounded-md flex flex-wrap items-center">
<div class="flex flex-wrap items-center p-1 flex-grow min-h-[38px] outline-none border-none">
<div v-for="(tag, index) in tags" :key="index" class="bg-blue-500 text-white mr-1 mb-1 px-2.5 py-1 rounded-full flex items-center text-sm">
{{ tag }}
<span class="ml-2 cursor-pointer font-bold" @click.stop="removeTag(index)">&times;</span>
</div>
<input
ref="tagInput"
v-model="newTag"
@keyup="checkInputKey"
@keydown.delete="deleteLastTag"
placeholder="Add a tag"
class="flex-grow outline-none border-none p-0 m-0 text-sm"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const tags = ref([])
const newTag = ref('')
const tagInput = ref(null)
function focusInput() {
tagInput.value.focus()
}
function addTag() {
if (newTag.value.trim() && !tags.value.includes(newTag.value.trim())) {
tags.value.push(newTag.value.trim())
newTag.value = ''
}
}
function removeTag(index) {
tags.value.splice(index, 1)
}
function deleteLastTag(event) {
if (newTag.value === '' && event.key === 'Backspace' && tags.value.length > 0) {
tags.value.pop()
}
}
function checkInputKey(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault()
addTag()
}
}
onMounted(() => {
focusInput()
})
</script>

@ -7,22 +7,12 @@
:class="{ 'p-invalid': isInvalid }" :class="{ 'p-invalid': isInvalid }"
:aria-label="label" :aria-label="label"
type="text" type="text"
@update:model-value="$emit('update:modelValue', $event)" @update:model-value="updateValue"
/>
<label
v-t="label"
:class="{ 'p-error': isInvalid }"
:for="id"
class="text-primary/40"
/> />
<label :for="id" :class="{ 'p-error': isInvalid }">{{ label }}</label>
</div> </div>
<slot name="errors"> <small v-if="formSubmitted && isInvalid" class="p-error">{{ errorText }}</small>
<small <small v-if="helpText" class="form-text text-muted">{{ helpText }}</small>
v-if="isInvalid"
v-t="errorText"
class="p-error"
/>
</slot>
</div> </div>
</template> </template>
@ -47,14 +37,26 @@ const props = defineProps({
errorText: { errorText: {
type: String, type: String,
required: false, required: false,
default: null, default: "",
}, },
isInvalid: { isInvalid: {
type: Boolean, type: Boolean,
required: false, required: false,
default: false, default: false,
}, },
required: {
type: Boolean,
default: false,
},
helpText: String,
formSubmitted: {
type: Boolean,
default: false
},
}) })
defineEmits(["update:modelValue"]) const emits = defineEmits(["update:modelValue"])
const updateValue = (value) => {
emits("update:modelValue", value)
}
</script> </script>

@ -0,0 +1,67 @@
<template>
<div class="field">
<div class="p-float-label">
<MultiSelect
id="multiSelect"
v-model="selectedValues"
:options="options"
optionLabel="name"
optionValue="id"
display="chip"
@update:model-value="updateModelValue"
@focus="isFocused = true"
@blur="isFocused = false"
panelClass="multi-select-panel"
/>
<label :for="inputId" v-text="label" />
</div>
<small v-if="isInvalid" :class="{ 'p-error': isInvalid }" v-text="errorText" />
</div>
</template>
<script setup>
import { ref, watch } from 'vue'
import MultiSelect from 'primevue/multiselect'
const props = defineProps({
modelValue: {
type: Array,
default: () => []
},
options: {
type: Array,
default: () => []
},
placeholder: String,
inputId: {
type: String,
required: true,
default: "",
},
label: {
type: String,
required: true,
default: "",
},
errorText: {
type: String,
required: false,
default: null,
},
isInvalid: {
type: Boolean,
required: false,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const selectedValues = ref([...props.modelValue])
const isFocused = ref(false)
watch(() => props.modelValue, (newValue) => {
selectedValues.value = [...newValue]
})
const updateModelValue = (newValue) => {
emit('update:modelValue', newValue)
}
</script>

@ -1,26 +1,29 @@
<template> <template>
<Toolbar :class="toolbarClass"> <Toolbar :class="toolbarClass">
<template #start> <template v-slot:start>
<slot></slot> <slot name="start"></slot>
</template>
<template v-slot:end>
<slot name="end"></slot>
</template> </template>
</Toolbar> </Toolbar>
</template> </template>
<script setup> <script setup>
import Toolbar from "primevue/toolbar"; import Toolbar from "primevue/toolbar"
import { computed } from "vue"; import { computed } from "vue"
const props = defineProps({ const props = defineProps({
showTopBorder: { showTopBorder: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}); })
const toolbarClass = computed(() => { const toolbarClass = computed(() => {
if (props.showTopBorder) { if (props.showTopBorder) {
return "pt-5 border-t border-b"; return "pt-5 border-t border-b"
} }
return ""; return "p-toolbar"
}); })
</script> </script>

@ -1,174 +1,174 @@
<template> <template>
<v-form> <div class="course-form-container">
<v-container fluid> <div class="form-header">
<v-row> <BaseInputText
<v-col cols="12" sm="6" md="6"> id="course-name"
<v-text-field :label="t('Course name')"
v-model="item.title" :help-text="t('Write a short and striking course name, For example: Innovation Management')"
:error-messages="titleErrors" v-model="courseName"
:label="$t('Title')" :error-text="courseNameError"
:is-invalid="isCourseNameInvalid"
required required
@input="$v.item.title.$touch()"
@blur="$v.item.title.$touch()"
/> />
</v-col> <BaseAdvancedSettingsButton v-model="showAdvancedSettings"></BaseAdvancedSettingsButton>
</div>
<v-col cols="12" sm="6" md="6"> <div v-if="showAdvancedSettings" class="advanced-settings">
<v-text-field <BaseMultiSelect
v-model="item.code" id="category-multiselect"
:error-messages="codeErrors" v-model="courseCategory"
:label="$t('code')" :options="categoryOptions"
required :label="t('Category')"
@input="$v.item.code.$touch()" input-id="multiselect-category"
@blur="$v.item.code.$touch()"
/> />
</v-col> <BaseInputText
</v-row> id="course-code"
:label="t('Course code')"
<v-row> :help-text="t('Only letters (a-z) and numbers (0-9)')"
<v-col cols="12" sm="6" md="6"> v-model="courseCode"
<v-combobox :maxlength="40"
v-model="item.category" :error-text="courseCodeError"
:items="categorySelectItems" :is-invalid="isCodeInvalid"
:error-messages="categoryErrors" validation-message="Only letters (a-z) and numbers (0-9) are allowed."
:no-data-text="$t('No results')"
:label="$t('category')"
item-text="name"
item-value="@id"
/> />
</v-col> <BaseDropdown
name="language"
<v-col cols="12" sm="6" md="6"> v-model="courseLanguage"
<v-text-field :options="languageOptions"
v-model.number="item.visibility" :placeholder="t('Select Language')"
:error-messages="visibilityErrors" input-id="language-dropdown"
:label="$t('visibility')" :label="t('Language')"
required option-label="name"
@input="$v.item.visibility.$touch()" />
@blur="$v.item.visibility.$touch()" <BaseCheckbox
id="demo-content"
:label="t('Fill with demo content')"
v-model="fillDemoContent"
name=""
/>
<BaseAutocomplete
id="template"
v-model="courseTemplate"
:label="t('Select Template')"
:search="searchTemplates"
/>
</div>
<!-- Form Footer -->
<div class="form-footer">
<BaseButton
label="Back"
icon="back"
type="secondary"
@click="goBack"
class="mr-4"
/> />
</v-col> <BaseButton
</v-row> :label="t('Create this course')"
</v-container> icon="plus"
</v-form> type="primary"
@click="submitForm"
/>
</div>
</div>
</template> </template>
<script> <script setup>
import has from 'lodash/has'; import { onMounted, ref, watch } from "vue"
import useVuelidate from '@vuelidate/core'; import BaseInputText from "../basecomponents/BaseInputText.vue"
import { required } from '@vuelidate/validators'; import BaseAdvancedSettingsButton from "../basecomponents/BaseAdvancedSettingsButton.vue"
import { mapActions } from 'vuex'; import BaseDropdown from "../basecomponents/BaseDropdown.vue"
import { mapFields } from 'vuex-map-fields'; import BaseCheckbox from "../basecomponents/BaseCheckbox.vue"
import BaseButton from "../basecomponents/BaseButton.vue"
export default { import { useRouter } from "vue-router"
name: 'CourseForm', import courseService from "../../services/courseService"
setup () { import languageService from "../../services/languageService"
return { v$: useVuelidate() } import BaseAutocomplete from "../basecomponents/BaseAutocomplete.vue"
}, import BaseMultiSelect from "../basecomponents/BaseMultiSelect.vue"
props: { import { useI18n } from "vue-i18n"
values: {
type: Object, const { t } = useI18n()
required: true const courseName = ref('')
}, const courseCategory = ref([])
const courseCode = ref('')
errors: { const courseLanguage = ref(null)
type: Object, const fillDemoContent = ref(false)
default: () => {} const courseTemplate = ref(null);
}, const showAdvancedSettings = ref(false)
const router = useRouter()
initialValues: {
type: Object, const categoryOptions = ref([])
default: () => {} const languageOptions = ref([])
const courseNameError = ref('')
const courseCodeError = ref('')
const isCodeInvalid = ref(false)
const isCourseNameInvalid = ref(false)
const formSubmitted = ref(false)
const emit = defineEmits(['submit'])
const validateCourseCode = () => {
const pattern = /^[a-zA-Z0-9]*$/
if (!pattern.test(courseCode.value)) {
isCodeInvalid.value = true
courseCodeError.value = 'Only letters (a-z) and numbers (0-9) are allowed.'
return false
}
courseCodeError.value = ''
return true
} }
},
data() {
return {
title: null,
code: null,
category: null,
visibility: null,
};
},
computed: {
...mapFields('coursecategory', {
categorySelectItems: 'selectItems'
}),
// eslint-disable-next-line
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;
},
codeErrors() {
const errors = [];
if (!this.$v.item.code.$dirty) return errors;
has(this.violations, 'code') && errors.push(this.violations.code);
!this.$v.item.code.required && errors.push(this.$t('Field is required'));
return errors;
},
categoryErrors() {
const errors = [];
if (!this.$v.item.category.$dirty) return errors;
has(this.violations, 'category') && errors.push(this.violations.category);
return errors;
},
visibilityErrors() {
const errors = [];
if (!this.$v.item.visibility.$dirty) return errors; const submitForm = () => {
formSubmitted.value = true
if (!courseName.value) {
isCourseNameInvalid.value = true
courseNameError.value = 'This field is required'
return
}
has(this.violations, 'visibility') && errors.push(this.violations.visibility); if (!validateCourseCode()) {
return
}
!this.$v.item.visibility.required && errors.push(this.$t('Field is required')); emit('submit', {
name: courseName.value,
category: courseCategory.value ? courseCategory.value : null,
code: courseCode.value,
language: courseLanguage.value,
template: courseTemplate.value ? courseTemplate.value.value : null,
fillDemoContent: fillDemoContent.value
})
}
return errors; onMounted(async () => {
}, try {
const categoriesResponse = await courseService.getCategories('categories');
categoryOptions.value = categoriesResponse.map(category => ({
name: category.name,
id: category.id,
}))
const languagesResponse = await languageService.findAll()
const data = await languagesResponse.json()
languageOptions.value = data['hydra:member'].map(language => ({
name: language.englishName,
id: language.isocode,
}))
} catch (error) {
console.error('Failed to load dropdown data', error)
}
});
violations() { const searchTemplates = async (query) => {
return this.errors || {}; if (query && query.length >= 3) {
return courseService.searchTemplates(query)
} else {
return []
} }
},
mounted() {
this.categoryGetSelectItems();
},
methods: {
...mapActions({
categoryGetSelectItems: 'coursecategory/load'
}),
},
validations: {
item: {
title: {
required,
},
code: {
required,
},
category: {
},
visibility: {
required,
},
} }
const goBack = () => {
router.go(-1)
} }
};
</script> </script>

@ -6,6 +6,7 @@
:label="t('Course')" :label="t('Course')"
class="p-button-secondary hidden md:inline-flex" class="p-button-secondary hidden md:inline-flex"
icon="pi pi-plus" icon="pi pi-plus"
@click="redirectToCreateCourse"
/> />
</div> </div>
<hr> <hr>
@ -17,7 +18,13 @@ import Button from 'primevue/button';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { storeToRefs } from "pinia" import { storeToRefs } from "pinia"
import { useSecurityStore } from "../store/securityStore" import { useSecurityStore } from "../store/securityStore"
import { useRouter } from "vue-router"
const { t } = useI18n(); const { t } = useI18n();
const { isTeacher } = storeToRefs(useSecurityStore()) const { isTeacher } = storeToRefs(useSecurityStore())
const router = useRouter()
const redirectToCreateCourse = () => {
router.push({ name: 'CourseCreate' })
}
</script> </script>

@ -1,6 +1,7 @@
import { ENTRYPOINT } from "../config/entrypoint" import { ENTRYPOINT } from "../config/entrypoint"
import axios from "axios" import axios from "axios"
const API_URL = '/course';
const courseService = { const courseService = {
/** /**
* @param {number} courseId * @param {number} courseId
@ -71,7 +72,7 @@ const courseService = {
*/ */
checkLegal: async (courseId, sessionId = 0) => { checkLegal: async (courseId, sessionId = 0) => {
const { data } = await axios.get( const { data } = await axios.get(
`/course/${courseId}/checkLegal.json`, `${API_URL}/${courseId}/checkLegal.json`,
{ {
params: { params: {
sid: sessionId, sid: sessionId,
@ -81,6 +82,41 @@ const courseService = {
return data return data
}, },
/**
* Creates a new course with the provided data.
* @param {Object} courseData - The data for the course to be created.
* @returns {Promise<Object>} The server response after creating the course.
*/
createCourse: async (courseData) => {
const response = await axios.post(`${API_URL}/create`, courseData);
console.log('response create ::', response);
return response.data;
},
/**
* Fetches available categories for courses.
* @returns {Promise<Array>} A list of available categories.
*/
getCategories: async () => {
const response = await axios.get(`${API_URL}/categories`);
return response.data;
},
/**
* Searches for templates based on a provided search term.
* @param {string} searchTerm - The search term for the templates.
* @returns {Promise<Array>} A list of templates matching the search term.
*/
searchTemplates: async (searchTerm) => {
const response = await axios.get(`${API_URL}/search_templates`, {
params: { search: searchTerm }
});
return response.data.items.map(item => ({
name: item.name,
value: item.id
}));
},
} }
export default courseService export default courseService

@ -1,53 +1,64 @@
<template> <template>
<div> <div class="create-course-page m-10">
<div class="message-container mb-4">
<Message severity="info">
{{ t('Once you click on "Create a course", a course is created with a section for Tests, Project based learning, Assessments, Courses, Dropbox, Agenda and much more. Logging in as teacher provides you with editing privileges for this course.') }}
</Message>
</div>
<h1 class="page-title text-xl text-gray-90">{{ t('Add a new course') }}</h1>
<hr />
<CourseForm <CourseForm
ref="createForm" ref="createForm"
:errors="violations" :errors="violations"
:values="item" :values="item"
@submit="submitCourse"
/> />
<Loading :visible="isLoading" /> <Loading :visible="isLoading" />
<Toolbar
:handle-reset="resetForm"
:handle-submit="onSendForm"
></Toolbar>
</div> </div>
</template> </template>
<script> <script setup>
import { mapActions } from "vuex" import { ref, computed } from 'vue'
import { createHelpers } from "vuex-map-fields" import { useStore } from 'vuex'
import CourseForm from "../../components/course/Form.vue" import CourseForm from '../../components/course/Form.vue'
import Loading from "../../components/Loading.vue" import Loading from '../../components/Loading.vue'
import Toolbar from "../../components/Toolbar.vue" import { useRouter } from "vue-router"
import CreateMixin from "../../mixins/CreateMixin" import Message from 'primevue/message'
import courseService from "../../services/courseService"
import { useI18n } from "vue-i18n"
const servicePrefix = "Course" const store = useStore()
const item = ref({})
const router = useRouter()
const { t } = useI18n()
const { mapFields } = createHelpers({ const isLoading = computed(() => store.getters['course/getField']('isLoading'))
getterType: "course/getField", const violations = computed(() => store.getters['course/getField']('violations'))
mutationType: "course/updateField", const courseData = ref({})
})
export default { const submitCourse = async (formData) => {
name: "CourseCreate", isLoading.value = true
servicePrefix, try {
mixins: [CreateMixin], let tempResponse = await courseService.createCourse(formData)
components: { if (tempResponse.success) {
Loading, const courseId = tempResponse.courseId
Toolbar, const sessionId = 0
CourseForm, await router.push(`/course/${courseId}/home?sid=${sessionId}`)
}, } else {
data() { console.error(tempResponse.message)
return { }
item: {}, } catch (error) {
console.error(error)
if (error.response && error.response.data) {
violations.value = error.response.data
} else {
console.error('An unexpected error occurred.')
}
} finally {
isLoading.value = false
} }
},
computed: {
...mapFields(["error", "isLoading", "created", "violations"]),
},
methods: {
...mapActions("course", ["create", "reset"]),
},
} }
</script> </script>

@ -2777,7 +2777,8 @@ function store_reply(CForum $forum, CForumThread $thread, $values, $courseId = 0
send_notification_mails( send_notification_mails(
$forum, $forum,
$thread, $thread,
$values $values,
$course
); );
add_forum_attachment_file('', $post); add_forum_attachment_file('', $post);
@ -3175,9 +3176,14 @@ function get_unaproved_messages($forum_id)
* This function sends the notification mails to everybody who stated that they wanted to be informed when a new post * This function sends the notification mails to everybody who stated that they wanted to be informed when a new post
* was added to a given thread. * was added to a given thread.
*/ */
function send_notification_mails(CForum $forum, CForumThread $thread, $reply_info) function send_notification_mails(CForum $forum, CForumThread $thread, $reply_info, ?Course $course = null)
{ {
$courseEntity = api_get_course_entity(); $courseEntity = api_get_course_entity();
if (null !== $course) {
$courseEntity = $course;
}
$courseId = $courseEntity->getId(); $courseId = $courseEntity->getId();
$sessionId = api_get_session_id(); $sessionId = api_get_session_id();

@ -43,17 +43,17 @@ $request = Request::createFromGlobals();
$request->request->set('load_legacy', true); $request->request->set('load_legacy', true);
$currentBaseUrl = $request->getBaseUrl(); $currentBaseUrl = $request->getBaseUrl();
$kernel->boot(); $kernel->boot();
$currentUri = $request->getRequestUri();
if (empty($currentBaseUrl)) {
$currentBaseUrl = $request->getSchemeAndHttpHost() . $request->getBasePath();
}
$container = $kernel->getContainer(); $container = $kernel->getContainer();
$router = $container->get('router'); $router = $container->get('router');
$context = $router->getContext(); $context = $router->getContext();
$router->setContext($context); $router->setContext($context);
/** @var FlashBag $flashBag */ /** @var FlashBag $flashBag */
$saveFlashBag = null;
$flashBag = $container->get('session')->getFlashBag(); $flashBag = $container->get('session')->getFlashBag();
if (!empty($flashBag->keys())) { $saveFlashBag = !empty($flashBag->keys()) ? $flashBag->all() : null;
$saveFlashBag = $flashBag->all();
}
$response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, false); $response = $kernel->handle($request, HttpKernelInterface::MAIN_REQUEST, false);
$context = Container::getRouter()->getContext(); $context = Container::getRouter()->getContext();
@ -70,21 +70,25 @@ if ($isCli) {
if ($isCli && $baseUrl) { if ($isCli && $baseUrl) {
$context->setBaseUrl($baseUrl); $context->setBaseUrl($baseUrl);
} else { } else {
$pos = strpos($currentBaseUrl, 'main'); $fullUrl = $currentBaseUrl . $currentUri;
$posPlugin = strpos($currentBaseUrl, 'plugin'); $posMain = strpos($fullUrl, '/main');
$posCertificate = strpos($currentBaseUrl, 'certificate'); $posPlugin = strpos($fullUrl, '/plugin');
$posCourse = strpos($fullUrl, '/course');
$posCertificate = strpos($fullUrl, '/certificate');
if (false === $pos && false === $posPlugin && false === $posCertificate) { if (false === $posMain && false === $posPlugin && false === $posCourse && false === $posCertificate) {
echo 'Cannot load current URL'; echo 'Cannot load current URL';
exit; exit;
} }
if (false !== $pos) { if (false !== $posMain) {
$newBaseUrl = substr($currentBaseUrl, 0, $pos - 1); $newBaseUrl = substr($fullUrl, 0, $posMain);
} elseif (false !== $posPlugin) { } elseif (false !== $posPlugin) {
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1); $newBaseUrl = substr($fullUrl, 0, $posPlugin);
} elseif (false !== $posCourse) {
$newBaseUrl = substr($fullUrl, 0, $posCourse);
} elseif (false !== $posCertificate) { } elseif (false !== $posCertificate) {
$newBaseUrl = substr($currentBaseUrl, 0, $posPlugin - 1); $newBaseUrl = substr($fullUrl, 0, $posCertificate);
} }
$context->setBaseUrl($newBaseUrl); $context->setBaseUrl($newBaseUrl);

@ -4,6 +4,8 @@
use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\CourseRelUser; use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\GradebookCategory;
use Chamilo\CoreBundle\Entity\GradebookLink;
use Chamilo\CoreBundle\Framework\Container; use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CourseBundle\Entity\CGroupCategory; use Chamilo\CourseBundle\Entity\CGroupCategory;
use Chamilo\CourseBundle\Entity\CToolIntro; use Chamilo\CourseBundle\Entity\CToolIntro;
@ -224,33 +226,57 @@ class AddCourse
} }
/** /**
* Fills the course database with some required content and example content. * Populates the course with essential settings, group categories, example content, and installs course plugins.
* *
* @param bool Whether to fill the course with example content * This method initializes a new course by setting up various course settings, creating a default group category,
* @param int $authorId * inserting example content if required, and installing course-specific plugins. The method can also fill the course
* with exemplary content based on the parameter provided or the system setting for example content creation.
*
* @param Course $course The course entity to be populated.
* @param bool|null $fillWithExemplaryContent Determines whether to fill the course with exemplary content. If null,
* the system setting for example material course creation is used.
* @param int $authorId The ID of the user who is considered the author of the exemplary content. Defaults to the
* current user if not specified.
* *
* @return bool False on error, true otherwise * @return bool Returns true if the course is successfully populated, false otherwise.
* *
* @version 1.2 * @throws Exception Throws exception on error.
* @assert (null, '', '', null) === false
* @assert (1, 'ABC', null, null) === false
* @assert (1, 'TEST', 'spanish', true) === true
*/ */
public static function fillCourse( public static function fillCourse(Course $course, bool $fillWithExemplaryContent = null, int $authorId = 0): bool
Course $course, {
$fill_with_exemplary_content = null, $entityManager = Database::getManager();
$authorId = 0 $authorId = $authorId ?: api_get_user_id();
) {
if (is_null($fill_with_exemplary_content)) { self::insertCourseSettings($course);
$fill_with_exemplary_content = 'false' !== api_get_setting('example_material_course_creation'); self::createGroupCategory($course);
if ($fillWithExemplaryContent ?? api_get_setting('example_material_course_creation') !== 'false') {
self::insertExampleContent($course, $authorId, $entityManager);
} }
$course_id = $course->getId(); self::installCoursePlugins($course->getId());
$authorId = empty($authorId) ? api_get_user_id() : (int) $authorId;
return true;
}
/**
* Inserts default and specified settings for a given course.
*
* This method takes a Course object as input and applies a predefined
* set of settings. These settings include configurations for email alerts,
* permissions for users to edit various components like agenda and announcements,
* theme settings, and more. It also handles the case where a course needs to
* enable certain features by default based on platform-wide settings.
*
* @param Course $course The course object to which the settings will be applied.
*
* @return void
* @throws Exception
*/
private static function insertCourseSettings(Course $course): void
{
$TABLESETTING = Database::get_course_table(TABLE_COURSE_SETTING); $TABLESETTING = Database::get_course_table(TABLE_COURSE_SETTING);
$TABLEGRADEBOOK = Database::get_main_table(TABLE_MAIN_GRADEBOOK_CATEGORY);
$TABLEGRADEBOOKLINK = Database::get_main_table(TABLE_MAIN_GRADEBOOK_LINK);
$settingsManager = Container::getCourseSettingsManager(); $settingsManager = Container::getCourseSettingsManager();
$settingsManager->setCourse($course); $settingsManager->setCourse($course);
@ -294,13 +320,26 @@ class AddCourse
$title = $setting['title'] ?? ''; $title = $setting['title'] ?? '';
Database::query( Database::query(
"INSERT INTO $TABLESETTING (c_id, title, variable, value, category) "INSERT INTO $TABLESETTING (c_id, title, variable, value, category)
VALUES ($course_id, '".$title."', '".$variable."', '".$setting['default']."', '".$setting['category']."')" VALUES ({$course->getId()}, '".$title."', '".$variable."', '".$setting['default']."', '".$setting['category']."')"
); );
$counter++; $counter++;
} }
}
/* Course homepage tools for platform admin only */ /**
/* Group tool */ * Creates a default group category for the specified course.
*
* This method initializes a new group category with a default title
* and associates it with the given course. The default group category
* is essential for organizing groups within the course, allowing for
* better management and classification of course participants.
*
* @param Course $course The course object for which the default group category is created.
*
* @return void
*/
private static function createGroupCategory(Course $course): void
{
$groupCategory = new CGroupCategory(); $groupCategory = new CGroupCategory();
$groupCategory $groupCategory
->setTitle(get_lang('Default groups')) ->setTitle(get_lang('Default groups'))
@ -309,38 +348,35 @@ class AddCourse
; ;
Database::getManager()->persist($groupCategory); Database::getManager()->persist($groupCategory);
Database::getManager()->flush(); Database::getManager()->flush();
}
/**
* Inserts example content into a given course.
*
* This method populates the specified course with a predefined set of example
* content, including documents, links, announcements, and exercises. This content
* serves as a template or starting point for instructors, showcasing the various
* types of materials and activities that can be included in a course. The content
* is added under the authority of the specified author ID.
*
* @param Course $course The course object into which the example content will be inserted.
* @param int $authorId The ID of the user who will be listed as the author of the inserted content.
*
* @return void
* @throws Exception
*/
private static function insertExampleContent(Course $course, int $authorId): void
{
$now = api_get_utc_datetime(); $now = api_get_utc_datetime();
/*$files = [
['path' => '/shared_folder', 'title' => get_lang('Folders of users'), 'filetype' => 'folder', 'size' => 0],
[
'path' => '/chat_files',
'title' => get_lang('Chat conversations history'),
'filetype' => 'folder',
'size' => 0,
],
['path' => '/certificates', 'title' => get_lang('Certificates'), 'filetype' => 'folder', 'size' => 0],
];
$counter = 1;
foreach ($files as $file) {
self::insertDocument($courseInfo, $counter, $file, $authorId);
$counter++;
}*/
$certificateId = 'NULL';
/* Documents */
if ($fill_with_exemplary_content) {
$files = [ $files = [
['path' => '/audio', 'title' => get_lang('Audio'), 'filetype' => 'folder', 'size' => 0], ['path' => '/audio', 'title' => get_lang('Audio'), 'filetype' => 'folder', 'size' => 0],
//['path' => '/flash', 'title' => get_lang('Flash'), 'filetype' => 'folder', 'size' => 0],
['path' => '/images', 'title' => get_lang('Images'), 'filetype' => 'folder', 'size' => 0], ['path' => '/images', 'title' => get_lang('Images'), 'filetype' => 'folder', 'size' => 0],
['path' => '/images/gallery', 'title' => get_lang('Gallery'), 'filetype' => 'folder', 'size' => 0], ['path' => '/images/gallery', 'title' => get_lang('Gallery'), 'filetype' => 'folder', 'size' => 0],
['path' => '/video', 'title' => get_lang('Video'), 'filetype' => 'folder', 'size' => 0], ['path' => '/video', 'title' => get_lang('Video'), 'filetype' => 'folder', 'size' => 0],
//['path' => '/video/flv', 'title' => 'flv', 'filetype' => 'folder', 'size' => 0],
]; ];
$paths = []; $paths = [];
$courseInfo = ['real_id' => $course->getId()]; $courseInfo = ['real_id' => $course->getId(), 'code' => $course->getCode()];
$counter = 1;
foreach ($files as $file) { foreach ($files as $file) {
$doc = self::insertDocument($courseInfo, $counter, $file, $authorId); $doc = self::insertDocument($courseInfo, $counter, $file, $authorId);
$paths[$file['path']] = $doc->getIid(); $paths[$file['path']] = $doc->getIid();
@ -422,7 +458,7 @@ class AddCourse
$link->setCourse($courseInfo); $link->setCourse($courseInfo);
$links = [ $links = [
[ [
'c_id' => $course_id, 'c_id' => $course->getId(),
'url' => 'http://www.google.com', 'url' => 'http://www.google.com',
'title' => 'Quick and powerful search engine', 'title' => 'Quick and powerful search engine',
'description' => get_lang('Quick and powerful search engine'), 'description' => get_lang('Quick and powerful search engine'),
@ -432,7 +468,7 @@ class AddCourse
'session_id' => 0, 'session_id' => 0,
], ],
[ [
'c_id' => $course_id, 'c_id' => $course->getId(),
'url' => 'http://www.wikipedia.org', 'url' => 'http://www.wikipedia.org',
'title' => 'Free online encyclopedia', 'title' => 'Free online encyclopedia',
'description' => get_lang('Free online encyclopedia'), 'description' => get_lang('Free online encyclopedia'),
@ -459,39 +495,8 @@ class AddCourse
$now $now
); );
$manager = Database::getManager();
/* Introduction text */
$intro_text = '<p style="text-align: center;">
<img src="'.api_get_path(REL_CODE_PATH).'img/mascot.png" alt="Mr. Chamilo" title="Mr. Chamilo" />
<h2>'.get_lang('Introduction text').'</h2>
</p>';
$toolIntro = (new CToolIntro())
->setIntroText($intro_text)
->addCourseLink($course)
;
$manager->persist($toolIntro);
$toolIntro = (new CToolIntro())
->setIntroText(get_lang('This page allows users and groups to publish documents.'))
->addCourseLink($course)
;
$manager->persist($toolIntro);
$toolIntro = (new CToolIntro())
->setIntroText(
get_lang(
'The word Wiki is short for WikiWikiWeb. Wikiwiki is a Hawaiian word, meaning "fast" or "speed". In a wiki, people write pages together. If one person writes something wrong, the next person can correct it. The next person can also add something new to the page. Because of this, the pages improve continuously.'
)
)
->addCourseLink($course)
;
$manager->persist($toolIntro);
$manager->flush();
/* Exercise tool */ /* Exercise tool */
$exercise = new Exercise($course_id); $exercise = new Exercise($course->getId());
$exercise->exercise = get_lang('Sample test'); $exercise->exercise = get_lang('Sample test');
$html = '<table width="100%" border="0" cellpadding="0" cellspacing="0"> $html = '<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr> <tr>
@ -507,8 +512,6 @@ class AddCourse
$exercise->description = $html; $exercise->description = $html;
$exercise->save(); $exercise->save();
$exercise_id = $exercise->id;
$question = new MultipleAnswer(); $question = new MultipleAnswer();
$question->course = $courseInfo; $question->course = $courseInfo;
$question->question = get_lang('Socratic irony is...'); $question->question = get_lang('Socratic irony is...');
@ -557,30 +560,90 @@ class AddCourse
saveThread($forumEntity, $params, $courseInfo, false); saveThread($forumEntity, $params, $courseInfo, false);
self::createGradebookStructure($course, $exercise->id);
}
/**
* Creates the gradebook structure for a course.
*
* This method sets up the initial gradebook categories and links for a new course.
* It creates a parent gradebook category representing the course itself and a child
* gradebook category for course activities. It then creates a gradebook link associated
* with a specific course activity, identified by the $refId parameter.
*
* @param Course $course The course entity for which the gradebook structure will be created.
* @param int $refId The reference ID of the course activity to link in the gradebook.
*
* @return void
*/
private static function createGradebookStructure(Course $course, int $refId): void
{
$manager = Database::getManager();
/* Gradebook tool */ /* Gradebook tool */
$course_code = $courseInfo['code']; $courseCode = $course->getCode();
// father gradebook
Database::query( $parentGradebookCategory = new GradebookCategory();
"INSERT INTO $TABLEGRADEBOOK (title, locked, generate_certificates, description, user_id, c_id, parent_id, weight, visible, certif_min_score, session_id, document_id) $parentGradebookCategory->setTitle($courseCode);
VALUES ('$course_code','0',0,'',1,$course_id,0,100,0,75,NULL,$certificateId)" $parentGradebookCategory->setLocked(0);
); $parentGradebookCategory->setGenerateCertificates(false);
$gbid = Database::insert_id(); $parentGradebookCategory->setDescription('');
Database::query( $parentGradebookCategory->setCourse($course);
"INSERT INTO $TABLEGRADEBOOK (title, locked, generate_certificates, description, user_id, c_id, parent_id, weight, visible, certif_min_score, session_id, document_id) $parentGradebookCategory->setWeight(100);
VALUES ('$course_code','0',0,'',1,$course_id,$gbid,100,1,75,NULL,$certificateId)" $parentGradebookCategory->setVisible(false);
); $parentGradebookCategory->setCertifMinScore(75);
$gbid = Database:: insert_id(); $parentGradebookCategory->setUser(api_get_user_entity());
Database::query(
"INSERT INTO $TABLEGRADEBOOKLINK (type, ref_id, user_id, c_id, category_id, created_at, weight, visible, locked) $manager->persist($parentGradebookCategory);
VALUES (1,$exercise_id,1,$course_id,$gbid,'$now',100,1,0)" $manager->flush();
);
$childGradebookCategory = new GradebookCategory();
$childGradebookCategory->setTitle($courseCode);
$childGradebookCategory->setLocked(0);
$childGradebookCategory->setGenerateCertificates(false);
$childGradebookCategory->setDescription('');
$childGradebookCategory->setCourse($course);
$childGradebookCategory->setWeight(100);
$childGradebookCategory->setVisible(true);
$childGradebookCategory->setCertifMinScore(75);
$childGradebookCategory->setParent($parentGradebookCategory);
$childGradebookCategory->setUser(api_get_user_entity());
$manager->persist($childGradebookCategory);
$manager->flush();
$gradebookLink = new GradebookLink();
$gradebookLink->setType(1);
$gradebookLink->setRefId($refId);
$gradebookLink->setUser(api_get_user_entity());
$gradebookLink->setCourse($course);
$gradebookLink->setCategory($childGradebookCategory);
$gradebookLink->setCreatedAt(new \DateTime());
$gradebookLink->setWeight(100);
$gradebookLink->setVisible(1);
$gradebookLink->setLocked(0);
$manager->persist($gradebookLink);
$manager->flush();
} }
// Installing plugins in course /**
* Installs plugins for a given course.
*
* This method takes a course ID and uses the AppPlugin service to install
* all necessary or default plugins for that specific course. These plugins
* can enhance the functionality of the course by adding new features or
* tools that are not part of the core Chamilo platform.
*
* @param int $courseId The ID of the course for which the plugins will be installed.
*
* @return void
*/
private static function installCoursePlugins(int $courseId): void
{
$app_plugin = new AppPlugin(); $app_plugin = new AppPlugin();
$app_plugin->install_course_plugins($course_id); $app_plugin->install_course_plugins($courseId);
return true;
} }
/** /**

@ -50,28 +50,6 @@ class CourseManager
$accessUrlId = !empty($accessUrlId) ? (int) $accessUrlId : api_get_current_access_url_id(); $accessUrlId = !empty($accessUrlId) ? (int) $accessUrlId : api_get_current_access_url_id();
$authorId = empty($authorId) ? api_get_user_id() : (int) $authorId; $authorId = empty($authorId) ? api_get_user_id() : (int) $authorId;
// @todo Check that this was move inside the CourseListener in a pre persist throw an Exception
/*if (isset($_configuration[$accessUrlId]) && is_array($_configuration[$accessUrlId])) {
$return = self::checkCreateCourseAccessUrlParam(
$_configuration,
$accessUrlId,
'hosting_limit_courses',
'PortalCoursesLimitReached'
);
if (false != $return) {
return $return;
}
$return = self::checkCreateCourseAccessUrlParam(
$_configuration,
$accessUrlId,
'hosting_limit_active_courses',
'PortalActiveCoursesLimitReached'
);
if (false != $return) {
return $return;
}
}*/
if (empty($params['title'])) { if (empty($params['title'])) {
return false; return false;
} }
@ -97,7 +75,7 @@ class CourseManager
$params['directory'] = $keys['currentCourseRepository']; $params['directory'] = $keys['currentCourseRepository'];
$courseInfo = api_get_course_info($params['code']); $courseInfo = api_get_course_info($params['code']);
if (empty($courseInfo)) { if (empty($courseInfo)) {
$course = AddCourse::register_course($params, $accessUrlId); $course = AddCourse::register_course($params);
if (null !== $course) { if (null !== $course) {
self::fillCourse($course, $params, $authorId); self::fillCourse($course, $params, $authorId);
@ -2373,14 +2351,6 @@ class CourseManager
GroupManager::delete_category($category['iid'], $course->getCode()); GroupManager::delete_category($category['iid'], $course->getCode());
} }
} }
// Cleaning groups
// @todo should be cleaned by the resource.
/*$groups = GroupManager::get_groups($courseId);
if (!empty($groups)) {
foreach ($groups as $group) {
GroupManager::deleteGroup($group, $course['code']);
}
}*/
$course_tables = AddCourse::get_course_tables(); $course_tables = AddCourse::get_course_tables();
// Cleaning c_x tables // Cleaning c_x tables
@ -2391,11 +2361,12 @@ class CourseManager
continue; continue;
} }
$table = Database::get_course_table($table); $table = Database::get_course_table($table);
//$sql = "DELETE FROM $table WHERE c_id = $courseId ";
//Database::query($sql);
} }
} }
$resourceLink = $course->getFirstResourceLink();
$resourceLinkId = $resourceLink?->getId();
// Unsubscribe all users from the course // Unsubscribe all users from the course
$sql = "DELETE FROM $table_course_user WHERE c_id = $courseId"; $sql = "DELETE FROM $table_course_user WHERE c_id = $courseId";
Database::query($sql); Database::query($sql);
@ -2426,8 +2397,10 @@ class CourseManager
// but give information on the course history // but give information on the course history
//$sql = "DELETE FROM $table_stats_default WHERE c_id = $courseId"; //$sql = "DELETE FROM $table_stats_default WHERE c_id = $courseId";
//Database::query($sql); //Database::query($sql);
$sql = "DELETE FROM $table_stats_downloads WHERE c_id = $courseId"; if ($resourceLinkId) {
$sql = "DELETE FROM $table_stats_downloads WHERE resource_link_id = $resourceLinkId";
Database::query($sql); Database::query($sql);
}
$sql = "DELETE FROM $table_stats_links WHERE c_id = $courseId"; $sql = "DELETE FROM $table_stats_links WHERE c_id = $courseId";
Database::query($sql); Database::query($sql);
$sql = "DELETE FROM $table_stats_uploads WHERE c_id = $courseId"; $sql = "DELETE FROM $table_stats_uploads WHERE c_id = $courseId";

@ -1,6 +1,7 @@
<?php <?php
/* For licensing terms, see /license.txt */ /* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Tool;
use Chamilo\CourseBundle\Entity\CTool; use Chamilo\CourseBundle\Entity\CTool;
/** /**
@ -1044,17 +1045,13 @@ class Plugin
* *
* @param string $name The tool name * @param string $name The tool name
* @param int $courseId The course ID * @param int $courseId The course ID
* @param string $iconName Optional. Icon file name * @param string|null $iconName Optional. Icon file name
* @param string $link Optional. Link URL * @param string|null $link Optional. Link URL
* *
* @return CTool|null * @return CTool|null
*/ */
protected function createLinkToCourseTool( protected function createLinkToCourseTool(string $name, int $courseId, string $iconName = null, string $link = null): ?CTool
$name, {
$courseId,
$iconName = null,
$link = null
) {
if (!$this->addCourseTool) { if (!$this->addCourseTool) {
return null; return null;
} }
@ -1065,31 +1062,23 @@ class Plugin
$em = Database::getManager(); $em = Database::getManager();
/** @var CTool $tool */ /** @var CTool $tool */
$tool = $em $tool = $em->getRepository(CTool::class)->findOneBy([
->getRepository(CTool::class)
->findOneBy([
'title' => $name, 'title' => $name,
'course' => $courseId, 'course' => api_get_course_entity($courseId),
'category' => 'plugin',
]); ]);
if (!$tool) { if (!$tool) {
$cToolId = AddCourse::generateToolId($courseId);
$pluginName = $this->get_name(); $pluginName = $this->get_name();
$toolEntity = new Tool();
$toolEntity->setTitle($pluginName);
$tool = new CTool(); $tool = new CTool();
$tool $tool->setCourse(api_get_course_entity($courseId))
->setCourse(api_get_course_entity($courseId))
->setTitle($name.$visibilityPerStatus) ->setTitle($name.$visibilityPerStatus)
->setLink($link ?: "$pluginName/start.php")
->setImage($iconName ?: "$pluginName.png")
->setVisibility($visibility) ->setVisibility($visibility)
->setAdmin(0) ->setTool($toolEntity)
->setAddress('squaregrey.gif') ->setPosition(0);
->setAddedTool(false)
->setTarget('_self')
->setCategory('plugin')
->setSessionId(0);
$em->persist($tool); $em->persist($tool);
$em->flush(); $em->flush();

@ -7,6 +7,7 @@ declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller; namespace Chamilo\CoreBundle\Controller;
use Chamilo\CoreBundle\Entity\Course; use Chamilo\CoreBundle\Entity\Course;
use Chamilo\CoreBundle\Entity\CourseCategory;
use Chamilo\CoreBundle\Entity\CourseRelUser; use Chamilo\CoreBundle\Entity\CourseRelUser;
use Chamilo\CoreBundle\Entity\ExtraField; use Chamilo\CoreBundle\Entity\ExtraField;
use Chamilo\CoreBundle\Entity\Session; use Chamilo\CoreBundle\Entity\Session;
@ -15,12 +16,15 @@ use Chamilo\CoreBundle\Entity\Tag;
use Chamilo\CoreBundle\Entity\Tool; use Chamilo\CoreBundle\Entity\Tool;
use Chamilo\CoreBundle\Entity\User; use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Framework\Container; use Chamilo\CoreBundle\Framework\Container;
use Chamilo\CoreBundle\Repository\CourseCategoryRepository;
use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository; use Chamilo\CoreBundle\Repository\ExtraFieldValuesRepository;
use Chamilo\CoreBundle\Repository\LanguageRepository; use Chamilo\CoreBundle\Repository\LanguageRepository;
use Chamilo\CoreBundle\Repository\LegalRepository; use Chamilo\CoreBundle\Repository\LegalRepository;
use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository; use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Chamilo\CoreBundle\Repository\TagRepository; use Chamilo\CoreBundle\Repository\TagRepository;
use Chamilo\CoreBundle\Security\Authorization\Voter\CourseVoter; use Chamilo\CoreBundle\Security\Authorization\Voter\CourseVoter;
use Chamilo\CoreBundle\ServiceHelper\AccessUrlHelper;
use Chamilo\CoreBundle\Settings\SettingsManager; use Chamilo\CoreBundle\Settings\SettingsManager;
use Chamilo\CoreBundle\Tool\ToolChain; use Chamilo\CoreBundle\Tool\ToolChain;
use Chamilo\CourseBundle\Controller\ToolBaseController; use Chamilo\CourseBundle\Controller\ToolBaseController;
@ -48,6 +52,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Exception\ValidatorException; use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Contracts\Translation\TranslatorInterface;
use UserManager; use UserManager;
/** /**
@ -669,6 +674,106 @@ class CourseController extends ToolBaseController
]); ]);
} }
#[Route('/categories', name: 'chamilo_core_course_form_lists')]
public function getCategories(
SettingsManager $settingsManager,
AccessUrlHelper $accessUrlHelper,
CourseCategoryRepository $courseCategoriesRepo
): JsonResponse {
$allowBaseCourseCategory = 'true' === $settingsManager->getSetting('course.allow_base_course_category');
$accessUrlId = $accessUrlHelper->getCurrent()->getId();
$categories = $courseCategoriesRepo->findAllInAccessUrl(
$accessUrlId,
$allowBaseCourseCategory
);
$data = [];
$categoryToAvoid = '';
if (!$this->isGranted('ROLE_ADMIN')) {
$categoryToAvoid = $settingsManager->getSetting('course.course_category_code_to_use_as_model');
}
foreach ($categories as $category) {
$categoryCode = $category->getCode();
if (!empty($categoryToAvoid) && $categoryToAvoid == $categoryCode) {
continue;
}
$data[] = ['id' => $category->getId(), 'name' => $category->__toString()];
}
return new JsonResponse($data);
}
#[Route('/search_templates', name: 'chamilo_core_course_search_templates')]
public function searchCourseTemplates(
Request $request,
AccessUrlHelper $accessUrlHelper,
CourseRepository $courseRepository
): JsonResponse {
$searchTerm = $request->query->get('search', '');
$accessUrl = $accessUrlHelper->getCurrent();
/** @var User|null $user */
$user = $this->getUser();
$courseList = $courseRepository->getCoursesInfoByUser($user, $accessUrl, 1, $searchTerm);
$results = ['items' => []];
foreach ($courseList as $course) {
$title = $course['title'];
$results['items'][] = [
'id' => $course['code'],
'name' => $title.' ('.$course['code'].') ',
];
}
return new JsonResponse($results);
}
#[Route('/create', name: 'chamilo_core_course_create')]
public function createCourse(Request $request, TranslatorInterface $translator): JsonResponse
{
$courseData = json_decode($request->getContent(), true);
$wantedCode = $courseData['code'] ?? null;
$categoryCode = $courseData['category'] ?? null;
$title = $courseData['name'];
$courseLanguage = isset($courseData['language']) ? $courseData['language']['id'] : '';
$exemplaryContent = $courseData['fillDemoContent'] ?? false;
$template = $courseData['template'] ?? '';
if (empty($wantedCode)) {
$wantedCode = CourseManager::generate_course_code(substr($title, 0, CourseManager::MAX_COURSE_LENGTH_CODE));
}
$courseCodeOk = !CourseManager::course_code_exists($wantedCode);
if ($courseCodeOk) {
$params = [
'title' => $title,
'exemplary_content' => $exemplaryContent,
'wanted_code' => $wantedCode,
'course_language' => $courseLanguage,
'course_template' => $template,
];
if ($categoryCode) {
$params['course_categories'] = $categoryCode;
}
$course = CourseManager::create_course($params);
if ($course) {
return new JsonResponse([
'success' => true,
'message' => $translator->trans('Course created successfully.'),
'courseId' => $course->getId(),
]);
}
}
return new JsonResponse(['success' => false, 'message' => $translator->trans('An error occurred while creating the course.')]);
}
private function autoLaunch(): void private function autoLaunch(): void
{ {
$autoLaunchWarning = ''; $autoLaunchWarning = '';

@ -32,7 +32,7 @@ class CourseCategoryRepository extends ServiceEntityRepository
* *
* @return CourseCategory[] * @return CourseCategory[]
*/ */
public function findAllInAccessUrl(int $accessUrl, bool $allowBaseCategories = false, int $parentId = 0) public function findAllInAccessUrl(int $accessUrl, bool $allowBaseCategories = false, int $parentId = 0): array
{ {
$qb = $this->createQueryBuilder('c'); $qb = $this->createQueryBuilder('c');
$qb $qb

@ -93,8 +93,7 @@ class CourseRepository extends ResourceRepository
{ {
$qb = $this->getEntityManager()->createQueryBuilder(); $qb = $this->getEntityManager()->createQueryBuilder();
$qb $qb->select('DISTINCT c.id, c.title, c.code')
->select('DISTINCT c.id')
->from(Course::class, 'c') ->from(Course::class, 'c')
->innerJoin(CourseRelUser::class, 'courseRelUser') ->innerJoin(CourseRelUser::class, 'courseRelUser')
->innerJoin('c.urls', 'accessUrlRelCourse') ->innerJoin('c.urls', 'accessUrlRelCourse')
@ -105,18 +104,19 @@ class CourseRepository extends ResourceRepository
'user' => $user, 'user' => $user,
'url' => $url, 'url' => $url,
'status' => $status, 'status' => $status,
]) ]);
;
if (!empty($keyword)) { if (!empty($keyword)) {
$qb $qb->andWhere($qb->expr()->orX(
->andWhere('c.title like = :keyword OR c.code like = :keyword') $qb->expr()->like('c.title', ':keyword'),
->setParameter('keyword', $keyword) $qb->expr()->like('c.code', ':keyword')
; ))
->setParameter('keyword', '%'.$keyword.'%');
} }
$query = $qb->getQuery(); $query = $qb->getQuery();
return $query->getResult(); return $query->getArrayResult();
} }
/** /**

Loading…
Cancel
Save