devilry_qualifiesforexam
¶
Database models, APIs and UI for qualifying students for final exams.
UI workflow¶
How users are qualified for final exam i plugin-based. The subject/period admin is taken through a wizard with the following steps/pages:
- If no configuration exists for the period:
List the title and description of each plugin (see Plugins below), and let the user select the plugin they want to use. The selection is stored in
QualifiesForFinalExamPeriodStatus.plugin
.- If a configuration exists for the period:
Show the overview of the semester (basically the same as the preview described as page 3 below). Includes a button to change the configuration. Clicking this button will show the list of plugins, just like when no configuration exists, with the previously used plugin selected. The change-button is only available on active periods.
Completely controlled by the plugin. May be more than one page if that should be needed by the plugin. The plugin can also just redirect directly to the next page if it does not require any input from the user. We supply a box with save and back buttons that should be the same for all plugins.
Preview the results with the option to save or go back to the previous page.
Plugins¶
A plugin is a regular Django app. Your best source for a simple example is the
devilry_qualifiesforexam_approved
-module which contains two plugins. You will find the
package in the src/
-directory of the devilry repository.
The role of the plugin¶
A plugin is basically one or more Django views that, for the qualifies-for-exam system, acts like a black box with the following input and output:
The input is a dict store by the qualifies-for-exam system in the users session (
request.session
):periodid
The ID of the class:devilry.apps.core.models.Period.
pluginsessionid
An ID that is generated by the qualifies-for-exam system. It is used to ensure that we do not get session key collisions when using the wizard from multiple browser windows at the same time.
The output is a
devilry_qualifiesforexam.pluginhelpers.PreviewData
-object stored in the users session (request.session
) under thequalifiesforexam-<pluginsessionid>
key. The output object is used by the REST-api that generates the preview-data.
Registering an app as a qualifiesforexam plugin¶
Add something like the following to yourapp/devilry_plugin.py
:
from devilry_qualifiesforexam.registry import qualifiesforexam_plugins
from django.urls import reverse
from django.utils.translation import gettext_lazy
qualifiesforexam_plugins.add(
id='myapp',
url=reverse('myapp-myplugin'), # The url of the view to use for step/page 2 in the workflow - the input parameters (see above) is added to this url.
title=gettext_lazy('My plugin'),
description=gettext_lazy('Does <strong>this</strong> and <em>that</em>.')
)
Create the view¶
See Plugin helpers and take a look at the sourcecode for
devilry_qualifiesforexam_approved
(in the src/
directory of the Devilry sources).
Configure available plugins¶
Available plugins are configured in settings.DEVILRY_QUALIFIESFOREXAM_PLUGINS
, which is
a list of plugin ids. Note that the apps containing the plugin must also be in
settings.INSTALLED_APPS
, and the urls must be registered.
The plugins are shown in listed order on page 1 of the wizard described in the
UI workflow.
Note
You can safely remove plugins from settings.DEVILRY_QUALIFIESFOREXAM_PLUGINS
.
They will simply not be available in the list of plugins in the
UI workflow.
Write tests¶
If you want your plugin to be considered for inclusion in Devilry you will have to write good
tests. These plugins handle very sensitive data, so it would be madness to deploy them in production
without proper tests. We provide a helper-mixin for tests,
devilry_qualifiesforexam.pluginhelpers.QualifiesForExamPluginTestMixin
, which you should
use. See the tests
-module in devilry_qualifiesforexam_approved
for examples.
Plugin helpers¶
The mixin classes¶
QualifiesForExamPluginViewMixin
is a mixin class
that simplifies the common tasks for all plugin views (getting input and setting
output).
Basic usage¶
Basic usage of the class turns the input and output steps described in
The role of the plugin into two methods:
get_plugin_input_and_authenticate()
, save_plugin_output()
. Those two
methods greatly simplify writing plugins. For example, we can create a view like this:
from django.views.generic import View
class MyPluginView(View, QualifiesForExamPluginViewMixin):
def post(self, request):
try:
self.get_plugin_input_and_authenticate()
except PermissionDenied:
return HttpResponseForbidden()
# Your code to detect passing students
passing_relatedstudentsids = [1,2,3]
self.save_plugin_output(passing_relatedstudentsids)
return HttpResponseRedirect(self.get_preview_url())
A more complete example¶
The example above is very simple. You will usually have to iterate over all the students in a period to find out who qualifies:
from django.views.generic import View
from devilry_qualifiesforexam.pluginhelpers import PeriodResultsCollector
from devilry_qualifiesforexam.pluginhelpers import QualifiesForExamPluginViewMixin
class MyPeriodResultsCollector(PeriodResultsCollector):
def student_qualifies_for_exam(self, aggregated_relstudentinfo):
# Test if the student in the AggreatedRelatedStudentInfo qualifies.
# Typically something like this (all students must pass all assignments):
for assignmentid, grouplist in aggregated_relstudentinfo.assignments.iteritems():
feedback = grouplist.get_feedback_with_most_points()
if not feedback or not feedback.is_passing_grade:
return False
return True
class MyPluginView(View, QualifiesForExamPluginViewMixin):
def post(self, request):
try:
self.get_plugin_input_and_authenticate()
except PermissionDenied:
return HttpResponseForbidden()
# Your code to detect passing students
passing_relatedstudentsids = MyPeriodResultsCollector().get_relatedstudents_that_qualify_for_exam()
self.save_plugin_output(passing_relatedstudentsids)
return HttpResponseRedirect(self.get_preview_url())
- class devilry_qualifiesforexam.pluginhelpers.QualifiesForExamPluginViewMixin¶
- periodid¶
The ID of the period — set by
get_plugin_input()
.
- period¶
The period object loaded using the
django.shortcuts.get_object_or_404()
— set byget_plugin_input()
.
- pluginsessionid¶
The pluginsessionid described in The role of the plugin — set by
get_plugin_input()
.
- get_plugin_input_and_authenticate()¶
Reads the parameters (periodid and pluginsessionid) from the querystring and store them as in the following instance variables:
periodid
,period
,pluginsessionid
.- Raise:
django.core.exceptions.PermissionDenied
if the request user is not administrator on the period.
- save_plugin_output(*args, **kwargs)¶
Shortcut that saves a
PreviewData
in the session key generated usingcreate_sessionkey()
. Args and kwargs are forwarded toPreviewData
.
- save_settings_in_session(settings)¶
Save settings in the session. You get this back as an argument to your
post_statussave
-handler if your plugin is configured withuses_settings=True
.
- get_preview_url()¶
Get the preview URL - the URL you must redirect to after saving the output (
save_plugin_output()
) to proceed to the preview.
- get_selectplugin_url()¶
Get the preview URL - the URL you should navigate to when users select Back from your plugin view.
- redirect_to_preview_url()¶
Returns a
HttpResponseRedirect
that redirects toget_preview_url()
.
Helper for unit tests¶
- class devilry_qualifiesforexam.pluginhelpers.QualifiesForExamPluginTestMixin¶
Mixin-class for test-cases for plugin-views (the views that typically inherit from
QualifiesForExamPluginViewMixin
). This class has a couple of helpers that simplifies writing tests, and some unimplemented methods that ensure you do not forget to write permission tests.Note
If you use this class as base for your tests, your chances of getting a plugin approved for inclusion as part of Devilry is greatly increased. You have to include at least one test in addition to the unimplemented tests, a test that uses a realistic dataset to make sure your plugin behaves as intended (E.g.: Approves/disapproves the expected students). You may need more than one extra test if your plugin is complex.
- testhelper¶
A
devilry.apps.core.testhelper.TestHelper
-object which is required forcreate_feedbacks()
andcreate_relatedstudent()
to work.Typcally created with something like this in
setUp
:from django.test import TestCase from devilry.apps.core.testhelper import TestHelper class TestMyPluginView(TestCase, QualifiesForExamPluginTestMixin): def setUp(self): self.testhelper = TestHelper() # Create: # - the uni-node with ``uniadmin`` as admin # - the uni.sub.p1 period with ``periodadmin`` as admin. # - the a1 and a2 assignments within ``p1``, with separate groups on each # assignment for student1 and student2, and with examiner1 as examiner. # - a deadline on each group self.testhelper.add(nodes='uni:admin(uniadmin)', subjects=['sub'], periods=['p1:admin(periodadmin):begins(-3):ends(6)'], assignments=['a1', 'a2'], assignmentgroups=[ 'gstudent1:candidate(student1):examiner(examiner1)', 'gstudent2:candidate(student2):examiner(examiner1)'], deadlines=['d1:ends(10)'] )
- period¶
The period you use in your tests. Needs to be set in the
setUp
-method forcreate_relatedstudent()
to work. Typically defined with the following code after the core in the example intesthelper
:self.period = self.testhelper.sub_p1
Create and return a related student on the
period
. A user with the given username is created if it does not exist.
- create_feedbacks(*feedbacks):
Create feedbacks on groups from the given list of
feedbacks
.- Parameters:
feedbacks –
Each item in the arguments list is a
(group, feedback)
tuple wheregroup
is thedevilry.apps.core.models.AssignmentGroup
-object that it to be given feedback, andfeedbacks
is a dict with attributes for thedevilry.apps.core.models.StaticFeedback
with the following keys:grade
See
devilry.apps.core.models.StaticFeedback.grade
.points
See
devilry.apps.core.models.StaticFeedback.points
.is_passing_grade
See
devilry.apps.core.models.StaticFeedback.is_passing_grade
.
A delivery to save the feedback on is created automatically, so all that is needed of the groups is an examiner, a candidate and a deadline.
Example:
self.create_feedbacks( (self.testhelper.sub_p1_a1_gstudent2, {'grade': 'B', 'points': 86, 'is_passing_grade': True}), (self.testhelper.sub_p1_a2_gstudent2, {'grade': 'A', 'points': 97, 'is_passing_grade': True}) )
- test_perms_as_periodadmin()¶
Must be implemented in subclasses.
- test_perms_as_nodeadmin()¶
Must be implemented in subclasses.
- test_perms_as_superuser¶
Must be implemented in subclasses.
- test_perms_as_nobody¶
Must be implemented in subclasses.
- test_invalid_period¶
Must be implemented in subclasses.
Other helpers¶
- class devilry_qualifiesforexam.pluginhelpers.PreviewData(passing_relatedstudentids)¶
Stores the output from a plugin. You should not need to use this directly. Use
QualifiesForExamPluginViewMixin.save_plugin_output()
instead.- Parameters:
passing_relatedstudentids – See
passing_relatedstudentids
.
List of the IDs of all
devilry.apps.core.models.RelatedStudent
that qualifies for final exams according to the plugin that generated the data.
- devilry_qualifiesforexam.pluginhelpers.create_sessionkey(pluginsessionid)¶
Generate the session key for the plugin output as described in The role of the plugin. You should not need to use this directly. Use
QualifiesForExamPluginViewMixin.get_plugin_input_and_authenticate()
instead.
Plugins shipped with Devilry¶
devilry_qualifiesforexam_approved
¶
TODO
Database models¶
How the models fit together¶
Each time a periodadmin qualifies students for final exams, even when they only partly qualify their
students, a new Status
-record is saved in the database. A status has a ForeignKey to
devilry.apps.core.models.Period
, so the last saved Status is the active
qualified-for-exam status for a Period.
Each time a Status
is saved, all of the devilry.apps.core.models.RelatedStudent`s
for that period gets a :class:
.QualifiesForFinalExam`-record, which saves the qualifies-for-exam
status for the student. When a status is almostready
, we use NULL
in the
QualifiesForFinalExam.qualifies
-field to indicate students that are not ready.
Node administrators or systems that intergrate with Devilry uses Status.exported_timestamp
to mark Status
-records that have been exported to an external system. It is important to
note that we export statuses, not periods. This means that we can create new statuses, and re-export
them. An automatic system can check timestamps to handle status changes, and the Node admin UI
can show/hilight periods with exported statuses and more recent statuses.
DeadlineTag
is used to organize periods by the time when they should have made a
ready
-Status
.
The models¶
- class devilry_qualifiesforexam.models.DeadlineTag¶
A deadlinetag is used to tag
devilry.apps.core.models.Period
-objects with a timestamp and an optional tag describing the timestamp.- timestamp¶
Database field containing the date and time when a period admin should be finished qualifying students for final exams.
- tag¶
A tag for node-admins for this deadlinetag. Max 30 chars. May be empty or
null
.
- class devilry_qualifiesforexam.models.PeriodTag¶
This table is used to create a one-to-many relation from
DeadlineTag
todevilry.apps.core.models.Period
.- deadlinetag¶
Database foreign key to the
DeadlineTag
that the Period should be tagged by.
- period¶
Database foreign key to the
devilry.apps.core.models.Period
that this tag points to.
- class devilry_qualifiesforexam.models.Status¶
Every time the admin updates qualifies-for-exam on a period, we save new object of this database model.
This gives us a history of changes, and it makes it possible for subject/period admins to communicate simple information to whoever it is that is responsible for handling examinations.
- period¶
Database foreign key to the
devilry.apps.core.models.Period
that the status is for.
- exported_timestamp¶
Database datetime field that tells when the status was exported out of Devilry to an external system. This is
null
if the status has not been expored out of Devilry.
- status¶
Database char field that accepts the following values:
ready
is used to indicate the the entire period is ready for export/use.almostready
is used to indicate that the period is almost ready for export/use, and that the exceptions are explained in themessage
.notready
is used to indicate that the period has no useful data yet. This is typically only used when the period used to be ready or almostready, but had to be retracted for a reason explained in the status
- createtime¶
Database datetime field where we store when we added the status.
- message¶
Database field with an optional message about the status change.
- user¶
Database foreign key to the user that made the status change.
- class devilry_qualifiesforexam.models.QualifiesForFinalExam¶
Database one-to-one relation to
devilry.apps.core.models.RelatedStudent
.
- qualifies¶
Boolean database field telling if the student qualifies or not. This may be
None
(NULL
), if the status isalmostready
, to mark students as not ready for export.
- status¶
Foreign key to a
QualifiesForFinalExamPeriodStatus
.