Merge pull request #1593 from pateljannat/programming-exercises

feat: programming exercises
This commit is contained in:
Jannat Patel
2025-06-26 13:05:22 +05:30
committed by GitHub
55 changed files with 2728 additions and 70 deletions
+66
View File
@@ -1493,3 +1493,69 @@ def update_meta_info(type, route, meta_tags):
print(new_tag)
new_tag.insert()
print(new_tag.as_dict())
@frappe.whitelist()
def create_programming_exercise_submission(exercise, submission, code, test_cases):
if submission == "new":
return make_new_exercise_submission(exercise, code, test_cases)
else:
update_exercise_submission(submission, code, test_cases)
def make_new_exercise_submission(exercise, code, test_cases):
submission = frappe.new_doc("LMS Programming Exercise Submission")
submission.exercise = exercise
submission.member = frappe.session.user
submission.code = code
for test_case in test_cases:
submission.append(
"test_cases",
{
"input": test_case.get("input"),
"output": test_case.get("output"),
"expected_output": test_case.get("expected_output"),
"status": test_case.get("status", test_case.get("status", "Failed")),
},
)
submission.status = get_exercise_status(test_cases)
submission.insert()
return submission.name
def update_exercise_submission(submission, code, test_cases):
update_test_cases(test_cases, submission)
status = get_exercise_status(test_cases)
frappe.db.set_value(
"LMS Programming Exercise Submission", submission, {"status": status, "code": code}
)
def get_exercise_status(test_cases):
if not test_cases:
return "Failed"
if all(row.get("status", "Failed") == "Passed" for row in test_cases):
return "Passed"
else:
return "Failed"
def update_test_cases(test_cases, submission):
frappe.db.delete("LMS Test Case Submission", {"parent": submission})
for row in test_cases:
test_case = frappe.new_doc("LMS Test Case Submission")
test_case.update(
{
"parent": submission,
"parenttype": "LMS Programming Exercise Submission",
"parentfield": "test_cases",
"input": row.get("input"),
"output": row.get("output"),
"expected_output": row.get("expected_output"),
"status": row.get("status", "Failed"),
}
)
test_case.insert()
@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Programming Exercise", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,136 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-06-18 15:02:36.198855",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"title",
"column_break_jlzi",
"language",
"section_break_tjwv",
"problem_statement",
"section_break_ftkh",
"test_cases"
],
"fields": [
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "problem_statement",
"fieldtype": "Text Editor",
"in_list_view": 1,
"label": "Problem Statement",
"reqd": 1
},
{
"default": "Python",
"fieldname": "language",
"fieldtype": "Select",
"label": "Language",
"options": "Python\nJavaScript",
"reqd": 1
},
{
"fieldname": "column_break_jlzi",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_tjwv",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_ftkh",
"fieldtype": "Section Break"
},
{
"fieldname": "test_cases",
"fieldtype": "Table",
"label": "Test Cases",
"options": "LMS Test Case"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [
{
"link_doctype": "LMS Programming Exercise Submission",
"link_fieldname": "exercise"
}
],
"modified": "2025-06-24 14:42:27.463492",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Programming 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
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Batch Evaluator",
"share": 1,
"write": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
@@ -0,0 +1,15 @@
# Copyright (c) 2025, Frappe and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class LMSProgrammingExercise(Document):
def validate(self):
self.validate_test_cases()
def validate_test_cases(self):
if not self.test_cases:
frappe.throw(_("At least one test case is required for the programming exercise."))
@@ -0,0 +1,30 @@
# Copyright (c) 2025, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestLMSProgrammingExercise(UnitTestCase):
"""
Unit tests for LMSProgrammingExercise.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestLMSProgrammingExercise(IntegrationTestCase):
"""
Integration tests for LMSProgrammingExercise.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Programming Exercise Submission", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,172 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-06-18 20:01:37.678342",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"exercise",
"exercise_title",
"status",
"column_break_jkjs",
"member",
"member_name",
"member_image",
"section_break_onmz",
"code",
"section_break_idyi",
"test_cases"
],
"fields": [
{
"fieldname": "exercise",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Exercise",
"options": "LMS Programming Exercise",
"reqd": 1
},
{
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Member",
"options": "User",
"reqd": 1
},
{
"fetch_from": "member.full_name",
"fieldname": "member_name",
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "\nPassed\nFailed"
},
{
"fieldname": "column_break_jkjs",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_idyi",
"fieldtype": "Section Break"
},
{
"fieldname": "test_cases",
"fieldtype": "Table",
"label": "Test Cases",
"options": "LMS Test Case Submission"
},
{
"fieldname": "section_break_onmz",
"fieldtype": "Section Break"
},
{
"fieldname": "code",
"fieldtype": "Code",
"label": "Code",
"reqd": 1
},
{
"fetch_from": "exercise.title",
"fieldname": "exercise_title",
"fieldtype": "Data",
"label": "Exercise Title",
"read_only": 1
},
{
"fetch_from": "member.user_image",
"fieldname": "member_image",
"fieldtype": "Attach",
"label": "Member Image"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-06-24 14:42:08.288983",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Programming 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
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Course Creator",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Batch Evaluator",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "LMS Student",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"show_title_field_in_link": 1,
"sort_field": "creation",
"sort_order": "DESC",
"states": [
{
"color": "Green",
"title": "Passed"
},
{
"color": "Red",
"title": "Failed"
}
],
"title_field": "member_name"
}
@@ -0,0 +1,9 @@
# Copyright (c) 2025, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSProgrammingExerciseSubmission(Document):
pass
@@ -0,0 +1,30 @@
# Copyright (c) 2025, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestLMSProgrammingExerciseSubmission(UnitTestCase):
"""
Unit tests for LMSProgrammingExerciseSubmission.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestLMSProgrammingExerciseSubmission(IntegrationTestCase):
"""
Integration tests for LMSProgrammingExerciseSubmission.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,8 @@
// Copyright (c) 2025, Frappe and contributors
// For license information, please see license.txt
// frappe.ui.form.on("LMS Test Case", {
// refresh(frm) {
// },
// });
@@ -0,0 +1,45 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-06-18 16:12:10.010416",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"input",
"column_break_zkvg",
"expected_output"
],
"fields": [
{
"fieldname": "input",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Input"
},
{
"fieldname": "column_break_zkvg",
"fieldtype": "Column Break"
},
{
"fieldname": "expected_output",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Expected Output",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-06-20 12:57:19.186644",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Test Case",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,9 @@
# Copyright (c) 2025, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSTestCase(Document):
pass
@@ -0,0 +1,30 @@
# Copyright (c) 2025, Frappe and Contributors
# See license.txt
# import frappe
from frappe.tests import IntegrationTestCase, UnitTestCase
# On IntegrationTestCase, the doctype test records and all
# link-field test record dependencies are recursively loaded
# Use these module variables to add/remove to/from that list
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
class UnitTestLMSTestCase(UnitTestCase):
"""
Unit tests for LMSTestCase.
Use this class for testing individual functions and methods.
"""
pass
class IntegrationTestLMSTestCase(IntegrationTestCase):
"""
Integration tests for LMSTestCase.
Use this class for testing interactions between multiple components.
"""
pass
@@ -0,0 +1,63 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-06-18 20:05:03.467705",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"input",
"expected_output",
"column_break_bsjs",
"output",
"status"
],
"fields": [
{
"fieldname": "input",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Input"
},
{
"fieldname": "expected_output",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Expected Output",
"reqd": 1
},
{
"fieldname": "column_break_bsjs",
"fieldtype": "Column Break"
},
{
"fieldname": "output",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Output",
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Passed\nFailed",
"reqd": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-06-24 11:23:13.803159",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Test Case Submission",
"owner": "Administrator",
"permissions": [],
"row_format": "Dynamic",
"sort_field": "creation",
"sort_order": "DESC",
"states": []
}
@@ -0,0 +1,9 @@
# Copyright (c) 2025, Frappe and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LMSTestCaseSubmission(Document):
pass
+28
View File
@@ -1504,6 +1504,9 @@ def get_assessments(batch, member=None):
elif assessment.assessment_type == "LMS Quiz":
assessment = get_quiz_details(assessment, member)
elif assessment.assessment_type == "LMS Programming Exercise":
assessment = get_exercise_details(assessment, member)
return assessments
@@ -1576,6 +1579,31 @@ def get_quiz_details(assessment, member):
return assessment
def get_exercise_details(assessment, member):
assessment.title = frappe.db.get_value(
"LMS Programming Exercise", assessment.assessment_name, "title"
)
filters = {"member": member, "exercise": assessment.assessment_name}
if frappe.db.exists("LMS Programming Exercise Submission", filters):
assessment.submission = frappe.db.get_value(
"LMS Programming Exercise Submission",
filters,
["name", "status"],
as_dict=True,
)
assessment.completed = True
assessment.status = assessment.submission.status
assessment.edit_url = (
f"/exercises/{assessment.assessment_name}/submission/{assessment.submission.name}"
)
else:
assessment.status = "Not Attempted"
assessment.color = "red"
assessment.completed = False
assessment.edit_url = f"/exercises/{assessment.assessment_name}/submission/new"
@frappe.whitelist()
def get_batch_students(batch):
students = []