feat: configurable frontend base path

Co-authored-by: Suraj Shetty <surajshetty3416@users.noreply.github.com>
This commit is contained in:
Hussain Nagaria
2026-01-17 23:04:31 +05:30
parent 376de99ef7
commit fe1aa3dd40
36 changed files with 177 additions and 78 deletions

View File

@@ -1,3 +1,5 @@
import frappe
from . import __version__ as app_version
app_name = "frappe_lms"
@@ -6,11 +8,13 @@ app_publisher = "Frappe"
app_description = "Frappe LMS App"
app_icon_url = "/assets/lms/images/lms-logo.png"
app_icon_title = "Learning"
app_icon_route = "/lms"
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}"
# Includes in <head>
# ------------------
@@ -163,7 +167,8 @@ override_whitelisted_methods = {
# Add all simple route rules here
website_route_rules = [
{"from_route": "/lms/<path:app_path>", "to_route": "lms"},
{"from_route": f"/{lms_path}/<path:app_path>", "to_route": "_lms"},
{"from_route": f"/{lms_path}", "to_route": "_lms"},
{
"from_route": "/courses/<course_name>/<certificate_id>",
"to_route": "certificate",
@@ -172,24 +177,25 @@ website_route_rules = [
website_redirects = [
{"source": "/update-profile", "target": "/edit-profile"},
{"source": "/courses", "target": "/lms/courses"},
{"source": "/courses", "target": f"/{lms_path}/courses"},
{
"source": r"^/courses/.*$",
"target": "/lms/courses",
"target": f"/{lms_path}/courses",
},
{"source": "/batches", "target": "/lms/batches"},
{"source": "/batches", "target": f"/{lms_path}/batches"},
{
"source": r"/batches/(.*)",
"target": "/lms/batches",
"target": f"/{lms_path}/batches",
"match_with_query_string": True,
},
{"source": "/job-openings", "target": "/lms/job-openings"},
{"source": "/job-openings", "target": f"/{lms_path}/job-openings"},
{
"source": r"/job-openings/(.*)",
"target": "/lms/job-openings",
"target": f"/{lms_path}/job-openings",
"match_with_query_string": True,
},
{"source": "/statistics", "target": "/lms/statistics"},
{"source": "/statistics", "target": f"/{lms_path}/statistics"},
{"source": "_lms", "target": f"/{lms_path}"},
]
update_website_context = [
@@ -203,11 +209,16 @@ jinja = {
"lms.lms.utils.get_instructors",
"lms.lms.utils.get_lesson_index",
"lms.lms.utils.get_lesson_url",
"lms.lms.utils.get_lms_route",
"lms.lms.utils.is_instructor",
"lms.lms.utils.get_palette",
],
"filters": [],
}
extend_bootinfo = [
"lms.lms.utils.extend_bootinfo",
]
## Specify the additional tabs to be included in the user profile page.
## Each entry must be a subclass of lms.lms.plugins.ProfileTab
# profile_tabs = []
@@ -256,7 +267,7 @@ add_to_apps_screen = [
"name": "lms",
"logo": "/assets/lms/frontend/learning.svg",
"title": "Learning",
"route": "/lms",
"route": f"/{lms_path}",
"has_permission": "lms.lms.api.check_app_permission",
}
]

View File

@@ -28,6 +28,7 @@ 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,
@@ -1673,7 +1674,7 @@ def get_pwa_manifest():
"name": title,
"short_name": title,
"description": "Easy to use, 100% open source Learning Management System",
"start_url": "/lms",
"start_url": f"/{lms_path}",
"icons": [
{
"src": banner_image or "/assets/lms/frontend/manifest/manifest-icon-192.maskable.png",

View File

@@ -7,6 +7,8 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
from frappe.model.document import Document
from frappe.utils import validate_url
from lms.lms.utils import get_lms_route
class LMSAssignmentSubmission(Document):
def validate(self):
@@ -72,7 +74,7 @@ class LMSAssignmentSubmission(Document):
"document_name": self.name,
"from_user": self.evaluator,
"type": "Alert",
"link": f"/lms/assignment-submission/{self.assignment}/{self.name}",
"link": get_lms_route(f"assignment-submission/{self.assignment}/{self.name}"),
}
)
make_notification_logs(notification, [self.member])

View File

@@ -48,8 +48,9 @@ frappe.ui.form.on("LMS Batch", {
},
refresh: (frm) => {
const lmsPath = frappe.boot.lms_path || "lms";
frm.add_web_link(
`/lms/batches/details/${frm.doc.name}`,
`/${lmsPath}/batches/details/${frm.doc.name}`,
"See on website"
);
},

View File

@@ -18,6 +18,7 @@ from lms.lms.utils import (
get_instructors,
get_lesson_index,
get_lesson_url,
get_lms_route,
get_quiz_details,
update_payment_record,
)
@@ -164,7 +165,7 @@ def send_email_notification_for_published_batch(batch):
"medium": batch.medium,
"timezone": batch.timezone,
"instructors": instructors,
"batch_url": f"{frappe.utils.get_url()}/lms/batches/details/{batch.name}",
"batch_url": frappe.utils.get_url(get_lms_route(f"batches/details/{batch.name}")),
}
frappe.sendmail(
@@ -193,7 +194,7 @@ def send_system_notification_for_published_batch(batch):
"document_name": batch.name,
"from_user": instructors[0] if instructors else None,
"type": "Alert",
"link": f"/lms/batches/details/{batch.name}",
"link": get_lms_route(f"batches/details/{batch.name}"),
}
)
make_notification_logs(notification, students)

View File

@@ -20,7 +20,11 @@ frappe.ui.form.on("LMS Course", {
});
},
refresh: (frm) => {
frm.add_web_link(`/lms/courses/${frm.doc.name}`, "See on Website");
const lmsPath = frappe.boot.lms_path || "lms";
frm.add_web_link(
`/${lmsPath}/courses/${frm.doc.name}`,
"See on Website"
);
if (!frm.doc.currency)
frappe.db

View File

@@ -9,7 +9,13 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
from frappe.model.document import Document
from frappe.utils import cint, today
from ...utils import generate_slug, get_instructors, update_payment_record, validate_image
from ...utils import (
generate_slug,
get_instructors,
get_lms_route,
update_payment_record,
validate_image,
)
class LMSCourse(Document):
@@ -107,7 +113,7 @@ class LMSCourse(Document):
subject = self.title + " is available!"
args = {
"title": self.title,
"course_link": f"/lms/courses/{self.name}",
"course_link": get_lms_route(f"courses/{self.name}"),
"app_name": frappe.db.get_single_value("System Settings", "app_name"),
"site_url": frappe.utils.get_url(),
}
@@ -172,7 +178,7 @@ def send_email_notification_for_published_courses(courses):
"title": course.title,
"short_introduction": course.short_introduction,
"instructors": instructors,
"course_url": f"{frappe.utils.get_url()}/lms/courses/{course.name}",
"course_url": frappe.utils.get_url(get_lms_route(f"courses/{course.name}")),
}
frappe.sendmail(
@@ -202,7 +208,7 @@ def send_system_notification_for_published_courses(courses):
"document_name": course.name,
"from_user": instructors[0] if instructors else None,
"type": "Alert",
"link": f"/lms/courses/{course.name}",
"link": get_lms_route(f"courses/{course.name}"),
}
)
make_notification_logs(notification, students)

View File

@@ -5,6 +5,8 @@ import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.utils import get_lms_route
class LMSMentorRequest(Document):
def on_update(self):
@@ -37,7 +39,7 @@ class LMSMentorRequest(Document):
email_template.response,
{
"member_name": frappe.db.get_value("User", frappe.session.user, "full_name"),
"course_url": "/lms/courses/" + course_details.slug,
"course_url": get_lms_route(f"courses/{course_details.slug}"),
"course": course_details.title,
},
)

View File

@@ -7,6 +7,8 @@ from frappe.email.doctype.email_template.email_template import get_email_templat
from frappe.model.document import Document
from frappe.utils import add_days, flt, nowdate
from lms.lms.utils import get_lms_route
class LMSPayment(Document):
pass
@@ -76,7 +78,9 @@ def send_mail(payment):
"title": frappe.db.get_value(
payment.payment_for_document_type, payment.payment_for_document, "title"
),
"link": f"/lms/billing/{ payment.payment_for_document_type.split(' ')[-1].lower() }/{ payment.payment_for_document }",
"link": get_lms_route(
f"billing/{payment.payment_for_document_type.split(' ')[-1].lower()}/{payment.payment_for_document}"
),
}
if custom_template:

View File

@@ -1,3 +1,4 @@
frappe.pages["lms-home"].on_page_load = function (wrapper) {
window.location.href = "/lms/courses";
const lmsPath = frappe.boot.lms_path || "lms";
window.location.href = `/${lmsPath}/courses`;
};

View File

@@ -2,6 +2,7 @@ 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
@@ -235,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/courses/{self.course.name}/learn/{lesson.number}"
expected_url = f"/{lms_path}/courses/{self.course.name}/learn/{lesson.number}"
self.assertEqual(get_lesson_url(self.course.name, lesson.number), expected_url)
def test_is_instructor(self):

View File

@@ -4,6 +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
@@ -88,4 +89,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"] = "/lms"
frappe.local.response["home_page"] = f"/{lms_path}"

View File

@@ -32,6 +32,22 @@ from lms.lms.md import find_macros
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
def get_lms_path():
path = frappe.conf.get("lms_path") or "lms"
return path.strip("/")
def get_lms_route(path=""):
base = f"/{get_lms_path()}"
if not path:
return base
return f"{base}/{path.lstrip('/')}"
def extend_bootinfo(bootinfo):
bootinfo["lms_path"] = get_lms_path()
def slugify(title, used_slugs=None):
"""Converts title to a slug.
@@ -277,7 +293,7 @@ def get_lesson_index(lesson_name):
def get_lesson_url(course, lesson_number):
if not lesson_number:
return
return f"/lms/courses/{course}/learn/{lesson_number}"
return get_lms_route(f"courses/{course}/learn/{lesson_number}")
def get_progress(course, lesson, member=None):
@@ -421,7 +437,7 @@ def get_batch_details_for_notification(topic):
users = []
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
subject = _("New comment in batch {0}").format(batch_title)
link = f"/lms/batches/{topic.reference_docname}"
link = get_lms_route(f"batches/{topic.reference_docname}")
instructors = frappe.db.get_all(
"Course Instructor",
{"parenttype": "LMS Batch", "parent": topic.reference_docname},
@@ -475,7 +491,7 @@ def notify_mentions_on_portal(doc, topic):
subject = _("{0} mentioned you in a comment in {1}").format(
frappe.bold(from_user_name), frappe.bold(batch_title)
)
link = f"/lms/batches/{topic.reference_docname}#discussions"
link = get_lms_route(f"batches/{topic.reference_docname}#discussions")
for user in mentions:
notification = frappe._dict(
@@ -1295,7 +1311,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"/lms/assignment-submission/{assessment.assessment_name}/{submission_name}"
assessment.url = get_lms_route(f"assignment-submission/{assessment.assessment_name}/{submission_name}")
return assessment

View File

@@ -11,7 +11,7 @@
{{ widgets.CourseCard(course=course, read_only=False) }}
{% endfor %}
</div>
<a class="d-flex justify-content-center align-items-center mt-12" href="/lms/courses">
<a class="d-flex justify-content-center align-items-center mt-12" href="{{ get_lms_route('courses') }}">
<span>{{ _("Explore More") }}</span>
</a>
</div>

View File

@@ -12,7 +12,7 @@
{% endfor %}
</div>
<a class="d-flex justify-content-center align-items-center mt-12" href="/lms/courses">
<a class="d-flex justify-content-center align-items-center mt-12" href="{{ get_lms_route('courses') }}">
<span>{{ _("Explore More") }}</span>
</a>
</div>
</div>

View File

@@ -1,6 +1,6 @@
{% set color = get_palette(member.full_name) %}
<span class="avatar {{ avatar_class }}" title="{{ member.full_name }}">
<a class="button-links" href="/lms/users/{{ member.username }}">
<a class="button-links" href="{{ get_lms_route('users/' ~ member.username) }}">
{% if member.user_image %}
<img class="avatar-frame standard-image" style="object-fit: cover;" src="{{ member.user_image }}"
title="{{ member.full_name }}">

View File

@@ -93,7 +93,7 @@
</div>
{% endif %}
{% endfor %}
<a class="button-links" href="/lms/users/{{ instructors[0].username }}">
<a class="button-links" href="{{ get_lms_route('users/' ~ instructors[0].username) }}">
<span class="course-instructor">
{% if ins_len == 1 %}
{{ instructors[0].full_name }}
@@ -128,7 +128,7 @@
<a class="stretched-link" href="{{ get_lesson_url(course.name, lesson_index) }}{{ query_parameter }}"></a>
{% else %}
<a class="stretched-link" href="/lms/courses/{{ course.name }}"></a>
<a class="stretched-link" href="{{ get_lms_route('courses/' ~ course.name) }}"></a>
{% endif %}
{% endif %}
</div>

View File

@@ -4,7 +4,6 @@
<br>
<p> {{ _(" Please evaluate and grade it.") }} </p>
<br>`
<a href="/lms/assignment-submission/{{ assignment_name }}/{{ submission_name }}">
<a href="{{ get_lms_route('assignment-submission/' ~ assignment_name ~ '/' ~ submission_name) }}">
{{ _("Open Assignment") }}
</a>

View File

@@ -23,7 +23,7 @@
<br>
<p>
{{ _("Visit the following link to view your ") }}
<a href="/lms/batches/{{ name }}">{{ _("Batch Details") }}</a>
<a href="{{ get_lms_route('batches/' ~ name) }}">{{ _("Batch Details") }}</a>
</p>
<p>
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
@@ -32,4 +32,3 @@
<p>
{{ _("Best Regards") }}
</p>

View File

@@ -20,7 +20,7 @@
</p>
<br>
<p>
<a href="/lms/batches/{{ name }}">👉 {{ _("Visit your batch") }}</a>
<a href="{{ get_lms_route('batches/' ~ name) }}">👉 {{ _("Visit your batch") }}</a>
</p>
<br>
<p>

View File

@@ -17,7 +17,7 @@
</p>
<br>
<p>
<a href="/lms/batches/{{ batch_name }}">👉 {{ _("Visit your batch") }}</a>
<a href="{{ get_lms_route('batches/' ~ batch_name) }}">👉 {{ _("Visit your batch") }}</a>
</p>
<br>
<p>
@@ -26,4 +26,4 @@
<br>
<p>
{{ _("Best Regards") }}
</p>
</p>

View File

@@ -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 {}