diff options
-rw-r--r-- | data/dashboard.json | 236 | ||||
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Dashboard.pm | 155 | ||||
-rw-r--r-- | t/app/controller/dashboard.t | 639 | ||||
-rw-r--r-- | templates/web/default/dashboard/index.html | 76 |
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 %] |