Writing an ArchivesSpace plugin
I worked on ArchivesSpace as a part of the Hudson Molonglo development team. This article gives a worked example of how you can add functionality to ArchivesSpace by defining your own plugins.
Overview
ArchivesSpace allows you to write custom code to add new features, or change the behaviour of existing features. This code takes the form of a plugin: you structure your code according to certain conventions and it is automatically loaded when the ArchivesSpace system starts up. This article looks at the source code behind a plugin for generating accession identifiers.
When you create an accession through ArchivesSpace, you must provide a unique identifier for the record. An identifier can have between one and four components and it's for the institution to decide what these components represent. For example, an institution might structure its identifiers as:
[year] [sequence]
Where [year]
is the current year (e.g. 2013) and [sequence]
is a
running number (like "1234").
Such an identifier scheme doesn't really require human involvement at all. The system already knows what year it is, and addition is one of the few things computers do well, so it would be reasonable to expect the system to generate these identifiers for us.
In the following sections, we will work through the code required to make ArchivesSpace do this:
We'll start by adding some JavaScript to the accession form that will use an AJAX request to get the next identifier and pre-populate the "Create accession" form.
Next, we'll add a controller to the ArchivesSpace frontend to handle this AJAX request—requesting an identifier from the ArchivesSpace backend.
Finally, we'll add a REST endpoint to the ArchivesSpace backend API to support generating IDs in the format we require.
Changing the accession form
Loading custom JavaScript from the "Create accession" form
Let's start by adding some JavaScript code that will fire when we open the "Create accession" form. First, we create a directory to house our plugin:
$ mkdir generate_accession_identifiers
Within this, create a subdirectory to hold our custom view template:
$ mkdir -p generate_accession_identifiers/frontend/views
Within that directory, we create a file called layout_head.html.erb
with
the following content:
# generate_accession_identifiers/frontend/views/layout_head.html.erb
<% if controller.controller_name == 'accessions' && controller.action_name == 'new' %>
<%= javascript_include_tag "#{@base_url}/assets/generate_accession_identifiers.js" %>
<% end %>
The name layout_head.html.erb
is special: anything you put in a file
under [plugin_name]/frontend/views/layout_head.html.erb
will be
inserted at the top of every page delivered by ArchivesSpace.
In this case, we use an if
statement to restrict the effect of our
plugin to the "Create accession" form (by checking the controller and
action being requested). If we're looking at that form, our template
will insert a <script>
tag to pull in a custom JavaScript file.
The controller
and
javascript_include_tag
keywords are standard Rails facilities. The @base_url
instance
variable may contain a URL prefix for the application, so we include
that so our plugin doesn't break if the application isn't mounted at
the root URI.
Adding our JavaScript
Now that the "Create accession" form is pulling in our custom JavaScript file, we can handle the remaining form changes there. We create a new subdirectory within our plugin:
$ mkdir -p generate_accession_identifiers/frontend/assets
And put our generate_accession_identifiers.js
file in there:
/* generate_accession_identifiers/frontend/assets/generate_accession_identifiers.js */
$(function () {
var padding = 3;
var pad_number = function (number, padding) {
var s = ('' + number);
var padding_needed = (padding - s.length)
if (padding_needed > 0) {
s = (new Array(padding_needed + 1).join("0") + s);
}
return s;
};
var generate_accession_id = function () {
$.ajax({
url: APP_PATH + "plugins/generate_accession_identifier/generate",
data: {},
type: "POST",
success: function(identifier) {
$('#accession_id_0_').val(identifier.year);
$('#accession_id_1_').val(pad_number(identifier.number, padding));
$('#accession_id_1_').enable();
},
})
};
var identifier_is_blank = function () {
for (var i = 0; i < 4; i++) {
if ($("#accession_id_" + i + "_").val() !== "") {
return false;
}
}
return true;
};
if (identifier_is_blank()) {
generate_accession_id();
}
})
That's a fair bit of code, but the steps are pretty simple:
When the "Create accession" form loads, we generate an accession identifier if there isn't one already.
We generate an identifier by sending a POST request to
APP_PATH + "plugins/generate_accession_identifier/generate"
. This request will be handled by the controller we'll be adding in the following section.In response to our AJAX request, we'll receive an identifier containing a year and a number.
We insert that identifier into the accession form, padding the number to three digits.
The only non-standard thing here is APP_PATH
, which is a JavaScript
variable corresponding to the @base_url
member we saw back in Ruby
land. Again, we include this to make sure our code works when the
application isn't mounted at the root URI.
With that file in place, we now have the "Create accession" form loading a custom JavaScript file, and that JavaScript firing an AJAX request. In the next section, we'll add some code to handle that AJAX request.
Adding a controller to handle the AJAX request
Setting up routes
The accession form's AJAX request is going to reach a controller in the frontend. This controller isn't actually going to do very much, since generating unique identifiers is really the ArchivesSpace backend's job. As a result, we'll see that the frontend controller is really just going to mediate between the form and the backend.
The request generated by our JavaScript is going to look like this:
POST [base url]/plugins/generate_accession_identifier/generate
So we define a new Rails route to handle this URL. Create a file
named generate_accession_identifiers/frontend/routes.rb
with the
following contents:
# generate_accession_identifiers/frontend/routes.rb
ArchivesSpace::Application.routes.draw do
match('/plugins/generate_accession_identifier/generate' => 'generate_accession_identifiers#generate',
:via => [:post])
end
When loaded, this file will add a new route handler to the
ArchivesSpace application, mapping our POST requests to
the generate
method of the generate_accession_identifiers
controller. This routes.rb
file won't be loaded automatically, so
we need to add some code to a new file called
generate_accession_identifiers/frontend/plugin_init.rb
:
# generate_accession_identifiers/frontend/plugin_init.rb
my_routes = [File.join(File.dirname(__FILE__), "routes.rb")]
ArchivesSpace::Application.config.paths['config/routes'].concat(my_routes)
The plugin_init.rb
file is executed when the plugin is loaded (as
ArchivesSpace starts), so this gives us a place to add our custom
routes to the standard ones provided by the application.
Creating a new controller
Create a new directory to hold our controller:
$ mkdir -p generate_accession_identifiers/frontend/controllers
Then define the controller by adding a file called
generate_accession_identifiers_controller.rb
. This will be
automatically loaded when the system starts, and contains the code
that will send a request to the backend and deliver JSON back to the
JavaScript code:
# generate_accession_identifiers/frontend/controllers/generate_accession_identifiers_controller.rb
class GenerateAccessionIdentifiersController < ApplicationController
skip_before_filter :unauthorised_access
def generate
response = JSONModel::HTTP::post_form('/plugins/generate_accession_identifiers/next')
if response.code == '200'
render :json => ASUtils.json_parse(response.body)
else
render :status => 500
end
end
end
Walking through this code:
We use
skip_before_filter
to allow requests against this controller from unauthenticated users. We don't really care if anonymous users can generate identifiers.The
JSONModel::HTTP
module provides low-level wrappers around Ruby'sNet:HTTP
module for interacting with the ArchivesSpace backend. Thepost_form
method sends a POST request to a backend endpoint with optional form parameters. In this case, we just send a bare POST request.When the request succeeds, we parse it and ask Rails to render it as JSON. A bit funny to decode the JSON only to have Rails re-encode it, but sometimes life's a bit funny. The
ASUtils.json_parse
method is the standard way we parse JSON within ArchivesSpace: it gives us a central spot to specify default JSON parser options.If anything goes wrong we return an internal server error (status 500), and it's the client's problem.
That leaves the final piece: the backend code that handles the POST
request for /plugins/generate_accession_identifier/next
. Off we go!
Adding ID generation to the ArchivesSpace backend
It's time for a new subdirectory:
$ mkdir -p generate_accession_identifiers/backend/controllers
Within that, create a file called generate_4part_id.rb
to hold the
controller we'll add to the backend. That looks like this:
# generate_accession_identifiers/backend/controllers/generate_4part_id.rb
require 'time'
class ArchivesSpaceService < Sinatra::Base
Endpoint.post('/plugins/generate_accession_identifiers/next')
.description("Generate a new identifier based on the year and a running number")
.params()
.permissions([])
.returns([200, "{'year', 'YYYY', 'number', N}"]) \
do
year = Time.now.strftime('%Y')
number = Sequence.get("GENERATE_ACCESSION_IDENTIFIER_#{year}")
json_response(:year => year, :number => number)
end
end
Walking through once again:
We re-open the
ArchivesSpaceService
class to insert our new endpoint definition. This is the standard way that controllers add new REST endpoints to the system.We declare our endpoint using
Endpoint.post
, which sets up the endpoint to handle POST requests to the URI specified. We use the endpoint builder syntax to:Give the endpoint a description
Specify that it takes no parameters
Specify that there are no required permissions to access this endpoint
Specify that it will return a 200 status code and some JSON upon success.
The body of the endpoint hardly has to do anything: first it uses Ruby's
Time
class to figure out what year it is.Then it uses ArchivesSpace's built in
Sequence
class to get a running number for the sequence for that year. Each call toSequence.get
yields the next number in the sequence (0, 1, 2, ...) and guarantees that it will never yield the same number twice.Finally, we return the year and sequence number as a JSON document.
And that's it! The frontend controller returns that JSON to the JavaScript, and the JavaScript parses it and inserts it into the "Create accession" form.
File locations
Along the way we casually mentioned a couple of files and directories that the ArchivesSpace system treats in a special way. Let's review those now:
[plugin]/frontend/views/layout_head.html.erb
— automatically inserted into the top of every page.[plugin]/frontend/assets
— files placed in this directory are available viaGET /assets/[filename]
.[plugin]/frontend/plugin_init.rb
— automatically loaded when the plugin is loaded. Place frontend-specific initialisation code here.[plugin]/frontend/controllers
— add custom frontend Rails controllers here. Automatically loaded when the plugin is loaded.[plugin]/backend/controllers
— add custom ArchivesSpace backend controllers here. Automatically loaded when the plugin is loaded.
Installing the plugin
Now that everything is in place, the final step is to install the custom plugin. To do that, simply copy it into your ArchivesSpace plugins directory:
$ cp -a generate_accession_identifiers /path/to/archivesspace/plugins/
and then modify your ArchivesSpace config file to load it on startup:
# /path/to/archivesspace/config/config.rb
...
AppConfig[:plugins] = ['local', 'generate_accession_identifiers']
...
Links
If you get stuck, feel free to drop me a line at mark@teaspoon-consulting.com.