aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthew Somerville <matthew-github@dracos.co.uk>2017-10-20 17:02:09 +0100
committerMatthew Somerville <matthew-github@dracos.co.uk>2017-10-20 17:02:09 +0100
commit8e5853830f0fb65985881272b3b0178b37ac947b (patch)
tree3be466a2381a89c819002f91f350f341bdd14554
parent235502e48f2a94ac90c425a04cda09cb22ad78d2 (diff)
parent6e1d005093e6a97f2f8bd90def4aa794b2ca7cc3 (diff)
Merge branch 'reports-dashboard-councils'
-rw-r--r--CHANGELOG.md1
-rwxr-xr-xbin/update-all-reports32
-rwxr-xr-xperllib/FixMyStreet/App/Controller/About.pm1
-rw-r--r--perllib/FixMyStreet/App/Controller/Reports.pm110
-rw-r--r--perllib/FixMyStreet/Cobrand/FixMyStreet.pm98
-rw-r--r--perllib/FixMyStreet/DB/Result/Body.pm5
-rwxr-xr-xperllib/FixMyStreet/Script/UpdateAllReports.pm157
-rw-r--r--t/app/controller/reports.t19
-rw-r--r--t/cobrand/fixmystreet.t47
-rwxr-xr-xtemplates/web/base/reports/index.html39
-rw-r--r--templates/web/fixmystreet.com/about/council-dashboard.html60
-rw-r--r--web/cobrands/fixmystreet.com/fmsforcouncils.scss2
12 files changed, 456 insertions, 115 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2dad6f80..b4a57c9c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
- Allow multiple wards to be shown on reports page
- Don't cover whole map with pin loading indicator.
- Add Expand map toggle to more mobile maps.
+ - Add functionality to have per-body /reports page.
- Bugfixes
- Shortlist menu item always remains a link #1855
- Fix encoded entities in RSS output. #1859
diff --git a/bin/update-all-reports b/bin/update-all-reports
index 4c0e96d31..0f9231f87 100755
--- a/bin/update-all-reports
+++ b/bin/update-all-reports
@@ -16,19 +16,45 @@ BEGIN {
require "$d/../setenv.pl";
}
+use FixMyStreet::DB;
use FixMyStreet::Script::UpdateAllReports;
+use Path::Tiny;
use Getopt::Long::Descriptive;
+use JSON::MaybeXS;
my ($opt, $usage) = describe_options(
'%c %o',
[ 'table', "Output JSON for old table-style page." ],
- [ 'areas', "Include area IDs in output JSON." ],
+ [ 'body=i', "Restrict results to a particular body (dashboard-style)." ],
+ [ 'all-bodies', "Generate set of results for all bodies." ],
+ [ 'areas', "Include area IDs in output JSON (table-style)." ],
[ 'help', "print usage message and exit", { shortcircuit => 1 } ],
);
print($usage->text), exit if $opt->help;
+my ($data, $filename);
if ($opt->table) {
- FixMyStreet::Script::UpdateAllReports::generate($opt->areas);
+ $data = FixMyStreet::Script::UpdateAllReports::generate($opt->areas);
+ output('all-reports', $data);
+} elsif ($opt->all_bodies) {
+ my $bodies = FixMyStreet::DB->resultset("Body")->search({ deleted => 0 });
+ while (my $body = $bodies->next) {
+ my $data = FixMyStreet::Script::UpdateAllReports::generate_dashboard($body->id);
+ output("all-reports-dashboard-" . $body->id, $data);
+ }
+} elsif (my $body_id = $opt->body) {
+ my $body = FixMyStreet::DB->resultset("Body")->find({ id => $body_id });
+ die "Could not find body $body_id" unless $body;
+ $data = FixMyStreet::Script::UpdateAllReports::generate_dashboard($body);
+ output("all-reports-dashboard-$body_id", $data);
} else {
- FixMyStreet::Script::UpdateAllReports::generate_dashboard();
+ $data = FixMyStreet::Script::UpdateAllReports::generate_dashboard();
+ output("all-reports-dashboard", $data);
+}
+
+sub output {
+ my ($filename, $data) = @_;
+ my $json = encode_json($data);
+ path(FixMyStreet->path_to('../data/'))->mkpath;
+ path(FixMyStreet->path_to("../data/$filename.json"))->spew_utf8($json);
}
diff --git a/perllib/FixMyStreet/App/Controller/About.pm b/perllib/FixMyStreet/App/Controller/About.pm
index 233da25d3..48a5dfffd 100755
--- a/perllib/FixMyStreet/App/Controller/About.pm
+++ b/perllib/FixMyStreet/App/Controller/About.pm
@@ -23,6 +23,7 @@ sub page : Path("/about") : Args(1) {
my $template = $c->forward('find_template');
$c->detach('/page_error_404_not_found', []) unless $template;
$c->stash->{template} = $template;
+ $c->cobrand->call_hook('about_hook');
}
sub index : Path("/about") : Args(0) {
diff --git a/perllib/FixMyStreet/App/Controller/Reports.pm b/perllib/FixMyStreet/App/Controller/Reports.pm
index 8beb2c091..b6281f0ca 100644
--- a/perllib/FixMyStreet/App/Controller/Reports.pm
+++ b/perllib/FixMyStreet/App/Controller/Reports.pm
@@ -2,9 +2,9 @@ package FixMyStreet::App::Controller::Reports;
use Moose;
use namespace::autoclean;
-use File::Slurp;
use JSON::MaybeXS;
use List::MoreUtils qw(any);
+use Path::Tiny;
use POSIX qw(strcoll);
use RABX;
use mySociety::MaPit;
@@ -69,34 +69,27 @@ sub index : Path : Args(0) {
}
}
- # Fetch all bodies
- my @bodies = $c->model('DB::Body')->search({
- deleted => 0,
- }, {
- '+select' => [ { count => 'area_id' } ],
- '+as' => [ 'area_count' ],
- join => 'body_areas',
- distinct => 1,
- })->all;
- @bodies = sort { strcoll($a->name, $b->name) } @bodies;
- $c->stash->{bodies} = \@bodies;
- $c->stash->{any_empty_bodies} = any { $_->get_column('area_count') == 0 } @bodies;
-
my $dashboard = eval {
- my $data = File::Slurp::read_file(
- FixMyStreet->path_to( '../data/all-reports-dashboard.json' )->stringify
- );
- $c->stash(decode_json($data));
+ my $data = FixMyStreet->config('TEST_DASHBOARD_DATA');
+ # uncoverable branch true
+ unless ($data) {
+ my $fn = '../data/all-reports-dashboard';
+ if ($c->stash->{body}) {
+ $fn .= '-' . $c->stash->{body}->id;
+ }
+ $data = decode_json(path(FixMyStreet->path_to($fn . '.json'))->slurp_utf8);
+ }
+ $c->stash($data);
return 1;
};
- my $table = eval {
- my $data = File::Slurp::read_file(
- FixMyStreet->path_to( '../data/all-reports.json' )->stringify
- );
+ my $table = !$c->stash->{body} && eval {
+ my $data = path(FixMyStreet->path_to('../data/all-reports.json'))->slurp_utf8;
$c->stash(decode_json($data));
return 1;
};
if (!$dashboard && !$table) {
+ $c->detach('/page_error_404_not_found') if $c->stash->{body};
+
my $message = _("There was a problem showing the All Reports page. Please try again later.");
if ($c->config->{STAGING_SITE}) {
$message .= '</p><p>Perhaps the bin/update-all-reports script needs running. Use: bin/update-all-reports</p><p>'
@@ -105,6 +98,26 @@ sub index : Path : Args(0) {
$c->detach('/page_error_500_internal_error', [ $message ]);
}
+ if ($c->stash->{body}) {
+ my $children = $c->stash->{body}->first_area_children;
+ unless ($children->{error}) {
+ $c->stash->{children} = $children;
+ }
+ } else {
+ # Fetch all bodies
+ my @bodies = $c->model('DB::Body')->search({
+ deleted => 0,
+ }, {
+ '+select' => [ { count => 'area_id' } ],
+ '+as' => [ 'area_count' ],
+ join => 'body_areas',
+ distinct => 1,
+ })->all;
+ @bodies = sort { strcoll($a->name, $b->name) } @bodies;
+ $c->stash->{bodies} = \@bodies;
+ $c->stash->{any_empty_bodies} = any { $_->get_column('area_count') == 0 } @bodies;
+ }
+
# Down here so that error pages aren't cached.
$c->response->header('Cache-Control' => 'max-age=3600');
}
@@ -133,6 +146,20 @@ sub ward : Path : Args(2) {
my @wards = split /\|/, $ward || "";
$c->forward( 'body_check', [ $body ] );
+
+ my $body_short = $c->cobrand->short_name( $c->stash->{body} );
+ $c->stash->{body_url} = '/reports/' . $body_short;
+
+ if ($ward && $ward eq 'summary') {
+ if (my $actual_ward = $c->get_param('ward')) {
+ $ward = $c->cobrand->short_name({ name => $actual_ward });
+ $c->res->redirect($ward);
+ $c->detach;
+ }
+ $c->cobrand->call_hook('council_dashboard_hook');
+ $c->go('index');
+ }
+
$c->forward( 'ward_check', [ @wards ] )
if @wards;
$c->forward( 'check_canonical_url', [ $body ] );
@@ -143,13 +170,10 @@ sub ward : Path : Args(2) {
$c->detach('ajax', [ 'reports/_problem-list.html' ]);
}
- my $body_short = $c->cobrand->short_name( $c->stash->{body} );
$c->stash->{rss_url} = '/rss/reports/' . $body_short;
$c->stash->{rss_url} .= '/' . $c->cobrand->short_name( $c->stash->{ward} )
if $c->stash->{ward};
- $c->stash->{body_url} = '/reports/' . $body_short;
-
$c->stash->{stats} = $c->cobrand->get_report_stats();
my @categories = $c->stash->{body}->contacts->not_deleted->search( undef, {
@@ -178,9 +202,7 @@ sub ward : Path : Args(2) {
# List of wards
if ( !$c->stash->{wards} && $c->stash->{body}->id && $c->stash->{body}->body_areas->first ) {
- my $children = mySociety::MaPit::call('area/children', [ $c->stash->{body}->body_areas->first->area_id ],
- type => $c->cobrand->area_types_children,
- );
+ my $children = $c->stash->{body}->first_area_children;
unless ($children->{error}) {
foreach (values %$children) {
$_->{url} = $c->uri_for( $c->stash->{body_url}
@@ -310,17 +332,35 @@ sub body_check : Private {
# Oslo/ kommunes sharing a name in Norway
return if $c->cobrand->reports_body_check( $c, $q_body );
+ my $body = $c->forward('body_find', [ $q_body ]);
+ if ($body) {
+ $c->stash->{body} = $body;
+ return;
+ }
+
+ # No result, bad body name.
+ $c->detach( 'redirect_index' );
+}
+
+=head2
+
+Given a string, try and find a body starting with/matching that string.
+Returns the matching body object if found.
+
+=cut
+
+sub body_find : Private {
+ my ($self, $c, $q_body) = @_;
+
# We must now have a string to check
my @bodies = $c->model('DB::Body')->search( { name => { -like => "$q_body%" } } )->all;
if (@bodies == 1) {
- $c->stash->{body} = $bodies[0];
- return;
+ return $bodies[0];
} else {
foreach (@bodies) {
if (lc($_->name) eq lc($q_body) || $_->name =~ /^\Q$q_body\E (Borough|City|District|County) Council$/i) {
- $c->stash->{body} = $_;
- return;
+ return $_;
}
}
}
@@ -333,13 +373,9 @@ sub body_check : Private {
if (@translations == 1) {
if ( my $body = $c->model('DB::Body')->find( { id => $translations[0]->object_id } ) ) {
- $c->stash->{body} = $body;
- return;
+ return $body;
}
}
-
- # No result, bad body name.
- $c->detach( 'redirect_index' );
}
=head2 ward_check
diff --git a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
index c50721334..a50a22ff9 100644
--- a/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
+++ b/perllib/FixMyStreet/Cobrand/FixMyStreet.pm
@@ -1,6 +1,9 @@
package FixMyStreet::Cobrand::FixMyStreet;
use base 'FixMyStreet::Cobrand::UK';
+use strict;
+use warnings;
+
use mySociety::Random;
use constant COUNCIL_ID_BROMLEY => 2482;
@@ -62,5 +65,98 @@ sub extra_contact_validation {
return %errors;
}
-1;
+=head2 council_dashboard_hook
+
+This is for council-specific dashboard pages, which can only be seen by
+superusers and logged-in users with an email domain matching a body name.
+
+=cut
+
+sub council_dashboard_hook {
+ my $self = shift;
+ my $c = $self->{c};
+
+ unless ( $c->user_exists ) {
+ $c->res->redirect('/about/council-dashboard');
+ $c->detach;
+ }
+
+ return if $c->user->is_superuser;
+
+ my $body = $c->user->from_body || _user_to_body($c);
+ if ($body) {
+ # Matching URL and user's email body
+ return if $body->id eq $c->stash->{body}->id;
+
+ # Matched /a/ body, redirect to its summary page
+ $c->stash->{body} = $body;
+ $c->stash->{wards} = [ { name => 'summary' } ];
+ $c->detach('/reports/redirect_body');
+ }
+
+ $c->res->redirect('/about/council-dashboard');
+}
+sub _user_to_body {
+ my $c = shift;
+ my $email = lc $c->user->email;
+ return _email_to_body($c, $email);
+}
+
+sub _email_to_body {
+ my ($c, $email) = @_;
+ my ($domain) = $email =~ m{ @ (.*) \z }x;
+
+ my @data = eval { FixMyStreet->path_to('../data/fixmystreet-councils.csv')->slurp };
+ my $body;
+ foreach (@data) {
+ chomp;
+ my ($d, $b) = split /\|/;
+ if ($d eq $domain) {
+ $body = $b;
+ last;
+ }
+ }
+ # If we didn't find a lookup entry, default to the first part of the domain
+ unless ($body) {
+ $domain =~ s/\.gov\.uk$//;
+ $body = ucfirst $domain;
+ }
+
+ $body = $c->forward('/reports/body_find', [ $body ]);
+ return $body;
+}
+
+sub about_hook {
+ my $self = shift;
+ my $c = $self->{c};
+
+ if ($c->stash->{template} eq 'about/council-dashboard.html') {
+ $c->stash->{form_name} = $c->get_param('name') || '';
+ $c->stash->{email} = $c->get_param('username') || '';
+ if ($c->user_exists) {
+ my $body = _user_to_body($c);
+ if ($body) {
+ $c->stash->{body} = $body;
+ $c->stash->{wards} = [ { name => 'summary' } ];
+ $c->detach('/reports/redirect_body');
+ }
+ }
+ if (my $email = $c->get_param('username')) {
+ $email = lc $email;
+ $email =~ s/\s+//g;
+ my $body = _email_to_body($c, $email);
+ if ($body) {
+ # Send confirmation email (hopefully)
+ $c->stash->{template} = 'auth/general.html';
+ $c->detach('/auth/general');
+ } else {
+ $c->stash->{no_body_found} = 1;
+ $c->set_param('em', $email); # What the contact form wants
+ $c->detach('/contact/submit');
+ }
+ }
+ }
+}
+
+1;
diff --git a/perllib/FixMyStreet/DB/Result/Body.pm b/perllib/FixMyStreet/DB/Result/Body.pm
index 6481d5cfc..da5c38168 100644
--- a/perllib/FixMyStreet/DB/Result/Body.pm
+++ b/perllib/FixMyStreet/DB/Result/Body.pm
@@ -156,12 +156,13 @@ sub areas {
}
sub first_area_children {
- my ( $self, $c ) = @_;
+ my ( $self ) = @_;
my $area_id = $self->body_areas->first->area_id;
+ my $cobrand = $self->result_source->schema->cobrand;
my $children = mySociety::MaPit::call('area/children', $area_id,
- type => $c->cobrand->area_types_children,
+ type => $cobrand->area_types_children,
);
return $children;
diff --git a/perllib/FixMyStreet/Script/UpdateAllReports.pm b/perllib/FixMyStreet/Script/UpdateAllReports.pm
index 1bd069ee8..f4f444d5b 100755
--- a/perllib/FixMyStreet/Script/UpdateAllReports.pm
+++ b/perllib/FixMyStreet/Script/UpdateAllReports.pm
@@ -4,11 +4,9 @@ use strict;
use warnings;
use FixMyStreet;
+use FixMyStreet::Cobrand;
use FixMyStreet::DB;
-use File::Path ();
-use File::Slurp;
-use JSON::MaybeXS;
use List::MoreUtils qw(zip);
use List::Util qw(sum);
@@ -21,6 +19,11 @@ if ( FixMyStreet->config('BASE_URL') =~ /zurich|zueri/ ) {
$age_column = 'created';
}
+my $dtf = FixMyStreet::DB->schema->storage->datetime_parser;
+
+my $cobrand = FixMyStreet::Cobrand->get_class_for_moniker('default')->new;
+FixMyStreet::DB->schema->cobrand($cobrand);
+
sub generate {
my $include_areas = shift;
@@ -81,13 +84,10 @@ sub generate {
}
}
- my $body = encode_json( {
+ return {
fixed => \%fixed,
open => \%open,
- } );
-
- File::Path::mkpath( FixMyStreet->path_to( '../data/' )->stringify );
- File::Slurp::write_file( FixMyStreet->path_to( '../data/all-reports.json' )->stringify, \$body );
+ };
}
sub end_period {
@@ -107,10 +107,18 @@ sub loop_period {
}
sub generate_dashboard {
+ my $body = shift;
+
my %data;
+ my $rs = FixMyStreet::DB->resultset('Problem');
+ $rs = $rs->to_body($body) if $body;
+
+ my $rs_c = FixMyStreet::DB->resultset('Comment');
+ $rs_c = $rs_c->to_body($body) if $body;
+
my $end_today = end_period('day');
- my $min_confirmed = FixMyStreet::DB->resultset('Problem')->search({
+ my $min_confirmed = $rs->search({
state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
}, {
select => [ { min => 'confirmed' } ],
@@ -134,11 +142,11 @@ sub generate_dashboard {
my @problem_periods = loop_period($min_confirmed, $group_by, $extra);
my %problems_reported_by_period = stuff_by_day_or_year(
- $group_by, 'Problem',
+ $group_by, $rs,
state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
);
my %problems_fixed_by_period = stuff_by_day_or_year(
- $group_by, 'Problem',
+ $group_by, $rs,
state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
);
@@ -158,24 +166,23 @@ sub generate_dashboard {
);
$data{last_seven_days} = \%last_seven_days;
- my $dtf = FixMyStreet::DB->schema->storage->datetime_parser;
my $eight_ago = $dtf->format_datetime(DateTime->now->subtract(days => 8));
%problems_reported_by_period = stuff_by_day_or_year('day',
- 'Problem',
+ $rs,
state => [ FixMyStreet::DB::Result::Problem->visible_states() ],
- confirmed => { '>=', $eight_ago },
+ 'me.confirmed' => { '>=', $eight_ago },
);
%problems_fixed_by_period = stuff_by_day_or_year('day',
- 'Comment',
- confirmed => { '>=', $eight_ago },
+ $rs_c,
+ 'me.confirmed' => { '>=', $eight_ago },
-or => [
problem_state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
mark_fixed => 1,
],
);
my %problems_updated_by_period = stuff_by_day_or_year('day',
- 'Comment',
- confirmed => { '>=', $eight_ago },
+ $rs_c,
+ 'me.confirmed' => { '>=', $eight_ago },
);
my $date = DateTime->today->subtract(days => 7);
@@ -189,47 +196,17 @@ sub generate_dashboard {
$last_seven_days{fixed_total} = sum @{$last_seven_days{fixed}};
$last_seven_days{updated_total} = sum @{$last_seven_days{updated}};
- my(@top_five_bodies);
- $data{top_five_bodies} = \@top_five_bodies;
-
- my $bodies = FixMyStreet::DB->resultset('Body')->search;
- my $substmt = "select min(id) from comment where me.problem_id=comment.problem_id and (problem_state in ('fixed', 'fixed - council', 'fixed - user') or mark_fixed)";
- while (my $body = $bodies->next) {
- my $subquery = FixMyStreet::DB->resultset('Comment')->to_body($body)->search({
- -or => [
- problem_state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
- mark_fixed => 1,
- ],
- 'me.id' => \"= ($substmt)",
- 'me.state' => 'confirmed',
- }, {
- select => [
- { extract => "epoch from me.confirmed-problem.confirmed", -as => 'time' },
- ],
- as => [ qw/time/ ],
- rows => 100,
- order_by => { -desc => 'me.confirmed' },
- join => 'problem'
- })->as_subselect_rs;
- my $avg = $subquery->search({
- }, {
- select => [ { avg => "time" } ],
- as => [ qw/avg/ ],
- })->first->get_column('avg');
- push @top_five_bodies, { name => $body->name, days => int($avg / 60 / 60 / 24 + 0.5) }
- if defined $avg;
+ if ($body) {
+ calculate_top_five_wards(\%data, $rs, $body);
+ } else {
+ calculate_top_five_bodies(\%data, $rs_c);
}
- @top_five_bodies = sort { $a->{days} <=> $b->{days} } @top_five_bodies;
- $data{average} = @top_five_bodies
- ? int((sum map { $_->{days} } @top_five_bodies) / @top_five_bodies + 0.5) : undef;
-
- @top_five_bodies = @top_five_bodies[0..4] if @top_five_bodies > 5;
my $week_ago = $dtf->format_datetime(DateTime->now->subtract(days => 7));
- my $last_seven_days = FixMyStreet::DB->resultset("Problem")->search({
+ my $last_seven_days = $rs->search({
confirmed => { '>=', $week_ago },
})->count;
- my @top_five_categories = FixMyStreet::DB->resultset("Problem")->search({
+ my @top_five_categories = $rs->search({
confirmed => { '>=', $week_ago },
category => { '!=', 'Other' },
}, {
@@ -247,19 +224,17 @@ sub generate_dashboard {
}
$data{other_categories} = $last_seven_days;
- my $body = encode_json( \%data );
- File::Path::mkpath( FixMyStreet->path_to( '../data/' )->stringify );
- File::Slurp::write_file( FixMyStreet->path_to( '../data/all-reports-dashboard.json' )->stringify, \$body );
+ return \%data;
}
sub stuff_by_day_or_year {
my $period = shift;
- my $table = shift;
+ my $rs = shift;
my %params = @_;
- my $results = FixMyStreet::DB->resultset($table)->search({
+ my $results = $rs->search({
%params
}, {
- select => [ { extract => \"$period from confirmed", -as => $period }, { count => 'id' } ],
+ select => [ { extract => \"$period from me.confirmed", -as => $period }, { count => 'me.id' } ],
as => [ $period, 'count' ],
group_by => [ $period ],
});
@@ -271,4 +246,66 @@ sub stuff_by_day_or_year {
return %out;
}
+sub calculate_top_five_bodies {
+ my ($data, $rs_c) = @_;
+
+ my(@top_five_bodies);
+
+ my $bodies = FixMyStreet::DB->resultset('Body')->search;
+ my $substmt = "select min(id) from comment where me.problem_id=comment.problem_id and (problem_state in ('fixed', 'fixed - council', 'fixed - user') or mark_fixed)";
+ while (my $body = $bodies->next) {
+ my $subquery = $rs_c->to_body($body)->search({
+ -or => [
+ problem_state => [ FixMyStreet::DB::Result::Problem->fixed_states() ],
+ mark_fixed => 1,
+ ],
+ 'me.id' => \"= ($substmt)",
+ 'me.state' => 'confirmed',
+ }, {
+ select => [
+ { extract => "epoch from me.confirmed-problem.confirmed", -as => 'time' },
+ ],
+ as => [ qw/time/ ],
+ rows => 100,
+ order_by => { -desc => 'me.confirmed' },
+ join => 'problem'
+ })->as_subselect_rs;
+ my $avg = $subquery->search({
+ }, {
+ select => [ { avg => "time" } ],
+ as => [ qw/avg/ ],
+ })->first->get_column('avg');
+ push @top_five_bodies, { name => $body->name, days => int($avg / 60 / 60 / 24 + 0.5) }
+ if defined $avg;
+ }
+ @top_five_bodies = sort { $a->{days} <=> $b->{days} } @top_five_bodies;
+ $data->{average} = @top_five_bodies
+ ? int((sum map { $_->{days} } @top_five_bodies) / @top_five_bodies + 0.5) : undef;
+
+ @top_five_bodies = @top_five_bodies[0..4] if @top_five_bodies > 5;
+ $data->{top_five_bodies} = \@top_five_bodies;
+}
+
+sub calculate_top_five_wards {
+ my ($data, $rs, $body) = @_;
+
+ my $children = $body->first_area_children;
+ die $children->{error} if $children->{error};
+
+ my $week_ago = $dtf->format_datetime(DateTime->now->subtract(days => 7));
+ my $last_seven_days = $rs->search({ confirmed => { '>=', $week_ago } });
+ my $last_seven_days_count = $last_seven_days->count;
+ $last_seven_days = $last_seven_days->search(undef, { select => 'areas' });
+
+ while (my $row = $last_seven_days->next) {
+ $children->{$_}{reports}++ foreach grep { $children->{$_} } split /,/, $row->areas;
+ }
+ my @wards = sort { $b->{reports} <=> $a->{reports} } grep { $_->{reports} } values %$children;
+ @wards = @wards[0..4] if @wards > 5;
+
+ my $sum_five = (sum map { $_->{reports} } @wards) || 0;
+ $data->{other_wards} = $last_seven_days_count - $sum_five;
+ $data->{wards} = \@wards;
+}
+
1;
diff --git a/t/app/controller/reports.t b/t/app/controller/reports.t
index 7773223dd..76c920562 100644
--- a/t/app/controller/reports.t
+++ b/t/app/controller/reports.t
@@ -12,9 +12,6 @@ END {
ok( my $mech = FixMyStreet::TestMech->new, 'Created mech object' );
-# Run the cron script with empty database
-FixMyStreet::Script::UpdateAllReports::generate_dashboard();
-
$mech->create_body_ok(2514, 'Birmingham City Council');
my $body_edin_id = $mech->create_body_ok(2651, 'City of Edinburgh Council')->id;
my $body_west_id = $mech->create_body_ok(2504, 'Westminster City Council')->id;
@@ -99,10 +96,14 @@ $fife_problems[10]->update( {
});
# Run the cron script that makes the data for /reports so we don't get an error.
-FixMyStreet::Script::UpdateAllReports::generate_dashboard();
+my $data = FixMyStreet::Script::UpdateAllReports::generate_dashboard();
# check that we can get the page
-$mech->get_ok('/reports');
+FixMyStreet::override_config {
+ TEST_DASHBOARD_DATA => $data,
+}, sub {
+ $mech->get_ok('/reports');
+};
$mech->title_like(qr{Dashboard});
$mech->content_contains('Birmingham');
@@ -138,6 +139,7 @@ is scalar @$problems, 5, 'correct number of problems displayed';
FixMyStreet::override_config {
MAPIT_URL => 'http://mapit.uk/',
+ TEST_DASHBOARD_DATA => $data,
}, sub {
$mech->get_ok('/reports');
$mech->submit_form_ok({ with_fields => { body => $body_slash_id } }, 'Submitted dropdown okay');
@@ -199,13 +201,18 @@ is scalar @$problems, 4, 'only public problems are displayed';
$mech->content_lacks('All reports Test 3 for ' . $body_west_id, 'non public problem is not visible');
# No change to numbers if report is non-public
-$mech->get_ok('/reports');
+FixMyStreet::override_config {
+ TEST_DASHBOARD_DATA => $data,
+}, sub {
+ $mech->get_ok('/reports');
+};
$mech->content_contains('&quot;Apr&quot;,&quot;May&quot;,&quot;Jun&quot;,&quot;Jul&quot;');
$mech->content_contains('5,9,10,22');
subtest "test fiksgatami all reports page" => sub {
FixMyStreet::override_config {
ALLOWED_COBRANDS => [ 'fiksgatami' ],
+ TEST_DASHBOARD_DATA => $data, # Not relevant to what we're testing, just so page loads
}, sub {
$mech->create_body_ok(3, 'Oslo');
ok $mech->host("fiksgatami.no"), 'change host to fiksgatami';
diff --git a/t/cobrand/fixmystreet.t b/t/cobrand/fixmystreet.t
new file mode 100644
index 000000000..eda93e187
--- /dev/null
+++ b/t/cobrand/fixmystreet.t
@@ -0,0 +1,47 @@
+use FixMyStreet::Script::UpdateAllReports;
+
+use FixMyStreet::TestMech;
+my $mech = FixMyStreet::TestMech->new;
+
+my $body = $mech->create_body_ok( 2514, 'Birmingham' );
+
+my $data;
+FixMyStreet::override_config {
+ MAPIT_URL => 'http://mapit.uk/',
+}, sub {
+ $data = FixMyStreet::Script::UpdateAllReports::generate_dashboard($body);
+};
+
+FixMyStreet::override_config {
+ MAPIT_URL => 'http://mapit.uk/',
+ TEST_DASHBOARD_DATA => $data,
+ ALLOWED_COBRANDS => 'fixmystreet',
+}, sub {
+ # Not logged in, redirected
+ $mech->get_ok('/reports/Birmingham/summary');
+ is $mech->uri->path, '/about/council-dashboard';
+
+ $mech->submit_form_ok({ with_fields => { username => 'someone@somewhere.example.org' }});
+ $mech->content_contains('We will be in touch');
+ # XXX Check email arrives
+
+ $mech->log_in_ok('someone@somewhere.example.org');
+ $mech->get_ok('/reports/Birmingham/summary');
+ is $mech->uri->path, '/about/council-dashboard';
+ $mech->content_contains('Ending in .gov.uk');
+
+ $mech->submit_form_ok({ with_fields => { name => 'Someone', username => 'someone@birmingham.gov.uk' }});
+ $mech->content_contains('Now check your email');
+ # XXX Check email arrives, click link
+
+ $mech->log_in_ok('someone@birmingham.gov.uk');
+ # Logged in, redirects
+ $mech->get_ok('/about/council-dashboard');
+ is $mech->uri->path, '/reports/Birmingham/summary';
+ $mech->content_contains('Top 5 wards');
+
+};
+
+END {
+ done_testing();
+}
diff --git a/templates/web/base/reports/index.html b/templates/web/base/reports/index.html
index a653a2686..70f4b3929 100755
--- a/templates/web/base/reports/index.html
+++ b/templates/web/base/reports/index.html
@@ -15,7 +15,9 @@
[% INCLUDE 'header.html', title = loc('Dashboard'), bodyclass => 'dashboard fullwidthpage' %]
<div class="dashboard-header">
- <h1>[% loc('Dashboard') %]</h1>
+ <h1>[% loc('Dashboard') %]
+ [% IF body %] – [% body.name %] [% END %]
+ </h1>
</div>
<div class="dashboard-row">
@@ -58,19 +60,31 @@
</div>
</div>
<div class="dashboard-item dashboard-item--6">
- <form class="dashboard-search" action="/reports">
+ <form class="dashboard-search">
<h2>[% loc('Show reports in your area') %]</h2>
+ [% IF body %]
+ <label for="ward">[% loc('Pick your ward') %]</label>
+ <div class="dashboard-search__input">
+ <select id="ward" name="ward" class="js-autocomplete">
+ <option value="">[% loc('Pick your ward') %]</option>
+ [% FOR child IN children.values.sort('name') %]
+ <option>[% child.name | html ~%]</option>
+ [% END %]
+ </select>
+ </div>
+ [% ELSE %]
<label for="body">[% loc('Pick your council') %]</label>
<div class="dashboard-search__input">
<select id="body" name="body" class="js-autocomplete">
<option value="">[% loc('Pick your council') %]</option>
- [% FOR body IN bodies %]
- <option value="[% body.id %]">[% body.name | html ~%]
- [% IF NOT body.get_column("area_count") %] [% loc('(no longer exists)') %][% END ~%]
+ [% FOR b IN bodies # Not body as 'body' may be on stash %]
+ <option value="[% b.id %]">[% b.name | html ~%]
+ [% IF NOT b.get_column("area_count") %] [% loc('(no longer exists)') %][% END ~%]
</option>
[% END %]
</select>
</div>
+ [% END %]
<div class="dashboard-search__submit">
<input type="submit" value="[% loc('Go') %]">
</div>
@@ -80,6 +94,20 @@
<div class="dashboard-row">
<div class="dashboard-item dashboard-item--6">
+ [% IF body %]
+ <h2 class="dashboard-subheading">[% loc('Top 5 wards') %]</h2>
+ <p>[% loc('Number of problems reported in each ward, in the last 7 days.') %]</p>
+ <table class="dashboard-ranking-table">
+ <tbody>
+ [% FOR line IN wards %]
+ <tr><td>[% line.name %]</td><td>[% tprintf(nget("%s report", "%s reports", line.reports), line.reports) %]</td></tr>
+ [% END %]
+ </tbody>
+ <tfoot>
+ <tr><td>[% loc('Other wards') %]</td><td>[% tprintf(nget("%s report", "%s reports", other_wards), other_wards) %]</td></tr>
+ </tfoot>
+ </table>
+ [% ELSE %]
<h2 class="dashboard-subheading">[% loc('Top 5 responsive councils') %]</h2>
<p>[% loc('Average time between a problem being reported and being fixed, last 100 reports.') %]</p>
<table class="dashboard-ranking-table">
@@ -92,6 +120,7 @@
<tr><td>[% loc('Overall average') %]</td><td>[% tprintf(nget("%s day", "%s days", average), average) %]</td></tr>
</tfoot>
</table>
+ [% END %]
</div>
<div class="dashboard-item dashboard-item--6">
<h2 class="dashboard-subheading">[% loc('Top 5 most used categories') %]</h2>
diff --git a/templates/web/fixmystreet.com/about/council-dashboard.html b/templates/web/fixmystreet.com/about/council-dashboard.html
new file mode 100644
index 000000000..7acaee207
--- /dev/null
+++ b/templates/web/fixmystreet.com/about/council-dashboard.html
@@ -0,0 +1,60 @@
+[% extra_css = BLOCK %]
+ <link rel="stylesheet" href="[% version('/cobrands/fixmystreet.com/fmsforcouncils.css') %]">
+ <link href="https://fonts.googleapis.com/css?family=Rubik:400,500" rel="stylesheet">
+[% END %]
+
+[% IF no_body_found %]
+
+[% INCLUDE header.html
+ title = 'FixMyStreet Professional', bodyclass = 'fullwidthpage'
+%]
+
+<div class="confirmation-header confirmation-header--inbox">
+ <h1>Thanks!</h1>
+ <p>We will be in touch with a confirmation link soon.</p>
+</div>
+
+[% ELSE %]
+
+[% INCLUDE header.html
+ title = 'FixMyStreet Professional', bodyclass = 'fms-for-councils fullwidthpage'
+%]
+
+<div class="fixed-container">
+ <div class="council-header">
+ <h1 class="councils-logo">FixMyStreet Professional</h1>
+ </div>
+ <div class="councils-hero">
+ <div class="councils-hero__demo-access">
+ <p>To access a council-specific version of our main dashboard page,
+ please provide your name and email below and we'll send you a link.</p>
+ <form method="post" class="councils-hero__demo-access__form">
+ <div class="form-group">
+ <label for="demo-name">Name</label>
+ <span class="required">required</span>
+ <input type="text" name="name" id="demo-name" required value="[% form_name | html %]">
+ </div>
+ <div class="form-group">
+ <label for="demo-email">Contact email</label>
+ <span class="required">required</span>
+ <input type="email" name="username" id="demo-email" required value="[% email | html %]">
+ <p class="form-note">Ending in .gov.uk, or other official council domain</p>
+ </div>
+ <div class="form-group submit-group">
+ <input type="hidden" name="r" value="about/council-dashboard">
+ <input type="hidden" name="extra.referer" value="[% c.req.headers.referer | html %]">
+ <input type="hidden" name="subject" value="Council dashboard request">
+ <input type="hidden" name="message" value="Filled in the council dashboard form">
+ <input type="hidden" name="recipient" value="bettercities">
+ <input type="hidden" name="dest" value="from_council">
+ <input type="submit" value="Let me in" class="btn">
+ </div>
+ </form>
+ </div>
+ </div>
+
+</div>
+
+[% END %]
+
+[% INCLUDE footer.html %]
diff --git a/web/cobrands/fixmystreet.com/fmsforcouncils.scss b/web/cobrands/fixmystreet.com/fmsforcouncils.scss
index ee166b8fd..0e7c6a281 100644
--- a/web/cobrands/fixmystreet.com/fmsforcouncils.scss
+++ b/web/cobrands/fixmystreet.com/fmsforcouncils.scss
@@ -147,7 +147,7 @@ $fms-pink: #E65376;
border-radius: 3px;
color: #fff;
padding: 2em;
- margin: 4em auto -4em auto;
+ margin: 4em auto 4em auto;
max-width: 26em;
position: relative;
z-index: 1;