Skip to content

Commit

Permalink
Collecting the scripts into a single commandline program. (#64)
Browse files Browse the repository at this point in the history
* Added argparse with some documentation and subcommands. It should work as before.
* I think this works as intended.
  • Loading branch information
WilliamDue committed Sep 27, 2023
1 parent 6b088f6 commit bf86d0a
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 165 deletions.
1 change: 1 addition & 0 deletions .github/workflows/check-types.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
name: Sanity check
on: [push, pull_request]

jobs:
python-check:
runs-on: ubuntu-latest
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,20 +65,20 @@ Fetch Submissions for an Assignment
-----------------------------------
There are multiple options for fetching submissions.

The general command is `<staffeli_nt_path>/download.py <course_id> <template.yaml> <assignment-dir> [flags]`, where
The general command is `python <staffeli_nt_path> download <course_id> <template.yaml> <assignment-dir> [flags]`, where
- `<staffeli_nt_path>` is the path to the directory where `staffeli_nt` is located, i.e. where the files `download.py` and `upload.py` etc. can be found.
- `<course_id>` is the canvas `course_id` for the course.
- `<template.yaml>` is the template file to use when generating the `grade.yml` file for each submission
- `<assignment_dir>` is a *non-existing* directory, that staffeli will create and store the submissions in.

**Windows**:
Since `staffeli_nt` is written in `python3`, you will need to invoke it via your `python3` interpreter.
Example: `python <staffeli_nt_path>/download.py <course_id> <template.yml> <assignment-dir> [flags]`
Example: `python <staffeli_nt_path> download <course_id> <template.yml> <assignment-dir> [flags]`

**Fetching all submissions**:
To fetch **all** submissions from the course with id `12345`, using the template-file `ass1-template.yml` and create a new directory "ass1dir" to store the submissions in:

$ <staffeli_nt_path>/download.py 12345 ass1-template.yml ass1dir
$ python <staffeli_nt_path> download 12345 ass1-template.yml ass1dir

This will present you with a list of assignments for the course, where you will interactively choose which assignment to fetch.
For each submission, a directory will be created in `<assignment_dir>`, in which the handed-in files of the submission will be stored, alongside a file `grade.yml` generated form the `<template.yml>` for a TA to fill out during grading of the assignment.
Expand All @@ -91,7 +91,7 @@ Submission comments, if any, will be downloaded as well, and stored alongside `g
What we call "Hold", canvas/absalon calls sections.
To fetch all submissions for an assignment, where the student belongs to a given section, and the `<course_id>` is `12345`:

$ <staffeli_nt_path>/download.py 12345 ass1-template.yml ass1dir --select-section
$ python <staffeli_nt_path> download 12345 ass1-template.yml ass1dir --select-section

This will present you with a list of assignments for the course, where you will interactively choose which assignment to fetch, followed by a list of sections for you to choose from.

Expand All @@ -111,7 +111,7 @@ TA2:

To then fetch all submissions for an assignment for a given TA:

$ <staffeli_nt_path>/download.py <course_id> ass1-template.yml ass1dir --select-ta ta_list.yml
$ python <staffeli_nt_path> download <course_id> ass1-template.yml ass1dir --select-ta ta_list.yml

where `ta_list.yml` is a YAML-file following the above format.

Expand All @@ -131,36 +131,36 @@ This will (attempt to) run onlineTA for each downloaded submission.
#### Fetching only ungraded submissions (resubs)
It is possile to only fetch submissions that are either ungraded or have a score < 1.0.
Currently this is implemented specifically for the PoP-course and might not be available in the current form in later releases.
This can be achieved by appending the `--resub` flag to any use of the `download.py`-script.
This can be achieved by appending the `--resub` flag to any use of the `download` subcommand.



Upload Feedback and grades
--------------------------

Use `upload.py <template.yaml> <assignment-dir> [--live] [--step]`.
Use `python <staffeli_nt_path> upload <template.yaml> <assignment-dir> [--live] [--step]`.
The default to do a *dry run*, that is **not** to upload anything
unless the `--live` flag is given.

For instance, to review all feedback for submissions in the directory
`ass1` before uploading:

$ <staffeli_nt_path>/upload.py ass1-template.yml ass1 --step
$ python <staffeli_nt_path> upload ass1-template.yml ass1 --step


To upload all feedback for submissions in the directory
`ass1`:

$ <staffeli_nt_path>/upload.py ass1-template.yml ass1 --live
$ python <staffeli_nt_path> upload ass1-template.yml ass1 --live

To upload feedback for a single submission:

$ upload_single.py <POINTS> <meta.yml> <grade.yml> <feedback.txt> [--live]
$ python <staffeli_nt_path> upload-single <POINTS> <meta.yml> <grade.yml> <feedback.txt> [--live]


To generate `feedback.txt` locally for submissions in the directory `ass1`:

$ <staffeli_nt_path>/upload.py ass1-template.yml ass1 --write-local
$ python <staffeli_nt_path> upload ass1-template.yml ass1 --write-local


Template format
Expand Down
23 changes: 23 additions & 0 deletions shell.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
nativeBuildInputs = [
(pkgs.python39.withPackages(ps: with ps; [
ruamel-yaml
(
buildPythonPackage rec {
pname = "canvasapi";
version = "2.2.0";
src = fetchPypi {
inherit pname version;
sha256 = "5087db773cac9d92f4f4609b3c160dbeeceb636801421808afee2d438bc43f62";
};
doCheck = false;
propagatedBuildInputs = [
pkgs.python39Packages.pytz
pkgs.python39Packages.requests
];
}
)
]))
];
}
46 changes: 46 additions & 0 deletions staffeli_nt/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3


import argparse
import scan
import download
import info
import upload
import upload_single
import os
import sys
from pathlib import Path

def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title="subcommands", dest="subcommand")
scan.add_subparser(subparsers)
download.add_subparser(subparsers)
info.add_subparser(subparsers)
upload.add_subparser(subparsers)
upload_single.add_subparser(subparsers)

path_token = os.path.join(
str(Path.home()),
'.canvas.token'
)

if not os.path.exists(path_token):
print(f'Error: Missing Canvas token at {path_token}.')
sys.exit(0)

api_url = 'https://absalon.ku.dk/'

with open(path_token, 'r') as f:
api_key = f.read().strip()

args = parser.parse_args()
if not hasattr(args, 'main'):
parser.print_help()
return

args.main(api_url, api_key, args)


if __name__ == '__main__':
main()
68 changes: 29 additions & 39 deletions staffeli_nt/download.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
#!/usr/bin/env python3

import os
import sys
import glob
import shutil
import zipfile
import hashlib
import re
from zipfile import BadZipFile, ZipFile
from zipfile import BadZipFile
from pathlib import Path
import requests
from typing import Dict, Any
import argparse


from vas import *
Expand All @@ -35,15 +32,8 @@ def smart_key(name):
def sort_by_name(named):
return sorted( list(named), key=lambda x: smart_key(x.name) )

def ta_file(args):
for idx, arg in enumerate(args):
if arg == "--select-ta":
return args[idx+1]
return ""


def grab_submission_comments(submission):
if (len(submission.submission_comments) == 0):
if len(submission.submission_comments) == 0:
return []
comments = []
for comment in submission.submission_comments:
Expand All @@ -54,30 +44,30 @@ def grab_submission_comments(submission):
comments = "\n".join(sorted(comments))
return comments

if __name__ == '__main__':
course_id = sys.argv[1]
path_template = sys.argv[2]
path_destination = sys.argv[3]
path_token = os.path.join(
str(Path.home()),
'.canvas.token'
)
select_section = '--select-section' in sys.argv
select_ta = ta_file(sys.argv) # use --select-ta file.yaml

resubmissions_only = '--resub' in sys.argv
def add_subparser(subparsers: argparse._SubParsersAction):
parser : argparse.ArgumentParser = subparsers.add_parser(name='download', help='fetch submissions')
parser.add_argument('course_id', type=str, metavar='INT', help='the course id')
parser.add_argument('path_template', type=str, metavar='TEMPLATE_PATH', help='path to the YAML template')
parser.add_argument('path_destination', type=str, metavar='SUBMISSIONS_PATH', help='destination to submissions folder')
parser.add_argument('--select-section', action='store_true', help='whether section selection is used')
parser.add_argument('--select-ta', type=str, metavar='PATH', help='path to a YAML file with TA distributions')
parser.add_argument('--resub', action='store_true', help='whether only resubmissions should be fetched')
parser.set_defaults(main=main)


def main(api_url, api_key, args: argparse.Namespace):
course_id = args.course_id
path_template = args.path_template
path_destination = args.path_destination
select_section = args.select_section
select_ta = args.select_ta # use --select-ta file.yaml
resubmissions_only = args.resub

# sanity check
with open(path_template, 'r') as f:
template = parse_template(f.read())

API_URL = 'https://absalon.ku.dk/'

with open(path_token, 'r') as f:
API_KEY = f.read().strip()


canvas = Canvas(API_URL, API_KEY)
canvas = Canvas(api_url, api_key)
course = canvas.get_course(course_id)

assignments = sort_by_name(course.get_assignments())
Expand All @@ -90,7 +80,7 @@ def grab_submission_comments(submission):
assignment = assignments[index]

ta = None
if select_ta:
if select_ta is not None:
with open(select_ta, 'r') as f:
try:
(tas,stud) = parse_students_and_tas(f)
Expand All @@ -109,7 +99,7 @@ def grab_submission_comments(submission):
students += course.get_users(search_term=i,enrollment_type=['student'],
enrollment_state='active')
section = None
if select_section:
if select_section is not None:
sections = sort_by_name(course.get_sections())

print('\nSections:')
Expand All @@ -122,19 +112,19 @@ def grab_submission_comments(submission):


print(f'\nFetching: {assignment}')
if select_ta:
if select_ta is not None:
print(f'for {ta}')
if select_section:
if select_section is not None:
print(f'from {section}')

handins: Dict[str, Any] = {}
participants = []
empty_handins = []
submissions = []

if select_ta:
if select_ta is not None:
submissions = [assignment.get_submission(s.id, include=['submission_comments']) for s in students]
elif section:
elif section is not None:
s_ids = [s['id'] for s in section.students if all([ e['enrollment_state'] == 'active'
for e in s['enrollments']])]
submissions = section.get_multiple_submissions(assignment_ids=[assignment.id],
Expand All @@ -157,7 +147,7 @@ def grab_submission_comments(submission):
# NOTE: This is a terribly hacky solution and should really be rewritten
# collect which attachments to download
# if only fetching resubmissions
if resubmissions_only:
if resubmissions_only:
if hasattr(submission, 'score'):
print(f'Score: {submission.score}')
# If a submission has not yet been graded, submission.score will be None
Expand Down

0 comments on commit bf86d0a

Please sign in to comment.