diff --git a/frontend/src/components/Settings/SettingFields.vue b/frontend/src/components/Settings/SettingFields.vue index efb9c071..eb622e36 100644 --- a/frontend/src/components/Settings/SettingFields.vue +++ b/frontend/src/components/Settings/SettingFields.vue @@ -65,7 +65,7 @@
diff --git a/frontend/src/pages/Courses/StudentCourseProgress.vue b/frontend/src/pages/Courses/StudentCourseProgress.vue index 4f531f21..43345688 100644 --- a/frontend/src/pages/Courses/StudentCourseProgress.vue +++ b/frontend/src/pages/Courses/StudentCourseProgress.vue @@ -38,7 +38,7 @@
@@ -75,7 +75,7 @@
@@ -98,7 +98,7 @@
@@ -120,7 +120,7 @@
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):