diff --git a/frontend/src/components/CourseCardOverlay.vue b/frontend/src/components/CourseCardOverlay.vue index b1d5ed89..fb09e74d 100644 --- a/frontend/src/components/CourseCardOverlay.vue +++ b/frontend/src/components/CourseCardOverlay.vue @@ -57,16 +57,21 @@ {{ __('Start Learning') }} - + +
{{ __('This course has:') }}
@@ -154,4 +159,6 @@ function enrollStudent() { }) } } + +const is_instructor = () => {} diff --git a/frontend/src/components/CourseOutline.vue b/frontend/src/components/CourseOutline.vue index 294cbf0e..e9c6e55a 100644 --- a/frontend/src/components/CourseOutline.vue +++ b/frontend/src/components/CourseOutline.vue @@ -1,6 +1,9 @@ @@ -129,89 +201,196 @@ import { TextEditor, Button, createResource, + createDocumentResource, FormControl, FileUploader, } from 'frappe-ui' -import { reactive, inject, onMounted } from 'vue' +import { inject, onMounted, computed, ref } from 'vue' import { convertToTitleCase, createToast, getFileSize } from '../utils' import Link from '@/components/Controls/Link.vue' -import { FileText } from 'lucide-vue-next' +import { FileText, X } from 'lucide-vue-next' const user = inject('$user') +const tags = ref('') +const newTag = ref('') +const image = ref(null) + +const props = defineProps({ + courseName: { + type: String, + }, +}) + +const breadcrumbs = computed(() => { + let crumbs = [ + { + label: 'Courses', + route: { name: 'Courses' }, + }, + ] + if (courseResource.doc) { + crumbs.push({ + label: courseResource.doc?.title, + route: { name: 'CourseDetail', params: { courseName: props.courseName } }, + }) + } + crumbs.push({ + label: props.courseName == 'new' ? 'New Course' : 'Edit Course', + route: { name: 'CreateCourse', params: { courseName: props.courseName } }, + }) + return crumbs +}) + +const courseResource = createDocumentResource({ + doctype: 'LMS Course', + name: props.courseName, + auto: false, + onSuccess(data) { + imageResource.reload({ image: data.image }) + tags.value = data.tags + }, +}) + +const imageResource = createResource({ + url: 'lms.lms.api.get_file_info', + makeParams(values) { + return { + file_url: values.image, + } + }, + auto: false, + onSuccess(data) { + image.value = data + }, +}) onMounted(() => { if (!user.data?.is_moderator || !user.data?.is_instructor) { window.location.href = '/login' } + if (props.courseName !== 'new') { + courseResource.reload() + } }) -const course = reactive({ - title: '', - short_introduction: '', - description: '', - video_link: '', - tags: '', - published: false, - upcoming: false, - image: null, - paid_course: false, - course_price: null, - currency: '', +const course = computed(() => { + return { + title: courseResource.doc?.title || '', + short_introduction: courseResource.doc?.short_introduction || '', + description: courseResource.doc?.description || '', + video_link: courseResource.doc?.video_link || '', + course_image: courseResource.doc?.image || null, + tags: tags.value, + published: courseResource.doc?.published ? true : false, + upcoming: courseResource.doc?.upcoming ? true : false, + disable_self_learning: courseResource.doc?.disable_self_learning + ? true + : false, + course_image: image.value, + paid_course: courseResource.doc?.paid_course ? true : false, + course_price: courseResource.doc?.course_price || '', + currency: courseResource.doc?.currency || '', + } }) -const courseResource = createResource({ +const courseCreationResource = createResource({ url: 'frappe.client.insert', - makeParams() { + makeParams(values) { return { doc: { doctype: 'LMS Course', - ...course, + image: image.value.file_url, + ...values, }, } }, }) const submitCourse = () => { - courseResource.submit( - {}, - { + if (courseResource.doc) { + courseResource.setValue.submit( + { + image: image.value?.file_url || null, + ...course.value, + }, + { + validate() { + return validateMandatoryFields() + }, + onError(err) { + showToast(err) + }, + } + ) + } else { + courseCreationResource.submit(course.value, { validate() { - const mandatory_fields = [ - 'title', - 'short_introduction', - 'description', - 'video_link', - 'image', - ] - for (const field of mandatory_fields) { - if (!course[field]) { - let fieldLabel = convertToTitleCase(field.split('_').join(' ')) - return `${fieldLabel} is mandatory` - } - } - if (course.paid_course && (!course.course_price || !course.currency)) { - return 'Course price and currency are mandatory for paid courses' - } + return validateMandatoryFields() }, onError(err) { - createToast({ - title: 'Error', - text: err.messages?.[0] || err, - icon: 'x', - iconClasses: 'bg-red-600 text-white rounded-md p-px', - position: 'top-center', - timeout: 10, - }) + showToast(err) }, + }) + } +} + +const validateMandatoryFields = () => { + const mandatory_fields = [ + 'title', + 'short_introduction', + 'description', + 'video_link', + 'course_image', + ] + for (const field of mandatory_fields) { + if (!course.value[field]) { + let fieldLabel = convertToTitleCase(field.split('_').join(' ')) + return `${fieldLabel} is mandatory` } - ) + } + if ( + course.value.paid_course && + (!course.value.course_price || !course.value.currency) + ) { + return 'Course price and currency are mandatory for paid courses' + } } const validateFile = (file) => { - console.log(file) let extension = file.name.split('.').pop().toLowerCase() if (!['jpg', 'jpeg', 'png'].includes(extension)) { return 'Only image file is allowed.' } } + +const updateTags = () => { + if (newTag.value) { + tags.value = tags.value ? `${tags.value}, ${newTag.value}` : newTag.value + newTag.value = '' + } +} + +const removeTag = (tag) => { + tags.value = tags.value + ?.split(', ') + .filter((t) => t !== tag) + .join(', ') + newTag.value = '' +} + +const showToast = (err) => { + createToast({ + title: 'Error', + text: err.messages?.[0] || err, + icon: 'x', + iconClasses: 'bg-red-600 text-white rounded-md p-px', + position: 'top-center', + timeout: 10, + }) +} + +const removeImage = () => { + image.value = null + course.value.course_image = null +} diff --git a/lms/lms/api.py b/lms/lms/api.py index 719ec43c..7d417467 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -267,3 +267,12 @@ def get_chart_details(): ) details.lesson_completions = frappe.db.count("LMS Course Progress") return details + + +@frappe.whitelist() +def get_file_info(file_url): + """Get file info for the given file URL.""" + file_info = frappe.db.get_value( + "File", {"file_url": file_url}, ["file_name", "file_size", "file_url"], as_dict=1 + ) + return file_info diff --git a/lms/patches.txt b/lms/patches.txt index e0b2caf6..338fa7df 100644 --- a/lms/patches.txt +++ b/lms/patches.txt @@ -82,5 +82,5 @@ lms.patches.v1_0.create_batch_source [post_model_sync] lms.patches.v1_0.batch_tabs_settings execute:frappe.delete_doc("Notification", "Assignment Submission Notification") -lms.patches.v1_0.change_jobs_url #17-01-2024 +lms.patches.v1_0.change_jobs_url #19-01-2024 lms.patches.v1_0.custom_perm_for_discussions #14-01-2024 \ No newline at end of file