fix: Restructuring to send course_id and score to edx submission fix: Refactoring of sending information to sent_to_submission fix: Elimination of unnecessary functions fix: Added usage comment to ProblemBlock in XqueueInterface constructor fix: update doc ADR fix: setting for Quality Others test (ubuntu-24.04, 3.11, 20) fix: Deprecation for django-trans-escape-filter-parse-error test: Add @pytest.mark.django_db decorator to test functions test: Fix for pylint being disabled fix: updated changes for pylint disable fix: update error from docs ADR-0005 update: xmodule/docs/decisions/0005-send-data-to-edx-submission.rst Co-authored-by: Sarina Canelake <sarina@axim.org> update: xmodule/docs/decisions/0005-send-data-to-edx-submission.rst Co-authored-by: Sarina Canelake <sarina@axim.org> fix: Adjusted correction fix: update date for docs ADR Revert "fix: update date for docs ADR" This reverts commit 0b4229c51c4937f95cb407872645dd448df45418. fix: replace call created_submission to create_external_grader_detail fix: update test xqueue_submission fix: add docstring in test_xqueue_submission fix: update date doc ADR fix: update version edx-submission 3.8.6 fix: add @pytest.mark.xfail fix: add 20 chances in test_capa_block: fix: increase retry attempts for seed generation in ProblemBlockTest fix: change version to edx-submission lib fix: new version edx-submission in testings fix: replace parameter file to files fix: update variable grader_file_name and points_possible fix: Adjustment in the is_flag_active function to always take the last record edited in the waffle fix: wrap large line of code fix: update function is_flag_active fix: code style adjustment fix: changes for 60 retry feat: use CourseWaffleFlag to determine xqueue callback path fix: Code style adjustment fix: remove deprecated xqueue callback route and simplify callback type logic fix: Deleting a comment in the ADR document fix: add log in self.block is None fix: Code style adjustment in log
114 lines
4.0 KiB
Python
114 lines
4.0 KiB
Python
"""
|
|
This module provides an interface for submitting student responses
|
|
to an external grading system through XQueue.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
from xmodule.capa.errors import (
|
|
GetSubmissionParamsError,
|
|
JSONParsingError,
|
|
MissingKeyError,
|
|
ValidationError,
|
|
TypeErrorSubmission,
|
|
RuntimeErrorSubmission
|
|
)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class XQueueInterfaceSubmission:
|
|
"""Interface to the external grading system."""
|
|
|
|
def __init__(self, block):
|
|
self.block = block
|
|
|
|
def _parse_json(self, data, name):
|
|
"""
|
|
Helper function to safely parse data that may or may not be a JSON string.
|
|
This is necessary because some callers may already provide parsed Python dicts
|
|
(e.g., during internal calls or test cases), while other sources may send raw JSON strings.
|
|
This helper ensures consistent behavior regardless of input format.
|
|
Args:
|
|
data: The input to parse, either a JSON string or a Python dict.
|
|
name: Name of the field (used for error reporting).
|
|
Returns:
|
|
Parsed Python object or original data if already parsed.
|
|
Raises:
|
|
JSONParsingError: If `data` is a string and cannot be parsed as JSON.
|
|
"""
|
|
try:
|
|
return json.loads(data) if isinstance(data, str) else data
|
|
except json.JSONDecodeError as e:
|
|
raise JSONParsingError(name, str(e)) from e
|
|
|
|
def get_submission_params(self, header, payload):
|
|
"""
|
|
Extracts student submission data from the given header and payload.
|
|
"""
|
|
header = self._parse_json(header, "header")
|
|
payload = self._parse_json(payload, "payload")
|
|
|
|
queue_name = header.get('queue_name', 'default')
|
|
|
|
if not self.block:
|
|
raise GetSubmissionParamsError()
|
|
|
|
course_id = str(self.block.scope_ids.usage_id.context_key)
|
|
item_type = self.block.scope_ids.block_type
|
|
points_possible = self.block.max_score()
|
|
|
|
item_id = str(self.block.scope_ids.usage_id)
|
|
|
|
try:
|
|
grader_payload = self._parse_json(payload["grader_payload"], "grader_payload")
|
|
grader_file_name = grader_payload.get("grader", '')
|
|
except KeyError as e:
|
|
raise MissingKeyError("grader_payload") from e
|
|
|
|
student_info = self._parse_json(payload["student_info"], "student_info")
|
|
student_id = student_info.get("anonymous_student_id")
|
|
|
|
if not student_id:
|
|
raise ValidationError("The field 'anonymous_student_id' is missing from student_info.")
|
|
|
|
student_answer = payload.get("student_response")
|
|
if student_answer is None:
|
|
raise ValidationError("The field 'student_response' does not exist.")
|
|
|
|
student_dict = {
|
|
'item_id': item_id,
|
|
'item_type': item_type,
|
|
'course_id': course_id,
|
|
'student_id': student_id
|
|
}
|
|
|
|
return student_dict, student_answer, queue_name, grader_file_name, points_possible
|
|
|
|
def send_to_submission(self, header, body, files_to_upload=None):
|
|
"""
|
|
Submits the extracted student data to the edx-submissions system.
|
|
"""
|
|
try:
|
|
from submissions.api import create_external_grader_detail
|
|
student_item, answer, queue_name, grader_file_name, points_possible = (
|
|
self.get_submission_params(header, body)
|
|
)
|
|
return create_external_grader_detail(
|
|
student_item,
|
|
answer,
|
|
queue_name=queue_name,
|
|
grader_file_name=grader_file_name,
|
|
points_possible=points_possible,
|
|
files=files_to_upload
|
|
)
|
|
except (JSONParsingError, MissingKeyError, ValidationError) as e:
|
|
log.error("%s", e)
|
|
return {"error": str(e)}
|
|
except TypeError as e:
|
|
log.error("%s", e)
|
|
raise TypeErrorSubmission(str(e)) from e
|
|
except RuntimeError as e:
|
|
log.error("%s", e)
|
|
raise RuntimeErrorSubmission(str(e)) from e
|