Setup#

Python#

See the installation instructions. You’ll only need to do this once at the start of semester.

Keeping Lucy up to date#

Your course’s lucy repo is a fork of the upstream lucy/lucy repo, so you (and your tutors) pick up course changes, like a new students.csv or allocations file, with a plain git pull in the lucy directory.

To pick up a new version of lucy itself, merge upstream into your fork:

git remote add upstream https://gitlab.comp.anu.edu.au/lucy/lucy.git  # once
git pull upstream master

Course-specific code lives in plugins rather than in the lucy/ package, so these merges shouldn’t touch your course’s code.

GitLab repo organisation#

This tool is built around a few assumptions about how you organise your course assessment items on GitLab:

  • all “base” repos belong to the course/year group/subgroup (e.g. compXXXX/20XX)

  • for each item, each student forks the “base” repo to their own “uni ID” account

  • there’s a dedicated GitLab “marking” user (e.g. compXXXX-20XX-marker) which is automatically added as a Maintainer to each student fork by a server-side hook when the student forks

Talk to the SoCo Facilities Team about getting the marker account and the hook set up — see the fork hook for what the hook does, and what it looks like when it doesn’t fire.

Marking#

If you’re a convenor and you want to kick off the marking process for a particular assignment (you don’t do the marking yourself, but you need to set things up for your tutors to mark things), the workflow is:

  1. poetry run ./lucy.py fork <assignment_name> <-_ target> - create marking forks of all submission repos on GitLab. This locks the submission to the last commit before the deadline. This is the first command to run after the submission deadline.

  2. (optional) poetry run ./lucy.py update-marking-files <assignment_name> <-_ target> - populate the marking files with any standard deductions derived from GitLab CI jobs. Only needed if your course uses automated CI-based deductions. See below for more information

  3. (optional) poetry run ./lucy.py allocate <assignment_name> - assign students to tutors for marking. Required if tutors will clone by tutor name (--tutor). See below for more information

    If tutors will mark in the GitLab web UI rather than with lucy, also run share-marking-forks here (see on-web marking).

  4. (optional) poetry run ./lucy.py moss <assignment_name> <-_ target> - check the submissions for plagiarism. See below for more information

  5. hassle your tutors to get their marking done (no lucy command provided)

  6. poetry run ./lucy.py pull <assignment_name> <-_ target> - after tutors have pushed marks to the repos, pull them onto your machine.

  7. poetry run ./lucy.py check <assignment_name> <-_ target> - check that everything’s hunky-dory for that group—if it’s not, goto 5

  8. (optional) poetry run ./lucy.py pam <assignment_name> <-_ target> - print a summary of all students flagged for Potential Academic Misconduct across all marking files. See below for more information

  9. poetry run ./lucy.py collate <assignment_name> <-G group_name> - collate all the current marks (on local disk - so make sure you pull just beforehand). See below for more information

  10. (optional) poetry run ./lucy.py canvas-upload <assignment_name> - upload the collated marks and comments directly to Canvas. Requires Canvas integration to be configured. See below for more information

  11. poetry run ./lucy.py push-feedback <assignment_name> <-_ target> - squash marking commits & push feedback file to each student’s GitLab project (i.e. student-origin) so that they can see it. See below for more information

Configuration#

You need to make sure that the info file (data/course_info.yml) contains the correct information.

It’s a YAML file, so it’s basically a bunch of nested arrays & key-value pairs.

Note: this info file is the main source of truth for information about the course. Make sure you get the details right or you’ll have weird problems.

Student information and canvas-sync#

The class list, which contains the uid, name, small group (lab), course & degree information for all the students in the course(s) must be provided in a csv file. Which file the lucy looks in for the student info is determined by the students_file key in the info file.

The default way to populate this file is from the Canvas API using the canvas-sync command. If Canvas isn’t an option for your course, you can fall back to manually downloading an excel export from MyTimetable and importing it with the mytt-sync command (see below).

You will likely have to update your students.csv file a few times during the semester as students add or drop your course or change lab group. MyTimetable allocations are locked in after about week 4.

canvas-sync#

Sync the student list from Canvas with:

poetry run ./lucy.py canvas-sync

This requires a Canvas API token with appropriate read permissions set in data/secrets.yml, and the canvas section of data/course_info.yml to be configured. The keys here are:

  • url the base URL of your Canvas instance (e.g. https://canvas.anu.edu.au).
  • course_id the Canvas course ID, as appearing in the browser address bar when in a course (e.g. https://canvas.anu.edu.au/courses/XXXX).
  • activity_id the name of the main “lab” or “tutorial” activity group, if you have one computer lab activity, it will probably be ComA.
  • group_names (optional) a mapping from the Canvas section names to your preferred lab group names.

After this you should commit and push the updated students.csv file so your tutors can see it.

Manual syncronisation#

If the Canvas API doesn’t work for your course then you can download the excel spreadsheet from MyTimetable manually and use Lucy to import it as a students.csv file. Here is the process:

  1. login to the MyTimetable staff website
  2. navigate to your course
  3. click “Select All” for the ComA (or other default name) activity
  4. click “Custom Report” excel image
  5. select all rows and click the right arrow >
  6. click “Get Report”
  7. run poetry run ./lucy.py mytt-sync /PATH/TO/DOWNLOADED/EXCEL.xlsx
  8. follow the prompts

If you want to map the MyTimetable group names to your preferred lab group names, set the (optional) group_names mapping in the mytt section of data/course_info.yml. As with canvas-sync, commit and push the updated data/students.csv afterwards so your tutors can see it.

Tutor & lab group information#

You need to add information about your tutors into data/course_info.yml and can also define which tutors are responsible for which tutor. This data allows you to do marking allocations to each tutor and arrange it so that tutors mark students from their own labs if this is desired.

The value for the top-level groups key must be an array; each element contains the information for a specific group. The “default” course info file has an example in there already, edit it to suit your needs.

Note: the group names must match the small group names in MyTimetable exactly. Or if mapping has been set up in data/course_info.yml then they must match the mapped names.

Group names#

Wherever the CLI takes a group_name parameter, usually using the -G or --group flags to specify this, the group name must match a group name from the course info file. All the CLI commands will validate these inputs and bail out if they see an unknown group name.

The one exception to this is the special group name all, which will perform the operation on all students (i.e. the whole class). All is usually the default if no other target is specified.

Targets#

Most commands that act on student repos take an assignment name and then a target flag saying which students to act on. With no target flag, they act on every student (--group all).

There are 3 target flags:

--group or -G: specify a lab group to run the command on, or all for every student
--tutor or -T: specify a tutor name to run the command on where the allocations for that assignment for that tutor has been filled out
--uid or -U: specify a uid to run the command on (for use as just a single student)

Worked example#

So this is what for example clone will look like:

poetry run ./lucy.py clone ass1
clones all ass1 repos

poetry run ./lucy.py clone ass1 -G mon3pm
clones all ass1 repos by students registered to mon3pm

poetry run ./lucy.py clone ass1 -T Brent
clones all ass1 repos assigned to Brent in the ass1 allocations file

poetry run ./lucy.py clone ass1 -U u1234567
clones the ass1 repo for only the student with the uid u1234567

These flags are available for most commands, if unsure check with poetry run ./lucy.py COMMAND --help.

Assignment information#

The assignment information for the course (GitLab template repo, due dates, git branches, extensions, feedback/marking templates etc.) must be provided under the top-level assignments key in the course info file.

The assignment name is just the name of the template repo: e.g. compXXXX-20XX-assignment-1. There is also provision for providing per-student alternate due dates (e.g. for an extension).

Again, all the CLI commands will validate their assignment_name parameter and bail out if they see an unknown assignment name.

Due Dates#

Lucy works on due dates using the pendulum python package and formatted time strings.

The drawback of this is that due date strings are treated as absolute to the second.

For example:

A due date string of 2023-08-12 21:00 is treated as 2023-08-12 21:00:00, meaning that something submitted even 1 second later at 21:00:01 is considered a late submission. This catches a lot of students out because they like to make the submission on the literal due time, and then factoring in push times etc. results in a lot of students having a late submission, which can often lead to a 0 in most CS courses.

Thus lucy has been outfitted with a “tolerance” value that can be set which is added onto any students due date.

The tolerance value is specified as a whole number in minutes to be added to any due date, including extensions. It can be used in the following ways and order of priority:

  1. A specific assignment’s due_date_tolerance key
  2. The top level global_due_date_tolerance key
  3. If neither are specified, then a default value of 15 minutes is used

To disable this behaviour, set the global_due_date_tolerance key to 0 (not recommended).

Moss Extended#

By default moss can be run on every submission in the current year, however there exist a few ways to add extra files / folders to check on, this can be done in 2 ways:

Previous years, same format#

If you want to add previous years submissions which happen to be in the same format (that is also gitlab repos with the same branches etc.) then you can do so by having a copy of these repos in the submissions/assignment_name/moss-extra/20XX/ folder which should consist of the submissions as a folder with the uid. These can then be operated on by adding the --include-extras flag to the moss command.
eg. poetry run ./lucy.py moss <assignment_name> <-_ target> --include-extras

Everything else#

These files can be included by changing the moss_extras function in lucy/compXXXX.py to return a list of paths to add to the moss results to be checked.

Allocating students to tutors#

Before tutors start marking, you can assign students to specific tutors using the allocate command. This generates an allocations file at data/assignments/<shortname>-allocations.yml, which tutors can then use to clone only their assigned students with --tutor <name>.

poetry run ./lucy.py allocate <assignment_name> [OPTIONS]

The key options for controlling how students are distributed:

Who marks what:

  • --assigned / -A — tutors mark students from their own labs (where possible)
  • --out-group / -OG — tutors mark students from labs they don’t teach
  • --random / -R — students are assigned randomly (default)

How many marks each tutor gets:

  • --equal / -EQ — all tutors mark the same number of students (default)
  • --assigned / -A — marking load is weighted by how many labs each tutor teaches
  • --dist-interactive / -DI — set ratios interactively

Which tutors are included:

  • --all-tutors / -AT — all tutors listed in course_info.yml (default)
  • --lab-tutors / -LT — only tutors assigned to at least one lab
  • --tutors-interactive / -ST — select tutors interactively

By default allocate sources the student list from repos already cloned locally (--by-cloned). Use --by-list to allocate from the full student list instead.

After running allocate, commit and push the allocations file so tutors can use it.

Automated marking file population#

If your course uses GitLab CI jobs to determine standard deductions (e.g. failing automated tests), you can run update-marking-files after forking to populate those deductions automatically into each student’s marking file:

poetry run ./lucy.py update-marking-files <assignment_name> [OPTIONS]

Run this after fork and once the CI jobs triggered by the fork have finished running. If your course doesn’t use CI-based deductions, you don’t need this command.

After updating, the command will prompt you to push the updated marking files back to the marking repos.

On-web marking (tutors mark in the GitLab web UI)#

Instead of every tutor installing lucy locally, you can run marking entirely in the GitLab web UI. The convenor is the only person who needs a working lucy setup; tutors just need to be in a GitLab group. This workflow was developed in COMP3900 in 2025 and has run for two semesters.

The moving parts:

  1. Put your tutors in a GitLab group and name it in course_info.yml under gitlab: tutor_group: (e.g. compXXXX/20YY).

  2. After the deadline, fork as usual, then share every marking fork with the tutor group at MAINTAINER level:

    poetry run ./lucy.py share-marking-forks <assignment_name>
    

    Re-running is safe (shares are refreshed, not duplicated). Use -U for a single late fork (e.g. after an extension).

  3. Generate the allocations file as usual (allocate). You can now send each tutor links to their allocated marking forks — tutors open each repo in the web UI, read the submission, and edit the marking file(s) (and pam.md if needed) directly in the web editor, committing as they go.

  4. Periodically (e.g. from cron), refresh the local marking clones and publish a progress report:

    poetry run ./lucy.py clone <assignment_name>
    poetry run ./lucy.py pull <assignment_name>
    poetry run ./lucy.py marking-report <assignment_name>
    poetry run ./lucy.py collate <assignment_name>
    

    marking-report writes reports/<short_name>/marking_report.md: one section per tutor, linking each allocated student’s marking fork with a per-repo pass/fail from the marking checks and completion percentages. Commit reports/ to a branch the tutors can see (e.g. a marks branch of your course’s lucy fork) and they can track their own progress — and you can see at a glance who is behind.

The rest of the workflow (check, push-feedback, canvas-upload) is unchanged: because tutors commit their marks straight to the marking forks on the server, your regular pull picks them up.

Reviewing Potential Academic Misconduct#

Once tutors have finished marking, you can get a consolidated view of all submissions flagged for Potential Academic Misconduct (PAM):

poetry run ./lucy.py pam <assignment_name> [OPTIONS]

This scans all marking files for students where PAM: true has been set and prints a summary. Use the standard target flags (-G, -T, -U) to narrow the scope.

Collating marks#

poetry run ./lucy.py collate <assignment_name> [-G group_name]

collates the marks from the local marking clones (so pull first) into:

  • a “full” marks file in marks/full with all the info (student name, group, etc.)
  • one or more gradebook CSV files in marks/gradebook (one per git branch), which canvas-upload reads

It also prints summary statistics for the marks and saves them to reports/<short_name>/<assignment_name>-mark-statistics.md, ready to commit somewhere your tutors can see.

Students listed under the assignment’s skip_push_feedback key are left out of the gradebook files unless you pass --include-skip-uids true. Pass --insert-mark-breakdown true to append each student’s mark breakdown to their comment.

Uploading marks to Canvas#

After running collate, you can upload the marks and comments directly to Canvas:

poetry run ./lucy.py canvas-upload <assignment_name>

This reads the gradebook CSV generated by collate and pushes marks and comments to the Canvas assignment with the same name as the lucy assignment. If your Canvas assignment is named differently, set canvas_name under the assignment in course_info.yml; if no Canvas assignment matches, lucy lists the names it can see. Run canvas-sync first to make sure your local student list has up-to-date Canvas student IDs.

Existing marks in Canvas will be overwritten if present in the gradebook. Existing comments will not be overwritten by default — they are appended to. To clear existing comments for students in the gradebook before uploading, use:

poetry run ./lucy.py canvas-upload <assignment_name> --delete-comments

To use a different gradebook CSV than the one generated by collate, specify it with:

poetry run ./lucy.py canvas-upload <assignment_name> -F path/to/file.csv

The CSV must have UID, marks, and comment as its first three columns.

Returning feedback to students#

poetry run ./lucy.py push-feedback <assignment_name> [OPTIONS]

builds a feedback.md from each marking file (the mark, plus everything below the frontmatter) and pushes it to a new branch of the student’s own repo, named <branch>-<assignment_name>-feedback (set feedback_postfix on the assignment to change the last part). pull first, so you’re pushing the latest marks.

Students listed under the assignment’s skip_push_feedback key in course_info.yml are skipped (e.g. while an academic integrity case is open); --include-skipped-uids true pushes to them as well. If you’ve changed marks after feedback has already gone out, re-run with --overwrite to push a new commit to students whose feedback differs from their local marking files.

The assignment’s extra_params (hide_mark, hide_sections, hide_feedback, show_deductions) control what the feedback file shows, and included_feedback_files lists extra files from the marking fork to send along.

Course-specific commands (plugins)#

Course-specific commands and helpers go in a plugins/ directory at the top of your course’s lucy fork. Every plugins/*.py file is imported when lucy starts, after the course config has loaded, so plugins can use lucy’s modules (lucy.students, lucy.gitlab, lucy.utils, …). Any click.Command defined at the top level of a plugin becomes a lucy command:

# plugins/comp1234.py
import click
import lucy.students as students


@click.command(name="1234-count")
def count() -> None:
    """how many students are enrolled"""
    click.echo(len(students.uids("all")))

Plain functions need no decorator: run one with

poetry run ./lucy.py run <function> [args...]

which passes its arguments as strings (use module:function if two plugins define the same name). This is a good home for one-off scripts too.

A plugin that fails to import stops lucy from starting, so a broken plugin gets fixed rather than silently skipped.

Listing and monitoring students#

list-students#

Print the student list for a group in various formats:

poetry run ./lucy.py list-students [-G <group_name>] [--format <format>]

Available formats: name, uid, email, md (default), ed, edlabs.

The --format=email option is handy for getting in touch with a specific lab group — but note the warning Lucy prints: make sure you BCC to avoid leaking email addresses. The ed and edlabs formats produce output suitable for pasting into Ed Discussion.

list-unenrolled#

List students who have unenrolled from the course since the last canvas-sync or mytt-sync:

poetry run ./lucy.py list-unenrolled [--format <format>]

Available formats: name, uid, email, degree, md (default).

Useful for checking whether any already-marked students have since withdrawn.

list-pushes#

Show the full push history for a specific student’s assignment repo:

poetry run ./lucy.py list-pushes <assignment_name> <uid> <branch>

This queries the GitLab events API (not the git commit timestamps, which students can falsify) to show when pushes actually reached the server. Handy for checking whether a student worked steadily or only pushed at the last minute.

badger#

List students who haven’t forked the template repo or haven’t pushed anything to their fork:

poetry run ./lucy.py badger <assignment_name> [OPTIONS]

Use --format=email to get a list of email addresses to contact. Students with extensions are excluded automatically. This is most useful in the first few weeks of an assignment to identify students who haven’t started.

If badger (or fork) reports that a student has no fork when you can see that they obviously do, the cause is usually access rather than absence: student forks are private, and the marker only reaches them by being a member. See when the fork hook doesn’t fire.

Direct cloning (without deadline restriction)#

The standard fork + clone workflow uses the last commit before the deadline. If you need to look at a student’s repo as it currently exists on GitLab (ignoring deadlines), use:

poetry run ./lucy.py direct-clone <assignment_name> [OPTIONS]

This clones repos into submissions/<assignment_name>/direct/<uid> rather than the normal submissions/<assignment_name>/<group>/<uid> path, so they won’t interfere with marked submissions.

To update direct-cloned repos after they’ve been cloned:

poetry run ./lucy.py direct-pull <assignment_name> [OPTIONS]
bars search times