Subtracting points for late submissions

Use AutoTest submission metadata to automatically and flexibly subtract points for late submissions.

One of the most common applications of AutoTest submission metadata is to automatically subtract points for late days. By default, students can not hand in after the deadline, to allow students to do that, turn it on by following the steps in:

pageAllow students to hand in after deadline

In our example, we want to automatically subtract 1 point from the total rubric score for each day after the deadline, up to a maximum of 10 points subtracted:

  1. Set up a rubric category with 11 items ranging from -10 to 0 or a continuous rubric category with a negative lower bound of -10.

  2. Add a new "Custom Test" block to your AutoTest, nest it in a "Connect Rubric" block and select the rubric category you just made.

  3. Execute the following command to run our custom deadline.py script (shared later in this guide) and make sure the CG_INFO variable is visible for the script:

    export CG_INFO; python3 $UPLOADED_FILES/deadline.py

Now, we'll have to upload the deadline.py script. This is done in the "Setup" tab using a "Upload Files" block.

You may copy and paste the script below and edit it to your own needs and wishes.

"""""This module allows you to more easily use the ``CG_INFO`` feature of
CodeGrade and deduct points from students for days past the deadline.
"""""
import os as _os
import json as _json
import dataclasses
from typing import Any as _Any
from typing import Optional as _Optional
from datetime import datetime as _datetime
from datetime import timedelta
import math

__all__ = ('CG_INFO', )

@dataclasses.dataclass
class _CGInfo:
    submission_id: _Optional[int]
    student_id: _Optional[int]
    submitted_at: _Optional[_datetime]
    deadline: _Optional[_datetime]
    lock_date: _Optional[_datetime]

    @classmethod
    def _load(cls) -> '_CGInfo':
        # pylint: disable=import-outside-toplevel

        def _maybe_datetime(value: _Optional[str]) -> _Optional[_datetime]:
            if value is None:
                return None
            try:
                return _datetime.fromisoformat(value.replace('Z', '+00:00'))
            except (ValueError, TypeError):
                return None

        def _to_int(value: _Any) -> _Optional[int]:
            try:
                return int(value)
            except (ValueError, TypeError):
                return None

        try:
            unparsed_cg_info = _json.loads(_os.getenv('CG_INFO', '{}'))
        except ValueError as ex:
            print(ex)
            unparsed_cg_info = {}

        return cls(
            submission_id=_to_int(unparsed_cg_info.get('submission_id')),
            student_id=_to_int(unparsed_cg_info.get('student_id')),
            submitted_at=_maybe_datetime(unparsed_cg_info.get('submitted_at')),
            deadline=_maybe_datetime(unparsed_cg_info.get('deadline')),
            lock_date=_maybe_datetime(unparsed_cg_info.get('lock_date')),
        )


CG_INFO = _CGInfo._load()

ONE_DAY      = timedelta(days=1)
deadline     = CG_INFO.deadline
submitted_at = CG_INFO.submitted_at
days_late    = math.ceil((submitted_at - deadline) / ONE_DAY)

if days_late <= 0:
    print('submitted on time :)')
    print(1.0)
elif days_late <= 10:
    print('{} days late'.format(days_late))
    print(1 - days_late / 10)
else:
    print('very late, maximum penalty')
    print(0.0)

with open(3, "w") as f:
    result = { "tag": "points", "points": str(1 - min(days_late / 10, 1))}
    f.write(_json.dumps(result))

In this example, we are subtracting 1 rubric point per late day. Keep in mind that rubric points are not final grade points, unless your rubric has exactly 10 rubric points.

Last updated