AssessmentWorkflow.staff_score_exists() always returns False (looks up get_latest_assessment; the staff API defines get_latest_staff_assessment), so the ORA Summary Report's is_staff_grade_received is always 0
openassessment/workflow/models.py:558-571 (read at eee6b7b, current master)
def staff_score_exists(self):
"""
Check if a staff score exists for this submission.
"""
steps = self._get_steps()
step_for_name = {step.name: step for step in steps}
staff_step = step_for_name.get(self.STAFF_STEP_NAME)
if staff_step is not None:
get_latest_func = getattr(staff_step.api(), 'get_latest_assessment', None) # :566
if get_latest_func is not None:
staff_assessment = get_latest_func(self.submission_uuid)
if staff_assessment is not None:
return True
return False
For the staff step, api() resolves to openassessment.assessment.api.staff (models.py:934-936). _get_steps() always creates that step (models.py:472-505), so this path runs for every AssessmentWorkflow. The staff module defines get_latest_staff_assessment (staff.py:163) and no get_latest_assessment, and it has no alias or module-level __getattr__. teams.py is the same (teams.py:157). getattr(..., None) therefore returns None on every call, and the function falls through to return False.
Repo-wide, get_latest_assessment appears only at models.py:566 and as test method names in assessment/test/test_team.py:152,160, which call get_latest_staff_assessment. The lookup has been mismatched since the method was introduced in #758, the same PR that added get_latest_staff_assessment to staff.py.
Two things read this function:
OraAggregateData.collect_ora2_summary() (data.py:816): is_staff_grade_received = 1 if aw.staff_score_exists() else 0. This backs the instructor-dashboard ORA Summary Report (upload_ora2_summary in openedx-platform lms/djangoapps/instructor_task/tasks_helper/misc.py:282-291). Per the code path, the is_staff_grade_received column is 0 for every row, including learners who have a staff assessment.
AssessmentWorkflow.set_score() (models.py:551): if not self.staff_score_exists(): sub_api.set_score(...). The guard never blocks, but its practical reach is narrow. update_from_assessments (models.py:453-456) already skips set_score when the score carries a staff_id, and get_score (models.py:254-293) returns the staff score first whenever one exists. The only call where the guard would change behaviour is cancel() (models.py:605-607).
Measured
This was a modeled reproduction, not an observation on a running Open edX instance. No repository code was imported, installed or run. The function text of staff_score_exists, set_score and cancel was copied verbatim from models.py (ast segments) and executed in a plain Python process with no network. Substitutions used:
- a
SimpleNamespace in place of the workflow instance
- a
Step class whose api() returns a stand-in module
- a stand-in module exposing exactly the top-level
def names that ast finds in api/staff.py (only get_latest_staff_assessment has behaviour)
- a recorder in place of
sub_api.set_score
- a
logger stub
The data.py:816 line was executed as read from the file.
| Case |
staff_score_exists() |
is_staff_grade_received |
sub_api.set_score writes |
| A. staff assessment exists, real staff.py names |
False |
0 |
1 (guard did not block) |
| B. no staff assessment, real staff.py names |
False |
0 |
1 |
C. staff assessment exists, module exposes get_latest_assessment |
True |
1 |
0 |
D. no staff assessment, module exposes get_latest_assessment |
False |
0 |
1 |
A and B are indistinguishable. C and D are the controls: they show the function body behaves correctly once the name resolves.
cancel() (models.py:605-607) with a 7/10 staff score present, using the copied cancel text:
| Surface |
points written to the submissions API |
status |
| real staff.py names (as shipped) |
[0] |
cancelled |
module exposing get_latest_assessment |
[] |
cancelled |
Consequence
- The ORA Summary Report's
is_staff_grade_received column cannot report a staff grade. It is 0 for staff-graded learners as well as ungraded ones.
- The existing
test_collect_ora2_summary (openassessment/tests/test_data.py:914) expects is_staff_grade_received=0 on both rows. Its fixture has only peer assessments, so 0 is correct there; the test does not cover a staff-graded submission and cannot catch this.
- The
set_score guard has never been live. cancel() currently zeroes a staff-scored submission only because the guard is dead; its docstring says "Set the points earned to 0 and workflow status to cancelled." Correcting the name alone would make set_score refuse inside cancel(), so a cancelled submission would keep its staff points in the score table.
Suggested fix
Look up get_latest_staff_assessment, the name already used by api/staff.py:60, api/teams.py:55 and xblock/apis/assessments/staff_assessment_api.py:51. Because the guard has never run, the rename should not land alone. Land it together with:
cancel() writing the zero directly through sub_api.set_score (or a force argument on set_score), so that cancellation still zeroes a staff score.
- Tests:
staff_score_exists: True with a staff assessment, False without.
cancel() on a staff-scored workflow: points end at 0.
- A staff-graded row in
test_collect_ora2_summary asserting is_staff_grade_received=1.
AssessmentWorkflow.staff_score_exists() always returns False (looks up
get_latest_assessment; the staff API definesget_latest_staff_assessment), so the ORA Summary Report'sis_staff_grade_receivedis always 0openassessment/workflow/models.py:558-571(read at eee6b7b, current master)For the staff step,
api()resolves toopenassessment.assessment.api.staff(models.py:934-936)._get_steps()always creates that step (models.py:472-505), so this path runs for everyAssessmentWorkflow. The staff module definesget_latest_staff_assessment(staff.py:163) and noget_latest_assessment, and it has no alias or module-level__getattr__.teams.pyis the same (teams.py:157).getattr(..., None)therefore returnsNoneon every call, and the function falls through toreturn False.Repo-wide,
get_latest_assessmentappears only at models.py:566 and as test method names inassessment/test/test_team.py:152,160, which callget_latest_staff_assessment. The lookup has been mismatched since the method was introduced in #758, the same PR that addedget_latest_staff_assessmenttostaff.py.Two things read this function:
OraAggregateData.collect_ora2_summary()(data.py:816):is_staff_grade_received = 1 if aw.staff_score_exists() else 0. This backs the instructor-dashboard ORA Summary Report (upload_ora2_summaryin openedx-platformlms/djangoapps/instructor_task/tasks_helper/misc.py:282-291). Per the code path, theis_staff_grade_receivedcolumn is 0 for every row, including learners who have a staff assessment.AssessmentWorkflow.set_score()(models.py:551):if not self.staff_score_exists(): sub_api.set_score(...). The guard never blocks, but its practical reach is narrow.update_from_assessments(models.py:453-456) already skipsset_scorewhen the score carries astaff_id, andget_score(models.py:254-293) returns the staff score first whenever one exists. The only call where the guard would change behaviour iscancel()(models.py:605-607).Measured
This was a modeled reproduction, not an observation on a running Open edX instance. No repository code was imported, installed or run. The function text of
staff_score_exists,set_scoreandcancelwas copied verbatim from models.py (ast segments) and executed in a plain Python process with no network. Substitutions used:SimpleNamespacein place of the workflow instanceStepclass whoseapi()returns a stand-in moduledefnames thatastfinds inapi/staff.py(onlyget_latest_staff_assessmenthas behaviour)sub_api.set_scoreloggerstubThe data.py:816 line was executed as read from the file.
get_latest_assessmentget_latest_assessmentA and B are indistinguishable. C and D are the controls: they show the function body behaves correctly once the name resolves.
cancel()(models.py:605-607) with a 7/10 staff score present, using the copiedcanceltext:[0]get_latest_assessment[]Consequence
is_staff_grade_receivedcolumn cannot report a staff grade. It is 0 for staff-graded learners as well as ungraded ones.test_collect_ora2_summary(openassessment/tests/test_data.py:914) expectsis_staff_grade_received=0on both rows. Its fixture has only peer assessments, so 0 is correct there; the test does not cover a staff-graded submission and cannot catch this.set_scoreguard has never been live.cancel()currently zeroes a staff-scored submission only because the guard is dead; its docstring says "Set the points earned to 0 and workflow status to cancelled." Correcting the name alone would makeset_scorerefuse insidecancel(), so a cancelled submission would keep its staff points in the score table.Suggested fix
Look up
get_latest_staff_assessment, the name already used byapi/staff.py:60,api/teams.py:55andxblock/apis/assessments/staff_assessment_api.py:51. Because the guard has never run, the rename should not land alone. Land it together with:cancel()writing the zero directly throughsub_api.set_score(or aforceargument onset_score), so that cancellation still zeroes a staff score.staff_score_exists: True with a staff assessment, False without.cancel()on a staff-scored workflow: points end at 0.test_collect_ora2_summaryassertingis_staff_grade_received=1.