diff --git a/frontend/src/components/Controls/Uploader.vue b/frontend/src/components/Controls/Uploader.vue index 73643892..8c83ff4c 100644 --- a/frontend/src/components/Controls/Uploader.vue +++ b/frontend/src/components/Controls/Uploader.vue @@ -70,7 +70,7 @@ const props = withDefaults( modelValue: string label?: string description?: string - type: 'image' | 'video' + type?: 'image' | 'video' required?: boolean }>(), { diff --git a/frontend/src/components/CourseCardOverlay.vue b/frontend/src/components/CourseCardOverlay.vue index b01e5fc2..2978ef61 100644 --- a/frontend/src/components/CourseCardOverlay.vue +++ b/frontend/src/components/CourseCardOverlay.vue @@ -285,9 +285,6 @@ const certificate = createResource({ }&format=${encodeURIComponent(data.template)}`, '_blank' ) - capture('certificate_issued', { - course: props.course.data.name, - }) }, }) diff --git a/frontend/src/components/Settings/PaymentGatewayDetails.vue b/frontend/src/components/Settings/PaymentGatewayDetails.vue index 2af7d6d4..c6392140 100644 --- a/frontend/src/components/Settings/PaymentGatewayDetails.vue +++ b/frontend/src/components/Settings/PaymentGatewayDetails.vue @@ -52,14 +52,12 @@ import { } from 'frappe-ui' import { computed, ref, watch } from 'vue' import SettingFields from '@/components/Settings/SettingFields.vue' -import { useTelemetry } from 'frappe-ui/frappe' const show = defineModel({ required: true, default: false }) const paymentGateways = defineModel('paymentGateways') const newGateway = ref(null) const newGatewayFields = ref([]) const newGatewayData = ref>({}) -const { capture } = useTelemetry() const props = defineProps<{ gatewayID: string | null @@ -160,7 +158,6 @@ const saveNewGateway = (close: () => void) => { ...newGatewayData.value, }, }).then((data: any) => { - capture('payment_gateway_configured') paymentGateways.value.reload() close() }) diff --git a/frontend/src/components/UpcomingEvaluations.vue b/frontend/src/components/UpcomingEvaluations.vue index edc5551c..6bb0e8ec 100644 --- a/frontend/src/components/UpcomingEvaluations.vue +++ b/frontend/src/components/UpcomingEvaluations.vue @@ -4,15 +4,30 @@
{{ __('Upcoming Evaluations') }}
- +
+ {{ __('The last day to schedule your evaluations is ') }} + + {{ dayjs(endDate).format('DD MMMM YYYY') }} . + {{ __('Please make sure to schedule your evaluation before this date.') }} +
+
+ {{ + __( + 'The deadline to schedule evaluations has passed. Please contact the Instructor for assistance.' + ) + }} +
-
+
{{ __('Schedule an evaluation to get certified.') }}
@@ -173,6 +188,18 @@ const evaluationCourses = computed(() => { }) }) +const canScheduleEvals = computed(() => { + return ( + upcoming_evals.data?.length != evaluationCourses.length && + !props.forHome && + !endDateHasPassed.value + ) +}) + +const endDateHasPassed = computed(() => { + return dayjs().isSameOrAfter(dayjs(props.endDate)) +}) + const cancelEvaluation = (evl) => { $dialog({ title: __('Cancel this evaluation?'), diff --git a/frontend/src/pages/Batch.vue b/frontend/src/pages/Batch.vue index bf3b1135..ad5c47bb 100644 --- a/frontend/src/pages/Batch.vue +++ b/frontend/src/pages/Batch.vue @@ -144,20 +144,7 @@ -
- {{ __('The last day to schedule your evaluations is ') }} - - {{ - dayjs(batch.data.evaluation_end_date).format('DD MMMM YYYY') - }} . - {{ - __('Please make sure to schedule your evaluation before this date.') - }} -
+
{{ __('Feedback') }} diff --git a/frontend/src/pages/BatchForm.vue b/frontend/src/pages/BatchForm.vue index 8338cb45..099e9daa 100644 --- a/frontend/src/pages/BatchForm.vue +++ b/frontend/src/pages/BatchForm.vue @@ -281,21 +281,13 @@ import { import { Breadcrumbs, FormControl, - FileUploader, Button, TextEditor, createResource, usePageMeta, toast, call, - Toast, } from 'frappe-ui' -import { useRouter } from 'vue-router' -import { Image, Trash2 } from 'lucide-vue-next' -import { useOnboarding, useTelemetry } from 'frappe-ui/frappe' -import { sessionStore } from '../stores/session' -import MultiSelect from '@/components/Controls/MultiSelect.vue' -import Link from '@/components/Controls/Link.vue' import { escapeHTML, getMetaInfo, @@ -303,7 +295,13 @@ import { sanitizeHTML, updateMetaInfo, } from '@/utils' +import { useRouter } from 'vue-router' +import { Trash2 } from 'lucide-vue-next' +import { useOnboarding, useTelemetry } from 'frappe-ui/frappe' +import { sessionStore } from '../stores/session' import Uploader from '@/components/Controls/Uploader.vue' +import MultiSelect from '@/components/Controls/MultiSelect.vue' +import Link from '@/components/Controls/Link.vue' const router = useRouter() const user = inject('$user') @@ -466,6 +464,7 @@ const validateFields = () => { !['description', 'batch_details'].includes(key) && typeof batch[key] === 'string' ) { + console.log(key) batch[key] = escapeHTML(batch[key]) } }) diff --git a/frontend/src/pages/CourseForm.vue b/frontend/src/pages/CourseForm.vue index 9a62a2de..65a91ec4 100644 --- a/frontend/src/pages/CourseForm.vue +++ b/frontend/src/pages/CourseForm.vue @@ -75,58 +75,11 @@
-
-
- {{ __('Course Image') }} -
- - - -
-
- -
- -
- {{ - __('Appears on the course card in the course list') - }} -
-
-
-
-
+ ({ instructor: instructor, })), @@ -469,7 +421,7 @@ const courseEditResource = createResource({ doctype: 'LMS Course', name: values.course, fieldname: { - image: course.course_image?.file_url || '', + image: course.image, instructors: instructors.value.map((instructor) => ({ instructor: instructor, })), @@ -519,24 +471,10 @@ const courseResource = createResource({ course[key] = course[key] ? true : false } - if (data.image) imageResource.reload({ image: data.image }) check_permission() }, }) -const imageResource = createResource({ - url: 'lms.lms.api.get_file_info', - makeParams(values) { - return { - file_url: values.image, - } - }, - auto: false, - onSuccess(data) { - course.course_image = data - }, -}) - const validateFields = () => { course.description = sanitizeHTML(course.description) @@ -580,16 +518,12 @@ const createCourse = () => { } const editCourse = () => { - let was_published = courseResource.data.published courseEditResource.submit( { course: courseResource.data.name, }, { onSuccess() { - if (!was_published && course.published) { - capture('publish_course') - } updateMetaInfo('courses', props.courseName, meta) toast.success(__('Course updated successfully')) }, @@ -657,14 +591,6 @@ const removeTag = (tag) => { newTag.value = '' } -const saveImage = (file) => { - course.course_image = file -} - -const removeImage = () => { - course.course_image = null -} - const check_permission = () => { let user_is_instructor = false if (user.data?.is_moderator) return diff --git a/frontend/src/pages/JobForm.vue b/frontend/src/pages/JobForm.vue index c81d5c62..3b850371 100644 --- a/frontend/src/pages/JobForm.vue +++ b/frontend/src/pages/JobForm.vue @@ -83,47 +83,11 @@ class="mb-4" :required="true" /> - - - - -
-
-
- -
-
- - {{ job.image.file_name }} - - - {{ getFileSize(job.image.file_size) }} - -
- -
-
+
@@ -150,15 +114,14 @@ import { createResource, Button, TextEditor, - FileUploader, usePageMeta, toast, } from 'frappe-ui' import { computed, onMounted, reactive, inject } from 'vue' -import { FileText, X } from 'lucide-vue-next' import { sessionStore } from '@/stores/session' import { useRouter } from 'vue-router' -import { escapeHTML, getFileSize, sanitizeHTML, validateFile } from '@/utils' +import { escapeHTML, sanitizeHTML } from '@/utils' +import Uploader from '@/components/Controls/Uploader.vue' const user = inject('$user') const router = useRouter() @@ -177,7 +140,7 @@ const newJob = createResource({ return { doc: { doctype: 'Job Opportunity', - company_logo: job.image?.file_url, + company_logo: job.company_logo, ...job, }, } @@ -191,7 +154,7 @@ const updateJob = createResource({ doctype: 'Job Opportunity', name: props.jobName, fieldname: { - company_logo: job.image.file_url, + company_logo: job.company_logo, ...job, }, } @@ -215,20 +178,6 @@ const jobDetail = createResource({ Object.keys(data).forEach((key) => { if (Object.hasOwn(job, key)) job[key] = data[key] }) - if (data.company_logo) imageResource.reload({ image: data.company_logo }) - }, -}) - -const imageResource = createResource({ - url: 'lms.lms.api.get_file_info', - makeParams(values) { - return { - file_url: values.image, - } - }, - auto: false, - onSuccess(data) { - job.image = data }, }) @@ -241,7 +190,7 @@ const job = reactive({ status: 'Open', company_name: '', company_website: '', - image: null, + company_logo: null, description: '', company_email_address: '', }) @@ -322,14 +271,6 @@ const validateJobFields = () => { }) } -const saveImage = (file) => { - job.image = file -} - -const removeImage = () => { - job.image = null -} - const jobTypes = computed(() => { return [ { label: 'Full Time', value: 'Full Time' }, diff --git a/frontend/src/pages/LessonForm.vue b/frontend/src/pages/LessonForm.vue index 1a79db57..41aaa5eb 100644 --- a/frontend/src/pages/LessonForm.vue +++ b/frontend/src/pages/LessonForm.vue @@ -665,6 +665,15 @@ iframe { padding: 8px; } +.codex-editor--narrow .ce-toolbox .ce-popover { + right: unset; + left: initial; +} + +.ce-popover { + border-radius: 12px; +} + .cdx-search-field { border: none; } diff --git a/frontend/src/pages/QuizForm.vue b/frontend/src/pages/QuizForm.vue index e43e06f8..dc8d753f 100644 --- a/frontend/src/pages/QuizForm.vue +++ b/frontend/src/pages/QuizForm.vue @@ -233,7 +233,6 @@ import { ClipboardList, ListChecks, Plus, Trash2 } from 'lucide-vue-next' import { useRouter } from 'vue-router' import { escapeHTML } from '@/utils' import Question from '@/components/Modals/Question.vue' -import { useTelemetry } from 'frappe-ui/frappe' const { brand } = sessionStore() const showQuestionModal = ref(false) @@ -242,7 +241,6 @@ const currentQuestion = reactive({ marks: 0, name: '', }) -const { capture } = useTelemetry() const user = inject('$user') const router = useRouter() const readOnlyMode = window.read_only_mode @@ -260,9 +258,7 @@ onMounted(() => { if (!user.data?.is_moderator && !user.data?.is_instructor) { router.push({ name: 'Courses' }) } - if (props.quizID !== 'new') { - quizDetails.reload() - } + quizDetails.reload() window.addEventListener('keydown', keyboardShortcut) }) @@ -310,9 +306,6 @@ const submitQuiz = () => { }, { onSuccess(data) { - if (props.quizID === 'new') { - capture('quiz_created') - } quizDetails.doc.total_marks = data.total_marks toast.success(__('Quiz updated successfully')) }, @@ -408,7 +401,7 @@ const breadcrumbs = computed(() => { ] crumbs.push({ - label: props.quizID == 'new' ? __('New Quiz') : quizDetails.doc?.title, + label: quizDetails.doc?.title, route: { name: 'QuizForm', params: { quizID: props.quizID } }, }) return crumbs @@ -416,7 +409,7 @@ const breadcrumbs = computed(() => { usePageMeta(() => { return { - title: props.quizID == 'new' ? __('New Quiz') : quizDetails.doc?.title, + title: quizDetails.doc?.title, icon: brand.favicon, } }) diff --git a/frontend/yarn.lock b/frontend/yarn.lock index c3ca947a..c6f02e93 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1896,9 +1896,9 @@ integrity sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg== "@types/node@*": - version "25.0.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.7.tgz#afd2f0d34358631a287c6726dabe0c845abc2bae" - integrity sha512-C/er7DlIZgRJO7WtTdYovjIFzGsz0I95UlMyR9anTb4aCpBSRWe5Jc1/RvLKUfzmOxHPGjSE5+63HgLtndxU4w== + version "25.0.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-25.0.8.tgz#e54e00f94fe1db2497b3e42d292b8376a2678c8d" + integrity sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg== dependencies: undici-types "~7.16.0" diff --git a/lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json b/lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json index c9f3e793..a2f05643 100644 --- a/lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json +++ b/lms/lms/doctype/lms_batch_enrollment/lms_batch_enrollment.json @@ -73,7 +73,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2025-12-04 12:53:38.246250", + "modified": "2026-01-14 08:53:16.672825", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Batch Enrollment", @@ -108,6 +108,18 @@ "if_owner": 1, "read": 1, "role": "LMS Student" + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Batch Evaluator", + "share": 1, + "write": 1 } ], "row_format": "Dynamic", diff --git a/lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.json b/lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.json index 89e99c33..683a8876 100644 --- a/lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.json +++ b/lms/lms/doctype/lms_batch_feedback/lms_batch_feedback.json @@ -76,7 +76,7 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-05-21 15:58:51.667270", + "modified": "2026-01-14 08:53:38.088168", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Batch Feedback", @@ -105,6 +105,30 @@ "role": "LMS Student", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Moderator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Batch Evaluator", + "share": 1, + "write": 1 } ], "row_format": "Dynamic", diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.json b/lms/lms/doctype/lms_live_class/lms_live_class.json index e734da4a..d71727ed 100644 --- a/lms/lms/doctype/lms_live_class/lms_live_class.json +++ b/lms/lms/doctype/lms_live_class/lms_live_class.json @@ -177,7 +177,7 @@ "link_fieldname": "live_class" } ], - "modified": "2025-05-27 14:44:35.679712", + "modified": "2026-01-14 08:54:07.684781", "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Live Class", @@ -215,6 +215,18 @@ "report": 1, "role": "LMS Student", "share": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Batch Evaluator", + "share": 1, + "write": 1 } ], "row_format": "Dynamic", diff --git a/lms/lms/doctype/lms_live_class/lms_live_class.py b/lms/lms/doctype/lms_live_class/lms_live_class.py index f54372fc..dc6dfa2f 100644 --- a/lms/lms/doctype/lms_live_class/lms_live_class.py +++ b/lms/lms/doctype/lms_live_class/lms_live_class.py @@ -155,11 +155,17 @@ def create_attendance(live_class, data): doc = frappe.new_doc("LMS Live Class Participant") doc.live_class = live_class.name doc.member = participant.get("user_email") - doc.joined_at = participant.get("join_time") - doc.left_at = participant.get("leave_time") - doc.duration = participant.get("duration") + doc.joined_at = get_datetime(participant.get("join_time")) + doc.left_at = get_datetime(participant.get("leave_time")) + doc.duration = get_minutes(participant.get("duration")) doc.insert() def update_attendees_count(live_class, data): frappe.db.set_value("LMS Live Class", live_class.name, "attendees", len(data)) + + +def get_minutes(duration_in_seconds): + if duration_in_seconds: + return int(duration_in_seconds) // 60 + return 0 diff --git a/lms/lms/doctype/lms_live_class_participant/lms_live_class_participant.json b/lms/lms/doctype/lms_live_class_participant/lms_live_class_participant.json index 7dff5dfd..f1117c12 100644 --- a/lms/lms/doctype/lms_live_class_participant/lms_live_class_participant.json +++ b/lms/lms/doctype/lms_live_class_participant/lms_live_class_participant.json @@ -89,8 +89,8 @@ "grid_page_length": 50, "index_web_pages_for_search": 1, "links": [], - "modified": "2025-05-27 22:32:24.196643", - "modified_by": "Administrator", + "modified": "2026-01-14 08:54:21.421321", + "modified_by": "sayali@frappe.io", "module": "LMS", "name": "LMS Live Class Participant", "owner": "Administrator", @@ -106,6 +106,30 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Moderator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Batch Evaluator", + "share": 1, + "write": 1 } ], "row_format": "Dynamic", diff --git a/lms/lms/utils.py b/lms/lms/utils.py index e6608211..9bad9eee 100644 --- a/lms/lms/utils.py +++ b/lms/lms/utils.py @@ -1293,7 +1293,7 @@ def get_assignment_details(assessment, member): assessment.edit_url = f"/assignments/{assessment.assessment_name}" submission_name = existing_submission if existing_submission else "new-submission" - assessment.url = f"/assignment-submission/{assessment.assessment_name}/{submission_name}" + assessment.url = f"/lms/assignment-submission/{assessment.assessment_name}/{submission_name}" return assessment diff --git a/pyproject.toml b/pyproject.toml index 34a916e2..1463f1bc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,4 +66,4 @@ indent-style = "tab" docstring-code-format = true [tool.bench.frappe-dependencies] -frappe = ">=15.0.0,<17.0.0" \ No newline at end of file +frappe = ">=15.0.0,<=17.0.0-dev" \ No newline at end of file