chore: resolved conflicts

This commit is contained in:
Jannat Patel
2025-12-15 15:18:08 +05:30
59 changed files with 1850 additions and 2615 deletions

86
lms/command_palette.py Normal file
View File

@@ -0,0 +1,86 @@
import frappe
from frappe.utils import nowdate
@frappe.whitelist()
def search_sqlite(query: str):
from lms.sqlite import LearningSearch, LearningSearchIndexMissingError
search = LearningSearch()
try:
result = search.search(query)
except LearningSearchIndexMissingError:
return []
return prepare_search_results(result)
def prepare_search_results(result):
roles = frappe.get_roles()
groups = {}
for r in result["results"]:
doctype = r["doctype"]
if doctype == "LMS Course" and can_access_course(r, roles):
r["author_info"] = get_instructor_info(doctype, r)
groups.setdefault("Courses", []).append(r)
elif doctype == "LMS Batch" and can_access_batch(r, roles):
r["author_info"] = get_instructor_info(doctype, r)
groups.setdefault("Batches", []).append(r)
elif doctype == "Job Opportunity" and can_access_job(r, roles):
r["author_info"] = get_instructor_info(doctype, r)
groups.setdefault("Job Opportunities", []).append(r)
out = []
for key in groups:
out.append({"title": key, "items": groups[key]})
return out
def can_access_course(course, roles):
if can_create_course(roles):
return True
elif course.get("published"):
return True
return False
def can_access_batch(batch, roles):
if can_create_batch(roles):
return True
elif batch.get("published") and batch.get("start_date") >= nowdate():
return True
return False
def can_access_job(job, roles):
if "Moderator" in roles:
return True
return job.get("status") == "Open"
def can_create_course(roles):
return "Course Creator" in roles or "Moderator" in roles
def can_create_batch(roles):
return "Batch Evaluator" in roles or "Moderator" in roles
def get_instructor_info(doctype, record):
instructors = frappe.get_all(
"Course Instructor", filters={"parenttype": doctype, "parent": record.get("name")}, pluck="instructor"
)
instructor = record.get("author")
if len(instructors):
instructor = instructors[0]
return frappe.db.get_value(
"User",
instructor,
["full_name", "email", "user_image", "username"],
as_dict=True,
)

View File

@@ -0,0 +1,19 @@
{
"app": "lms",
"creation": "2025-12-15 14:31:50.704854",
"docstatus": 0,
"doctype": "Desktop Icon",
"hidden": 0,
"icon_type": "App",
"idx": 0,
"label": "Frappe LMS",
"link": "/lms",
"link_type": "External",
"logo_url": "/assets/lms/frontend/learning.svg",
"modified": "2025-12-15 14:31:50.704854",
"modified_by": "Administrator",
"name": "Frappe LMS",
"owner": "Administrator",
"roles": [],
"standard": 1
}

View File

@@ -64,6 +64,9 @@ after_install = "lms.install.after_install"
after_sync = "lms.install.after_sync"
before_uninstall = "lms.install.before_uninstall"
setup_wizard_requires = "assets/lms/js/setup_wizard.js"
after_migrate = [
"lms.sqlite.build_index_in_background",
]
# Desk Notifications
# ------------------
@@ -115,6 +118,9 @@ doc_events = {
# Scheduled Tasks
# ---------------
scheduler_events = {
"all": [
"lms.sqlite.build_index_in_background",
],
"hourly": [
"lms.lms.doctype.lms_certificate_request.lms_certificate_request.schedule_evals",
"lms.lms.api.update_course_statistics",
@@ -191,7 +197,6 @@ update_website_context = [
jinja = {
"methods": [
"lms.lms.utils.get_signup_optin_checks",
"lms.lms.utils.get_tags",
"lms.lms.utils.get_lesson_count",
"lms.lms.utils.get_instructors",
@@ -254,3 +259,5 @@ add_to_apps_screen = [
"has_permission": "lms.lms.api.check_app_permission",
}
]
sqlite_search = ["lms.sqlite.LearningSearch"]

View File

@@ -6,6 +6,7 @@ import re
import shutil
import xml.etree.ElementTree as ET
import zipfile
from datetime import timedelta
from xml.dom.minidom import parseString
import frappe
@@ -23,47 +24,13 @@ from frappe.utils import (
flt,
format_date,
get_datetime,
getdate,
now,
)
from frappe.utils.response import Response
from lms.lms.doctype.course_lesson.course_lesson import save_progress
from lms.lms.utils import get_average_rating, get_lesson_count
@frappe.whitelist()
def autosave_section(section, code):
"""Saves the code edited in one of the sections."""
doc = frappe.get_doc(doctype="Code Revision", section=section, code=code, author=frappe.session.user)
doc.insert()
return {"name": doc.name}
@frappe.whitelist()
def submit_solution(exercise, code):
"""Submits a solution.
@exerecise: name of the exercise to submit
@code: solution to the exercise
"""
ex = frappe.get_doc("LMS Exercise", exercise)
if not ex:
return
doc = ex.submit(code)
return {"name": doc.name, "creation": doc.creation}
@frappe.whitelist()
def save_current_lesson(course_name, lesson_name):
"""Saves the current lesson for a student/mentor."""
name = frappe.get_value(
doctype="LMS Enrollment",
filters={"course": course_name, "member": frappe.session.user},
fieldname="name",
)
if not name:
return
frappe.db.set_value("LMS Enrollment", name, "current_lesson", lesson_name)
from lms.lms.utils import get_average_rating, get_batch_details, get_course_details, get_lesson_count
@frappe.whitelist(allow_guest=True)
@@ -102,9 +69,32 @@ def get_translations():
@frappe.whitelist()
def validate_billing_access(billing_type, name):
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
access, message = verify_billing_access(doctype, name, billing_type)
address = frappe.db.get_value(
"Address",
{"email_id": frappe.session.user},
[
"name",
"address_title as billing_name",
"address_line1",
"address_line2",
"city",
"state",
"country",
"pincode",
"phone",
],
as_dict=1,
)
return {"access": access, "message": message, "address": address}
def verify_billing_access(doctype, name, billing_type):
access = True
message = ""
doctype = "LMS Batch" if billing_type == "batch" else "LMS Course"
if frappe.session.user == "Guest":
access = False
@@ -154,47 +144,7 @@ def validate_billing_access(billing_type, name):
access = False
message = _("You have already purchased the certificate for this course.")
address = frappe.db.get_value(
"Address",
{"email_id": frappe.session.user},
[
"name",
"address_title as billing_name",
"address_line1",
"address_line2",
"city",
"state",
"country",
"pincode",
"phone",
],
as_dict=1,
)
return {"access": access, "message": message, "address": address}
@frappe.whitelist(allow_guest=True)
def get_job_details(job):
return frappe.db.get_value(
"Job Opportunity",
job,
[
"job_title",
"location",
"country",
"type",
"work_mode",
"company_name",
"company_logo",
"company_website",
"name",
"creation",
"description",
"owner",
],
as_dict=1,
)
return access, message
@frappe.whitelist(allow_guest=True)
@@ -1665,3 +1615,340 @@ def get_profile_details(username):
details.roles = frappe.get_roles(details.name)
return details
@frappe.whitelist()
def get_streak_info():
if frappe.session.user == "Guest":
return {}
all_dates = fetch_activity_dates(frappe.session.user)
streak, longest_streak = calculate_streaks(all_dates)
current_streak = calculate_current_streak(all_dates, streak)
return {
"current_streak": current_streak,
"longest_streak": longest_streak,
}
def fetch_activity_dates(user):
doctypes = [
"LMS Course Progress",
"LMS Quiz Submission",
"LMS Assignment Submission",
"LMS Programming Exercise Submission",
]
all_dates = []
for dt in doctypes:
all_dates.extend(frappe.get_all(dt, {"member": user}, pluck="creation"))
return sorted({d.date() if hasattr(d, "date") else d for d in all_dates})
def calculate_streaks(all_dates):
streak = 0
longest_streak = 0
prev_day = None
for d in all_dates:
if d.weekday() in (5, 6):
continue
if prev_day:
expected = prev_day + timedelta(days=1)
while expected.weekday() in (5, 6):
expected += timedelta(days=1)
streak = streak + 1 if d == expected else 1
else:
streak = 1
longest_streak = max(longest_streak, streak)
prev_day = d
return streak, longest_streak
def calculate_current_streak(all_dates, streak):
if not all_dates:
return 0
last_date = all_dates[-1]
today = getdate()
ref_day = today
while ref_day.weekday() in (5, 6):
ref_day -= timedelta(days=1)
if last_date == ref_day or last_date == ref_day - timedelta(days=1):
return streak
return 0
@frappe.whitelist()
def get_my_live_classes():
my_live_classes = []
if frappe.session.user == "Guest":
return my_live_classes
batches = frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
pluck="batch",
)
live_class_details = frappe.get_all(
"LMS Live Class",
filters={
"date": [">=", getdate()],
"batch_name": ["in", batches],
},
fields=[
"name",
"title",
"description",
"time",
"date",
"duration",
"attendees",
"start_url",
"join_url",
"owner",
],
limit=2,
order_by="date",
)
if len(live_class_details):
for live_class in live_class_details:
live_class.course_title = frappe.db.get_value("LMS Course", live_class.course, "title")
my_live_classes.append(live_class)
return my_live_classes
@frappe.whitelist()
def get_created_courses():
created_courses = []
if frappe.session.user == "Guest":
return created_courses
CourseInstructor = frappe.qb.DocType("Course Instructor")
Course = frappe.qb.DocType("LMS Course")
query = (
frappe.qb.from_(CourseInstructor)
.join(Course)
.on(CourseInstructor.parent == Course.name)
.select(Course.name)
.where(CourseInstructor.instructor == frappe.session.user)
.orderby(Course.published_on, order=frappe.qb.desc)
.limit(3)
)
results = query.run(as_dict=True)
courses = [row["name"] for row in results]
for course in courses:
course_details = get_course_details(course)
created_courses.append(course_details)
return created_courses
@frappe.whitelist()
def get_created_batches():
created_batches = []
if frappe.session.user == "Guest":
return created_batches
CourseInstructor = frappe.qb.DocType("Course Instructor")
Batch = frappe.qb.DocType("LMS Batch")
query = (
frappe.qb.from_(CourseInstructor)
.join(Batch)
.on(CourseInstructor.parent == Batch.name)
.select(Batch.name)
.where(CourseInstructor.instructor == frappe.session.user)
.where(Batch.start_date >= getdate())
.orderby(Batch.start_date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
batches = [row["name"] for row in results]
for batch in batches:
batch_details = get_batch_details(batch)
created_batches.append(batch_details)
return created_batches
@frappe.whitelist()
def get_admin_live_classes():
if frappe.session.user == "Guest":
return []
CourseInstructor = frappe.qb.DocType("Course Instructor")
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
query = (
frappe.qb.from_(CourseInstructor)
.join(LMSLiveClass)
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
.select(
LMSLiveClass.name,
LMSLiveClass.title,
LMSLiveClass.description,
LMSLiveClass.time,
LMSLiveClass.date,
LMSLiveClass.duration,
LMSLiveClass.attendees,
LMSLiveClass.start_url,
LMSLiveClass.join_url,
LMSLiveClass.owner,
)
.where(CourseInstructor.instructor == frappe.session.user)
.where(LMSLiveClass.date >= getdate())
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
return results
@frappe.whitelist()
def get_admin_evals():
if frappe.session.user == "Guest":
return []
evals = frappe.get_all(
"LMS Certificate Request",
{
"evaluator": frappe.session.user,
"date": [">=", getdate()],
},
[
"name",
"date",
"start_time",
"course",
"evaluator",
"google_meet_link",
"member",
"member_name",
],
limit=4,
order_by="date asc",
)
for evaluation in evals:
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
return evals
@frappe.whitelist()
def get_my_courses():
my_courses = []
if frappe.session.user == "Guest":
return my_courses
courses = get_my_latest_courses()
if not len(courses):
courses = get_featured_home_courses()
if not len(courses):
courses = get_popular_courses()
for course in courses:
my_courses.append(get_course_details(course))
return my_courses
def get_my_latest_courses():
return frappe.get_all(
"LMS Enrollment",
{
"member": frappe.session.user,
},
order_by="modified desc",
limit=3,
pluck="course",
)
def get_featured_home_courses():
return frappe.get_all(
"LMS Course",
{"published": 1, "featured": 1},
order_by="published_on desc",
limit=3,
pluck="name",
)
def get_popular_courses():
return frappe.get_all(
"LMS Course",
{
"published": 1,
},
order_by="enrollments desc",
limit=3,
pluck="name",
)
@frappe.whitelist()
def get_my_batches():
my_batches = []
if frappe.session.user == "Guest":
return my_batches
batches = get_my_latest_batches()
if not len(batches):
batches = get_upcoming_batches()
for batch in batches:
batch_details = get_batch_details(batch)
if batch_details:
my_batches.append(batch_details)
return my_batches
def get_my_latest_batches():
return frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
limit=4,
pluck="batch",
)
def get_upcoming_batches():
return frappe.get_all(
"LMS Batch",
{
"published": 1,
"start_date": [">=", getdate()],
},
order_by="start_date asc",
limit=4,
pluck="name",
)

View File

@@ -1,7 +0,0 @@
// Copyright (c) 2021, Frappe and contributors
// For license information, please see license.txt
frappe.ui.form.on("Exercise Latest Submission", {
// refresh: function(frm) {
// }
});

View File

@@ -1,150 +0,0 @@
{
"actions": [],
"creation": "2021-12-08 17:56:26.049675",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"exercise",
"status",
"batch_old",
"column_break_4",
"exercise_title",
"course",
"lesson",
"section_break_8",
"solution",
"image",
"test_results",
"comments",
"latest_submission",
"member",
"member_email"
],
"fields": [
{
"fieldname": "exercise",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Exercise",
"options": "LMS Exercise",
"search_index": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Correct\nIncorrect"
},
{
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
"fieldtype": "Data",
"label": "Exercise Title",
"read_only": 1
},
{
"fetch_from": "exercise.course",
"fieldname": "course",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fetch_from": "exercise.lesson",
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fetch_from": "latest_submission.solution",
"fieldname": "solution",
"fieldtype": "Code",
"label": "Solution"
},
{
"fetch_from": "latest_submission.image",
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fetch_from": "latest_submission.test_results",
"fieldname": "test_results",
"fieldtype": "Small Text",
"label": "Test Results"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fieldname": "latest_submission",
"fieldtype": "Link",
"label": "Latest Submission",
"options": "Exercise Submission"
},
{
"fieldname": "member",
"fieldtype": "Link",
"label": "Member",
"options": "LMS Enrollment"
},
{
"fetch_from": "member.member",
"fieldname": "member_email",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Member Email",
"options": "User",
"search_index": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-14 20:56:52.370697",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Latest Submission",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2021, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ExerciseLatestSubmission(Document):
pass

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2021, Frappe and Contributors
# See license.txt
# import frappe
import unittest
class TestExerciseLatestSubmission(unittest.TestCase):
pass

View File

@@ -1,7 +0,0 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("Exercise Submission", {
// refresh: function(frm) {
// }
});

View File

@@ -1,126 +0,0 @@
{
"actions": [],
"creation": "2021-05-19 11:41:18.108316",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"exercise",
"status",
"batch_old",
"column_break_4",
"exercise_title",
"course",
"lesson",
"section_break_8",
"solution",
"image",
"test_results",
"comments",
"member"
],
"fields": [
{
"fieldname": "exercise",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Exercise",
"options": "LMS Exercise"
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Exercise Title",
"read_only": 1
},
{
"fetch_from": "exercise.course",
"fieldname": "course",
"fieldtype": "Link",
"label": "Course",
"options": "LMS Course",
"read_only": 1
},
{
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fetch_from": "exercise.lesson",
"fieldname": "lesson",
"fieldtype": "Link",
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Correct\nIncorrect"
},
{
"fieldname": "test_results",
"fieldtype": "Small Text",
"label": "Test Results"
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"label": "Comments"
},
{
"fieldname": "solution",
"fieldtype": "Code",
"in_list_view": 1,
"label": "Solution"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break"
},
{
"fieldname": "member",
"fieldtype": "Link",
"label": "Member",
"options": "LMS Enrollment"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-12-08 22:25:05.809377",
"modified_by": "Administrator",
"module": "LMS",
"name": "Exercise Submission",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -1,29 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
class ExerciseSubmission(Document):
def on_update(self):
self.update_latest_submission()
def update_latest_submission(self):
names = frappe.get_all(
"Exercise Latest Submission", {"exercise": self.exercise, "member": self.member}
)
if names:
doc = frappe.get_doc("Exercise Latest Submission", names[0])
doc.latest_submission = self.name
doc.save(ignore_permissions=True)
else:
doc = frappe.get_doc(
{
"doctype": "Exercise Latest Submission",
"exercise": self.exercise,
"member": self.member,
"latest_submission": self.name,
}
)
doc.insert(ignore_permissions=True)

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestExerciseSubmission(unittest.TestCase):
pass

View File

@@ -1,7 +0,0 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Batch Old", {
// refresh: function(frm) {
// }
});

View File

@@ -1,150 +0,0 @@
{
"actions": [],
"autoname": "format: BATCH-{#####}",
"creation": "2021-03-18 19:37:34.614796",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"course",
"start_date",
"start_time",
"column_break_3",
"title",
"sessions_on",
"end_time",
"section_break_5",
"description",
"section_break_7",
"visibility",
"membership",
"column_break_9",
"status",
"stage"
],
"fields": [
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Course",
"options": "LMS Course",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"label": "Title",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Markdown Editor",
"label": "Description"
},
{
"default": "Public",
"fieldname": "visibility",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Visibility",
"options": "Public\nUnlisted\nPrivate"
},
{
"fieldname": "membership",
"fieldtype": "Select",
"label": "Membership",
"options": "\nOpen\nRestricted\nInvite Only\nClosed"
},
{
"default": "Active",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Active\nInactive"
},
{
"default": "Ready",
"fieldname": "stage",
"fieldtype": "Select",
"label": "Stage",
"options": "Ready\nIn Progress\nCompleted\nCancelled"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Batch Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Batch Settings"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
},
{
"fieldname": "start_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "Start Time"
},
{
"fieldname": "sessions_on",
"fieldtype": "Data",
"label": "Sessions On Days"
},
{
"fieldname": "end_time",
"fieldtype": "Time",
"in_list_view": 1,
"label": "End Time"
}
],
"index_web_pages_for_search": 1,
"links": [
{
"group": "Members",
"link_doctype": "LMS Enrollment",
"link_fieldname": "batch_old"
}
],
"modified": "2022-09-28 18:43:22.955907",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch Old",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,90 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from lms.lms.doctype.lms_enrollment.lms_enrollment import create_membership
from lms.lms.utils import is_mentor
class LMSBatchOld(Document):
def validate(self):
pass
# self.validate_if_mentor()
def validate_if_mentor(self):
if not is_mentor(self.course, frappe.session.user):
course_title = frappe.db.get_value("LMS Course", self.course, "title")
frappe.throw(_("You are not a mentor of the course {0}").format(course_title))
def after_insert(self):
create_membership(batch=self.name, course=self.course, member_type="Mentor")
def is_member(self, email, member_type=None):
"""Checks if a person is part of a batch.
If member_type is specified, checks if the person is a Student/Mentor.
"""
filters = {"batch_old": self.name, "member": email}
if member_type:
filters["member_type"] = member_type
return frappe.db.exists("LMS Enrollment", filters)
def get_membership(self, email):
"""Returns the membership document of given user."""
name = frappe.get_value(
doctype="LMS Enrollment",
filters={"batch_old": self.name, "member": email},
fieldname="name",
)
return frappe.get_doc("LMS Enrollment", name)
def get_current_lesson(self, user):
"""Returns the name of the current lesson for the given user."""
membership = self.get_membership(user)
return membership and membership.current_lesson
@frappe.whitelist()
def save_message(message, batch):
doc = frappe.get_doc(
{
"doctype": "LMS Message",
"batch_old": batch,
"author": frappe.session.user,
"message": message,
}
)
doc.save(ignore_permissions=True)
def switch_batch(course_name, email, batch_name):
"""Switches the user from the current batch of the course to a new batch."""
membership = frappe.get_last_doc("LMS Enrollment", filters={"course": course_name, "member": email})
batch = frappe.get_doc("LMS Batch Old", batch_name)
if not batch:
raise ValueError(f"Invalid Batch: {batch_name}")
if batch.course != course_name:
raise ValueError("Can not switch batches across courses")
if batch.is_member(email):
print(f"{email} is already a member of {batch.title}")
return
old_batch = frappe.get_doc("LMS Batch Old", membership.batch_old)
membership.batch_old = batch_name
membership.save()
# update exercise submissions
filters = {"owner": email, "batch_old": old_batch.name}
for name in frappe.db.get_all("Exercise Submission", filters=filters, pluck="name"):
doc = frappe.get_doc("Exercise Submission", name)
print("updating exercise submission", name)
doc.batch_old = batch_name
doc.save()

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
# import frappe
import unittest
class TestLMSBatchOld(unittest.TestCase):
pass

View File

@@ -6,9 +6,7 @@ from frappe import _
from frappe.email.doctype.email_template.email_template import get_email_template
from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.utils import add_years, nowdate
from lms.lms.utils import is_certified
from frappe.utils import nowdate
class LMSCertificate(Document):
@@ -112,6 +110,13 @@ def has_website_permission(doc, ptype, user, verbose=False):
return False
def is_certified(course):
certificate = frappe.get_all("LMS Certificate", {"member": frappe.session.user, "course": course})
if len(certificate):
return certificate[0].name
return
@frappe.whitelist()
def create_certificate(course):
if is_certified(course):

View File

@@ -3,35 +3,6 @@
import unittest
import frappe
from frappe.utils import cint, nowdate
from lms.lms.doctype.lms_certificate.lms_certificate import create_certificate
from lms.lms.doctype.lms_course.test_lms_course import new_course
class TestLMSCertificate(unittest.TestCase):
def test_certificate_creation(self):
course = new_course(
"Test Certificate",
{
"enable_certification": 1,
},
)
create_enrollment(course.name)
certificate = create_certificate(course.name)
self.assertEqual(certificate.member, "Administrator")
self.assertEqual(certificate.course, course.name)
self.assertEqual(certificate.issue_date, nowdate())
frappe.db.delete("LMS Certificate", certificate.name)
frappe.db.delete("LMS Course", course.name)
def create_enrollment(course):
enrollment = frappe.new_doc("LMS Enrollment")
enrollment.course = course
enrollment.member = frappe.session.user
enrollment.progress = cint(100)
enrollment.save()
pass

View File

@@ -1,12 +1,5 @@
{
"actions": [
{
"action": "lms.lms.doctype.lms_course.lms_course.reindex_exercises",
"action_type": "Server Action",
"group": "Reindex",
"label": "Reindex Exercises"
}
],
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2022-02-22 15:28:26.091549",
@@ -76,6 +69,8 @@
"default": "0",
"fieldname": "published",
"fieldtype": "Check",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Published"
},
{
@@ -152,8 +147,6 @@
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "In Progress\nUnder Review\nApproved",
"read_only": 1
@@ -313,7 +306,7 @@
}
],
"make_attachments_public": 1,
"modified": "2025-10-13 15:08:11.734204",
"modified": "2025-12-15 15:15:42.226098",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Course",
@@ -336,6 +329,7 @@
"delete": 1,
"email": 1,
"export": 1,
"import": 1,
"print": 1,
"read": 1,
"report": 1,
@@ -348,6 +342,7 @@
"delete": 1,
"email": 1,
"export": 1,
"import": 1,
"print": 1,
"read": 1,
"report": 1,

View File

@@ -9,8 +9,6 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, today
from lms.lms.utils import get_chapters
from ...utils import generate_slug, update_payment_record, validate_image
@@ -133,77 +131,3 @@ class LMSCourse(Document):
def __repr__(self):
return f"<Course#{self.name}>"
def has_mentor(self, email):
"""Checks if this course has a mentor with given email."""
if not email or email == "Guest":
return False
mapping = frappe.get_all("LMS Course Mentor Mapping", {"course": self.name, "mentor": email})
return mapping != []
def add_mentor(self, email):
"""Adds a new mentor to the course."""
if not email:
raise ValueError("Invalid email")
if email == "Guest":
raise ValueError("Guest user can not be added as a mentor")
# given user is already a mentor
if self.has_mentor(email):
return
doc = frappe.get_doc({"doctype": "LMS Course Mentor Mapping", "course": self.name, "mentor": email})
doc.insert()
def get_student_batch(self, email):
"""Returns the batch the given student is part of.
Returns None if the student is not part of any batch.
"""
if not email:
return
batch_name = frappe.get_value(
doctype="LMS Enrollment",
filters={"course": self.name, "member_type": "Student", "member": email},
fieldname="batch_old",
)
return batch_name and frappe.get_doc("LMS Batch Old", batch_name)
def get_batches(self, mentor=None):
batches = frappe.get_all("LMS Batch Old", {"course": self.name})
if mentor:
# TODO: optimize this
memberships = frappe.db.get_all("LMS Enrollment", {"member": mentor}, ["batch_old"])
batch_names = {m.batch_old for m in memberships}
return [b for b in batches if b.name in batch_names]
def reindex_exercises(self):
for i, c in enumerate(get_chapters(self.name), start=1):
self._reindex_exercises_in_chapter(c, i)
def _reindex_exercises_in_chapter(self, c, index):
i = 1
for lesson in self.get_lessons(c):
for exercise in lesson.get_exercises():
exercise.index_ = i
exercise.index_label = f"{index}.{i}"
exercise.save()
i += 1
def get_all_memberships(self, member):
all_memberships = frappe.get_all(
"LMS Enrollment", {"member": member, "course": self.name}, ["batch_old"]
)
for membership in all_memberships:
membership.batch_title = frappe.db.get_value("LMS Batch Old", membership.batch_old, "title")
return all_memberships
@frappe.whitelist()
def reindex_exercises(doc):
course_data = json.loads(doc)
course = frappe.get_doc("LMS Course", course_data["name"])
course.reindex_exercises()
frappe.msgprint("All exercises in this course have been re-indexed.")

View File

@@ -13,31 +13,15 @@ class TestLMSCourse(unittest.TestCase):
course = new_course("Test Course")
assert course.title == "Test Course"
# disabled this test as it is failing
def _test_add_mentors(self):
course = new_course("Test Course")
assert course.get_mentors() == []
new_user("Tester", "tester@example.com")
course.add_mentor("tester@example.com")
mentors = course.get_mentors()
mentors_data = [dict(email=mentor.email, batch_count=mentor.batch_count) for mentor in mentors]
assert mentors_data == [{"email": "tester@example.com", "batch_count": 0}]
def tearDown(self):
if frappe.db.exists("User", "tester@example.com"):
frappe.delete_doc("User", "tester@example.com")
if frappe.db.exists("LMS Course", "test-course"):
frappe.db.delete("Batch Course", {"course": "test-course"})
frappe.db.delete("Exercise Submission", {"course": "test-course"})
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
frappe.db.delete("LMS Exercise", {"course": "test-course"})
frappe.db.delete("LMS Enrollment", {"course": "test-course"})
frappe.db.delete("Course Lesson", {"course": "test-course"})
frappe.db.delete("Course Chapter", {"course": "test-course"})
frappe.db.delete("LMS Batch Old", {"course": "test-course"})
frappe.db.delete("LMS Course Mentor Mapping", {"course": "test-course"})
frappe.db.delete("Course Instructor", {"parent": "test-course"})
frappe.db.sql("delete from `tabCourse Instructor`")
@@ -80,6 +64,7 @@ def new_course(title, additional_filters=None):
"video_link": "https://youtu.be/pEbIhUySqbk",
"image": "/assets/lms/images/course-home.png",
"instructors": [{"instructor": user}],
"published": 1,
}
if additional_filters:

View File

@@ -2,17 +2,24 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import cint
class LMSCourseReview(Document):
def validate(self):
self.validate_enrollment()
self.validate_if_already_reviewed()
def validate_enrollment(self):
enrollment = frappe.db.exists("LMS Enrollment", {"course": self.course, "member": self.owner})
if not enrollment:
frappe.throw(_("You must be enrolled in the course to submit a review"))
def validate_if_already_reviewed(self):
if frappe.db.exists("LMS Course Review", {"course": self.course, "owner": self.owner}):
frappe.throw(frappe._("You have already reviewed this course"))
frappe.throw(_("You have already reviewed this course"))
@frappe.whitelist()

View File

@@ -19,18 +19,11 @@
"purchased_certificate",
"certificate",
"section_break_8",
"batch_old",
"column_break_12",
"member_type",
"role"
],
"fields": [
{
"fieldname": "batch_old",
"fieldtype": "Link",
"label": "Batch Old",
"options": "LMS Batch Old"
},
{
"fieldname": "member",
"fieldtype": "Link",
@@ -141,7 +134,7 @@
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-12-14 20:59:37.166118",
"modified": "2025-12-15 21:27:30.733483",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Enrollment",

View File

@@ -9,56 +9,43 @@ from frappe.utils import ceil
class LMSEnrollment(Document):
def validate(self):
self.validate_membership_in_same_batch()
self.validate_membership_in_different_batch_same_course()
self.validate_course_enrollment_eligibility()
def on_update(self):
update_program_progress(self.member)
def validate_membership_in_same_batch(self):
filters = {"member": self.member, "course": self.course, "name": ["!=", self.name]}
if self.batch_old:
filters["batch_old"] = self.batch_old
previous_membership = frappe.db.get_value(
"LMS Enrollment", filters, fieldname=["member_type", "member"], as_dict=1
def validate_course_enrollment_eligibility(self):
course_details = frappe.db.get_value(
"LMS Course",
self.course,
["published", "disable_self_learning", "paid_course", "paid_certificate"],
as_dict=True,
)
if previous_membership:
member_name = frappe.db.get_value("User", self.member, "full_name")
course_title = frappe.db.get_value("LMS Course", self.course, "title")
if course_details.disable_self_learning:
frappe.throw(
_("{0} is already a {1} of the course {2}").format(
member_name, previous_membership.member_type, course_title
_(
"You cannot enroll in this course as self-learning is disabled. Please contact the Administrator."
)
)
def validate_membership_in_different_batch_same_course(self):
"""Ensures that a studnet is only part of one batch."""
# nothing to worry if the member is not a student
if self.member_type != "Student":
return
if not course_details.published:
frappe.throw(_("You cannot enroll in an unpublished course."))
course = frappe.db.get_value("LMS Batch Old", self.batch_old, "course")
memberships = frappe.get_all(
"LMS Enrollment",
filters={
"member": self.member,
"name": ["!=", self.name],
"member_type": "Student",
"course": self.course,
},
fields=["batch_old", "member_type", "name"],
)
if memberships:
membership = memberships[0]
member_name = frappe.db.get_value("User", self.member, "full_name")
frappe.throw(
_("{0} is already a Student of {1} course through {2} batch").format(
member_name, course, membership.batch_old
)
if course_details.paid_course:
payment = frappe.db.exists(
"LMS Payment",
{
"reference_doctype": "LMS Course",
"reference_docname": course,
"member": member,
"payment_receipt": True,
},
)
if not payment:
frappe.throw(_("You need to complete the payment for this course before enrolling."))
def update_program_progress(member):
programs = frappe.get_all("LMS Program Member", {"member": member}, ["parent", "name"])
@@ -77,8 +64,6 @@ def update_program_progress(member):
@frappe.whitelist()
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
validate_course_enrollment_eligibility(course, member)
enrollment = frappe.new_doc("LMS Enrollment")
enrollment.update(
{
@@ -94,42 +79,6 @@ def create_membership(course, batch=None, member=None, member_type="Student", ro
return enrollment
def validate_course_enrollment_eligibility(course, member):
if not member:
member = frappe.session.user
course_details = frappe.db.get_value(
"LMS Course",
course,
["published", "disable_self_learning", "paid_course", "paid_certificate"],
as_dict=True,
)
if course_details.disable_self_learning:
frappe.throw(
_(
"You cannot enroll in this course as self-learning is disabled. Please contact the Administrator."
)
)
if not course_details.published:
frappe.throw(_("You cannot enroll in an unpublished course."))
if course_details.paid_course:
payment = frappe.db.exists(
"LMS Payment",
{
"reference_doctype": "LMS Course",
"reference_docname": course,
"member": member,
"payment_receipt": True,
},
)
if not payment:
frappe.throw(_("You need to complete the payment for this course before enrolling."))
@frappe.whitelist()
def update_current_membership(batch, course, member):
all_memberships = frappe.get_all("LMS Enrollment", {"member": member, "course": course})

View File

@@ -5,19 +5,6 @@ import unittest
import frappe
from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
class TestLMSEnrollment(unittest.TestCase):
def test_membership(self):
course = new_course("Test Enrollment")
enrollment = frappe.new_doc("LMS Enrollment")
enrollment.course = course.name
enrollment.member = frappe.session.user
enrollment.save()
self.assertEqual(enrollment.course, course.name)
self.assertEqual(enrollment.member, "Administrator")
frappe.db.delete("LMS Enrollment", enrollment.name)
frappe.db.delete("LMS Course", course.name)
pass

View File

@@ -1,7 +0,0 @@
// Copyright (c) 2021, FOSS United and contributors
// For license information, please see license.txt
frappe.ui.form.on("LMS Exercise", {
// refresh: function(frm) {
// }
});

View File

@@ -1,123 +0,0 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-05-19 17:43:39.923430",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"description",
"code",
"answer",
"column_break_4",
"course",
"hints",
"tests",
"image",
"lesson",
"index_",
"index_label"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title"
},
{
"fieldname": "course",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Course",
"options": "LMS Course"
},
{
"columns": 4,
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
"label": "Description"
},
{
"columns": 4,
"fieldname": "answer",
"fieldtype": "Code",
"label": "Answer"
},
{
"fieldname": "tests",
"fieldtype": "Code",
"label": "Tests"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"columns": 4,
"fieldname": "hints",
"fieldtype": "Small Text",
"label": "Hints"
},
{
"columns": 4,
"fieldname": "code",
"fieldtype": "Code",
"label": "Code"
},
{
"fieldname": "image",
"fieldtype": "Code",
"label": "Image",
"read_only": 1
},
{
"fieldname": "lesson",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Lesson",
"options": "Course Lesson"
},
{
"fieldname": "index_",
"fieldtype": "Int",
"label": "Index",
"read_only": 1
},
{
"fieldname": "index_label",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Index Label",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-09-29 15:27:55.585874",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Exercise",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "title",
"sort_field": "index_label",
"sort_order": "ASC",
"title_field": "title",
"track_changes": 1
}

View File

@@ -1,52 +0,0 @@
# Copyright (c) 2021, FOSS United and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from lms.lms.utils import get_membership
class LMSExercise(Document):
def get_user_submission(self):
"""Returns the latest submission for this user."""
user = frappe.session.user
if not user or user == "Guest":
return
result = frappe.get_all(
"Exercise Submission",
fields="*",
filters={"owner": user, "exercise": self.name},
order_by="creation desc",
page_length=1,
)
if result:
return result[0]
def submit(self, code):
"""Submits the given code as solution to exercise."""
user = frappe.session.user
if not user or user == "Guest":
return
old_submission = self.get_user_submission()
if old_submission and old_submission.solution == code:
return old_submission
member = get_membership(self.course, frappe.session.user)
doc = frappe.get_doc(
doctype="Exercise Submission",
exercise=self.name,
exercise_title=self.title,
course=self.course,
lesson=self.lesson,
batch=member.batch_old,
solution=code,
member=member.name,
)
doc.insert(ignore_permissions=True)
return doc

View File

@@ -1,10 +0,0 @@
# Copyright (c) 2021, FOSS United and Contributors
# See license.txt
import unittest
# import frappe
class TestLMSExercise(unittest.TestCase):
pass

View File

@@ -2,16 +2,246 @@ import unittest
import frappe
from .utils import slugify
from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template, is_certified
from .utils import (
get_average_rating,
get_chapters,
get_instructors,
get_lesson_index,
get_lesson_url,
get_lessons,
get_membership,
get_reviews,
get_tags,
has_course_instructor_role,
has_evaluator_role,
has_moderator_role,
has_student_role,
is_instructor,
slugify,
)
class TestUtils(unittest.TestCase):
def test_simple(self):
def setUp(self):
self.student1 = self.create_user("student1@example.com", "Ashley", "Smith", ["LMS Student"])
self.student2 = self.create_user("student2@example.com", "John", "Doe", ["LMS Student"])
self.admin = self.create_user(
"frappe@example.com", "Frappe", "Admin", ["Moderator", "Course Creator", "Batch Evaluator"]
)
self.create_a_course()
self.add_chapters()
self.add_lessons()
self.add_enrollment(self.course.name, self.student1.email)
self.add_enrollment(self.course.name, self.student2.email)
self.add_rating(self.course.name, self.student1.email, 0.8, "Good course")
self.add_rating(self.course.name, self.student2.email, 1, "Excellent course")
self.create_certificate(self.course.name, self.student1.email)
def create_a_course(self):
course = frappe.new_doc("LMS Course")
course.title = "Utility Course"
course.short_introduction = "A course to test utilities of Frappe Learning"
course.description = "This is a detailed description of the Utility Course."
course.tags = "Frappe,Learning,Utility"
course.published = 1
course.append("instructors", {"instructor": "frappe@example.com"})
course.save()
self.course = course
def add_chapters(self):
chapters = []
for i in range(1, 4):
chapter = frappe.new_doc("Course Chapter")
chapter.course = self.course.name
chapter.title = f"Chapter {i}"
chapter.save()
chapters.append(chapter)
self.course.reload()
for chapter in chapters:
self.course.append("chapters", {"chapter": chapter.name})
self.course.save()
def add_lessons(self):
for chapter in self.course.chapters:
chapterDoc = frappe.get_doc("Course Chapter", chapter.chapter)
lessons = []
for j in range(1, 3):
lesson = frappe.new_doc("Course Lesson")
lesson.course = self.course.name
lesson.chapter = chapter.chapter
lesson.title = f"Lesson {j} of {chapter.chapter}"
content = '{"time":1765194986690,"blocks":[{"id":"dkLzbW14ds","type":"markdown","data":{"text":"This is a simple content for the current lesson."}},{"id":"KBwuWPc8rV","type":"markdown","data":{"text":""}}],"version":"2.29.0"}'
lesson.content = content
lesson.save()
lessons.append(lesson)
for lesson in lessons:
chapterDoc.append("lessons", {"lesson": lesson.name})
chapterDoc.save()
def create_user(self, email, first_name, last_name, roles):
if not frappe.db.exists("User", email):
user = frappe.new_doc("User")
user.email = email
user.first_name = first_name
user.last_name = last_name
user.user_type = "Website User"
for role in roles:
user.append("roles", {"role": role})
user.save()
return user
else:
return frappe.get_doc("User", email)
def create_certificate(self, course_name, member):
certificate = frappe.new_doc("LMS Certificate")
certificate.course = course_name
certificate.member = member
certificate.issue_date = frappe.utils.nowdate()
certificate.template = get_default_certificate_template()
certificate.save()
return certificate
def test_simple_slugs(self):
self.assertEqual(slugify("hello-world"), "hello-world")
self.assertEqual(slugify("Hello World"), "hello-world")
self.assertEqual(slugify("Hello, World!"), "hello-world")
def test_duplicates(self):
def test_duplicates_slugs(self):
self.assertEqual(slugify("Hello World", ["hello-world"]), "hello-world-2")
self.assertEqual(slugify("Hello World", ["hello-world", "hello-world-2"]), "hello-world-3")
def add_enrollment(self, course, member):
enrollment = frappe.new_doc("LMS Enrollment")
enrollment.course = course
enrollment.member = member
enrollment.save()
def test_get_membership(self):
membership = get_membership(self.course.name, self.student1.email)
self.assertIsNotNone(membership)
self.assertEqual(membership.course, self.course.name)
self.assertEqual(membership.member, self.student1.email)
def test_get_chapters(self):
chapters = get_chapters(self.course.name)
self.assertEqual(len(chapters), len(self.course.chapters))
for i, chapter in enumerate(chapters, start=1):
self.assertEqual(chapter.title, f"Chapter {i}")
def test_get_lessons(self):
lessons = get_lessons(self.course.name)
all_lessons = frappe.db.count("Course Lesson", {"course": self.course.name})
self.assertEqual(len(lessons), all_lessons)
for chapter in self.course.chapters:
chapter_lessons = [lesson for lesson in lessons if lesson.chapter == chapter.chapter]
self.assertEqual(len(chapter_lessons), 2)
for j, lesson in enumerate(chapter_lessons, start=1):
self.assertEqual(lesson.title, f"Lesson {j} of {chapter.chapter}")
self.assertEqual(lesson.number, f"{chapter.idx}-{j}")
def test_get_tags(self):
tags = get_tags(self.course.name)
expected_tags = ["Frappe", "Learning", "Utility"]
self.assertEqual(set(tags), set(expected_tags))
def test_get_instructors(self):
instructors = get_instructors("LMS Course", self.course.name)
self.assertEqual(len(instructors), len(self.course.instructors))
self.assertEqual(instructors[0].name, "frappe@example.com")
def test_get_average_rating(self):
average_rating = get_average_rating(self.course.name)
self.assertEqual(average_rating, 4.5)
def add_rating(self, course_name, member, rating, review):
frappe.session.user = member
review = frappe.new_doc("LMS Course Review")
review.course = course_name
review.rating = rating
review.review = review
review.save()
frappe.session.user = "Administrator"
def test_get_reviews(self):
reviews = get_reviews(self.course.name)
self.assertEqual(len(reviews), 2)
for review in reviews:
if review.rating == 0.8:
self.assertEqual(review.member, self.student1.email)
self.assertEqual(review.review, "Good course")
elif review.rating == 1:
self.assertEqual(review.member, self.student2.email)
self.assertEqual(review.review, "Excellent course")
def test_get_lesson_index(self):
lessons = get_lessons(self.course.name)
for lesson in lessons:
self.assertEqual(get_lesson_index(lesson.name), lesson.number)
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}"
self.assertEqual(get_lesson_url(self.course.name, lesson.number), expected_url)
def test_is_instructor(self):
frappe.session.user = "frappe@example.com"
self.assertTrue(is_instructor(self.course.name))
frappe.session.user = "Administrator"
self.assertFalse(is_instructor(self.course.name))
def test_has_course_instructor_role(self):
self.assertIsNotNone(has_course_instructor_role("frappe@example.com"))
self.assertIsNone(has_course_instructor_role("student1@example.com"))
def test_has_moderator_role(self):
self.assertIsNotNone(has_moderator_role("frappe@example.com"))
self.assertIsNone(has_moderator_role("student2@example.com"))
def test_has_evaluator_role(self):
self.assertIsNotNone(has_evaluator_role("frappe@example.com"))
self.assertIsNone(has_evaluator_role("student2@example.com"))
def test_has_student_role(self):
self.assertIsNotNone(has_student_role("student1@example.com"))
self.assertIsNotNone(has_student_role("student2@example.com"))
def test_is_certified(self):
frappe.session.user = self.student1.email
self.assertIsNotNone(is_certified(self.course.name))
frappe.session.user = self.student2.email
self.assertIsNone(is_certified(self.course.name))
frappe.session.user = "Administrator"
def test_rating_validation(self):
student3 = self.create_user("student3@example.com", "Emily", "Cooper", ["LMS Student"])
with self.assertRaises(frappe.exceptions.ValidationError):
self.add_rating(self.course.name, student3.email, -0.5, "Bad course")
frappe.session.user = "Administrator"
frappe.delete_doc("User", student3.email)
def tearDown(self):
if frappe.db.exists("LMS Course", self.course.name):
frappe.db.delete("LMS Certificate", {"course": self.course.name})
frappe.db.delete("LMS Enrollment", {"course": self.course.name})
frappe.db.delete("LMS Course Review", {"course": self.course.name})
frappe.db.delete("Course Lesson", {"course": self.course.name})
frappe.db.delete("Course Chapter", {"course": self.course.name})
frappe.db.delete("Course Instructor", {"parent": self.course.name})
frappe.delete_doc("LMS Course", self.course.name)
frappe.delete_doc("User", "student1@example.com")
frappe.delete_doc("User", "student2@example.com")
frappe.delete_doc("User", "frappe@example.com")

View File

@@ -1,8 +1,6 @@
import hashlib
import json
import re
import string
from datetime import timedelta
import frappe
import requests
@@ -14,7 +12,6 @@ from frappe.rate_limiter import rate_limit
from frappe.utils import (
add_months,
cint,
cstr,
flt,
fmt_money,
format_datetime,
@@ -27,7 +24,7 @@ from frappe.utils import (
rounded,
)
from lms.lms.md import find_macros, markdown_to_html
from lms.lms.md import find_macros
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
@@ -83,6 +80,7 @@ def get_membership(course, member=None):
"current_lesson",
"progress",
"member",
"course",
"purchased_certificate",
"certificate",
],
@@ -150,11 +148,12 @@ def get_lesson_details(chapter, progress=False):
"file_type",
"instructor_notes",
"course",
"chapter",
"content",
],
as_dict=True,
)
lesson_details.number = f"{chapter.idx}.{row.idx}"
lesson_details.number = f"{chapter.idx}-{row.idx}"
lesson_details.icon = get_lesson_icon(lesson_details.body, lesson_details.content)
if progress:
@@ -228,16 +227,6 @@ def get_instructors(doctype, docname):
return instructor_details
def get_students(course, batch=None):
"""Returns (email, full_name, username) of all the students of this batch as a list of dict."""
filters = {"course": course, "member_type": "Student"}
if batch:
filters["batch_old"] = batch
return frappe.get_all("LMS Enrollment", filters, ["member"])
def get_average_rating(course):
ratings = [review.rating for review in get_reviews(course)]
if not len(ratings):
@@ -269,29 +258,6 @@ def get_reviews(course):
return reviews
def get_sorted_reviews(course):
rating_count = rating_percent = frappe._dict()
keys = ["5.0", "4.0", "3.0", "2.0", "1.0"]
for key in keys:
rating_count[key] = 0
reviews = get_reviews(course)
for review in reviews:
rating_count[cstr(review.rating)] += 1
for key in keys:
rating_percent[key] = rating_count[key] / len(reviews) * 100
return rating_percent
def is_certified(course):
certificate = frappe.get_all("LMS Certificate", {"member": frappe.session.user, "course": course})
if len(certificate):
return certificate[0].name
return
def get_lesson_index(lesson_name):
"""Returns the {chapter_index}.{lesson_index} for the lesson."""
lesson = frappe.db.get_value("Lesson Reference", {"lesson": lesson_name}, ["idx", "parent"], as_dict=True)
@@ -311,14 +277,6 @@ def get_lesson_url(course, lesson_number):
return f"/lms/courses/{course}/learn/{lesson_number}"
def get_batch(course, batch_name):
return frappe.get_all("LMS Batch Old", {"name": batch_name, "course": course})
def get_slugified_chapter_title(chapter):
return slugify(chapter)
def get_progress(course, lesson, member=None):
if not member:
member = frappe.session.user
@@ -330,52 +288,6 @@ def get_progress(course, lesson, member=None):
)
def render_html(lesson):
youtube = lesson.youtube
quiz_id = lesson.quiz_id
body = lesson.body
if youtube and "/" in youtube:
youtube = youtube.split("/")[-1]
quiz_id = "{{ Quiz('" + quiz_id + "') }}" if quiz_id else ""
youtube = "{{ YouTubeVideo('" + youtube + "') }}" if youtube else ""
text = youtube + body + quiz_id
if lesson.question:
assignment = "{{ Assignment('" + lesson.question + "-" + lesson.file_type + "') }}"
text = text + assignment
return markdown_to_html(text)
def is_mentor(course, email):
"""Checks if given user is a mentor for this course."""
if not email:
return False
return frappe.db.count("LMS Course Mentor Mapping", {"course": course, "mentor": email})
def get_mentors(course):
"""Returns the list of all mentors for this course."""
course_mentors = []
mentors = frappe.get_all("LMS Course Mentor Mapping", {"course": course}, ["mentor"])
for mentor in mentors:
member = frappe.db.get_value("User", mentor.mentor, ["name", "username", "full_name", "user_image"])
member.batch_count = frappe.db.count(
"LMS Enrollment", {"member": member.name, "member_type": "Mentor"}
)
course_mentors.append(member)
return course_mentors
def is_eligible_to_review(course):
"""Checks if user is eligible to review the course"""
if frappe.db.count("LMS Course Review", {"course": course, "owner": frappe.session.user}):
return False
return True
def get_course_progress(course, member=None):
"""Returns the course progress of the session user"""
lesson_count = get_lessons(course, get_details=False)
@@ -389,20 +301,6 @@ def get_course_progress(course, member=None):
return flt(((completed_lessons / lesson_count) * 100), precision)
def get_initial_members(course):
members = frappe.get_all("LMS Enrollment", {"course": course}, ["member"], limit=3)
member_details = []
for member in members:
member_details.append(
frappe.db.get_value(
"User", member.member, ["name", "username", "full_name", "user_image"], as_dict=True
)
)
return member_details
def is_instructor(course):
instructors = get_instructors("LMS Course", course)
for instructor in instructors:
@@ -411,57 +309,6 @@ def is_instructor(course):
return False
def convert_number_to_character(number):
return string.ascii_uppercase[number]
def get_signup_optin_checks():
mapper = frappe._dict(
{
"terms_of_use": {"page_name": "terms_page", "title": _("Terms of Use")},
"privacy_policy": {"page_name": "privacy_policy_page", "title": _("Privacy Policy")},
"cookie_policy": {"page_name": "cookie_policy_page", "title": _("Cookie Policy")},
}
)
checks = ["terms_of_use", "privacy_policy", "cookie_policy"]
links = []
for check in checks:
if frappe.db.get_single_value("LMS Settings", check):
page = frappe.db.get_single_value("LMS Settings", mapper[check].get("page_name"))
route = frappe.db.get_value("Web Page", page, "route")
links.append("<a href='/" + route + "'>" + mapper[check].get("title") + "</a>")
return (", ").join(links)
def format_amount(amount, currency):
amount_reduced = amount / 1000
if amount_reduced < 1:
return fmt_money(amount, 0, currency)
precision = 0 if amount % 1000 == 0 else 1
return _("{0}k").format(fmt_money(amount_reduced, precision, currency))
def format_number(number):
number_reduced = number / 1000
if number_reduced < 1:
return number
return f"{frappe.utils.flt(number_reduced, 1)}k"
def first_lesson_exists(course):
first_chapter = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": 1}, "name")
if not first_chapter:
return False
first_lesson = frappe.db.get_value("Lesson Reference", {"parent": first_chapter, "idx": 1}, "name")
if not first_lesson:
return False
return True
def has_course_instructor_role(member=None):
return frappe.db.get_value(
"Has Role",
@@ -470,33 +317,6 @@ def has_course_instructor_role(member=None):
)
def can_create_courses(course, member=None):
if not member:
member = frappe.session.user
instructors = frappe.get_all(
"Course Instructor",
{
"parent": course,
},
pluck="instructor",
)
if frappe.session.user == "Guest":
return False
if has_moderator_role(member):
return True
if has_course_instructor_role(member) and member in instructors:
return True
if not course and has_course_instructor_role(member):
return True
return False
def can_create_batches(member=None):
if not member:
member = frappe.session.user
@@ -702,44 +522,6 @@ def get_lesson_count(course):
return lesson_count
def get_all_memberships(member):
return frappe.get_all(
"LMS Enrollment",
{"member": member},
["name", "course", "batch_old", "current_lesson", "member_type", "progress"],
)
def get_filtered_membership(course, memberships):
current_membership = list(filter(lambda x: x.course == course, memberships))
return current_membership[0] if len(current_membership) else None
def show_start_learing_cta(course, membership):
if course.disable_self_learning or course.upcoming:
return False
if is_instructor(course.name):
return False
if course.status != "Approved":
return False
if not has_lessons(course):
return False
if not membership:
return True
def has_lessons(course):
lesson_exists = False
chapter_exists = frappe.db.get_value(
"Chapter Reference", {"parent": course.name}, ["name", "chapter"], as_dict=True
)
if chapter_exists:
lesson_exists = frappe.db.exists("Lesson Reference", {"parent": chapter_exists.chapter})
return lesson_exists
@frappe.whitelist(allow_guest=True)
@rate_limit(limit=500, seconds=60 * 60)
def get_chart_data(
@@ -763,7 +545,6 @@ def get_chart_data(
value_field = chart.value_based_on or "1"
filters = [([chart.document_type, "docstatus", "<", 2])]
print(chart.filters_json)
filters = filters + json.loads(chart.filters_json)
filters.append([doctype, datefield, ">=", from_date])
filters.append([doctype, datefield, "<=", to_date])
@@ -801,43 +582,6 @@ def get_course_completion_data():
]
def get_telemetry_boot_info():
POSTHOG_PROJECT_FIELD = "posthog_project_id"
POSTHOG_HOST_FIELD = "posthog_host"
if not frappe.conf.get(POSTHOG_HOST_FIELD) or not frappe.conf.get(POSTHOG_PROJECT_FIELD):
return {}
return {
"posthog_host": frappe.conf.get(POSTHOG_HOST_FIELD),
"posthog_project_id": frappe.conf.get(POSTHOG_PROJECT_FIELD),
"enable_telemetry": 1,
}
@frappe.whitelist()
def is_onboarding_complete():
if not has_moderator_role():
return {"is_onboarded": True}
course_created = frappe.db.a_row_exists("LMS Course")
chapter_created = frappe.db.a_row_exists("Course Chapter")
lesson_created = frappe.db.a_row_exists("Course Lesson")
if course_created and chapter_created and lesson_created:
frappe.db.set_single_value("LMS Settings", "is_onboarding_complete", 1)
return {
"is_onboarded": frappe.db.get_single_value("LMS Settings", "is_onboarding_complete"),
"course_created": course_created,
"chapter_created": chapter_created,
"lesson_created": lesson_created,
"first_course": frappe.get_all("LMS Course", limit=1, order_by=None, pluck="name")[0]
if course_created
else None,
}
def get_evaluator(course, batch=None):
evaluator = None
if batch:
@@ -958,13 +702,6 @@ def get_current_exchange_rate(source, target="USD"):
return details["rates"][target]
@frappe.whitelist()
def change_currency(amount, currency, country=None):
amount = cint(amount)
amount, currency = check_multicurrency(amount, currency, country)
return fmt_money(amount, 0, currency)
@frappe.whitelist(allow_guest=True)
@rate_limit(limit=500, seconds=60 * 60)
def get_courses(filters=None, start=0):
@@ -1085,6 +822,7 @@ def get_course_fields():
"title",
"tags",
"image",
"video_link",
"card_gradient",
"short_introduction",
"published",
@@ -1109,35 +847,11 @@ def get_course_fields():
@frappe.whitelist(allow_guest=True)
@rate_limit(limit=500, seconds=60 * 60)
def get_course_details(course):
fields = get_course_fields()
course_details = frappe.db.get_value(
"LMS Course",
course,
[
"name",
"title",
"tags",
"description",
"image",
"video_link",
"short_introduction",
"published",
"upcoming",
"featured",
"disable_self_learning",
"published_on",
"category",
"status",
"paid_course",
"paid_certificate",
"course_price",
"currency",
"amount_usd",
"enable_certification",
"lessons",
"enrollments",
"rating",
"card_gradient",
],
fields,
as_dict=1,
)
@@ -2351,343 +2065,6 @@ def persona_captured():
frappe.db.set_single_value("LMS Settings", "persona_captured", 1)
@frappe.whitelist()
def get_my_courses():
my_courses = []
if frappe.session.user == "Guest":
return my_courses
courses = get_my_latest_courses()
if not len(courses):
courses = get_featured_home_courses()
if not len(courses):
courses = get_popular_courses()
for course in courses:
my_courses.append(get_course_details(course))
return my_courses
def get_my_latest_courses():
return frappe.get_all(
"LMS Enrollment",
{
"member": frappe.session.user,
},
order_by="modified desc",
limit=3,
pluck="course",
)
def get_featured_home_courses():
return frappe.get_all(
"LMS Course",
{"published": 1, "featured": 1},
order_by="published_on desc",
limit=3,
pluck="name",
)
def get_popular_courses():
return frappe.get_all(
"LMS Course",
{
"published": 1,
},
order_by="enrollments desc",
limit=3,
pluck="name",
)
@frappe.whitelist()
def get_my_batches():
my_batches = []
if frappe.session.user == "Guest":
return my_batches
batches = get_my_latest_batches()
if not len(batches):
batches = get_upcoming_batches()
for batch in batches:
batch_details = get_batch_details(batch)
if batch_details:
my_batches.append(batch_details)
return my_batches
def get_my_latest_batches():
return frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
limit=4,
pluck="batch",
)
def get_upcoming_batches():
return frappe.get_all(
"LMS Batch",
{
"published": 1,
"start_date": [">=", getdate()],
},
order_by="start_date asc",
limit=4,
pluck="name",
)
@frappe.whitelist()
def get_my_live_classes():
my_live_classes = []
if frappe.session.user == "Guest":
return my_live_classes
batches = frappe.get_all(
"LMS Batch Enrollment",
{
"member": frappe.session.user,
},
order_by="creation desc",
pluck="batch",
)
live_class_details = frappe.get_all(
"LMS Live Class",
filters={
"date": [">=", getdate()],
"batch_name": ["in", batches],
},
fields=[
"name",
"title",
"description",
"time",
"date",
"duration",
"attendees",
"start_url",
"join_url",
"owner",
],
limit=2,
order_by="date",
)
if len(live_class_details):
for live_class in live_class_details:
live_class.course_title = frappe.db.get_value("LMS Course", live_class.course, "title")
my_live_classes.append(live_class)
return my_live_classes
@frappe.whitelist()
def get_created_courses():
created_courses = []
if frappe.session.user == "Guest":
return created_courses
CourseInstructor = frappe.qb.DocType("Course Instructor")
Course = frappe.qb.DocType("LMS Course")
query = (
frappe.qb.from_(CourseInstructor)
.join(Course)
.on(CourseInstructor.parent == Course.name)
.select(Course.name)
.where(CourseInstructor.instructor == frappe.session.user)
.orderby(Course.published_on, order=frappe.qb.desc)
.limit(3)
)
results = query.run(as_dict=True)
courses = [row["name"] for row in results]
for course in courses:
course_details = get_course_details(course)
created_courses.append(course_details)
return created_courses
@frappe.whitelist()
def get_created_batches():
created_batches = []
if frappe.session.user == "Guest":
return created_batches
CourseInstructor = frappe.qb.DocType("Course Instructor")
Batch = frappe.qb.DocType("LMS Batch")
query = (
frappe.qb.from_(CourseInstructor)
.join(Batch)
.on(CourseInstructor.parent == Batch.name)
.select(Batch.name)
.where(CourseInstructor.instructor == frappe.session.user)
.where(Batch.start_date >= getdate())
.orderby(Batch.start_date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
batches = [row["name"] for row in results]
for batch in batches:
batch_details = get_batch_details(batch)
created_batches.append(batch_details)
return created_batches
@frappe.whitelist()
def get_admin_live_classes():
if frappe.session.user == "Guest":
return []
CourseInstructor = frappe.qb.DocType("Course Instructor")
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
query = (
frappe.qb.from_(CourseInstructor)
.join(LMSLiveClass)
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
.select(
LMSLiveClass.name,
LMSLiveClass.title,
LMSLiveClass.description,
LMSLiveClass.time,
LMSLiveClass.date,
LMSLiveClass.duration,
LMSLiveClass.attendees,
LMSLiveClass.start_url,
LMSLiveClass.join_url,
LMSLiveClass.owner,
)
.where(CourseInstructor.instructor == frappe.session.user)
.where(LMSLiveClass.date >= getdate())
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
.limit(4)
)
results = query.run(as_dict=True)
return results
@frappe.whitelist()
def get_admin_evals():
if frappe.session.user == "Guest":
return []
evals = frappe.get_all(
"LMS Certificate Request",
{
"evaluator": frappe.session.user,
"date": [">=", getdate()],
},
[
"name",
"date",
"start_time",
"course",
"evaluator",
"google_meet_link",
"member",
"member_name",
],
limit=4,
order_by="date asc",
)
for evaluation in evals:
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
return evals
def fetch_activity_dates(user):
doctypes = [
"LMS Course Progress",
"LMS Quiz Submission",
"LMS Assignment Submission",
"LMS Programming Exercise Submission",
]
all_dates = []
for dt in doctypes:
all_dates.extend(frappe.get_all(dt, {"member": user}, pluck="creation"))
return sorted({d.date() if hasattr(d, "date") else d for d in all_dates})
def calculate_streaks(all_dates):
streak = 0
longest_streak = 0
prev_day = None
for d in all_dates:
if d.weekday() in (5, 6):
continue
if prev_day:
expected = prev_day + timedelta(days=1)
while expected.weekday() in (5, 6):
expected += timedelta(days=1)
streak = streak + 1 if d == expected else 1
else:
streak = 1
longest_streak = max(longest_streak, streak)
prev_day = d
return streak, longest_streak
def calculate_current_streak(all_dates, streak):
if not all_dates:
return 0
last_date = all_dates[-1]
today = getdate()
ref_day = today
while ref_day.weekday() in (5, 6):
ref_day -= timedelta(days=1)
if last_date == ref_day or last_date == ref_day - timedelta(days=1):
return streak
return 0
@frappe.whitelist()
def get_streak_info():
if frappe.session.user == "Guest":
return {}
all_dates = fetch_activity_dates(frappe.session.user)
streak, longest_streak = calculate_streaks(all_dates)
current_streak = calculate_current_streak(all_dates, streak)
return {
"current_streak": current_streak,
"longest_streak": longest_streak,
}
def validate_discussion_reply(doc, method):
topic = frappe.db.get_value(
"Discussion Topic", doc.topic, ["reference_doctype", "reference_docname"], as_dict=True

View File

@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-05 16:04+0000\n"
"PO-Revision-Date: 2025-12-09 18:55\n"
"PO-Revision-Date: 2025-12-13 20:35\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Hungarian\n"
"MIME-Version: 1.0\n"
@@ -1514,7 +1514,7 @@ msgstr ""
#. Label of the confirmation_email_template (Link) field in DocType 'LMS Batch'
#: lms/lms/doctype/lms_batch/lms_batch.json
msgid "Confirmation Email Template"
msgstr ""
msgstr "Megerősítő E-mail Sablon"
#: lms/lms/doctype/lms_certificate/lms_certificate.py:30
msgid "Congratulations on getting certified!"
@@ -4476,7 +4476,7 @@ msgstr ""
#: frontend/src/pages/BatchForm.vue:292 frontend/src/pages/CourseForm.vue:298
msgid "Meta Tags"
msgstr ""
msgstr "Meta Címkék"
#: lms/lms/api.py:1510
msgid "Meta tags should be a list."
@@ -4994,7 +4994,7 @@ msgstr ""
#. Label of the output (Data) field in DocType 'LMS Test Case Submission'
#: lms/lms/doctype/lms_test_case_submission/lms_test_case_submission.json
msgid "Output"
msgstr ""
msgstr "Kimenet"
#: frontend/src/components/Settings/BadgeForm.vue:216
#: lms/lms/doctype/lms_badge/lms_badge.js:37
@@ -5213,7 +5213,7 @@ msgstr ""
#: lms/lms/doctype/lms_coupon/lms_coupon.json
#: lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json
msgid "Percentage"
msgstr ""
msgstr "Százalék"
#. Option for the 'Grade Type' (Select) field in DocType 'Education Detail'
#: lms/lms/doctype/education_detail/education_detail.json
@@ -5857,7 +5857,7 @@ msgstr ""
#: lms/lms/doctype/lms_course/lms_course.json
#: lms/lms/doctype/lms_lesson_note/lms_lesson_note.json
msgid "Red"
msgstr ""
msgstr "Piros"
#. Label of the redemption_count (Int) field in DocType 'LMS Coupon'
#: frontend/src/components/Settings/Coupons/CouponList.vue:189
@@ -5873,7 +5873,7 @@ msgstr ""
#. Timetable'
#: lms/lms/doctype/lms_batch_timetable/lms_batch_timetable.json
msgid "Reference DocName"
msgstr ""
msgstr "Hivatkozás DocNév"
#. Label of the reference_doctype (Link) field in DocType 'LMS Batch Timetable'
#. Label of the reference_doctype (Select) field in DocType 'LMS Coupon Item'
@@ -6532,7 +6532,7 @@ msgstr ""
#: frontend/src/pages/Billing.vue:118
msgid "State/Province"
msgstr ""
msgstr "Állam / Megye"
#. Label of the tab_4_tab (Tab Break) field in DocType 'LMS Course'
#. Label of the statistics (Check) field in DocType 'LMS Settings'
@@ -6853,7 +6853,7 @@ msgstr ""
#: lms/lms/doctype/lms_assignment/lms_assignment.json
#: lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
msgid "Text"
msgstr ""
msgstr "Szöveg"
#: frontend/src/components/BatchFeedback.vue:6
msgid "Thank you for providing your feedback."
@@ -7396,12 +7396,12 @@ msgstr ""
#. Label of the user_field (Select) field in DocType 'LMS Badge'
#: lms/lms/doctype/lms_badge/lms_badge.json
msgid "User Field"
msgstr ""
msgstr "Felhasználói Mező"
#. Label of the user_image (Attach Image) field in DocType 'Course Evaluator'
#: lms/lms/doctype/course_evaluator/course_evaluator.json
msgid "User Image"
msgstr ""
msgstr "Profilkép"
#. Option for the 'Type' (Select) field in DocType 'LMS Question'
#. Option for the 'Type' (Select) field in DocType 'LMS Quiz Question'
@@ -7459,7 +7459,7 @@ msgstr ""
#: frontend/src/pages/Notifications.vue:39
msgid "View"
msgstr ""
msgstr "Nézet"
#: frontend/src/pages/JobDetail.vue:31
msgid "View Applications"
@@ -7958,7 +7958,7 @@ msgstr ""
#: frontend/src/pages/CertifiedParticipants.vue:79
msgid "certificate"
msgstr ""
msgstr "tanúsítvány"
#: frontend/src/pages/CertifiedParticipants.vue:78
msgid "certificates"

File diff suppressed because it is too large Load Diff

137
lms/sqlite.py Normal file
View File

@@ -0,0 +1,137 @@
from contextlib import suppress
import frappe
from frappe.search.sqlite_search import SQLiteSearch, SQLiteSearchIndexMissingError
from frappe.utils import nowdate
class LearningSearch(SQLiteSearch):
INDEX_NAME = "learning.db"
INDEX_SCHEMA = {
"metadata_fields": [
"owner",
"published",
"published_on",
"start_date",
"status",
"company_name",
"creation",
],
"tokenizer": "unicode61 remove_diacritics 2 tokenchars '-_'",
}
INDEXABLE_DOCTYPES = {
"LMS Course": {
"fields": [
"name",
"title",
{"content": "description"},
"short_introduction",
"published",
"category",
"owner",
{"modified": "published_on"},
],
},
"LMS Batch": {
"fields": [
"name",
"title",
"description",
{"content": "batch_details"},
"published",
"category",
"owner",
{"modified": "start_date"},
],
},
"Job Opportunity": {
"fields": [
"name",
{"title": "job_title"},
{"content": "description"},
"owner",
"location",
"country",
"company_name",
"status",
"creation",
{"modified": "creation"},
],
},
}
DOCTYPE_FIELDS = {
"LMS Course": [
"name",
"title",
"description",
"short_introduction",
"category",
"creation",
"modified",
"owner",
],
"LMS Batch": [
"name",
"title",
"description",
"batch_details",
"category",
"creation",
"modified",
"owner",
],
"Job Opportunity": [
"name",
"job_title",
"company_name",
"description",
"creation",
"modified",
"owner",
],
}
def build_index(self):
try:
super().build_index()
except Exception as e:
frappe.throw(e)
def get_search_filters(self):
return {}
@SQLiteSearch.scoring_function
def get_doctype_boost(self, row, query, query_words):
doctype = row["doctype"]
if doctype == "LMS Course":
if row["published"]:
return 1.3
elif doctype == "LMS Batch":
if row["published"] and row["start_date"] >= nowdate():
return 1.3
elif row["published"]:
return 1.2
return 1.0
class LearningSearchIndexMissingError(SQLiteSearchIndexMissingError):
pass
def build_index():
search = LearningSearch()
search.build_index()
def build_index_in_background():
if not frappe.cache().get_value("learning_search_indexing_in_progress"):
frappe.enqueue(build_index, queue="long")
def build_index_if_not_exists():
search = LearningSearch()
if not search.index_exists():
build_index()

View File

@@ -1,168 +0,0 @@
<script type="text/javascript" src="/assets/frappe/node_modules/moment/min/moment-with-locales.min.js"></script>
<script type="text/javascript" src="/assets/frappe/node_modules/moment-timezone/builds/moment-timezone-with-data.min.js"></script>
<script type="text/javascript" src="/assets/frappe/js/frappe/utils/datetime.js"></script>
<script type="text/javascript">
// comment_when is failing because of this
if (!frappe.sys_defaults) {
frappe.sys_defaults = {}
}
</script>
<script type="text/javascript" src="{{ livecode_url }}/static/livecode.js"></script>
<script type="text/javascript" src="/assets/mon_school/js/livecode-files.js"></script>
<template id="livecode-template">
<div class="livecode-editor livecode-editor-inline">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="controls">
<button class="run">{{ _("Run") }}</button>
<div class="exercise-controls pull-right">
<span style="padding-right: 10px;"><span class="last-submitted human-time" data-timestamp=""></span></span>
<button class="submit btn-primary">{{ _("Submit") }}</button>
</div>
</div>
</div>
</div>
<div class="code-editor">
<div class="row">
<div class="col-lg-8 col-md-6">
<div class="code-wrapper">
<textarea class="code"></textarea>
</div>
</div>
<div class="col-lg-4 col-md-6 canvas-wrapper">
<div class="svg-image" width="300" height="300"></div>
<pre class="output"></pre>
</div>
</div>
</div>
</div>
</template>
<script type="text/javascript">
function getLiveCodeOptions() {
return {
base_url: "{{ livecode_url }}",
runtime: "python",
files: LIVECODE_FILES, // loaded from livecode-files.js
command: ["python", "start.py"],
codemirror: true,
onMessage: {
image: function(editor, msg) {
const element = editor.parent.querySelector(".svg-image");
element.innerHTML = msg.image;
}
}
}
}
$(function() {
var editorLookup = {};
$("pre.example, pre.exercise").each((i, e) => {
var code = $(e).text();
var template = document.querySelector('#livecode-template');
var clone = template.content.cloneNode(true);
$(e)
.wrap('<div></div>')
.hide()
.parent()
.append(clone)
.find("textarea.code")
.val(code);
if ($(e).hasClass("exercise")) {
var last_submitted = $(e).data("last-submitted");
if (last_submitted) {
$(e).parent().find(".last-submitted")
.data("timestamp", last_submitted)
.html(__("Submitted {0}", [comment_when(last_submitted)]));
}
}
else {
$(e).parent().find(".exercise-controls").remove();
}
var editor = new LiveCodeEditor(e.parentElement, {
...getLiveCodeOptions(),
codemirror: true,
onMessage: {
image: function(editor, msg) {
const canvasElement = editor.parent.querySelector("div.svg-image");
canvasElement.innerHTML = msg.image;
}
}
});
$(e).parent().find(".submit").on('click', function() {
var name = $(e).data("name");
let code = editor.codemirror.doc.getValue();
frappe.call("lms.lms.api.submit_solution", {
"exercise": name,
"code": code
}).then(r => {
if (r.message.name) {
frappe.msgprint("Submitted successfully!");
let d = r.message.creation;
$(e).parent().find(".human-time").html(__("Submitted {0}", [comment_when(d)]));
}
});
});
});
$(".exercise-image").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).html(svg);
});
$("pre.exercise").each((i, e) => {
var svg = JSON.parse($(e).data("image"));
$(e).parent().find(".svg-image").html(svg);
});
});
</script>
<style type="text/css">
.svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 310px;
height: 310px;
}
.livecode-editor {
margin-bottom: 30px;
}
.livecode-editor-small .svg-image {
border: 5px solid #ddd;
position: relative;
z-index: 0;
width: 210px;
height: 210px;
}
/* work-in-progress styles for showing admonition */
.admonition {
border: 1px solid #aaa;
border-left: .5rem solid #888;
border-radius: .3em;
font-size: 0.9em;
margin: 1.5em 0;
padding: 0 0.5em;
}
.admonition-title {
padding: 0.5em 0px;
font-weight: bold;
padding-top:
}
</style>

View File

@@ -1,8 +0,0 @@
<link rel="stylesheet" href="{{ livecode_url }}/static/codemirror/lib/codemirror.css">
<script src="{{ livecode_url }}/static/codemirror/lib/codemirror.js"></script>
<script src="{{ livecode_url }}/static/codemirror/mode/python/python.js"></script>
<script src="{{ livecode_url }}/static/codemirror/keymap/sublime.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/edit/matchbrackets.js"></script>
<script src="{{ livecode_url }}/static/codemirror/addon/comment/comment.js"></script>

View File

@@ -1,167 +0,0 @@
{% if not course.upcoming %}
<div class="reviews-parent">
{% set reviews = get_reviews(course.name) %}
<div class="page-title mb-5"> {{ _("Reviews") }} </div>
{% if avg_rating %}
<div class="reviews-header">
<div class="text-center">
<div class="avg-rating">
{{ frappe.utils.flt(avg_rating, frappe.get_system_settings("float_precision") or 3) }}
</div>
<div class="avg-rating-stars">
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-lg {% if i <= frappe.utils.ceil(avg_rating) %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
<div class="course-meta"> {{ reviews | length }} {{ _("ratings") }} </div>
<!--
-->
<div class="mt-5">
{% include "lms/templates/reviews_cta.html" %}
</div>
</div>
<div class="vertical-divider"></div>
{% set sorted_reviews = get_sorted_reviews(course.name) %}
<div>
{% for review in sorted_reviews %}
<div class="d-flex align-items-center mb-3">
<div class="course-meta mr-2">
{{ frappe.utils.cint(review) }} {{ _("stars") }}
</div>
<div class="progress">
<div class="progress-bar" role="progressbar" aria-valuenow="{{ sorted_reviews[review] }}"
aria-valuemin="0" aria-valuemax="100" style="width:{{ sorted_reviews[review] }}%">
<span class="sr-only"> {{ sorted_reviews[review] }} {{ _("Complete") }} </span>
</div>
</div>
<div class="course-meta ml-3"> {{ frappe.utils.cint(sorted_reviews[review]) }}% </div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% if reviews | length %}
<div class="mt-12">
{% for review in reviews %}
<div class="mb-4">
<div class="d-flex align-items-center">
<div class="mr-4">
{{ widgets.Avatar(member=review.owner_details, avatar_class="avatar-medium") }}
</div>
<div>
<div class="d-flex align-items-center">
<a class="button-links mr-4" href="/lms/users/{{ review.owner_details.username }}">
<span class="bold-heading">
{{ review.owner_details.full_name }}
</span>
</a>
<div class="frappe-timestamp course-meta" data-timestamp="{{ review.creation }}">
{{ review.creation }}
</div>
</div>
<div class="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md {% if i <= review.rating %} star-click {% endif %}" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
<div class="review-content"> {{ review.review }} </div>
</div>
{% if loop.index != reviews | length %}
<div class="card-divider"></div>
{% endif %}
{% endfor %}
</div>
{% else %}
<div class="empty-state">
<img class="icon icon-xl" src="/assets/lms/icons/comment.svg">
<div class="empty-state-text">
<div class="empty-state-heading">{{ _("Review the course") }}</div>
<div class="course-meta">{{ _("Help us improve our course material.") }}</div>
<div class="mt-2">
{% include "lms/templates/reviews_cta.html" %}
</div>
</div>
</div>
{% endif %}
</div>
<div class="modal fade review-modal" id="review-modal" tabindex="-1" role="dialog"
aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">{{ _("Write a Review") }}</div>
</div>
<div class="modal-body">
<form class="review-form" id="review-form">
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Rating") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<div class="rating rating-field" id="rating">
{% for i in [1, 2, 3, 4, 5] %}
<svg class="icon icon-md icon-rating" data-rating="{{ i }}">
<use href="#icon-star"></use>
</svg>
{% endfor %}
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="clearfix">
<label class="control-label reqd" style="padding-right: 0px;">{{ _("Review") }}</label>
</div>
<div class="control-input-wrapper">
<div class="control-input">
<textarea type="text" autocomplete="off" class="input-with-feedback form-control review-field"
data-fieldtype="Text" data-fieldname="feedback_comments" spellcheck="false"></textarea>
</div>
</div>
</div>
<p class="error-field muted-text"></p>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-secondary btn-sm mr-2" data-dismiss="modal" aria-label="Close">
{{ _("Discard") }}
</button>
<button class="btn btn-primary btn-sm" data-course="{{ course.name | urlencode}}" id="submit-review">
{{ _("Submit") }}
</button>
</div>
</div>
</div>
</div>
{% endif %}

View File

@@ -1,9 +0,0 @@
{% if membership and is_eligible_to_review(course.name) %}
<span class="btn btn-secondary btn-sm review-link">
{{ _("Write a review") }}
</span>
{% elif not is_instructor and frappe.session.user == "Guest" %}
<a class="btn btn-secondary btn-sm" href="/login?redirect-to=/courses/{{ course.name }}">
{{ _("Write a review") }}
</a>
{% endif %}