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.
A grade editor is essentially very simple:
Make it easy to create a devilry.apps.core.models.StaticFeedback.
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.
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.
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.
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.
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)
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:
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.
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.
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().
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.
{
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!');
},
}
The draft editor works almost like a config editor. The primary difference is:
{
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;
}
}
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.
Bases: django.core.exceptions.ValidationError
Raised when RegistryItem.validate_config() fails to validate the configstring.
Bases: django.core.exceptions.ValidationError
Raised when RegistryItem.validate_draft() fails to validate the draftstring.
Bases: object
Information about a grade plugin.
The attributes documented below are required.
A unique string for this editor. If two editors with the same gradeeditorid is registered, an exception will be raised on load time.
A short title for the grade editor.
A longer description of the grade editor.
The URL to the config editor.
The URL to the draft editor.
Validate configstring and raise ConfigValidationError if it does not validate.
Validate draftstring and raise DraftValidationError if the validation fails.
Bases: devilry.apps.gradeeditors.registry.RegistryItem
RegistryItem with extra utility functions for use with JSON config and draft strings
Decode configstring using json.loads and return the result. Raise ConfigValidationError if it fails.
Bases: object
Grade editor registry. You should not create a object of this class. It is already available as gradeeditor_registry.
Add a RegistryItem to the registry.
Bases: django.db.models.base.Model
Stored by admins.
A gradeeditorid that is used to get the devilry.apps.gradeeditors.registry.RegistryItem for this config.
The primary key. This means we have one config for each assignment.
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.
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.
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. |
---|
Bases: django.db.models.base.Model
Stored by examiners.
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.