From fe1aa3dd4019c654bfe8a35a8681d0b9070d2468 Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Sat, 17 Jan 2026 23:04:31 +0530
Subject: [PATCH 1/3] feat: configurable frontend base path
Co-authored-by: Suraj Shetty
---
.gitignore | 1 +
frontend/package.json | 2 +-
frontend/src/components/AssessmentPlugin.vue | 8 +++-
frontend/src/pages/Batch.vue | 5 ++-
frontend/src/pages/Billing.vue | 15 ++++---
frontend/src/pages/Lesson.vue | 5 ++-
frontend/src/pages/ProfileAbout.vue | 5 ++-
.../ProgrammingExerciseSubmission.vue | 6 ++-
frontend/src/router.js | 3 +-
frontend/src/utils/assignment.js | 6 ++-
frontend/src/utils/basePath.js | 12 ++++++
frontend/src/utils/program.ts | 8 +++-
frontend/src/utils/quiz.js | 4 +-
frontend/vite.config.js | 2 +-
lms/hooks.py | 31 +++++++++-----
lms/lms/api.py | 3 +-
.../lms_assignment_submission.py | 4 +-
lms/lms/doctype/lms_batch/lms_batch.js | 3 +-
lms/lms/doctype/lms_batch/lms_batch.py | 5 ++-
lms/lms/doctype/lms_course/lms_course.js | 6 ++-
lms/lms/doctype/lms_course/lms_course.py | 14 +++++--
.../lms_mentor_request/lms_mentor_request.py | 4 +-
lms/lms/doctype/lms_payment/lms_payment.py | 6 ++-
lms/lms/page/lms_home/lms_home.js | 3 +-
lms/lms/test_utils.py | 3 +-
lms/lms/user.py | 3 +-
lms/lms/utils.py | 24 +++++++++--
.../course_cards/course_cards.html | 2 +-
.../recently_published_courses.html | 4 +-
lms/lms/widgets/Avatar.html | 2 +-
lms/lms/widgets/CourseCard.html | 4 +-
.../emails/assignment_submission.html | 3 +-
lms/templates/emails/batch_confirmation.html | 3 +-
.../emails/batch_start_reminder.html | 2 +-
lms/templates/emails/live_class_reminder.html | 4 +-
lms/www/{lms.py => _lms.py} | 40 ++++++++++---------
36 files changed, 177 insertions(+), 78 deletions(-)
create mode 100644 frontend/src/utils/basePath.js
rename lms/www/{lms.py => _lms.py} (88%)
diff --git a/.gitignore b/.gitignore
index 95242ce7..e6c74964 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,4 +12,5 @@ node_modules
package-lock.json
lms/public/frontend
lms/www/lms.html
+lms/www/_lms.html
frappe-ui
\ No newline at end of file
diff --git a/frontend/package.json b/frontend/package.json
index bead43a6..9bd26413 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -7,7 +7,7 @@
"dev": "vite",
"serve": "vite preview",
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry && yarn copy-colors-json",
- "copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html",
+ "copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/_lms.html",
"copy-colors-json": "cp node_modules/frappe-ui/tailwind/colors.json src/utils/frappe-ui-colors.json"
},
"dependencies": {
diff --git a/frontend/src/components/AssessmentPlugin.vue b/frontend/src/components/AssessmentPlugin.vue
index 75e747c5..53b51392 100644
--- a/frontend/src/components/AssessmentPlugin.vue
+++ b/frontend/src/components/AssessmentPlugin.vue
@@ -65,6 +65,7 @@ import { Dialog, FormControl } from 'frappe-ui'
import { nextTick, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { Link } from 'frappe-ui/frappe'
+import { getLmsRoute } from '@/utils/basePath'
const show = ref(false)
const quiz = ref(null)
@@ -94,7 +95,10 @@ const addAssessment = () => {
}
const redirectToForm = () => {
- if (props.type == 'quiz') window.open('/lms/quizzes?new=true', '_blank')
- else window.open('/lms/assignments?new=true', '_blank')
+ if (props.type == 'quiz') {
+ window.open(getLmsRoute('quizzes?new=true'), '_blank')
+ } else {
+ window.open(getLmsRoute('assignments?new=true'), '_blank')
+ }
}
diff --git a/frontend/src/pages/Batch.vue b/frontend/src/pages/Batch.vue
index ad5c47bb..3bd57614 100644
--- a/frontend/src/pages/Batch.vue
+++ b/frontend/src/pages/Batch.vue
@@ -248,6 +248,7 @@ import DateRange from '@/components/Common/DateRange.vue'
import BulkCertificates from '@/components/Modals/BulkCertificates.vue'
import BatchFeedback from '@/components/BatchFeedback.vue'
import dayjs from 'dayjs/esm'
+import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const showAnnouncementModal = ref(false)
@@ -357,7 +358,9 @@ const isStudent = computed(() => {
})
const redirectToLogin = () => {
- window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
+ window.location.href = `/login?redirect-to=${getLmsRoute(
+ `batches/${props.batchName}`
+ )}`
}
const openAnnouncementModal = () => {
diff --git a/frontend/src/pages/Billing.vue b/frontend/src/pages/Billing.vue
index 00a98a94..02f9ce11 100644
--- a/frontend/src/pages/Billing.vue
+++ b/frontend/src/pages/Billing.vue
@@ -207,14 +207,18 @@
:text="access.data.message"
:buttonLabel="type == 'course' ? 'Checkout Course' : 'Checkout Batch'"
:buttonLink="
- type == 'course' ? `/lms/courses/${name}` : `/lms/batches/${name}`
+ type == 'course'
+ ? getLmsRoute(`courses/${name}`)
+ : getLmsRoute(`batches/${name}`)
"
/>
@@ -235,6 +239,7 @@ import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
import { X } from 'lucide-vue-next'
import { useTelemetry } from 'frappe-ui/frappe'
+import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const { brand } = sessionStore()
@@ -441,11 +446,11 @@ const changeCurrency = (country) => {
const redirectTo = computed(() => {
if (props.type == 'course') {
- return `/lms/courses/${props.name}`
+ return getLmsRoute(`courses/${props.name}`)
} else if (props.type == 'batch') {
- return `/lms/batches/${props.name}`
+ return getLmsRoute(`batches/${props.name}`)
} else if (props.type == 'certificate') {
- return `/lms/courses/${props.name}/certification`
+ return getLmsRoute(`courses/${props.name}/certification`)
}
})
diff --git a/frontend/src/pages/Lesson.vue b/frontend/src/pages/Lesson.vue
index d503b35c..1c658774 100644
--- a/frontend/src/pages/Lesson.vue
+++ b/frontend/src/pages/Lesson.vue
@@ -378,6 +378,7 @@ import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue'
import Notes from '@/components/Notes/Notes.vue'
import InlineLessonMenu from '@/components/Notes/InlineLessonMenu.vue'
+import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const socket = inject('$socket')
@@ -902,7 +903,9 @@ watch(allowDiscussions, () => {
})
const redirectToLogin = () => {
- window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
+ window.location.href = `/login?redirect-to=${getLmsRoute(
+ `courses/${props.courseName}`
+ )}`
}
usePageMeta(() => {
diff --git a/frontend/src/pages/ProfileAbout.vue b/frontend/src/pages/ProfileAbout.vue
index f6497645..d2fb068c 100644
--- a/frontend/src/pages/ProfileAbout.vue
+++ b/frontend/src/pages/ProfileAbout.vue
@@ -122,6 +122,7 @@ import { X, LinkedinIcon, Twitter } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { decodeEntities } from '@/utils'
import DOMPurify from 'dompurify'
+import { getLmsRoute } from '@/utils/basePath'
const dayjs = inject('$dayjs')
const { branding } = sessionStore()
@@ -158,7 +159,9 @@ const badges = createResource({
const shareOnSocial = (badge, medium) => {
let shareUrl
const url = encodeURIComponent(
- `${window.location.origin}/lms/badges/${badge.badge}/${props.profile.data?.email}`
+ `${window.location.origin}${getLmsRoute(
+ `badges/${badge.badge}/${props.profile.data?.email}`
+ )}`
)
const summary = `I am happy to announce that I earned the ${
badge.badge
diff --git a/frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue b/frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue
index d11adcea..71f9563c 100644
--- a/frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue
+++ b/frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue
@@ -158,6 +158,7 @@ import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router'
import { openSettings } from '@/utils'
import { useSettings } from '@/stores/settings'
+import { getLmsRoute } from '@/utils/basePath'
const user = inject('$user')
const code = ref('')
@@ -255,7 +256,10 @@ const updateBoilerPlate = () => {
const checkIfUserIsPermitted = (doc: any = null) => {
if (!user.data) {
- window.location.href = `/login?redirect-to=/lms/programming-exercises/${props.exerciseID}/submission/${props.submissionID}`
+ const redirectPath = getLmsRoute(
+ `programming-exercises/${props.exerciseID}/submission/${props.submissionID}`
+ )
+ window.location.href = `/login?redirect-to=${redirectPath}`
}
if (!doc) return
diff --git a/frontend/src/router.js b/frontend/src/router.js
index fe6b311d..8894f3b9 100644
--- a/frontend/src/router.js
+++ b/frontend/src/router.js
@@ -2,6 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import { usersStore } from './stores/user'
import { sessionStore } from './stores/session'
import { useSettings } from './stores/settings'
+import { getLmsBasePath } from './utils/basePath'
const routes = [
{
@@ -268,7 +269,7 @@ const routes = [
]
let router = createRouter({
- history: createWebHistory('/lms'),
+ history: createWebHistory(`/${getLmsBasePath()}`),
routes,
})
diff --git a/frontend/src/utils/assignment.js b/frontend/src/utils/assignment.js
index 99f9b101..1f1e894d 100644
--- a/frontend/src/utils/assignment.js
+++ b/frontend/src/utils/assignment.js
@@ -5,6 +5,7 @@ import translationPlugin from '../translation'
import { usersStore } from '@/stores/user'
import { call } from 'frappe-ui'
import router from '@/router'
+import { getLmsRoute } from '@/utils/basePath'
export class Assignment {
constructor({ data, api, readOnly }) {
@@ -53,7 +54,10 @@ export class Assignment {
fieldname: ['name'],
}).then((data) => {
let submission = data.name || 'new'
- this.wrapper.innerHTML = ``
+ const submissionPath = getLmsRoute(
+ `assignment-submission/${assignment}/${submission}?fromLesson=1`
+ )
+ this.wrapper.innerHTML = ``
})
return
}
diff --git a/frontend/src/utils/basePath.js b/frontend/src/utils/basePath.js
new file mode 100644
index 00000000..78782a9b
--- /dev/null
+++ b/frontend/src/utils/basePath.js
@@ -0,0 +1,12 @@
+export function getLmsBasePath() {
+ return window.lms_path || 'lms'
+}
+
+export function getLmsRoute(path = '') {
+ const base = getLmsBasePath()
+ if (!path) {
+ return base
+ }
+ const normalized = path.startsWith('/') ? path.slice(1) : path
+ return `/${base}/${normalized}`
+}
diff --git a/frontend/src/utils/program.ts b/frontend/src/utils/program.ts
index a2d9b548..22cc48eb 100644
--- a/frontend/src/utils/program.ts
+++ b/frontend/src/utils/program.ts
@@ -4,6 +4,7 @@ import translationPlugin from '@/translation'
import ProgrammingExerciseModal from '@/pages/ProgrammingExercises/ProgrammingExerciseModal.vue';
import { call } from 'frappe-ui';
import { usersStore } from '@/stores/user'
+import { getLmsRoute } from '@/utils/basePath'
export class Program {
@@ -73,7 +74,10 @@ export class Program {
fieldname: ['name'],
}).then((data: { name: string }) => {
let submission = data.name || 'new'
- this.wrapper.innerHTML = ``
+ const submissionPath = getLmsRoute(
+ `programming-exercises/${exercise}/submission/${submission}?fromLesson=1`
+ )
+ this.wrapper.innerHTML = ``
})
return
}
@@ -100,4 +104,4 @@ export class Program {
exercise: this.data.exercise,
}
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/utils/quiz.js b/frontend/src/utils/quiz.js
index add3d680..b84f5286 100644
--- a/frontend/src/utils/quiz.js
+++ b/frontend/src/utils/quiz.js
@@ -5,6 +5,7 @@ import { usersStore } from '../stores/user'
import translationPlugin from '../translation'
import { CircleHelp } from 'lucide-vue-next'
import router from '@/router'
+import { getLmsRoute } from '@/utils/basePath'
export class Quiz {
constructor({ data, api, readOnly }) {
@@ -42,7 +43,8 @@ export class Quiz {
renderQuiz(quiz) {
if (this.readOnly) {
- this.wrapper.innerHTML = ``
+ const quizPath = getLmsRoute(`quiz/${quiz}?fromLesson=1`)
+ this.wrapper.innerHTML = ``
return
}
this.wrapper.innerHTML = `
-
+
{{ _("Explore More") }}
diff --git a/lms/lms/web_template/recently_published_courses/recently_published_courses.html b/lms/lms/web_template/recently_published_courses/recently_published_courses.html
index 448ef3d5..ce2fdcb6 100644
--- a/lms/lms/web_template/recently_published_courses/recently_published_courses.html
+++ b/lms/lms/web_template/recently_published_courses/recently_published_courses.html
@@ -12,7 +12,7 @@
{% endfor %}
-
+
{{ _("Explore More") }}
-
\ No newline at end of file
+
diff --git a/lms/lms/widgets/Avatar.html b/lms/lms/widgets/Avatar.html
index ab21f7ba..bfb71327 100644
--- a/lms/lms/widgets/Avatar.html
+++ b/lms/lms/widgets/Avatar.html
@@ -1,6 +1,6 @@
{% set color = get_palette(member.full_name) %}
-
+
{% if member.user_image %}
diff --git a/lms/lms/widgets/CourseCard.html b/lms/lms/widgets/CourseCard.html
index de74f2c0..0c541ad2 100644
--- a/lms/lms/widgets/CourseCard.html
+++ b/lms/lms/widgets/CourseCard.html
@@ -93,7 +93,7 @@
{% endif %}
{% endfor %}
-
+
{% if ins_len == 1 %}
{{ instructors[0].full_name }}
@@ -128,7 +128,7 @@
{% else %}
-
+
{% endif %}
{% endif %}
diff --git a/lms/templates/emails/assignment_submission.html b/lms/templates/emails/assignment_submission.html
index d3942aaa..96ff3d5b 100644
--- a/lms/templates/emails/assignment_submission.html
+++ b/lms/templates/emails/assignment_submission.html
@@ -4,7 +4,6 @@
{{ _(" Please evaluate and grade it.") }}
`
-
+
{{ _("Open Assignment") }}
-
diff --git a/lms/templates/emails/batch_confirmation.html b/lms/templates/emails/batch_confirmation.html
index d4cc26df..5176e5db 100644
--- a/lms/templates/emails/batch_confirmation.html
+++ b/lms/templates/emails/batch_confirmation.html
@@ -23,7 +23,7 @@
{{ _("Visit the following link to view your ") }}
- {{ _("Batch Details") }}
+ {{ _("Batch Details") }}
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
@@ -32,4 +32,3 @@
{{ _("Best Regards") }}
-
diff --git a/lms/templates/emails/batch_start_reminder.html b/lms/templates/emails/batch_start_reminder.html
index de240de6..95443e6a 100644
--- a/lms/templates/emails/batch_start_reminder.html
+++ b/lms/templates/emails/batch_start_reminder.html
@@ -20,7 +20,7 @@
- 👉 {{ _("Visit your batch") }}
+ 👉 {{ _("Visit your batch") }}
diff --git a/lms/templates/emails/live_class_reminder.html b/lms/templates/emails/live_class_reminder.html
index cd1db5ef..20865d7a 100644
--- a/lms/templates/emails/live_class_reminder.html
+++ b/lms/templates/emails/live_class_reminder.html
@@ -17,7 +17,7 @@
- 👉 {{ _("Visit your batch") }}
+ 👉 {{ _("Visit your batch") }}
@@ -26,4 +26,4 @@
{{ _("Best Regards") }}
-
\ No newline at end of file
+
diff --git a/lms/www/lms.py b/lms/www/_lms.py
similarity index 88%
rename from lms/www/lms.py
rename to lms/www/_lms.py
index fcda12cd..10a9b796 100644
--- a/lms/www/lms.py
+++ b/lms/www/_lms.py
@@ -5,6 +5,9 @@ from bs4 import BeautifulSoup
from frappe import _
from frappe.utils.telemetry import capture
+from lms.hooks import lms_path
+from lms.lms.utils import get_lms_route
+
no_cache = 1
@@ -32,6 +35,7 @@ def get_boot():
"read_only_mode": frappe.flags.read_only,
"csrf_token": frappe.sessions.get_csrf_token(),
"site_name": frappe.local.site,
+ "lms_path": lms_path,
}
)
@@ -85,7 +89,7 @@ def get_meta_from_document(app_path):
return {
"title": _("Course List"),
"keywords": "All Courses, Courses, Learn",
- "link": "/courses",
+ "link": get_lms_route("courses"),
}
if re.match(r"^courses/.*$", app_path):
@@ -94,7 +98,7 @@ def get_meta_from_document(app_path):
"title": _("New Course"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"keywords": "New Course, Create Course",
- "link": "/lms/courses/new/edit",
+ "link": get_lms_route("courses/new/edit"),
}
course_name = app_path.split("/")[1]
course = frappe.db.get_value(
@@ -113,14 +117,14 @@ def get_meta_from_document(app_path):
"image": course.image,
"description": course.description,
"keywords": course.tags,
- "link": f"/courses/{course_name}",
+ "link": get_lms_route(f"courses/{course_name}"),
}
if app_path == "batches":
return {
"title": _("Batches"),
"keywords": "All Batches, Batches, Learn",
- "link": "/batches",
+ "link": get_lms_route("batches"),
}
if re.match(r"^batches/details/.*$", app_path):
batch_name = app_path.split("/")[2]
@@ -140,7 +144,7 @@ def get_meta_from_document(app_path):
"image": batch.meta_image,
"description": batch.batch_details,
"keywords": f"{batch.category} {batch.medium}",
- "link": f"/batches/details/{batch_name}",
+ "link": get_lms_route(f"batches/details/{batch_name}"),
}
if re.match(r"^batches/.*$", app_path):
@@ -149,7 +153,7 @@ def get_meta_from_document(app_path):
return {
"title": _("New Batch"),
"keywords": "New Batch, Create Batch",
- "link": "/lms/batches/new/edit",
+ "link": get_lms_route("batches/new/edit"),
}
batch = frappe.db.get_value(
"LMS Batch",
@@ -167,14 +171,14 @@ def get_meta_from_document(app_path):
"image": batch.meta_image,
"description": batch.batch_details,
"keywords": f"{batch.category} {batch.medium}",
- "link": f"/batches/{batch_name}",
+ "link": get_lms_route(f"batches/{batch_name}"),
}
if app_path == "job-openings":
return {
"title": _("Job Openings"),
"keywords": "Job Openings, Jobs, Vacancies",
- "link": "/job-openings",
+ "link": get_lms_route("job-openings"),
}
if re.match(r"^job-openings/.*$", app_path):
@@ -195,14 +199,14 @@ def get_meta_from_document(app_path):
"image": job_opening.company_logo,
"description": job_opening.description,
"keywords": "Job Openings, Jobs, Vacancies",
- "link": f"/job-openings/{job_opening_name}",
+ "link": get_lms_route(f"job-openings/{job_opening_name}"),
}
if app_path == "statistics":
return {
"title": _("Statistics"),
"keywords": "Enrollment Count, Completion, Signups",
- "link": "/statistics",
+ "link": get_lms_route("statistics"),
}
if re.match(r"^user/.*$", app_path):
@@ -225,7 +229,7 @@ def get_meta_from_document(app_path):
"image": user.user_image,
"description": user.bio,
"keywords": f"{user.full_name}, {user.bio}",
- "link": f"/user/{username}",
+ "link": get_lms_route(f"user/{username}"),
}
if re.match(r"^badges/.*/.*$", app_path):
@@ -242,14 +246,14 @@ def get_meta_from_document(app_path):
"image": badge.image,
"description": badge.description,
"keywords": f"{badge.title}, {badge.description}",
- "link": f"/badges/{badgeName}/{email}",
+ "link": get_lms_route(f"badges/{badgeName}/{email}"),
}
if app_path == "quizzes":
return {
"title": _("Quizzes"),
"keywords": "Quizzes, interactive quizzes, online quizzes",
- "link": "/quizzes",
+ "link": get_lms_route("quizzes"),
}
if re.match(r"^quizzes/[^/]+$", app_path):
@@ -264,14 +268,14 @@ def get_meta_from_document(app_path):
return {
"title": quiz.title,
"keywords": quiz.title,
- "link": f"/quizzes/{quiz_name}",
+ "link": get_lms_route(f"quizzes/{quiz_name}"),
}
if app_path == "assignments":
return {
"title": _("Assignments"),
"keywords": "Assignments, interactive assignments, online assignments",
- "link": "/assignments",
+ "link": get_lms_route("assignments"),
}
if re.match(r"^assignments/[^/]+$", app_path):
@@ -286,21 +290,21 @@ def get_meta_from_document(app_path):
return {
"title": assignment.title,
"keywords": assignment.title,
- "link": f"/assignments/{assignment_name}",
+ "link": get_lms_route(f"assignments/{assignment_name}"),
}
if app_path == "programs":
return {
"title": _("Programs"),
"keywords": "All Programs, Programs, Learn",
- "link": "/programs",
+ "link": get_lms_route("programs"),
}
if app_path == "certified-participants":
return {
"title": _("Certified Participants"),
"keywords": "All Certified Participants, Certified Participants, Learn, Certification",
- "link": "/certified-participants",
+ "link": get_lms_route("certified-participants"),
}
return {}
From 3b88892905a4930b1263c9f201838a5958e5e16e Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Sat, 17 Jan 2026 23:14:33 +0530
Subject: [PATCH 2/3] refactor: change global variable to function in hooks
---
lms/hooks.py | 30 +++++++++++++++++-------------
lms/lms/api.py | 4 ++--
lms/lms/test_utils.py | 4 ++--
lms/lms/user.py | 5 ++---
lms/www/_lms.py | 5 ++---
5 files changed, 25 insertions(+), 23 deletions(-)
diff --git a/lms/hooks.py b/lms/hooks.py
index 8a0fd55e..ce08b4bd 100644
--- a/lms/hooks.py
+++ b/lms/hooks.py
@@ -12,8 +12,12 @@ app_color = "grey"
app_email = "jannat@frappe.io"
app_license = "AGPL"
-lms_path = frappe.conf.get("lms_path") or "lms"
-app_icon_route = f"/{lms_path}"
+
+def get_lms_path():
+ return (frappe.conf.get("lms_path") or "lms").strip("/")
+
+
+app_icon_route = f"/{get_lms_path()}"
# Includes in
# ------------------
@@ -167,8 +171,8 @@ override_whitelisted_methods = {
# Add all simple route rules here
website_route_rules = [
- {"from_route": f"/{lms_path}/", "to_route": "_lms"},
- {"from_route": f"/{lms_path}", "to_route": "_lms"},
+ {"from_route": f"/{get_lms_path()}/", "to_route": "_lms"},
+ {"from_route": f"/{get_lms_path()}", "to_route": "_lms"},
{
"from_route": "/courses//",
"to_route": "certificate",
@@ -177,25 +181,25 @@ website_route_rules = [
website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"},
- {"source": "/courses", "target": f"/{lms_path}/courses"},
+ {"source": "/courses", "target": f"/{get_lms_path()}/courses"},
{
"source": r"^/courses/.*$",
- "target": f"/{lms_path}/courses",
+ "target": f"/{get_lms_path()}/courses",
},
- {"source": "/batches", "target": f"/{lms_path}/batches"},
+ {"source": "/batches", "target": f"/{get_lms_path()}/batches"},
{
"source": r"/batches/(.*)",
- "target": f"/{lms_path}/batches",
+ "target": f"/{get_lms_path()}/batches",
"match_with_query_string": True,
},
- {"source": "/job-openings", "target": f"/{lms_path}/job-openings"},
+ {"source": "/job-openings", "target": f"/{get_lms_path()}/job-openings"},
{
"source": r"/job-openings/(.*)",
- "target": f"/{lms_path}/job-openings",
+ "target": f"/{get_lms_path()}/job-openings",
"match_with_query_string": True,
},
- {"source": "/statistics", "target": f"/{lms_path}/statistics"},
- {"source": "_lms", "target": f"/{lms_path}"},
+ {"source": "/statistics", "target": f"/{get_lms_path()}/statistics"},
+ {"source": "_lms", "target": f"/{get_lms_path()}"},
]
update_website_context = [
@@ -267,7 +271,7 @@ add_to_apps_screen = [
"name": "lms",
"logo": "/assets/lms/frontend/learning.svg",
"title": "Learning",
- "route": f"/{lms_path}",
+ "route": f"/{get_lms_path()}",
"has_permission": "lms.lms.api.check_app_permission",
}
]
diff --git a/lms/lms/api.py b/lms/lms/api.py
index 731c0dde..faee30ce 100644
--- a/lms/lms/api.py
+++ b/lms/lms/api.py
@@ -28,7 +28,6 @@ from frappe.utils import (
)
from frappe.utils.response import Response
-from lms.hooks import lms_path
from lms.lms.doctype.course_lesson.course_lesson import save_progress
from lms.lms.utils import (
get_average_rating,
@@ -36,6 +35,7 @@ from lms.lms.utils import (
get_course_details,
get_instructors,
get_lesson_count,
+ get_lms_route,
)
@@ -1674,7 +1674,7 @@ def get_pwa_manifest():
"name": title,
"short_name": title,
"description": "Easy to use, 100% open source Learning Management System",
- "start_url": f"/{lms_path}",
+ "start_url": get_lms_route(),
"icons": [
{
"src": banner_image or "/assets/lms/frontend/manifest/manifest-icon-192.maskable.png",
diff --git a/lms/lms/test_utils.py b/lms/lms/test_utils.py
index cbfe8640..c786a8b7 100644
--- a/lms/lms/test_utils.py
+++ b/lms/lms/test_utils.py
@@ -2,7 +2,6 @@ import frappe
from frappe.tests import UnitTestCase
from frappe.utils import add_days, nowdate
-from lms.hooks import lms_path
from lms.lms.api import get_certified_participants
from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template, is_certified
@@ -14,6 +13,7 @@ from .utils import (
get_lesson_index,
get_lesson_url,
get_lessons,
+ get_lms_route,
get_membership,
get_reviews,
get_tags,
@@ -236,7 +236,7 @@ class TestUtils(UnitTestCase):
def test_get_lesson_url(self):
lessons = get_lessons(self.course.name)
for lesson in lessons:
- expected_url = f"/{lms_path}/courses/{self.course.name}/learn/{lesson.number}"
+ expected_url = get_lms_route(f"courses/{self.course.name}/learn/{lesson.number}")
self.assertEqual(get_lesson_url(self.course.name, lesson.number), expected_url)
def test_is_instructor(self):
diff --git a/lms/lms/user.py b/lms/lms/user.py
index d89455ce..d6535977 100644
--- a/lms/lms/user.py
+++ b/lms/lms/user.py
@@ -4,8 +4,7 @@ from frappe.model.naming import append_number_if_name_exists
from frappe.utils import escape_html, random_string
from frappe.website.utils import cleanup_page_name, is_signup_disabled
-from lms.hooks import lms_path
-from lms.lms.utils import get_country_code
+from lms.lms.utils import get_country_code, get_lms_route
def validate_username_duplicates(doc, method):
@@ -89,4 +88,4 @@ def set_country_from_ip(login_manager=None, user=None):
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"] = f"/{lms_path}"
+ frappe.local.response["home_page"] = get_lms_route()
diff --git a/lms/www/_lms.py b/lms/www/_lms.py
index 10a9b796..1aca0b9f 100644
--- a/lms/www/_lms.py
+++ b/lms/www/_lms.py
@@ -5,8 +5,7 @@ from bs4 import BeautifulSoup
from frappe import _
from frappe.utils.telemetry import capture
-from lms.hooks import lms_path
-from lms.lms.utils import get_lms_route
+from lms.lms.utils import get_lms_path, get_lms_route
no_cache = 1
@@ -35,7 +34,7 @@ def get_boot():
"read_only_mode": frappe.flags.read_only,
"csrf_token": frappe.sessions.get_csrf_token(),
"site_name": frappe.local.site,
- "lms_path": lms_path,
+ "lms_path": get_lms_path(),
}
)
From 5ce8e8c4ffc9e0dbdd5de113e2fe402e27e0d58b Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Sat, 17 Jan 2026 23:22:38 +0530
Subject: [PATCH 3/3] test: flaky evaluation schedule
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Earlier logic was flaky because the test calculated the last expected date using a 56‑day window, while the production code builds
schedules for 60 days. Those extra 4 days sometimes include another Monday or Wednesday, so the schedule ends later than the test
expects.
---
.../course_evaluator/test_course_evaluator.py | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/lms/lms/doctype/course_evaluator/test_course_evaluator.py b/lms/lms/doctype/course_evaluator/test_course_evaluator.py
index 605bb553..764fdec5 100644
--- a/lms/lms/doctype/course_evaluator/test_course_evaluator.py
+++ b/lms/lms/doctype/course_evaluator/test_course_evaluator.py
@@ -5,7 +5,10 @@
from frappe.tests import UnitTestCase
from frappe.utils import add_days, format_time, getdate
-from lms.lms.doctype.course_evaluator.course_evaluator import get_schedule
+from lms.lms.doctype.course_evaluator.course_evaluator import (
+ get_schedule,
+ get_schedule_range_end_date,
+)
from lms.lms.test_utils import TestUtils
@@ -52,17 +55,9 @@ class TestCourseEvaluator(UnitTestCase):
return first_date
def calculated_last_date_of_schedule(self, first_date):
- last_day = add_days(getdate(), 56)
- offset_monday = (0 - last_day.weekday() + 7) % 7 # 0 for Monday
- offset_wednesday = (2 - last_day.weekday() + 7) % 7 # 2 for Wednesday
-
- if offset_monday < offset_wednesday and offset_monday <= 4:
- last_day = add_days(last_day, offset_monday)
- elif offset_wednesday <= 4:
- last_day = add_days(last_day, offset_wednesday)
- else:
- last_day = add_days(last_day, min(offset_monday, offset_wednesday) + 7)
-
+ last_day = getdate(get_schedule_range_end_date(getdate(), self.batch.name))
+ while last_day.weekday() not in (0, 2):
+ last_day = add_days(last_day, -1)
return last_day
def test_unavailability_dates(self):