From fe1aa3dd4019c654bfe8a35a8681d0b9070d2468 Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Sat, 17 Jan 2026 23:04:31 +0530
Subject: [PATCH] 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 {}