aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--data/dashboard.json229
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm10
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm241
-rw-r--r--templates/web/default/dashboard/index.html150
4 files changed, 624 insertions, 6 deletions
diff --git a/data/dashboard.json b/data/dashboard.json
new file mode 100644
index 000000000..c921950be
--- /dev/null
+++ b/data/dashboard.json
@@ -0,0 +1,229 @@
+{
+ "category_options": [
+ "Abandoned vehicles",
+ "Grafitti",
+ "Potholes",
+ "Street lighting",
+ "Trees",
+ "Other"
+ ],
+ "counts_all": {
+ "wtd": {
+ "total": 10,
+ "planned": 2,
+ "in progress": 1,
+ "investigating": 1,
+ "fixed - council": 3,
+ "fixed_user": 2,
+ "time_to_fix": 2,
+ "time_to_mark": 2,
+ "not_marked": 1
+ },
+ "week": {
+ "total": 21,
+ "planned": 5,
+ "in progress": 3,
+ "investigating": 4,
+ "fixed - council": 6,
+ "fixed_user": 3,
+ "time_to_fix": 3,
+ "time_to_mark": 2,
+ "not_marked": 1
+ },
+ "weeks": {
+ "total": 57,
+ "planned": 5,
+ "in progress": 16,
+ "investigating": 4,
+ "fixed - council": 23,
+ "fixed_user": 9,
+ "time_to_fix": 2,
+ "time_to_mark": 2,
+ "not_marked": 1
+ },
+ "ytd": {
+ "total": 171,
+ "planned": 23,
+ "in progress": 34,
+ "investigating": 9,
+ "fixed - council": 72,
+ "fixed_user": 33,
+ "time_to_fix": 2,
+ "time_to_mark": 2,
+ "not_marked": 1
+ }
+ },
+ "counts_some": {
+ "wtd": {
+ "total": 5,
+ "planned": 0,
+ "in progress": 1,
+ "investigating": 1,
+ "fixed - council": 2,
+ "fixed_user": 1,
+ "time_to_fix": 2,
+ "time_to_mark": 2,
+ "not_marked": 0
+ },
+ "week": {
+ "total": 7,
+ "planned": 1,
+ "in progress": 2,
+ "investigating": 1,
+ "fixed - council": 2,
+ "fixed_user": 1,
+ "time_to_fix": 3,
+ "time_to_mark": 2,
+ "not_marked": 0
+ },
+ "weeks": {
+ "total": 57,
+ "planned": 5,
+ "in progress": 16,
+ "investigating": 4,
+ "fixed - council": 23,
+ "fixed_user": 9,
+ "time_to_fix": 2,
+ "time_to_mark": 2,
+ "not_marked": 1
+ },
+ "ytd": {
+ "total": 57,
+ "planned": 5,
+ "in progress": 16,
+ "investigating": 4,
+ "fixed - council": 23,
+ "fixed_user": 9,
+ "time_to_fix": 2,
+ "time_to_mark": 2,
+ "not_marked": 1
+ }
+ },
+ "lists": {
+ "all": {
+ "1": [
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Fly tipping" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Uneven paving" }
+ ],
+ "2": [
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Fly tipping" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Uneven paving" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Blocked drain" }
+ ],
+ "3": [
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Fly tipping" },
+ { "id": 0, "title": "Uneven paving" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Fly tipping" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Uneven paving" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Pothole" }
+ ]
+ },
+ "filtered": {
+ "1": [
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Uneven paving" }
+ ],
+ "2": [
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Fly tipping" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Fly tipping" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Uneven paving" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Pothole" }
+ ],
+ "3": [
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Uneven paving" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Loose kerb" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Fallen Tree" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Fly tipping" },
+ { "id": 0, "title": "Burst pipe" },
+ { "id": 0, "title": "Abandoned car" },
+ { "id": 0, "title": "Pothole" },
+ { "id": 0, "title": "Uneven paving" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Blocked drain" },
+ { "id": 0, "title": "Pothole" }
+ ]
+ }
+ }
+}
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index 198acade6..21b820423 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -571,8 +571,7 @@ sub report_edit : Path('report_edit') : Args(1) {
}
)->first;
- $c->detach( '/page_error_404_not_found',
- [ _('The requested URL was not found on this server.') ] )
+ $c->detach( '/page_error_404_not_found' )
unless $problem;
$c->stash->{problem} = $problem;
@@ -734,8 +733,7 @@ sub update_edit : Path('update_edit') : Args(1) {
}
)->first;
- $c->detach( '/page_error_404_not_found',
- [ _('The requested URL was not found on this server.') ] )
+ $c->detach( '/page_error_404_not_found' )
unless $update;
$c->forward('get_token');
@@ -1068,7 +1066,7 @@ sub check_token : Private {
my ( $self, $c ) = @_;
if ( !$c->req->param('token') || $c->req->param('token' ) ne $c->stash->{token} ) {
- $c->detach( '/page_error_404_not_found', [ _('The requested URL was not found on this server.') ] );
+ $c->detach( '/page_error_404_not_found' );
}
return 1;
@@ -1238,7 +1236,7 @@ sub check_page_allowed : Private {
$page ||= 'summary';
if ( !grep { $_ eq $page } keys %{ $c->stash->{allowed_pages} } ) {
- $c->detach( '/page_error_404_not_found', [ _('The requested URL was not found on this server.') ] );
+ $c->detach( '/page_error_404_not_found' );
}
return 1;
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
new file mode 100644
index 000000000..f2dddaea0
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm
@@ -0,0 +1,241 @@
+package FixMyStreet::App::Controller::Dashboard;
+use Moose;
+use namespace::autoclean;
+
+use DateTime;
+use File::Slurp;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+=head1 NAME
+
+FixMyStreet::App::Controller::Dashboard - Catalyst Controller
+
+=head1 DESCRIPTION
+
+Catalyst Controller.
+
+=head1 METHODS
+
+=cut
+
+sub example : Local : Args(0) {
+ my ( $self, $c ) = @_;
+ $c->stash->{template} = 'dashboard/index.html';
+
+ $c->stash->{children} = {};
+ for my $i (1..3) {
+ $c->stash->{children}{$i} = { id => $i, name => "Ward $i" };
+ }
+
+ # TODO Set up manual version of what the below would do
+ #$c->forward( '/report/new/setup_categories_and_councils' );
+
+ # See if we've had anything from the dropdowns - perhaps vary results if so
+ $c->stash->{ward} = $c->req->param('ward');
+ $c->stash->{category} = $c->req->param('category');
+ $c->stash->{q_state} = $c->req->param('state');
+
+ eval {
+ my $data = File::Slurp::read_file(
+ FixMyStreet->path_to( 'data/dashboard.json' )->stringify
+ );
+ my $j = JSON->new->utf8->decode($data);
+ if ( !$c->stash->{ward} && !$c->stash->{category} ) {
+ $c->stash->{problems} = $j->{counts_all};
+ } else {
+ $c->stash->{problems} = $j->{counts_some};
+ }
+ $c->stash->{category_options} = $j->{category_options};
+ if ( lc($c->stash->{q_state}) eq 'all' or !$c->stash->{q_state} ) {
+ $c->stash->{lists} = $j->{lists}->{all};
+ } else {
+ $c->stash->{lists} = $j->{lists}->{filtered};
+ }
+ };
+ if ($@) {
+ $c->stash->{message} = _("There was a problem showing this page. Please try again later.") . ' ' .
+ sprintf(_('The error was: %s'), $@);
+ $c->stash->{template} = 'errors/generic.html';
+ }
+}
+
+=head2 check_page_allowed
+
+Checks if we can view this page, and if not redirect to 404.
+
+=cut
+
+sub check_page_allowed : Private {
+ my ( $self, $c ) = @_;
+
+ $c->detach( '/auth/redirect' ) unless $c->user_exists;
+
+ $c->detach( '/page_error_404_not_found' )
+ unless $c->user_exists && $c->user->from_council;
+
+ return $c->user->from_council;
+}
+
+=head2 index
+
+Show the dashboard table.
+
+=cut
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $council = $c->forward('check_page_allowed');
+
+ # Set up the data for the dropdowns
+
+ my $children = mySociety::MaPit::call('area/children', $council,
+ type => $mySociety::VotingArea::council_child_types,
+ );
+ $c->stash->{children} = $children;
+
+ # XXX Hmm, this is probably the best way to go
+ $c->stash->{all_councils} = { $council => { id => $council } };
+ $c->forward( '/report/new/setup_categories_and_councils' );
+
+ # See if we've had anything from the dropdowns
+
+ $c->stash->{ward} = $c->req->param('ward');
+ $c->stash->{category} = $c->req->param('category');
+
+ my %where = (
+ council => $council, # XXX This will break in a two tier council. Restriction needs looking at...
+ 'problem.state' => [ FixMyStreet::DB::Result::Problem->visible_states() ],
+ );
+ $where{areas} = { 'like', '%,' . $c->stash->{ward} . ',%' }
+ if $c->stash->{ward};
+ $where{category} = $c->stash->{category}
+ if $c->stash->{category};
+ $c->stash->{where} = \%where;
+ my $prob_where = { %where };
+ $prob_where->{state} = $prob_where->{'problem.state'};
+ delete $prob_where->{'problem.state'};
+ $c->stash->{prob_where} = $prob_where;
+
+ my %counts;
+ my $t = DateTime->today;
+ $counts{wtd} = $c->forward( 'updates_search', [ $t->subtract( days => $t->dow - 1 ) ] );
+ $counts{week} = $c->forward( 'updates_search', [ DateTime->now->subtract( weeks => 1 ) ] );
+ $counts{weeks} = $c->forward( 'updates_search', [ DateTime->now->subtract( weeks => 4 ) ] );
+ $counts{ytd} = $c->forward( 'updates_search', [ DateTime->today->set( day => 1, month => 1 ) ] );
+
+ $c->stash->{problems} = \%counts;
+
+ # List of reports underneath summary table
+
+ $c->stash->{q_state} = $c->req->param('state') || '';
+ if ( $c->stash->{q_state} eq 'fixed' ) {
+ $prob_where->{state} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
+ } elsif ( $c->stash->{q_state} ) {
+ $prob_where->{state} = $c->stash->{q_state};
+ }
+ my $params = {
+ %$prob_where,
+ 'me.confirmed' => { '>=', DateTime->now->subtract( days => 30 ) },
+ };
+ my @problems = $c->cobrand->problems->search( $params )->all;
+ my %problems;
+ foreach (@problems) {
+ if ($_->confirmed >= DateTime->now->subtract(days => 7)) {
+ push @{$problems{1}}, $_;
+ } elsif ($_->confirmed >= DateTime->now->subtract(days => 14)) {
+ push @{$problems{2}}, $_;
+ } else {
+ push @{$problems{3}}, $_;
+ }
+ }
+ $c->stash->{lists} = \%problems;
+}
+
+sub updates_search : Private {
+ my ( $self, $c, $time ) = @_;
+
+ my $params = {
+ %{$c->stash->{where}},
+ 'me.confirmed' => { '>=', $time },
+ };
+
+ my $comments = $c->model('DB::Comment')->search(
+ $params,
+ {
+ group_by => [ 'problem_state' ],
+ select => [ 'problem_state', { count => 'me.id' } ],
+ as => [ qw/state state_count/ ],
+ join => 'problem'
+ }
+ );
+
+ my %counts =
+ map { ($_->state||'-') => $_->get_column('state_count') } $comments->all;
+ %counts =
+ map { $_ => $counts{$_} || 0 }
+ ('confirmed', 'investigating', 'in progress', 'closed', 'fixed - council',
+ 'fixed - user', 'fixed', 'unconfirmed', 'hidden',
+ 'partial', 'planned');
+
+ for my $vars (
+ [ 'time_to_fix', 'fixed - council' ],
+ [ 'time_to_mark', 'in progress', 'planned', 'investigating' ],
+ ) {
+ my $col = shift @$vars;
+ my $substmt = "select min(id) from comment where me.problem_id=comment.problem_id and problem_state in ('"
+ . join("','", @$vars) . "')";
+ $comments = $c->model('DB::Comment')->search(
+ { %$params,
+ problem_state => $vars,
+ 'me.id' => \"= ($substmt)",
+ },
+ {
+ select => [
+ { count => 'me.id' },
+ { avg => { extract => "epoch from me.confirmed-problem.confirmed" } },
+ ],
+ as => [ qw/state_count time/ ],
+ join => 'problem'
+ }
+ )->first;
+ $counts{$col} = int( ($comments->get_column('time')||0) / 60 / 60 / 24 + 0.5 );
+ }
+
+ $counts{fixed_user} = $c->model('DB::Comment')->search(
+ { %$params, mark_fixed => 1 }, { join => 'problem' }
+ )->count;
+
+ $params = {
+ %{$c->stash->{prob_where}},
+ 'me.confirmed' => { '>=', $time },
+ };
+ $counts{total} = $c->cobrand->problems->search( $params )->count;
+
+ $params = {
+ %{$c->stash->{prob_where}},
+ 'me.confirmed' => { '>=', $time },
+ state => 'confirmed',
+ '(select min(id) from comment where me.id=problem_id and problem_state is not null)' => undef,
+ };
+ $counts{not_marked} = $c->cobrand->problems->search( $params )->count;
+
+ return \%counts;
+}
+
+=head1 AUTHOR
+
+Matthew Somerville
+
+=head1 LICENSE
+
+Copyright (c) 2012 UK Citizens Online Democracy. All rights reserved.
+Licensed under the Affero GPL.
+
+=cut
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
diff --git a/templates/web/default/dashboard/index.html b/templates/web/default/dashboard/index.html
new file mode 100644
index 000000000..f7c058a99
--- /dev/null
+++ b/templates/web/default/dashboard/index.html
@@ -0,0 +1,150 @@
+[%
+ INCLUDE 'header.html'
+ title = loc('Dashboard')
+ robots = 'noindex, nofollow'
+ bodyclass = 'fullwidthpage'
+%]
+
+<style>
+ th[scope=row] { text-align: left; }
+ tr.subtotal { background-color: #eee; }
+ #overview tr:nth-child(2) { background-color: #fee; }
+</style>
+
+<form>
+
+<p>Ward: <select name="ward"><option value=''>All</option>
+ [% FOR w IN children.values.sort('name') %]
+ <option value="[% w.id %]"[% ' selected' IF w.id == ward %]>[% w.name %]</option>
+ [% END %]
+</select>
+
+<p>Report category: <select name="category"><option value=''>All</option>
+ [% FOR cat_op IN category_options %]
+ <option value='[% cat_op | html %]'[% ' selected' IF category == cat_op %]>[% cat_op | html %]</option>
+ [% END %]
+ </select>
+
+<p><input type="submit" value="Look up">
+
+<h2>Summary Statistics</h2>
+
+<table width="100%" id="overview">
+ <tr>
+ <td>&nbsp;</td>
+ <th scope="col">WTD</th>
+ <th scope="col">Last 7 days</th>
+ <th scope="col">Last 4 weeks</th>
+ <th scope="col">YTD</th>
+ </tr>
+
+ [%
+ rows = {
+ '0' => [ "total", "Total reports received" ]
+ '1' => [ "fixed - council", "Council has marked as fixed" ]
+ '2' => [ "fixed_user", "User has marked as fixed" ]
+ };
+ FOR row IN rows %]
+ <tr>
+ <th scope="row">[% row.value.1 %]</th>
+ <td>[% problems.wtd.${row.value.0} %]</td>
+ <td>[% problems.week.${row.value.0} %]</td>
+ <td>[% problems.weeks.${row.value.0} %]</td>
+ <td>[% problems.ytd.${row.value.0} %]</td>
+ </tr>
+ [% END %]
+
+ <tr class='subtotal'>
+ <th scope="row">Total marked as fixed</th>
+ <td>[% problems.wtd.${"fixed - council"} + problems.wtd.fixed_user %]</td>
+ <td>[% problems.week.${"fixed - council"} + problems.week.fixed_user %]</td>
+ <td>[% problems.weeks.${"fixed - council"} + problems.weeks.fixed_user %]</td>
+ <td>[% problems.ytd.${"fixed - council"} + problems.ytd.fixed_user %]</td>
+ </tr>
+
+ [%
+ rows = {
+ '0' => [ "in progress", "Council has marked as in progress" ]
+ '1' => [ "planned", "Council has marked as planned" ]
+ '2' => [ "investigating", "Council has marked as investigating" ]
+ };
+ wtd = 0, week = 0, weeks = 0, ytd = 0;
+ FOR row IN rows %]
+ <tr>
+ <th scope="row">[% row.value.1 %]</th>
+ <td>[% problems.wtd.${row.value.0} %]</td>
+ <td>[% problems.week.${row.value.0} %]</td>
+ <td>[% problems.weeks.${row.value.0} %]</td>
+ <td>[% problems.ytd.${row.value.0} %]</td>
+ </tr>
+ [% END %]
+
+ <tr class='subtotal'>
+ <th scope="row">Total marked</th>
+ <td>[% problems.wtd.${"in progress"} + problems.wtd.planned + problems.wtd.investigating %]</td>
+ <td>[% problems.week.${"in progress"} + problems.week.planned + problems.week.investigating %]</td>
+ <td>[% problems.weeks.${"in progress"} + problems.weeks.planned + problems.weeks.investigating %]</td>
+ <td>[% problems.ytd.${"in progress"} + problems.ytd.planned + problems.ytd.investigating %]</td>
+ </tr>
+
+ <tr>
+ <th scope="row">Average time to council marking fixed (days)</th>
+ <td>[% problems.wtd.time_to_fix %]</td>
+ <td>[% problems.week.time_to_fix %]</td>
+ <td>[% problems.weeks.time_to_fix %]</td>
+ <td>[% problems.ytd.time_to_fix %]</td>
+ </tr>
+
+ <tr>
+ <th scope="row">Average time to first council state change (days)</th>
+ <td>[% problems.wtd.time_to_mark %]</td>
+ <td>[% problems.week.time_to_mark %]</td>
+ <td>[% problems.weeks.time_to_mark %]</td>
+ <td>[% problems.ytd.time_to_mark %]</td>
+ </tr>
+
+ <tr class='subtotal'>
+ <th scope="row">Total not marked</th>
+ <td>[% problems.wtd.not_marked %]</td>
+ <td>[% problems.week.not_marked %]</td>
+ <td>[% problems.weeks.not_marked %]</td>
+ <td>[% problems.ytd.not_marked %]</td>
+ </tr>
+
+</table>
+
+<h2>Reports</h2>
+
+ </select>
+<p>Report state: <select name="state">
+<option value=''>All</option>
+ [% FOREACH state IN [ ['confirmed', loc('Open')], ['investigating',
+ loc('Investigating')], ['planned', loc('Planned')], ['in progress',
+ loc('In Progress')], ['closed', loc('Closed')], ['fixed', loc('Fixed')] ] %]
+ <option [% 'selected ' IF state.0 == q_state %] value="[% state.0 %]">[% state.1 %]</option>
+ [% END %]
+</select>
+<input type="submit" value="Look up">
+
+<table width="100%">
+ <tr>
+ <th scope="col">Less than 7 days old</th>
+ <th scope="col">7-14 days old</th>
+ <th scope="col">14-30 days old</th>
+ </tr>
+ <tr>
+ <td><ul>[% INCLUDE list, list = lists.1 %]</ul></td>
+ <td><ul>[% INCLUDE list, list = lists.2 %]</ul></td>
+ <td><ul>[% INCLUDE list, list = lists.3 %]</ul></td>
+ </tr>
+</table>
+
+</form>
+
+[% INCLUDE 'footer.html' %]
+
+[% BLOCK list %]
+[% FOR p IN list %]
+<li><a href="/report/[% p.id %]">[% p.title | html %]</a></li>
+[% END %]
+[% END %]