aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--data/dashboard.json264
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm2
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/AreaStats.pm225
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Stats.pm83
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm381
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm1
-rw-r--r--t/app/controller/area_stats.t234
-rw-r--r--t/app/controller/dashboard.t810
-rw-r--r--templates/web/base/admin/areastats/area.html72
-rw-r--r--templates/web/base/admin/areastats/index.html11
-rw-r--r--templates/web/base/admin/stats/index.html98
-rw-r--r--templates/web/base/dashboard/index.html219
-rw-r--r--web/cobrands/fixmystreet/dashboard.scss5
14 files changed, 476 insertions, 1930 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5f9e335a9..68b07ca0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,7 @@
- Admins can now unban users #1881
- Council dashboard has date range for report generation #1885
- More JavaScript-enhanced `<select multiple>` elements #1589
+ - Consolidate various admin summary statistics page. #1919.
- UK:
- Use SVG logo, inlined on front page. #1887
- Inline critical CSS on front page.
diff --git a/data/dashboard.json b/data/dashboard.json
index a15ff1b02..83d9d6555 100644
--- a/data/dashboard.json
+++ b/data/dashboard.json
@@ -1,236 +1,58 @@
{
- "council": { "name": "South Borsetshire District Council" },
- "wards": {
+ "body": { "name": "South Borsetshire District Council" },
+ "children": {
"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": [
- { "name": "Abandoned vehicles", "value": "Abandoned vehicles" },
- { "name": "Grafitti", "value": "Grafitti" },
- { "name": "Potholes", "value": "Potholes" },
- { "name": "Street lighting", "value": "Street lighting" },
- { "name": "Trees", "value": "Trees" },
- { "name": "Other", "value": "Other" }
+ "contacts": [
+ { "category": "Abandoned vehicles", "category_display": "Abandoned vehicles" },
+ { "category": "Grafitti", "category_display": "Grafitti" },
+ { "category": "Potholes", "category_display": "Potholes" },
+ { "category": "Street lighting", "category_display": "Street lighting" },
+ { "category": "Trees", "category_display": "Trees" },
+ { "category": "Other", "category_display": "Other" }
],
- "counts_all": {
- "wtd": {
- "total": 10,
- "action scheduled": 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,
- "action scheduled": 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,
- "action scheduled": 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,
- "action scheduled": 23,
- "in progress": 34,
- "investigating": 9,
- "fixed - council": 72,
- "fixed_user": 33,
- "time_to_fix": 2,
- "time_to_mark": 2,
- "not_marked": 1
- }
+ "summary_open": 3133,
+ "summary_closed": 1928,
+ "summary_fixed": 1895,
+ "totals": {
+ "total": 543,
+ "open": 139,
+ "closed": 178,
+ "fixed": 226
},
- "counts_some": {
- "wtd": {
- "total": 5,
- "action scheduled": 0,
- "in progress": 1,
- "investigating": 1,
- "fixed - council": 2,
- "fixed_user": 1,
- "time_to_fix": 2,
- "time_to_mark": 2,
- "not_marked": 0
+ "grouped": {
+ "Abandoned vehicles": {
+ "total": 140,
+ "open": 21,
+ "closed": 59,
+ "fixed": 10
},
- "week": {
- "total": 7,
- "action scheduled": 1,
- "in progress": 2,
- "investigating": 1,
- "fixed - council": 2,
- "fixed_user": 1,
- "time_to_fix": 3,
- "time_to_mark": 2,
- "not_marked": 0
+ "Graffiti": {
+ "total": 96,
+ "open": 7,
+ "closed": 5,
+ "fixed": 84
},
- "weeks": {
- "total": 57,
- "action scheduled": 5,
- "in progress": 16,
- "investigating": 4,
- "fixed - council": 23,
- "fixed_user": 9,
- "time_to_fix": 2,
- "time_to_mark": 2,
- "not_marked": 1
+ "Street lighting": {
+ "total": 175,
+ "open": 39,
+ "closed": 23,
+ "fixed": 113
},
- "ytd": {
- "total": 57,
- "action scheduled": 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" }
- ]
+ "Trees": {
+ "total": 67,
+ "open": 39,
+ "closed": 27,
+ "fixed": 1
},
- "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" }
- ]
+ "Potholes": {
+ "total": 115,
+ "open": 33,
+ "closed": 64,
+ "fixed": 18
}
}
}
diff --git a/perllib/FixMyStreet/App/Controller/Admin.pm b/perllib/FixMyStreet/App/Controller/Admin.pm
index 0551a4d73..b485ea2dc 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -471,7 +471,7 @@ sub fetch_contacts : Private {
my $contacts = $c->stash->{body}->contacts->search(undef, { order_by => [ 'category' ] } );
$c->stash->{contacts} = $contacts;
- $c->stash->{live_contacts} = $contacts->search({ state => { '!=' => 'deleted' } });
+ $c->stash->{live_contacts} = $contacts->not_deleted;
$c->stash->{any_not_confirmed} = $contacts->search({ state => 'unconfirmed' })->count;
if ( $c->get_param('text') && $c->get_param('text') eq '1' ) {
diff --git a/perllib/FixMyStreet/App/Controller/Admin/AreaStats.pm b/perllib/FixMyStreet/App/Controller/Admin/AreaStats.pm
deleted file mode 100644
index 2058ea872..000000000
--- a/perllib/FixMyStreet/App/Controller/Admin/AreaStats.pm
+++ /dev/null
@@ -1,225 +0,0 @@
-package FixMyStreet::App::Controller::Admin::AreaStats;
-use Moose;
-use namespace::autoclean;
-use List::Util qw(sum);
-
-BEGIN { extends 'Catalyst::Controller'; }
-
-sub index : Path : Args(0) {
- my ( $self, $c ) = @_;
-
- my $user = $c->user;
-
- if ($user->is_superuser) {
- $c->forward('/admin/fetch_all_bodies');
- } elsif ( $user->from_body ) {
- $c->forward('load_user_body', [ $user->from_body->id ]);
- $c->stash->{body_id} = $user->from_body->id;
- if ($user->area_id) {
- $c->stash->{area_id} = $user->area_id;
- $c->forward('setup_area');
- $c->visit( 'stats' );
- } else {
- # visit body_stats so we load the list of child areas
- $c->visit( 'body_stats', [ $user->from_body->id], [] );
- }
- } else {
- $c->detach( '/page_error_404_not_found' );
- }
-}
-
-sub check_user : Private {
- my ( $self, $c, $body_id, $area_id ) = @_;
-
- my $user = $c->user;
-
- return if $user->is_superuser;
-
- if ($body_id and $user->from_body->id eq $body_id) {
- if (not $user->area_id) {
- return;
- } elsif ($area_id and $user->area_id eq $area_id) {
- return;
- }
- }
-
- $c->detach( '/page_error_404_not_found' );
-}
-
-sub setup_area : Private {
- my ($self, $c) = @_;
-
- my $area = mySociety::MaPit::call('area', $c->stash->{area_id} );
- $c->detach( '/page_error_404_not_found' ) if $area->{error};
- $c->stash->{area} = $area;
-}
-
-sub body_base : Chained('/') : PathPart('admin/areastats') : CaptureArgs(1) {
- my ($self, $c, $body_id) = @_;
-
- $c->forward('/admin/lookup_body', $body_id);
- $c->stash->{areas} = mySociety::MaPit::call('area/children', [ $c->stash->{body_id} ] );
-}
-
-sub body_stats : Chained('body_base') : PathPart('') : Args(0) {
- my ($self, $c) = @_;
-
- if ($c->get_param('area')) {
- $c->forward('check_user', [$c->stash->{body_id}, $c->get_param('area')]);
- $c->stash->{area_id} = $c->get_param('area');
- $c->forward('setup_area');
- } else {
- $c->forward('check_user', [$c->stash->{body_id}]);
- }
- $c->forward('stats');
-}
-
-sub stats : Private {
- my ($self, $c) = @_;
-
- my $date = DateTime->now->subtract(days => 30);
- # set it to midnight so we get consistent result through the day
- $date->truncate( to => 'day' );
-
- $c->forward('/admin/fetch_contacts');
-
- $c->stash->{template} = 'admin/areastats/area.html';
-
- my $dtf = $c->model('DB')->storage->datetime_parser;
- my $time = $dtf->format_datetime($date);
-
- my $params = {
- 'me.confirmed' => { '>=', $time },
- };
-
- my %area_param = ();
- if ($c->stash->{area}) {
- my $area_id = $c->stash->{area_id};
- $c->stash->{area_name} = $c->stash->{area}->{name};
- $params->{'problem.areas'} = { like => "%,$area_id,%" };
- %area_param = (
- areas => { like => "%,$area_id,%" },
- );
- } else {
- $c->stash->{area_name} = $c->stash->{body}->name;
- }
-
- my %by_category = map { $_->category => {} } $c->stash->{contacts}->all;
- my %recent_by_category = map { $_->category => 0 } $c->stash->{contacts}->all;
-
- my $state_map = {};
-
- $state_map->{$_} = 'open' foreach FixMyStreet::DB::Result::Problem->open_states;
- $state_map->{$_} = 'closed' foreach FixMyStreet::DB::Result::Problem->closed_states;
- $state_map->{$_} = 'fixed' foreach FixMyStreet::DB::Result::Problem->fixed_states;
- $state_map->{$_} = 'scheduled' foreach ('planned', 'action scheduled');
-
- # current problems by category and state
- my $problems = $c->model('DB::Problem')->to_body(
- $c->stash->{body}
- )->search(
- \%area_param,
- {
- group_by => [ 'category', 'state' ],
- select => [ 'category', 'state', { count => 'me.id' } ],
- as => [ qw/category state state_count/ ],
- }
- );
-
- while (my $p = $problems->next) {
- my $meta_state = $state_map->{$p->state};
- $by_category{$p->category}->{$meta_state} += $p->get_column('state_count');
- }
- $c->stash->{by_category} = \%by_category;
-
- # problems this month by state
- $c->stash->{$_} = 0 for values %$state_map;
-
- $c->stash->{open} = $c->model('DB::Problem')->to_body(
- $c->stash->{body}
- )->search(
- {
- %area_param,
- confirmed => { '>=' => $time },
- }
- )->count;
-
- my $comments = $c->model('DB::Comment')->to_body(
- $c->stash->{body}
- )->search(
- {
- %$params,
- 'me.id' => { 'in' => \"(select min(id) from comment where me.problem_id=comment.problem_id and problem_state not in ('', 'confirmed') group by problem_state)" },
- },
- {
- join => 'problem',
- group_by => [ 'problem_state' ],
- select => [ 'problem_state', { count => 'me.id' } ],
- as => [ qw/problem_state state_count/ ],
- }
- );
-
- while (my $comment = $comments->next) {
- my $meta_state = $state_map->{$comment->problem_state};
- $c->stash->{$meta_state} += $comment->get_column('state_count');
- }
-
- $params = {
- %area_param,
- 'me.confirmed' => { '>=', $time },
- };
-
- # problems this month by category
- my $recent_problems = $c->model('DB::Problem')->to_body(
- $c->stash->{body}
- )->search(
- $params,
- {
- group_by => [ 'category' ],
- select => [ 'category', { count => 'me.id' } ],
- as => [ qw/category category_count/ ],
- }
- );
-
- while (my $p = $recent_problems->next) {
- $recent_by_category{$p->category} += $p->get_column('category_count');
- }
- $c->stash->{recent_by_category} = \%recent_by_category;
-
- # average time to state change in last month
- $params = {
- %area_param,
- 'problem.confirmed' => { '>=', $time },
- };
-
- $comments = $c->model('DB::Comment')->to_body(
- $c->stash->{body}
- )->search(
- { %$params,
- 'me.id' => \"= (select min(id) from comment where me.problem_id=comment.problem_id)",
- 'me.problem_state' => { '!=' => 'confirmed' },
- },
- {
- select => [
- { avg => { extract => "epoch from me.confirmed-problem.confirmed" } },
- ],
- as => [ qw/time/ ],
- join => 'problem'
- }
- )->first;
- my $raw_average = $comments->get_column('time');
- if (defined $raw_average) {
- $c->stash->{average} = int( $raw_average / 60 / 60 / 24 + 0.5 );
- } else {
- $c->stash->{average} = -1;
- }
-}
-
-sub load_user_body : Private {
- my ($self, $c, $body_id) = @_;
-
- $c->stash->{body} = $c->model('DB::Body')->find($body_id)
- or $c->detach( '/page_error_404_not_found' );
-}
-
-1;
diff --git a/perllib/FixMyStreet/App/Controller/Admin/Stats.pm b/perllib/FixMyStreet/App/Controller/Admin/Stats.pm
index 35e150890..2860b3531 100644
--- a/perllib/FixMyStreet/App/Controller/Admin/Stats.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin/Stats.pm
@@ -6,88 +6,7 @@ BEGIN { extends 'Catalyst::Controller'; }
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
-
- my $selected_body;
- if ( $c->user->is_superuser ) {
- $c->forward('/admin/fetch_all_bodies');
- $selected_body = $c->get_param('body');
- } else {
- $selected_body = $c->user->from_body->id;
- }
-
- if ( $c->cobrand->moniker eq 'zurich' ) {
- return $c->cobrand->admin_stats();
- }
-
- if ( $c->get_param('getcounts') ) {
-
- my ( $start_date, $end_date, @errors );
- my $parser = DateTime::Format::Strptime->new( pattern => '%d/%m/%Y' );
-
- $start_date = $parser-> parse_datetime ( $c->get_param('start_date') );
-
- push @errors, _('Invalid start date') unless defined $start_date;
-
- $end_date = $parser-> parse_datetime ( $c->get_param('end_date') ) ;
-
- push @errors, _('Invalid end date') unless defined $end_date;
-
- $c->stash->{errors} = \@errors;
- $c->stash->{start_date} = $start_date;
- $c->stash->{end_date} = $end_date;
-
- $c->stash->{unconfirmed} = $c->get_param('unconfirmed') eq 'on' ? 1 : 0;
-
- return 1 if @errors;
-
- my $bymonth = $c->get_param('bymonth');
- $c->stash->{bymonth} = $bymonth;
-
- $c->stash->{selected_body} = $selected_body;
-
- my $field = 'confirmed';
-
- $field = 'created' if $c->get_param('unconfirmed');
-
- my $one_day = DateTime::Duration->new( days => 1 );
-
-
- my %select = (
- select => [ 'state', { 'count' => 'me.id' } ],
- as => [qw/state count/],
- group_by => [ 'state' ],
- order_by => [ 'state' ],
- );
-
- if ( $c->get_param('bymonth') ) {
- %select = (
- select => [
- { extract => \"year from $field", -as => 'c_year' },
- { extract => \"month from $field", -as => 'c_month' },
- { 'count' => 'me.id' }
- ],
- as => [qw/c_year c_month count/],
- group_by => [qw/c_year c_month/],
- order_by => [qw/c_year c_month/],
- );
- }
-
- my $p = $c->cobrand->problems->to_body($selected_body)->search(
- {
- -AND => [
- $field => { '>=', $start_date},
- $field => { '<=', $end_date + $one_day },
- ],
- },
- \%select,
- );
-
- # in case the total_report count is 0
- $c->stash->{show_count} = 1;
- $c->stash->{states} = $p;
- }
-
- return 1;
+ return $c->cobrand->admin_stats() if $c->cobrand->moniker eq 'zurich';
}
sub state : Local : Args(0) {
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
index 27661b736..bc9e78333 100644
--- a/perllib/FixMyStreet/App/Controller/Dashboard.pm
+++ b/perllib/FixMyStreet/App/Controller/Dashboard.pm
@@ -3,8 +3,9 @@ use Moose;
use namespace::autoclean;
use DateTime;
-use File::Slurp;
use JSON::MaybeXS;
+use Path::Tiny;
+use Time::Piece;
BEGIN { extends 'Catalyst::Controller'; }
@@ -20,43 +21,21 @@ Catalyst Controller.
=cut
+sub auto : Private {
+ my ($self, $c) = @_;
+ $c->stash->{filter_states} = $c->cobrand->state_groups_inspect;
+ return 1;
+}
+
sub example : Local : Args(0) {
my ( $self, $c ) = @_;
$c->stash->{template} = 'dashboard/index.html';
- $c->stash->{filter_states} = $c->cobrand->state_groups_inspect;
-
- $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_bodies' );
-
- # See if we've had anything from the dropdowns - perhaps vary results if so
- $c->stash->{ward} = $c->get_param('ward');
- $c->stash->{category} = $c->get_param('category');
- $c->stash->{q_state} = $c->get_param('state');
+ $c->stash->{group_by} = 'category+state';
eval {
- my $data = File::Slurp::read_file(
- FixMyStreet->path_to( 'data/dashboard.json' )->stringify
- );
- my $j = decode_json($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};
- }
+ my $j = decode_json(path(FixMyStreet->path_to('data/dashboard.json'))->slurp_utf8);
+ $c->stash($j);
};
if ($@) {
my $message = _("There was a problem showing this page. Please try again later.") . ' ' .
@@ -77,141 +56,244 @@ sub check_page_allowed : Private {
$c->detach( '/auth/redirect' ) unless $c->user_exists;
$c->detach( '/page_error_404_not_found' )
- unless $c->user_exists && $c->user->from_body;
+ unless $c->user->from_body || $c->user->is_superuser;
+
+ my $body = $c->user->from_body;
+ if (!$body && $c->get_param('body')) {
+ # Must be a superuser, so allow query parameter if given
+ $body = $c->model('DB::Body')->find({ id => $c->get_param('body') });
+ }
- return $c->user->from_body;
+ return $body;
}
=head2 index
-Show the dashboard table.
+Show the summary statistics table.
=cut
sub index : Path : Args(0) {
my ( $self, $c ) = @_;
- my $body = $c->forward('check_page_allowed');
- $c->stash->{body} = $body;
+ my $body = $c->stash->{body} = $c->forward('check_page_allowed');
- # Set up the data for the dropdowns
- $c->stash->{filter_states} = $c->cobrand->state_groups_inspect;
+ if ($body) {
+ $c->stash->{body_name} = $body->name;
- # Just take the first area ID we find
- my $area_id = $body->body_areas->first->area_id;
+ my $area_id = $body->body_areas->first->area_id;
+ my $children = mySociety::MaPit::call('area/children', $area_id,
+ type => $c->cobrand->area_types_children,
+ );
+ $c->stash->{children} = $children;
- my $council_detail = mySociety::MaPit::call('area', $area_id );
- $c->stash->{council} = $council_detail;
+ $c->forward('/admin/fetch_contacts');
+ $c->stash->{contacts} = [ $c->stash->{contacts}->all ];
- my $children = mySociety::MaPit::call('area/children', $area_id,
- type => $c->cobrand->area_types_children,
- );
- $c->stash->{children} = $children;
+ # See if we've had anything from the body dropdowns
+ $c->stash->{category} = $c->get_param('category');
+ $c->stash->{ward} = $c->get_param('ward');
+ if ($c->user->area_id) {
+ $c->stash->{ward} = $c->user->area_id;
+ $c->stash->{body_name} = join "", map { $children->{$_}->{name} } grep { $children->{$_} } $c->user->area_id;
+ }
+ } else {
+ $c->forward('/admin/fetch_all_bodies');
+ }
- $c->stash->{all_areas} = { $area_id => $council_detail };
- $c->forward( '/report/new/setup_categories_and_bodies' );
+ $c->stash->{start_date} = $c->get_param('start_date');
+ $c->stash->{end_date} = $c->get_param('end_date');
+ $c->stash->{q_state} = $c->get_param('state') || '';
- # See if we've had anything from the dropdowns
+ $c->forward('construct_rs_filter');
- $c->stash->{ward} = $c->get_param('ward');
- $c->stash->{category} = $c->get_param('category');
+ if ( $c->get_param('export') ) {
+ $self->export_as_csv($c);
+ } else {
+ $self->generate_data($c);
+ }
+}
- my %where = (
- 'problem.state' => [ FixMyStreet::DB::Result::Problem->visible_states() ],
- );
+sub construct_rs_filter : Private {
+ my ($self, $c) = @_;
+
+ my %where;
$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->{'me.state'} = $prob_where->{'problem.state'};
- delete $prob_where->{'problem.state'};
- $c->stash->{prob_where} = $prob_where;
-
- my $dtf = $c->model('DB')->storage->datetime_parser;
- my %counts;
- my $now = DateTime->now( time_zone => FixMyStreet->local_time_zone );
- my $t = $now->clone->truncate( to => 'day' );
- $counts{wtd} = $c->forward( 'updates_search',
- [ $dtf->format_datetime( $t->clone->subtract( days => $t->dow - 1 ) ) ] );
- $counts{week} = $c->forward( 'updates_search',
- [ $dtf->format_datetime( $now->clone->subtract( weeks => 1 ) ) ] );
- $counts{weeks} = $c->forward( 'updates_search',
- [ $dtf->format_datetime( $now->clone->subtract( weeks => 4 ) ) ] );
- $counts{ytd} = $c->forward( 'updates_search',
- [ $dtf->format_datetime( $t->clone->set( day => 1, month => 1 ) ) ] );
-
- $c->stash->{problems} = \%counts;
+ my $state = $c->stash->{q_state};
+ if ( FixMyStreet::DB::Result::Problem->fixed_states->{$state} ) { # Probably fixed - council
+ $where{'me.state'} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
+ } elsif ( $state ) {
+ $where{'me.state'} = $state;
+ } else {
+ $where{'me.state'} = [ FixMyStreet::DB::Result::Problem->visible_states() ];
+ }
- # List of reports underneath summary table
+ my $dtf = $c->model('DB')->storage->datetime_parser;
+ my $date = DateTime->now( time_zone => FixMyStreet->local_time_zone )->subtract(days => 30);
+ $date->truncate( to => 'day' );
- $c->stash->{q_state} = $c->get_param('state') || '';
- if ( $c->stash->{q_state} eq 'fixed - council' ) {
- $prob_where->{'me.state'} = [ FixMyStreet::DB::Result::Problem->fixed_states() ];
- } elsif ( $c->stash->{q_state} ) {
- $prob_where->{'me.state'} = $c->stash->{q_state};
- }
- my $params = {
- %$prob_where,
- 'me.confirmed' => { '>=', $dtf->format_datetime( $now->clone->subtract( days => 30 ) ) },
- };
+ $where{'me.confirmed'} = { '>=', $dtf->format_datetime($date) };
- if ( $c->get_param('start_date') or $c->get_param('end_date') ) {
+ my $start_date = $c->stash->{start_date};
+ my $end_date = $c->stash->{end_date};
+ if ($start_date or $end_date) {
my @parts;
- if ($c->get_param('start_date')) {
- my $date = $dtf->parse_datetime( $c->get_param('start_date') );
+ if ($start_date) {
+ my $date = $dtf->parse_datetime($start_date);
push @parts, { '>=', $dtf->format_datetime( $date ) };
- $c->stash->{start_date} = $c->get_param('start_date');
}
- if ($c->get_param('end_date')) {
+ if ($end_date) {
my $one_day = DateTime::Duration->new( days => 1 );
- my $date = $dtf->parse_datetime( $c->get_param('end_date') );
+ my $date = $dtf->parse_datetime($end_date);
push @parts, { '<', $dtf->format_datetime( $date + $one_day ) };
- $c->stash->{end_date} = $c->get_param('end_date');
}
if (scalar @parts == 2) {
- $params->{'me.confirmed'} = [ -and => $parts[0], $parts[1] ];
+ $where{'me.confirmed'} = [ -and => $parts[0], $parts[1] ];
} else {
- $params->{'me.confirmed'} = $parts[0];
+ $where{'me.confirmed'} = $parts[0];
}
}
- my $problems_rs = $c->cobrand->problems->to_body($body)->search( $params );
- my @problems = $problems_rs->all;
+ $c->stash->{params} = \%where;
+ $c->stash->{problems_rs} = $c->cobrand->problems->to_body($c->stash->{body})->search( \%where );
+}
- my %problems;
- foreach (@problems) {
- if ($_->confirmed >= $now->clone->subtract(days => 7)) {
- push @{$problems{1}}, $_;
- } elsif ($_->confirmed >= $now->clone->subtract(days => 14)) {
- push @{$problems{2}}, $_;
- } else {
- push @{$problems{3}}, $_;
+sub generate_data {
+ my ($self, $c) = @_;
+
+ my $state_map = $c->stash->{state_map} = {};
+ $state_map->{$_} = 'open' foreach FixMyStreet::DB::Result::Problem->open_states;
+ $state_map->{$_} = 'closed' foreach FixMyStreet::DB::Result::Problem->closed_states;
+ $state_map->{$_} = 'fixed' foreach FixMyStreet::DB::Result::Problem->fixed_states;
+
+ $self->generate_grouped_data($c);
+ $self->generate_summary_figures($c);
+}
+
+sub generate_grouped_data {
+ my ($self, $c) = @_;
+ my $state_map = $c->stash->{state_map};
+
+ my $group_by = $c->get_param('group_by') || '';
+ my (%grouped, @groups, %totals);
+ if ($group_by eq 'category') {
+ %grouped = map { $_->category => {} } @{$c->stash->{contacts}};
+ @groups = qw/category/;
+ } elsif ($group_by eq 'state') {
+ @groups = qw/state/;
+ } elsif ($group_by eq 'month') {
+ @groups = (
+ { extract => \"month from confirmed", -as => 'c_month' },
+ { extract => \"year from confirmed", -as => 'c_year' },
+ );
+ } elsif ($group_by eq 'device+site') {
+ @groups = qw/cobrand service/;
+ } else {
+ $group_by = 'category+state';
+ @groups = qw/category state/;
+ %grouped = map { $_->category => {} } @{$c->stash->{contacts}};
+ }
+ my $problems = $c->stash->{problems_rs}->search(undef, {
+ group_by => [ map { ref $_ ? $_->{-as} : $_ } @groups ],
+ select => [ @groups, { count => 'me.id' } ],
+ as => [ @groups == 2 ? qw/key1 key2 count/ : qw/key1 count/ ],
+ } );
+ $c->stash->{group_by} = $group_by;
+
+ my %columns;
+ while (my $p = $problems->next) {
+ my %cols = $p->get_columns;
+ my ($col1, $col2) = ($cols{key1}, $cols{key2});
+ if ($group_by eq 'category+state') {
+ $col2 = $state_map->{$cols{key2}};
+ } elsif ($group_by eq 'month') {
+ $col1 = Time::Piece->strptime("2017-$cols{key1}-01", '%Y-%m-%d')->fullmonth;
}
+ $grouped{$col1}->{$col2} += $cols{count} if defined $col2;
+ $grouped{$col1}->{total} += $cols{count};
+ $totals{$col2} += $cols{count} if defined $col2;
+ $totals{total} += $cols{count};
+ $columns{$col2} = 1 if defined $col2;
}
- $c->stash->{lists} = \%problems;
- if ( $c->get_param('export') ) {
- $self->export_as_csv($c, $problems_rs, $body);
+ my @columns = keys %columns;
+ my @rows = keys %grouped;
+ if ($group_by eq 'month') {
+ my %months;
+ my @months = qw/January February March April May June
+ July August September October November December/;
+ @months{@months} = (0..11);
+ @rows = sort { $months{$a} <=> $months{$b} } @rows;
+ } elsif ($group_by eq 'state') {
+ my $state_map = $c->stash->{state_map};
+ my %map = (confirmed => 0, open => 1, fixed => 2, closed => 3);
+ @rows = sort {
+ my $am = $map{$a} // $map{$state_map->{$a}};
+ my $bm = $map{$b} // $map{$state_map->{$b}};
+ $am <=> $bm;
+ } @rows;
+ } else {
+ @rows = sort @rows;
+ }
+ $c->stash->{rows} = \@rows;
+ $c->stash->{columns} = \@columns;
+
+ $c->stash->{grouped} = \%grouped;
+ $c->stash->{totals} = \%totals;
+}
+
+sub generate_summary_figures {
+ my ($self, $c) = @_;
+ my $state_map = $c->stash->{state_map};
+
+ # problems this month by state
+ $c->stash->{"summary_$_"} = 0 for values %$state_map;
+
+ $c->stash->{summary_open} = $c->stash->{problems_rs}->count;
+
+ my $params = $c->stash->{params};
+ $params = { map { my $n = $_; s/me\./problem\./ unless /me\.confirmed/; $_ => $params->{$n} } keys %$params };
+
+ my $comments = $c->model('DB::Comment')->to_body(
+ $c->stash->{body}
+ )->search(
+ {
+ %$params,
+ 'me.id' => { 'in' => \"(select min(id) from comment where me.problem_id=comment.problem_id and problem_state not in ('', 'confirmed') group by problem_state)" },
+ },
+ {
+ join => 'problem',
+ group_by => [ 'problem_state' ],
+ select => [ 'problem_state', { count => 'me.id' } ],
+ as => [ qw/problem_state count/ ],
+ }
+ );
+
+ while (my $comment = $comments->next) {
+ my $meta_state = $state_map->{$comment->problem_state};
+ next if $meta_state eq 'open';
+ $c->stash->{"summary_$meta_state"} += $comment->get_column('count');
}
}
sub export_as_csv {
- my ($self, $c, $problems_rs, $body) = @_;
+ my ($self, $c) = @_;
require Text::CSV;
- my $problems = $problems_rs->search(
+ my $problems = $c->stash->{problems_rs}->search(
{}, { prefetch => 'comments', order_by => 'me.confirmed' });
my $filename = do {
my %where = (
- body => $body->id,
category => $c->stash->{category},
state => $c->stash->{q_state},
ward => $c->stash->{ward},
);
+ $where{body} = $c->stash->{body}->id if $c->stash->{body};
join '-',
$c->req->uri->host,
map {
@@ -304,88 +386,13 @@ sub export_as_csv {
$c->res->body( join "", @body );
}
-sub updates_search : Private {
- my ( $self, $c, $time ) = @_;
-
- my $body = $c->stash->{body};
-
- my $params = {
- %{$c->stash->{where}},
- 'me.confirmed' => { '>=', $time },
- };
-
- my $comments = $c->model('DB::Comment')->to_body($body)->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', 'action scheduled', 'planned');
-
- $counts{'action scheduled'} += $counts{planned} || 0;
-
- for my $vars (
- [ 'time_to_fix', 'fixed - council' ],
- [ 'time_to_mark', 'in progress', 'action scheduled', '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')->to_body($body)->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')->to_body($body)->search(
- { %$params, mark_fixed => 1, problem_state => undef }, { join => 'problem' }
- )->count;
-
- $params = {
- %{$c->stash->{prob_where}},
- 'me.confirmed' => { '>=', $time },
- };
- $counts{total} = $c->cobrand->problems->to_body($body)->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->to_body($body)->search( $params )->count;
-
- return \%counts;
-}
-
=head1 AUTHOR
Matthew Somerville
=head1 LICENSE
-Copyright (c) 2012 UK Citizens Online Democracy. All rights reserved.
+Copyright (c) 2017 UK Citizens Online Democracy. All rights reserved.
Licensed under the Affero GPL.
=cut
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index 82455f262..c33bda7f3 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -620,7 +620,6 @@ sub admin_pages {
'summary' => [_('Summary'), 0],
'timeline' => [_('Timeline'), 5],
'stats' => [_('Stats'), 8],
- 'areastats' => [_('Area Stats'), 14],
};
# There are some pages that only super users can see
diff --git a/t/app/controller/area_stats.t b/t/app/controller/area_stats.t
deleted file mode 100644
index ce2e3d7d6..000000000
--- a/t/app/controller/area_stats.t
+++ /dev/null
@@ -1,234 +0,0 @@
-use strict;
-use warnings;
-use Test::More;
-
-use FixMyStreet::TestMech;
-
-my $mech = FixMyStreet::TestMech->new;
-
-my $oxfordshire = $mech->create_body_ok(2237, 'Oxfordshire County Council');
-my $superuser = $mech->create_user_ok('superuser@example.com', name => 'Super User', is_superuser => 1);
-my $oxfordshireuser = $mech->create_user_ok('counciluser@example.com', name => 'Council User', from_body => $oxfordshire);
-
-$mech->create_contact_ok( body_id => $oxfordshire->id, category => 'Potholes', email => 'potholes@example.com' );
-$mech->create_contact_ok( body_id => $oxfordshire->id, category => 'Traffic lights', email => 'lights@example.com' );
-$mech->create_contact_ok( body_id => $oxfordshire->id, category => 'Litter', email => 'litter@example.com' );
-
-my $body_id = $oxfordshire->id;
-my $area_id = '20720';
-my $alt_area_id = '20721';
-
-$mech->create_problems_for_body(2, $oxfordshire->id, 'Title', { areas => ",$area_id,6753,2237,", created => \'current_timestamp', category => 'Potholes' });
-$mech->create_problems_for_body(3, $oxfordshire->id, 'Title', { areas => ",$area_id,6753,2237,", created => \'current_timestamp', category => 'Traffic lights' });
-$mech->create_problems_for_body(1, $oxfordshire->id, 'Title', { areas => ",$alt_area_id,6753,2237,", created => \'current_timestamp', category => 'Litter' });
-
-my @scheduled_problems = $mech->create_problems_for_body(7, $oxfordshire->id, 'Title', { areas => ",$area_id,6753,2237,", created => \'current_timestamp', category => 'Traffic lights' });
-my @fixed_problems = $mech->create_problems_for_body(4, $oxfordshire->id, 'Title', { areas => ",$area_id,6753,2237,", created => \'current_timestamp', category => 'Potholes' });
-my @closed_problems = $mech->create_problems_for_body(3, $oxfordshire->id, 'Title', { areas => ",$area_id,6753,2237,", created => \'current_timestamp', category => 'Traffic lights' });
-
-foreach my $problem (@scheduled_problems) {
- $problem->update({ state => 'planned' });
- $mech->create_comment_for_problem($problem, $oxfordshireuser, 'Title', 'text', 0, 'confirmed', 'planned', { confirmed => \'current_timestamp' });
-}
-
-foreach my $problem (@fixed_problems) {
- $problem->update({ state => 'fixed - council' });
- $mech->create_comment_for_problem($problem, $oxfordshireuser, 'Title', 'text', 0, 'confirmed', 'fixed', { confirmed => \'current_timestamp' });
-}
-
-foreach my $problem (@closed_problems) {
- $problem->update({ state => 'closed' });
- $mech->create_comment_for_problem($problem, $oxfordshireuser, 'Title', 'text', 0, 'confirmed', 'closed', { confirmed => \'current_timestamp' });
-}
-
-$mech->log_in_ok( $superuser->email );
-
-FixMyStreet::override_config {
- MAPIT_URL => 'http://mapit.uk/',
- ALLOWED_COBRANDS => [ 'oxfordshire' ],
-}, sub {
- subtest 'superuser gets areas listed' => sub {
- $mech->create_body_ok(1234, 'Some Other Council');
- $mech->get_ok('/admin/areastats');
- $mech->content_contains('Oxfordshire County Council', 'Oxfordshire is shown on the page');
- $mech->content_contains('Some Other Council', 'Some other council is shown on the page');
- };
-
- subtest 'body user sees whole body stats page' => sub {
- $mech->log_in_ok( $oxfordshireuser->email );
- $mech->get_ok('/admin/areastats');
- $mech->content_contains("Area stats for Oxfordshire County Council");
- $mech->content_contains('Trowbridge');
- $mech->content_contains('Bradford-on-Avon');
- };
-
- subtest 'area user can only see their area' => sub {
- $oxfordshireuser->update({area_id => 20720});
-
- $mech->get("/admin/areastats/$body_id");
- is $mech->status, 404, 'area user cannot see parent area';
-
- $mech->get("/admin/areastats/$body_id?area=20721");
- is $mech->status, 404, 'area user cannot see another area';
-
- $mech->get_ok('/admin/areastats');
- $mech->text_contains('Area 20720', 'index page displays their area to area user');
-
- $oxfordshireuser->update({area_id => undef});
- };
-
- subtest 'gets an area' => sub {
- $mech->log_in_ok( $oxfordshireuser->email );
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
- $mech->content_contains('Area 20720', 'Area name is shown on the page');
-
- $mech->get('/admin/areastats/999');
- is $mech->status, 404, 'Getting a non-existent body returns 404';
-
- $mech->get("/admin/areastats/$body_id/999");
- is $mech->status, 404, 'Getting a non-existent area returns 404';
- };
-
- subtest 'shows correct stats for ward' => sub {
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
- $mech->content_contains('19 opened, 7 scheduled, 3 closed, 4 fixed');
- $mech->text_contains('Potholes2004');
- $mech->text_contains('Traffic lights3730');
- $mech->text_contains('Litter0000');
-
- $mech->text_contains('Potholes6');
- $mech->text_contains('Traffic lights13');
-
- $mech->text_contains('average time between issue being opened and set to another status was 0 days');
- };
-
- subtest 'shows correct stats to area user' => sub {
- $oxfordshireuser->update({area_id => 20720});
-
- $mech->get_ok("/admin/areastats");
- $mech->content_contains('19 opened, 7 scheduled, 3 closed, 4 fixed');
- $mech->text_contains('Potholes2004');
- $mech->text_contains('Traffic lights3730');
- $mech->text_contains('Litter0000');
-
- $mech->text_contains('Potholes6');
- $mech->text_contains('Traffic lights13');
-
- $oxfordshireuser->update({area_id => undef});
- };
-
- subtest 'shows correct stats for ward using area param' => sub {
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
- $mech->content_contains('19 opened, 7 scheduled, 3 closed, 4 fixed');
- $mech->text_contains('Potholes2004');
- $mech->text_contains('Traffic lights3730');
- $mech->text_contains('Litter0000');
-
- $mech->text_contains('Potholes6');
- $mech->text_contains('Traffic lights13');
- };
-
- subtest 'shows correct stats for council' => sub {
- $mech->get_ok("/admin/areastats/$body_id");
- $mech->content_contains('20 opened, 7 scheduled, 3 closed, 4 fixed');
- $mech->text_contains('Potholes2004');
- $mech->text_contains('Traffic lights3730');
- $mech->text_contains('Litter1000');
-
- $mech->text_contains('Potholes6');
- $mech->text_contains('Traffic lights13');
- };
-
- subtest 'shows average correctly' => sub {
- $fixed_problems[0]->update({ confirmed => DateTime->now->subtract(days => 2) });
- $fixed_problems[1]->update({ confirmed => DateTime->now->subtract(days => 3) });
- $fixed_problems[2]->update({ confirmed => DateTime->now->subtract(days => 7) });
- $fixed_problems[3]->update({ confirmed => DateTime->now->subtract(days => 4) });
- $scheduled_problems[0]->update({ confirmed => DateTime->now->subtract(days => 2) });
- $scheduled_problems[1]->update({ confirmed => DateTime->now->subtract(days => 4) });
- $scheduled_problems[2]->update({ confirmed => DateTime->now->subtract(days => 6) });
- $scheduled_problems[3]->update({ confirmed => DateTime->now->subtract(days => 7) });
- $scheduled_problems[4]->update({ confirmed => DateTime->now->subtract(days => 1) });
- $scheduled_problems[6]->update({ confirmed => DateTime->now->subtract(days => 1) });
- $closed_problems[0]->update({ confirmed => DateTime->now->subtract(days => 6) });
- $closed_problems[1]->update({ confirmed => DateTime->now->subtract(days => 9) });
- $closed_problems[2]->update({ confirmed => DateTime->now->subtract(days => 12) });
-
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
- $mech->text_contains('average time between issue being opened and set to another status was 5 days');
- };
-
- subtest 'shows this month stats correctly' => sub {
- $fixed_problems[0]->update({ confirmed => DateTime->now->subtract(days => 50) });
- $fixed_problems[1]->update({ confirmed => DateTime->now->subtract(days => 50) });
- $scheduled_problems[1]->update({ confirmed => DateTime->now->subtract(days => 50) });
- $scheduled_problems[2]->update({ confirmed => DateTime->now->subtract(days => 50) });
-
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
-
- $mech->content_contains('15 opened, 7 scheduled, 3 closed, 4 fixed');
- $mech->text_contains('Potholes2004');
- $mech->text_contains('Traffic lights3730');
-
- $mech->text_contains('Potholes4');
- $mech->text_contains('Traffic lights11');
-
- $mech->text_contains('average time between issue being opened and set to another status was 5 days');
- };
-
- subtest 'ignores multiple comments with the same state' => sub {
- $mech->create_comment_for_problem($scheduled_problems[0], $oxfordshireuser, 'Title', 'text', 0, 'confirmed', 'planned', { confirmed => \'current_timestamp' });
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
-
- $mech->content_contains('15 opened, 7 scheduled, 3 closed, 4 fixed');
- $mech->text_contains('Potholes2004');
- $mech->text_contains('Traffic lights3730');
- };
-
- subtest 'ignores second state change if first was last month' => sub {
- my $comment = $scheduled_problems[0]->comments->search({}, { order_by => { '-asc' => 'id' } } )->first;
- $comment->update({ confirmed => DateTime->now->subtract(days => 40) });
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
-
- $mech->content_contains('15 opened, 6 scheduled, 3 closed, 4 fixed');
- $mech->text_contains('Potholes2004');
- $mech->text_contains('Traffic lights3730');
- $comment->update({ confirmed => DateTime->now });
- };
-
- subtest 'average is only to first state change' => sub {
- for my $i (0..4) {
- $scheduled_problems[$i]->comments->first->update({ confirmed => $scheduled_problems[$i]->confirmed });
- $mech->create_comment_for_problem($scheduled_problems[$i], $oxfordshireuser, 'Title', 'text', 0, 'confirmed', 'fixed', { confirmed => \'current_timestamp' });
- }
-
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
- $mech->text_contains('average time between issue being opened and set to another status was 4 days');
- };
-
- subtest 'average only includes actual state changes' => sub {
- my @probs = $mech->create_problems_for_body(2, $oxfordshire->id, 'Title',
- { areas => ",$area_id,6753,2237,", created => DateTime->now->subtract(days => 12), confirmed => DateTime->now->subtract(days => 12), category => 'Potholes' });
- $mech->create_comment_for_problem($probs[0], $oxfordshireuser, 'Title', 'text', 0, 'confirmed', 'confirmed', { confirmed => \'current_timestamp' });
-
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
- $mech->text_contains('average time between issue being opened and set to another status was 4 days');
- };
-
- subtest 'shows no problems changed state if no average' => sub {
- for my $p (@scheduled_problems, @fixed_problems, @closed_problems) {
- $p->comments->delete;
- }
-
- $mech->get_ok("/admin/areastats/$body_id?area=20720");
- $mech->text_contains('17 opened, 0 scheduled, 0 closed, 0 fixed');
- $mech->text_contains('no problems changed state');
- }
-};
-
-END {
- FixMyStreet::DB->resultset('UserPlannedReport')->delete_all;
- $mech->delete_user( $superuser );
- $mech->delete_user( $oxfordshireuser );
- done_testing();
-}
diff --git a/t/app/controller/dashboard.t b/t/app/controller/dashboard.t
index c1706cc81..7d0b0d217 100644
--- a/t/app/controller/dashboard.t
+++ b/t/app/controller/dashboard.t
@@ -1,600 +1,150 @@
use Test::MockTime ':all';
+use strict;
+use warnings;
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;
-
-my $body = $mech->create_body_ok($test_council, 'City of Edinburgh Council');
-
-$mech->delete_user( $test_user );
-my $user = $mech->create_user_ok($test_user, password => $test_pass);
-
-my $p_user = $mech->create_user_ok('p_user@example.com');
-
-# Dashboard tests assume we are not too early in year, to allow reporting
-# within same year, as a convenience.
-set_absolute_time('2014-03-01T12:00:00');
-FixMyStreet::override_config {
- ALLOWED_COBRANDS => [ { fixmystreet => '.' } ],
- MAPIT_URL => 'http://mapit.uk/',
-}, sub {
+set_absolute_time('2014-02-01T12:00:00');
- $mech->not_logged_in_ok;
- $mech->get_ok('/dashboard');
+my $mech = FixMyStreet::TestMech->new;
- $mech->content_contains( 'sign in' );
+my $other_body = $mech->create_body_ok(1234, 'Some Other Council');
+my $body = $mech->create_body_ok(2651, 'City of Edinburgh Council');
+my @cats = ('Litter', 'Other', 'Potholes', 'Traffic lights');
+for my $contact ( @cats ) {
+ $mech->create_contact_ok(body_id => $body->id, category => $contact, email => "$contact\@example.org");
+}
- $mech->submit_form(
- with_fields => { username => $test_user, password_sign_in => $test_pass }
- );
+my $superuser = $mech->create_user_ok('superuser@example.com', name => 'Super User', is_superuser => 1);
+my $counciluser = $mech->create_user_ok('counciluser@example.com', name => 'Council User', from_body => $body);
+my $normaluser = $mech->create_user_ok('normaluser@example.com', name => 'Normal User');
- is $mech->status, '404', 'If not council user get 404';
+my $body_id = $body->id;
+my $area_id = '60705';
+my $alt_area_id = '62883';
- $user->from_body( $body->id );
- $user->update;
+my $last_month = DateTime->now->subtract(months => 2);
+$mech->create_problems_for_body(2, $body->id, 'Title', { areas => ",$area_id,2651,", category => 'Potholes', cobrand => 'fixmystreet' });
+$mech->create_problems_for_body(3, $body->id, 'Title', { areas => ",$area_id,2651,", category => 'Traffic lights', cobrand => 'fixmystreet', dt => $last_month });
+$mech->create_problems_for_body(1, $body->id, 'Title', { areas => ",$alt_area_id,2651,", category => 'Litter', cobrand => 'fixmystreet' });
- $mech->log_out_ok;
- $mech->get_ok('/dashboard');
- $mech->submit_form_ok( {
- with_fields => { username => $test_user, password_sign_in => $test_pass }
- } );
+my @scheduled_problems = $mech->create_problems_for_body(7, $body->id, 'Title', { areas => ",$area_id,2651,", category => 'Traffic lights', cobrand => 'fixmystreet' });
+my @fixed_problems = $mech->create_problems_for_body(4, $body->id, 'Title', { areas => ",$area_id,2651,", category => 'Potholes', cobrand => 'fixmystreet' });
+my @closed_problems = $mech->create_problems_for_body(3, $body->id, 'Title', { areas => ",$area_id,2651,", category => 'Traffic lights', cobrand => 'fixmystreet' });
- $mech->content_contains( 'Area 2651' );
+foreach my $problem (@scheduled_problems) {
+ $problem->update({ state => 'action scheduled' });
+ $mech->create_comment_for_problem($problem, $counciluser, 'Title', 'text', 0, 'confirmed', 'action scheduled');
+}
- FixMyStreet::App->model('DB::Contact')->search( { body_id => $body->id } )
- ->delete;
+foreach my $problem (@fixed_problems) {
+ $problem->update({ state => 'fixed - council' });
+ $mech->create_comment_for_problem($problem, $counciluser, 'Title', 'text', 0, 'confirmed', 'fixed');
+}
- delete_problems();
+foreach my $problem (@closed_problems) {
+ $problem->update({ state => 'closed' });
+ $mech->create_comment_for_problem($problem, $counciluser, 'Title', 'text', 0, 'confirmed', 'closed', { confirmed => \'current_timestamp' });
+}
- my @cats = qw( Grafitti Litter Potholes Other );
- for my $contact ( @cats ) {
- FixMyStreet::App->model('DB::Contact')->create(
- {
- body_id => $body->id,
- category => $contact,
- email => "$contact\@example.org",
- state => 'confirmed',
- whenedited => DateTime->now,
- editor => 'test',
- note => 'test',
- }
- );
- }
+my $categories = scraper {
+ process "select[name=category] > option", 'cats[]' => 'TEXT',
+ process "table[id=overview] > tr", 'rows[]' => scraper {
+ process 'td', 'cols[]' => 'TEXT'
+ },
+};
- $mech->get_ok('/dashboard');
+FixMyStreet::override_config {
+ ALLOWED_COBRANDS => [ { fixmystreet => '.' } ],
+ MAPIT_URL => 'http://mapit.uk/',
+}, sub {
- 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=action_scheduled] > td", 'action_scheduled[]' => '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'
- },
+ subtest 'not logged in, redirected to login' => sub {
+ $mech->not_logged_in_ok;
+ $mech->get_ok('/dashboard');
+ $mech->content_contains( 'sign in' );
};
- my $expected_cats = [ 'All', '-- Pick a category --', @cats ];
- 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';
- }
-
- my $now = DateTime->now(time_zone => 'local');
- foreach my $test (
- {
- desc => 'confirmed today with no state',
- dt => $now,
- counts => [1,1,1,1],
- report_counts => [1, 0, 0],
- },
- {
- desc => 'confirmed last 7 days with no state',
- dt => $now->clone->subtract( days => 6, hours => 23 ),
- counts => [1,2,2,2],
- report_counts => [2, 0, 0],
- },
- {
- desc => 'confirmed last 8 days with no state',
- dt => $now->clone->subtract( days => 8 ),
- counts => [1,2,3,3],
- report_counts => [2, 1, 0],
- },
- {
- desc => 'confirmed last 2 weeks with no state',
- dt => $now->clone->subtract( weeks => 2, hours => 1 ),
- counts => [1,2,4,4],
- report_counts => [2, 1, 1],
- },
- {
- desc => 'confirmed this year with no state',
- dt => $now->clone->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();
-
- my $is_monday = DateTime->now->day_of_week == 1 ? 1 : 0;
-
- foreach my $test (
- {
- desc => 'user fixed today',
- confirm_dt => DateTime->now->subtract( days => 1 ),
- mark_dt => DateTime->now,
- state => 'fixed - user',
- counts => {
- totals => $is_monday ? [0,1,1,1] : [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 => $is_monday ? [0,2,2,2] : [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 => $is_monday ? [0,3,3,3] : [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 => $is_monday ? [0,4,4,4] : [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 action scheduled today',
- confirm_dt => DateTime->now->subtract( days => 1 ),
- mark_dt => DateTime->now,
- state => 'action scheduled',
- counts => {
- totals => $is_monday ? [ 0,5,5,5] : [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],
- action_scheduled => [1,1,1,1],
- marked => [3,3,3,3]
- }
- },
- {
- desc => 'marked as action scheduled today, confirmed a week ago',
- confirm_dt => DateTime->now->subtract( days => 8 ),
- mark_dt => DateTime->now,
- state => 'action scheduled',
- counts => {
- totals => $is_monday ? [0,5,6,6] : [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],
- action_scheduled => [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 => $is_monday ? [0,5,7,7] : [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],
- action_scheduled => [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 => $is_monday ? [0,5,8,8] : [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],
- action_scheduled => [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 => $is_monday ? [0,5,8,9] : [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],
- action_scheduled => [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 => $is_monday ? [0,6,9,10] : [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],
- action_scheduled => [2,2,2,2],
- closed => [1,1,1,1],
- marked => [5,5,5,5]
- }
- },
- {
- desc => 'marked as planned',
- confirm_dt => DateTime->now->subtract( days => 1 ),
- mark_dt => DateTime->now,
- state => 'planned',
- counts => {
- totals => $is_monday ? [0,7,10,11] : [7,7,10,11],
- 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],
- action_scheduled => [3,3,3,3],
- closed => [1,1,1,1],
- marked => [6,6,6,6]
- }
- },
- ) {
- 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} );
- };
- }
+ subtest 'normal user, 404' => sub {
+ $mech->log_in_ok( $normaluser->email );
+ $mech->get('/dashboard');
+ is $mech->status, '404', 'If not council user get 404';
+ };
- delete_problems();
+ subtest 'superuser, body list' => sub {
+ $mech->log_in_ok( $superuser->email );
+ $mech->get_ok('/dashboard');
+ # Contains body name, in list of bodies
+ $mech->content_contains('Some Other Council');
+ $mech->content_contains('Edinburgh Council');
+ $mech->content_lacks('Category:');
+ $mech->get_ok('/dashboard?body=' . $body->id);
+ $mech->content_lacks('Some Other Council');
+ $mech->content_contains('Edinburgh Council');
+ $mech->content_contains('Trowbridge');
+ $mech->content_contains('Category:');
+ };
- for my $test (
- {
- desc => 'Selecting no state does nothing',
- p1 => {
- state => 'fixed - user',
- conf_dt => DateTime->now()->subtract( minutes => 1 ),
- category => 'Potholes',
- },
- p2 => {
- state => 'confirmed',
- conf_dt => DateTime->now()->subtract( minutes => 1 ),
- category => 'Litter',
- },
- state => '',
- report_counts => [2,0,0],
- report_counts_after => [2,0,0],
- },
- {
- desc => 'limit by state works',
- state => 'fixed - council',
- 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()->subtract( minutes => 1 ),
- category => 'Potholes',
- },
- p2 => {
- state => 'fixed',
- conf_dt => DateTime->now()->subtract( minutes => 1 ),
- 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};
+ subtest 'council user, ward list' => sub {
+ $mech->log_in_ok( $counciluser->email );
+ $mech->get_ok('/dashboard');
+ $mech->content_lacks('Some Other Council');
+ $mech->content_contains('Edinburgh Council');
+ $mech->content_contains('Trowbridge');
+ $mech->content_contains('Category:');
+ };
- $mech->get_ok('/dashboard');
+ subtest 'area user can only see their area' => sub {
+ $counciluser->update({area_id => $area_id});
- $res = $categories->scrape( $mech->content );
+ $mech->get_ok("/dashboard");
+ $mech->content_contains('<h1>Trowbridge</h1>');
+ $mech->get_ok("/dashboard?body=" . $other_body->id);
+ $mech->content_contains('<h1>Trowbridge</h1>');
+ $mech->get_ok("/dashboard?ward=$alt_area_id");
+ $mech->content_contains('<h1>Trowbridge</h1>');
- check_report_counts( $res, $test->{report_counts} );
+ $counciluser->update({area_id => undef});
+ };
- $mech->submit_form_ok( {
- with_fields => {
- state => $test->{state},
- }
- } );
+ subtest 'The correct categories and totals shown by default' => sub {
+ $mech->get_ok("/dashboard");
+ my $expected_cats = [ 'All', @cats ];
+ my $res = $categories->scrape( $mech->content );
+ is_deeply( $res->{cats}, $expected_cats, 'correct list of categories' );
+ # Three missing as more than a month ago
+ test_table($mech->content, 1, 0, 0, 1, 0, 0, 0, 0, 2, 0, 4, 6, 7, 3, 0, 10, 10, 3, 4, 17);
+ };
- $res = $categories->scrape( $mech->content );
+ subtest 'test filters' => sub {
+ $mech->get_ok("/dashboard");
+ $mech->submit_form_ok({ with_fields => { category => 'Litter' } });
+ test_table($mech->content, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1);
+ $mech->submit_form_ok({ with_fields => { category => '', state => 'fixed - council' } });
+ test_table($mech->content, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 0, 0, 0, 0, 0, 4, 4);
+ $mech->submit_form_ok({ with_fields => { state => 'action scheduled' } });
+ test_table($mech->content, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 7, 7, 0, 0, 7);
+ my $start = DateTime->now->subtract(months => 3)->strftime('%Y-%m-%d');
+ my $end = DateTime->now->subtract(months => 1)->strftime('%Y-%m-%d');
+ $mech->submit_form_ok({ with_fields => { state => '', start_date => $start, end_date => $end } });
+ test_table($mech->content, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 3, 3, 0, 0, 3);
+ };
- check_report_counts( $res, $test->{report_counts_after} );
- };
- }
+ subtest 'test grouping' => sub {
+ $mech->get_ok("/dashboard?group_by=category");
+ test_table($mech->content, 1, 0, 6, 10, 17);
+ $mech->get_ok("/dashboard?group_by=state");
+ test_table($mech->content, 3, 7, 4, 3, 17);
+ $mech->get_ok("/dashboard?start_date=2000-01-01&group_by=month");
+ test_table($mech->content, 0, 17, 17, 3, 0, 3, 3, 17, 20);
+ };
subtest 'export as csv' => sub {
- make_problem( {
+ $mech->create_problems_for_body(1, $body->id, 'Title', {
detail => "this report\nis split across\nseveral lines",
- state => "confirmed",
- conf_dt => DateTime->now(),
- areas => 62883,
- } );
+ areas => ",$alt_area_id,2651,",
+ });
$mech->get_ok('/dashboard?export=1');
open my $data_handle, '<', \$mech->content;
my $csv = Text::CSV->new( { binary => 1 } );
@@ -602,7 +152,7 @@ FixMyStreet::override_config {
while ( my $row = $csv->getline( $data_handle ) ) {
push @rows, $row;
}
- is scalar @rows, 6, '1 (header) + 5 (reports) = 6 lines';
+ is scalar @rows, 19, '1 (header) + 18 (reports) = 19 lines';
is scalar @{$rows[0]}, 18, '18 columns present';
@@ -629,123 +179,25 @@ FixMyStreet::override_config {
],
'Column headers look correct';
- is $rows[5]->[14], 'Bradford-on-Avon', 'Ward column is name not ID';
-
- is $rows[5]->[15], '610591', 'Correct Easting conversion';
- is $rows[5]->[16], '126573', 'Correct Northing conversion';
+ is $rows[5]->[14], 'Trowbridge', 'Ward column is name not ID';
+ is $rows[5]->[15], '529025', 'Correct Easting conversion';
+ is $rows[5]->[16], '179716', 'Correct Northing conversion';
};
- delete_problems();
- subtest 'check date restriction' => sub {
- make_problem({ state => 'confirmed', conf_dt => DateTime->now->subtract( 'days' => 1 ) });
- make_problem({ state => 'confirmed', conf_dt => DateTime->now->subtract( 'days' => 50 ) });
- make_problem({ state => 'confirmed', conf_dt => DateTime->now->subtract( 'days' => 31 ) });
-
- $mech->get_ok('/dashboard?export=1');
- open my $data_handle, '<', \$mech->content;
- my $csv = Text::CSV->new( { binary => 1 } );
- my @rows;
- while ( my $row = $csv->getline( $data_handle ) ) {
- push @rows, $row;
- }
-
- is scalar @rows, 2, '1 (header) + 1 (reports) = 2 lines';
-
- $mech->get_ok('/dashboard?export=1&start_date=' . DateTime->now->subtract('days' => 32)->ymd);
-
- open $data_handle, '<', \$mech->content;
- $csv = Text::CSV->new( { binary => 1 } );
- @rows = ();
- while ( my $row = $csv->getline( $data_handle ) ) {
- push @rows, $row;
- }
-
- is scalar @rows, 3, '1 (header) + 2 (reports) = 3 lines';
-
- $mech->get_ok('/dashboard?export=1&start_date=' . DateTime->now->subtract('days' => 51)->ymd . '&end_date=' . DateTime->now->subtract('days' => 49)->ymd );
-
- open $data_handle, '<', \$mech->content;
- $csv = Text::CSV->new( { binary => 1 } );
- @rows = ();
- while ( my $row = $csv->getline( $data_handle ) ) {
- push @rows, $row;
- }
-
- is scalar @rows, 2, '1 (header) + 1 (reports) = 2 lines';
- };
};
-restore_time;
-
-sub make_problem {
- my $args = shift;
-
- my $p = FixMyStreet::App->model('DB::Problem')->create( {
- title => 'a problem',
- name => 'a user',
- anonymous => 1,
- detail => $args->{detail} || 'some detail',
- state => $args->{state},
- confirmed => $args->{conf_dt},
- whensent => $args->{conf_dt},
- lastupdate => $args->{mark_dt} || $args->{conf_dt},
- bodies_str => $body->id,
- 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 test_table {
+ my ($content, @expected) = @_;
+ my $res = $categories->scrape( $mech->content );
+ my $i = 0;
+ foreach my $row ( @{ $res->{rows} }[1 .. 11] ) {
+ foreach my $col ( @{ $row->{cols} } ) {
+ is $col, $expected[$i++];
}
}
}
-sub delete_problems {
- FixMyStreet::App->model('DB::Comment')
- ->search( { 'problem.bodies_str' => $body->id }, { join => 'problem' } )
- ->delete;
- FixMyStreet::App->model('DB::Problem')
- ->search( { bodies_str => $body->id } )->delete();
+END {
+ restore_time;
+ done_testing();
}
-
-done_testing;
diff --git a/templates/web/base/admin/areastats/area.html b/templates/web/base/admin/areastats/area.html
deleted file mode 100644
index a7330692d..000000000
--- a/templates/web/base/admin/areastats/area.html
+++ /dev/null
@@ -1,72 +0,0 @@
-[% INCLUDE 'admin/header.html' title=tprintf(('Area stats for %s'), area_name) -%]
-<p>
-[% loc('There are currently:') %]
-</p>
-
-<table>
- <tr>
- <th></th>
- <th>[% loc('Open') %]</th>
- <th>[% loc('Scheduled') %]</th>
- <th>[% loc('Closed') %]</th>
- <th>[% loc('Fixed') %]</th>
- </tr>
- [% FOR k IN by_category.keys.sort %]
- <tr>
- <td>[% k %]</td>
- <td>[% by_category.$k.open OR 0 %]</td>
- <td>[% by_category.$k.scheduled OR 0 %]</td>
- <td>[% by_category.$k.closed OR 0 %]</td>
- <td>[% by_category.$k.fixed OR 0 %]</td>
- </tr>
- [% END %]
-
-</table>
-
-<p>
-[% loc('Issues in the last month:') %]
-</p>
-
-<p>
-[% tprintf(
- loc('%d opened, %d scheduled, %d closed, %d fixed'),
- open,
- scheduled,
- closed,
- fixed
- );
-%]
-</p>
-
-<table>
- [% FOR k IN recent_by_category.keys.sort %]
- <tr>
- <td>[% k %]</td>
- <td>[% recent_by_category.$k OR 0 %]</td>
- </tr>
- [% END %]
-</table>
-
-[% IF average >= 0 %]
-<p>[% tprintf(loc('In the last month – average time between issue being opened and set to another status was %s days'), average) %]</p>
-[% ELSE %]
-<p>[% loc('In the last month no problems changed state') %]</p>
-[% END %]
-
-
-[% IF NOT c.user.area_id %]
-<p>
-<form action="" method="GET">
-[% loc('Show stats for:') %]
- <select name="area">
- <option value="">[% loc('Whole council') %]</option>
- [% FOR area IN areas.values.sort('name')%]
- <option value="[% area.id %]">[% area.name %]</option>
- [% END %]
- </select>
- <input type="submit" value="Go">
-</form>
-</p>
-[% END %]
-
-[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/admin/areastats/index.html b/templates/web/base/admin/areastats/index.html
deleted file mode 100644
index 1ebde20e7..000000000
--- a/templates/web/base/admin/areastats/index.html
+++ /dev/null
@@ -1,11 +0,0 @@
-[% INCLUDE 'admin/header.html' title=('Area Stats') -%]
-
-<ul>
- [% FOR body IN bodies %]
- <li>
- <a href="[% c.uri_for('', body.id) %]">[% body.name %]</a>
- </li>
- [% END %]
-</ul>
-
-[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/admin/stats/index.html b/templates/web/base/admin/stats/index.html
index 248bea077..6ea1ae403 100644
--- a/templates/web/base/admin/stats/index.html
+++ b/templates/web/base/admin/stats/index.html
@@ -1,106 +1,10 @@
[% INCLUDE 'admin/header.html' title=loc('Stats') %]
-[% IF show_count %]
-<p>
-<strong>[% tprintf( unconfirmed ? loc( 'All reports between %s and %s' ) : loc( 'Confirmed reports between %s and %s' ), start_date.ymd, end_date.ymd ) | html %]</strong>
-</p>
-[% IF bymonth %]
-<table>
- <thead>
- <td style="width: 8em"><strong>[% loc('Year') %]</strong></td>
- <td style="width: 8em"><strong>[% loc('Month') %]</strong></td>
- <td><strong>[% loc('Count') %]</strong></td>
- </thead>
- [% total = 0 %]
- [% WHILE ( state = states.next ) %]
- [% total = total + state.get_column( 'count' ) %]
- <tr>
- <td>[% state.get_column( 'c_year') | html %]</td>
- <td>[% state.get_column( 'c_month') | html %]</td>
- <td>[% state.get_column( 'count' ) %]</td>
- </tr>
- [% END %]
- <tr>
- <td colspan="2"><strong>[% loc( 'Total' ) %]</strong></td>
- <td><strong>[% total %]</strong></td>
- </tr>
-</table>
-[% ELSE %]
-<table>
- <thead>
- <td style="width: 8em"><strong>[% loc('Current state') %]</strong></td>
- <td><strong>[% loc('Count') %]</strong></td>
- </thead>
- [% total = 0 %]
- [% WHILE ( state = states.next ) %]
- [% total = total + state.get_column( 'count' ) %]
- <tr>
- <td>[% state.state | html %]</td>
- <td>[% state.get_column( 'count' ) %]</td>
- </tr>
- [% END %]
- <tr>
- <td><strong>[% loc( 'Total' ) %]</strong></td>
- <td><strong>[% total %]</strong></td>
- </tr>
-</table>
-[% END %]
-
-[% IF unconfirmed %]
- <p>
- <small>[% loc( 'Note that when including unconfirmed reports we use the date the report was created which may not be in the same month the report was confirmed so the numbers may jump about a little' ) %]</small>
- </p>
-[% END %]
-[% ELSE %]
<ul>
<li><a href="[% c.uri_for_action('admin/stats/questionnaire') %]">[% loc('Survey Results') %]</a></li>
<li><a href="[% c.uri_for_action('admin/stats/state') %]">[% loc('Problem breakdown by state') %]</a></li>
<li><a href="[% c.uri_for_action('admin/stats/fix_rate') %]">[% loc('Category fix rate for problems > 4 weeks old') %]</a></li>
+<li><a href="[% c.uri_for_action('dashboard/index') %]">[% loc('Summary statistics') %]</a></li>
</ul>
-[% END %]
-
-[% IF errors %]
- [% FOREACH error IN errors %]
- <p class="error">[% error %]</p>
- [% END %]
-[% END %]
-
-<form method="post" action="[% c.uri_for_action('admin/stats/index') %]" enctype="application/x-www-form-urlencoded" accept-charset="utf-8">
- <p>
- <label for="start_date">[% loc('Start Date:') %]</label><input type="text" class="form-control"
- placeholder="[% loc('Click here or enter as dd/mm/yyyy') %]" name="start_date" id="start_date"
- value="[% start_date ? start_date.strftime( '%d/%m/%Y') : '' | html %]" />
- </p>
-
- <p>
- <label for="end_date">[% loc('End Date:') %]</label><input type="text" class="form-control"
- placeholder="[% loc('Click here or enter as dd/mm/yyyy') %]" name="end_date" id="end_date" size="5"
- value="[% end_date ? end_date.strftime( '%d/%m/%Y') : '' | html %]" />
- </p>
-
- <p>
- <input type="checkbox" name="unconfirmed" id="unconfirmed"[% unconfirmed ? ' checked' : '' %] /><label class="inline" for="unconfirmed">[% loc('Include unconfirmed reports') %]</label>
- </p>
-
- <p>
- <input type="checkbox" name="bymonth" id="bymonth"[% bymonth ? ' checked' : '' %] /><label class="inline" for="bymonth">[% loc('By Date') %]</label>
- </p>
-
- [% IF c.user.is_superuser %]
- <p>
- [% loc('Council:') %] <select class="form-control" id='body' name='body'>
- <option value=''>[% loc('No council') %]</option>
- [% FOR body IN bodies %]
- <option value="[% body.id %]"[% ' selected' IF body.id == selected_body %]>[% body.name %]</option>
- [% END %]
- </select>
- </p>
- [% END %]
-
- <p>
- <input type="submit" class="btn" name="getcounts" size="30" id="getcounts" value="[% loc('Get Count') %]" />
- </p>
-</form>
-
[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/dashboard/index.html b/templates/web/base/dashboard/index.html
index d7ad6ddfa..b201ebcc9 100644
--- a/templates/web/base/dashboard/index.html
+++ b/templates/web/base/dashboard/index.html
@@ -1,3 +1,5 @@
+[% USE Number.Format %]
+
[% extra_css = BLOCK %]
<link rel="stylesheet" href="[% version('/cobrands/fixmystreet/dashboard.css') %]">
[% END %]
@@ -11,11 +13,17 @@
<form>
+[% IF body %]
<hgroup>
- [% tprintf(loc('<h2>Reports, Statistics and Actions for</h2> <h1>%s</h1>'), council.name) %]
+ [% tprintf(loc('<h2>Reports, Statistics and Actions for</h2> <h1>%s</h1>'), body_name) %]
</hgroup>
+[% ELSE %]
+<h1>[% loc('Summary statistics') %]</h1>
+[% END %]
<div class="filters">
+ [% IF body %]
+ [% IF NOT c.user.area_id %]
<p>
<label for="ward">[% loc('Ward:') %]</label>
<select class="form-control" name="ward"><option value=''>[% loc('All') %]</option>
@@ -24,114 +32,30 @@
[% END %]
</select>
</p>
+ [% END %]
<p>
<label for="category">[% loc('Category:') %]</label>
<select class="form-control" name="category"><option value=''>[% loc('All') %]</option>
- [% FOR cat_op IN category_options %]
- <option value='[% cat_op.name | html %]'[% ' selected' IF category == cat_op.name %]>[% cat_op.value | html %]</option>
+ [% FOR cat IN contacts %]
+ <option value='[% cat.category | html %]'[% ' selected' IF category == cat.category %]>[% cat.category_display | html %]</option>
[% END %]
</select>
</p>
- <p class="no-label">
- <input type="submit" class="btn" value="[% loc('Look up') %]">
- </p>
-</div>
-
-
-<table width="100%" id="overview">
- <tr>
- <th>&nbsp;</th>
- <th scope="col"><abbr title="[% loc('Week To Date') %]">[% loc('WTD', "Week to date") %]</abbr></th>
- <th scope="col">[% loc('Last 7 days') %]</th>
- <th scope="col">[% loc('Last 4 weeks') %]</th>
- <th scope="col">[% loc('YTD', "Year to date") %]</th>
- </tr>
-
- [%
- rows = {
- '0' => [ "total", loc("Total reports received") ]
- '1' => [ "fixed - council", loc("Council has marked as fixed") ]
- '2' => [ "fixed_user", loc("User has marked as fixed") ]
- };
- FOR row IN rows %]
- <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>
- <td>[% problems.weeks.${row.value.0} %]</td>
- <td>[% problems.ytd.${row.value.0} %]</td>
- </tr>
- [% END %]
-
- <tr class='subtotal' id="total_fixed">
- <th scope="row">[% loc('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", loc("Council has marked as in progress") ]
- '1' => [ "action scheduled", loc("Council has marked as planned") ]
- '2' => [ "investigating", loc("Council has marked as investigating") ]
- '3' => [ "closed", loc("Council has marked as closed") ]
- };
- wtd = 0, week = 0, weeks = 0, ytd = 0;
- FOR row IN rows %]
- <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>
- <td>[% problems.weeks.${row.value.0} %]</td>
- <td>[% problems.ytd.${row.value.0} %]</td>
- </tr>
- [% END %]
-
- <tr class='subtotal' id="marked">
- <th scope="row">[% loc('Total marked') %]</th>
- <td>[% problems.wtd.${"in progress"} + problems.wtd.${"action scheduled"} +
- problems.wtd.investigating + problems.wtd.closed %]</td>
- <td>[% problems.week.${"in progress"} + problems.week.${"action scheduled"} +
- problems.week.investigating + problems.wtd.closed %]</td>
- <td>[% problems.weeks.${"in progress"} + problems.weeks.${"action scheduled"} +
- problems.weeks.investigating + problems.wtd.closed %]</td>
- <td>[% problems.ytd.${"in progress"} + problems.ytd.${"action scheduled"} +
- problems.ytd.investigating + problems.wtd.closed %]</td>
- </tr>
-
- <tr id="avg_fixed">
- <th scope="row">[% loc('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 id="avg_marked">
- <th scope="row">[% loc('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' id="not_marked">
- <th scope="row">[% loc('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>
+ [% ELSE %]
-</table>
+ <p>
+ <label for="ward">[% loc('Council:') %]</label>
+ <select class="form-control" name="body"><option value=''>[% loc('All') %]</option>
+ [% FOR b IN bodies %]
+ <option value="[% b.id %]">[% b.name %]</option>
+ [% END %]
+ </select>
+ </p>
-<h2>[% loc('Reports') %]</h2>
+ [% END %]
-<div class="filters">
<p>
<label for="state">[% loc('Report state:') %]</label>
<select class="form-control" name="state">
@@ -152,31 +76,94 @@
<label for="end_date">[% loc('End Date') %]</label>
<input name="end_date" type="date" value="[% end_date | html %]" class="form-control">
</p>
- <p class="no-label">
- <input type="submit" class="btn" value="[% loc('Look up') %]">
- <a class="btn export_as_csv" href="[% c.req.uri_with({ export => 1 }) %]">[% loc('Export as CSV') %]</a>
- </p>
</div>
-<table width="100%" id="reports">
+<p align="center">
+ <input type="hidden" name="group_by" value="[% group_by | html %]">
+ <input type="hidden" name="body" value="[% body.id | html %]">
+ <input type="submit" class="btn" value="[% loc('Look up') %]">
+ <input type="submit" class="btn" name="export" value="[% loc('Export as CSV') %]">
+</p>
+
+</form>
+
+[% BLOCK gb %]
+[% IF group_by == new_gb %]
+ <strong>[% text %]</strong>
+[% ELSE %]
+ <a href="[% c.uri_with({ group_by => new_gb }) %]">[% text %]</a>
+[% END %]
+[% END %]
+
+<table width="100%" id="overview">
+ <caption>
+ [% loc('Current state of filtered reports') %]
+ <p>
+ [% loc('Group by:') %]
+ [% INCLUDE gb new_gb='category' text=loc('Category') %]
+ | [% INCLUDE gb new_gb='state' text=loc('State') %]
+ | [% INCLUDE gb new_gb='month' text=loc('Month') %]
+ | [% INCLUDE gb new_gb='category+state' text=loc('Category and State') %]
+ | [% INCLUDE gb new_gb='device+site' text=loc('Device and Site') %]
+ </p>
+ </caption>
<tr>
- <th scope="col">[% loc('Less than 7 days old') %]</th>
- <th scope="col">[% loc('7-14 days old') %]</th>
- <th scope="col">[% loc('14-30 days old') %]</th>
+ <th></th>
+ [% IF group_by == 'category+state' %]
+ <th scope="col">[% loc('Open') %]</th>
+ <th scope="col">[% loc('Closed') %]</th>
+ <th scope="col">[% loc('Fixed') %]</th>
+ <th scope="col">[% loc('Total') %]</th>
+ [% ELSE %]
+ [% FOR k2 IN columns.sort %]
+ <th scope="col">[% k2 or loc('Website') %]</td>
+ [% END %]
+ <th scope="col">[% loc('Total') %]</th>
+ [% END %]
</tr>
+ [% FOR k IN rows %]
<tr>
- <td width="34%"><ul>[% INCLUDE list, list = lists.1 %]</ul></td>
- <td width="33%"><ul>[% INCLUDE list, list = lists.2 %]</ul></td>
- <td width="33%"><ul>[% INCLUDE list, list = lists.3 %]</ul></td>
+ [% IF group_by == 'state' %]
+ <th scope="row">[% prettify_state(k) %]</th>
+ [% ELSE %]
+ <th scope="row">[% k %]</th>
+ [% END %]
+ [% IF group_by == 'category+state' %]
+ <td>[% grouped.$k.open OR 0 %]</td>
+ <td>[% grouped.$k.closed OR 0 %]</td>
+ <td>[% grouped.$k.fixed OR 0 %]</td>
+ <td>[% grouped.$k.total OR 0 %]</td>
+ [% ELSE %]
+ [% FOR k2 IN columns.sort %]
+ <td>[% grouped.$k.$k2 OR 0 %]</td>
+ [% END %]
+ <td>[% grouped.$k.total OR 0 %]</td>
+ [% END %]
+ </tr>
+ [% END %]
+ <tr class="subtotal">
+ <th scope="row">[% loc('Total') %]</th>
+ [% IF group_by == 'category+state' %]
+ <td>[% totals.open OR 0 %]</td>
+ <td>[% totals.closed OR 0 %]</td>
+ <td>[% totals.fixed OR 0 %]</td>
+ [% ELSE %]
+ [% FOR k2 IN columns.sort %]
+ <td>[% totals.$k2 OR 0 %]</td>
+ [% END %]
+ [% END %]
+ <td>[% totals.total OR 0 %]</td>
</tr>
</table>
-</form>
+<p>
+[% loc('Within the specified timeframe:') %]
+[%
+summary_open = summary_open | format_number;
+summary_closed = summary_closed | format_number;
+summary_fixed = summary_fixed | format_number;
+tprintf( loc('%s opened, %s closed, %s fixed'),
+ decode(summary_open), decode(summary_closed), decode(summary_fixed) ) %]
+</p>
[% INCLUDE 'footer.html' %]
-
-[% BLOCK list %]
-[% FOR p IN list %]
-<li><a href="/report/[% p.id %]">[% p.title | html %]</a> <date>[% p.confirmed.dmy('/') %]</date></li>
-[% END %]
-[% END %]
diff --git a/web/cobrands/fixmystreet/dashboard.scss b/web/cobrands/fixmystreet/dashboard.scss
index 3e0de0d2b..8d153d8e5 100644
--- a/web/cobrands/fixmystreet/dashboard.scss
+++ b/web/cobrands/fixmystreet/dashboard.scss
@@ -8,10 +8,6 @@
th[scope=col] {
font-size:0.8em;
}
-
- tr:nth-child(2) {
- background-color:#fee;
- }
}
th {
@@ -93,6 +89,7 @@
color:#737373;
font-size:1.25em;
margin-bottom:0.5em;
+ margin-top: 0;
}
h1 {
color:#333;