Tag Archives: Free Software

Semi Open Book Exams

A few years ago, I switched one of my first year courses to use what I call a semi-open-book approach.

Open-book exams of course allow students to bring whatever materials they wish into them, but they have the disadvantage that students will often bring in materials that they have not studied in detail, or even at all. In such cases, sifting through materials to help them answer a question could be counter productive.

On the other hand, the real world is now an increasingly “open-book” environment, which huge amounts of information available to those in the workplace which is now almost always Internet connected.

So I decided to look at another approach. Students are allowed to bring in a single, personalised, A4 sheet, on which they can write whatever they wish on both sides. There are a few rules:

  • the sheet must be written on “by hand”, that is to say, it cannot be printed to from a computer, or typed;
  • the sheet must be “original”, that is to say, it cannot be a photocopy of another sheet (though students may of course copy their original for reference);
  • the sheet must be the student’s own work, and they must formally declare as much (with a tick box);
  • the sheet must be handed in with the exam paper, although it is not marked.

The purpose of these restrictions are to ensure that each student takes a lead in producing an individual sheet, and to inhibit cottage industries of copied sheets.

In terms of what can go on the sheet? Well anything really. It can be sections from notes, important formulae, sample questions or solutions. The main purpose here is to prompt students to work out what they would individually distill down to an A4 page. So they go through all the module notes, tutorial problems and more, and work out the most valuable material that deserves to go on one A4 page. I believe that this process itself is the greatest value of the sheet, its production rather than its existence in the exam. I’m working on some research to test this.

So I email them each an A4 PDF, which they can print out at home, and on whatever colour paper they may desire. The sheet is individual and has their student number on it with a barcode, for automated processing and analysis afterwards for a project I’m working on, but this is anonymised. The student’s name in particular does not appear, since in Ulster University, it does not appear on the exam booklet.

The top of my sheet looks like this:

The top of a sample guide sheet.

So, if you would like to do the same, I am enclosing the Python script, and LaTeX that I use to achieve this. You could of course use any other technology, or not individualise the sheet at all.

For convenience the most recent code will also be placed on a GitHub repository here, feel free to clone away.

My script has just been rewritten for Python 3.x, and I’ve added a lot of command line parameters to decouple it from me and Ulster University only use. It opens a CSV file from my University which contains student id numbers, student names, and emails in specific columns. These are the default for the script but can be changed. For each student it uses LaTeX to generate the page. It actually creates inserts for each student of the name and student number, you can then edit open-book.tex to allow the page to be as you wish it. You don’t need to know much LaTeX to achieve this, but ping me if you need help. I am also using a LaTeX package to create the barcodes automatically.

I’ve spent a bit of time adding command line parameters to this script, but you can try using

python3 open-book.py --help

for information. The script has been rewritten for Python 3. If you run it without parameters it will enter interactive mode and prompt you.

I’d strongly recommend running with the –test-only option at first to make sure all looks good, and opening open-book.pdf will show you the last generated page so you can see it’s what you want.

Anyway, feel free to do your own thing, or mutilate the code. Enjoy!

#!/usr/bin/env python

#
# Copyright Colin Turner 2014-2017
#
# Free and Open Source Software under GPL v3
#
import argparse

import csv
import re
import subprocess
import smtplib
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText


def process_student(args, row):
    """Takes a line from the CSV file, you will likely need to edit aspects of this."""
    'kicks of the processing of a single student'
    student_number = row[args.student_id_column]
    student_name = row[args.student_name_column]
    student_email = row[args.student_email_column]
    print('  Processing:', student_name , ':', student_email)
    create_latex_inserts(student_number, student_name)
    create_pdf()
    send_email(args, student_name, student_email)


def create_latex_inserts(student_number, student_name):
    """Write LaTeX inserts for the barcode and student name

    For each student this will create two tiny LaTeX files:

     * open-book-insert-barcode.tex which contains the LaTeX code for a barcode representing the student number
     * open-book-insert-name.tex which will contain simply the student's name

    These files can be included/inputted from open-book.tex as desired to personalise that document

    student_number is the ID in the students record system for the student
    student_name is the name of the student"""

    # Open a tiny LaTeX file to put this in
    file = open('open-book-insert-barcode.tex', 'w')

    # All the file contains is LaTeX to code to create the bar code
    string = '\psbarcode{' + student_number + '}{includetext height=0.25}{code39}'
    file.write(string)
    file.close()

    # The same exercise for the second file to contain the student name
    file = open('open-book-insert-name.tex', 'w')
    string = student_name
    file.write(string)
    file.close()


def create_pdf():
    """Calls LaTeX and dvipdf to create the personalised PDF with inserts from create_latex_inserts()"""

    # Suppress stdout, but we leave stderr enabled.
    subprocess.call("latex open-book", stdout=subprocess.DEVNULL, shell=True)
    subprocess.call("dvipdf open-book", stdout=subprocess.DEVNULL, shell=True)


def send_email(args, student_name, student_email):
    """Emails a single student with the generated PDF."""
    #TODO: Might be useful to improve the to address
    #TODO: Allow subject to be tailored.

    subject = args.email_subject
    from_address = args.email_sender
    # to_address = student_name + ' <' + student_email + '>'
    to_address = student_email

    msg = MIMEMultipart()
    msg['Subject'] = subject
    msg['From'] = from_address
    msg['To'] = to_address

    text = 'Dear Student\nPlease find enclosed your guide sheet template for the exam. Read the following email carefully.\n'
    part1 = MIMEText(text, 'plain')
    msg.attach(part1)

    # Open the files in binary mode.  Let the MIMEImage class automatically
    # guess the specific image type.
    fp = open('open-book.pdf', 'rb')
    img = MIMEApplication(fp.read(), 'pdf')
    fp.close()

    msg.attach(img)

    # Send the email via our own SMTP server, if we are not testing.
    if not args.test_only:
        s = smtplib.SMTP(args.smtp_server)
        s.sendmail(from_address, to_address, msg.as_string())
        s.quit()


def override_arguments(args):
    """If necessary, prompt for arguments and override them

    Takes, as input, args from an ArgumentParser and returns the same after processing or overrides.
    """

    # If the user enabled batch mode, we disable interactive mode
    if args.batch_mode:
        args.interactive_mode = False

    if args.interactive_mode:
        override = input("CSV filename? default=[{}] :".format(args.input_file))
        if len(override):
            args.input_file = override

        override = input("Student ID Column? default=[{}] :".format(args.student_id_column))
        if len(override):
            args.student_id_column = int(override)

        override = input("Student Name Column? default=[{}] :".format(args.student_name_column))
        if len(override):
            args.student_name_column = int(override)

        override = input("Student Email Column? default=[{}] :".format(args.student_email_column))
        if len(override):
            args.student_email_column = int(override)

        override = input("Student ID Regular Expression? default=[{}] :".format(args.student_id_regexp))
        if len(override):
            args.student_id_regexp = override

        override = input("SMTP Server? default=[{}] :".format(args.smtp_server))
        if len(override):
            args.smtp_server = override

        override = input("Email subject? default=[{}] :".format(args.email_subject))
        if len(override):
            args.email_subject = override

        override = input("Email sender address? default=[{}] :".format(args.email_sender))
        if len(override):
            args.email_sender = override

    return(args)


def parse_arguments():
    """Get all the command line arguments for the file and return the args from an ArgumentParser"""

    parser = argparse.ArgumentParser(
        description="A script to email students study pages for a semi-open book exam",
        epilog="Note that column count arguments start from zero."

    )

    parser.add_argument('-b', '--batch-mode',
                        action='store_true',
                        dest='batch_mode',
                        default=False,
                        help='run automatically with values given')

    parser.add_argument('--interactive-mode',
                        action='store_true',
                        dest='interactive_mode',
                        default=True,
                        help='prompt the user for details (default)')

    parser.add_argument('-i', '--input-file',
                        dest='input_file',
                        default='students.csv',
                        help='the name of the input CSV file with one row per student')

    parser.add_argument('-sidc', '--student-id-column',
                        dest='student_id_column',
                        default=1,
                        help='the column containing the student id (default 1)')

    parser.add_argument('-snc', '--student-name-column',
                        dest='student_name_column',
                        default=2,
                        help='the column containing the student name (default 2)')

    parser.add_argument('-sec', '--student-email-column',
                        dest='student_email_column',
                        default=9,
                        help='the column containing the student email (default 9)')

    parser.add_argument('-sidregexp', '--student-id-regexp',
                        dest='student_id_regexp',
                        default='B[0-9]+',
                        help='a regular expression for valid student IDs (default B[0-9]+)')

    parser.add_argument('--smtp-server',
                        dest='smtp_server',
                        default='localhost',
                        help='the address of an smtp server')

    parser.add_argument('--email-subject',
                        dest='email_subject',
                        default='IMPORTANT: Your semi-open-book Guide Sheet',
                        help='the subject of emails that are sent')

    parser.add_argument('--email-sender',
                        dest='email_sender',
                        default='noreply@nowhere.org',
                        help='the sender address from which to send emails')

    parser.add_argument('-t', '--test-only',
                        action='store_true',
                        dest='test_only',
                        default=False,
                        help='do not send any emails')

    args = parser.parse_args()

    # Allow for any overrides from program logic or interaction with the user
    args = override_arguments(args)
    return(args)


def main():
    """the main function that kicks everything else off"""

    print("Hello")
    args = parse_arguments()

    print("Starting open-book...")
    print(args)
    csvReader = csv.reader(open(args.input_file, 'r'), dialect='excel')

    student_count = 0
    # Go through each row
    for row in csvReader:
        student_number = row[args.student_id_column]
        # Check if the second cell looks like a student number
        if re.match(args.student_id_regexp, row[args.student_id_column]):
            student_count = student_count + 1
            process_student(args, row)
        else:
            print('  Skipping: non matching row')

    print('Stopping open-book...')


if __name__ == '__main__':
    main()

I use a LaTeX template for the base information, this can be easily edited for taste.

\documentclass[12pt,a4paper]{minimal}
\usepackage[latin1]{inputenc}
\usepackage{pst-barcode}
\usepackage[margin=2cm]{geometry}


%
% Does it all have to be Arial now? <sigh>
%
\renewcommand{\familydefault}{\sfdefault}

\author{Professor Colin Turner}
\begin{document}
\begin{centering}
\textbf{EEE122 Examination Guide Sheet}

This sheet, and its contents that you have added, can be brought into
the examination for EEE122. The contents \textbf{must} be compiled
by yourself, be handwritten, and be original (i.e. \textbf{NOT} 
photocopied or similar). You may use the
reverse side. You may retain a copy you have made before the examination
but the original must be handed in with your examination scripts at the
end of your examination.

%\input{open-book-insert-name.tex}
% D'Oh! Not supposed to put a name on anything going in the exam.
\input{open-book-insert-barcode.tex}
\hfill
\begin{pspicture}(7,1in)
%\psbarcode{
%\input{./open_book_insert.tex}
%B00526636
%}{includetext height=0.25}{code39}
\input{open-book-insert-barcode.tex}
\end{pspicture}
\end{centering}

\vfill
\begin{centering}
Please read the following declaration and tick the box to indicate you agree:

I declare this sheet to have been compiled by myself and not by another, and that the student number above is mine.

%\rule[-1 cm]{10 cm}{1 pt}
\framebox[0.3 cm]{ }

\end{centering}

\end{document}

Pretty Printing C++ Archives from Emails

I’m just putting this here because I nearly managed to lose it. This is a part of a pretty unvarnished BASH script for a very specific purpose, taking an email file containing a ZIP of submitted C++ code from students. This script produces pretty printed PDFs of the source files named after each author to facilitate marking and annotation. It’s not a thing of beauty. I think I’ll probably write a new cleaner version in future.

#!/bin/bash
# 
# A script to take C++ files in coursework and produce pretty printed PDF
# listings named with the author information.
#
# It takes a ZIP file of .cpp and .h files and produces a ZIP file of PDFs
#

# Requires
#   enscript
#   ps2pdf
#   munpack

#
# Called for each file to be encoded
#
pretty_print_file()
{
  # Extract the Author JavaDoc information
  author=(cat1 | sed -n -e 's/^.*@[Aa]uthor .*/\1/gp')   # And the local part of the email address   author_snip=(cat 1 | sed -n -e 's/^.*@[Aa]uthor.*<<img src="https://www.piglets.org/blog/wp-content/ql-cache/quicklatex.com-6bbee176f5e99b093ce5754610f65df6_l3.png" class="ql-img-inline-formula quicklatex-auto-format" alt=".*" title="Rendered by QuickLaTeX.com" height="9" width="12" style="vertical-align: 0px;"/>@.*/\1/gp')

  # How many lines did we get back?
  lines=(echo "author_snip" | wc -l)

  # If we got no author info
  if [ lines -eq "0" ]     then       author="no-author"       author_snip="no-author"   fi    # If we got no author info                                                                                         if [ -z "author_snip" ]
    then
      author="no-author"
      author_snip="no-author"
  fi

  # if we got too many
  if [ lines -ge "2" ]     then       author="multiple-authors"       author_snip="multiple-authors"   fi    echo "File1, Author author (author_snip)"
  output=author_snip-1
  output+=".pdf"
  echo "Encoding output..."   enscript -q --color=1 -C -r -Ecpp -fCourier8 -o -1 | ps2pdf - parsed-output/output }  # # Usage info # if [ ! -f1 ]
  then
    echo "Usage: unpack_coursework <email_file>"
    exit
fi

# Make a temporary directory and copy the email file into it.
echo "Creating temporary directory..."
dir=`mktemp -d`
echo dir cp1 dir # Move to that directory pushddir

# Unpack the email
echo "Unpacking email..."
munpack 1  # Create a directory into which to drop the pretty printed output mkdir parsed-output  # Not so elegant, but extract any .cpp and .h source from resulting ZIPs echo "Unpacking any zips..." for f in *.zip do   unzip -Cjf *.cpp
  unzip -Cj f *.h done  # Produce an author renamed, pretty printed PDF for each header file shopt -s globstar echo "Parse .h files..." for f in *.h do   pretty_print_filef
done

# And the same for source files
echo "Parse .cpp files..."
for f in *.cpp
do
  pretty_print_file f done  # Pull together all the pretty printed content into a new ZIP echo "Zipping parsed output..." cddir/parsed-output
zip parsed-output *.pdf
cd ..

# Back to the directory we started in.
popd

# Copy the parsed ZIP to the current directory for inspection and marking
cp dir/parsed-output/parsed-output.zip .  # Cleanup echo "Deleting temporary directory..." #echodir
rm -rf $dir

 

 

OPUS and Assessment 2 – Adding Custom Assessments

This is a follow on to the previous article on setting up assessment in OPUS, an on-line system for placement learning. You probably want to read that first. This is much more advanced and requires some technical knowledge (or someone that has that).

Making New Assessments

Suppose that OPUS doesn’t have the assessment you want, then you will have to build your own, from scratch, or by modifying an existing one. This takes some minor HTML skill and access to your OPUS code to add a new file. So if you can’t do this yourself, ensure you get appropriate support.

Look at an existing assessment closely first. Go back to Advanced on the OPUS admin menu, and then Assessments.

For each assessment, clicking on Structure allows access to underlying variables that are captured. These can be numeric, text, or checkboxes, and some validation is possible too.

The Structure of an Assessment

you need to work out what things you will capture, and create a skin for the assessment, most usually from modifying one from another. This following snippet from a related Smarty template shows this is just HTML, but OPUS, through Smarty drops in an assessment variable that gives access to any existing values, and any validation errors. <pre class="lang:xhtml decode:true" title="An extract from a template.">{* Smarty *} {* Template for SEME Final Visit *} {* Assessment specific layout *} {* Really, only the form contents need to be added *} {* Note the use of get_value and flag_error to *}Â Â  Â  {* bring in assessment specific material *}  ... ...  <tr> <td class="property" rowspan="5">Use of English</td> <td colspan="1"><strong>Mark</strong></td><td colspan="4"><strong>Comments</strong></td></tr>  <tr><td colspan="1">   {assessment->flag_error(“mark5″)}
<input type=”text” class=”data_entry_required” size=”2″ value=”{assessment->get_value("mark5")}" name="mark5"> </td> <td colspan="4">   {assessment->flag_error(“comment5″)}
{include file=”general/assessment/textarea.tpl” name=”comment5″ rows=”7″ cols=”60″}
</td>
</tr>

<tr><td colspan=”5″>Marking Scheme</td></tr>
<tr>
<td> Uses language to clearly express views concisely </td>
<td> Expresses clearly but with some minor errors </td>
<td> Good expression, logical flow, reasonably concise </td>
<td> Reasonable flow, some contorted expressions, a little verbose </td>
<td> Poor expression, verbose, some colloquialisms </td>
</tr>


This is a representative snippet. You can see this full template here. Note the “special” code in between braces { }. The variables in the template pertain to the names in the structure.

Create and Save Your Template

Create your template, probably using one of the existing ones to help you understand the format. This provides the layout and skin for your pro-forma and allows you to do anything you can wish with HTML/CSS. Be mindful of security considerations, but you aren’t writing main code, just an included bit. OPUS will top and tail the file for you when it runs.

Save it under the templates/assessments directory in your OPUS install. I recommend you make a subdirectory for your institution.

Avoid using the “uu” directory. This is used for pre-shipped assessments and those used at Ulster University. There is a chance your changes will get clobbered by a new OPUS version if you put your template in there.

Adding the Assessment variables into OPUS

Then you need to create your new Assessment item itself as at the top of the article. Once you have created it, click on structure and add each variable you will capture in turn, whether it is text, a number, or a checkbox, and any simple validation rules – such as minimum or maximum values.

The final detail of one variable
The final detail of one variable

The description appears in feedback and validation, so make sure it is meaningful to the end user. The name is the variable name as it appears in your template. The weighting field is used to determine if numeric values contribute to the score. Usually use 1 if you want the score to be counted, and 0 if you want the score to be ignored. Finally you can choose whether each field is compulsory or not. Optional fields will be ignored in a total when OPUS creates a percentage.

Once complete, add your new assessment into a test regime as detailed in the first article and do some careful testing before adding the regime to live students.

OPUS and Assessment 1 – The Basics

OPUS is a FOSS (Free and Open Source Software) web application I wrote at Ulster University to manage work based learning. It has been, and is used by some other universities too.

Among its features is a way to understand the assessment structure for different groups and how it can change over years in such a way that legacy data is still correct for audit.

You don’t have to use the in-built assessment functionality in OPUS, but the features were written to promote transparency of assessment, and ensure all stakeholders could easily access assessment information for a student.

So here’s how to do it, it takes a bit of set-up but then should run smoothly until you ever decide to change how you assess. This is one of a short series on the matter.

Assessment Regimes

OPUS uses a “bank” of individual assessments that can be built from different weightings into as assessment regime. To be precise OPUS provides a means of capturing the rubric for each assessment and the feedback to students. Each assessment has a Smarty template which “skins” the assessment form. These can be found in the Assessment section of the Advanced tab of the admin interface.

A list of OPUS assessments
A list of OPUS assessments

For most people using OPUS, you build an assessment regime from these components in a pick and mix fashion. Head to the Configuration tab, and select Assessment Groups. This may well be empty, in an out-of-the-box install, in which case create a group with an appropriate name and some commentary on what it is for.

A list of Assessment Groups
A list of Assessment Groups

Once you have a group, you will see an option to edit the regime that is associated with it.

A typical assessment regime.
A typical assessment regime.

When we add an item, a dialog appears to enter some information.

A regime item.
A regime item.

In this we pick which of the assessments from the very start we want to use, you might decide, for instance, to use the same assessment twice in a given regime, at different stages. Give the student a description of what the assessment name should be for them, a weighting (which could be zero for formative only assessments).

You can also specify who should assess this – it could be the academic tutor assigned to the student, the workplace supervisor, the student themselves or labelled as “other”.

The year is specified in relation to the year of placement, and should usually therefore be zero. Finally start and end are the month and day (MMDD) for when work should begin on such assessments, and the deadline. These are used to help prompt staff and order assessments for students.

Adding Regimes to Programmes

Once an assessment regime has been created, you need to tell OPUS you want to use it with students in a given programme.

Go to Configuration and then Organisation Details and get to the school of study that’s relevant and pull up their list of programmes. For each programme you can click on assessment, from here you can select which regime is appropriate for the programme, and the year in which the regime started and ended being valid. You can leave out an end year to let the decision roll on.

More often than not you wish to apply these changes to at least a School. Clicking on Bulk Change Assessment will allow you to select all the programmes within a School, the new assessment regime you want and the start year, and it will do the rest.

Once you have done this the functionality in OPUS to show the assessments, their structures and marks, and to enable marking will appear for all relevant students and the staff working with them.

Sample Assessment Information
Sample Assessment Information

A table like that above will appear under each related student (this one is dummy information) and students can click view to see the pro-forma whether complete or not to understand how they will be assessed, or what the results were as appropriate.

An assessment pro-forma
An assessment pro-forma

Naturally staff who have no business with a student cannot see the marks or information pertaining to them.

When completing an assessment on a student a member of staff has 24 hours to edit their findings before the results “lock” and can only be removed by an administrator – this allows most minor errors to be corrected.

Workload Allocation Modelling Update – Scalability

I have been doing some more work on my software to handle Academic Workload Modelling, developing a roadmap for two future versions, one being modifications needed to run real allocations for next year without scrapping existing data, and another being code to handle the moderation of exams and coursework (which isn’t really anything to do with workload modelling, there’s some more mission creep going on).

Improvements to Task Handling

Speaking of mission creep I noted in the last article I’d added some code to capture tasks that staff members would be reminded off and could self-certify as complete. I improved this a lot with more rich detail about when tasks were overdue and UI improvements.

I wanted to automate some batch code to send emails from the system periodically. I discovered that using a Django management command provided an elegant way to the batch mode code into the project that could be called with cron through the usual Django manage.py script that it creates to handle its own internal related tasks for the project from the command line.

#
# Regular cron jobs for the wam package
#
#  m h  dom mon dow user command
# Every week
#
# Each Monday at 7 am, send all reminders
0 7 * * 1 root /usr/bin/python3 /usr/local/share/wam/manage.py email_reminders
# Each Wednesday and Friday at 7 am, send reminders for overdue and less than 7 days to deadline tasks
0 7 * * 3,5 root /usr/bin/python3 /usr/local/share/wam/manage.py email_reminders --urgent-only

It was easy to use this framework to add command switches and configuration of verbosity (you might note I haven’t disabled all output at the moment so I can monitor execution at this stage). I have set this up to email folks on a Monday morning with all the tasks, but also on Wednesday and Friday if there are urgent tasks still outstanding (less than a week to deadline).

I’ve been using this functionality live and it has worked very well. I used Django templates to help provide the email bodies, both in HTML and plain text.

Sample Task Reminder Email
Sample Task Reminder Email

Issues of Scale

My early prototype handled data for one academic year, albeit with fields in the schema to try and solve this at a later stage. It also suffered from a problem in that if other Schools wanted to use the system, how would I disaggregate the data both for security and convenience?

In the end I hit upon a solution for both issues, a WorkPackage model that allows a range of dates (usually one academic year) and a collection of Django User Groups to be specified. This allows all manually allocated activities, and module data to be specified with a package and therefore both invisible to other packages (users in other Schools, or in other Academic Years). I was also able to put the constants I’m using to model workload into the Django model, making it easier to tweak year on year.

class WorkPackage(models.Model):
    '''Groups workload by user groups and time
    
    A WorkPackage can represent all the users and the time period
    for which activities are relevant. A most usual application
    would be to group activities by School and Academic Year.
    
    name        the name of the package, probably the academic unit
    details     any further details of the package
    startdate   the first date of activities related to the package
    enddate     the end date of the activities related to the package
    draft       indicates the package is still being constructed
    archive     indicates the package is maintained for record only
    groups      a collection of all django groups affected
    created     when the package was created
    modified    when the package was last modified
    nominal_hours
                the considered normal number of load hours in a year
    credit_contact_scaling
                multiplier from credit points to contact hours
    contact_admin_scaling
                multiplier from contact hours to admin hours
    contact_assessment_scaling
                multiplier from contact hours to assessment hours
    
    '''
    
    name = models.CharField(max_length = 100)
    details = models.TextField()
    startdate = models.DateField()
    enddate = models.DateField()
    draft = models.BooleanField(default=True)
    archive = models.BooleanField(default=False)
    groups = models.ManyToManyField(Group, blank=True)
    nominal_hours = models.PositiveIntegerField(default=1600)
    credit_contact_scaling = models.FloatField(default=8/20)
    contact_admin_scaling = models.FloatField(default=1)
    contact_assessment_scaling = models.FloatField(default=1)
    created = models.DateTimeField(auto_now_add = True)
    modified = models.DateTimeField(auto_now = True)
    
    def __str__(self):
        return self.name + ' (' + str(self.startdate) + ' - ' + str(self.enddate) + ')'
    
    class Meta:
        ordering = ['name', '-startdate']

I’m pretty much ready to use the system for a real allocation now without having to purge the test data I used this this year. I can simply create a new WorkPackage.

I need to write some functionality to allow one package’s allocations to be automatically rolled over to the next as a starting point, but I reckon that’s maybe two or three more hours.

Future Plans for the Application

The next part of planned functionality is an ability to handle coursework and examination and the moderation process. It will be quite a big chunk of new functionality and moving the system again to something quite a bit bigger than just a workload allocation system.

This of course means I need a better Application name, (WAM isn’t so awesome anyway). Suggestions on a post card.

Django Issues

I think I’m getting more to grips with Django all the time – although I often have the nagging feeling I’m writing several lines of code that would be simpler if I had a better feel for its syntax for dealing with QuerySets.

The big problem I hit, again, was issues in migrations. I created and executed migrations on my (SQLite) development system, but when I moved these over to production (MySQL) it barfed spectacularly.

Once again the lack of idempotent execution means you have to work out what part of the migration worked and then tag the migration as “faked” in order to move onto the next. This was sufficient this time, and I didn’t have to write custom migrations like last time, but it’s really not very reassuring.

Further Details

As before, the code is on GitHub, and the development website on foss.ulster.ac.uk, if you want more details.

Boot problems with systemd? Check /etc/fstab

My (actually this) Debian server failed to boot after a power failure last week, it turns out the graphics card failed too, probably because of the cold and the thermal shock, but replacing the card did not allow the computer to boot.

With systemd, if something happens in the boot process, despite some obviously specific failure triggering the problem, it tends to fob you off with a message to run

journalctl -xb

This feels like the machine equivalent of “Hey, I just saw some needle go past that broke the machine. Let me hand you a haystack so you can find it.”

The command that is a whole lot more useful, is:

systemctl status systemd-modules-load.service

which allows much more rapid diagnosis of many problems. In my case the whole thing turns out to be an old line in /etc/fstab – an obselete line to mount /proc/bus/usb. It shouldn’t have been there anymore, but I am slightly amazed and irritated that the whole boot process was abandoned because of one line in /etc/fstab.

But hopefully someone else in this situation will find the suggestion to check that file first useful.

Manually completing a botched django migration

I wrote a lot of code for my Workload Allocation system on Friday, and had been developing it on the machine with django’s built in lightweight web server, and a (default) sqlite database backend. In production I decided to use a MySQL backend in case sqlite was, well, too lite.

One of the things that is really neat about django, but which also profoundly scares me, is that it handles changes to the database schema automatically. I am used to doing all of this by hand. It has been a pleasant change, but I wondered what would happen if it went wrong.

Which it did on Friday. The migrations had worked perfectly well on the development server and after some testing I decided to roll the code into production, whereupon the migration failed. I’m still not sure why, but something in the django deep magic failed. To make things worse the process is, I have discovered, not idempotent, and trying to run the migration again caused it to fail in new places because some of the database schema changes had been successful; so it was now bailing out with “already exists” kind of errors.

Removing some tables and trying again didn’t quite do the trick. I thought about trying to fix the schema manually, since with the mysql command line tool I could see what fields needed to be added, but upon inspection the restraints added by django were complex and I was unsure how important they were.

So this is my clumsy workaround, that will no doubt come back to haunt me.

I used the following commands from the top of the django app directory to find the name of the migration that was failing, and than used –fake to force django to forget about having to apply it.

python manage.py migrate --list
python manage.py migrate --fake [failed migration]

I then created a “manual” django migration that added the new fields.

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ('loads', '0013_auto_20151130_1540'),
    ]

    operations = [
        migrations.AddField(
            model_name='modulestaff',
            name='module',
            field=models.ForeignKey(to='loads.Module'),
        ),
        migrations.AddField(
            model_name='modulestaff',
            name='staff',
            field=models.ForeignKey(to='loads.Staff'),
        ),
   

  ]

It turns out that getting the dependency right at the top is very important, it needs to be previous migration.

The name of this script is important, follow the naming convention of your most recent failed migration, changing auto to custom and the timestamp appropriately. I discovered that django, would not run this migration. It detected a conflict with the previous migration that should have created the fields and wanted me to try and merge them. That would be pointless since the previous migration failed. I also discovered to my surprise there was no –force command line switch to override this logic, though Google perhaps suggests that previous versions of django allowed this.

So, I used the sqlmigration django command to output the correct SQL that it would produce if this migration did run. Once I got it showing in the shell, I forwarded this to a file.

python manage.py sqlmigration [migrationname] > fixme.sql

Finally I used the mysql command line tool

mysql -u root -p [appname]

to get access to the database, and then used the following command to import and run the SQL produced above.

source fixme.sql

And so far so good. I had been getting Server Errors on pages relating to the botched model before and at the moment they seem to be behaving correctly. Hopefully this may help you and not come back to haunt me.

Workload Allocation Monitoring (WAM) Prototype

I decided to start writing a workload allocation monitoring system for Higher Education. I found one written as part of a JISC project at Cambridge, but despite my experience with PHP I found it difficult to set-up, a bit crude (sorry) and hard to maintain. It was clearly very flexible, and I wanted something flexible, simple and clean.

So I decided I’d try writing something quickly using the Python django framework. This is my first web-app written in Python and so I dare say I would do some things differently with more experience, but I have now reached the point where I have a workable prototype that I can start to use myself. I’ve got to say, I found django to be pretty neat.

At its heart is a list of the loads against Academic Staff in a department or school. The idea is to try and increase transparency. There are problems with this approach: some known irregularities of loading can be for confidential reasons; small numbers of staff with key skills can cause issues as well, but it is intended to provide a basis.

Overall loads for staff.
Overall loads for staff.

 

 

 

 

While classically the word semester implies that there are two of them, most Universities operate a three semester system with the third covering the Summer. Unevenness in loading over the Summer is another cause of potential trouble, so the system tries to show loading as spread across semesters. A scaled column accounts for staff who do not have a 100% FTE contribution but their hours are up-scaled for comparison.

Naturally staff will want to see some granularity of these loads and they are broken into individual activities that are allocated to given members of staff.

Breakdown of activities for a staff member.
Breakdown of activities for a staff member.

An individual activity can be specified as occupying a number of hours, or alternatively a percentage of a staff member’s time. It can occupy one or more semesters (in which case it is spread evenly across them). Types can be allocated for activities to help track contributions of different types. It might be that an activity is related to a module or study, or not.

Activities are long term parts of work allocated hours or a percentage of time.
Activities are long term parts of work allocated hours or a percentage of time.

Speaking of modules basic information is stored for these, and another issue I think will help, tracking the submission of exams and coursework through various QA processes.

At a glance the most recent information about the exam and coursework status can be seen.
At a glance the most recent information about the exam and coursework status can be seen.

While activities are considered to be events with long engagements, another issue for staff are tasks that are allocated to them, usually of comparatively short duration. It can be hard to staff to remember all of these tasks, and hard for manager to follow up their completion, especially without annoying staff who have completed them already.

Tasks can be allocated against individual members of staff or groups or both.
Tasks can be allocated against individual members of staff or groups or both.

The web-app will allow tasks to be defined against one person, many people, categories of people and so on.

A list of tasks and their deadlines.
A list of tasks and their deadlines.

 

 

 

 

 

It is possible to easily see which tasks are still open and whether their deadline has come and gone.

The staff required to complete a task are shown, and those that have indicated completion. The system politely nags those still outstanding.
The staff required to complete a task are shown, and those that have indicated completion. The system politely nags those still outstanding.

A look at a given task will show who has completed it and who still needs to.

A given staff member can sign off their own task.
A given staff member can sign off their own task.

 

It is often the case that admin and clerical staff check off colleagues who have responded to a given call, so the system allows for staff with given permissions to indicate someone has having completed the task. Alternatively the member of staff can do this for themselves.

So while it is still a bit rough and ready I’ve reached the point where the system is stable enough for use. Of course the challenge comes when we consider the assumptions to come up with the hours and percentage loading in the first place. So I hope to pick the brains of some colleagues about this and start testing the system.

I’ve yet to make a formal release, but the code is Affero GPL (you can use the code free of restrictions (and charge) but cannot deprive others of the same freedom on derivative works) so feel free to have a look at it.

My roadmap for an initial release can be found on foss.ulster.ac.uk, where I will eventually host the code as well, but at the moment it can be found at GitHub. My previous post detailed how to get the app to work with a central authentication system your University likely has, or something similar.

Yeah… design and CSS is not my strongest skill, more work to be done on that.

Share and enjoy.

Django, CAS authentication and Apache

I am certainly no stranger to Web Development, but I decide to really look at the Python web framework django in some detail last week to write a small web application for Workload Modelling for Academic Staff.

Yes, this is a geeky, programming post.

In doing so I ran into some trouble trying to get CAS authentication to work with the app. I tried using a django-cas client I found, having found no direct CAS support in django. This took a reasonable number of code modifications, in several source files (really only a pain because I would have to maintain both development code and production code on different authentication). However the critical problem was that while I could get authentication into the “userland” parts of the app, I was getting redirect issues with the django generated administration interface.

So, I found a totally different approach. Django does have generic remote user support built-in which I hadn’t initially found. There are some details here. As you can see there are only two lines of code needed to enable this support.

I found this worked without any drama when I used Apache to force the CAS authentication. So the code required (in version 1.8 of django) is simply as follows, in the settings.py file.

MIDDLEWARE_CLASSES = (
    '...',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    # This is where the new line needs to be added
    'django.contrib.auth.middleware.RemoteUserMiddleware',
    '...',
)

AUTHENTICATION_BACKENDS = (
    'django.contrib.auth.backends.RemoteUserBackend',
)

The Apache Configuration looks something like this.

WSGIPythonPath /usr/local/share/WAM/

<IfModule mod_auth_cas.c>
    CASValidateServer Off
    CASLoginURL https://your.cas.server/login
    CASVersion 2
    CASDebug On
    CASValidateURL https://your.cas.server/serviceValidate
    CASCookiePath /tmp/
    CASTimeout 43200
    CASIdleTimeout 3600
</IfModule>


<Location /wam>
    AuthType CAS
    Require valid-user
</Location>

<Directory /usr/local/share/WAM/loads/static>
    Require all granted
</Directory>

WSGIScriptAlias /wam /usr/local/share/WAM/WAM/wsgi.py

<Directory /usr/local/share/WAM/WAM>
    <Files wsgi.py>
        Require all granted
    </Files>
</Directory>

You will need to ensure you have Apache’s CAS and wsgi modules installed and enabled too.

I wasted a couple of hours going around the houses on this one, so hopefully it may save you. I will be hosting the project for my modeller on foss.ulster.ac.uk along with the code once I move it from GitHub.

Upgrading from Serendipity to WordPress on Debian

As you may have noticed, I have upgraded from Serendipity, which was creaking a bit, and seems to no longer be supported by Debian to WordPress. It was a moderately complex task, as I wanted to preserve backwards compatibility and a lot of content with mathematics and code.

I installed the Debian package, and tried to follow the instructions on the Debian wiki but they are perhaps out of date. I got an error trying to setup the database, but found it was there and functional.

I  then used this excellent script to help import the old serendipity data. It wasn’t without problems, the script needed to be placed (on the Debian installed package) under /var/lib/wordpress/wp-content/plugins/ within a directory to be registered by WordPress as a plugin, but I got that working in the end.

This was an attempt to preserve ID fields as well. It seems to have worked – which has simplified redirects (see below). Comments have been “flattened” as the script warned, and there’s clearly a character-set issue here and there, but these weren’t serious issues for me. Your mileage may vary.

I found a good plugin for GeHSI style code formatting which I was using in Serendipity, albeit the syntax is very slightly different so I have some work to do editing a few entries (I don’t want to attempt a global SQL regexp find and replace if I don’t have to). I found this excellent seeming plugin for Latex and switched it into site-wide mode. So far, checking a few old articles, it JustWorks (TM).

Some of my old posts have images in the serendipity media folders that will need moved, but I was keen to have links to the old blog redirect automatically. I was able to use

RedirectMatch permanent ^/serendipity/archives/([0-9]+).* http://www.piglets.org/blog/?p=$1

in my Serendipity Apache configuration to jump to the new articles.

I can start to dismantle the rest of serendipity, except for the media, quite soon now. It’s nice to have a platform that respects multiple device layouts, and hopefully comment spam will be easier to control too. A sample of most articles show they render OK, there are a few gotchas, and I’ll try to work through them in time.