Skip to content

Commit

Permalink
Add missing grade_cells before autograding
Browse files Browse the repository at this point in the history
  • Loading branch information
tuncbkose committed Mar 29, 2023
1 parent 85d0ad3 commit 3ed1936
Showing 1 changed file with 104 additions and 0 deletions.
104 changes: 104 additions & 0 deletions nbgrader/preprocessors/overwritecells.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,72 @@
from .. import utils
from ..api import Gradebook, MissingEntry
from . import NbGraderPreprocessor
from ..nbgraderformat import MetadataValidator
from nbconvert.exporters.exporter import ResourcesDict
from nbformat.notebooknode import NotebookNode
from traitlets import Bool
from typing import Tuple, Any
from textwrap import dedent


class OverwriteCells(NbGraderPreprocessor):
"""A preprocessor to overwrite information about grade and solution cells."""

add_missing_cells = Bool(
False,
help=dedent(
"""
Whether or not missing grade_cells should be added back
to the notebooks being graded.
"""
),
).tag(config=True)


@staticmethod
def missing_cell_transform(source_cell, max_score, is_also_solution=False):
"""
Converts source_cell obtained from Gradebook into a cell that can be added to the notebook.
Assumptions:
- this is a grade_cell
- since this is a grade_cell, it should be executed and graded
"""

missing_cell_notification = f"This cell (id:{source_cell.name}) was missing from the submission. " + \
"It was added back by nbgrader.\n" # Fix this for markdown case

cell = {
"cell_type": source_cell.cell_type,
"metadata": {
"deletable": False,
"editable": False,
"nbgrader": {
"grade": True,
"grade_id": source_cell.name,
"locked": source_cell.locked,
"checksum": source_cell.checksum,
"cell_type": source_cell.cell_type,
"points": max_score,
"solution": False
}
},
"source": missing_cell_notification+source_cell.source
}

# Don't want this if not code
if cell["cell_type"] == "code":
cell["execution_count"] = None
cell["outputs"] = []
cell["source"] = "# " + cell["source"] # code cell, make notification a comment

# TODO test
if is_also_solution:
del cell["metadata"]["editable"]
cell["metadata"]["nbgrader"]["solution"] = True
cell = NotebookNode(cell)
cell = MetadataValidator().upgrade_cell_metadata(cell)
return cell

def preprocess(self, nb: NotebookNode, resources: ResourcesDict) -> Tuple[NotebookNode, ResourcesDict]:
# pull information from the resources
self.notebook_id = resources['nbgrader']['notebook']
Expand All @@ -22,9 +80,55 @@ def preprocess(self, nb: NotebookNode, resources: ResourcesDict) -> Tuple[Notebo

with self.gradebook:
nb, resources = super(OverwriteCells, self).preprocess(nb, resources)
if self.add_missing_cells:
nb, resources = self.add_missing_grade_cells(nb, resources)

return nb, resources

def add_missing_grade_cells(self, nb: NotebookNode, resources: ResourcesDict) -> Tuple[NotebookNode, ResourcesDict]:
"""
Add missing grade cells back to the notebook.
If missing, find the previous solution/grade cell, and add the current cell after it.
It is assumed such a cell exists because
presumably the grade_cell exists to grade some work in the solution cell.
"""
source_nb = self.gradebook.find_notebook(self.notebook_id, self.assignment_id)
source_cells = source_nb.source_cells
source_cell_ids = [cell.name for cell in source_cells]
grade_cells = {cell.name: cell for cell in source_nb.grade_cells}
solution_cell_ids = [cell.name for cell in source_nb.solution_cells]

# only for solution and grade cells
submitted_cell_idxs = dict()
for idx, cell in enumerate(nb.cells):
if utils.is_grade(cell) or utils.is_solution(cell):
submitted_cell_idxs[cell.metadata.nbgrader["grade_id"]] = idx
# Every time we add a cell, the idxs above gets shifted
# We could process the notebook backwards, but that makes adding the cells in the right place more difficult
# So we keep track of how many we have added so far
added_count = 0

for grade_cell_id, grade_cell in grade_cells.items():
# If missing, find the previous solution/grade cell, and add the current cell after it.
if grade_cell_id not in submitted_cell_idxs:
self.log.warning(f"Missing grade cell {grade_cell_id} encountered, adding to notebook")
source_cell_idx = source_cell_ids.index(grade_cell_id)
prev_cell_id = source_cell_ids[source_cell_idx - 1]
cell_to_add = source_cells[source_cell_idx]

prev_cell_idx = submitted_cell_idxs[prev_cell_id]+added_count # TODO: if the prev cell was also missing, we add it but it is not in this dict
cell_to_add = OverwriteCells.missing_cell_transform(cell_to_add, grade_cell.max_score,
is_also_solution=grade_cell_id in solution_cell_ids)
nb.cells.insert(prev_cell_idx+1, cell_to_add) # +1 to add it after

# If the cell we just added is followed by other missing cells, we need to know its index in the nb
# However, no need to add `added_count` to avoid double-counting
submitted_cell_idxs[grade_cell_id] = submitted_cell_idxs[prev_cell_id]
added_count += 1 # shift idxs

return nb, resources


def update_cell_type(self, cell: NotebookNode, cell_type: str) -> None:
if cell.cell_type == cell_type:
return
Expand Down

0 comments on commit 3ed1936

Please sign in to comment.