devilry.apps.gradeeditors — Grade editors

Introduction

To make it easy for examiners to create all the information related to a grade, Devilry use grade editors. Grade editors give examiners a unified user-interface tailored for different kinds of grading systems.

What they do

A grade editor is essentially very simple:

Make it easy to create a devilry.apps.core.models.StaticFeedback.

How they store their data — FeedbackDraft

Since grade editors vary greatly, but devilry.apps.core.models.StaticFeedback only have four fields that they can save, grade editors store their actual data in devilry.apps.gradeeditors.models.FeedbackDraft. A draft has a text field, draft, where a grade editors can store their data.

Grade editor specific storage format

The draft field uses a draft-editor specific storage format. Since this plays well with JavaScript, all out built-in grade editors uses JSON to encode this data. However, XML would also be a good alternative, especially for complex data where validation would become difficult with JSON.

From FeedbackDraft to StaticFeedback

devilry.apps.core.models.StaticFeedback is, as the name suggests, unchangable. StaticFeedback objects can only be appended to a delivery. When a devilry.apps.gradeeditors.models.FeedbackDraft is published, it converts the grade-editor specific storage format in the draft-field into XHTML for rendered_view attribute of devilry.apps.core.models.StaticFeedback.

The draft is not deleted, so the original data is still available in the grade-editor specific storage format, while a view of the data is available as a StaticFeedback.

Note: rendered_view is not validated at this point. Howver, we plan to define a subset of XHTML at some point in the future when we have a clearer picture of what developers are able to create using the grade editor framework.

Creating a grade editor

In this guide, we will walk you through the creation of devilry.apps.asminimalaspossible_gradeeditor. This is available in devilry/apps/asminimalaspossible_gradeeditor in the devilry source code.

Add to registry

First of all, we need to register the grade editor with devilry.apps.gradeeditors.registry.gradeeditor_registry. To make the plugin register itself when the server starts, we put the registry code in devilry_plugin.py (see How to write a plugin):

import json
from django.conf import settings

from devilry.apps.gradeeditors import (gradeeditor_registry, JsonRegistryItem,
                                       DraftValidationError, ConfigValidationError)
from devilry.defaults.encoding import CHARSET



class AsMinimalAsPossible(JsonRegistryItem):
    """
    Serves as a minimal example of a grade editor, and as a well suited grade
    editor for use in test cases.
    """
    gradeeditorid = 'asminimalaspossible'
    title = 'Minimal'
    description = 'A minimal grade editor for testing. Allows examiners to select if delivery is approved or not approved.'
    config_editor_url = settings.DEVILRY_STATIC_URL + '/asminimalaspossible_gradeeditor/configeditor.js'
    draft_editor_url = settings.DEVILRY_STATIC_URL + '/asminimalaspossible_gradeeditor/drafteditor.js'

    @classmethod
    def validate_config(cls, configstring):
        config = cls.decode_configstring(configstring)
        cls.validate_dict(config, ConfigValidationError, {'defaultvalue': bool,
                                                          'fieldlabel': basestring})

    @classmethod
    def validate_draft(cls, draftstring, configstring):
        is_approved = cls.decode_draftstring(draftstring)
        if not isinstance(is_approved, bool):
            raise DraftValidationError('The draft string must contain a single boolean value.')
        ## Uncomment to see how validation errors work:
        #raise DraftValidationError('Some error occurred.')

    @classmethod
    def draft_to_staticfeedback_kwargs(cls, draftstring, configstring):
        is_approved = json.loads(draftstring)
        if is_approved:
            grade = 'approved'
        else:
            grade = 'not approved'
        return dict(is_passing_grade=is_approved,
                    grade=grade,
                    points=int(is_approved),
                    rendered_view='Your grade is: {0}'.format(grade.encode(CHARSET)))


gradeeditor_registry.register(AsMinimalAsPossible)

devilry_plugin.AsMinimalAsPossible code explained

Since we use JSON as the data format in asminimalaspossible_gradeeditor, we inherit from devilry.apps.gradeeditors.registry.JsonRegistryItem, a sublclass of RegistryItem.

We set a title and description which administrators should be able to read to get an understanding of what the grade editor provides. Furthermore, we define:

gradeeditorid
A unique id for this grade editor.
config_editor_url and validate_config(...)
See Config editor section below.
draft_editor_url and validate_draft(...)
See Draft editor section below.
draft_to_staticfeedback_kwargs(...)
Convert a draft string in our grade editor specific format into a devilry.apps.core.models.StaticFeedback as explained in How they store their data — FeedbackDraft.

Config editor

The config editor makes it possible for administrators to configure the grade editor. A gradeeditor is not required to provide a config editor.

The config editor has three components: view, storage and validation.

The view

Its view is defined as a ExtJS JavaScript file configured through the config_editor_url attribute. This view is loaded as a child component of the ConfigEditorWindow widget. This widget is available through the this.getMainWin() method.

Saving a config

The main window for config editor provides provides the saveConfig(configstring, onFailure) method that should be used to save the config. saveConfig(...) method makes a request to the server which ends up saving the devilry.apps.gradeeditor.models.Config as long as devilry.apps.gradeeditor.models.Config.clean().

Validation

devilry.apps.gradeeditor.models.Config.clean() uses uses the validate_config(...) method in the registry item that we defined in devilry_plugin.py in Creating a grade editor.

Example code

{
    padding: 20,
    border: false,
    frame: false,
    xtype: 'form', // Does not have to be a form. More complex config editors will probably use a panel with more complex layouts than what forms support.

    /**
     * Called by the config-editor main window when it is opened.
     *
     * @param config Get the grade editor configuration that is stored on the
     *      current assignment.
     */
    initializeEditor: function(config) {
        this.getMainWin().changeSize(400, 200); // Change window size to a more appropritate size for so little content.

        // Load configuration, and fall back on defaults
        var configobj = {
            defaultvalue: false,
            fieldlabel: 'Approved'
        };
        if(config.config) {
            configobj = Ext.JSON.decode(config.config);
        }

        // Create and add the fields
        this.defaultvalueField = Ext.widget('checkboxfield', {
            boxLabel: 'Choose default value',
            checked: configobj.defaultvalue
        });
        this.add(this.defaultvalueField);

        this.fieldlabelField = Ext.widget('textfield', {
            fieldLabel: 'Field label',
            labelWidth: 80,
            width: 340,
            value: configobj.fieldlabel
        });
        this.add(this.fieldlabelField);

        this.getEl().unmask(); // Unmask the loading mask (set by the main window).
    },

    /**
     * Called when the 'Save' button is clicked.
     */
    onSave: function() {
        if (this.getForm().isValid()) {
            var config = Ext.JSON.encode({
                defaultvalue: this.defaultvalueField.getValue(),
                fieldlabel: this.fieldlabelField.getValue()
            });
            this.getMainWin().saveConfig(config, this.onFailure);
        }
    },

    /**
     * @private
     * Get the grade config editor main window.
     */
    getMainWin: function() {
        return this.up('gradeconfigeditormainwin');
    },

    /**
     * @private
     * Used by onSave to handle save-failures.
     */
    onFailure: function() {
        console.error('Failed!');
    },
}

Draft editor

The draft editor works almost like a config editor. The primary difference is:

  • getMainWin() returns a DraftEditorWindow.
  • The draft editor uses draft_editor_url and validate_draft(...).
  • Drafts are saved as devilry.apps.gradeeditor.models.FeedbackDraft.
  • Drafts are appended, not overwritten (each time you save a draft, a new FeedbackDraft database record is saved).
  • Drafts can be published using getMainWin().saveDraftAndPublish(...). This results in draft_to_staticfeedback_kwargs(...) beeing used to create a new devilry.apps.core.models.StaticFeedback.

Example code

{
    padding: 20,
    border: false,
    frame: false,
    xtype: 'form',

    // These 3 settings are optional.
    help: '<h1>Some useful help here</h1>' +
        '<p>Help is never needed, since all users are really smart, and all of them knows the internals of how Devilry works...</p>',
    helpwidth: 500,
    helpheight: 300,

    /**
     * Called by the grade-editor main window just before calling
     * setDraftstring() for the first time.
     *
     * @param config Get the grade editor configuration that is stored on the
     *      current assignment.
     */
    initializeEditor: function(config) {
        this.editorConfig = Ext.JSON.decode(config.config);
        this.checkbox = Ext.widget('checkboxfield', {
            boxLabel: this.editorConfig.fieldlabel
        });
        this.add(this.checkbox);

        // This opens a new window. Just to show how to load classes in a grade editor.
        Ext.require('devilry.asminimalaspossible_gradeeditor.DummyWindow');
        this.add({
            xtype: 'button',
            text: 'click me',
            listeners: {
                scope: this,
                click: function() {
                    var win = Ext.create('devilry.asminimalaspossible_gradeeditor.DummyWindow', {
                        message: 'Just to show how to do loading of classes from draft editor!',
                        buttonLabel: 'Hello world!',
                        listeners: {
                            scope: this,
                            gotSomeValue: this.onGotSomeValue
                        }
                    });
                    win.show();
                }
            }
        });
    },

    onGotSomeValue: function() {
        console.log(stuff);
    },

    /**
     * Called by the grade-editor main window to set the current draft. Used
     * both on initialization and when selecting a draft from history (rolling
     * back to a previous draft).
     *
     * @param draftstring The current draftstring, or ``undefined`` if no
     *      drafts have been saved yet.
     */
    setDraftstring: function(draftstring) {
        if(draftstring === undefined) {
            this.checkbox.setValue(this.editorConfig.defaultvalue);
        } else {
            var approved = Ext.JSON.decode(draftstring);
            this.checkbox.setValue(approved);
        }
        this.getEl().unmask(); // Unmask the loading mask (set by the main window).
    },

    /**
     * Called when the 'save draft' button is clicked.
     */
    onSaveDraft: function() {
        if (this.getForm().isValid()) {
            var draft = this.createDraft();
            this.getMainWin().saveDraft(draft);
        }
    },

    /**
     * Called when the publish button is clicked.
     */
    onPublish: function() {
        if (this.getForm().isValid()) {
            var draft = this.createDraft();
            this.getMainWin().saveDraftAndPublish(draft);
        }
    },



    /**
     * @private
     * Get the grade draft editor main window.
     */
    getMainWin: function() {
        return this.up('gradedrafteditormainwin');
    },

    /**
     * @private
     * Create a draft (used in onSaveDraft and onPublish)
     */
    createDraft: function() {
        var approved = this.checkbox.getValue();
        var draft = Ext.JSON.encode(approved);
        return draft;
    }
}

Using the RESTful API to get data grade editor data

It is common to use the StaticFeedback API to get data from a running Devilry instance. This is explained in the RESTful python bindings page on our wiki.

If you want to have more detailes than the data provided by the StaticFeedback RESTful API, you can fetch data from the gradeeditor APIs directly. Their URLs are:

/gradeeditors/administrator/restfulsimplifiedconfig
/gradeeditors/administrator/restfulsimplifiedfeedbackdraft

However one challenge is that drafts are personal. This means that you will only be able to get drafts that you have made. We will provide an API for administrators to get all drafts some time in the future.

API

devilry.apps.gradeeditors.registry.gradeeditor_registry

A Registry-object.

exception devilry.apps.gradeeditors.registry.ConfigValidationError(message, code=None, params=None)[source]

Bases: django.core.exceptions.ValidationError

Raised when RegistryItem.validate_config() fails to validate the configstring.

exception devilry.apps.gradeeditors.registry.DraftValidationError(message, code=None, params=None)[source]

Bases: django.core.exceptions.ValidationError

Raised when RegistryItem.validate_draft() fails to validate the draftstring.

class devilry.apps.gradeeditors.registry.RegistryItem[source]

Bases: object

Information about a grade plugin.

The attributes documented below are required.

gradeeditorid

A unique string for this editor. If two editors with the same gradeeditorid is registered, an exception will be raised on load time.

title

A short title for the grade editor.

description

A longer description of the grade editor.

config_editor_url

The URL to the config editor.

draft_editor_url::

The URL to the draft editor.

classmethod validate_config(configstring)[source]

Validate configstring and raise ConfigValidationError if it does not validate.

classmethod validate_draft(draftstring)[source]

Validate draftstring and raise DraftValidationError if the validation fails.

classmethod draft_to_staticfeedback_kwargs(draftstring)[source]

Convert draftstring into a dictionary of keyword arguments for StaticFeedback. The returned dict should only contain the following keys:

  • is_passing_grade
  • grade
  • points
  • rendered_view
class devilry.apps.gradeeditors.registry.JsonRegistryItem[source]

Bases: devilry.apps.gradeeditors.registry.RegistryItem

RegistryItem with extra utility functions for use with JSON config and draft strings

classmethod decode_configstring(configstring)[source]

Decode configstring using json.loads and return the result. Raise ConfigValidationError if it fails.

classmethod decode_draftstring(draftstring)[source]

Decode draftstring using json.loads and return the result. Raise DraftValidationError if it fails.

classmethod validate_dict(valuedict, exceptioncls, typedict)[source]

Validate that each key in typedict is in valuedict, and that the type of the values in valuedict reflects the types in typedict.

Raise exceptioncls with an error message if any validation fails.

class devilry.apps.gradeeditors.registry.Registry[source]

Bases: object

Grade editor registry. You should not create a object of this class. It is already available as gradeeditor_registry.

register(registryitem)[source]

Add a RegistryItem to the registry.

getdefaultkey()[source]

Get the default key (the key defined in the DEVILRY_DEFAULT_GRADEEDITOR setting).

itertitles()[source]

Iterate over the registry yielding (key, title).

class devilry.apps.gradeeditors.models.Config(*args, **kwargs)[source]

Bases: django.db.models.base.Model

Stored by admins.

gradeeditorid

A gradeeditorid that is used to get the devilry.apps.gradeeditors.registry.RegistryItem for this config.

assignment

The primary key. This means we have one config for each assignment.

config

A text field where the config editor can store its data in any format it chooses. JSON is usually a good choice because of easy interraction with JavaScript.

clean()[source]

Gets a devilry.apps.gradeeditors.registry.RegistryItem from the gradeeditor_registry in devilry.apps.gradeeditor.registry.

Uses devilry.apps.gradeeditors.registry.RegistryItem.validate_config() to validate the config before saving it.

devilry.apps.gradeeditors.models.create_gradeconfig_for_assignment(sender, **kwargs)[source]

Signal handler which is invoked when an Assignment is created.

Create default grade Config for Assignment with config='' if the assignment has no grade Config.

Parameters:kwargs – Must have an instance key with an assignment object as value.
class devilry.apps.gradeeditors.models.FeedbackDraft(*args, **kwargs)[source]

Bases: django.db.models.base.Model

Stored by examiners.

save(*args, **kwargs)[source]

Save the draft and optionally a devilry.core.models.StaticFeedback in the database. The StaticFeedback is only saved if self.publish is True.

Uses to_staticfeedback() to create the staticfeedback.

to_staticfeedback()[source]

Return a staticfeedback generated from self.