Skip to content

Commit 41a39bf

Browse files
committed
fix: Feedback generation when some notebooks are missing in a submission
1 parent f920f59 commit 41a39bf

File tree

3 files changed

+85
-16
lines changed

3 files changed

+85
-16
lines changed

grader_service/convert/converters/generate_feedback.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,20 @@ def __init__(
5656
self.force = True # always overwrite generated assignments
5757

5858
def init_notebooks(self) -> None:
59-
# We only generate feedback for the notebooks for which there is a gradebook entry,
60-
# and ignore e.g. additional notebooks created by the student, because feedback
61-
# generation would fail for them anyway.
59+
super().init_notebooks()
6260
json_path = os.path.join(self._output_directory, "gradebook.json")
6361
with Gradebook(json_path) as gb:
64-
self.notebooks = [f"{nb_id}.ipynb" for nb_id in gb.model.notebook_id_set]
62+
# `self.notebooks` contains the notebooks actually submitted by the student.
63+
# `notebook_id_set` contains the original notebooks from the assignment.
64+
# We generate feedback for the notebooks belonging to these both sets.
65+
student_nbs = {
66+
self.init_single_notebook_resources(nb)["unique_key"]: nb for nb in self.notebooks
67+
}
68+
assign_nb_ids = gb.model.notebook_id_set
69+
self.notebooks = [path for id, path in student_nbs.items() if id in assign_nb_ids]
70+
71+
if len(self.notebooks) == 0:
72+
self.log.warning("No notebooks to generate feedback")
6573

6674
def get_include_patterns(self, gb: Gradebook) -> list[str]:
6775
"""Get glob patterns specifying for which submission files to generate feedback.

grader_service/tests/convert/converters/test_autograde.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import shutil
23
from unittest.mock import patch
34

@@ -116,18 +117,13 @@ def test_autograde_with_student_notebooks_copied_over(tmp_path):
116117
_generate_test_submission(input_dir, output_dir)
117118

118119
student_nb = output_dir / "student.ipynb"
119-
student_nb.touch()
120+
shutil.copyfile(input_dir / "simple.ipynb", student_nb)
120121

121122
output_dir2 = tmp_path / "output_dir2"
122123
output_dir2.mkdir()
123124
shutil.copyfile(output_dir / "gradebook.json", output_dir2 / "gradebook.json")
124125

125-
with (
126-
patch.object(NotebookClient, "kernel_name", "python3"),
127-
patch(
128-
"grader_service.convert.converters.autograde.Autograde.convert_single_notebook"
129-
) as convert_mock,
130-
):
126+
with patch.object(NotebookClient, "kernel_name", "python3"):
131127
Autograde(
132128
input_dir=str(output_dir),
133129
output_dir=str(output_dir2),
@@ -140,4 +136,31 @@ def test_autograde_with_student_notebooks_copied_over(tmp_path):
140136
assert (output_dir2 / "gradebook.json").exists()
141137
assert (output_dir2 / "student.ipynb").exists()
142138

143-
assert convert_mock.call_count == 2 # `convert_single_notebook` was called for both notebooks
139+
140+
def test_autograde_with_missing_submission_file(tmp_path):
141+
"""Test that Autograde handles files missing in the student submission gracefully"""
142+
input_dir, output_dir = _create_input_output_dirs(tmp_path, ["simple.ipynb"])
143+
_generate_test_submission(input_dir, output_dir)
144+
145+
# Student didn't submit the required notebook, but instead submitted a different one
146+
os.remove(output_dir / "simple.ipynb")
147+
student_nb = output_dir / "student.ipynb"
148+
student_nb.touch()
149+
150+
output_dir2 = tmp_path / "output_dir2"
151+
output_dir2.mkdir()
152+
shutil.copyfile(output_dir / "gradebook.json", output_dir2 / "gradebook.json")
153+
154+
with patch.object(NotebookClient, "kernel_name", "python3"):
155+
autograder = Autograde(
156+
input_dir=str(output_dir),
157+
output_dir=str(output_dir2),
158+
file_pattern="*.ipynb",
159+
assignment_settings=AssignmentSettings(allowed_files=["*.ipynb"]),
160+
config=None,
161+
)
162+
autograder.start()
163+
164+
assert autograder.notebooks == [str(student_nb)]
165+
assert (output_dir2 / "gradebook.json").exists()
166+
assert (output_dir2 / "student.ipynb").exists()

grader_service/tests/convert/converters/test_generate_feedback.py

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import shutil
23

34
from grader_service.api.models.assignment_settings import AssignmentSettings
@@ -84,7 +85,7 @@ def test_generate_feedback_with_student_notebooks(tmp_path):
8485
input_dir, output_dir = _create_input_output_dirs(tmp_path, ["simple.ipynb"])
8586

8687
_generate_test_submission(
87-
input_dir, output_dir, assignment_settings_kwargs={"allowed_files": ["*"]}
88+
input_dir, output_dir, assignment_settings_kwargs={"allowed_files": ["*.ipynb"]}
8889
)
8990

9091
student_nb = output_dir / "student.ipynb"
@@ -95,7 +96,7 @@ def test_generate_feedback_with_student_notebooks(tmp_path):
9596
shutil.copyfile(output_dir / "gradebook.json", output_dir2 / "gradebook.json")
9697

9798
_autograde_test_submission(
98-
str(output_dir), str(output_dir2), assignment_settings_kwargs={"allowed_files": ["*"]}
99+
str(output_dir), str(output_dir2), assignment_settings_kwargs={"allowed_files": ["*.ipynb"]}
99100
)
100101

101102
output_dir3 = tmp_path / "output_dir3"
@@ -107,7 +108,7 @@ def test_generate_feedback_with_student_notebooks(tmp_path):
107108
output_dir=str(output_dir3),
108109
file_pattern="*.ipynb",
109110
config=None,
110-
assignment_settings=AssignmentSettings(allowed_files=["*"]),
111+
assignment_settings=AssignmentSettings(allowed_files=["*.ipynb"]),
111112
).start()
112113

113114
# Only the original notebook should be copied to the feedback
@@ -116,7 +117,7 @@ def test_generate_feedback_with_student_notebooks(tmp_path):
116117
assert not (output_dir3 / "student.html").exists()
117118

118119

119-
def test_generate_assignment_copy_with_dirs(tmp_path):
120+
def test_generate_feedback_with_dirs(tmp_path):
120121
input_dir, output_dir = _create_input_output_dirs(tmp_path, ["simple.ipynb"])
121122

122123
dir_1 = input_dir / "dir_1"
@@ -161,3 +162,40 @@ def test_generate_assignment_copy_with_dirs(tmp_path):
161162
assert not (output_dir3 / "dir_1/dir_2").exists()
162163
assert not (output_dir3 / "dir_1/dir_2/dir_3").exists()
163164
assert not (output_dir3 / "dir_1/dir_2/dir_3/test.txt").exists()
165+
166+
167+
def test_generate_feedback_with_missing_submission_file(tmp_path):
168+
"""Test that GenerateFeedback handles files missing in the student submission gracefully"""
169+
input_dir, output_dir = _create_input_output_dirs(tmp_path, ["simple.ipynb"])
170+
171+
_generate_test_submission(
172+
input_dir, output_dir, assignment_settings_kwargs={"allowed_files": ["*.ipynb"]}
173+
)
174+
175+
# Student didn't submit the required notebook, but instead submitted a different one
176+
os.remove(output_dir / "simple.ipynb")
177+
student_nb = output_dir / "student.ipynb"
178+
student_nb.touch()
179+
180+
output_dir2 = tmp_path / "output_dir2"
181+
output_dir2.mkdir()
182+
shutil.copyfile(output_dir / "gradebook.json", output_dir2 / "gradebook.json")
183+
184+
_autograde_test_submission(
185+
str(output_dir), str(output_dir2), assignment_settings_kwargs={"allowed_files": ["*.ipynb"]}
186+
)
187+
188+
output_dir3 = tmp_path / "output_dir3"
189+
output_dir3.mkdir()
190+
shutil.copyfile(output_dir2 / "gradebook.json", output_dir3 / "gradebook.json")
191+
192+
gf = GenerateFeedback(
193+
input_dir=str(output_dir2),
194+
output_dir=str(output_dir3),
195+
file_pattern="*.ipynb",
196+
config=None,
197+
assignment_settings=AssignmentSettings(allowed_files=["*.ipynb"]),
198+
)
199+
gf.start()
200+
201+
assert gf.notebooks == []

0 commit comments

Comments
 (0)