-
-
-
-
- {{ __('Course Description') }}
-
-
(course.description = val)"
- :editable="true"
- :fixedMenu="true"
- editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
- />
-
-
-
{
- console.log(file)
- course.image = file
- console.log(course.image)
- }
- "
- >
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+ {{ __('Course Details') }}
+
+
+
+
+
+ {{ __('Course Description') }}
+
+
(course.description = val)"
+ :editable="true"
+ :fixedMenu="true"
+ editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
+ />
+
+
{
+ image = file
+ }
+ "
+ >
+
+
+
+
+
+
+
+
+ {{ __('Course Image') }}
+
+
+
+
+
+
+
+ {{ image.file_name }}
+
+
+ {{ getFileSize(image.file_size) }}
+
+
+
+
+
+
+
-
-
+
+
+ {{ __('Course Settings') }}
+
+
+
+
+
+
+
+
+
+ {{ __('Course Pricing') }}
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+ {{ course.title }}
+
+
+ {{ courseResource.chapters }}
+
+
+
+ {{
+ __(
+ 'There are no chapters in this course. Create and manage chapters from here.'
+ )
+ }}
+
+
+
+
+
@@ -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