Merge pull request #1593 from pateljannat/programming-exercises
feat: programming exercises
This commit is contained in:
@@ -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
|
||||
+8
@@ -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) {
|
||||
|
||||
// },
|
||||
// });
|
||||
+172
@@ -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"
|
||||
}
|
||||
+9
@@ -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
|
||||
+30
@@ -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
|
||||
@@ -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 = []
|
||||
|
||||
Reference in New Issue
Block a user