Partially automated grading in Ans
This library can help you automate some tasks when grading assignments in Ans. Basically, it gives you a way to run scripts on each answer, while adding the output of those scripts as flags.
Caveat emptor: this library has grown organically to fit the needs of its author; as a result, the code can be unpolished in places. The API may also change over time; you may want to pin your copy of this code to a specific version to keep it working.
Currently, the best way to use the code in your project is to use a git submodule:
$ git submodule add git@github.com:TobiasKappe/grading.git
If you are using poetry
, you can easily make the library available by adding the following to pyproject.toml
(if the tool.poetry.dependencies
section already exists, add the second line there).
[tool.poetry.dependencies]
grading = { path = "./grading", develop = true }
After that, run poetry lock
and poetry install
, and you should be able to import the grading
module in your environment now (don't forget to activate your poetry shell
too).
The library is built around three concepts:
- A checker is a class that can be instantiated to verify the answer to a question. It may return an iterable of strings, each of which should be added as a flag to the answer in Ans.
- A marker is a Python module that connects checkers to questions within an assignment. Each checker can also be configured with question-specific parameters.
- A flagger is a script that calls the built-in flagging script, along with a bunch of arguments. Additionally, a flagger holds a list of marker modules to be exposed to the user.
There are probably better ways to do this, and I am not too keen on the names, but it works.
Let's work through a toy example; with a little bit of puzzling, you should be able to customize this to your needs.
Suppose you had an assignment where students are asked to perform some hard calculation. Furthermore, because you are a charitable teacher, you also decide to build in a 10% tolerance.
First, let's build a checker and save it to calculation.py
:
from grading.checker import Checker
@Checker.parametrize('right_answer', 'tolerance')
class CalculationChecker(Checker):
def __init__(self, students, question, submissions, files, **kwargs):
self.answer = submissions[question]['response']
def check(self):
try:
answer_numeric = float(self.answer)
except ValueError:
yield 'Answer cannot be parsed as a float.'
return
if answer_numeric == self.right_answer:
yield 'Answer is exactly right!'
elif answer_numeric < (1 - self.tolerance) * self.right_answer:
yield (
f'Answer below {self.tolerance} '
f'tolerance of {self.right_answer}.'
)
elif answer_numeric > (1 + self.tolerance) * self.right_answer:
yield (
f'Answer above {self.tolerance} '
f'tolerance of {self.right_answer}.'
)
else:
yield (
f'Answer within {self.tolerance} '
f'tolerance of {self.right_answer}.'
)
The constructor has several parameters:
students
is a list of raw user objects obtained from Ans, holding information about the students who made the submission (possibly more than one, in the case of group assignments).question
is the question number that this checker is being run against; it can be used to index intosubmissions
.submissions
corresponds to the assignment that the checker is being run against; it maps question numbers to a raw submission object from Ans.files
contains information about files that were submitted as attachments by the student; concretely, each element is a pair consisting of a file name (as chosen by the student!) and its contents.
Next, we need to build a marker to configure our checker, and point it to the right question.
To this end, we create mark_calculation.py
and put in the following:
from calculation import CalculationChecker
markers = [{
"question": 7,
"name": "The Big Calculation Question",
"checkers": [
CalculationChecker(
right_answer=42,
tolerance=0.1
),
]
}]
assignment_default = 'The Assignment with the Big Calculation'
Concretely, a marker module is expected to have at least two values in it:
markers
is a list of dictionaries, each of which holds information about checkers. In this case, there is one dictionary that points to question 7, which holds one instance ofCalculationChecker
instantiated with a right answer and the 10% tolerance mentioned before. Thename
attribute is required, but purely cosmetic; it will only show up in the output of the flagger script.assignment_default
is the default name of the assignment (in Ans) that this marker should run against. It can be overridden by at run-time.
The last piece of the puzzle is to configure the flagger script.
This can be by putting the following in a file, say flagger.py
:
from grading.flagger import main
import mark_calculation
modules = {
'calc': mark_calculation,
}
main(
'The Course with the Big Calculation',
modules,
ans_school_id,
'ans_api_token',
)
The first argument to main
is the default name of the course (in Ans) to grade; it can be overridden at run-time.
The second argument is a dictionary mapping short assignment names to modules that mark them.
The third and fourth arguments are a school ID and API token that you must obtain from Ans.
Finally, we can run our flagging script
$ python flagger.py calc build
This will do a dry-run on all of the submissions to "The Assignment with the Big Calculation" in "The Course with the Big Calculation" and print the flags that would be added. To actually add the flags, you can run
$ python flagger.py calc build -f
You can clear all flags by running
$ python flagger.py calc clear
Finally, one useful feature is to run the script only on submissions that currently do not have any flags:
$ python flagger.py calc build -u
Again, you should add -f
if you want the flags to actually be placed.
Further options are available; run python flagger.py --help
for an overview.