|
1 | 1 | """ |
2 | 2 | Grades related signals. |
3 | 3 | """ |
4 | | - |
5 | | - |
| 4 | +import json |
6 | 5 | from contextlib import contextmanager |
7 | 6 | from logging import getLogger |
8 | 7 |
|
9 | 8 | from django.dispatch import receiver |
10 | | -from opaque_keys.edx.keys import LearningContextKey |
| 9 | +from opaque_keys.edx.keys import CourseKey, LearningContextKey, UsageKey |
| 10 | +from opaque_keys import InvalidKeyError |
| 11 | +from openedx_events.learning.signals import EXTERNAL_GRADER_SCORE_SUBMITTED |
11 | 12 | from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, EXAM_ATTEMPT_VERIFIED |
12 | 13 | from submissions.models import score_reset, score_set |
13 | 14 | from xblock.scorable import ScorableXBlockMixin, Score |
|
23 | 24 | recalculate_subsection_grade_v3 |
24 | 25 | ) |
25 | 26 | from openedx.core.djangoapps.course_groups.signals.signals import COHORT_MEMBERSHIP_UPDATED |
| 27 | +from openedx.core.djangoapps.signals.signals import ( # lint-amnesty, pylint: disable=wrong-import-order |
| 28 | + COURSE_GRADE_NOW_FAILED, |
| 29 | + COURSE_GRADE_NOW_PASSED |
| 30 | +) |
26 | 31 | from openedx.core.lib.grade_utils import is_score_higher_or_equal |
| 32 | +from xmodule.modulestore.django import modulestore |
27 | 33 |
|
28 | 34 | from .. import events |
29 | 35 | from ..constants import GradeOverrideFeatureEnum, ScoreDatabaseTableEnum |
30 | 36 | from ..course_grade_factory import CourseGradeFactory |
31 | 37 | from ..scores import weighted_score |
32 | 38 | from .signals import ( |
| 39 | + COURSE_GRADE_PASSED_FIRST_TIME, |
33 | 40 | PROBLEM_RAW_SCORE_CHANGED, |
34 | 41 | PROBLEM_WEIGHTED_SCORE_CHANGED, |
35 | 42 | SCORE_PUBLISHED, |
36 | 43 | SUBSECTION_OVERRIDE_CHANGED, |
37 | | - SUBSECTION_SCORE_CHANGED, |
38 | | - COURSE_GRADE_PASSED_FIRST_TIME |
39 | | -) |
40 | | -from openedx.core.djangoapps.signals.signals import ( # lint-amnesty, pylint: disable=wrong-import-order |
41 | | - COURSE_GRADE_NOW_FAILED, |
42 | | - COURSE_GRADE_NOW_PASSED |
| 44 | + SUBSECTION_SCORE_CHANGED |
43 | 45 | ) |
44 | 46 |
|
45 | 47 | log = getLogger(__name__) |
@@ -347,3 +349,98 @@ def exam_attempt_rejected_event_handler(sender, signal, **kwargs): # pylint: di |
347 | 349 | overrider=None, |
348 | 350 | comment=None, |
349 | 351 | ) |
| 352 | + |
| 353 | + |
| 354 | +@receiver(EXTERNAL_GRADER_SCORE_SUBMITTED) |
| 355 | +def handle_external_grader_score(signal, sender, score, **kwargs): |
| 356 | + """ |
| 357 | + Event handler for external grader score submissions. |
| 358 | +
|
| 359 | + This function is triggered when an external grader submits a score through the |
| 360 | + EXTERNAL_GRADER_SCORE_SUBMITTED signal. It processes the score and updates |
| 361 | + the corresponding XBlock instance with the grading results. |
| 362 | +
|
| 363 | + Args: |
| 364 | + signal: The signal that triggered this handler |
| 365 | + sender: The object that sent the signal |
| 366 | + score: An object containing the score data with attributes: |
| 367 | + - score_msg: The actual score message/response from the grader |
| 368 | + - course_id: String ID of the course |
| 369 | + - user_id: ID of the user who submitted the problem |
| 370 | + - module_id: ID of the module/problem |
| 371 | + - submission_id: ID of the submission |
| 372 | + - queue_key: Key identifying the submission in the queue |
| 373 | + - queue_name: Name of the queue used for grading |
| 374 | + **kwargs: Additional keyword arguments passed with the signal |
| 375 | +
|
| 376 | + The function logs details about the score event, formats the grader message |
| 377 | + appropriately, and then calls the module's score_update handler to record |
| 378 | + the grade in the learning management system. |
| 379 | + """ |
| 380 | + |
| 381 | + log.info(f"Received external grader score event: {signal}, {sender}, {score}, {kwargs}") |
| 382 | + |
| 383 | + grader_msg = score.score_msg |
| 384 | + log.info( |
| 385 | + "External grader event score payload received: user_id=%s, module_id=%s, submission_id=%s, course_id=%s", |
| 386 | + score.user_id, |
| 387 | + score.module_id, |
| 388 | + score.submission_id, |
| 389 | + score.course_id, |
| 390 | + ) |
| 391 | + |
| 392 | + # Since we already confirm this in edx-submissions, it is safe to parse this |
| 393 | + grader_msg = json.loads(grader_msg) |
| 394 | + log.info(f"External grader score: {grader_msg['score']}") |
| 395 | + |
| 396 | + data = { |
| 397 | + 'xqueue_header': json.dumps({ |
| 398 | + 'lms_key': str(score.submission_id), |
| 399 | + 'queue_name': score.queue_name |
| 400 | + }), |
| 401 | + 'xqueue_body': json.dumps(grader_msg), |
| 402 | + 'queuekey': score.queue_key |
| 403 | + } |
| 404 | + |
| 405 | + try: |
| 406 | + course_key = CourseKey.from_string(score.course_id) |
| 407 | + course = modulestore().get_course(course_key, depth=0) |
| 408 | + except InvalidKeyError: |
| 409 | + log.error("Invalid course_id received from external grader: %s", score.course_id) |
| 410 | + return |
| 411 | + |
| 412 | + try: |
| 413 | + usage_key = UsageKey.from_string(score.module_id) |
| 414 | + except InvalidKeyError: |
| 415 | + log.error("Invalid usage key received from external grader: %s", score.module_id) |
| 416 | + return |
| 417 | + |
| 418 | + # pylint: disable=broad-exception-caught |
| 419 | + try: |
| 420 | + # Use our new function instead of load_single_xblock |
| 421 | + # NOTE: Importing this at module level causes a circular import because |
| 422 | + # score_render → block_render → grades signals → back into this module. |
| 423 | + # Keeping it inside the handler avoids that by loading it only when needed. |
| 424 | + from xmodule.capa.score_render import load_xblock_for_external_grader |
| 425 | + instance = load_xblock_for_external_grader(score.user_id, |
| 426 | + course_key, |
| 427 | + usage_key, |
| 428 | + course=course) |
| 429 | + |
| 430 | + # Call the handler method (mirroring the original xqueue_callback) |
| 431 | + instance.handle_ajax('score_update', data) |
| 432 | + |
| 433 | + # Save any state changes |
| 434 | + instance.save() |
| 435 | + |
| 436 | + log.info(f"Successfully processed external grade for module {score.module_id}, user {score.user_id}") |
| 437 | + |
| 438 | + except Exception as e: |
| 439 | + log.exception( |
| 440 | + "Error processing external grade for user_id=%s, module_id=%s, submission_id=%s: %s", |
| 441 | + score.user_id, |
| 442 | + score.module_id, |
| 443 | + score.submission_id, |
| 444 | + e, |
| 445 | + ) |
| 446 | + raise |
0 commit comments