Compare commits

...

11 Commits

Author SHA1 Message Date
perryharlock
68f3bbe4f7 Version 1.0.0-beta.2 2013-10-04 15:58:02 +01:00
Perry Harlock
2d0b15dbf7 Add screenshots 2013-10-04 14:46:42 +01:00
perryharlock
98ed72adaa Fix h2 nd h3 margins 2013-10-04 11:52:06 +01:00
perryharlock
b619087cf9 Add bower package management 2013-10-04 11:37:40 +01:00
perryharlock
086122ca51 Issue 43 - Stop graph appearing if only one result 2013-10-03 15:30:58 +01:00
Rowan Manning
84d36d383b Give pages unique H1s 2013-10-03 14:50:02 +01:00
perryharlock
872cabc0e0 Remove refresh text from New results incoming alert 2013-10-03 14:46:35 +01:00
Rowan Manning
c2ed1beb0e Add the ability to run tasks ad-hoc 2013-10-03 14:18:31 +01:00
Rowan Manning
cd38bd0586 Update webservice client 2013-10-03 13:53:45 +01:00
perryharlock
8fc041ff3e Issue 44 - Added more info to the footer 2013-10-03 13:52:37 +01:00
perryharlock
5c7f3087a5 Issue 45 - Change Home to Dashboard on breadcrumb 2013-10-03 10:57:21 +01:00
30 changed files with 156 additions and 850 deletions

3
.bowerrc Normal file
View File

@@ -0,0 +1,3 @@
{
"directory" : "public/js/vendor"
}

3
.gitignore vendored
View File

@@ -9,3 +9,6 @@ npm-debug.log
# Generated CSS files # Generated CSS files
public/css public/css
# Bower installed js files
public/js/vendor

View File

@@ -6,6 +6,7 @@ all: deps lint
deps: deps:
@echo "Installing dependencies..." @echo "Installing dependencies..."
@npm install @npm install
@./node_modules/.bin/bower install
# Lint JavaScript # Lint JavaScript
lint: lint:

View File

@@ -1,19 +1,22 @@
pa11y-dashboard pa11y-dashboard
=============== ===============
pa11y-dashboard is a visual web interface to the [pa11y][pa11y] accessibility reporter. pa11y-dashboard is a visual web interface to the [pa11y][pa11y] accessibility reporter.
**Current Version:** *1.0.0-beta.1* **Current Version:** *1.0.0-beta.2*
**Node Version Support:** *0.10* **Node Version Support:** *0.10*
![The Dashboard Page](https://f.cloud.github.com/assets/1225142/1269563/2fc6e4e0-2cfb-11e3-8f49-e74a9d49bb32.jpg)
![The URL Page](https://f.cloud.github.com/assets/1225142/1269564/2fe12f26-2cfb-11e3-8a24-d6eba09a940d.jpg)
Setup Setup
----- -----
pa11y-dashboard requires [Node.js][node] 0.10+ and [pa11y-webservice][pa11y-webservice] to be installed and running. You'll need to follow the setup guide for pa11y-webservice before setting up pa11y-dashboard. pa11y-dashboard requires [Node.js][node] 0.10+ and [pa11y-webservice][pa11y-webservice] to be installed and running. You'll need to follow the setup guide for pa11y-webservice before setting up pa11y-dashboard.
You'll then need to clone this repo locally and install dependencies with `npm install`. Once you have a local clone, you'll need to copy some sample configuration files in order to run the application. From within the repo, run the following commands: You'll then need to clone this repo locally and install dependencies with `make deps`, this installs npm and bower dependencies. Once you have a local clone, you'll need to copy some sample configuration files in order to run the application. From within the repo, run the following commands:
```sh ```sh
$ cp config/development.sample.json config/development.json $ cp config/development.sample.json config/development.json

9
app.js
View File

@@ -6,6 +6,7 @@ var express = require('express');
var hbs = require('express-hbs'); var hbs = require('express-hbs');
var http = require('http'); var http = require('http');
var lessMiddleware = require('less-middleware'); var lessMiddleware = require('less-middleware');
var pkg = require('./package.json');
module.exports = initApp; module.exports = initApp;
@@ -52,9 +53,14 @@ function initApp (config, callback) {
// Populate view locals // Populate view locals
app.express.locals({ app.express.locals({
lang: 'en', lang: 'en',
year: (new Date()).getFullYear() year: (new Date()).getFullYear(),
version: pkg.version,
repo: pkg.homepage,
bugtracker: pkg.bugs,
rules: pkg.snifferules
}); });
app.express.use(function (req, res, next) { app.express.use(function (req, res, next) {
res.locals.isHomePage = (req.path === '/');
res.locals.host = req.host; res.locals.host = req.host;
next(); next();
}); });
@@ -64,6 +70,7 @@ function initApp (config, callback) {
require('./route/new')(app); require('./route/new')(app);
require('./route/task/index')(app); require('./route/task/index')(app);
require('./route/task/delete')(app); require('./route/task/delete')(app);
require('./route/task/run')(app);
require('./route/result/index')(app); require('./route/result/index')(app);
require('./route/result/download')(app); require('./route/result/download')(app);

9
bower.json Normal file
View File

@@ -0,0 +1,9 @@
{
"name": "pa11y-dashboard",
"dependencies": {
"jquery": "~1.10",
"bootstrap": "~3.0",
"flot": "~0.8"
}
}

View File

@@ -1,6 +1,6 @@
{ {
"name": "pa11y-dashboard", "name": "pa11y-dashboard",
"version": "1.0.0-beta.1", "version": "1.0.0-beta.2",
"private": true, "private": true,
"description": "pa11y-dashboard is a visual web interface to the pa11y accessibility reporter", "description": "pa11y-dashboard is a visual web interface to the pa11y accessibility reporter",
@@ -16,17 +16,19 @@
}, },
"homepage": "https://github.com/nature/pa11y-dashboard", "homepage": "https://github.com/nature/pa11y-dashboard",
"bugs": "https://github.com/nature/pa11y-dashboard/issues", "bugs": "https://github.com/nature/pa11y-dashboard/issues",
"snifferules": "https://github.com/nature/pa11y/wiki/HTML-CodeSniffer-Rules",
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
}, },
"dependencies": { "dependencies": {
"bower": "~1.2",
"chalk": "~0.2", "chalk": "~0.2",
"express": "~3.4", "express": "~3.4",
"express-hbs": "~0.2", "express-hbs": "~0.2",
"less-middleware": "~0.1", "less-middleware": "~0.1",
"moment": "~2.2", "moment": "~2.2",
"pa11y-webservice-client-node": "git+ssh://git@github.com:nature/pa11y-webservice-client-node.git#1.0.0-beta.3", "pa11y-webservice-client-node": "git+ssh://git@github.com:nature/pa11y-webservice-client-node.git#1.0.0-beta.4",
"underscore": "~1.5" "underscore": "~1.5"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,98 +0,0 @@
/* ========================================================================
* Bootstrap: alert.js v3.0.0
* http://twbs.github.com/bootstrap/javascript.html#alerts
* ========================================================================
* Copyright 2013 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ======================================================================== */
+function ($) { "use strict";
// ALERT CLASS DEFINITION
// ======================
var dismiss = '[data-dismiss="alert"]'
var Alert = function (el) {
$(el).on('click', dismiss, this.close)
}
Alert.prototype.close = function (e) {
var $this = $(this)
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') // strip for ie7
}
var $parent = $(selector)
if (e) e.preventDefault()
if (!$parent.length) {
$parent = $this.hasClass('alert') ? $this : $this.parent()
}
$parent.trigger(e = $.Event('close.bs.alert'))
if (e.isDefaultPrevented()) return
$parent.removeClass('in')
function removeElement() {
$parent.trigger('closed.bs.alert').remove()
}
$.support.transition && $parent.hasClass('fade') ?
$parent
.one($.support.transition.end, removeElement)
.emulateTransitionEnd(150) :
removeElement()
}
// ALERT PLUGIN DEFINITION
// =======================
var old = $.fn.alert
$.fn.alert = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.alert')
if (!data) $this.data('bs.alert', (data = new Alert(this)))
if (typeof option == 'string') data[option].call($this)
})
}
$.fn.alert.Constructor = Alert
// ALERT NO CONFLICT
// =================
$.fn.alert.noConflict = function () {
$.fn.alert = old
return this
}
// ALERT DATA-API
// ==============
$(document).on('click.bs.alert.data-api', dismiss, Alert.prototype.close)
}(window.jQuery);

View File

@@ -1,154 +0,0 @@
/* ========================================================================
* Bootstrap: dropdown.js v3.0.0
* http://twbs.github.com/bootstrap/javascript.html#dropdowns
* ========================================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ======================================================================== */
+function ($) { "use strict";
// DROPDOWN CLASS DEFINITION
// =========================
var backdrop = '.dropdown-backdrop'
var toggle = '[data-toggle=dropdown]'
var Dropdown = function (element) {
var $el = $(element).on('click.bs.dropdown', this.toggle)
}
Dropdown.prototype.toggle = function (e) {
var $this = $(this)
if ($this.is('.disabled, :disabled')) return
var $parent = getParent($this)
var isActive = $parent.hasClass('open')
clearMenus()
if (!isActive) {
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
// if mobile we we use a backdrop because click events don't delegate
$('<div class="dropdown-backdrop"/>').insertAfter($(this)).on('click', clearMenus)
}
$parent.trigger(e = $.Event('show.bs.dropdown'))
if (e.isDefaultPrevented()) return
$parent
.toggleClass('open')
.trigger('shown.bs.dropdown')
$this.focus()
}
return false
}
Dropdown.prototype.keydown = function (e) {
if (!/(38|40|27)/.test(e.keyCode)) return
var $this = $(this)
e.preventDefault()
e.stopPropagation()
if ($this.is('.disabled, :disabled')) return
var $parent = getParent($this)
var isActive = $parent.hasClass('open')
if (!isActive || (isActive && e.keyCode == 27)) {
if (e.which == 27) $parent.find(toggle).focus()
return $this.click()
}
var $items = $('[role=menu] li:not(.divider):visible a', $parent)
if (!$items.length) return
var index = $items.index($items.filter(':focus'))
if (e.keyCode == 38 && index > 0) index-- // up
if (e.keyCode == 40 && index < $items.length - 1) index++ // down
if (!~index) index=0
$items.eq(index).focus()
}
function clearMenus() {
$(backdrop).remove()
$(toggle).each(function (e) {
var $parent = getParent($(this))
if (!$parent.hasClass('open')) return
$parent.trigger(e = $.Event('hide.bs.dropdown'))
if (e.isDefaultPrevented()) return
$parent.removeClass('open').trigger('hidden.bs.dropdown')
})
}
function getParent($this) {
var selector = $this.attr('data-target')
if (!selector) {
selector = $this.attr('href')
selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
}
var $parent = selector && $(selector)
return $parent && $parent.length ? $parent : $this.parent()
}
// DROPDOWN PLUGIN DEFINITION
// ==========================
var old = $.fn.dropdown
$.fn.dropdown = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('dropdown')
if (!data) $this.data('dropdown', (data = new Dropdown(this)))
if (typeof option == 'string') data[option].call($this)
})
}
$.fn.dropdown.Constructor = Dropdown
// DROPDOWN NO CONFLICT
// ====================
$.fn.dropdown.noConflict = function () {
$.fn.dropdown = old
return this
}
// APPLY TO STANDARD DROPDOWN ELEMENTS
// ===================================
$(document)
.on('click.bs.dropdown.data-api', clearMenus)
.on('click.bs.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
.on('click.bs.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
.on('keydown.bs.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
}(window.jQuery);

View File

@@ -1,386 +0,0 @@
/* ========================================================================
* Bootstrap: tooltip.js v3.0.0
* http://twbs.github.com/bootstrap/javascript.html#tooltip
* Inspired by the original jQuery.tipsy by Jason Frame
* ========================================================================
* Copyright 2012 Twitter, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ======================================================================== */
+function ($) { "use strict";
// TOOLTIP PUBLIC CLASS DEFINITION
// ===============================
var Tooltip = function (element, options) {
this.type =
this.options =
this.enabled =
this.timeout =
this.hoverState =
this.$element = null
this.init('tooltip', element, options)
}
Tooltip.DEFAULTS = {
animation: true
, placement: 'top'
, selector: false
, template: '<div class="tooltip"><div class="tooltip-arrow">&nbsp;</div><div class="tooltip-inner"></div></div>'
, trigger: 'hover focus'
, title: ''
, delay: 0
, html: false
, container: false
}
Tooltip.prototype.init = function (type, element, options) {
this.enabled = true
this.type = type
this.$element = $(element)
this.options = this.getOptions(options)
var triggers = this.options.trigger.split(' ')
for (var i = triggers.length; i--;) {
var trigger = triggers[i]
if (trigger == 'click') {
this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
} else if (trigger != 'manual') {
var eventIn = trigger == 'hover' ? 'mouseenter' : 'focus'
var eventOut = trigger == 'hover' ? 'mouseleave' : 'blur'
this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
}
}
this.options.selector ?
(this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
this.fixTitle()
}
Tooltip.prototype.getDefaults = function () {
return Tooltip.DEFAULTS
}
Tooltip.prototype.getOptions = function (options) {
options = $.extend({}, this.getDefaults(), this.$element.data(), options)
if (options.delay && typeof options.delay == 'number') {
options.delay = {
show: options.delay
, hide: options.delay
}
}
return options
}
Tooltip.prototype.getDelegateOptions = function () {
var options = {}
var defaults = this.getDefaults()
this._options && $.each(this._options, function (key, value) {
if (defaults[key] != value) options[key] = value
})
return options
}
Tooltip.prototype.enter = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'in'
if (!self.options.delay || !self.options.delay.show) return self.show()
self.timeout = setTimeout(function () {
if (self.hoverState == 'in') self.show()
}, self.options.delay.show)
}
Tooltip.prototype.leave = function (obj) {
var self = obj instanceof this.constructor ?
obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type)
clearTimeout(self.timeout)
self.hoverState = 'out'
if (!self.options.delay || !self.options.delay.hide) return self.hide()
self.timeout = setTimeout(function () {
if (self.hoverState == 'out') self.hide()
}, self.options.delay.hide)
}
Tooltip.prototype.show = function () {
var e = $.Event('show.bs.'+ this.type)
if (this.hasContent() && this.enabled) {
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
var $tip = this.tip()
this.setContent()
if (this.options.animation) $tip.addClass('fade')
var placement = typeof this.options.placement == 'function' ?
this.options.placement.call(this, $tip[0], this.$element[0]) :
this.options.placement
var autoToken = /\s?auto?\s?/i
var autoPlace = autoToken.test(placement)
if (autoPlace) placement = placement.replace(autoToken, '') || 'top'
$tip
.detach()
.css({ top: 0, left: 0, display: 'block' })
.addClass(placement)
this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element)
var pos = this.getPosition()
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (autoPlace) {
var $parent = this.$element.parent()
var orgPlacement = placement
var docScroll = document.documentElement.scrollTop || document.body.scrollTop
var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth()
var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight()
var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left
placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' :
placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' :
placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' :
placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' :
placement
$tip
.removeClass(orgPlacement)
.addClass(placement)
}
var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight)
this.applyPlacement(calculatedOffset, placement)
this.$element.trigger('shown.bs.' + this.type)
}
}
Tooltip.prototype.applyPlacement = function(offset, placement) {
var replace
var $tip = this.tip()
var width = $tip[0].offsetWidth
var height = $tip[0].offsetHeight
// manually read margins because getBoundingClientRect includes difference
var marginTop = parseInt($tip.css('margin-top'), 10)
var marginLeft = parseInt($tip.css('margin-left'), 10)
// we must check for NaN for ie 8/9
if (isNaN(marginTop)) marginTop = 0
if (isNaN(marginLeft)) marginLeft = 0
offset.top = offset.top + marginTop
offset.left = offset.left + marginLeft
$tip
.offset(offset)
.addClass('in')
// check to see if placing tip in new offset caused the tip to resize itself
var actualWidth = $tip[0].offsetWidth
var actualHeight = $tip[0].offsetHeight
if (placement == 'top' && actualHeight != height) {
replace = true
offset.top = offset.top + height - actualHeight
}
if (/bottom|top/.test(placement)) {
var delta = 0
if (offset.left < 0) {
delta = offset.left * -2
offset.left = 0
$tip.offset(offset)
actualWidth = $tip[0].offsetWidth
actualHeight = $tip[0].offsetHeight
}
this.replaceArrow(delta - width + actualWidth, actualWidth, 'left')
} else {
this.replaceArrow(actualHeight - height, actualHeight, 'top')
}
if (replace) $tip.offset(offset)
}
Tooltip.prototype.replaceArrow = function(delta, dimension, position) {
this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + "%") : '')
}
Tooltip.prototype.setContent = function () {
var $tip = this.tip()
var title = this.getTitle()
$tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
$tip.removeClass('fade in top bottom left right')
}
Tooltip.prototype.hide = function () {
var that = this
var $tip = this.tip()
var e = $.Event('hide.bs.' + this.type)
function complete() {
if (that.hoverState != 'in') $tip.detach()
}
this.$element.trigger(e)
if (e.isDefaultPrevented()) return
$tip.removeClass('in')
$.support.transition && this.$tip.hasClass('fade') ?
$tip
.one($.support.transition.end, complete)
.emulateTransitionEnd(150) :
complete()
this.$element.trigger('hidden.bs.' + this.type)
return this
}
Tooltip.prototype.fixTitle = function () {
var $e = this.$element
if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
$e.attr('data-original-title', $e.attr('title') || '').attr('title', '')
}
}
Tooltip.prototype.hasContent = function () {
return this.getTitle()
}
Tooltip.prototype.getPosition = function () {
var el = this.$element[0]
return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : {
width: el.offsetWidth
, height: el.offsetHeight
}, this.$element.offset())
}
Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) {
return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } :
placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } :
/* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width }
}
Tooltip.prototype.getTitle = function () {
var title
var $e = this.$element
var o = this.options
title = $e.attr('data-original-title')
|| (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
return title
}
Tooltip.prototype.tip = function () {
return this.$tip = this.$tip || $(this.options.template)
}
Tooltip.prototype.arrow = function () {
return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow')
}
Tooltip.prototype.validate = function () {
if (!this.$element[0].parentNode) {
this.hide()
this.$element = null
this.options = null
}
}
Tooltip.prototype.enable = function () {
this.enabled = true
}
Tooltip.prototype.disable = function () {
this.enabled = false
}
Tooltip.prototype.toggleEnabled = function () {
this.enabled = !this.enabled
}
Tooltip.prototype.toggle = function (e) {
var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this
self.tip().hasClass('in') ? self.leave(self) : self.enter(self)
}
Tooltip.prototype.destroy = function () {
this.hide().$element.off('.' + this.type).removeData('bs.' + this.type)
}
// TOOLTIP PLUGIN DEFINITION
// =========================
var old = $.fn.tooltip
$.fn.tooltip = function (option) {
return this.each(function () {
var $this = $(this)
var data = $this.data('bs.tooltip')
var options = typeof option == 'object' && option
if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options)))
if (typeof option == 'string') data[option]()
})
}
$.fn.tooltip.Constructor = Tooltip
// TOOLTIP NO CONFLICT
// ===================
$.fn.tooltip.noConflict = function () {
$.fn.tooltip = old
return this
}
}(window.jQuery);

File diff suppressed because one or more lines are too long

View File

@@ -1,44 +0,0 @@
/* Flot plugin for plotting textual data or categories.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
Consider a dataset like [["February", 34], ["March", 20], ...]. This plugin
allows you to plot such a dataset directly.
To enable it, you must specify mode: "categories" on the axis with the textual
labels, e.g.
$.plot("#placeholder", data, { xaxis: { mode: "categories" } });
By default, the labels are ordered as they are met in the data series. If you
need a different ordering, you can specify "categories" on the axis options
and list the categories there:
xaxis: {
mode: "categories",
categories: ["February", "March", "April"]
}
If you need to customize the distances between the categories, you can specify
"categories" as an object mapping labels to values
xaxis: {
mode: "categories",
categories: { "February": 1, "March": 3, "April": 4 }
}
If you don't specify all categories, the remaining categories will be numbered
from the max value plus 1 (with a spacing of 1 between each).
Internally, the plugin works by transforming the input data through an auto-
generated mapping where the first category becomes 0, the second 1, etc.
Hence, a point like ["February", 34] becomes [0, 34] internally in Flot (this
is visible in hover and click events that return numbers rather than the
category labels). The plugin also overrides the tick generator to spit out the
categories as ticks instead of the values.
If you need to map a value back to its label, the mapping is always accessible
as "categories" on the axis object, e.g. plot.getAxes().xaxis.categories.
*/(function(e){function n(e,t,n,r){var i=t.xaxis.options.mode=="categories",s=t.yaxis.options.mode=="categories";if(!i&&!s)return;var o=r.format;if(!o){var u=t;o=[],o.push({x:!0,number:!0,required:!0}),o.push({y:!0,number:!0,required:!0});if(u.bars.show||u.lines.show&&u.lines.fill){var a=!!(u.bars.show&&u.bars.zero||u.lines.show&&u.lines.zero);o.push({y:!0,number:!0,required:!1,defaultValue:0,autoscale:a}),u.bars.horizontal&&(delete o[o.length-1].y,o[o.length-1].x=!0)}r.format=o}for(var f=0;f<o.length;++f)o[f].x&&i&&(o[f].number=!1),o[f].y&&s&&(o[f].number=!1)}function r(e){var t=-1;for(var n in e)e[n]>t&&(t=e[n]);return t+1}function i(e){var t=[];for(var n in e.categories){var r=e.categories[n];r>=e.min&&r<=e.max&&t.push([r,n])}return t.sort(function(e,t){return e[0]-t[0]}),t}function s(t,n,r){if(t[n].options.mode!="categories")return;if(!t[n].categories){var s={},u=t[n].options.categories||{};if(e.isArray(u))for(var a=0;a<u.length;++a)s[u[a]]=a;else for(var f in u)s[f]=u[f];t[n].categories=s}t[n].options.ticks||(t[n].options.ticks=i),o(r,n,t[n].categories)}function o(e,t,n){var i=e.points,s=e.pointsize,o=e.format,u=t.charAt(0),a=r(n);for(var f=0;f<i.length;f+=s){if(i[f]==null)continue;for(var l=0;l<s;++l){var c=i[f+l];if(c==null||!o[l][u])continue;c in n||(n[c]=a,++a),i[f+l]=n[c]}}}function u(e,t,n){s(t,"xaxis",n),s(t,"yaxis",n)}function a(e){e.hooks.processRawData.push(n),e.hooks.processDatapoints.push(u)}var t={xaxis:{categories:null},yaxis:{categories:null}};e.plot.plugins.push({init:a,options:t,name:"categories",version:"1.0"})})(jQuery);

File diff suppressed because one or more lines are too long

View File

@@ -1,19 +0,0 @@
/* Flot plugin for automatically redrawing plots as the placeholder resizes.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
It works by listening for changes on the placeholder div (through the jQuery
resize event plugin) - if the size changes, it will redraw the plot.
There are no options. If you need to disable the plugin for some plots, you
can just fix the size of their placeholders.
*//* Inline dependency:
* jQuery resize event - v1.1 - 3/14/2010
* http://benalman.com/projects/jquery-resize-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/(function(e,t,n){function c(){s=t[o](function(){r.each(function(){var t=e(this),n=t.width(),r=t.height(),i=e.data(this,a);(n!==i.w||r!==i.h)&&t.trigger(u,[i.w=n,i.h=r])}),c()},i[f])}var r=e([]),i=e.resize=e.extend(e.resize,{}),s,o="setTimeout",u="resize",a=u+"-special-event",f="delay",l="throttleWindow";i[f]=250,i[l]=!0,e.event.special[u]={setup:function(){if(!i[l]&&this[o])return!1;var t=e(this);r=r.add(t),e.data(this,a,{w:t.width(),h:t.height()}),r.length===1&&c()},teardown:function(){if(!i[l]&&this[o])return!1;var t=e(this);r=r.not(t),t.removeData(a),r.length||clearTimeout(s)},add:function(t){function s(t,i,s){var o=e(this),u=e.data(this,a);u.w=i!==n?i:o.width(),u.h=s!==n?s:o.height(),r.apply(this,arguments)}if(!i[l]&&this[o])return!1;var r;if(e.isFunction(t))return r=t,s;r=t.handler,t.handler=s}}})(jQuery,this),function(e){function n(e){function t(){var t=e.getPlaceholder();if(t.width()==0||t.height()==0)return;e.resize(),e.setupGrid(),e.draw()}function n(e,n){e.getPlaceholder().resize(t)}function r(e,n){e.getPlaceholder().unbind("resize",t)}e.hooks.bindEvents.push(n),e.hooks.shutdown.push(r)}var t={};e.plot.plugins.push({init:n,options:t,name:"resize",version:"1.0"})}(jQuery);

View File

@@ -1,79 +0,0 @@
/* Flot plugin for selecting regions of a plot.
Copyright (c) 2007-2013 IOLA and Ole Laursen.
Licensed under the MIT license.
The plugin supports these options:
selection: {
mode: null or "x" or "y" or "xy",
color: color,
shape: "round" or "miter" or "bevel",
minSize: number of pixels
}
Selection support is enabled by setting the mode to one of "x", "y" or "xy".
In "x" mode, the user will only be able to specify the x range, similarly for
"y" mode. For "xy", the selection becomes a rectangle where both ranges can be
specified. "color" is color of the selection (if you need to change the color
later on, you can get to it with plot.getOptions().selection.color). "shape"
is the shape of the corners of the selection.
"minSize" is the minimum size a selection can be in pixels. This value can
be customized to determine the smallest size a selection can be and still
have the selection rectangle be displayed. When customizing this value, the
fact that it refers to pixels, not axis units must be taken into account.
Thus, for example, if there is a bar graph in time mode with BarWidth set to 1
minute, setting "minSize" to 1 will not make the minimum selection size 1
minute, but rather 1 pixel. Note also that setting "minSize" to 0 will prevent
"plotunselected" events from being fired when the user clicks the mouse without
dragging.
When selection support is enabled, a "plotselected" event will be emitted on
the DOM element you passed into the plot function. The event handler gets a
parameter with the ranges selected on the axes, like this:
placeholder.bind( "plotselected", function( event, ranges ) {
alert("You selected " + ranges.xaxis.from + " to " + ranges.xaxis.to)
// similar for yaxis - with multiple axes, the extra ones are in
// x2axis, x3axis, ...
});
The "plotselected" event is only fired when the user has finished making the
selection. A "plotselecting" event is fired during the process with the same
parameters as the "plotselected" event, in case you want to know what's
happening while it's happening,
A "plotunselected" event with no arguments is emitted when the user clicks the
mouse to remove the selection. As stated above, setting "minSize" to 0 will
destroy this behavior.
The plugin allso adds the following methods to the plot object:
- setSelection( ranges, preventEvent )
Set the selection rectangle. The passed in ranges is on the same form as
returned in the "plotselected" event. If the selection mode is "x", you
should put in either an xaxis range, if the mode is "y" you need to put in
an yaxis range and both xaxis and yaxis if the selection mode is "xy", like
this:
setSelection({ xaxis: { from: 0, to: 10 }, yaxis: { from: 40, to: 60 } });
setSelection will trigger the "plotselected" event when called. If you don't
want that to happen, e.g. if you're inside a "plotselected" handler, pass
true as the second parameter. If you are using multiple axes, you can
specify the ranges on any of those, e.g. as x2axis/x3axis/... instead of
xaxis, the plugin picks the first one it sees.
- clearSelection( preventEvent )
Clear the selection rectangle. Pass in true to avoid getting a
"plotunselected" event.
- getSelection()
Returns the current selection in the same format as the "plotselected"
event. If there's currently no selection, the function returns null.
*/(function(e){function t(t){function s(e){n.active&&(h(e),t.getPlaceholder().trigger("plotselecting",[a()]))}function o(t){if(t.which!=1)return;document.body.focus(),document.onselectstart!==undefined&&r.onselectstart==null&&(r.onselectstart=document.onselectstart,document.onselectstart=function(){return!1}),document.ondrag!==undefined&&r.ondrag==null&&(r.ondrag=document.ondrag,document.ondrag=function(){return!1}),c(n.first,t),n.active=!0,i=function(e){u(e)},e(document).one("mouseup",i)}function u(e){return i=null,document.onselectstart!==undefined&&(document.onselectstart=r.onselectstart),document.ondrag!==undefined&&(document.ondrag=r.ondrag),n.active=!1,h(e),m()?f():(t.getPlaceholder().trigger("plotunselected",[]),t.getPlaceholder().trigger("plotselecting",[null])),!1}function a(){if(!m())return null;if(!n.show)return null;var r={},i=n.first,s=n.second;return e.each(t.getAxes(),function(e,t){if(t.used){var n=t.c2p(i[t.direction]),o=t.c2p(s[t.direction]);r[e]={from:Math.min(n,o),to:Math.max(n,o)}}}),r}function f(){var e=a();t.getPlaceholder().trigger("plotselected",[e]),e.xaxis&&e.yaxis&&t.getPlaceholder().trigger("selected",[{x1:e.xaxis.from,y1:e.yaxis.from,x2:e.xaxis.to,y2:e.yaxis.to}])}function l(e,t,n){return t<e?e:t>n?n:t}function c(e,r){var i=t.getOptions(),s=t.getPlaceholder().offset(),o=t.getPlotOffset();e.x=l(0,r.pageX-s.left-o.left,t.width()),e.y=l(0,r.pageY-s.top-o.top,t.height()),i.selection.mode=="y"&&(e.x=e==n.first?0:t.width()),i.selection.mode=="x"&&(e.y=e==n.first?0:t.height())}function h(e){if(e.pageX==null)return;c(n.second,e),m()?(n.show=!0,t.triggerRedrawOverlay()):p(!0)}function p(e){n.show&&(n.show=!1,t.triggerRedrawOverlay(),e||t.getPlaceholder().trigger("plotunselected",[]))}function d(e,n){var r,i,s,o,u=t.getAxes();for(var a in u){r=u[a];if(r.direction==n){o=n+r.n+"axis",!e[o]&&r.n==1&&(o=n+"axis");if(e[o]){i=e[o].from,s=e[o].to;break}}}e[o]||(r=n=="x"?t.getXAxes()[0]:t.getYAxes()[0],i=e[n+"1"],s=e[n+"2"]);if(i!=null&&s!=null&&i>s){var f=i;i=s,s=f}return{from:i,to:s,axis:r}}function v(e,r){var i,s,o=t.getOptions();o.selection.mode=="y"?(n.first.x=0,n.second.x=t.width()):(s=d(e,"x"),n.first.x=s.axis.p2c(s.from),n.second.x=s.axis.p2c(s.to)),o.selection.mode=="x"?(n.first.y=0,n.second.y=t.height()):(s=d(e,"y"),n.first.y=s.axis.p2c(s.from),n.second.y=s.axis.p2c(s.to)),n.show=!0,t.triggerRedrawOverlay(),!r&&m()&&f()}function m(){var e=t.getOptions().selection.minSize;return Math.abs(n.second.x-n.first.x)>=e&&Math.abs(n.second.y-n.first.y)>=e}var n={first:{x:-1,y:-1},second:{x:-1,y:-1},show:!1,active:!1},r={},i=null;t.clearSelection=p,t.setSelection=v,t.getSelection=a,t.hooks.bindEvents.push(function(e,t){var n=e.getOptions();n.selection.mode!=null&&(t.mousemove(s),t.mousedown(o))}),t.hooks.drawOverlay.push(function(t,r){if(n.show&&m()){var i=t.getPlotOffset(),s=t.getOptions();r.save(),r.translate(i.left,i.top);var o=e.color.parse(s.selection.color);r.strokeStyle=o.scale("a",.8).toString(),r.lineWidth=1,r.lineJoin=s.selection.shape,r.fillStyle=o.scale("a",.4).toString();var u=Math.min(n.first.x,n.second.x)+.5,a=Math.min(n.first.y,n.second.y)+.5,f=Math.abs(n.second.x-n.first.x)-1,l=Math.abs(n.second.y-n.first.y)-1;r.fillRect(u,a,f,l),r.strokeRect(u,a,f,l),r.restore()}}),t.hooks.shutdown.push(function(t,n){n.unbind("mousemove",s),n.unbind("mousedown",o),i&&e(document).unbind("mouseup",i)})}e.plot.plugins.push({init:t,options:{selection:{mode:null,color:"#e8cfac",shape:"round",minSize:5}},name:"selection",version:"1.1"})})(jQuery);

File diff suppressed because one or more lines are too long

View File

@@ -51,6 +51,17 @@
.graph-spacer { .graph-spacer {
padding-bottom:80px; padding-bottom:80px;
} }
.footer {
text-align:center;
.nav {
float:none;
li {
width:33%;
}
}
}
} }
@media (max-width:767px) { @media (max-width:767px) {
.date-selector { .date-selector {
@@ -137,4 +148,11 @@
} }
} }
} }
.footer {
.nav {
a {
padding:10px 5px;
}
}
}
} }

View File

@@ -34,6 +34,18 @@
padding:40px 0 20px 0; padding:40px 0 20px 0;
margin-top:40px; margin-top:40px;
} }
.nav {
list-style-type:none;
padding:0;
}
.footer .nav {
float:right;
a {
transition: background 0.3s;
-webkit-transition: background 0.3s;
}
}
.supersize-me { .supersize-me {
text-align:center; text-align:center;
font-size:112px; font-size:112px;
@@ -85,6 +97,13 @@
color:lighten(@brand-danger, 8%); color:lighten(@brand-danger, 8%);
} }
/* Type */
.h1 {
margin-top: @line-height-computed;
margin-bottom: (@line-height-computed / 2);
}
/* Task stats boxes */ /* Task stats boxes */
.task-stats { .task-stats {
margin-bottom:10px; margin-bottom:10px;
@@ -153,6 +172,8 @@
color:@gray-dark; color:@gray-dark;
min-height:200px; min-height:200px;
display:block; display:block;
transition: background 0.5s;
-webkit-transition: background 0.5s;
} }
.delete-button { .delete-button {
display:none; display:none;
@@ -170,6 +191,7 @@
text-overflow:ellipsis; text-overflow:ellipsis;
overflow:hidden; overflow:hidden;
width:100%; width:100%;
white-space: nowrap;
} }
.task-stats li { .task-stats li {
padding:7px 0 6px 0; padding:7px 0 6px 0;
@@ -202,7 +224,7 @@
} }
} }
.date { .date {
margin-top:45px; margin-top:5px;
} }
.tasks-list { .tasks-list {
padding:15px; padding:15px;
@@ -277,6 +299,14 @@ ul.date-links {
.btn-group > .btn { .btn-group > .btn {
float:none; float:none;
} }
&.single-result {
margin-top:-55px;
.show-stats {
display:none;
}
}
} }
/* Graph */ /* Graph */

View File

@@ -18,11 +18,14 @@ function route (app) {
if (err) { if (err) {
return next(err); return next(err);
} }
var presentedResults = presentResultList(results.map(presentResult));
res.render('task', { res.render('task', {
task: presentTask(task), task: presentTask(task),
results: presentResultList(results.map(presentResult)), results: presentedResults,
mainResult: task.lastResult || null, mainResult: task.lastResult || null,
added: (typeof req.query.added !== 'undefined'), added: (typeof req.query.added !== 'undefined'),
running: (typeof req.query.running !== 'undefined'),
hasOneResult: (presentedResults.length < 2),
isTaskPage: true isTaskPage: true
}); });
}); });

17
route/task/run.js Normal file
View File

@@ -0,0 +1,17 @@
'use strict';
module.exports = route;
// Route definition
function route (app) {
app.express.get('/:id/run', function (req, res, next) {
app.webservice.task(req.params.id).run(function (err, task) {
if (err) {
return next();
}
res.redirect('/' + req.params.id + '?running');
});
});
}

View File

@@ -41,16 +41,16 @@
{{> page-footer}} {{> page-footer}}
<!-- Javascript loveliness. --> <!-- Javascript loveliness. -->
<script type="text/javascript" src="/js/vendor/jquery.js"></script> <script type="text/javascript" src="/js/vendor/jquery/jquery.min.js"></script>
<script type="text/javascript" src="/js/vendor/bootstrap/alert.js"></script> <script type="text/javascript" src="/js/vendor/bootstrap/js/alert.js"></script>
<script type="text/javascript" src="/js/vendor/bootstrap/dropdown.js"></script> <script type="text/javascript" src="/js/vendor/bootstrap/js/dropdown.js"></script>
<script type="text/javascript" src="/js/vendor/bootstrap/tooltip.js"></script> <script type="text/javascript" src="/js/vendor/bootstrap/js/tooltip.js"></script>
<!--[if lte IE 8]> <!--[if lte IE 8]>
<script language="javascript" type="text/javascript" src="/js/vendor/flot/excanvas.min.js"></script><![endif]--> <script language="javascript" type="text/javascript" src="/js/vendor/flot/excanvas.min.js"></script><![endif]-->
<script src="/js/vendor/flot/jquery.flot.min.js"></script> <script src="/js/vendor/flot/jquery.flot.js"></script>
<script src="/js/vendor/flot/jquery.flot.categories.min.js"></script> <script src="/js/vendor/flot/jquery.flot.categories.js"></script>
<script src="/js/vendor/flot/jquery.flot.selection.min.js"></script> <script src="/js/vendor/flot/jquery.flot.selection.js"></script>
<script src="/js/vendor/flot/jquery.flot.resize.min.js"></script> <script src="/js/vendor/flot/jquery.flot.resize.js"></script>
<script type="text/javascript" src="/js/site.js"></script> <script type="text/javascript" src="/js/site.js"></script>
</body> </body>

View File

@@ -6,7 +6,7 @@
<form role="form" class="col-md-12" action="/new" method="post"> <form role="form" class="col-md-12" action="/new" method="post">
<div class="legend"> <div class="legend">
<legend>Add a new URL</legend> <h1 class="h2 crunch-top">Add a new URL</h1>
</div> </div>
{{#error}} {{#error}}

View File

@@ -3,7 +3,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<ol class="breadcrumb"> <ol class="breadcrumb">
<li><a href="/">Home</a></li> <li><a href="/">Dashboard</a></li>
{{#if isNewTaskPage}} {{#if isNewTaskPage}}
<li class="active">Add URL</li> <li class="active">Add URL</li>
{{/if}} {{/if}}

View File

@@ -1,15 +1,22 @@
<footer> <footer>
<div class="footer" role="contentinfo"> <div class="footer" role="contentinfo">
<div class="container"> <div class="container">
<ul class="pull-right crunch-bottom"> <div class="col-md-7">
<li> <p>&copy; {{year}} Nature Publishing Group.<br/>pa11y dashboard is licensed under the GNU General Public License 3.0.<br/>Version {{version}}</p>
<a href="https://github.com/nature/pa11y">github repo</a> </div>
</li> <div class="col-md-5 clearfix">
<li> <ul class="crunch-bottom floated-list nav">
<a href="https://github.com/nature/pa11y/wiki/HTML-CodeSniffer-Rules">list of rules</a> <li>
</li> <a href="{{repo}}">github repo</a>
</ul> </li>
<p>Copyright {{year}} Nature Publishing Group.<br/>pa11y is licensed under the GNU General Public License 3.0.</p> <li>
<a href="{{bugtracker}}">bug tracker</a>
</li>
<li>
<a href="{{rules}}">list of rules</a>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</footer> </footer>

View File

@@ -1,7 +1,10 @@
<header> <header>
<div role="banner" class="header"> <div role="banner" class="header">
<div class="container"> <div class="container">
<h1><a href="/">pa11y dashboard</a> - <span class="h3">your automated accessibility testing pal</span></h1> {{#if isHomePage}}<h1>{{else}}<div class="h1">{{/if}}
<a href="/">pa11y dashboard</a> -
<span class="h3">your automated accessibility testing pal</span>
{{#if isHomePage}}</h1>{{else}}</div>{{/if}}
</div> </div>
</div> </div>
</header> </header>

View File

@@ -3,7 +3,7 @@
<div class="row clearfix"> <div class="row clearfix">
<div class="col-md-12"> <div class="col-md-12">
<div class="h3 crunch well-med well pull-right"><span class="glyphicon glyphicon-calendar"></span>&nbsp;{{date-format mainResult.date format="DD MMM YYYY"}}</div> <div class="h3 crunch well-med well pull-right"><span class="glyphicon glyphicon-calendar"></span>&nbsp;{{date-format mainResult.date format="DD MMM YYYY"}}</div>
<h2 class="crunch-top">{{simplify-url task.url}}</h2> <h1 class="h2 crunch-top">{{simplify-url task.url}}</h1>
<p class="h4">({{task.standard}})</p> <p class="h4">({{task.standard}})</p>
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="col-md-12 zfix"> <div class="col-md-12 zfix">
<div class="row"> <div class="row">
<div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3"> <div class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3">
<div class="date-selector"> <div class="date-selector{{#if hasOneResult}} single-result{{/if}}">
<h4 class="show-stats text-center">Select a date to show stats for</h4> <h4 class="show-stats text-center">Select a date to show stats for</h4>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="btn-group block-level clearfix"> <li class="btn-group block-level clearfix">

View File

@@ -2,11 +2,11 @@
<div class="ruled task-header"> <div class="ruled task-header">
<div class="row clearfix"> <div class="row clearfix">
<div class="col-md-10"> <div class="col-md-10">
<h2 class="crunch-top">{{simplify-url task.url}}</h2> <h1 class="h2 crunch-top">{{simplify-url task.url}}</h1>
<p class="h4">({{task.standard}})</p> <p class="h4">({{task.standard}})</p>
</div> </div>
<div class="col-md-2 text-right run-details"> <div class="col-md-2 text-right run-details">
<!-- <button class="btn btn-success">Run <span class="glyphicon glyphicon-play"></span></button> --> <a href="{{task.hrefRun}}" class="btn btn-success">Run <span class="glyphicon glyphicon-play"></span></a>
{{#if mainResult}} {{#if mainResult}}
<div class="date">Last run : {{date-format mainResult.date format="DD MMM YYYY"}}</div> <div class="date">Last run : {{date-format mainResult.date format="DD MMM YYYY"}}</div>
{{else}} {{else}}

View File

@@ -12,6 +12,7 @@ function presentTask (task) {
// Add additional info // Add additional info
task.href = '/' + task.id; task.href = '/' + task.id;
task.hrefDelete = '/' + task.id + '/delete'; task.hrefDelete = '/' + task.id + '/delete';
task.hrefRun = '/' + task.id + '/run';
task.hrefJson = '/' + task.id + '.json'; task.hrefJson = '/' + task.id + '.json';
// Enhance the ignored rules // Enhance the ignored rules

View File

@@ -13,11 +13,26 @@
</div> </div>
{{/added}} {{/added}}
{{#running}}
<div class="col-md-12 clearfix">
<div class="alert alert-success">
<button aria-hidden="true" data-dismiss="alert" class="close" type="button">×</button>
<strong>New results incoming!</strong>
<p>
New results are being generated for this URL in the background.
This can take up to a minute to complete.
</p>
</div>
</div>
{{/running}}
{{> task-header}} {{> task-header}}
{{#if results}} {{#if results}}
{{> graph}} {{#unless hasOneResult}}
{{> graph}}
{{/unless}}
{{> result-selector}} {{> result-selector}}
@@ -30,7 +45,7 @@
<div class="alert alert-info"> <div class="alert alert-info">
<h4>There are no results to show</h4> <h4>There are no results to show</h4>
<p>pa11y has not been run against this URL yet so there are no results to show.</p> <p>pa11y has not been run against this URL yet so there are no results to show.</p>
<!-- <p>To run pa11y for this URL now <a href="">click here</a></p> --> <p><a href="{{task.hrefRun}}">Click here to generate results for this URL</a>.</p>
</div> </div>
</div> </div>
{{/if}} {{/if}}