aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Somerville <matthew-github@dracos.co.uk>2017-11-29 21:01:55 +0000
committerMatthew Somerville <matthew-github@dracos.co.uk>2017-11-29 21:01:55 +0000
commit725a1aa5874de9417d89998f59925e9b508826e4 (patch)
tree9f89a6a10b0f4d11876f8d1892aebd403eddef22
parent12653962d58df6be6ff1a753e3370ff3077030c1 (diff)
parentf6bbdb1400ca3961bb39b1b6891610b644a704fd (diff)
Merge branch '1919-consolidate-statistics'
-rw-r--r--CHANGELOG.md1
-rw-r--r--data/dashboard.json264
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin.pm153
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/AreaStats.pm225
-rw-r--r--perllib/FixMyStreet/App/Controller/Admin/Stats.pm75
-rw-r--r--perllib/FixMyStreet/App/Controller/Dashboard.pm381
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm1
-rw-r--r--t/app/controller/admin.t6
-rw-r--r--t/app/controller/area_stats.t234
-rw-r--r--t/app/controller/dashboard.t815
-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.html106
-rw-r--r--templates/web/base/admin/stats/fix_rate.html (renamed from templates/web/base/admin/stats_fix_rate.html)0
-rw-r--r--templates/web/base/admin/stats/index.html10
-rw-r--r--templates/web/base/admin/stats/questionnaire.html (renamed from templates/web/base/admin/questionnaire.html)0
-rw-r--r--templates/web/base/admin/stats/state.html (renamed from templates/web/base/admin/stats_by_state.html)0
-rw-r--r--templates/web/base/dashboard/index.html219
-rw-r--r--templates/web/zurich/admin/stats/index.html (renamed from templates/web/zurich/admin/stats.html)0
-rw-r--r--web/cobrands/fixmystreet/dashboard.scss5
20 files changed, 568 insertions, 2010 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a80b26720..67f2da48d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -50,6 +50,7 @@
- Council dashboard has date range for report generation #1885
- More JavaScript-enhanced `<select multiple>` elements #1589
- Council dashboard CSV export now has token based authentication #1911
+ - 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 27aeb9e5b..b485ea2dc 100644
--- a/perllib/FixMyStreet/App/Controller/Admin.pm
+++ b/perllib/FixMyStreet/App/Controller/Admin.pm
@@ -73,7 +73,7 @@ sub index : Path : Args(0) {
return $c->cobrand->admin();
}
- $c->forward('stats_by_state');
+ $c->forward('/admin/stats/state');
my @unsent = $c->cobrand->problems->search( {
state => [ FixMyStreet::DB::Result::Problem::open_states() ],
@@ -183,39 +183,6 @@ sub timeline : Path( 'timeline' ) : Args(0) {
return 1;
}
-sub questionnaire : Path('stats/questionnaire') : Args(0) {
- my ( $self, $c ) = @_;
-
- my $questionnaires = $c->model('DB::Questionnaire')->search(
- { whenanswered => { '!=', undef } },
- { group_by => [ 'ever_reported' ],
- select => [ 'ever_reported', { count => 'me.id' } ],
- as => [ qw/reported questionnaire_count/ ] }
- );
-
- my %questionnaire_counts = map {
- ( defined $_->get_column( 'reported' ) ? $_->get_column( 'reported' ) : -1 )
- => $_->get_column( 'questionnaire_count' )
- } $questionnaires->all;
- $questionnaire_counts{1} ||= 0;
- $questionnaire_counts{0} ||= 0;
- $questionnaire_counts{total} = $questionnaire_counts{0} + $questionnaire_counts{1};
- $c->stash->{questionnaires} = \%questionnaire_counts;
-
- $c->stash->{state_changes_count} = $c->model('DB::Questionnaire')->search(
- { whenanswered => \'is not null' }
- )->count;
- $c->stash->{state_changes} = $c->model('DB::Questionnaire')->search(
- { whenanswered => \'is not null' },
- {
- group_by => [ 'old_state', 'new_state' ],
- columns => [ 'old_state', 'new_state', { c => { count => 'id' } } ],
- },
- );
-
- return 1;
-}
-
sub bodies : Path('bodies') : Args(0) {
my ( $self, $c ) = @_;
@@ -504,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' ) {
@@ -1660,122 +1627,6 @@ sub flagged : Path('flagged') : Args(0) {
return 1;
}
-sub stats_by_state : Path('stats/state') : Args(0) {
- my ( $self, $c ) = @_;
-
- my $problems = $c->cobrand->problems->summary_count;
-
- my %prob_counts =
- map { $_->state => $_->get_column('state_count') } $problems->all;
-
- %prob_counts =
- map { $_ => $prob_counts{$_} || 0 }
- ( FixMyStreet::DB::Result::Problem->all_states() );
- $c->stash->{problems} = \%prob_counts;
- $c->stash->{total_problems_live} += $prob_counts{$_} ? $prob_counts{$_} : 0
- for ( FixMyStreet::DB::Result::Problem->visible_states() );
- $c->stash->{total_problems_users} = $c->cobrand->problems->unique_users;
-
- my $comments = $c->cobrand->updates->summary_count;
-
- my %comment_counts =
- map { $_->state => $_->get_column('state_count') } $comments->all;
-
- $c->stash->{comments} = \%comment_counts;
-}
-
-sub stats_fix_rate : Path('stats/fix-rate') : Args(0) {
- my ( $self, $c ) = @_;
-
- $c->stash->{categories} = $c->cobrand->problems->categories_summary();
-}
-
-sub stats : Path('stats') : Args(0) {
- my ( $self, $c ) = @_;
-
- my $selected_body;
- if ( $c->user->is_superuser ) {
- $c->forward('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;
-}
-
=head2 set_allowed_pages
Sets up the allowed_pages stash entry for checking if the current page is
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
new file mode 100644
index 000000000..2860b3531
--- /dev/null
+++ b/perllib/FixMyStreet/App/Controller/Admin/Stats.pm
@@ -0,0 +1,75 @@
+package FixMyStreet::App::Controller::Admin::Stats;
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller'; }
+
+sub index : Path : Args(0) {
+ my ( $self, $c ) = @_;
+ return $c->cobrand->admin_stats() if $c->cobrand->moniker eq 'zurich';
+}
+
+sub state : Local : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $problems = $c->cobrand->problems->summary_count;
+
+ my %prob_counts =
+ map { $_->state => $_->get_column('state_count') } $problems->all;
+
+ %prob_counts =
+ map { $_ => $prob_counts{$_} || 0 }
+ ( FixMyStreet::DB::Result::Problem->all_states() );
+ $c->stash->{problems} = \%prob_counts;
+ $c->stash->{total_problems_live} += $prob_counts{$_} ? $prob_counts{$_} : 0
+ for ( FixMyStreet::DB::Result::Problem->visible_states() );
+ $c->stash->{total_problems_users} = $c->cobrand->problems->unique_users;
+
+ my $comments = $c->cobrand->updates->summary_count;
+
+ my %comment_counts =
+ map { $_->state => $_->get_column('state_count') } $comments->all;
+
+ $c->stash->{comments} = \%comment_counts;
+}
+
+sub fix_rate : Path('fix-rate') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ $c->stash->{categories} = $c->cobrand->problems->categories_summary();
+}
+
+sub questionnaire : Local : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $questionnaires = $c->model('DB::Questionnaire')->search(
+ { whenanswered => { '!=', undef } },
+ { group_by => [ 'ever_reported' ],
+ select => [ 'ever_reported', { count => 'me.id' } ],
+ as => [ qw/reported questionnaire_count/ ] }
+ );
+
+ my %questionnaire_counts = map {
+ ( defined $_->get_column( 'reported' ) ? $_->get_column( 'reported' ) : -1 )
+ => $_->get_column( 'questionnaire_count' )
+ } $questionnaires->all;
+ $questionnaire_counts{1} ||= 0;
+ $questionnaire_counts{0} ||= 0;
+ $questionnaire_counts{total} = $questionnaire_counts{0} + $questionnaire_counts{1};
+ $c->stash->{questionnaires} = \%questionnaire_counts;
+
+ $c->stash->{state_changes_count} = $c->model('DB::Questionnaire')->search(
+ { whenanswered => \'is not null' }
+ )->count;
+ $c->stash->{state_changes} = $c->model('DB::Questionnaire')->search(
+ { whenanswered => \'is not null' },
+ {
+ group_by => [ 'old_state', 'new_state' ],
+ columns => [ 'old_state', 'new_state', { c => { count => 'id' } } ],
+ },
+ );
+
+ return 1;
+}
+
+1;
diff --git a/perllib/FixMyStreet/App/Controller/Dashboard.pm b/perllib/FixMyStreet/App/Controller/Dashboard.pm
index 264845d40..834d9c8d6 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,14 +56,20 @@ 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
@@ -95,127 +80,224 @@ sub index : Path : Args(0) {
$c->authenticate(undef, "access_token");
}
- 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 {
@@ -308,88 +390,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/admin.t b/t/app/controller/admin.t
index 3f69829f7..0be54dbc5 100644
--- a/t/app/controller/admin.t
+++ b/t/app/controller/admin.t
@@ -1746,4 +1746,10 @@ subtest "response priorities can't be viewed across councils" => sub {
};
};
+subtest "smoke view some stats pages" => sub {
+ $mech->log_in_ok( $superuser->email );
+ $mech->get_ok('/admin/stats/fix-rate');
+ $mech->get_ok('/admin/stats/questionnaire');
+};
+
done_testing();
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 83833ee7d..b53056968 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');
+set_absolute_time('2014-02-01T12:00:00');
-$mech->delete_user( $test_user );
-my $user = $mech->create_user_ok($test_user, password => $test_pass);
+my $mech = FixMyStreet::TestMech->new;
-my $p_user = $mech->create_user_ok('p_user@example.com');
+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");
+}
-# 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 {
+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');
- $mech->not_logged_in_ok;
- $mech->get_ok('/dashboard');
+my $body_id = $body->id;
+my $area_id = '60705';
+my $alt_area_id = '62883';
- $mech->content_contains( 'sign in' );
+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->submit_form(
- 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' });
- is $mech->status, '404', 'If not council user get 404';
+foreach my $problem (@scheduled_problems) {
+ $problem->update({ state => 'action scheduled' });
+ $mech->create_comment_for_problem($problem, $counciluser, 'Title', 'text', 0, 'confirmed', 'action scheduled');
+}
- $user->from_body( $body->id );
- $user->update;
+foreach my $problem (@fixed_problems) {
+ $problem->update({ state => 'fixed - council' });
+ $mech->create_comment_for_problem($problem, $counciluser, 'Title', 'text', 0, 'confirmed', 'fixed');
+}
- $mech->log_out_ok;
- $mech->get_ok('/dashboard');
- $mech->submit_form_ok( {
- with_fields => { username => $test_user, password_sign_in => $test_pass }
- } );
+foreach my $problem (@closed_problems) {
+ $problem->update({ state => 'closed' });
+ $mech->create_comment_for_problem($problem, $counciluser, 'Title', 'text', 0, 'confirmed', 'closed', { confirmed => \'current_timestamp' });
+}
- $mech->content_contains( 'Area 2651' );
+my $categories = scraper {
+ process "select[name=category] > option", 'cats[]' => 'TEXT',
+ process "table[id=overview] > tr", 'rows[]' => scraper {
+ process 'td', 'cols[]' => 'TEXT'
+ },
+};
- FixMyStreet::App->model('DB::Contact')->search( { body_id => $body->id } )
- ->delete;
+FixMyStreet::override_config {
+ ALLOWED_COBRANDS => [ { fixmystreet => '.' } ],
+ MAPIT_URL => 'http://mapit.uk/',
+}, sub {
- delete_problems();
+ subtest 'not logged in, redirected to login' => sub {
+ $mech->not_logged_in_ok;
+ $mech->get_ok('/dashboard');
+ $mech->content_contains( 'sign in' );
+ };
- 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',
- }
- );
- }
+ subtest 'normal user, 404' => sub {
+ $mech->log_in_ok( $normaluser->email );
+ $mech->get('/dashboard');
+ is $mech->status, '404', 'If not council user get 404';
+ };
- $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=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 '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:');
};
- my $expected_cats = [ 'All', '-- Pick a category --', @cats ];
- my $res = $categories->scrape( $mech->content );
- is_deeply( $res->{cats}, $expected_cats, 'correct list of categories' );
+ 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:');
+ };
- foreach my $row ( @{ $res->{rows} }[1 .. 11] ) {
- foreach my $col ( @{ $row->{cols} } ) {
- is $col, 0;
- }
- }
+ subtest 'area user can only see their area' => sub {
+ $counciluser->update({area_id => $area_id});
- for my $reports ( @{ $res->{report_lists} } ) {
- is_deeply $reports, {}, 'No reports';
- }
+ $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>');
- 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} );
- };
- }
+ $counciluser->update({area_id => undef});
+ };
- 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} );
- }
- };
- }
+ 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);
+ };
- 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 '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);
+ };
- delete_problems();
-
- 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};
-
- $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} );
- };
- }
+ 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,56 +179,16 @@ 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';
- };
-
- 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';
+ 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';
};
subtest 'export as csv using token' => sub {
$mech->log_out_ok;
- $user->set_extra_metadata('access_token', '1234567890abcdefgh');
- $user->update();
+ $counciluser->set_extra_metadata('access_token', '1234567890abcdefgh');
+ $counciluser->update();
$mech->get_ok('/dashboard?export=1');
like $mech->res->header('Content-type'), qr'text/html';
@@ -690,78 +200,19 @@ FixMyStreet::override_config {
$mech->content_contains('Report ID');
};
};
-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.html b/templates/web/base/admin/stats.html
deleted file mode 100644
index 150afd619..000000000
--- a/templates/web/base/admin/stats.html
+++ /dev/null
@@ -1,106 +0,0 @@
-[% 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="stats/questionnaire">[% loc('Survey Results') %]</a></li>
-<li><a href="stats/state">[% loc('Problem breakdown by state') %]</a></li>
-<li><a href="stats/fix-rate">[% loc('Category fix rate for problems > 4 weeks old') %]</a></li>
-</ul>
-
-[% END %]
-
-[% IF errors %]
- [% FOREACH error IN errors %]
- <p class="error">[% error %]</p>
- [% END %]
-[% END %]
-
-<form method="post" action="[% c.uri_for('stats') %]" 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/admin/stats_fix_rate.html b/templates/web/base/admin/stats/fix_rate.html
index fb88a1c06..fb88a1c06 100644
--- a/templates/web/base/admin/stats_fix_rate.html
+++ b/templates/web/base/admin/stats/fix_rate.html
diff --git a/templates/web/base/admin/stats/index.html b/templates/web/base/admin/stats/index.html
new file mode 100644
index 000000000..6ea1ae403
--- /dev/null
+++ b/templates/web/base/admin/stats/index.html
@@ -0,0 +1,10 @@
+[% INCLUDE 'admin/header.html' title=loc('Stats') %]
+
+<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>
+
+[% INCLUDE 'admin/footer.html' %]
diff --git a/templates/web/base/admin/questionnaire.html b/templates/web/base/admin/stats/questionnaire.html
index 680e0d214..680e0d214 100644
--- a/templates/web/base/admin/questionnaire.html
+++ b/templates/web/base/admin/stats/questionnaire.html
diff --git a/templates/web/base/admin/stats_by_state.html b/templates/web/base/admin/stats/state.html
index 6bcd35f88..6bcd35f88 100644
--- a/templates/web/base/admin/stats_by_state.html
+++ b/templates/web/base/admin/stats/state.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/templates/web/zurich/admin/stats.html b/templates/web/zurich/admin/stats/index.html
index ce8e238f7..ce8e238f7 100644
--- a/templates/web/zurich/admin/stats.html
+++ b/templates/web/zurich/admin/stats/index.html
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;