aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--data/dashboard.json236
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm155
-rw-r--r--t/app/controller/dashboard.t639
-rw-r--r--templates/web/default/dashboard/index.html76
4 files changed, 1040 insertions, 66 deletions
diff --git a/data/dashboard.json b/data/dashboard.json
new file mode 100644
index 000000000..1da667356
--- /dev/null
+++ b/data/dashboard.json
@@ -0,0 +1,236 @@
+{
+ "council": { "name": "South Borsetshire District Council" },
+ "wards": {
+ "1": { "id": 1, "name": "West Ward" },
+ "2": { "id": 2, "name": "East Ward" },
+ "3": { "id": 3, "name": "North Ward" },
+ "4": { "id": 4, "name": "South Ward" }
+ },
+ "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/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
index 0273e0eda..e4266dc64 100644
--- a/perllib/FixMyStreet/App/Controller/Dashboard.pm
+++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm
@@ -3,6 +3,7 @@ use Moose;
use namespace::autoclean;
use DateTime;
+use File::Slurp;
BEGIN { extends 'Catalyst::Controller'; }
@@ -18,6 +19,49 @@ Catalyst Controller.
=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->{council} = $j->{council};
+ $c->stash->{children} = $j->{wards};
+ $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.
@@ -48,20 +92,21 @@ sub index : Path : Args(0) {
# Set up the data for the dropdowns
+ my $council_detail = mySociety::MaPit::call('area', $council );
+ $c->stash->{council} = $council_detail;
+
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->stash->{all_councils} = { $council => $council_detail };
$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');
- $c->stash->{q_state} = $c->req->param('state');
my %where = (
council => $council, # XXX This will break in a two tier council. Restriction needs looking at...
@@ -71,40 +116,54 @@ sub index : Path : Args(0) {
if $c->stash->{ward};
$where{category} = $c->stash->{category}
if $c->stash->{category};
- if ( $c->stash->{q_state} eq 'fixed' ) {
- $where{'problem.state'} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
- } elsif ( $c->stash->{q_state} ) {
- $where{'problem.state'} = $c->stash->{q_state}
- }
+ $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 ) ] );
- $counts{wtd} = $c->forward( 'updates_search', [ {
- %where,
- 'me.confirmed' => { '>=', $t->subtract( days => $t->dow - 1 )
- } } ] );
-
- $counts{week} = $c->forward( 'updates_search', [ {
- %where,
- 'me.confirmed' => { '>=', DateTime->now->subtract( weeks => 1 )
- } } ] );
-
- $counts{weeks} = $c->forward( 'updates_search', [ {
- %where,
- 'me.confirmed' => { '>=', DateTime->now->subtract( weeks => 4 )
- } } ] );
+ $c->stash->{problems} = \%counts;
- $counts{ytd} = $c->forward( 'updates_search', [ {
- %where,
- 'me.confirmed' => { '>=', DateTime->today->set( day => 1, month => 1 )
- } } ] );
+ # List of reports underneath summary table
- $c->stash->{problems} = \%counts;
+ $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, $params ) = @_;
+ my ( $self, $c, $time ) = @_;
+
+ my $params = {
+ %{$c->stash->{where}},
+ 'me.confirmed' => { '>=', $time },
+ };
my $comments = $c->model('DB::Comment')->search(
$params,
@@ -124,14 +183,48 @@ sub updates_search : Private {
'fixed - user', 'fixed', 'unconfirmed', 'hidden',
'partial', 'planned');
+ for my $vars (
+ [ 'time_to_fix', 'fixed - council' ],
+ [ 'time_to_mark', 'in progress', 'planned', 'investigating', 'closed' ],
+ ) {
+ 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' }
+ { %$params, mark_fixed => 1, problem_state => undef }, { join => 'problem' }
)->count;
- $params->{state} = $params->{'problem.state'};
- delete $params->{'problem.state'};
+ $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;
}
diff --git a/t/app/controller/dashboard.t b/t/app/controller/dashboard.t
new file mode 100644
index 000000000..7033fa02c
--- /dev/null
+++ b/t/app/controller/dashboard.t
@@ -0,0 +1,639 @@
+use strict;
+use warnings;
+use Test::More;
+
+use FixMyStreet::TestMech;
+use Web::Scraper;
+
+my $mech = FixMyStreet::TestMech->new;
+
+my $test_user = 'council_user@example.com';
+my $test_pass = 'password';
+my $test_council = 2651;
+my $test_ward = 20723;
+
+$mech->delete_user( $test_user );
+my $user = FixMyStreet::App->model('DB::User')->create( {
+ email => $test_user,
+ password => $test_pass,
+} );
+
+my $p_user = FixMyStreet::App->model('DB::User')->find_or_create( {
+ email => 'p_user@example.com'
+} );
+
+$mech->not_logged_in_ok;
+$mech->get_ok('/dashboard');
+
+$mech->content_contains( 'sign in' );
+
+$mech->submit_form(
+ with_fields => { email => $test_user, password_sign_in => $test_pass }
+);
+
+is $mech->status, '404', 'If not council user get 404';
+
+$user->from_council( $test_council );
+$user->update;
+
+$mech->log_out_ok;
+$mech->get_ok('/dashboard');
+$mech->submit_form_ok( {
+ with_fields => { email => $test_user, password_sign_in => $test_pass }
+} );
+
+$mech->content_contains( 'Summary Statistics for City of Edinburgh' );
+
+FixMyStreet::App->model('DB::Contact')->search( { area_id => $test_council } )
+ ->delete;
+
+delete_problems();
+
+my @cats = qw( Grafitti Litter Potholes );
+for my $contact ( @cats ) {
+ FixMyStreet::App->model('DB::Contact')->create(
+ {
+ area_id => $test_council,
+ category => $contact,
+ email => "$contact\@example.org",
+ confirmed => 1,
+ whenedited => DateTime->now,
+ deleted => 0,
+ editor => 'test',
+ note => 'test',
+ }
+ );
+}
+
+$mech->get_ok('/dashboard');
+
+my $categories = scraper {
+ process "select[name=category] > option", 'cats[]' => 'TEXT',
+ process "select[name=ward] > option", 'wards[]' => 'TEXT',
+ process "table[id=overview] > tr", 'rows[]' => scraper {
+ process 'td', 'cols[]' => 'TEXT'
+ },
+ process "tr[id=total] > td", 'totals[]' => 'TEXT',
+ process "tr[id=fixed_council] > td", 'council[]' => 'TEXT',
+ process "tr[id=fixed_user] > td", 'user[]' => 'TEXT',
+ process "tr[id=total_fixed] > td", 'total_fixed[]' => 'TEXT',
+ process "tr[id=in_progress] > td", 'in_progress[]' => 'TEXT',
+ process "tr[id=planned] > td", 'planned[]' => 'TEXT',
+ process "tr[id=investigating] > td", 'investigating[]' => 'TEXT',
+ process "tr[id=marked] > td", 'marked[]' => 'TEXT',
+ process "tr[id=avg_marked] > td", 'avg_marked[]' => 'TEXT',
+ process "tr[id=avg_fixed] > td", 'avg_fixed[]' => 'TEXT',
+ process "tr[id=not_marked] > td", 'not_marked[]' => 'TEXT',
+ process "tr[id=closed] > td", 'closed[]' => 'TEXT',
+ process "table[id=reports] > tr > td", 'report_lists[]' => scraper {
+ process 'ul > li', 'reports[]' => 'TEXT'
+ },
+};
+
+my $expected_cats = [ 'All', '-- Pick a category --', @cats, 'Other' ];
+my $res = $categories->scrape( $mech->content );
+is_deeply( $res->{cats}, $expected_cats, 'correct list of categories' );
+
+foreach my $row ( @{ $res->{rows} }[1 .. 11] ) {
+ foreach my $col ( @{ $row->{cols} } ) {
+ is $col, 0;
+ }
+}
+
+for my $reports ( @{ $res->{report_lists} } ) {
+ is_deeply $reports, {}, 'No reports';
+}
+
+foreach my $test (
+ {
+ desc => 'confirmed today with no state',
+ dt => DateTime->now,
+ counts => [1,1,1,1],
+ report_counts => [1, 0, 0],
+ },
+ {
+ desc => 'confirmed last 7 days with no state',
+ dt => DateTime->now->subtract( days => 6, hours => 23 ),
+ counts => [1,2,2,2],
+ report_counts => [2, 0, 0],
+ },
+ {
+ desc => 'confirmed last 8 days with no state',
+ dt => DateTime->now->subtract( days => 8 ),
+ counts => [1,2,3,3],
+ report_counts => [2, 1, 0],
+ },
+ {
+ desc => 'confirmed last 4 weeks with no state',
+ dt => DateTime->now->subtract( weeks => 2 ),
+ counts => [1,2,4,4],
+ report_counts => [2, 1, 1],
+ },
+ {
+ desc => 'confirmed this year with no state',
+ dt => DateTime->now->subtract( weeks => 7 ),
+ counts => [1,2,4,5],
+ report_counts => [2, 1, 1],
+ },
+) {
+ subtest $test->{desc} => sub {
+ make_problem( { state => 'confirmed', conf_dt => $test->{dt} } );
+
+ $mech->get_ok('/dashboard');
+ $res = $categories->scrape( $mech->content );
+
+ check_row( $res, 'totals', $test->{counts} );
+ check_row( $res, 'not_marked', $test->{counts} );
+
+ check_report_counts( $res, $test->{report_counts} );
+ };
+}
+
+delete_problems();
+
+foreach my $test (
+ {
+ desc => 'user fixed today',
+ confirm_dt => DateTime->now->subtract( days => 1 ),
+ mark_dt => DateTime->now,
+ state => 'fixed - user',
+ counts => {
+ totals => [1,1,1,1],
+ user => [1,1,1,1],
+ council => [0,0,0,0],
+ avg_fixed => [0,0,0,0],
+ total_fixed => [1,1,1,1],
+ }
+ },
+ {
+ desc => 'council fixed today',
+ confirm_dt => DateTime->now->subtract( days => 1 ),
+ mark_dt => DateTime->now,
+ state => 'fixed - council',
+ counts => {
+ totals => [2,2,2,2],
+ user => [1,1,1,1],
+ council => [1,1,1,1],
+ avg_fixed => [1,1,1,1],
+ total_fixed => [2,2,2,2],
+ }
+ },
+ {
+ desc => 'marked investigating today',
+ confirm_dt => DateTime->now->subtract( days => 1 ),
+ mark_dt => DateTime->now,
+ state => 'investigating',
+ counts => {
+ totals => [3,3,3,3],
+ user => [1,1,1,1],
+ council => [1,1,1,1],
+ total_fixed => [2,2,2,2],
+ avg_marked => [1,1,1,1],
+ investigating => [1,1,1,1],
+ marked => [1,1,1,1]
+ }
+ },
+ {
+ desc => 'marked in progress today',
+ confirm_dt => DateTime->now->subtract( days => 1 ),
+ mark_dt => DateTime->now,
+ state => 'in progress',
+ counts => {
+ totals => [4,4,4,4],
+ user => [1,1,1,1],
+ council => [1,1,1,1],
+ total_fixed => [2,2,2,2],
+ avg_marked => [1,1,1,1],
+ investigating => [1,1,1,1],
+ in_progress => [1,1,1,1],
+ marked => [2,2,2,2]
+ }
+ },
+ {
+ desc => 'marked as planned today',
+ confirm_dt => DateTime->now->subtract( days => 1 ),
+ mark_dt => DateTime->now,
+ state => 'planned',
+ counts => {
+ totals => [5,5,5,5],
+ user => [1,1,1,1],
+ council => [1,1,1,1],
+ total_fixed => [2,2,2,2],
+ avg_marked => [1,1,1,1],
+ investigating => [1,1,1,1],
+ in_progress => [1,1,1,1],
+ planned => [1,1,1,1],
+ marked => [3,3,3,3]
+ }
+ },
+ {
+ desc => 'marked as planned today, confirmed a week ago',
+ confirm_dt => DateTime->now->subtract( days => 8 ),
+ mark_dt => DateTime->now,
+ state => 'planned',
+ counts => {
+ totals => [5,5,6,6],
+ user => [1,1,1,1],
+ council => [1,1,1,1],
+ total_fixed => [2,2,2,2],
+ avg_marked => [3,3,3,3],
+ investigating => [1,1,1,1],
+ in_progress => [1,1,1,1],
+ planned => [2,2,2,2],
+ marked => [4,4,4,4]
+ }
+ },
+ {
+ desc => 'marked as council fixed today, confirmed a week ago',
+ confirm_dt => DateTime->now->subtract( days => 8 ),
+ mark_dt => DateTime->now,
+ state => 'fixed - council',
+ counts => {
+ totals => [5,5,7,7],
+ user => [1,1,1,1],
+ council => [2,2,2,2],
+ total_fixed => [3,3,3,3],
+ avg_fixed => [5,5,5,5],
+ avg_marked => [3,3,3,3],
+ investigating => [1,1,1,1],
+ in_progress => [1,1,1,1],
+ planned => [2,2,2,2],
+ marked => [4,4,4,4]
+ }
+ },
+ {
+ desc => 'marked as council fixed a week ago, confirmed 3 weeks ago',
+ confirm_dt => DateTime->now->subtract( days => 21),
+ mark_dt => DateTime->now->subtract( days => 8 ),
+ state => 'fixed - council',
+ counts => {
+ totals => [5,5,8,8],
+ user => [1,1,1,1],
+ council => [2,2,3,3],
+ total_fixed => [3,3,4,4],
+ avg_fixed => [5,5,7,7],
+ avg_marked => [3,3,3,3],
+ investigating => [1,1,1,1],
+ in_progress => [1,1,1,1],
+ planned => [2,2,2,2],
+ marked => [4,4,4,4]
+ }
+ },
+ {
+ desc => 'marked as user fixed 6 weeks ago, confirmed 7 weeks ago',
+ confirm_dt => DateTime->now->subtract( weeks => 6 ),
+ mark_dt => DateTime->now->subtract( weeks => 7 ),
+ state => 'fixed - user',
+ counts => {
+ totals => [5,5,8,9],
+ user => [1,1,1,2],
+ council => [2,2,3,3],
+ total_fixed => [3,3,4,5],
+ avg_fixed => [5,5,7,7],
+ avg_marked => [3,3,3,3],
+ investigating => [1,1,1,1],
+ in_progress => [1,1,1,1],
+ planned => [2,2,2,2],
+ marked => [4,4,4,4]
+ }
+ },
+ {
+ desc => 'marked as closed',
+ confirm_dt => DateTime->now->subtract( days => 1 ),
+ mark_dt => DateTime->now,
+ state => 'closed',
+ counts => {
+ totals => [6,6,9,10],
+ user => [1,1,1,2],
+ council => [2,2,3,3],
+ total_fixed => [3,3,4,5],
+ avg_fixed => [5,5,7,7],
+ avg_marked => [2,2,2,2],
+ investigating => [1,1,1,1],
+ in_progress => [1,1,1,1],
+ planned => [2,2,2,2],
+ closed => [1,1,1,1],
+ marked => [5,5,5,5]
+ }
+ },
+) {
+ subtest $test->{desc} => sub {
+ make_problem(
+ {
+ state => $test->{state},
+ conf_dt => $test->{confirm_dt},
+ mark_dt => $test->{mark_dt},
+ }
+ );
+
+ $mech->get_ok('/dashboard');
+ $res = $categories->scrape( $mech->content );
+
+ foreach my $row ( keys %{ $test->{counts} } ) {
+ check_row( $res, $row, $test->{counts}->{$row} );
+ }
+ };
+}
+
+delete_problems();
+
+for my $test (
+ {
+ desc => 'Selecting no category does nothing',
+ p1 => {
+ state => 'confirmed',
+ conf_dt => DateTime->now(),
+ category => 'Potholes',
+ },
+ p2 => {
+ state => 'confirmed',
+ conf_dt => DateTime->now(),
+ category => 'Litter',
+ },
+ category => '',
+ counts => {
+ totals => [2,2,2,2],
+ },
+ counts_after => {
+ totals => [2,2,2,2],
+ },
+ report_counts => [2,0,0],
+ report_counts_after => [2,0,0],
+ },
+ {
+ desc => 'Limit display by category',
+ category => 'Potholes',
+ counts => {
+ totals => [2,2,2,2],
+ },
+ counts_after => {
+ totals => [1,1,1,1],
+ },
+ report_counts => [2,0,0],
+ report_counts_after => [1,0,0],
+ },
+ {
+ desc => 'Limit display for category with no entries',
+ category => 'Grafitti',
+ counts => {
+ totals => [2,2,2,2],
+ },
+ counts_after => {
+ totals => [0,0,0,0],
+ },
+ report_counts => [2,0,0],
+ report_counts_after => [0,0,0],
+ },
+ {
+ desc => 'Limit display by category for council fixed',
+ p1 => {
+ state => 'fixed - council',
+ conf_dt => DateTime->now()->subtract( weeks => 1 ),
+ mark_dt => DateTime->now()->subtract( weeks => 1 ),
+ category => 'Potholes',
+ },
+ p2 => {
+ state => 'fixed - council',
+ conf_dt => DateTime->now()->subtract( weeks => 1 ),
+ mark_dt => DateTime->now()->subtract( weeks => 1 ),
+ category => 'Litter',
+ },
+ category => 'Potholes',
+ counts => {
+ council => [0,0,2,2],
+ totals => [2,2,4,4],
+ },
+ counts_after => {
+ council => [0,0,1,1],
+ totals => [1,1,2,2],
+ },
+ report_counts => [2,2,0],
+ report_counts_after => [1,1,0],
+ },
+ {
+ desc => 'Limit display by category for user fixed',
+ p1 => {
+ state => 'fixed - user',
+ conf_dt => DateTime->now()->subtract( weeks => 1 ),
+ mark_dt => DateTime->now()->subtract( weeks => 1 ),
+ category => 'Potholes',
+ },
+ p2 => {
+ state => 'fixed - user',
+ conf_dt => DateTime->now()->subtract( weeks => 1 ),
+ mark_dt => DateTime->now()->subtract( weeks => 1 ),
+ category => 'Litter',
+ },
+ category => 'Potholes',
+ counts => {
+ user => [0,0,2,2],
+ council => [0,0,2,2],
+ totals => [2,2,6,6],
+ },
+ counts_after => {
+ user => [0,0,1,1],
+ council => [0,0,1,1],
+ totals => [1,1,3,3],
+ },
+ report_counts => [2,4,0],
+ report_counts_after => [1,2,0],
+ },
+ {
+ desc => 'Limit display by ward',
+ p1 => {
+ state => 'confirmed',
+ conf_dt => DateTime->now()->subtract( weeks => 1 ),
+ category => 'Potholes',
+ # in real life it has commas around it and the search
+ # uses them
+ areas => ',20720,',
+ },
+ p2 => {
+ state => 'fixed - council',
+ conf_dt => DateTime->now()->subtract( weeks => 1 ),
+ mark_dt => DateTime->now()->subtract( weeks => 1 ),
+ category => 'Litter',
+ areas => ',20720,',
+ },
+ ward => 20720,
+ counts => {
+ user => [0,0,2,2],
+ council => [0,0,3,3],
+ totals => [2,2,8,8],
+ },
+ counts_after => {
+ user => [0,0,0,0],
+ council => [0,0,1,1],
+ totals => [0,0,2,2],
+ },
+ report_counts => [2,6,0],
+ report_counts_after => [0,2,0],
+ },
+) {
+ subtest $test->{desc} => sub {
+ make_problem( $test->{p1} ) if $test->{p1};
+ make_problem( $test->{p2} ) if $test->{p2};
+
+ $mech->get_ok('/dashboard');
+
+ $res = $categories->scrape( $mech->content );
+
+ foreach my $row ( keys %{ $test->{counts} } ) {
+ check_row( $res, $row, $test->{counts}->{$row} );
+ }
+
+ check_report_counts( $res, $test->{report_counts} );
+
+ $mech->submit_form_ok( {
+ with_fields => {
+ category => $test->{category},
+ ward => $test->{ward},
+ }
+ } );
+
+ $res = $categories->scrape( $mech->content );
+
+ foreach my $row ( keys %{ $test->{counts_after} } ) {
+ check_row( $res, $row, $test->{counts_after}->{$row} );
+ }
+ check_report_counts( $res, $test->{report_counts_after} );
+ };
+}
+
+delete_problems();
+
+for my $test (
+ {
+ desc => 'Selecting no state does nothing',
+ p1 => {
+ state => 'fixed - user',
+ conf_dt => DateTime->now(),
+ category => 'Potholes',
+ },
+ p2 => {
+ state => 'confirmed',
+ conf_dt => DateTime->now(),
+ category => 'Litter',
+ },
+ state => '',
+ report_counts => [2,0,0],
+ report_counts_after => [2,0,0],
+ },
+ {
+ desc => 'limit by state works',
+ state => 'fixed',
+ report_counts => [2,0,0],
+ report_counts_after => [1,0,0],
+ },
+ {
+ desc => 'All fixed states count as fixed',
+ p1 => {
+ state => 'fixed - council',
+ conf_dt => DateTime->now(),
+ category => 'Potholes',
+ },
+ p2 => {
+ state => 'fixed',
+ conf_dt => DateTime->now(),
+ category => 'Potholes',
+ },
+ state => 'fixed',
+ report_counts => [4,0,0],
+ report_counts_after => [3,0,0],
+ },
+) {
+ subtest $test->{desc} => sub {
+ make_problem( $test->{p1} ) if $test->{p1};
+ make_problem( $test->{p2} ) if $test->{p2};
+
+ $mech->get_ok('/dashboard');
+
+ $res = $categories->scrape( $mech->content );
+
+ check_report_counts( $res, $test->{report_counts} );
+
+ $mech->submit_form_ok( {
+ with_fields => {
+ state => $test->{state},
+ }
+ } );
+
+ $res = $categories->scrape( $mech->content );
+
+ check_report_counts( $res, $test->{report_counts_after} );
+ };
+}
+
+sub make_problem {
+ my $args = shift;
+
+ my $p = FixMyStreet::App->model('DB::Problem')->create( {
+ title => 'a problem',
+ name => 'a user',
+ anonymous => 1,
+ detail => 'some detail',
+ state => $args->{state},
+ confirmed => $args->{conf_dt},
+ whensent => $args->{conf_dt},
+ lastupdate => $args->{mark_dt} || $args->{conf_dt},
+ council => $test_council,
+ postcode => 'EH99 1SP',
+ latitude => '51',
+ longitude => '1',
+ areas => $args->{areas} || $test_ward,
+ used_map => 0,
+ user_id => $p_user->id,
+ category => $args->{category} || 'Other',
+ } );
+
+ if ( $args->{state} ne 'confirmed' ) {
+ my $c = FixMyStreet::App->model('DB::Comment')->create( {
+ problem => $p,
+ user_id => $p_user->id,
+ state => 'confirmed',
+ problem_state => $args->{state} =~ /^fixed - user|fixed$/ ? undef : $args->{state},
+ confirmed => $args->{mark_dt},
+ text => 'an update',
+ mark_fixed => $args->{state} =~ /fixed/ ? 1 : 0,
+ anonymous => 1,
+ } );
+ }
+}
+
+sub check_row {
+ my $res = shift;
+ my $row = shift;
+ my $totals = shift;
+
+ is $res->{ $row }->[0], $totals->[0], "Correct count in $row for WTD";
+ is $res->{ $row }->[1], $totals->[1], "Correct count in $row for last 7 days";
+ is $res->{ $row }->[2], $totals->[2], "Correct count in $row for last 4 weeks";
+ is $res->{ $row }->[3], $totals->[3], "Correct count in $row for YTD";
+}
+
+sub check_report_counts {
+ my $res = shift;
+ my $counts = shift;
+
+ for my $i ( 0 .. 2 ) {
+ if ( $counts->[$i] == 0 ) {
+ is_deeply $res->{report_lists}->[$i], {}, "No reports for column $i";
+ } else {
+ if ( ref( $res->{report_lists}->[$i]->{reports} ) eq 'ARRAY' ) {
+ is scalar @{ $res->{report_lists}->[$i]->{reports} }, $counts->[$i], "Correct report count for column $i";
+ } else {
+ fail "Correct report count for column $i ( no reports )";
+ }
+ }
+ }
+}
+
+sub delete_problems {
+ FixMyStreet::App->model('DB::Comment')
+ ->search( { 'problem.council' => $test_council }, { join => 'problem' } )
+ ->delete;
+ FixMyStreet::App->model('DB::Problem')
+ ->search( { council => $test_council } )->delete();
+}
+
+done_testing;
diff --git a/templates/web/default/dashboard/index.html b/templates/web/default/dashboard/index.html
index a0ef7b911..be38234f2 100644
--- a/templates/web/default/dashboard/index.html
+++ b/templates/web/default/dashboard/index.html
@@ -9,6 +9,7 @@
th[scope=row] { text-align: left; }
tr.subtotal { background-color: #eee; }
#overview tr:nth-child(2) { background-color: #fee; }
+ select { width: auto; }
</style>
<form>
@@ -18,16 +19,16 @@
<option value="[% w.id %]"[% ' selected' IF w.id == ward %]>[% w.name %]</option>
[% END %]
</select>
-<input type="submit" value="Look up">
-
-<h2>Performance Overview</h2>
<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>
-<input type="submit" value="Look up">
+
+<p><input type="submit" value="Look up">
+
+<h2>Summary Statistics for [% council.name %]</h2>
<table width="100%" id="overview">
<tr>
@@ -45,7 +46,7 @@
'2' => [ "fixed_user", "User has marked as fixed" ]
};
FOR row IN rows %]
- <tr>
+ <tr id="[% row.value.0.replace('[^\w]+', '_' ) %]">
<th scope="row">[% row.value.1 %]</th>
<td>[% problems.wtd.${row.value.0} %]</td>
<td>[% problems.week.${row.value.0} %]</td>
@@ -54,7 +55,7 @@
</tr>
[% END %]
- <tr class='subtotal'>
+ <tr class='subtotal' id="total_fixed">
<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>
@@ -67,10 +68,11 @@
'0' => [ "in progress", "Council has marked as in progress" ]
'1' => [ "planned", "Council has marked as planned" ]
'2' => [ "investigating", "Council has marked as investigating" ]
+ '3' => [ "closed", "Council has marked as closed" ]
};
wtd = 0, week = 0, weeks = 0, ytd = 0;
FOR row IN rows %]
- <tr>
+ <tr id="[% row.value.0.replace('[^\w]+', '_' ) %]">
<th scope="row">[% row.value.1 %]</th>
<td>[% problems.wtd.${row.value.0} %]</td>
<td>[% problems.week.${row.value.0} %]</td>
@@ -79,36 +81,36 @@
</tr>
[% END %]
- <tr class='subtotal'>
+ <tr class='subtotal' id="marked">
<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>
+ <td>[% problems.wtd.${"in progress"} + problems.wtd.planned + problems.wtd.investigating + problems.wtd.closed %]</td>
+ <td>[% problems.week.${"in progress"} + problems.week.planned + problems.week.investigating + problems.wtd.closed %]</td>
+ <td>[% problems.weeks.${"in progress"} + problems.weeks.planned + problems.weeks.investigating + problems.wtd.closed %]</td>
+ <td>[% problems.ytd.${"in progress"} + problems.ytd.planned + problems.ytd.investigating + problems.wtd.closed %]</td>
</tr>
- <tr>
- <th scope="row">Average time to fix</th>
- <td>-</td>
- <td>-</td>
- <td>-</td>
- <td>-</td>
+ <tr id="avg_fixed">
+ <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 mark</th>
- <td>-</td>
- <td>-</td>
- <td>-</td>
- <td>-</td>
+ <tr id="avg_marked">
+ <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'>
+ <tr class='subtotal' id="not_marked">
<th scope="row">Total not marked</th>
- <td>-</td>
- <td>-</td>
- <td>-</td>
- <td>-</td>
+ <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>
@@ -126,21 +128,25 @@
</select>
<input type="submit" value="Look up">
-<table width="100%">
+<table width="100%" id="reports">
<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>
- <th scope="col">30+ days old</th>
</tr>
<tr>
- <td><ul><li></li></ul></td>
- <td><ul><li></li></ul></td>
- <td><ul><li></li></ul></td>
- <td><ul><li></li></ul></td>
+ <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 %]