/org-lms

A simple Emacs mode for marking papers with Org.

Primary LanguageHTML

Org-Mode Marking & Integrations for the Canvas LMS

Intro

This repository includes generic functions for interacting with the Canvas LMS as well as instructions for a highly individualized and specific grading setup. Where possible I have tried to distinguish between the two but there are still places where I have not adequately separated them.

Also, this package is fairly unstable due to the low quality of the code :-/. Expect API’s to continue to change.

Installation

This package is developed with emacs 27.0.50 and org-mode from git; there’s no guarantee it will work with other versions, sorry :-(.

It also depends on request.el and org-mime, both of which can be downloaded from MELPA. Request.el in particular has changed a lot in the last little while (mid-2019) so the MELPA version is required.

As of <2019-09-30 Mon> dash, ts, and let-alist are also all dependencies.

I personally use password-store to manage my oauth token and userid, and I recommend that method for others as well.

Once you have the dependencies, then simply

  • clone this repository
  • add the directory to your load-path
  • require org-lms

This is my use-package declaration; you’ll have to adjust the path of course:

(use-package org-lms
:load-path "/home/matt/src/org-grading"
:pin manual
:after (org password-store ts dash let-alist)
:commands (org-lms-setup org-lms-get-courseid)
:config 
;; nothing yet
)

Usage

The Canvas API

Canvas has a rich RESTful API, which accepts and returns JSON representations of many kinds of course-related objects, including students, assignments, announcements, and pages. A basic function, org-lms-canvas-request, uses Emacs’s built-in JSON.el and the powerful request.el library to standardize the formats of requests and responses. Then, for each supported API endpoint, org-lms has a set of convenience functions:

  • one or more getters for retrieving data from Canvas, usually named org-lms-get-*
  • one or more setters for posting or modifying data to Canvas, usually named org-lms-post-*
  • one or more transformers for modidying the data provided by canvas into a form that’s useful for local use. Some of these may become less necessary as I adapt my local schema to come into closer alignment with Canvas, but there will always be extra information that Canvas cannot easily store (Github ID’s, nicknames, etc.).

At present, this package provides only a very limited set of functions related to these categories:

  • courses
  • students
  • assignments
  • announcements

In order to use them, you’ll first need to set the following variables:

  • org-lms-baseurl, a system-wide variablethat represents your institution’s Canvas API URL
  • org-lms-token, your secret API token, which you can acces svia the Canvas web interface (follow the link for details)
  • ORG_LMS_COURSEID, set either as a file-wide keyword or a headline-level org property. You can use org-lms-get-courses to retrieve all the course objects and their :id properties.
  • other ID’s for individual students, assignments, submissions, announcements, etc; the higher-level functions here will retrieve those values and store them in headline-level properties.

Note on GraphQL API <2019-09-30 Mon>

Canvas also has a GraphQL API which is dramatically more efficient than the RESTful API. I would like to ocnvert som portions of this oce to use that API, but that will take a while. See the Github Issue in this repo for more details.

Initial Setup

Create a course-specific directory containing your org-mode source files. In each file, add the #+ORG_LMS_COURSEID: value specific to your course. This will have to be set each year, since the course ids will change.

Write your announcements and assignments as org-mode subtrees, and then post them to canvas with the appropriate convenience functions. Hopefully the most important information from the API response will then be recorded in the headlines.

See ./Grading-template.org for some examples.

Posting the Syllabus

Follow the instructions above, navigate to your syllabus document, and simply M-x org-lms-post-syllabus. If everything’s working properly, the syllabus should appear on the front page of your Canvas site.

This function assumes he syllabus lives in its own file.

Posting Announcements

I’ve added a method to export subtrees as HTML announcements. This saves me having to compose in the web interface (finally!). Just call org-lms-headline-to-announcement from the right subtree (sorry, it won’t traverse up the tree like ox-hugo's “dwim” scope – haven’t programmed that yet!). A successful post will set the ORG_LMS_ANNOUNCEMENT_ID property, and the next time you call from this subtree, the existing announcement will be updated (that is, no new announcement will be posted). Also, the ORLG_LMS_ANNOUNCEMENT_URL property will be set, and a new browser tab will open up with the announcement page.

Creating and Updating Pages

Pages are pretty easy to create from subtrees; just execute org-lms-post-page from a subtree and its content will be uploaded as HTML to your Canvas site.

STARTED Assignments and Marking

Marking is the bulk of the work associated with an LMS and the most complex part of the workflow this package is designed for. These instructions may not be up to date and may also be somewhat idiosyncratic.

See Grading-template.org and Assignments.org for an example of how to set up assignments. The functions defined here expect each assignment to be a subtree. They will look for a number of headline properties and file-wide keyword values before making the API call:

Posting Assignments to the LMS

In order to use this system, assignments need to be created as org-mode headlines and posted to the LMS via =org-lms=. Otherwise the metadata that org-lms relies on for retrieving student work won’t be present. If you have another workflow, you might want to modify some of the existing functions.

Assuming you keep all your assignments in a file Assignments.org in the root directory of your course repo, do the following:

  • add a keyword line #+ORG_LMS_COURSEID: XXXX somewhere in your org file (I prefer the top). You can easily get the course ID just by inspecting the URL of your course, or by using the org-lms-WHATISITAGAIN function
  • In each assignment headline, you’ll want to set a number of properties. This is somewhat tedious, so I recommend creating a template and modifying later:
:DUE_AT: 2018-11-23
:GRADING_TYPE: letter_grade
:OL_PUBLISH:  t
:ASSIGNMENT_TYPE: canvas
:ASSIGNMENT_WEIGHT: 0.10
:CANVAS_SUBMISSION_TYPES: (online_upload)
:PUBLISH:  t
:GRADING_STANDARD_ID: 458

Let’s go through these one by one. They are a little repetitive and should probably be rationalized.

  • DUE_AT sets a due date. Right now, the time component is hard-coded to be 11:59PM EST on that date. This should be fixed!
  • GRADING_TYPE is required ify ou want to use letter grades.
  • If using letter grades, then GRSADING_STANDARD may also be necessary
  • OL_PUBLISH is nil by default, though maybe that should be changed
  • CANVAS_SUBMISSION_TYPES is a list object and must be set if you intend to ocllect student work via Canvas.
  • ASSIGNMENT_TYPE should be set to `canvas` unless you are collecting work some other way
  • Canvas also requires that an ASSIGNMENT_WEIGHT be set, or it won’t record marks properly.
  • OL_DIRECTORY is the directory in which to collect student work. It defaults to a downcased, whitespace-free transformation of the assignment name (that is, the headline content) and will later be created in the main Grading directory if it doesn’t exist (see below).

Once you’ve set the metadata, go ahead and write the assignment. If you include a subheading tagged `rubric` then that subheading will be used later by org-lms when constructing grading headlines (see the next section).

When you’re done, post your work to Canvas with org-lms-parse-assignment (misleading name, should be changed!), and, importantly, parse the assignments file with =org-lms-save-assignment-map=. This will create an emacs-lisp file whose sole contents are an alist containing a representation of the assignment

Retrieving Student Work

I generally use a file called Comments.org and keep it in a directory Grading which I exclude from the main git repo for my course (obvious reasons). This file also needs to have certain metadata set:

  • courseid with #+ORG_LMS_COURSEID: NUMBER
  • location of assignments org file with +#ORG_LMS_ASSIGNMENTS: PATH (Note: this is a *change from earlier practice, and org-lms-setup will no longer work if you do this!)
  • the variable org-lms-merged-students should also be set. This is a little baroque and should be streamlined; the name derives from my perhaps idiosyncratic practice of maintaining a student list that includes both nicknames and github ids for students. Right now, the easiest way to generate this list involves creating a file students.csv and running (org-lms-merge-student-lists) to sync the existing csv and the current list of students (which will change every time someone adds or drops the class).
  • run org-lms-setup-grading to generate a table of assignments. You can then manually click the “create headlines” field

Ideal Workflow

  • [X] Write assignment(s) in Assignments.org (see template)
  • [X] generate an assignments.el file from the WIM contents of Assignments.org, and ideally
  • [X] automaticlaly write to this file every time I upload an assignment
  • [X] inside the grading template,
    • [X] having set the location of assignments.el as a file-level keyword variable,
    • [X] read its contents and
    • [X] use them to generate headlines.

So, this is roughly finished. Now just need to add a few more keywords to make everything run smmmoooooottthh as butter.

The Assignments Object

Each local assignment has as its cdr a plist which will be used to construct the grading document & to handle a variety of grading-related tasks. Here is the initial structure of an assignment:

(test .
                  (:name "Test Assignment"
                         :directory "response-paper-1"
                         :weight 0.10
                         :grade-type "letter"
                         :submission-type "canvas"
                         :rubric-list ("Organization" "Clarity of Argument"
                                       "Grammar and Spelling" "Grade"
                                       "See Attached Paper for further Comments")
                         ))
:name
used both to construct the headline for the assignment, and to associate the local assignment with a Canvas assignment object
:directory
local storage of student work
:weight
used in constructing final grades (not implemented)
:grade-type
one of “letter”, “number”, or “passfail” – but not yet implemented properly
:submission-type
one of “email” “github”, or “canvas”. Should be used in the future for handling (a) attachment of student files and (b) return of student works. Right now there’s no canvas implementation.
:rubric-list
This is what I started with – My grading rubrics are all definition lists, with comments entered at the end of the list entry.

Run org-lms-merge-assignments to add a few extra properties from an associated Canvas assignment. I’m not yet able to automate the creation of these assignments, though that should be possible.

Creating Assignments

There is now preliminary support for uploading assignments to a course. This is very much a work in progress.

Collecting Student work

I have two existing systems for marking student work:

  • students email me their papers or submit via Dropbox. I collect the papers in a single directory.
  • Students submit work via Github Classroom. I bulk-clone their repos and mark via PR comments

It would be nice to replace the first of these with a system for downloading papers directly from Canvas. I’m working on that right now. This is now implemented! Use org-lms-get-canvas-attachments to getthese. Now I need to hook it up to org-lms-make-headinges.

Creating Grading Trees

Running (org-lms-make-headings assignment-name) will generate org heading trees with the following structure:

  • Assignment Name
    • Student Name 1
    • Student Name 2
    • etc

Each headline will have a number of properties set to make marking easier. Existing student papers will be attached to the grading subtree and can be quickly opened with C-c C-a o. I find the workflow very quick and easy. I have libreoffice configured with a few shortcuts for commonly used editing markup (checkmarks, smileyfaces, paragraph marks, and expansion shortcuts for “wrong word” and “awkward”). PDFs are much slower for me to mark, as neither pdf-view nor evince has really excellent text annotation UI. TThis may be a limitation of the PDF annotation standards. For github repos, the PR interface is quite rich for code; for text work it’s a little bit clumsier, but I don’t have a solution for that yet.

Using Agenda to rapidly filter grading trees

The org agenda is a powerful tool for sorting and filtering headlines. Since each student assignment is a headline, we can mis-use the agenda to rapidly navigate to specific places in a comments file. So, for instance, to find all trees with a grade of “0” enter the following:

C-c a < m GRADE="0"

This will open the agenda, searching only the current buffer for matches, matching the property “GRADE” with value “0”

It should be possible to add this to org-agenda-custom-commands, but I’m having trouble with this right now.

Also having trouble matching with “|” (“or”).

Once created, the agenda can be filtered, e.g., search for a partiular student using = and then entering a substring of the student’s name

Unfortunately I’m not very adept right now with the agenda but maybe this wil lget easier!!

Returning student work

Right now I run org-lms-mail-all to mail out all subtrees marked with a READY org-todo state. This is generally fairly reliable, though sometimes there are issues with the message queue.

NEW: I have written org-lms-put-single-submission-from-headline which half-works and is ready for testing.

Known issues

HTML Entities

The canvas API does not accept all HTML5 semantically-named entities.

(let ((entities (json-read-file "/home/matt/entities.json")))
entities)

Development

I’m experimenting with using literate programming and org-babel-tangle as my main development modality. It’s convenient to navigate the various parts of the code, etc. But it may be hard to submit PR’s etc.

Plans (see issues in GH as well)

STARTED Integrate with Canvas API

The Canvas API is described in the offocial docs, which lives in a different form here. Here is a tutorial on using Postman to test canvas lms, and an official Getting Started guide. Here’s a similar resouce organized as a course. Examples in these docs mostly use cURL. Instead we are using request.el (github) and restclient (github) (where appropriate) for inspecting api requests. helpful stackexchange intor to restclient. some advanced org-mode restclient shit to aspire to.

I have had a hell of a time parsing api results; important to always set json-key-type, json-object-type, and json-array-type before invoking json-read. In request this has to be done in the parser declaration which is a pain. Otherwise lists end up as vectors, which sucks b/c I don’t knw how to use vectors in lisp!

Anyway, some progress being made in sample code in ./Grading-template.org, but still have abunch of progress to make on this front!!

In case I forget, you gneerate oauth tkens in the settings pane on quercus.

ACTION elimate remaining cruft

There’s still some shitty junk in here

ACTION write tests

THis is abig one – not sure how to do this!!

Set a “docroot” property i n the parent to make it eadier to find papers in the directory

also makes it easier to give the directory w/ student papers & the headline different names

ACTION generate tables that can be fed back into canvas

this would e nice!

STARTED add in letter/numbber grade conversion

a little bit difficult

make it easier to make a template

generating assignments is too finicky right now. I’d like to be able to do it form the Assignments page (!!)

ACTION Add some CSS

WOuld be nice if the marks were a bit easier to read in email clients

WON’T DO Write a script to grab user.csv

and move it to the right place. But this in each repo or maybe just run from repo.

ACTION Make default messages for github assignments more comprehensible

Shouldn’t be so har.d Also add all comments PRs if you can. Hig