From 0692aceda406e0fef28f6a7c8a85afc952cfb7cd Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 19 Mar 2025 10:45:47 +0530 Subject: [PATCH 1/6] fix: don't allow billing page access if batch is sold out --- frontend/src/pages/Billing.vue | 2 -- lms/lms/api.py | 6 ++++++ lms/lms/doctype/lms_payment/lms_payment.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/Billing.vue b/frontend/src/pages/Billing.vue index ca74fbdd..7c4242d6 100644 --- a/frontend/src/pages/Billing.vue +++ b/frontend/src/pages/Billing.vue @@ -245,12 +245,10 @@ const paymentLink = createResource({ }) const generatePaymentLink = () => { - console.log('called') paymentLink.submit( {}, { validate() { - console.log('validation start') if (!billingDetails.source) { return __('Please let us know where you heard about us from.') } diff --git a/lms/lms/api.py b/lms/lms/api.py index 119fb845..83af8015 100644 --- a/lms/lms/api.py +++ b/lms/lms/api.py @@ -229,6 +229,12 @@ def validate_billing_access(billing_type, name): access = False message = _("You are already enrolled for this batch.") + seat_count = frappe.get_cached_value("LMS Batch", name, "seat_count") + number_of_students = frappe.db.count("LMS Batch Enrollment", {"batch": name}) + if seat_count <= number_of_students: + access = False + message = _("Batch is sold out.") + elif access and billing_type == "certificate": purchased_certificate = frappe.db.exists( "LMS Enrollment", diff --git a/lms/lms/doctype/lms_payment/lms_payment.py b/lms/lms/doctype/lms_payment/lms_payment.py index 1378f4e3..3d28c7f0 100644 --- a/lms/lms/doctype/lms_payment/lms_payment.py +++ b/lms/lms/doctype/lms_payment/lms_payment.py @@ -35,6 +35,9 @@ def send_payment_reminder(): for payment in incomplete_payments: if has_paid_later(payment): continue + + if is_batch_sold_out(payment): + continue send_mail(payment) @@ -51,6 +54,16 @@ def has_paid_later(payment): ) +def is_batch_sold_out(payment): + if payment.payment_for_document_type == "LMS Batch": + seat_count = frappe.get_cached_value("LMS Batch", payment.payment_for_document, "seat_count") + number_of_students = frappe.db.count("LMS Batch Enrollment", {"batch": payment.payment_for_document}) + + if seat_count <= number_of_students: + return True + + return False + def send_mail(payment): subject = _("Complete Your Enrollment - Don't miss out!") template = "payment_reminder" From c6e658e26b24ef859bf67801103abba9c2c48678 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 19 Mar 2025 11:04:29 +0530 Subject: [PATCH 2/6] fix: show tabs and featured courses on list for guest users --- frontend/src/pages/Courses.vue | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/frontend/src/pages/Courses.vue b/frontend/src/pages/Courses.vue index c43ad88a..b55b5d60 100644 --- a/frontend/src/pages/Courses.vue +++ b/frontend/src/pages/Courses.vue @@ -29,7 +29,6 @@ class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4" > @@ -199,10 +198,6 @@ const updateCertificationFilter = () => { } const updateTabFilter = () => { - if (!user.data) { - return - } - delete filters.value['live'] delete filters.value['created'] delete filters.value['published_on'] @@ -295,7 +290,7 @@ const courseTabs = computed(() => { ] if (user.data?.is_student) { tabs.push({ label: __('Enrolled') }) - } else { + } else if (user.data) { tabs.push({ label: __('Created') }) } return tabs From 04cbd6a1d8096015585fff54bf8eb1d4d830fcae Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Wed, 19 Mar 2025 22:26:58 +0530 Subject: [PATCH 3/6] chore: use vite plugins from frappe-ui --- frontend/components.d.ts | 95 ++ frontend/package.json | 3 +- frontend/src/components/AppSidebar.vue | 3 +- frontend/src/components/Assignment.vue | 12 +- frontend/src/components/AssignmentBlock.vue | 4 +- .../src/components/CertificationLinks.vue | 4 +- frontend/src/components/CourseCardOverlay.vue | 2 +- frontend/src/utils/assignment.js | 4 + frontend/src/utils/code.ts | 22 +- frontend/src/utils/index.js | 9 - frontend/vite.config.js | 30 +- frontend/yarn.lock | 626 ++++++-- lms/install.py | 32 - .../doctype/lms_category/lms_category.json | 22 +- setup.py | 6 +- yarn.lock | 1354 ----------------- 16 files changed, 619 insertions(+), 1609 deletions(-) create mode 100644 frontend/components.d.ts delete mode 100644 yarn.lock diff --git a/frontend/components.d.ts b/frontend/components.d.ts new file mode 100644 index 00000000..bd14c7fb --- /dev/null +++ b/frontend/components.d.ts @@ -0,0 +1,95 @@ +/* eslint-disable */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +// biome-ignore lint: disable +export {} + +/* prettier-ignore */ +declare module 'vue' { + export interface GlobalComponents { + Annoucements: typeof import('./src/components/Annoucements.vue')['default'] + AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default'] + Apps: typeof import('./src/components/Apps.vue')['default'] + AppSidebar: typeof import('./src/components/AppSidebar.vue')['default'] + AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default'] + AssessmentPlugin: typeof import('./src/components/AssessmentPlugin.vue')['default'] + Assessments: typeof import('./src/components/Assessments.vue')['default'] + Assignment: typeof import('./src/components/Assignment.vue')['default'] + AssignmentBlock: typeof import('./src/components/AssignmentBlock.vue')['default'] + AudioBlock: typeof import('./src/components/AudioBlock.vue')['default'] + Autocomplete: typeof import('./src/components/Controls/Autocomplete.vue')['default'] + BatchCard: typeof import('./src/components/BatchCard.vue')['default'] + BatchCourseModal: typeof import('./src/components/Modals/BatchCourseModal.vue')['default'] + BatchCourses: typeof import('./src/components/BatchCourses.vue')['default'] + BatchDashboard: typeof import('./src/components/BatchDashboard.vue')['default'] + BatchFeedback: typeof import('./src/components/BatchFeedback.vue')['default'] + BatchIcon: typeof import('./src/components/Icons/BatchIcon.vue')['default'] + BatchOverlay: typeof import('./src/components/BatchOverlay.vue')['default'] + BatchStudentProgress: typeof import('./src/components/Modals/BatchStudentProgress.vue')['default'] + BatchStudents: typeof import('./src/components/BatchStudents.vue')['default'] + BrandSettings: typeof import('./src/components/BrandSettings.vue')['default'] + BulkCertificates: typeof import('./src/components/Modals/BulkCertificates.vue')['default'] + Categories: typeof import('./src/components/Categories.vue')['default'] + CertificationLinks: typeof import('./src/components/CertificationLinks.vue')['default'] + ChapterModal: typeof import('./src/components/Modals/ChapterModal.vue')['default'] + CodeEditor: typeof import('./src/components/Controls/CodeEditor.vue')['default'] + CollapseSidebar: typeof import('./src/components/Icons/CollapseSidebar.vue')['default'] + CourseCard: typeof import('./src/components/CourseCard.vue')['default'] + CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default'] + CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default'] + CourseOutline: typeof import('./src/components/CourseOutline.vue')['default'] + CourseReviews: typeof import('./src/components/CourseReviews.vue')['default'] + CreateOutline: typeof import('./src/components/CreateOutline.vue')['default'] + DateRange: typeof import('./src/components/Common/DateRange.vue')['default'] + DesktopLayout: typeof import('./src/components/DesktopLayout.vue')['default'] + DiscussionModal: typeof import('./src/components/Modals/DiscussionModal.vue')['default'] + DiscussionReplies: typeof import('./src/components/DiscussionReplies.vue')['default'] + Discussions: typeof import('./src/components/Discussions.vue')['default'] + EditCoverImage: typeof import('./src/components/Modals/EditCoverImage.vue')['default'] + EditProfile: typeof import('./src/components/Modals/EditProfile.vue')['default'] + EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default'] + Event: typeof import('./src/components/Modals/Event.vue')['default'] + ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default'] + FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default'] + IconPicker: typeof import('./src/components/Controls/IconPicker.vue')['default'] + IndicatorIcon: typeof import('./src/components/Icons/IndicatorIcon.vue')['default'] + JobApplicationModal: typeof import('./src/components/Modals/JobApplicationModal.vue')['default'] + JobCard: typeof import('./src/components/JobCard.vue')['default'] + LessonContent: typeof import('./src/components/LessonContent.vue')['default'] + LessonHelp: typeof import('./src/components/LessonHelp.vue')['default'] + Link: typeof import('./src/components/Controls/Link.vue')['default'] + LiveClass: typeof import('./src/components/LiveClass.vue')['default'] + LiveClassModal: typeof import('./src/components/Modals/LiveClassModal.vue')['default'] + LMSLogo: typeof import('./src/components/Icons/LMSLogo.vue')['default'] + Members: typeof import('./src/components/Members.vue')['default'] + MobileLayout: typeof import('./src/components/MobileLayout.vue')['default'] + MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default'] + NoPermission: typeof import('./src/components/NoPermission.vue')['default'] + NotPermitted: typeof import('./src/components/NotPermitted.vue')['default'] + OnboardingBanner: typeof import('./src/components/OnboardingBanner.vue')['default'] + PageModal: typeof import('./src/components/Modals/PageModal.vue')['default'] + PaymentSettings: typeof import('./src/components/PaymentSettings.vue')['default'] + ProgressBar: typeof import('./src/components/ProgressBar.vue')['default'] + Question: typeof import('./src/components/Modals/Question.vue')['default'] + Quiz: typeof import('./src/components/Quiz.vue')['default'] + QuizBlock: typeof import('./src/components/QuizBlock.vue')['default'] + Rating: typeof import('./src/components/Controls/Rating.vue')['default'] + ReviewModal: typeof import('./src/components/Modals/ReviewModal.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SettingDetails: typeof import('./src/components/SettingDetails.vue')['default'] + SettingFields: typeof import('./src/components/SettingFields.vue')['default'] + Settings: typeof import('./src/components/Modals/Settings.vue')['default'] + SidebarLink: typeof import('./src/components/SidebarLink.vue')['default'] + StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default'] + StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default'] + Tags: typeof import('./src/components/Tags.vue')['default'] + UnsplashImageBrowser: typeof import('./src/components/UnsplashImageBrowser.vue')['default'] + UpcomingEvaluations: typeof import('./src/components/UpcomingEvaluations.vue')['default'] + UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default'] + UserAvatar: typeof import('./src/components/UserAvatar.vue')['default'] + UserDropdown: typeof import('./src/components/UserDropdown.vue')['default'] + VideoBlock: typeof import('./src/components/VideoBlock.vue')['default'] + } +} diff --git a/frontend/package.json b/frontend/package.json index b0695d69..57752299 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,7 +26,8 @@ "codemirror-editor-vue3": "^2.8.0", "dayjs": "^1.11.6", "feather-icons": "^4.28.0", - "frappe-ui": "^0.1.112", + "frappe-ui": "^0.1.118", + "highlight.js": "^11.11.1", "lucide-vue-next": "^0.383.0", "markdown-it": "^14.0.0", "pinia": "^2.0.33", diff --git a/frontend/src/components/AppSidebar.vue b/frontend/src/components/AppSidebar.vue index e6ac5c0c..485dbe3b 100644 --- a/frontend/src/components/AppSidebar.vue +++ b/frontend/src/components/AppSidebar.vue @@ -109,7 +109,8 @@ import { sessionStore } from '@/stores/session' import { useSidebar } from '@/stores/sidebar' import { useSettings } from '@/stores/settings' import { ChevronRight, Plus } from 'lucide-vue-next' -import { Button, createResource, TrialBanner } from 'frappe-ui' +import { Button, createResource } from 'frappe-ui' +import { TrialBanner } from "frappe-ui/frappe" import PageModal from '@/components/Modals/PageModal.vue' const { user, sidebarSettings } = sessionStore() diff --git a/frontend/src/components/Assignment.vue b/frontend/src/components/Assignment.vue index 9056fb30..4003b4d5 100644 --- a/frontend/src/components/Assignment.vue +++ b/frontend/src/components/Assignment.vue @@ -1,7 +1,7 @@ diff --git a/frontend/src/pages/Batches.vue b/frontend/src/pages/Batches.vue index c7db7079..5a5b9d9f 100644 --- a/frontend/src/pages/Batches.vue +++ b/frontend/src/pages/Batches.vue @@ -72,7 +72,7 @@
diff --git a/lms/hooks.py b/lms/hooks.py index 3fde3094..d26b2b3c 100644 --- a/lms/hooks.py +++ b/lms/hooks.py @@ -242,6 +242,8 @@ profile_url_prefix = "/users/" signup_form_template = "lms.plugins.show_custom_signup" +on_login = "lms.lms.user.on_login" + add_to_apps_screen = [ { "name": "lms", diff --git a/lms/lms/user.py b/lms/lms/user.py index ade66c82..a223beeb 100644 --- a/lms/lms/user.py +++ b/lms/lms/user.py @@ -83,3 +83,8 @@ def set_country_from_ip(login_manager=None, user=None): # return frappe.db.set_value("User", user, "country", get_country_code()) return + +def on_login(login_manager): + default_app = frappe.db.get_single_value("System Settings", "default_app") + if default_app == "lms": + frappe.local.response["home_page"] = "/lms" \ No newline at end of file From edde95edeb21bd00d162ba67703e95b0ab650d73 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 20 Mar 2025 12:22:13 +0530 Subject: [PATCH 6/6] chore: fixed linters --- lms/lms/user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lms/lms/user.py b/lms/lms/user.py index a223beeb..64aaeafe 100644 --- a/lms/lms/user.py +++ b/lms/lms/user.py @@ -84,7 +84,8 @@ def set_country_from_ip(login_manager=None, user=None): frappe.db.set_value("User", user, "country", get_country_code()) return + def on_login(login_manager): default_app = frappe.db.get_single_value("System Settings", "default_app") if default_app == "lms": - frappe.local.response["home_page"] = "/lms" \ No newline at end of file + frappe.local.response["home_page"] = "/lms"