diff --git a/lms/lms/doctype/lms_assignment/lms_assignment.json b/lms/lms/doctype/lms_assignment/lms_assignment.json
index 06fa85c6..f9abc409 100644
--- a/lms/lms/doctype/lms_assignment/lms_assignment.json
+++ b/lms/lms/doctype/lms_assignment/lms_assignment.json
@@ -80,8 +80,13 @@
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2026-02-03 10:55:14.821486",
+ "links": [
+ {
+ "link_doctype": "LMS Assignment Submission",
+ "link_fieldname": "assignment"
+ }
+ ],
+ "modified": "2026-02-05 11:37:36.492016",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Assignment",
diff --git a/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
index c03bbd5c..485cef55 100644
--- a/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
+++ b/lms/lms/doctype/lms_assignment_submission/lms_assignment_submission.json
@@ -42,7 +42,8 @@
"fieldname": "assignment",
"fieldtype": "Link",
"label": "Assignment",
- "options": "LMS Assignment"
+ "options": "LMS Assignment",
+ "reqd": 1
},
{
"fieldname": "member",
@@ -150,7 +151,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2025-12-17 14:47:22.944223",
+ "modified": "2026-02-05 11:38:03.792865",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Assignment Submission",
diff --git a/lms/lms/test_api.py b/lms/lms/test_api.py
new file mode 100644
index 00000000..28700217
--- /dev/null
+++ b/lms/lms/test_api.py
@@ -0,0 +1,63 @@
+import frappe
+
+from lms.lms.api import get_certified_participants, get_course_assessment_progress
+from lms.lms.test_helpers import BaseTestUtils
+
+
+class TestLMSAPI(BaseTestUtils):
+ def setUp(self):
+ super().setUp()
+ self._setup_course_flow()
+
+ def test_certified_participants_with_category(self):
+ filters = {"category": "Utility Course"}
+ certified_participants = get_certified_participants(filters=filters)
+ self.assertEqual(len(certified_participants), 1)
+ self.assertEqual(certified_participants[0].member, self.student1.email)
+
+ filters = {"category": "Nonexistent Category"}
+ certified_participants_no_match = get_certified_participants(filters=filters)
+ self.assertEqual(len(certified_participants_no_match), 0)
+
+ def test_certified_participants_with_open_to_work(self):
+ filters = {"open_to_work": 1}
+ certified_participants_open_to_work = get_certified_participants(filters=filters)
+ self.assertEqual(len(certified_participants_open_to_work), 0)
+
+ frappe.db.set_value("User", self.student1.email, "open_to", "Work")
+ certified_participants_open_to_work = get_certified_participants(filters=filters)
+ self.assertEqual(len(certified_participants_open_to_work), 1)
+ frappe.db.set_value("User", self.student1.email, "open_to", "")
+
+ def test_certified_participants_with_open_to_hiring(self):
+ filters = {"hiring": 1}
+ certified_participants_hiring = get_certified_participants(filters=filters)
+ self.assertEqual(len(certified_participants_hiring), 0)
+
+ frappe.db.set_value("User", self.student1.email, "open_to", "Hiring")
+ certified_participants_hiring = get_certified_participants(filters=filters)
+ self.assertEqual(len(certified_participants_hiring), 1)
+ frappe.db.set_value("User", self.student1.email, "open_to", "")
+
+ def test_course_assessment_progress(self):
+ progress = get_course_assessment_progress(self.course.name, self.student1.name)
+ progress = frappe._dict(progress)
+
+ self.assertEqual(len(progress.quizzes), 1)
+ for quiz in progress.quizzes:
+ self.assertEqual(quiz.quiz, self.quiz.name)
+ self.assertEqual(quiz.quiz_title, self.quiz.title)
+ self.assertEqual(quiz.score, 12)
+ self.assertEqual(quiz.percentage, 80)
+
+ self.assertEqual(len(progress.assignments), 1)
+ for assignment in progress.assignments:
+ self.assertEqual(assignment.assignment, self.assignment.name)
+ self.assertEqual(assignment.assignment_title, self.assignment.title)
+ self.assertEqual(assignment.status, "Pass")
+
+ self.assertEqual(len(progress.exercises), 1)
+ for exercise in progress.exercises:
+ self.assertEqual(exercise.exercise, self.programming_exercise.name)
+ self.assertEqual(exercise.exercise_title, self.programming_exercise.title)
+ self.assertEqual(exercise.status, "Passed")
diff --git a/lms/lms/test_helpers.py b/lms/lms/test_helpers.py
index 6d6e2008..e6a2362a 100644
--- a/lms/lms/test_helpers.py
+++ b/lms/lms/test_helpers.py
@@ -19,8 +19,8 @@ class BaseTestUtils(UnitTestCase):
if frappe.db.exists(item_type, item_name):
try:
frappe.delete_doc(item_type, item_name, force=True)
- except Exception:
- pass
+ except Exception as e:
+ print(f"Error deleting {item_type} {item_name}: {e}")
def _create_user(self, email, first_name, last_name, roles, user_type="Website User"):
if frappe.db.exists("User", email):
@@ -82,18 +82,21 @@ class BaseTestUtils(UnitTestCase):
self.cleanup_items.append(("Course Chapter", chapter.name))
return chapter
- def _create_lesson(self, title, chapter, course):
+ def _create_lesson(self, title, chapter, course, content=None):
existing = frappe.db.exists("Course Lesson", {"course": course, "title": title})
if existing:
return frappe.get_doc("Course Lesson", existing)
+ if not content:
+ 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 = frappe.new_doc("Course Lesson")
lesson.update(
{
"course": course,
"chapter": chapter,
"title": title,
- "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"}',
+ "content": content,
}
)
lesson.save()
@@ -248,3 +251,289 @@ class BaseTestUtils(UnitTestCase):
certificate.save()
self.cleanup_items.append(("LMS Certificate", certificate.name))
return certificate
+
+ def _create_quiz_questions(self):
+ questions = []
+ for index in range(1, 4):
+ question = frappe.new_doc("LMS Question")
+ question.update(
+ {
+ "question": f"Utility Question {index}?",
+ "type": "Choices",
+ "option_1": "Option 1",
+ "is_correct_1": 1,
+ "option_2": "Option 2",
+ "is_correct_2": 0,
+ }
+ )
+ question.save()
+ self.cleanup_items.append(("LMS Quiz Question", question.name))
+ questions.append(question)
+ return questions
+
+ def _create_quiz(self, title="Utility Quiz"):
+ existing = frappe.db.exists("LMS Quiz", {"title": title})
+ if existing:
+ return frappe.get_doc("LMS Quiz", existing)
+
+ quiz = frappe.new_doc("LMS Quiz")
+ quiz.update(
+ {
+ "title": title,
+ "passing_percentage": 70,
+ "total_marks": 15,
+ }
+ )
+
+ for question in self.questions:
+ quiz.append(
+ "questions",
+ {
+ "question": question.name,
+ "marks": 5,
+ },
+ )
+ quiz.save()
+ self.cleanup_items.append(("LMS Quiz", quiz.name))
+ return quiz
+
+ def _create_assignment(self, title="Utility Assignment"):
+ existing = frappe.db.exists("LMS Assignment", {"title": title})
+ if existing:
+ return frappe.get_doc("LMS Assignment", existing)
+
+ assignment = frappe.new_doc("LMS Assignment")
+ assignment.update(
+ {
+ "title": title,
+ "question": "This is a utility assignment to test the assignment creation helper method.",
+ "type": "Text",
+ "grade_assignment": 1,
+ }
+ )
+ assignment.save()
+ self.cleanup_items.append(("LMS Assignment", assignment.name))
+ return assignment
+
+ def _setup_course_flow(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.course = self._create_course()
+ self._setup_quiz()
+ self._setup_assignment()
+ self._setup_programming_exercise()
+ self._setup_chapters()
+
+ self._create_enrollment(self.student1.email, self.course.name)
+ self._add_student_progress(self.student1.email, self.course.name)
+ self._create_enrollment(self.student2.email, self.course.name)
+ self._add_student_progress(self.student2.email, self.course.name)
+
+ 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 _setup_quiz(self):
+ self.questions = self._create_quiz_questions()
+ self.quiz = self._create_quiz()
+
+ def _setup_assignment(self):
+ self.assignment = self._create_assignment()
+
+ def _setup_programming_exercise(self):
+ self.programming_exercise = self._create_programming_exercise()
+
+ def _setup_chapters(self):
+ chapters = []
+ for i in range(1, 4):
+ chapter = self._create_chapter(f"Chapter {i}", self.course.name)
+ chapters.append(chapter)
+ self.course.reload()
+ for chapter in chapters:
+ if not any(c.chapter == chapter.name for c in self.course.chapters):
+ self.course.append("chapters", {"chapter": chapter.name})
+ self.course.save()
+ self._setup_lessons()
+
+ def _setup_lessons(self):
+ for index, chapter_ref in enumerate(self.course.chapters):
+ chapter_doc = frappe.get_doc("Course Chapter", chapter_ref.chapter)
+ for j in range(1, 5):
+ content = None
+ if j == 2 and index == 2:
+ content = self._get_quiz_lesson_content()
+ if j == 3 and index == 2:
+ content = self._get_assignment_lesson_content()
+ if j == 4 and index == 2:
+ content = self._get_exercise_lesson_content()
+ lesson_title = f"Lesson {j} of {chapter_ref.chapter}"
+ lesson = self._create_lesson(lesson_title, chapter_ref.chapter, self.course.name, content)
+
+ if not any(l.lesson == lesson.name for l in chapter_doc.lessons):
+ chapter_doc.append("lessons", {"lesson": lesson.name})
+
+ chapter_doc.save()
+
+ def _get_quiz_lesson_content(self):
+ return f"""{{
+ "time": 1765194986690,
+ "blocks": [
+ {{
+ "id": "dkLzbW14ds",
+ "type": "quiz",
+ "data": {{ "quiz": "{self.quiz.name}" }}
+ }}
+ ],
+ "version": "2.29.0"
+ }}"""
+
+ def _get_assignment_lesson_content(self):
+ return f"""{{
+ "time": 1765194986690,
+ "blocks": [
+ {{
+ "id": "dkLzbW14ds",
+ "type": "assignment",
+ "data": {{ "assignment": "{self.assignment.name}" }}
+ }}
+ ],
+ "version": "2.29.0"
+ }}"""
+
+ def _get_exercise_lesson_content(self):
+ return f"""{{
+ "time": 1765194986690,
+ "blocks": [
+ {{
+ "id": "dkLzbW14ds",
+ "type": "program",
+ "data": {{ "exercise": "{self.programming_exercise.name}" }}
+ }}
+ ],
+ "version": "2.29.0"
+ }}"""
+
+ def _setup_batch_flow(self):
+ self.evaluator = self._create_evaluator()
+ self.batch = self._create_batch(self.course.name)
+ self._create_batch_enrollment(self.student1.email, self.batch.name)
+ self._create_batch_enrollment(self.student2.email, self.batch.name)
+
+ def _add_student_progress(self, member, course):
+ self._create_quiz_submission(member)
+ self._create_assignment_submission(member)
+ self._create_programming_exercise_submission(member)
+ lessons = frappe.db.get_all(
+ "Course Lesson", {"course": course}, pluck="name", limit=2, order_by="creation desc"
+ )
+ for lesson in lessons:
+ self._create_lesson_progress(member, course, lesson)
+
+ def _create_lesson_progress(self, member, course, lesson):
+ existing = frappe.db.exists(
+ "LMS Course Progress", {"member": member, "course": course, "lesson": lesson}
+ )
+ if existing:
+ return frappe.get_doc("LMS Course Progress", existing)
+
+ progress = frappe.new_doc("LMS Course Progress")
+ progress.update({"member": member, "course": course, "lesson": lesson, "status": "Complete"})
+ progress.insert()
+ self.cleanup_items.append(("LMS Course Progress", progress.name))
+ return progress
+
+ def _create_quiz_submission(self, member):
+ existing = frappe.db.exists("LMS Quiz Submission", {"quiz": self.quiz.name, "member": member})
+ if existing:
+ return frappe.get_doc("LMS Quiz Submission", existing)
+ submission = frappe.new_doc("LMS Quiz Submission")
+ submission.update(
+ {
+ "quiz": self.quiz.name,
+ "member": member,
+ "score_out_of": self.quiz.total_marks,
+ "passing_percentage": self.quiz.passing_percentage,
+ }
+ )
+
+ for question in self.questions:
+ submission.append(
+ "result",
+ {
+ "question": question.name,
+ "marks": 4,
+ "marks_out_of": 5,
+ },
+ )
+
+ submission.insert()
+ self.cleanup_items.append(("LMS Quiz Submission", submission.name))
+ return submission
+
+ def _create_assignment_submission(self, member):
+ existing = frappe.db.exists(
+ "LMS Assignment Submission", {"assignment": self.assignment.name, "member": member}
+ )
+ if existing:
+ return frappe.get_doc("LMS Assignment Submission", existing)
+
+ submission = frappe.new_doc("LMS Assignment Submission")
+ submission.update(
+ {
+ "assignment": self.assignment.name,
+ "member": member,
+ "answer": "This is the submission content for the utility assignment.",
+ "status": "Pass",
+ }
+ )
+
+ submission.insert()
+ self.cleanup_items.append(("LMS Assignment Submission", submission.name))
+ return submission
+
+ def _create_programming_exercise(self, title="Utility Programming Exercise"):
+ existing = frappe.db.exists("LMS Programming Exercise", {"title": title})
+ if existing:
+ return frappe.get_doc("LMS Programming Exercise", existing)
+
+ programming_exercise = frappe.new_doc("LMS Programming Exercise")
+ programming_exercise.update(
+ {
+ "title": title,
+ "language": "Python",
+ "problem_statement": "Write a function to return the sum of two numbers.",
+ "test_cases": [
+ {"input": "2", "expected_output": "3"},
+ {"input": "11", "expected_output": "12"},
+ ],
+ }
+ )
+ programming_exercise.save()
+ self.cleanup_items.append(("LMS Programming Exercise", programming_exercise.name))
+ return programming_exercise
+
+ def _create_programming_exercise_submission(self, member):
+ existing = frappe.db.exists(
+ "LMS Programming Exercise Submission",
+ {"exercise": self.programming_exercise.name, "member": member},
+ )
+ if existing:
+ return frappe.get_doc("LMS Programming Exercise Submission", existing)
+
+ submission = frappe.new_doc("LMS Programming Exercise Submission")
+ submission.update(
+ {
+ "exercise": self.programming_exercise.name,
+ "member": member,
+ "code": "print(inputs[0] + 1)",
+ "status": "Passed",
+ }
+ )
+
+ submission.insert()
+ self.cleanup_items.append(("LMS Programming Exercise Submission", submission.name))
+ return submission
diff --git a/lms/lms/test_utils.py b/lms/lms/test_utils.py
index 0f59e454..827a7216 100644
--- a/lms/lms/test_utils.py
+++ b/lms/lms/test_utils.py
@@ -2,9 +2,8 @@
# See license.txt
import frappe
-from frappe.utils import get_time, getdate, to_timedelta
+from frappe.utils import getdate, to_timedelta
-from lms.lms.api import get_certified_participants
from lms.lms.doctype.lms_certificate.lms_certificate import is_certified
from lms.lms.test_helpers import BaseTestUtils
from lms.lms.utils import (
@@ -33,48 +32,8 @@ class TestLMSUtils(BaseTestUtils):
def setUp(self):
super().setUp()
- 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.course = self._create_course()
- self._setup_chapters_and_lessons()
-
- self._create_enrollment(self.student1.email, self.course.name)
- self._create_enrollment(self.student2.email, self.course.name)
-
- 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)
-
- self.evaluator = self._create_evaluator()
- self.batch = self._create_batch(self.course.name)
- self._create_batch_enrollment(self.student1.email, self.batch.name)
- self._create_batch_enrollment(self.student2.email, self.batch.name)
-
- def _setup_chapters_and_lessons(self):
- chapters = []
- for i in range(1, 4):
- chapter = self._create_chapter(f"Chapter {i}", self.course.name)
- chapters.append(chapter)
-
- self.course.reload()
- for chapter in chapters:
- if not any(c.chapter == chapter.name for c in self.course.chapters):
- self.course.append("chapters", {"chapter": chapter.name})
- self.course.save()
-
- for chapter_ref in self.course.chapters:
- chapter_doc = frappe.get_doc("Course Chapter", chapter_ref.chapter)
- for j in range(1, 3):
- lesson_title = f"Lesson {j} of {chapter_ref.chapter}"
- lesson = self._create_lesson(lesson_title, chapter_ref.chapter, self.course.name)
-
- if not any(l.lesson == lesson.name for l in chapter_doc.lessons):
- chapter_doc.append("lessons", {"lesson": lesson.name})
- chapter_doc.save()
+ self._setup_course_flow()
+ self._setup_batch_flow()
def test_simple_slugs(self):
self.assertEqual(slugify("hello-world"), "hello-world")
@@ -156,36 +115,6 @@ class TestLMSUtils(BaseTestUtils):
self.assertIsNone(is_certified(self.course.name))
frappe.session.user = "Administrator"
- def test_certified_participants_with_category(self):
- filters = {"category": "Utility Course"}
- certified_participants = get_certified_participants(filters=filters)
- self.assertEqual(len(certified_participants), 1)
- self.assertEqual(certified_participants[0].member, self.student1.email)
-
- filters = {"category": "Nonexistent Category"}
- certified_participants_no_match = get_certified_participants(filters=filters)
- self.assertEqual(len(certified_participants_no_match), 0)
-
- def test_certified_participants_with_open_to_work(self):
- filters = {"open_to_work": 1}
- certified_participants_open_to_work = get_certified_participants(filters=filters)
- self.assertEqual(len(certified_participants_open_to_work), 0)
-
- frappe.db.set_value("User", self.student1.email, "open_to", "Work")
- certified_participants_open_to_work = get_certified_participants(filters=filters)
- self.assertEqual(len(certified_participants_open_to_work), 1)
- frappe.db.set_value("User", self.student1.email, "open_to", "")
-
- def test_certified_participants_with_open_to_hiring(self):
- filters = {"hiring": 1}
- certified_participants_hiring = get_certified_participants(filters=filters)
- self.assertEqual(len(certified_participants_hiring), 0)
-
- frappe.db.set_value("User", self.student1.email, "open_to", "Hiring")
- certified_participants_hiring = get_certified_participants(filters=filters)
- self.assertEqual(len(certified_participants_hiring), 1)
- frappe.db.set_value("User", self.student1.email, "open_to", "")
-
def test_rating_validation(self):
student3 = self._create_user("student3@example.com", "Emily", "Cooper", ["LMS Student"])
with self.assertRaises(frappe.exceptions.ValidationError):