Using JS/Python Contexts in ActionKit¶
General Idea¶
We needed a way to do JS embedding of AK code, including user recognition, validation, etc. Briefly the way we do that is:
A client loads a static page. That page loads /samples/actionkit.js and has an action form (usually named ‘act’).
actionkit.js defines a bunch of JavaScript functions and attributes in an actionkit object, mostly in actionkit.forms and actionkit.utils.
After the <body> tag, the client’s HTML calls actionkit.forms.initPage().
After the </form> tag on their action form, the client needs to call actionkit.forms.initForm(‘act’).
initForm inserts a <script> element in the page pointing to some server side code (/context/).
The server side code returns JavaScript in the format callback_function (JSON data).
The javascript callback function runs, and does things like “not bob?”, prefilling, and validation. It also processes ak-templates, which look like <script type=”text/ak-template”>[% foo %]</script>
Even more simply: the page loads Javascript, which loads other JavaScript that has dynamic info from the server. It’s AJAX-y (JSONP is the buzzword for it), though the asynchronous calls are made in a cascade on page load and page submit rather than in response to particular user events like key presses or mouseovers. We use JSONP rather than straight AJAX so client forms can pull in context from any site, not just from pages at ActionKit (AJAX has to be same-domain).
Example Usage¶
db_templates/Original/lte.html
{% extends "./wrapper.html" %}{% load actionkit_tags %}
{% block script_additions %}
<script type="text/javascript">
function toggleChooser(on) { ... }
function countWords(textarea) { ... }
function commify(n) { ... }
function abbreviate(word, maxLength) { ... }
$(window).load( function() { ... }
$("input[name=media_target]").change(function() { toggleChooser(false) } );
</script>
{% endblock %}
{% block content %}
<form class="ak-form" name="act" method="POST" action="/act/" accept-charset="utf-8">
<input type="hidden" name="page" value="{{ page.name }}">
<div class="ak-grid-row">
<div class="ak-grid-col ak-grid-col-12-of-12">
<h2>{{ page.title }}</h2>
</div>
</div>
<div class="ak-grid-row">
<div class="ak-grid-col ak-grid-col-6-of-12">
{% if page.custom_fields.featured_image %}
<img class="ak-featured-img" src="{{page.custom_fields.featured_image}}">
{% endif %}
<div class="ak-styled-description ak-text-expander">
{% include_tmpl form.introduction_text %}
</div>
<a href="#" class="ak-read-more ak-mobile" data-lines="10">Read more</a>
<div id="lte-prelim"></div>
<div id="user_info_prompt">
</div>
<div id="lte-help"></div>
<script type="text/ak-template" for="lte-help">
<ul>
{% if form.talking_points %}
<li>
<div class="lte-help-head">
Talking Points
</div>
<div>
{% include_tmpl form.talking_points %}
</div>
</li>
{% endif %}
{% if form.writing_tips %}
<li>
<div class="lte-help-head">
Writing Tips
</div>
<div>
{% include_tmpl form.writing_tips %}
</div>
</li>
{% endif %}
{% for letter in form.cannedletter_set.all %}
<li>
<div class="lte-help-head">
Sample: {{ letter.subject|truncateletters:"20" }}
</div>
<div>
<h5>Subject:</h5>{{letter.subject}}
<h5>Message:</h5>{{letter.letter_text|linebreaks}}
</div>
</li>
{% endfor %}
</ul>
</script>
</div>
<div class="ak-grid-col ak-grid-col-6-of-12">
{% include "./progress_meter.html" %}
<script type="text/ak-template" for="user_info_prompt">
[% if (incomplete) { %]
<p>Please enter your information so we can find newspapers for you to contact.</p>
[% } %]
</script>
<div class="ak-styled-fields {{templateset.custom_fields.field_labels_class|default:"ak-labels-overlaid"}} {{templateset.custom_fields.field_errors_class|default:"ak-errs-below"}}">
{% include "./user_form_wrapper.html" %}
</div>
<div id="media_target"></div>
<script type="text/ak-template" for="media_target">
[% if (!incomplete) { %]
<p>Choose a newspaper to send a letter to:</p>
[%
var headers = {
"local": "Local Newspapers",
"regional": "Regional Newspapers",
"national": "National Newspapers"
};
var mediaTargets = actionkit.context.mediaTargets || {};
var mediaTargetTypes = ['national', 'regional', 'local'];
for (var j = 0; j < mediaTargetTypes.length; j++) {
var mediaTargetType = mediaTargetTypes[j];
var targetsOfType = mediaTargets[mediaTargetType];
if (targetsOfType) {
%]
<div class="ak-newspaper">
<h3>[%=headers[mediaTargetType]%]</h3>
</div>
[%
var shade = true;
for (var i = 0; i < targetsOfType.length; i++) {
var mediaTarget = targetsOfType[i],
targetId = "media_target_" + mediaTarget.id,
name = abbreviate(mediaTarget.name, 30),
label = "<a>" + name + "</a>";
if (mediaTarget.website_url) {
label = "<a target=\"_blank\" href=\"" + mediaTarget.website_url + "\">" + name + "</a>";
}
shade = !shade;
%]
<div class="[%= shade ? "shaded" : "" %] ak-newspaper-row">
<div class="ak-newspaper-title">[%=label%]</div>
<div>
<label for='[%=targetId%]'>
<input class='media_target' id='[%=targetId%]' value='[%=mediaTarget.id%]'
type='radio' name='media_target' onclick='javascript:toggleChooser(false)'>
Select</label>
</div>
<div class="number"><strong>Circulation:</strong> [%=commify(mediaTarget.circulation) %]</div>
[% if (actionkit.context.show_phones && mediaTarget.phone) { %]
<div class="nowrap"><strong>Phone:</strong> [%=mediaTarget.phone%]</div>
[% } %]
<div class="number"><strong>Sent:</strong> [%=mediaTarget.sent%]</div>
</div>
[%
}
}
}
} %]
</script>
<div id="lte-letter"></div>
<script type="text/ak-template" for="lte-letter">
[% if (!incomplete) { %]
<table class="ak-styled-fields">
<tr id="to_target_row" style="display: none;">
<td>To:</td>
<td>
<span id="to_target_name"></span>
<span style="font-size: smaller"> <a href="#" onclick="javascript:toggleChooser(true)">change</a></span>
</td>
</tr>
<tr>
<td>Subject</td>
<td><input id="letter_subject" type="text" name="subject" size="40"></td>
</tr>
<tr>
<td class="textarealabel">Message</td>
<td>
<textarea id="letter_text" name="letter_text" class="count[250]"></textarea>
<div class="wordCount"><strong>0</strong> Words. Most newspapers only consider letters of 250 to 350 words.</div>
</td>
</tr>
<tr>
<td> </td>
<td>Your name, address and phone number will be added as a signature.</td>
</tr>
</table>
[% } %]
</script>
<div id="lte-submit"><button type="submit" class="ak-styled-submit-button">Submit</button></div>
</div>
</div>
</form>
{% endblock %}
Implementation¶
There’s a large amount of code that makes contexts work, and it can be tough to follow the process because it jumps back and forth between client and server side. This section outlines the basic steps you need to take to use contexts in your pages.
Main Files And Functions Involved In Contexts¶
- samples/actionkit.js
This file adds the actionkit JS object to the window object, and has a number of methods and attributes under the headings forms and utils. The methods come together to make a fairly linear chain of calls and related callback methods, where the call causes a <script> element to be created, and the callback is what’s executed as the src of that element, wrapping the JSON that the original call requested.
- samples/prefill.js
This holds the jquery-fu that gets used to prefill whatever form is currently being acted on, if want_prefill_data and prefill are present in whatever JSON a particular callback is operating on. Prefilling usually happens from query string args after a user enters invalid data that’s only caught by the server. There’s also prefilling from data returned by the server, in the events tool.
- core.views.context()
context() returns user info and other data in the callback( JSON ) syntax, so that it gets executed on the client side.
- core.views.text()
Returns error messages in the user’s language, in the same JavaScript format as context()’s response.
- core.views.progress()
Returns thermometer/progress-against-our-goal data, if none was cached and returned by context().
- your_template.html
The template file is where you’ll add javascript that uses the context data. This happens via a series of blank divs combined with matching ak-template script blocks, as in the above examples.
Code You’ll Need To Write¶
The code that you will actually author and control will all live in your template file. Within your template(s), interactions with context data will happen in one of two ways: via the code in actionkit.js and prefill.js, and via your own ak-template blocks, where you’ll be able to execute javascript that adds content to divs throughout your template file.
Executable ak-template Blocks¶
You can put any number of ak-template blocks into your template page, and each one will allow you to set the content of a matching div element. Divs and their templates are matched by setting the id of the div and the ‘for’ attribute of the ak-template script tag, like this:
<div id="all_fields"></div>
<script type="text/ak-template" for="all_fields">
<p><b>Current context:</b></p>
[%=JSON.stringify(actionkit.context)%]
</script>
What appears inside the script tag will be processed into the innerHTML of the div. You can put raw HTML inside the script tag, as well as executable javascript. The javascript must be wrapped in a special bracketing syntax to get picked up by actionkit.utils.template():
<div id="sector_x"></div>
<script type="text/ak-template" for="sector_x">
[% if (actionkit.context.show_x) { %]
<h1>[%=actionkit.context.secret_stuff%]</h1>
[% } %]
</script>
Executable blocks will just get the [% … %] wrapping, while substitution tags will get [%=var_foo%]. You can use anything that’s available to you in the context, but make sure you’ve planned for each variable you use from the django side by placing it into the context dict in your Processor class’s context() method.
Flow Of Execution For A Context¶
The execution of contexts is a collaboration between the client side Javascript code, and the methods inside the ActionKit codebase. Below is a reference for the flow of execution for a context usage, primarily focused on the Javascript methods and processing side of the equation.
- SERVER
Request is made to apache; Template file is found, served with wrapper.
Subsequent request comes in for /samples/actionkit.js.
- CLIENT
- actionkit.js:
Adds the actionkit object to the window object.
Adds utils and forms to window.actionkit.
Adds a large number of methods and attributes to utils and forms.
- wrapper.html (line 60) calls actionkit.forms.initForm(‘act’):
- forms.initForm() (line 748 of actionkit.js):
forms.setForm() (line 703) sets the form element that utils and forms methods will work on.
Sets the form’s onsubmit to a function that will run forms.tryToValidate()
Runs forms.loadPrefiller() if prefill and want_prefill_data are in the args. That loads /samples/prefill.js into the document head with forms.createScriptElement().
- Calls forms.loadContext(), which:
Calls forms.beforeContextLoad(), which checks for some event related args.
Sets forms.onContextLoaded as the callback function that will be used to process what is returned from the server as JSON.
Sets a number of contextArgs values from ak.args, ak: form_name, action_id, akid, rd, want_progress, template, want_prefill_data.
Sets contextArgs.required to forms.required(), which returns a list of required names based on what’s in the ‘required’ element in the form.
Creates a contextUrl from the contextRoot and the contextArgs, and uses forms.createScriptElement to load that contextUrl as the src of a script element in the document head.
- SERVER
Request comes in from the browser for the constructed contextUrl, which will be of the type /context/?a=b&c=d…
- ActionKit code does the following (paraphrasing here–the contexts interface is stable, but AK internals may change):
Finds the specific page object that the contextUrl referenced.
Creates a blank dictionary (python’s associative array datatype) to store context info in.
Checks for required fields in the submission, and for this page.
Checks for language id and custom field info for this page.
Checks for any page-type specific fields, like media targets for LTEs.
Adds all that info to the context dictionary.
Renders that context dictionary as a JSON string.
Returns the JSON, wrapped by the callback function specified, as the executable src of a <script> tag.
- CLIENT
The text/javascript content returned by the server is run, as it’s the src of the <script> element that got created via forms.createScriptElement().
Since the generic callback for forms.loadContext() is forms.onContextLoaded(), that runs with the JSON as its argument.
- forms.onContextLoaded(), line 242 in actionkit.js:
If ak.context is already set, returns nothing; otherwise sets ak.context to the JSON argument it got.
Finds out whether it can recognize the user based on the info it has.
If it can, puts the akid into a hidden input and hides the user form.
If it can’t, shows the unknown user form.
Appends several fields as hidden inputs, if they’re present in the context.
Runs forms.onTargets() if there are targets.
Sets context.args to ak.args and adds some methods to context.
Runs forms.loadProgress() if necessary.
Selects all templates in the page (script tags with the type ‘text/ak-template’), and runs forms.doTemplate(context, template) on each.
- forms.doTemplate() does the following:
- Runs the context and template through utils.template, which:
Generates a cached function based on the javascript in the ak-template
Places the output of that function into the innerHTML of the div that has the id which the ak-template section’s for attribute lists.
Prefills the form if possible.
Runs forms.loadText(), which will perform internationalization on some of the context’s strings if necessary/possible.
Runs forms.handleQueryStringErrors(), which packs up errors and messages into ak.errors, and then runs forms.onValidationErrors()
- forms.onValidationErrors() does:
Clears existing errors from the form.
Marks the controls which have errors.
Marks the labels to those controls.
Marks the form with the class name contains-errors
Function Reference¶
Below is a guide to the various javascript object methods and attributes that are available via the context system.
Attributes And Shortcut Functions¶
- actionkit.context
Context from server
- actionkit.form
The action form (DOM element, not jQuery object)
- actionkit.forms.text
Error messages (in user’s language)
- actionkit.args
Query string args
- $log
Log to console (doesn’t crash on IE)
- $sel
Search like jQuery’s $(), but limited to current form if there is more than one
actionkit.forms¶
Attributes¶
- contextRoot
static value: ‘/context/’
- dateFormat
‘mm/dd/yy’
- dateRegexp
/^[01]?d/[0-3]?d/dddd$/
- timeRegexp
/^[01]?d(:[0-5]d)?$/
- defaultValidators
everything in validators
Methods¶
- errorMessage
Takes an error name like ‘card_num:invalid’
Returns a capitalized error string
- fieldName
- beforeContextLoad
Events Only
- loadContext
Runs beforeContextLoad
Adds action_id, akid, rd, want_progress, template, want_prefill_data, url to contextArgs
Uses a random number to keep ak from caching the response
Creates a script element from contextUrl
- loadPrefiller
pulls in the prefill script, /samples/prefill.js, into a script element
- loadProgress
pulls in /progress?page= page id & form_name & callback = onProgressLoaded
- onProgressLoaded
creates a script element that’s an ak-template for ‘progress’
- onPrefillerLoaded
runs forms.prefill() if ak.forms.awaitingPrefill
- prefill
If there’s a context and prefill data, use the prefill data, else use the args
If there’s a form, use setForm
Use $().deserialize() to set the form values
Use a jquery each() loop to set checkbox values
- loadText
brings in translated errors from /text/ using the same script-element technique as loadContext()
- onTextLoaded
sets forms.text to the input, is the callback for loadText
- createScriptElement
creates a <script> element consisting of an src that is the url passed in, and any additional attributes
- loadJSON
maintains a callback_id counter and a var ‘actionkitCallback’+callback_id
registers a callback with the window obj
adds the callback to args via args.callback = ‘window.’+callback_name
creates a script element via a url+args
- handleQueryStringErrors
if there’s no form in ak.args.form_name, do nothing
for each key in actionkit.args like ^(error|message)_, put them into ak.errors
if utils.hasAnyProperties( the errors ): ak.forms.onValidationErrors( the errors )
- onContextLoaded
is the callback for forms.loadContext()
takes a context as an arg
if ak.context is already set, returns nothing
otherwise sets ak.context to the arg
finds out whether it can recognize the person based on the info it has
if the person is recognized, stick the akid into a hidden input and hide the user form
if they’re not, show the unknown user form
append several fields as hidden inputs if they exist
run forms.onTargets() if there are targets
sets context.args to ak.args, adds some functions to context
runs loadProgress if necessary
selects all the templates in the page
for each, runs forms.doTemplate(context, template)
prefills if it can
runs forms.loadText()
runs forms.handleQueryStringErrors()
- doTemplate
runs the supplied context and element through utils.template()
makes the output the innerHTML of the element
- onTargets
does some pluralization, adds checkbox and listing html to ‘target_checkboxes’ element
- eventSearch
events only
- onEventSearchResults
events only
- logOut
if there’s an akid, store it as referring_akid
if there’s no akid, log the person out and redirect to their next location
otherwise, just strip the akid and rework the query string without it
- required
- validate
- clearErrors
- timeout
- onTimeout
- initPage
adds ‘js’ class to the document body
clears some window caching
loads firebug lite in IE if debug is on
- tryToValidate
- formData
- setForm
sets the current form that ak.form is acting on
- initForm
takes a form name
sets the form via setForm
if ak.form.onsubmit isn’t set, sets it to a function that runs tryToValidate
if the prefill is on, run loadPrefiller()
run loadContext()
- findConfirmationBox
- initValidation
- initTafForm
Validators¶
taf_emails
zip
postal
phone
mobile_phone
home_phone
work_phone
emergency_phone
phone
date
time
actionkit.utils¶
Methods¶
- escapeForQueryString
URI encodes a string and returns it
- makeQueryString
turn an object into an escaped query string
- getArgs
takes the url args and returns them as an object
- div
creates a div
- makeHiddenInput
adds a hidden input tag to a div as the first child
is called by appendHiddenInput
- appendHiddenInput
sticks a hidden input with name/value in the current form
- makeSet
turns a list into a dict/hash
- getAttr
returns the value of an attribute for a given element
used in onProgressLoaded, handleQueryStringErrors, doTemplate, validate, setForm
- hasAnyProperties
runs through a list of properties, returns true if any are present
- list
returns a list
used in forms.required
- val
returns the value of an element
used in validators.zip, validators.phone, forms.validate
- compile
pass a function name and a parameter list, it’ll return the eval’d version of same
used in forms.validate
- capitalize
uppercases and the first letter of a string passed to it
used in forms.errorMessage, forms.onContextLoaded
- add_commas
commifys a number
used in forms.onContextLoaded
- format
- template
takes a string and data
looks for the result in a cache
otherwise processes the result through a generic function
which returns a js-safe version of the string